web-资源中心修改
This commit is contained in:
BIN
schoolNewsWeb/src/assets/imgs/article-default.png
Normal file
BIN
schoolNewsWeb/src/assets/imgs/article-default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
schoolNewsWeb/src/assets/imgs/resource-head-bg.jpg
Normal file
BIN
schoolNewsWeb/src/assets/imgs/resource-head-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 446 KiB |
@@ -1,3 +1,4 @@
|
|||||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M7.5 0C11.642 0 15 3.358 15 7.5C15 9.2 14.4 10.8 13.4 12L16.7 15.3C17.1 15.7 17.1 16.3 16.7 16.7C16.3 17.1 15.7 17.1 15.3 16.7L12 13.4C10.8 14.4 9.2 15 7.5 15C3.358 15 0 11.642 0 7.5C0 3.358 3.358 0 7.5 0ZM7.5 2C4.462 2 2 4.462 2 7.5C2 10.538 4.462 13 7.5 13C10.538 13 13 10.538 13 7.5C13 4.462 10.538 2 7.5 2Z" fill="#ffffff"/>
|
<path d="M14.6665 2.66699C21.2905 2.66699 26.6665 8.04299 26.6665 14.667C26.6665 21.291 21.2905 26.667 14.6665 26.667C8.0425 26.667 2.6665 21.291 2.6665 14.667C2.6665 8.04299 8.0425 2.66699 14.6665 2.66699ZM14.6665 24.0003C19.8232 24.0003 23.9998 19.8237 23.9998 14.667C23.9998 9.51032 19.8232 5.33366 14.6665 5.33366C9.50984 5.33366 5.33317 9.51032 5.33317 14.667C5.33317 19.8237 9.50984 24.0003 14.6665 24.0003ZM25.9802 24.0951L29.7514 27.8663L27.8658 29.7519L24.0946 25.9807L25.9802 24.0951Z" fill="white"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 618 B |
3
schoolNewsWeb/src/assets/imgs/star-icon.svg
Normal file
3
schoolNewsWeb/src/assets/imgs/star-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 31.6667L7.91667 37.5L10.2917 25.1667L1.58333 16.8333L14.0417 15.0833L19 3.75L23.9583 15.0833L36.4167 16.8333L27.7083 25.1667L30.0833 37.5L19 31.6667Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
@@ -8,7 +8,10 @@
|
|||||||
v-for="menu in menus"
|
v-for="menu in menus"
|
||||||
:key="menu.menuID || menu.url"
|
:key="menu.menuID || menu.url"
|
||||||
class="sidebar-item"
|
class="sidebar-item"
|
||||||
:class="{ active: isActive(menu) }"
|
:class="{
|
||||||
|
active: isActive(menu),
|
||||||
|
'theme-resource': props.theme === 'resource'
|
||||||
|
}"
|
||||||
@click="handleClick(menu)"
|
@click="handleClick(menu)"
|
||||||
>
|
>
|
||||||
<div class="sidebar-link">
|
<div class="sidebar-link">
|
||||||
@@ -28,9 +31,12 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
interface Props {
|
interface Props {
|
||||||
menus: SysMenu[];
|
menus: SysMenu[];
|
||||||
activePath?: string;
|
activePath?: string;
|
||||||
|
theme?: 'default' | 'resource'; // 新增主题属性
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {});
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
theme: 'default'
|
||||||
|
});
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -113,7 +119,7 @@ function handleClick(menu: SysMenu) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -142,6 +148,42 @@ function handleClick(menu: SysMenu) {
|
|||||||
color: #C62828;
|
color: #C62828;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resource theme - 红色背景高亮
|
||||||
|
&.theme-resource {
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(198, 40, 40, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #C62828;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 5px 10px;
|
||||||
|
height: 44px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none; // 隐藏左侧指示条
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: -103px;
|
||||||
|
top: 3px;
|
||||||
|
width: 48.53px;
|
||||||
|
height: 50.54px;
|
||||||
|
background: linear-gradient(134deg, rgba(255, 255, 255, 1) 26%, rgba(255, 255, 255, 0) 86%);
|
||||||
|
opacity: 0.23;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-link {
|
.sidebar-link {
|
||||||
@@ -156,12 +198,16 @@ function handleClick(menu: SysMenu) {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
transition: color 0.2s;
|
transition: color 0.3s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.link-text {
|
.link-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-resource & {
|
||||||
|
margin-left: 32px; // Resource theme 文字左移
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
|
|||||||
90
schoolNewsWeb/src/components/base/Search.vue
Normal file
90
schoolNewsWeb/src/components/base/Search.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-search">
|
||||||
|
<div class="search-box">
|
||||||
|
<input
|
||||||
|
v-model="searchText"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索思政资源"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<div class="search-button" @click="handleSearch">
|
||||||
|
<img src="@/assets/imgs/search-icon.svg" alt="search" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
search: [keyword: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const searchText = ref('');
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
emit('search', searchText.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.resource-search {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid rgba(186, 192, 204, 0.5);
|
||||||
|
border-radius: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 90px 0 20px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-size: 14px;
|
||||||
|
color: #141F38;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
width: 60px;
|
||||||
|
height: 100%;
|
||||||
|
background: #C62828;
|
||||||
|
border-radius: 0 30px 30px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #B71C1C;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -47,20 +47,7 @@
|
|||||||
<!-- 右侧用户区域 -->
|
<!-- 右侧用户区域 -->
|
||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="nav-search">
|
<Search @search="handleSearch" />
|
||||||
<div class="search-box">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="搜索思政资源"
|
|
||||||
class="search-input"
|
|
||||||
v-model="searchKeyword"
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
/>
|
|
||||||
<div class="search-icon">
|
|
||||||
<img src="../../assets/imgs/search-icon.svg" alt="搜索" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<UserDropdown :user="userInfo" @logout="handleLogout" />
|
<UserDropdown :user="userInfo" @logout="handleLogout" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +61,7 @@ import { useStore } from 'vuex';
|
|||||||
import type { SysMenu } from '@/types';
|
import type { SysMenu } from '@/types';
|
||||||
import { MenuType } from '@/types/enums';
|
import { MenuType } from '@/types/enums';
|
||||||
// @ts-ignore - Vue 3.5 组件导入兼容性
|
// @ts-ignore - Vue 3.5 组件导入兼容性
|
||||||
import UserDropdown from './UserDropdown.vue';
|
import {UserDropdown, Search} from '@/components/base';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -475,6 +462,34 @@ function handleLogout() {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0; /* 防止右侧区域被压缩 */
|
flex-shrink: 0; /* 防止右侧区域被压缩 */
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
// 添加搜索框样式
|
||||||
|
:deep(.resource-search) {
|
||||||
|
width: 221px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 70px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
width: 48px;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export { default as MenuItem } from './MenuItem.vue';
|
|||||||
export { default as MenuNav } from './MenuNav.vue';
|
export { default as MenuNav } from './MenuNav.vue';
|
||||||
export { default as TopNavigation } from './TopNavigation.vue';
|
export { default as TopNavigation } from './TopNavigation.vue';
|
||||||
export { default as UserDropdown } from './UserDropdown.vue';
|
export { default as UserDropdown } from './UserDropdown.vue';
|
||||||
|
export { default as Search } from './Search.vue';
|
||||||
@@ -10,6 +10,8 @@ import { BaseDTO } from '../base';
|
|||||||
* 资源实体
|
* 资源实体
|
||||||
*/
|
*/
|
||||||
export interface Resource extends BaseDTO {
|
export interface Resource extends BaseDTO {
|
||||||
|
/** 资源ID */
|
||||||
|
resourceID?: string;
|
||||||
/** 资源标题 */
|
/** 资源标题 */
|
||||||
title?: string;
|
title?: string;
|
||||||
/** 资源内容 */
|
/** 资源内容 */
|
||||||
|
|||||||
@@ -12,26 +12,21 @@
|
|||||||
<!-- 文章头部信息 -->
|
<!-- 文章头部信息 -->
|
||||||
<div class="article-header">
|
<div class="article-header">
|
||||||
<h1 class="article-title">{{ articleData.title }}</h1>
|
<h1 class="article-title">{{ articleData.title }}</h1>
|
||||||
<div class="article-meta">
|
<div class="article-meta-info">
|
||||||
<span v-if="articleData.category" class="meta-item">
|
<div class="meta-item" v-if="articleData.publishTime || articleData.createTime">
|
||||||
分类:{{ getCategoryLabel(articleData.category) }}
|
发布时间:{{ formatDateSimple(articleData.publishTime || articleData.createTime || '') }}
|
||||||
</span>
|
</div>
|
||||||
<span v-if="articleData.tags && articleData.tags.length" class="meta-item">
|
<div class="meta-item" v-if="articleData.viewCount !== undefined">
|
||||||
标签:{{ getTagsString(articleData.tags) }}
|
浏览次数:{{ articleData.viewCount }}
|
||||||
</span>
|
</div>
|
||||||
<span v-if="articleData.author" class="meta-item">
|
<div class="meta-item" v-if="articleData.source">
|
||||||
作者:{{ articleData.author }}
|
来源:{{ articleData.source }}
|
||||||
</span>
|
</div>
|
||||||
<span v-if="articleData.createTime" class="meta-item">
|
|
||||||
发布时间:{{ formatDate(articleData.createTime) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 封面图片 -->
|
<!-- 分隔线 -->
|
||||||
<div v-if="articleData.coverImage" class="article-cover">
|
<div class="separator"></div>
|
||||||
<img :src="FILE_DOWNLOAD_URL + articleData.coverImage" class="cover-image" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文章内容 -->
|
<!-- 文章内容 -->
|
||||||
<div class="article-content ql-editor" v-html="articleData.content"></div>
|
<div class="article-content ql-editor" v-html="articleData.content"></div>
|
||||||
@@ -48,26 +43,21 @@
|
|||||||
<!-- 文章头部信息 -->
|
<!-- 文章头部信息 -->
|
||||||
<div class="article-header">
|
<div class="article-header">
|
||||||
<h1 class="article-title">{{ articleData.title }}</h1>
|
<h1 class="article-title">{{ articleData.title }}</h1>
|
||||||
<div class="article-meta">
|
<div class="article-meta-info">
|
||||||
<span v-if="articleData.category" class="meta-item">
|
<div class="meta-item" v-if="articleData.publishTime || articleData.createTime">
|
||||||
分类:{{ getCategoryLabel(articleData.category) }}
|
发布时间:{{ formatDateSimple(articleData.publishTime || articleData.createTime || '') }}
|
||||||
</span>
|
</div>
|
||||||
<span v-if="articleData.tags && articleData.tags.length" class="meta-item">
|
<div class="meta-item" v-if="articleData.viewCount !== undefined">
|
||||||
标签:{{ getTagsString(articleData.tags) }}
|
浏览次数:{{ articleData.viewCount }}
|
||||||
</span>
|
</div>
|
||||||
<span v-if="articleData.author" class="meta-item">
|
<div class="meta-item" v-if="articleData.source">
|
||||||
作者:{{ articleData.author }}
|
来源:{{ articleData.source }}
|
||||||
</span>
|
</div>
|
||||||
<span v-if="articleData.createTime" class="meta-item">
|
|
||||||
发布时间:{{ formatDate(articleData.createTime) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 封面图片 -->
|
<!-- 分隔线 -->
|
||||||
<div v-if="articleData.coverImage" class="article-cover">
|
<div class="separator"></div>
|
||||||
<img :src="FILE_DOWNLOAD_URL + articleData.coverImage" class="cover-image" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文章内容 -->
|
<!-- 文章内容 -->
|
||||||
<div class="article-content ql-editor" v-html="articleData.content"></div>
|
<div class="article-content ql-editor" v-html="articleData.content"></div>
|
||||||
@@ -77,25 +67,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { ElDialog, ElButton } from 'element-plus';
|
import { ElDialog, ElButton } from 'element-plus';
|
||||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
import { ResourceCategory, Resource, Tag } from '@/types';
|
||||||
import { ResourceCategory } from '@/types/resource';
|
|
||||||
|
|
||||||
interface ArticleData {
|
|
||||||
title?: string;
|
|
||||||
content?: string;
|
|
||||||
coverImage?: string;
|
|
||||||
category?: string;
|
|
||||||
tags?: Array<{ name?: string }> | string[]; // 支持对象数组或字符串数组
|
|
||||||
author?: string;
|
|
||||||
createTime?: string | Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: boolean; // Dialog 模式下的显示状态
|
modelValue?: boolean; // Dialog 模式下的显示状态
|
||||||
asDialog?: boolean; // 是否作为 Dialog 使用
|
asDialog?: boolean; // 是否作为 Dialog 使用
|
||||||
title?: string; // Dialog 标题
|
title?: string; // Dialog 标题
|
||||||
width?: string; // Dialog 宽度
|
width?: string; // Dialog 宽度
|
||||||
articleData?: ArticleData; // 文章数据
|
articleData?: Resource; // 文章数据
|
||||||
categoryList?: Array<ResourceCategory>; // 分类列表
|
categoryList?: Array<ResourceCategory>; // 分类列表
|
||||||
showEditButton?: boolean; // 是否显示编辑按钮
|
showEditButton?: boolean; // 是否显示编辑按钮
|
||||||
}
|
}
|
||||||
@@ -122,43 +102,17 @@ const visible = computed({
|
|||||||
set: (val) => props.asDialog ? emit('update:modelValue', val) : undefined
|
set: (val) => props.asDialog ? emit('update:modelValue', val) : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取标签字符串
|
|
||||||
function getTagsString(tags: Array<{ name?: string }> | string[]): string {
|
|
||||||
if (!tags || tags.length === 0) return '';
|
|
||||||
|
|
||||||
if (typeof tags[0] === 'string') {
|
// 格式化日期(简单格式:YYYY-MM-DD)
|
||||||
return (tags as string[]).join(', ');
|
function formatDateSimple(date: string | Date): string {
|
||||||
} else {
|
|
||||||
return (tags as Array<{ name?: string }>)
|
|
||||||
.map(tag => tag.name || '')
|
|
||||||
.filter(name => name)
|
|
||||||
.join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取分类标签
|
|
||||||
function getCategoryLabel(categoryId: string): string {
|
|
||||||
if (!props.categoryList || !categoryId) return '';
|
|
||||||
|
|
||||||
const category = props.categoryList.find(cat =>
|
|
||||||
cat.id === categoryId || cat.categoryID === categoryId
|
|
||||||
);
|
|
||||||
|
|
||||||
return category?.name || categoryId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化日期
|
|
||||||
function formatDate(date: string | Date): string {
|
|
||||||
if (!date) return '';
|
if (!date) return '';
|
||||||
|
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
return d.toLocaleDateString('zh-CN', {
|
const year = d.getFullYear();
|
||||||
year: 'numeric',
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
month: '2-digit',
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
return `${year}-${month}-${day}`;
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭处理
|
// 关闭处理
|
||||||
@@ -189,45 +143,44 @@ defineExpose({
|
|||||||
.article-show-container {
|
.article-show-container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-header {
|
.article-header {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 36px;
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-title {
|
.article-title {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 600;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: bold;
|
line-height: 28px;
|
||||||
color: #303133;
|
color: #141F38;
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 30px 0;
|
||||||
line-height: 1.4;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-meta {
|
.article-meta-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 48px;
|
||||||
color: #909399;
|
margin-top: 30px;
|
||||||
font-size: 14px;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-item {
|
.meta-item {
|
||||||
display: flex;
|
font-family: 'PingFang SC';
|
||||||
align-items: center;
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 28px;
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
.separator {
|
||||||
content: '';
|
width: 100%;
|
||||||
width: 4px;
|
height: 1px;
|
||||||
height: 4px;
|
background: #E9E9E9;
|
||||||
background: #909399;
|
margin-bottom: 40px;
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-cover {
|
.article-cover {
|
||||||
@@ -243,9 +196,11 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.article-content {
|
.article-content {
|
||||||
line-height: 1.8;
|
font-family: 'PingFang SC';
|
||||||
color: #303133;
|
font-weight: 400;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: #334155;
|
||||||
|
|
||||||
// 继承富文本编辑器的样式
|
// 继承富文本编辑器的样式
|
||||||
:deep(img) {
|
:deep(img) {
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="party-history-learning">
|
|
||||||
<h2 class="page-title">党史学习</h2>
|
|
||||||
|
|
||||||
<!-- 分类标签 -->
|
|
||||||
<div class="category-tabs">
|
|
||||||
<div
|
|
||||||
class="category-tab"
|
|
||||||
v-for="category in categories"
|
|
||||||
:key="category.id"
|
|
||||||
:class="{ active: activeCategory === category.id }"
|
|
||||||
@click="activeCategory = category.id"
|
|
||||||
>
|
|
||||||
{{ category.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文章列表 -->
|
|
||||||
<div class="article-list">
|
|
||||||
<div class="article-item" v-for="article in articles" :key="article.id" @click="viewArticle(article)">
|
|
||||||
<div class="article-image">
|
|
||||||
<img :src="article.image" :alt="article.title" />
|
|
||||||
</div>
|
|
||||||
<div class="article-content">
|
|
||||||
<h3>{{ article.title }}</h3>
|
|
||||||
<p class="article-summary">{{ article.summary }}</p>
|
|
||||||
<div class="article-meta">
|
|
||||||
<span class="author">{{ article.author }}</span>
|
|
||||||
<span class="date">{{ article.publishDate }}</span>
|
|
||||||
<span class="views">{{ article.views }} 阅读</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const activeCategory = ref(1);
|
|
||||||
const articles = ref<any[]>([]);
|
|
||||||
|
|
||||||
const categories = [
|
|
||||||
{ id: 1, name: '新民主主义革命时期' },
|
|
||||||
{ id: 2, name: '社会主义革命和建设时期' },
|
|
||||||
{ id: 3, name: '改革开放和社会主义现代化建设新时期' },
|
|
||||||
{ id: 4, name: '新时代中国特色社会主义' }
|
|
||||||
];
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// TODO: 加载党史学习文章
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewArticle(article: any) {
|
|
||||||
router.push(`/resource/article/${article.id}`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.party-history-learning {
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-tab {
|
|
||||||
padding: 8px 20px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #ffe6e6;
|
|
||||||
color: #C62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: #C62828;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-image {
|
|
||||||
width: 240px;
|
|
||||||
height: 160px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-summary {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="policy-interpretation">
|
|
||||||
<h2 class="page-title">政策解读</h2>
|
|
||||||
<div class="interpretation-grid">
|
|
||||||
<div class="interpretation-item" v-for="item in interpretations" :key="item.id" @click="viewInterpretation(item)">
|
|
||||||
<div class="item-badge">{{ item.category }}</div>
|
|
||||||
<h3>{{ item.title }}</h3>
|
|
||||||
<p class="item-summary">{{ item.summary }}</p>
|
|
||||||
<div class="item-footer">
|
|
||||||
<span class="item-date">{{ item.publishDate }}</span>
|
|
||||||
<span class="item-views">{{ item.views }} 阅读</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const interpretations = ref<any[]>([]);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// TODO: 加载政策解读数据
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewInterpretation(item: any) {
|
|
||||||
router.push(`/resource/interpretation/${item.id}`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.policy-interpretation {
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interpretation-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interpretation-item {
|
|
||||||
padding: 24px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #C62828;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 12px;
|
|
||||||
background: #ffe6e6;
|
|
||||||
color: #C62828;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-summary {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="red-classic">
|
|
||||||
<h2 class="page-title">红色经典</h2>
|
|
||||||
<div class="classic-grid">
|
|
||||||
<div class="classic-item" v-for="item in classics" :key="item.id" @click="viewClassic(item)">
|
|
||||||
<div class="classic-cover">
|
|
||||||
<img :src="item.cover" :alt="item.title" />
|
|
||||||
<div class="classic-type">{{ item.type }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="classic-info">
|
|
||||||
<h3>{{ item.title }}</h3>
|
|
||||||
<p class="classic-author">{{ item.author }}</p>
|
|
||||||
<p class="classic-description">{{ item.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const classics = ref<any[]>([]);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// TODO: 加载红色经典数据
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewClassic(item: any) {
|
|
||||||
router.push(`/resource/classic/${item.id}`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.red-classic {
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-item {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-8px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-cover {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-type {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
background: rgba(198, 40, 40, 0.9);
|
|
||||||
color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-info {
|
|
||||||
margin-top: 12px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-author {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.5;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,104 +1,149 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="resource-center-page">
|
<div class="resource-center-view">
|
||||||
<!-- 导航栏 -->
|
<ResourceHead
|
||||||
<div class="resource-nav">
|
:category-name="currentCategoryName"
|
||||||
<div
|
:show-article-mode="showArticle"
|
||||||
class="nav-tab"
|
/>
|
||||||
v-for="tab in tabs"
|
<div class="search-wrapper">
|
||||||
:key="tab.key"
|
<Search @search="handleSearch" />
|
||||||
:class="{ active: activeTab === tab.key }"
|
|
||||||
@click="activeTab = tab.key"
|
|
||||||
>
|
|
||||||
{{ tab.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content-wrapper">
|
||||||
<!-- 内容区域 -->
|
<div class="content-container">
|
||||||
<div class="resource-content">
|
<ResourceSideBar
|
||||||
<component :is="currentComponent" />
|
:active-category-id="currentCategoryId"
|
||||||
|
@category-change="handleCategoryChange"
|
||||||
|
/>
|
||||||
|
<ResourceList
|
||||||
|
v-show="!showArticle"
|
||||||
|
ref="resourceListRef"
|
||||||
|
:category-id="currentCategoryId"
|
||||||
|
:search-keyword="searchKeyword"
|
||||||
|
@resource-click="handleResourceClick"
|
||||||
|
@list-updated="handleListUpdated"
|
||||||
|
/>
|
||||||
|
<ResourceArticle
|
||||||
|
v-show="showArticle"
|
||||||
|
:resource-id="currentResourceId"
|
||||||
|
:category-id="currentCategoryId"
|
||||||
|
:resource-list="resourceList"
|
||||||
|
@resource-change="handleResourceChange"
|
||||||
|
@navigate="handleArticleNavigate"
|
||||||
|
@back-to-list="backToList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MediaArchive from './components/MediaArchive.vue';
|
import { ResourceHead, ResourceSideBar, ResourceList, ResourceArticle } from './components';
|
||||||
import PartyHistoryLearning from './PartyHistoryLearningView.vue';
|
import { Search } from '@/components/base';
|
||||||
import PolicySpeech from './components/PolicySpeech.vue';
|
import type { Resource, ResourceCategory } from '@/types/resource';
|
||||||
import PolicyInterpretation from './PolicyInterpretationView.vue';
|
|
||||||
import RedClassic from './RedClassicView.vue';
|
|
||||||
import SpecialReport from './SpecialReportView.vue';
|
|
||||||
import WorldCase from './WorldCaseView.vue';
|
|
||||||
|
|
||||||
const activeTab = ref('media');
|
const showArticle = ref(false);
|
||||||
|
const currentCategoryId = ref('party_history');
|
||||||
|
const currentCategoryName = ref('党史学习');
|
||||||
|
const currentResourceId = ref('');
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const resourceListRef = ref();
|
||||||
|
const resourceList = ref<Resource[]>([]);
|
||||||
|
|
||||||
const tabs = [
|
function handleCategoryChange(category: ResourceCategory) {
|
||||||
{ key: 'media', label: '媒体档案' },
|
currentCategoryId.value = category.categoryID || '';
|
||||||
{ key: 'party-history', label: '党史学习' },
|
currentCategoryName.value = category.name || '';
|
||||||
{ key: 'policy-speech', label: '政策讲话' },
|
searchKeyword.value = '';
|
||||||
{ key: 'policy-interpretation', label: '政策解读' },
|
showArticle.value = false;
|
||||||
{ key: 'red-classic', label: '红色经典' },
|
}
|
||||||
{ key: 'special-report', label: '专题报告' },
|
|
||||||
{ key: 'world-case', label: '世界案例' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const componentMap: Record<string, any> = {
|
function handleSearch(keyword: string) {
|
||||||
'media': MediaArchive,
|
searchKeyword.value = keyword;
|
||||||
'party-history': PartyHistoryLearning,
|
showArticle.value = false;
|
||||||
'policy-speech': PolicySpeech,
|
}
|
||||||
'policy-interpretation': PolicyInterpretation,
|
|
||||||
'red-classic': RedClassic,
|
|
||||||
'special-report': SpecialReport,
|
|
||||||
'world-case': WorldCase
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentComponent = computed(() => componentMap[activeTab.value]);
|
function handleListUpdated(list: Resource[]) {
|
||||||
|
resourceList.value = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResourceClick(resource: Resource) {
|
||||||
|
currentResourceId.value = resource.resourceID || '';
|
||||||
|
showArticle.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResourceChange(resourceId: string) {
|
||||||
|
currentResourceId.value = resourceId;
|
||||||
|
// ArticleShowView 会自动重新加载
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToList() {
|
||||||
|
showArticle.value = false;
|
||||||
|
currentResourceId.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文章内前后切换时,靠近列表头部或尾部触发列表翻页
|
||||||
|
async function handleArticleNavigate(direction: 'prev' | 'next', resourceId: string) {
|
||||||
|
const list = resourceListRef.value?.getResources?.() || [];
|
||||||
|
const index = list.findIndex((r: any) => r.resourceID === resourceId);
|
||||||
|
if (index === -1) return;
|
||||||
|
const nearHead = index <= 2;
|
||||||
|
const nearTail = index >= list.length - 3;
|
||||||
|
if (nearHead && direction === 'prev') {
|
||||||
|
await resourceListRef.value?.loadPrevPage?.();
|
||||||
|
} else if (nearTail && direction === 'next') {
|
||||||
|
await resourceListRef.value?.loadNextPage?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped lang="scss">
|
||||||
.resource-center-page {
|
.resource-center-view {
|
||||||
min-height: 100vh;
|
background: #F9F9F9;
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-nav {
|
.search-wrapper {
|
||||||
background: white;
|
width: 100%;
|
||||||
padding: 0 40px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
justify-content: center;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
padding: 20px 0;
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab {
|
:deep(.resource-search) {
|
||||||
padding: 16px 24px;
|
width: 1200px;
|
||||||
cursor: pointer;
|
height: 60px;
|
||||||
font-size: 16px;
|
padding: 0;
|
||||||
color: #666;
|
|
||||||
position: relative;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
.search-box {
|
||||||
color: #C62828;
|
height: 60px;
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
input {
|
||||||
color: #C62828;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
padding: 0 100px 0 40px;
|
||||||
|
}
|
||||||
|
|
||||||
&::after {
|
.search-button {
|
||||||
content: '';
|
width: 72px;
|
||||||
position: absolute;
|
height: 60px;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
img {
|
||||||
right: 0;
|
width: 24px;
|
||||||
height: 3px;
|
height: 24px;
|
||||||
background: #C62828;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-content {
|
.content-wrapper {
|
||||||
padding: 20px;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
width: 1200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="special-report">
|
|
||||||
<h2 class="page-title">专题报告</h2>
|
|
||||||
<div class="report-list">
|
|
||||||
<div class="report-item" v-for="report in reports" :key="report.id" @click="viewReport(report)">
|
|
||||||
<div class="report-banner">
|
|
||||||
<img :src="report.banner" :alt="report.title" />
|
|
||||||
</div>
|
|
||||||
<div class="report-content">
|
|
||||||
<h3>{{ report.title }}</h3>
|
|
||||||
<div class="report-tags">
|
|
||||||
<span class="tag" v-for="tag in report.tags" :key="tag">{{ tag }}</span>
|
|
||||||
</div>
|
|
||||||
<p class="report-summary">{{ report.summary }}</p>
|
|
||||||
<div class="report-footer">
|
|
||||||
<span class="report-speaker">主讲人:{{ report.speaker }}</span>
|
|
||||||
<span class="report-date">{{ report.date }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const reports = ref<any[]>([]);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// TODO: 加载专题报告数据
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewReport(report: any) {
|
|
||||||
router.push(`/resource/report/${report.id}`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.special-report {
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #C62828;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-banner {
|
|
||||||
width: 320px;
|
|
||||||
height: 200px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-tags {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
padding: 4px 12px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-summary {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-top: 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="world-case">
|
|
||||||
<h2 class="page-title">世界案例</h2>
|
|
||||||
|
|
||||||
<!-- 地区筛选 -->
|
|
||||||
<div class="region-filter">
|
|
||||||
<div
|
|
||||||
class="region-tab"
|
|
||||||
v-for="region in regions"
|
|
||||||
:key="region.id"
|
|
||||||
:class="{ active: activeRegion === region.id }"
|
|
||||||
@click="activeRegion = region.id"
|
|
||||||
>
|
|
||||||
{{ region.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 案例列表 -->
|
|
||||||
<div class="case-grid">
|
|
||||||
<div class="case-item" v-for="caseItem in cases" :key="caseItem.id" @click="viewCase(caseItem)">
|
|
||||||
<div class="case-image">
|
|
||||||
<img :src="caseItem.image" :alt="caseItem.title" />
|
|
||||||
<div class="case-country">{{ caseItem.country }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="case-content">
|
|
||||||
<h3>{{ caseItem.title }}</h3>
|
|
||||||
<p class="case-summary">{{ caseItem.summary }}</p>
|
|
||||||
<div class="case-footer">
|
|
||||||
<span class="case-category">{{ caseItem.category }}</span>
|
|
||||||
<span class="case-date">{{ caseItem.publishDate }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const activeRegion = ref('all');
|
|
||||||
const cases = ref<any[]>([]);
|
|
||||||
|
|
||||||
const regions = [
|
|
||||||
{ id: 'all', name: '全部' },
|
|
||||||
{ id: 'asia', name: '亚洲' },
|
|
||||||
{ id: 'europe', name: '欧洲' },
|
|
||||||
{ id: 'americas', name: '美洲' },
|
|
||||||
{ id: 'africa', name: '非洲' },
|
|
||||||
{ id: 'oceania', name: '大洋洲' }
|
|
||||||
];
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// TODO: 加载世界案例数据
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewCase(caseItem: any) {
|
|
||||||
router.push(`/resource/case/${caseItem.id}`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.world-case {
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-filter {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-tab {
|
|
||||||
padding: 8px 20px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #ffe6e6;
|
|
||||||
color: #C62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: #C62828;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-item {
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #C62828;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-image {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-country {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 12px;
|
|
||||||
right: 12px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
background: rgba(198, 40, 40, 0.9);
|
|
||||||
color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-content {
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-summary {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-category {
|
|
||||||
color: #C62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-date {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="media-archive">
|
|
||||||
<h2 class="page-title">媒体档案</h2>
|
|
||||||
<div class="media-grid">
|
|
||||||
<div class="media-item" v-for="item in mediaList" :key="item.id" @click="viewMedia(item)">
|
|
||||||
<div class="media-thumbnail">
|
|
||||||
<img :src="item.thumbnail" :alt="item.title" />
|
|
||||||
<div class="media-type">{{ item.type }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="media-info">
|
|
||||||
<h3>{{ item.title }}</h3>
|
|
||||||
<p class="media-description">{{ item.description }}</p>
|
|
||||||
<div class="media-meta">
|
|
||||||
<span>{{ item.publishDate }}</span>
|
|
||||||
<span>{{ item.views }} 观看</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const mediaList = ref<any[]>([]);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// TODO: 加载媒体档案数据
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewMedia(item: any) {
|
|
||||||
router.push(`/resource/media/${item.id}`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.media-archive {
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-item {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-thumbnail {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-type {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
background: rgba(198, 40, 40, 0.9);
|
|
||||||
color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-info {
|
|
||||||
margin-top: 12px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-description {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="policy-speech">
|
|
||||||
<h2 class="page-title">政策讲话</h2>
|
|
||||||
<div class="speech-list">
|
|
||||||
<div class="speech-item" v-for="speech in speeches" :key="speech.id" @click="viewSpeech(speech)">
|
|
||||||
<div class="speech-header">
|
|
||||||
<h3>{{ speech.title }}</h3>
|
|
||||||
<span class="speech-date">{{ speech.date }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="speech-info">
|
|
||||||
<span class="speaker">讲话人:{{ speech.speaker }}</span>
|
|
||||||
<span class="occasion">场合:{{ speech.occasion }}</span>
|
|
||||||
</div>
|
|
||||||
<p class="speech-summary">{{ speech.summary }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const speeches = ref<any[]>([]);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// TODO: 加载政策讲话数据
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewSpeech(speech: any) {
|
|
||||||
router.push(`/resource/speech/${speech.id}`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.policy-speech {
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speech-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speech-item {
|
|
||||||
padding: 24px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #C62828;
|
|
||||||
box-shadow: 0 2px 8px rgba(198, 40, 40, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.speech-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #141F38;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.speech-date {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speech-info {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speech-summary {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-bottom">
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="nav-link" :class="{ disabled: !prevResource }" @click="handleNavigate('prev')">
|
||||||
|
<span class="nav-label">上一篇:</span>
|
||||||
|
<span class="nav-title">{{ prevResource?.title || '没有了' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-link" :class="{ disabled: !nextResource }" @click="handleNavigate('next')">
|
||||||
|
<span class="nav-label">下一篇:</span>
|
||||||
|
<span class="nav-title">{{ nextResource?.title || '没有了' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Resource } from '@/types/resource';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
prevResource?: Resource | null;
|
||||||
|
nextResource?: Resource | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigate: [direction: 'prev' | 'next', resource: Resource];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleNavigate(direction: 'prev' | 'next') {
|
||||||
|
const resource = direction === 'prev' ? props.prevResource : props.nextResource;
|
||||||
|
if (resource) {
|
||||||
|
emit('navigate', direction, resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.resource-bottom {
|
||||||
|
margin-top: 80px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: #E9E9E9;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #334155;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover:not(.disabled) {
|
||||||
|
color: #C62828;
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
color: inherit;
|
||||||
|
transition: text-decoration 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-collect-btn">
|
||||||
|
<button
|
||||||
|
class="collect-button"
|
||||||
|
:class="{ collected: isCollected }"
|
||||||
|
@click="handleCollect"
|
||||||
|
>
|
||||||
|
<img src="@/assets/imgs/star-icon.svg" alt="collect" />
|
||||||
|
<span>{{ isCollected ? '已收藏' : '收藏' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
isCollected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
collect: [];
|
||||||
|
uncollect: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleCollect() {
|
||||||
|
if(props.isCollected) {
|
||||||
|
emit('uncollect');
|
||||||
|
} else {
|
||||||
|
emit('collect');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.resource-collect-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collect-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid #979797;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transition: filter 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #979797;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #C62828;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #C62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: brightness(0) saturate(100%) invert(17%) sepia(85%) saturate(3207%) hue-rotate(349deg) brightness(92%) contrast(92%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collected {
|
||||||
|
border-color: #C62828;
|
||||||
|
background: #FFF5F5;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #C62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: brightness(0) saturate(100%) invert(17%) sepia(85%) saturate(3207%) hue-rotate(349deg) brightness(92%) contrast(92%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-article">
|
||||||
|
<div class="page-header">
|
||||||
|
<el-button @click="handleBack" :icon="ArrowLeft">返回</el-button>
|
||||||
|
</div>
|
||||||
|
<ArticleShowView
|
||||||
|
v-if="articleData"
|
||||||
|
:as-dialog="false"
|
||||||
|
:article-data="articleData"
|
||||||
|
:category-list="[]"
|
||||||
|
/>
|
||||||
|
<div v-else class="loading">加载中...</div>
|
||||||
|
<ResouceCollect
|
||||||
|
:is-collected="isCollected"
|
||||||
|
@collect="handleCollect"
|
||||||
|
@uncollect="handleUncollect"
|
||||||
|
/>
|
||||||
|
<ResouceBottom
|
||||||
|
:prev-resource="prevResource"
|
||||||
|
:next-resource="nextResource"
|
||||||
|
@navigate="handleNavigate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { ArticleShowView } from '@/views/article';
|
||||||
|
import { ResouceCollect, ResouceBottom } from '@/views/resource-center/components';
|
||||||
|
import { resourceApi } from '@/apis/resource';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import type { Resource } from '@/types/resource';
|
||||||
|
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
resourceId?: string;
|
||||||
|
categoryId?: string;
|
||||||
|
resourceList?: Resource[];
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'resource-change': [resourceId: string];
|
||||||
|
'back-to-list': [];
|
||||||
|
'navigate': [direction: 'prev' | 'next', resourceId: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const articleData = ref<any>(null);
|
||||||
|
const isCollected = ref(false);
|
||||||
|
const prevResource = ref<Resource | null>(null);
|
||||||
|
const nextResource = ref<Resource | null>(null);
|
||||||
|
|
||||||
|
watch(() => props.resourceId, (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
// 进入加载前先置空,避免子组件读取到 null 字段
|
||||||
|
articleData.value = null;
|
||||||
|
loadArticle(newId);
|
||||||
|
updateAdjacentResources(newId);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(() => props.resourceList, () => {
|
||||||
|
if (props.resourceId) {
|
||||||
|
updateAdjacentResources(props.resourceId);
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
async function loadArticle(resourceId: string) {
|
||||||
|
try {
|
||||||
|
const res = await resourceApi.getResourceById(resourceId);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const resourceVO = res.data;
|
||||||
|
articleData.value = {
|
||||||
|
...resourceVO.resource,
|
||||||
|
tags: resourceVO.tags || []
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ElMessage.error('加载文章失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载文章失败:', error);
|
||||||
|
ElMessage.error('加载文章失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAdjacentResources(currentResourceId: string) {
|
||||||
|
if (!props.resourceList || props.resourceList.length === 0) {
|
||||||
|
prevResource.value = null;
|
||||||
|
nextResource.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = props.resourceList.findIndex(r =>
|
||||||
|
String(r.resourceID) === String(currentResourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentIndex !== -1) {
|
||||||
|
prevResource.value = currentIndex > 0 ? props.resourceList[currentIndex - 1] : null;
|
||||||
|
nextResource.value = currentIndex < props.resourceList.length - 1 ? props.resourceList[currentIndex + 1] : null;
|
||||||
|
} else {
|
||||||
|
prevResource.value = null;
|
||||||
|
nextResource.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCollect() {
|
||||||
|
try {
|
||||||
|
const resourceID = articleData.value?.resourceID;
|
||||||
|
if (!resourceID) return;
|
||||||
|
|
||||||
|
const res = await resourceApi.incrementCollectCount(resourceID);
|
||||||
|
if (res.success) {
|
||||||
|
isCollected.value = true;
|
||||||
|
ElMessage.success('收藏成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('收藏失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('收藏失败:', error);
|
||||||
|
ElMessage.error('收藏失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUncollect() {
|
||||||
|
// TODO: 实现取消收藏API
|
||||||
|
isCollected.value = false;
|
||||||
|
ElMessage.success('已取消收藏');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavigate(direction: 'prev' | 'next', resource: Resource) {
|
||||||
|
const resourceId = resource.resourceID;
|
||||||
|
if (resourceId) {
|
||||||
|
emit('resource-change', resourceId);
|
||||||
|
emit('navigate', direction, resourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
function handleBack() {
|
||||||
|
emit('back-to-list');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.resource-article {
|
||||||
|
flex: 1;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 40px 60px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-head">
|
||||||
|
<div class="head-title">资源中心</div>
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<span class="breadcrumb-label">当前位置:</span>
|
||||||
|
<div class="breadcrumb-nav">
|
||||||
|
<span class="breadcrumb-item">资源中心</span>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span class="breadcrumb-item">{{ categoryName }}</span>
|
||||||
|
<span class="separator" v-if="showArticleMode">/</span>
|
||||||
|
<span class="breadcrumb-item active" v-if="showArticleMode">正文</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
categoryName?: string;
|
||||||
|
showArticleMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
categoryName: '党史学习',
|
||||||
|
showArticleMode: false
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.resource-head {
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
background: url('@/assets/imgs/resource-head-bg.jpg') center/cover;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-title {
|
||||||
|
font-family: 'MiSans VF', 'PingFang SC';
|
||||||
|
font-weight: 630;
|
||||||
|
font-size: 40px;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #FFFFFF;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-label {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: #F1F1F1;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-size: 14px;
|
||||||
|
color: #C9CDD4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-list">
|
||||||
|
<div class="list-container" ref="listContainerRef">
|
||||||
|
<div
|
||||||
|
v-for="resource in resources"
|
||||||
|
:key="resource.resourceID "
|
||||||
|
class="resource-item"
|
||||||
|
@click="handleResourceClick(resource)"
|
||||||
|
>
|
||||||
|
<div class="resource-cover">
|
||||||
|
<img
|
||||||
|
:src="resource.coverImage ? (FILE_DOWNLOAD_URL + resource.coverImage) : defaultArticleImg"
|
||||||
|
alt="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="resource-info">
|
||||||
|
<h3 class="resource-title">{{ resource.title }}</h3>
|
||||||
|
<div class="resource-collect">
|
||||||
|
<img src="@/assets/imgs/star-icon.svg" alt="collect" />
|
||||||
|
<span>{{ (resource.collectCount || 0) > 0 ? '已收藏' : '收藏' }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="resource-summary">{{ resource.summary || '暂无简介' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading-more">加载中...</div>
|
||||||
|
<div v-if="resources.length === 0 && !loading" class="empty">暂无数据</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="total > 0" class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
layout="prev, pager, next, jumper"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted, nextTick } from 'vue';
|
||||||
|
import { resourceApi } from '@/apis/resource';
|
||||||
|
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||||
|
import type { Resource, ResourceSearchParams } from '@/types/resource';
|
||||||
|
import type { PageParam } from '@/types';
|
||||||
|
import defaultArticleImg from '@/assets/imgs/article-default.png';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
categoryId?: string;
|
||||||
|
searchKeyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'resource-click': [resource: Resource];
|
||||||
|
'list-updated': [resources: Resource[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const resources = ref<Resource[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
const listContainerRef = ref<HTMLElement>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadResources();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => [props.categoryId, props.searchKeyword], () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
loadResources();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
async function loadResources() {
|
||||||
|
if (loading.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filter: ResourceSearchParams = {
|
||||||
|
categoryID: props.categoryId,
|
||||||
|
keyword: props.searchKeyword,
|
||||||
|
// status: 1 // 只加载已发布的
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageParam: PageParam = {
|
||||||
|
page: currentPage.value,
|
||||||
|
size: pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await resourceApi.getResourcePage(pageParam, filter);
|
||||||
|
|
||||||
|
if (res.success && res.dataList) {
|
||||||
|
resources.value = res.dataList;
|
||||||
|
total.value = res.pageDomain?.total || 0;
|
||||||
|
|
||||||
|
// 通知父组件列表已更新
|
||||||
|
emit('list-updated', res.dataList);
|
||||||
|
|
||||||
|
// 重置滚动位置到顶部
|
||||||
|
await nextTick();
|
||||||
|
if (listContainerRef.value) {
|
||||||
|
listContainerRef.value.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载资源列表失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
currentPage.value = page;
|
||||||
|
loadResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResources() {
|
||||||
|
return resources.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageInfo() {
|
||||||
|
return { currentPage: currentPage.value, total: total.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadNextPage() {
|
||||||
|
const totalPages = Math.ceil(total.value / pageSize);
|
||||||
|
if (currentPage.value < totalPages) {
|
||||||
|
currentPage.value++;
|
||||||
|
await loadResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPrevPage() {
|
||||||
|
if (currentPage.value > 1) {
|
||||||
|
currentPage.value--;
|
||||||
|
await loadResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResourceClick(resource: Resource) {
|
||||||
|
emit('resource-click', resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
loadResources,
|
||||||
|
getResources,
|
||||||
|
getPageInfo,
|
||||||
|
loadNextPage,
|
||||||
|
loadPrevPage
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.resource-list {
|
||||||
|
flex: 1;
|
||||||
|
align-self: stretch;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
border-bottom: 1px solid #EEEEEE;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.resource-title {
|
||||||
|
color: #C62828;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cover {
|
||||||
|
width: 202px;
|
||||||
|
height: 123px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-title {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
color: #C62828;
|
||||||
|
margin: 0;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-collect {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-summary {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #334155;
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more,
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-size: 14px;
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-top: 1px solid #EEEEEE;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-sidebar">
|
||||||
|
<div class="sidebar-content">
|
||||||
|
<div
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.categoryID"
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ active: category.categoryID === activeCategoryId }"
|
||||||
|
@click="handleCategoryClick(category)"
|
||||||
|
>
|
||||||
|
<span class="category-name">{{ category.name }}</span>
|
||||||
|
<div v-if="category.categoryID === activeCategoryId" class="active-overlay"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { resourceCategoryApi } from '@/apis/resource';
|
||||||
|
import type { ResourceCategory } from '@/types/resource';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activeCategoryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
activeCategoryId: 'party_history'
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'category-change': [category: ResourceCategory];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const categories = ref<ResourceCategory[]>([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadCategories();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadCategories() {
|
||||||
|
try {
|
||||||
|
const res = await resourceCategoryApi.getCategoryList();
|
||||||
|
if (res.success && res.dataList) {
|
||||||
|
categories.value = res.dataList;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCategoryClick(category: ResourceCategory) {
|
||||||
|
emit('category-change', category);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.resource-sidebar {
|
||||||
|
width: 180px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
position: relative;
|
||||||
|
height: 54px;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.category-name {
|
||||||
|
color: #C62828;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.category-name {
|
||||||
|
color: #FFFFFF;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 5px;
|
||||||
|
background: #C62828;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #334155;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export { default as ResourceHead } from './ResourceHead.vue';
|
||||||
|
export { default as ResourceSideBar } from './ResourceSideBar.vue';
|
||||||
|
export { default as ResourceList } from './ResourceList.vue';
|
||||||
|
export { default as ResourceArticle } from './ResourceArticle.vue';
|
||||||
|
export { default as ResouceCollect } from './ResouceCollect.vue';
|
||||||
|
export { default as ResouceBottom } from './ResouceBottom.vue';
|
||||||
Reference in New Issue
Block a user