成就等界面接口调整
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { api } from '@/apis/index';
|
||||
import type { Resource, ResultDomain } from '@/types';
|
||||
import type { Resource, ResourceRecommendVO, ResultDomain } from '@/types';
|
||||
|
||||
/**
|
||||
* 推荐API服务
|
||||
@@ -38,5 +38,27 @@ export const recommendApi = {
|
||||
async getHotNews(limit?: number): Promise<ResultDomain<Resource>> {
|
||||
const response = await api.get<Resource>('/homepage/recommend/hot-news', { limit });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热门资源列表(推荐类型:1)
|
||||
* @param limit 限制数量
|
||||
* @returns Promise<ResultDomain<ResourceRecommendVO>>
|
||||
*/
|
||||
async getHotResources(limit?: number): Promise<ResultDomain<ResourceRecommendVO>> {
|
||||
|
||||
const response = await api.get<ResourceRecommendVO>('/homepage/recommend/hot', { limit });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取思政资源列表(推荐类型:2)
|
||||
* @param limit 限制数量
|
||||
* @returns Promise<ResultDomain<ResourceRecommendVO>>
|
||||
*/
|
||||
async getIdeologicalResources(limit?: number): Promise<ResultDomain<ResourceRecommendVO>> {
|
||||
|
||||
const response = await api.get<ResourceRecommendVO>('/homepage/recommend/ideological', { limit });
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
|
||||
export * from './resourceTag';
|
||||
export * from './resource';
|
||||
export { bannerApi} from './banner';
|
||||
export * from './resourceRecommend';
|
||||
export { bannerApi } from './banner';
|
||||
@@ -136,16 +136,6 @@ export const resourceApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 收藏次数增减
|
||||
* @param resourceID 资源ID
|
||||
* @returns Promise<ResultDomain<Resource>>
|
||||
*/
|
||||
async resourceCollect(collect: UserCollection): Promise<ResultDomain<Resource>> {
|
||||
const response = await api.post<Resource>(`/news/resources/resource/collect`, collect);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ==================== 资源推荐和轮播操作 ====================
|
||||
|
||||
/**
|
||||
|
||||
155
schoolNewsWeb/src/apis/resource/resourceRecommend.ts
Normal file
155
schoolNewsWeb/src/apis/resource/resourceRecommend.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @description 资源推荐管理API接口
|
||||
* @filename resourceRecommend.ts
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-31
|
||||
*/
|
||||
|
||||
import { api } from '@/apis';
|
||||
import type { ResultDomain, ResourceRecommendVO, PageParam } from '@/types';
|
||||
|
||||
/**
|
||||
* 推荐类型枚举
|
||||
*/
|
||||
export enum RecommendType {
|
||||
/** 热门资源推荐 */
|
||||
HOT = 1,
|
||||
/** 思政资源推荐 */
|
||||
IDEOLOGICAL = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源推荐API服务
|
||||
*/
|
||||
export const resourceRecommendApi = {
|
||||
/**
|
||||
* 获取推荐列表
|
||||
* @returns Promise<ResultDomain<ResourceRecommendVO>>
|
||||
*/
|
||||
async getRecommendList(): Promise<ResultDomain<ResourceRecommendVO>> {
|
||||
const response = await api.get<ResourceRecommendVO>('/news/recommends/list');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据推荐类型获取推荐资源列表
|
||||
* @param recommendType 推荐类型(1-热门资源,2-思政资源)
|
||||
* @param limit 限制数量
|
||||
* @returns Promise<ResultDomain<ResourceRecommendVO>>
|
||||
*/
|
||||
async getRecommendsByType(recommendType: number, limit?: number): Promise<ResultDomain<ResourceRecommendVO>> {
|
||||
const params = limit ? { limit } : null;
|
||||
const response = await api.get<ResourceRecommendVO>(`/news/recommends/type/${recommendType}`, params);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询推荐资源列表
|
||||
* @param filter 筛选条件
|
||||
* @param pageParam 分页参数
|
||||
* @returns Promise<ResultDomain<ResourceRecommendVO>>
|
||||
*/
|
||||
async getRecommendPage(pageParam: PageParam, filter?: any): Promise<ResultDomain<ResourceRecommendVO>> {
|
||||
const response = await api.post<ResourceRecommendVO>('/news/recommends/page', {
|
||||
pageParam,
|
||||
filter,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据ID获取推荐详情
|
||||
* @param recommendID 推荐ID
|
||||
* @returns Promise<ResultDomain<ResourceRecommendVO>>
|
||||
*/
|
||||
async getRecommendById(recommendID: string): Promise<ResultDomain<ResourceRecommendVO>> {
|
||||
const response = await api.get<ResourceRecommendVO>(`/news/recommends/recommend/${recommendID}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建推荐
|
||||
* @param recommend 推荐信息
|
||||
* @returns Promise<ResultDomain<any>>
|
||||
*/
|
||||
async createRecommend(recommend: any): Promise<ResultDomain<any>> {
|
||||
const response = await api.post<any>('/news/recommends/recommend', recommend);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量添加推荐资源
|
||||
* @param resourceIDs 资源ID列表
|
||||
* @param recommendType 推荐类型
|
||||
* @param reason 推荐理由(可选)
|
||||
* @returns Promise<ResultDomain<any>>
|
||||
*/
|
||||
async batchAddRecommends(resourceIDs: string[], recommendType: number, reason?: string): Promise<ResultDomain<any>> {
|
||||
const response = await api.post<any>('/news/recommends/recommend/batch', {
|
||||
resourceIDs,
|
||||
recommendType,
|
||||
reason
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新推荐
|
||||
* @param recommend 推荐信息
|
||||
* @returns Promise<ResultDomain<any>>
|
||||
*/
|
||||
async updateRecommend(recommend: any): Promise<ResultDomain<any>> {
|
||||
const response = await api.put<any>('/news/recommends/recommend', recommend);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除推荐
|
||||
* @param recommendID 推荐ID
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async deleteRecommend(recommendID: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`/news/recommends/recommend/${recommendID}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新推荐排序
|
||||
* @param recommendID 推荐ID
|
||||
* @param orderNum 排序号
|
||||
* @returns Promise<ResultDomain<any>>
|
||||
*/
|
||||
async updateRecommendOrder(recommendID: string, orderNum: number): Promise<ResultDomain<any>> {
|
||||
const response = await api.put<any>(`/news/recommends/recommend/${recommendID}/order`, null, {
|
||||
params: { orderNum }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查资源是否已推荐(按类型)
|
||||
* @param resourceID 资源ID
|
||||
* @param recommendType 推荐类型
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async isResourceRecommendedByType(resourceID: string, recommendType: number): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.get<boolean>(`/news/recommends/check/${resourceID}`, {
|
||||
recommendType
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 统计推荐资源总数
|
||||
* @param filter 筛选条件
|
||||
* @returns Promise<ResultDomain<number>>
|
||||
*/
|
||||
async countRecommends(filter?: any): Promise<ResultDomain<number>> {
|
||||
const response = await api.post<number>('/news/recommends/count', filter);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export default resourceRecommendApi;
|
||||
|
||||
@@ -19,8 +19,8 @@ export const resourceTagApi = {
|
||||
* 获取标签列表(获取所有标签)
|
||||
* @returns Promise<ResultDomain<Tag>>
|
||||
*/
|
||||
async getTagList(): Promise<ResultDomain<Tag>> {
|
||||
const response = await api.get<Tag>('/news/tags/list');
|
||||
async getTagList(filter: Tag): Promise<ResultDomain<Tag>> {
|
||||
const response = await api.get<Tag>('/news/tags/list', filter);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { api } from '@/apis/index';
|
||||
import type { UserCollection, ResultDomain } from '@/types';
|
||||
import type { UserCollection, UserCollectionVO, ResultDomain } from '@/types';
|
||||
|
||||
/**
|
||||
* 用户收藏API服务
|
||||
@@ -13,14 +13,13 @@ import type { UserCollection, ResultDomain } from '@/types';
|
||||
export const userCollectionApi = {
|
||||
baseUrl: '/usercenter/collections',
|
||||
/**
|
||||
* 获取用户收藏列表
|
||||
* 获取用户收藏列表(扁平化VO,包含资源/课程详情)
|
||||
* @param userID 用户ID
|
||||
* @param collectionType 收藏类型
|
||||
* @returns Promise<ResultDomain<UserCollection>>
|
||||
* @returns Promise<ResultDomain<UserCollectionVO>>
|
||||
*/
|
||||
async getUserCollections(userID: string, collectionType?: number): Promise<ResultDomain<UserCollection>> {
|
||||
const response = await api.get<UserCollection>(`${this.baseUrl}/list`, {
|
||||
userID,
|
||||
async getUserCollections(userID: string, collectionType?: number): Promise<ResultDomain<UserCollectionVO>> {
|
||||
const response = await api.get<UserCollectionVO>(`${this.baseUrl}/user/${userID}`, {
|
||||
collectionType
|
||||
});
|
||||
return response.data;
|
||||
@@ -43,12 +42,8 @@ export const userCollectionApi = {
|
||||
* @param collectionID 收藏对象ID
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async removeCollection(userID: string, collectionType: number, collectionID: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`${this.baseUrl}/collect`, {
|
||||
userID,
|
||||
collectionType,
|
||||
collectionID
|
||||
});
|
||||
async removeCollection(collection: UserCollection): Promise<ResultDomain<UserCollection>> {
|
||||
const response = await api.delete<UserCollection>(`${this.baseUrl}/collect`, collection);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
4
schoolNewsWeb/src/assets/imgs/hot.svg
Normal file
4
schoolNewsWeb/src/assets/imgs/hot.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99992 2C8.44436 3.77778 9.33325 5.22222 10.6666 6.33333C11.9999 7.44444 12.6666 8.66667 12.6666 10C12.6666 11.2377 12.1749 12.4247 11.2997 13.2998C10.4246 14.175 9.2376 14.6667 7.99992 14.6667C6.76224 14.6667 5.57526 14.175 4.70009 13.2998C3.82492 12.4247 3.33325 11.2377 3.33325 10C3.33325 9.27877 3.56718 8.57699 3.99992 8C3.99992 8.44203 4.17551 8.86595 4.48807 9.17851C4.80063 9.49107 5.22456 9.66667 5.66659 9.66667C6.10861 9.66667 6.53254 9.49107 6.8451 9.17851C7.15766 8.86595 7.33325 8.44203 7.33325 8C7.33325 6.66667 6.33325 6 6.33325 4.66667C6.33325 3.77778 6.88881 2.88889 7.99992 2Z" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 806 B |
@@ -37,7 +37,15 @@ $spacing-xl: 20px;
|
||||
$spacing-xxl: 24px;
|
||||
|
||||
// ============ 按钮样式 ============
|
||||
|
||||
.btn-default {
|
||||
background: #409eff;
|
||||
border: none;
|
||||
border-radius: $border-radius-medium;
|
||||
color: $color-bg-white;
|
||||
font-size: $font-size-base;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
// 主要操作按钮
|
||||
.btn-primary {
|
||||
align-items: center;
|
||||
@@ -188,7 +196,7 @@ $spacing-xxl: 24px;
|
||||
align-items: center;
|
||||
padding: $spacing-md 0;
|
||||
margin-top: 32px;
|
||||
background: #F9FAFB;
|
||||
// background: #F9FAFB;
|
||||
border-radius: $border-radius-large;
|
||||
|
||||
// Element Plus 分页组件自定义样式
|
||||
|
||||
@@ -178,6 +178,79 @@ export interface ResourceRecommend extends BaseDTO {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源推荐VO(平铺结构,包含资源信息和权限信息)
|
||||
*/
|
||||
export interface ResourceRecommendVO extends BaseDTO {
|
||||
// ==================== 推荐表字段 ====================
|
||||
/** 推荐ID */
|
||||
id?: string;
|
||||
/** 资源ID */
|
||||
resourceID?: string;
|
||||
/** 推荐类型(1-热门资源,2-思政资源) */
|
||||
recommendType?: number;
|
||||
/** 排序号 */
|
||||
orderNum?: number;
|
||||
/** 推荐理由 */
|
||||
reason?: string;
|
||||
/** 推荐创建者 */
|
||||
creator?: string;
|
||||
/** 推荐更新者 */
|
||||
updater?: string;
|
||||
/** 推荐创建时间 */
|
||||
createTime?: string;
|
||||
/** 推荐更新时间 */
|
||||
updateTime?: string;
|
||||
/** 推荐删除时间 */
|
||||
deleteTime?: string;
|
||||
/** 推荐是否删除 */
|
||||
deleted?: boolean;
|
||||
|
||||
// ==================== 资源表字段 ====================
|
||||
/** 资源标题 */
|
||||
title?: string;
|
||||
/** 资源简介 */
|
||||
summary?: string;
|
||||
/** 封面图片 */
|
||||
coverImage?: string;
|
||||
/** 标签ID(文章分类标签,tagType=1) */
|
||||
tagID?: string;
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
/** 来源 */
|
||||
source?: string;
|
||||
/** 来源URL */
|
||||
sourceUrl?: string;
|
||||
/** 浏览次数 */
|
||||
viewCount?: number;
|
||||
/** 点赞次数 */
|
||||
likeCount?: number;
|
||||
/** 收藏次数 */
|
||||
collectCount?: number;
|
||||
/** 状态(0草稿 1已发布 2下架) */
|
||||
status?: number;
|
||||
/** 是否推荐 */
|
||||
isRecommend?: boolean;
|
||||
/** 是否轮播 */
|
||||
isBanner?: boolean;
|
||||
/** 发布时间 */
|
||||
publishTime?: string;
|
||||
/** 资源创建者 */
|
||||
resourceCreator?: string;
|
||||
/** 资源更新者 */
|
||||
resourceUpdater?: string;
|
||||
/** 资源创建时间 */
|
||||
resourceCreateTime?: string;
|
||||
/** 资源更新时间 */
|
||||
resourceUpdateTime?: string;
|
||||
|
||||
// ==================== 权限字段 ====================
|
||||
/** 是否可读 */
|
||||
canRead?: boolean;
|
||||
/** 是否可写 */
|
||||
canWrite?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据采集配置实体
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { BaseDTO } from '@/types';
|
||||
import type { Tag } from '@/types/resource';
|
||||
|
||||
|
||||
/**
|
||||
@@ -344,6 +345,8 @@ export interface TaskVO extends BaseDTO {
|
||||
taskResources: TaskItemVO[];
|
||||
/** 任务关联的用户列表 */
|
||||
taskUsers: TaskItemVO[];
|
||||
/** 任务关联的标签列表 */
|
||||
taskTags?: Tag[];
|
||||
/** 总任务数 */
|
||||
totalTaskNum?: number;
|
||||
/** 已完成任务数 */
|
||||
|
||||
@@ -20,6 +20,67 @@ export interface UserCollection extends BaseDTO {
|
||||
collectionValue?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户收藏VO - 扁平化收藏信息和关联资源/课程详情
|
||||
*/
|
||||
export interface UserCollectionVO extends BaseDTO {
|
||||
// ========== 收藏基本信息 ==========
|
||||
/** 用户ID */
|
||||
userID?: string;
|
||||
/** 收藏类型(1资源 2课程) */
|
||||
collectionType?: CollectionType;
|
||||
/** 收藏对象ID */
|
||||
collectionID?: string;
|
||||
/** 是否新增收藏(1是 -1否) */
|
||||
collectionValue?: number;
|
||||
/** 收藏时间 */
|
||||
collectionTime?: string;
|
||||
|
||||
// ========== 资源详情(collectionType=1时有效) ==========
|
||||
/** 资源ID */
|
||||
resourceID?: string;
|
||||
/** 资源标题 */
|
||||
title?: string;
|
||||
/** 资源内容 */
|
||||
content?: string;
|
||||
/** 资源简介 */
|
||||
summary?: string;
|
||||
/** 封面图片 */
|
||||
coverImage?: string;
|
||||
/** 标签ID */
|
||||
tagID?: string;
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
/** 来源 */
|
||||
source?: string;
|
||||
/** 浏览次数 */
|
||||
viewCount?: number;
|
||||
/** 点赞次数 */
|
||||
likeCount?: number;
|
||||
/** 收藏次数 */
|
||||
collectCount?: number;
|
||||
/** 资源状态 */
|
||||
status?: number;
|
||||
/** 发布时间 */
|
||||
publishTime?: string;
|
||||
|
||||
// ========== 课程详情(collectionType=2时有效) ==========
|
||||
/** 课程ID */
|
||||
courseID?: string;
|
||||
/** 课程名称 */
|
||||
courseName?: string;
|
||||
/** 课程描述 */
|
||||
description?: string;
|
||||
/** 课程时长(分钟) */
|
||||
duration?: number;
|
||||
/** 授课老师 */
|
||||
teacher?: string;
|
||||
/** 课程状态 */
|
||||
courseStatus?: number;
|
||||
/** 学习人数 */
|
||||
learnCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户浏览记录实体
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,89 +13,171 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 标签卡片网格 -->
|
||||
<div v-loading="loading" class="tag-grid">
|
||||
<div
|
||||
v-for="tag in tags"
|
||||
:key="tag.tagID"
|
||||
class="tag-card"
|
||||
<!-- 按类型分类展示 -->
|
||||
<div class="tag-categories" v-loading="loading">
|
||||
<!-- 文章分类标签 -->
|
||||
<div class="tag-category-section">
|
||||
<div class="category-header">
|
||||
<h3 class="category-title">文章分类标签</h3>
|
||||
<span class="category-count">({{ articleTags.length }})</span>
|
||||
</div>
|
||||
<div class="tag-grid">
|
||||
<div
|
||||
v-for="tag in articleTags"
|
||||
:key="tag.tagID"
|
||||
class="tag-card"
|
||||
>
|
||||
<div class="tag-content">
|
||||
<div class="tag-info">
|
||||
<div class="tag-dot" :style="{ background: tag.color }"></div>
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-actions">
|
||||
<button class="edit-button" @click="editTag(tag)">
|
||||
<img src="@/assets/imgs/edit.svg" alt="编辑" />
|
||||
<span>编辑</span>
|
||||
</button>
|
||||
<button class="delete-button" @click="deleteTag(tag)">
|
||||
<img src="@/assets/imgs/trashbin.svg" alt="删除" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && articleTags.length === 0" class="empty-state">
|
||||
暂无文章分类标签
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 课程分类标签 -->
|
||||
<div class="tag-category-section">
|
||||
<div class="category-header">
|
||||
<h3 class="category-title">课程分类标签</h3>
|
||||
<span class="category-count">({{ courseTags.length }})</span>
|
||||
</div>
|
||||
<div class="tag-grid">
|
||||
<div
|
||||
v-for="tag in courseTags"
|
||||
:key="tag.tagID"
|
||||
class="tag-card"
|
||||
>
|
||||
<div class="tag-content">
|
||||
<div class="tag-info">
|
||||
<div class="tag-dot" :style="{ background: tag.color }"></div>
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-actions">
|
||||
<button class="edit-button" @click="editTag(tag)">
|
||||
<img src="@/assets/imgs/edit.svg" alt="编辑" />
|
||||
<span>编辑</span>
|
||||
</button>
|
||||
<button class="delete-button" @click="deleteTag(tag)">
|
||||
<img src="@/assets/imgs/trashbin.svg" alt="删除" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && courseTags.length === 0" class="empty-state">
|
||||
暂无课程分类标签
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学习任务分类标签 -->
|
||||
<div class="tag-category-section">
|
||||
<div class="category-header">
|
||||
<h3 class="category-title">学习任务分类标签</h3>
|
||||
<span class="category-count">({{ taskTags.length }})</span>
|
||||
</div>
|
||||
<div class="tag-grid">
|
||||
<div
|
||||
v-for="tag in taskTags"
|
||||
:key="tag.tagID"
|
||||
class="tag-card"
|
||||
>
|
||||
<div class="tag-content">
|
||||
<div class="tag-info">
|
||||
<div class="tag-dot" :style="{ background: tag.color }"></div>
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-actions">
|
||||
<button class="edit-button" @click="editTag(tag)">
|
||||
<img src="@/assets/imgs/edit.svg" alt="编辑" />
|
||||
<span>编辑</span>
|
||||
</button>
|
||||
<button class="delete-button" @click="deleteTag(tag)">
|
||||
<img src="@/assets/imgs/trashbin.svg" alt="删除" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && taskTags.length === 0" class="empty-state">
|
||||
暂无学习任务分类标签
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局空状态 -->
|
||||
<div v-if="!loading && tags.length === 0" class="global-empty-state">
|
||||
暂无标签数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑标签对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑标签' : '新增标签'"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<div class="tag-content">
|
||||
<div class="tag-info">
|
||||
<div class="tag-dot" :style="{ background: tag.color }"></div>
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
</div>
|
||||
<!-- <div class="tag-badge">{{ tag.usageCount || 0 }}</div> -->
|
||||
</div>
|
||||
<el-form :model="currentTag" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="currentTag.name" placeholder="请输入标签名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签类型" prop="tagType">
|
||||
<el-select v-model="currentTag.tagType" placeholder="请选择标签类型" style="width: 100%">
|
||||
<el-option label="文章分类标签" :value="1" />
|
||||
<el-option label="课程分类标签" :value="2" />
|
||||
<el-option label="学习任务分类标签" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<div class="tag-actions">
|
||||
<button class="edit-button" @click="editTag(tag)">
|
||||
<img src="@/assets/imgs/edit.svg" alt="编辑" />
|
||||
<span>编辑</span>
|
||||
</button>
|
||||
<button class="delete-button" @click="deleteTag(tag)">
|
||||
<img src="@/assets/imgs/trashbin.svg" alt="删除" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item label="标签颜色" prop="color">
|
||||
<div class="color-picker-wrapper">
|
||||
<el-color-picker v-model="currentTag.color" />
|
||||
<el-input v-model="currentTag.color" placeholder="#000000" style="width: 150px; margin-left: 10px" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && tags.length === 0" class="empty-state">
|
||||
暂无标签数据
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item label="标签描述">
|
||||
<el-input
|
||||
v-model="currentTag.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入标签描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 创建/编辑标签对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑标签' : '新增标签'"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form :model="currentTag" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="标签名称" prop="name">
|
||||
<el-input v-model="currentTag.name" placeholder="请输入标签名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签类型" prop="tagType">
|
||||
<el-select v-model="currentTag.tagType" placeholder="请选择标签类型" style="width: 100%">
|
||||
<el-option label="文章分类标签" :value="1" />
|
||||
<el-option label="课程分类标签" :value="2" />
|
||||
<el-option label="学习任务分类标签" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签颜色" prop="color">
|
||||
<div class="color-picker-wrapper">
|
||||
<el-color-picker v-model="currentTag.color" />
|
||||
<el-input v-model="currentTag.color" placeholder="#000000" style="width: 150px; margin-left: 10px" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签描述">
|
||||
<el-input
|
||||
v-model="currentTag.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入标签描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import type { Tag } from '@/types/resource';
|
||||
import { TagType } from '@/types/resource';
|
||||
import {AdminLayout} from '@/views/admin';
|
||||
|
||||
defineOptions({
|
||||
@@ -116,6 +198,19 @@ const currentTag = ref<Partial<Tag>>({
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 按类型分类的标签
|
||||
const articleTags = computed(() => {
|
||||
return tags.value.filter(tag => tag.tagType === TagType.ARTICLE_CATEGORY);
|
||||
});
|
||||
|
||||
const courseTags = computed(() => {
|
||||
return tags.value.filter(tag => tag.tagType === TagType.COURSE_CATEGORY);
|
||||
});
|
||||
|
||||
const taskTags = computed(() => {
|
||||
return tags.value.filter(tag => tag.tagType === TagType.LEARNING_TASK_CATEGORY);
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入标签名称', trigger: 'blur' }
|
||||
@@ -136,7 +231,7 @@ onMounted(() => {
|
||||
async function loadTags() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const result = await resourceTagApi.getTagList();
|
||||
const result = await resourceTagApi.getTagList({});
|
||||
if (result.success) {
|
||||
tags.value = result.dataList || [];
|
||||
// TODO: 加载每个标签的使用计数
|
||||
@@ -292,11 +387,48 @@ function handleDialogClose() {
|
||||
}
|
||||
}
|
||||
|
||||
.tag-categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.tag-category-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #F0F0F0;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
margin: 0;
|
||||
font-family: 'Inter', 'PingFang SC', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
line-height: 1.5em;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-family: 'Inter', 'PingFang SC', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.5em;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.tag-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
min-height: 200px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.tag-card {
|
||||
@@ -419,6 +551,15 @@ function handleDialogClose() {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.global-empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100px 0;
|
||||
font-size: 16px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.color-picker-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
@@ -431,12 +431,6 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
.detail-item {
|
||||
display: flex;
|
||||
|
||||
@@ -154,11 +154,11 @@
|
||||
/>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:page-sizes="[9, 18, 36]"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@@ -671,12 +671,6 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
@@ -593,12 +593,6 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
@@ -1,27 +1,56 @@
|
||||
<template>
|
||||
<div class="article-card">
|
||||
<div class="article-card" @click="handleClick">
|
||||
<div class="article-image">
|
||||
<div class="image-placeholder"></div>
|
||||
<img v-if="resource?.coverImage" :src="FILE_DOWNLOAD_URL + resource.coverImage" :alt="resource.title" />
|
||||
<div v-else class="image-placeholder"></div>
|
||||
<div class="article-tag">精选文章</div>
|
||||
</div>
|
||||
<div class="article-content">
|
||||
<h3 class="article-title">新时代中国特色社会主义发展历程</h3>
|
||||
<h3 class="article-title">{{ resource?.title || '标题' }}</h3>
|
||||
<p class="article-desc">
|
||||
习近平新时代中国特色社会主义思想是当代中国马克思主义、二十一世纪马克思主义,是中华文化和中国精神的时代精华,其核心要义与实践要求内涵丰富、意义深远。
|
||||
{{ resource?.summary || '暂无简介' }}
|
||||
</p>
|
||||
<div class="article-footer">
|
||||
<div class="meta-tag">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>热门文章</span>
|
||||
</div>
|
||||
<span class="view-count">2.1w次浏览</span>
|
||||
<span class="view-count">{{ formatViewCount(resource?.viewCount || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Document } from '@element-plus/icons-vue';
|
||||
import type { ResourceRecommendVO } from '@/types';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
const props = defineProps<{
|
||||
resource?: ResourceRecommendVO;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 格式化浏览量
|
||||
function formatViewCount(count: number): string {
|
||||
if (count < 1000) {
|
||||
return `${count}次浏览`;
|
||||
} else if (count < 10000) {
|
||||
return `${(count / 1000).toFixed(1)}k次浏览`;
|
||||
} else {
|
||||
return `${(count / 10000).toFixed(1)}w次浏览`;
|
||||
}
|
||||
}
|
||||
|
||||
// 点击卡片
|
||||
function handleClick() {
|
||||
if (props.resource?.resourceID) {
|
||||
router.push(`/article/show?articleId=${props.resource.resourceID}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -47,6 +76,12 @@ import { Document } from '@element-plus/icons-vue';
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,22 +1,55 @@
|
||||
<template>
|
||||
<div class="ideological-card">
|
||||
<div class="ideological-card" @click="handleClick">
|
||||
<div class="card-image">
|
||||
<div class="image-placeholder"></div>
|
||||
<img v-if="resource?.coverImage" :src="FILE_DOWNLOAD_URL + resource.coverImage" :alt="resource.title" />
|
||||
<div v-else class="image-placeholder"></div>
|
||||
</div>
|
||||
<div class="date-box">
|
||||
<div class="day">10</div>
|
||||
<div class="month">2025.10</div>
|
||||
<div class="date-box" v-if="publishDate">
|
||||
<div class="day">{{ publishDate.day }}</div>
|
||||
<div class="month">{{ publishDate.month }}</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">学校召开"习近平新时代中国特色社会主义思想概论"课程集体备课会</h3>
|
||||
<h3 class="card-title">{{ resource?.title || '标题' }}</h3>
|
||||
<p class="card-desc">
|
||||
深入贯彻习近平总书记关于思政课建设的重要论述,持续推进思政课教学改革创新。
|
||||
{{ resource?.summary || '暂无简介' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { ResourceRecommendVO } from '@/types';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
const props = defineProps<{
|
||||
resource?: ResourceRecommendVO;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 格式化发布日期
|
||||
const publishDate = computed(() => {
|
||||
if (!props.resource?.publishTime) return null;
|
||||
|
||||
const date = new Date(props.resource.publishTime);
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
|
||||
return {
|
||||
day: day.toString(),
|
||||
month: `${year}.${month.toString().padStart(2, '0')}`
|
||||
};
|
||||
});
|
||||
|
||||
// 点击卡片
|
||||
function handleClick() {
|
||||
if (props.resource?.resourceID) {
|
||||
router.push(`/article/show?articleId=${props.resource.resourceID}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -42,6 +75,12 @@
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -59,9 +98,8 @@
|
||||
}
|
||||
|
||||
.date-box {
|
||||
position: absolute;
|
||||
top: calc(57.55% - 3.5em);
|
||||
left: 5.7%;
|
||||
margin: 0 5.7%;
|
||||
transform: translateY(-50%);
|
||||
width: 18.75%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #C62828;
|
||||
@@ -73,7 +111,6 @@
|
||||
gap: 0.3em;
|
||||
padding: 0.4em 0.3em;
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
|
||||
.day {
|
||||
font-family: 'PingFang SC';
|
||||
@@ -97,7 +134,7 @@
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 17.4% 5.7% 5.7% 5.7%;
|
||||
padding: 0 5.7% 5.7% 5.7%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -645,8 +645,7 @@ defineExpose({
|
||||
<style lang="scss" scoped>
|
||||
// 路由页面模式样式
|
||||
.article-page-view {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
// background: #f5f7fa;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
@@ -675,7 +674,7 @@ defineExpose({
|
||||
padding: 40px 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
// box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
|
||||
@@ -60,6 +60,30 @@
|
||||
<span v-if="errors.endTime" class="error-msg">{{ errors.endTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">任务分类标签</label>
|
||||
<select
|
||||
v-model="selectedTagID"
|
||||
class="form-input form-select"
|
||||
@change="handleTagChange"
|
||||
>
|
||||
<option value="">请选择分类标签(可选)</option>
|
||||
<option
|
||||
v-for="tag in availableTags"
|
||||
:key="tag.tagID"
|
||||
:value="tag.tagID"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-if="selectedTag" class="selected-tag-preview">
|
||||
<div class="tag-badge" :style="{ backgroundColor: selectedTag.color || '#409eff' }">
|
||||
{{ selectedTag.name }}
|
||||
</div>
|
||||
<span v-if="selectedTag.description" class="tag-hint">{{ selectedTag.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 选择课程 -->
|
||||
@@ -273,6 +297,7 @@
|
||||
@confirm="handleUserSelectConfirm"
|
||||
@cancel="showUserSelector = false"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -283,9 +308,10 @@ import { courseApi } from '@/apis/study';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
import { userApi } from '@/apis/system';
|
||||
import { learningTaskApi } from '@/apis/study';
|
||||
import { resourceTagApi } from '@/apis/resource';
|
||||
import { GenericSelector } from '@/components/base';
|
||||
import type { TaskVO, Course, TaskItemVO } from '@/types/study';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { Resource, Tag } from '@/types/resource';
|
||||
import type { SysUser } from '@/types/user';
|
||||
|
||||
defineOptions({
|
||||
@@ -331,11 +357,14 @@ const errors = ref({
|
||||
const selectedCourses = ref<Course[]>([]);
|
||||
const selectedResources = ref<Resource[]>([]);
|
||||
const selectedUsers = ref<SysUser[]>([]);
|
||||
const selectedTagID = ref<string>('');
|
||||
const selectedTag = ref<Tag | null>(null);
|
||||
|
||||
// 可选数据
|
||||
const availableCourses = ref<Course[]>([]);
|
||||
const availableResources = ref<Resource[]>([]);
|
||||
const availableUsers = ref<SysUser[]>([]);
|
||||
const availableTags = ref<Tag[]>([]);
|
||||
|
||||
// 弹窗控制
|
||||
const showCourseSelector = ref(false);
|
||||
@@ -350,6 +379,7 @@ const resourceSearchKeyword = ref('');
|
||||
const courseLoading = ref(false);
|
||||
const resourceLoading = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const tagLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -357,7 +387,8 @@ onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadCourses(),
|
||||
loadResources(),
|
||||
loadUsers()
|
||||
loadUsers(),
|
||||
loadTags()
|
||||
]);
|
||||
|
||||
// 如果是编辑模式,加载任务数据并恢复选择
|
||||
@@ -398,6 +429,13 @@ async function loadTask() {
|
||||
const userIds = taskData.value.taskUsers.map(tu => tu.userID);
|
||||
selectedUsers.value = availableUsers.value.filter(u => userIds.includes(u.id));
|
||||
}
|
||||
|
||||
// 恢复标签选择
|
||||
if (taskData.value.taskTags && taskData.value.taskTags.length > 0) {
|
||||
const firstTag = taskData.value.taskTags[0];
|
||||
selectedTagID.value = firstTag.tagID || '';
|
||||
selectedTag.value = firstTag;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务失败:', error);
|
||||
@@ -671,6 +709,31 @@ function removeUser(index: number) {
|
||||
selectedUsers.value.splice(index, 1);
|
||||
}
|
||||
|
||||
// 加载标签列表
|
||||
async function loadTags() {
|
||||
tagLoading.value = true;
|
||||
try {
|
||||
const res = await resourceTagApi.getTagList({ tagType: 3 }); // 获取所有标签
|
||||
if (res.success && res.dataList) {
|
||||
// 只保留 tagType 为 3 的学习任务分类标签
|
||||
availableTags.value = res.dataList.filter(tag => tag.tagType === 3);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载标签失败:', error);
|
||||
} finally {
|
||||
tagLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签选择变化
|
||||
function handleTagChange() {
|
||||
if (selectedTagID.value) {
|
||||
selectedTag.value = availableTags.value.find(tag => tag.tagID === selectedTagID.value) || null;
|
||||
} else {
|
||||
selectedTag.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateTaskName() {
|
||||
if (!taskData.value.learningTask.name) {
|
||||
@@ -750,6 +813,9 @@ async function handleSubmit() {
|
||||
status: 0
|
||||
} as TaskItemVO));
|
||||
|
||||
// 组装任务标签数据
|
||||
taskData.value.taskTags = selectedTag.value ? [selectedTag.value] : [];
|
||||
|
||||
let res;
|
||||
if (props.taskId) {
|
||||
res = await learningTaskApi.updateTask(taskData.value);
|
||||
@@ -890,6 +956,38 @@ function handleCancel() {
|
||||
}
|
||||
}
|
||||
|
||||
.form-select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='%23606266' d='M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 14px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.selected-tag-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
|
||||
.tag-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
@@ -1185,5 +1283,6 @@ function handleCancel() {
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -31,13 +31,24 @@
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">热门资源推荐</h2>
|
||||
<div class="more-link">
|
||||
<div class="more-link" @click="handleMoreClick('hot')">
|
||||
<span>查看更多</span>
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-grid">
|
||||
<HotArticleCard v-for="item in 3" :key="item" />
|
||||
<div v-if="hotResourcesLoading" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<div v-else-if="hotResources.length > 0" class="article-grid">
|
||||
<HotArticleCard
|
||||
v-for="resource in hotResources"
|
||||
:key="resource.id || resource.resourceID"
|
||||
:resource="resource"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty-container">
|
||||
<p>暂无热门资源</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,13 +56,24 @@
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">思政新闻概览</h2>
|
||||
<div class="more-link">
|
||||
<div class="more-link" @click="handleMoreClick('ideological')">
|
||||
<span>查看更多</span>
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-grid">
|
||||
<IdeologicalArticleCard v-for="item in 3" :key="item" />
|
||||
<div v-if="ideologicalResourcesLoading" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<div v-else-if="ideologicalResources.length > 0" class="article-grid">
|
||||
<IdeologicalArticleCard
|
||||
v-for="resource in ideologicalResources"
|
||||
:key="resource.id || resource.resourceID"
|
||||
:resource="resource"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty-container">
|
||||
<p>暂无思政资源</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +81,7 @@
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">我的学习数据</h2>
|
||||
<div class="more-link">
|
||||
<div class="more-link" @click="handleMoreClick('learning')">
|
||||
<span>查看更多</span>
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
@@ -76,14 +98,26 @@ import { HotArticleCard, IdeologicalArticleCard } from '@/views/public/article';
|
||||
import { Carousel } from '@/components/base';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
import { bannerApi } from '@/apis/resource/banner';
|
||||
import { recommendApi } from '@/apis/homepage';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { Banner } from '@/types';
|
||||
import type { Banner, ResourceRecommendVO } from '@/types';
|
||||
import dangIcon from '@/assets/imgs/dang.svg';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 轮播数据
|
||||
const banners = ref<Banner[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 热门资源数据
|
||||
const hotResources = ref<ResourceRecommendVO[]>([]);
|
||||
const hotResourcesLoading = ref(false);
|
||||
|
||||
// 思政资源数据
|
||||
const ideologicalResources = ref<ResourceRecommendVO[]>([]);
|
||||
const ideologicalResourcesLoading = ref(false);
|
||||
|
||||
// 加载轮播图数据
|
||||
async function loadBanners() {
|
||||
try {
|
||||
@@ -104,9 +138,55 @@ async function loadBanners() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载热门资源数据
|
||||
async function loadHotResources() {
|
||||
try {
|
||||
hotResourcesLoading.value = true;
|
||||
const result = await recommendApi.getHotResources(3);
|
||||
|
||||
if (result.code === 200 && result.dataList) {
|
||||
hotResources.value = result.dataList;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载热门资源失败:', error);
|
||||
ElMessage.error('加载热门资源失败');
|
||||
} finally {
|
||||
hotResourcesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载思政资源数据
|
||||
async function loadIdeologicalResources() {
|
||||
try {
|
||||
ideologicalResourcesLoading.value = true;
|
||||
const result = await recommendApi.getIdeologicalResources(3);
|
||||
|
||||
if (result.code === 200 && result.dataList) {
|
||||
ideologicalResources.value = result.dataList;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载思政资源失败:', error);
|
||||
ElMessage.error('加载思政资源失败');
|
||||
} finally {
|
||||
ideologicalResourcesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMoreClick(type: string) {
|
||||
if (type === 'hot') {
|
||||
router.push('/resource-hot');
|
||||
} else if (type === 'ideological') {
|
||||
router.push('/resource-center');
|
||||
} else if (type === 'learning') {
|
||||
router.push('/learning-center');
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadBanners();
|
||||
loadHotResources();
|
||||
loadIdeologicalResources();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -208,5 +288,34 @@ onMounted(() => {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
min-height: 200px;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e4e7ed;
|
||||
border-top-color: #E7000B;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
716
schoolNewsWeb/src/views/user/resource-center/HotResourceView.vue
Normal file
716
schoolNewsWeb/src/views/user/resource-center/HotResourceView.vue
Normal file
@@ -0,0 +1,716 @@
|
||||
<template>
|
||||
<div class="hot-resource-view">
|
||||
<div class="hot-resource-view-head">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<button class="back-button" @click="goBack">
|
||||
<el-icon>
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
<span>返回</span>
|
||||
</button>
|
||||
<div class="header-info">
|
||||
<h1 class="page-title">
|
||||
<img src="@/assets/imgs/hot.svg" alt="热门" class="title-icon" />
|
||||
热门文章
|
||||
</h1>
|
||||
<p class="page-desc">根据浏览量为您推荐最受欢迎的文章内容</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排序和筛选 -->
|
||||
<div class="filter-controls">
|
||||
<!-- <el-select :model-value="sortType" @change="handleSortChange" placeholder="排序方式" style="width: 150px;">
|
||||
<el-option label="浏览量最多" value="viewCount" />
|
||||
<el-option label="点赞最多" value="likeCount" />
|
||||
<el-option label="收藏最多" value="collectCount" />
|
||||
<el-option label="最新发布" value="publishTime" />
|
||||
</el-select> -->
|
||||
|
||||
<el-select :model-value="selectedTagID" @change="handleTagChange" placeholder="文章分类" clearable
|
||||
style="width: 150px;">
|
||||
<el-option label="全部分类" :value="''" />
|
||||
<el-option v-for="tag in articleTags" :key="tag.tagID" :label="tag.name"
|
||||
:value="tag.tagID || ''" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">共找到</span>
|
||||
<span class="stat-value">{{ total }}</span>
|
||||
<span class="stat-label">篇热门文章</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总浏览量</span>
|
||||
<span class="stat-value">{{ formatNumber(totalViews) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 页面头部 -->
|
||||
|
||||
<!-- 文章列表 -->
|
||||
<div v-loading="loading" class="articles-container">
|
||||
<div class="articles-grid">
|
||||
<div v-for="(article, index) in articles" :key="article.resourceID" class="article-card"
|
||||
:class="{ 'top-rank': index < 3 }" @click="handleArticleClick(article)">
|
||||
<!-- 排名徽章 -->
|
||||
<div v-if="index < 3" class="rank-badge" :class="`rank-${index + 1}`">
|
||||
<span>{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div v-else class="rank-number">{{ (currentPage - 1) * pageSize + index + 1 }}</div>
|
||||
|
||||
<!-- 文章封面 -->
|
||||
<div class="article-cover">
|
||||
<img v-if="article.coverImage" :src="FILE_DOWNLOAD_URL + article.coverImage"
|
||||
:alt="article.title" />
|
||||
<div v-else class="cover-placeholder">
|
||||
<el-icon>
|
||||
<Document />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="cover-overlay">
|
||||
<span class="view-button">查看详情</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章信息 -->
|
||||
<div class="article-info">
|
||||
|
||||
<h3 class="article-title" :title="article.title">{{ article.title }}</h3>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div v-if="article.tagID" class="article-tag">
|
||||
{{ getTagName(article.tagID) }}
|
||||
</div>
|
||||
|
||||
<!-- 简介 -->
|
||||
<p class="article-summary">{{ article.summary || '暂无简介' }}</p>
|
||||
|
||||
<!-- 底部元信息 -->
|
||||
<div class="article-meta">
|
||||
<div class="meta-item">
|
||||
<img src="@/assets/imgs/hot.svg" alt="浏览" class="meta-icon" />
|
||||
<span>{{ formatNumber(article.viewCount) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<el-icon>
|
||||
<Star />
|
||||
</el-icon>
|
||||
<span>{{ formatNumber(article.likeCount) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<el-icon>
|
||||
<Star />
|
||||
</el-icon>
|
||||
<span>{{ formatNumber(article.collectCount) }}</span>
|
||||
</div>
|
||||
<div class="meta-item author">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>{{ article.author || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发布时间 -->
|
||||
<div class="article-time">
|
||||
{{ formatDate(article.publishTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && articles.length === 0" class="empty-state">
|
||||
<el-icon class="empty-icon">
|
||||
<Document />
|
||||
</el-icon>
|
||||
<p>暂无热门文章</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="total > 0" class="pagination-container">
|
||||
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total"
|
||||
:page-sizes="[9, 18, 27, 36]" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElIcon, ElSelect, ElOption, ElPagination } from 'element-plus';
|
||||
import { ArrowLeft, Document, Star, User } from '@element-plus/icons-vue';
|
||||
import { resourceApi, resourceTagApi } from '@/apis/resource';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
import type { Resource, Tag, ResourceSearchParams, PageParam } from '@/types';
|
||||
|
||||
defineOptions({
|
||||
name: 'HotResourceView'
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 数据状态
|
||||
const articles = ref<Resource[]>([]);
|
||||
const articleTags = ref<Tag[]>([]);
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(9);
|
||||
|
||||
// 筛选和排序
|
||||
const sortType = ref<'viewCount' | 'likeCount' | 'collectCount' | 'publishTime'>('viewCount');
|
||||
const selectedTagID = ref<string>('');
|
||||
|
||||
// 计算总浏览量
|
||||
const totalViews = computed(() => {
|
||||
return articles.value.reduce((sum, article) => sum + (article.viewCount || 0), 0);
|
||||
});
|
||||
|
||||
// 页面挂载
|
||||
onMounted(() => {
|
||||
loadTags();
|
||||
loadArticles();
|
||||
});
|
||||
|
||||
// 加载标签列表
|
||||
async function loadTags() {
|
||||
try {
|
||||
const result = await resourceTagApi.getTagList({ tagType: 1 }); // 1-文章分类标签
|
||||
if (result.success && result.dataList) {
|
||||
articleTags.value = result.dataList;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载标签失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载热门文章
|
||||
async function loadArticles() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const filter: ResourceSearchParams = {
|
||||
status: 1, // 只显示已发布的
|
||||
tagID: selectedTagID.value || undefined
|
||||
};
|
||||
|
||||
const pageParam: PageParam = {
|
||||
pageNumber: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
};
|
||||
|
||||
const result = await resourceApi.getResourcePage(pageParam, filter);
|
||||
|
||||
if (result.success && result.pageDomain?.dataList) {
|
||||
// 根据选择的排序方式进行排序
|
||||
let sortedArticles = [...result.pageDomain.dataList];
|
||||
|
||||
switch (sortType.value) {
|
||||
case 'viewCount':
|
||||
sortedArticles.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
|
||||
break;
|
||||
case 'likeCount':
|
||||
sortedArticles.sort((a, b) => (b.likeCount || 0) - (a.likeCount || 0));
|
||||
break;
|
||||
case 'collectCount':
|
||||
sortedArticles.sort((a, b) => (b.collectCount || 0) - (a.collectCount || 0));
|
||||
break;
|
||||
case 'publishTime':
|
||||
sortedArticles.sort((a, b) => {
|
||||
const timeA = a.publishTime ? new Date(a.publishTime).getTime() : 0;
|
||||
const timeB = b.publishTime ? new Date(b.publishTime).getTime() : 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
articles.value = sortedArticles;
|
||||
total.value = result.pageDomain.pageParam?.totalElements || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载热门文章失败:', error);
|
||||
ElMessage.error('加载热门文章失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签名称
|
||||
function getTagName(tagID: string): string {
|
||||
const tag = articleTags.value.find(t => t.tagID === tagID);
|
||||
return tag?.name || '未知分类';
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num?: number): string {
|
||||
if (num === undefined || num === null) return '0';
|
||||
if (num < 1000) return num.toString();
|
||||
if (num < 10000) return `${(num / 1000).toFixed(1)}k`;
|
||||
return `${(num / 10000).toFixed(1)}w`;
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr?: string): string {
|
||||
if (!dateStr) return '未知时间';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
|
||||
// 小于1小时
|
||||
if (diff < 3600000) {
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
return `${minutes}分钟前`;
|
||||
}
|
||||
// 小于1天
|
||||
if (diff < 86400000) {
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
return `${hours}小时前`;
|
||||
}
|
||||
// 小于7天
|
||||
if (diff < 604800000) {
|
||||
const days = Math.floor(diff / 86400000);
|
||||
return `${days}天前`;
|
||||
}
|
||||
|
||||
// 否则显示完整日期
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 处理排序变化
|
||||
function handleSortChange(value: string) {
|
||||
sortType.value = value as typeof sortType.value;
|
||||
currentPage.value = 1;
|
||||
loadArticles();
|
||||
}
|
||||
|
||||
// 处理标签筛选
|
||||
function handleTagChange(value: string) {
|
||||
selectedTagID.value = value;
|
||||
currentPage.value = 1;
|
||||
loadArticles();
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page;
|
||||
loadArticles();
|
||||
// 滚动到顶部
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 处理每页大小变化
|
||||
function handleSizeChange(size: number) {
|
||||
pageSize.value = size;
|
||||
currentPage.value = 1;
|
||||
loadArticles();
|
||||
}
|
||||
|
||||
// 点击文章卡片
|
||||
function handleArticleClick(article: Resource) {
|
||||
if (article.resourceID) {
|
||||
// 增加浏览次数
|
||||
resourceApi.incrementViewCount(article.resourceID);
|
||||
// 跳转到文章详情页
|
||||
router.push(`/article/show?articleId=${article.resourceID}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
router.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hot-resource-view {
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #FEF2F2 0%, #F9F9F9 100%);
|
||||
padding: 0 0 20px 0;
|
||||
|
||||
.hot-resource-view-head {
|
||||
height: 15%;
|
||||
}
|
||||
|
||||
// 页面头部
|
||||
.page-header {
|
||||
background: #FFFFFF;
|
||||
padding: 5px 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
// margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: #F9FAFB;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #4A5565;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #F3F4F6;
|
||||
color: #E7000B;
|
||||
border-color: #E7000B;
|
||||
}
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0 0 8px 0;
|
||||
|
||||
.title-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 14px;
|
||||
color: #6B7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 统计信息栏
|
||||
.stats-bar {
|
||||
|
||||
margin: 0 auto 24px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #E7000B;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 文章容器
|
||||
.articles-container {
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.articles-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 文章卡片
|
||||
.article-card {
|
||||
position: relative;
|
||||
background: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc((100% - 32px) / 3);
|
||||
height: 50%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(231, 0, 11, 0.15);
|
||||
|
||||
.cover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.top-rank {
|
||||
border: 2px solid;
|
||||
|
||||
&.article-card:nth-child(1) {
|
||||
border-color: #FFD700;
|
||||
}
|
||||
|
||||
&.article-card:nth-child(2) {
|
||||
border-color: #C0C0C0;
|
||||
}
|
||||
|
||||
&.article-card:nth-child(3) {
|
||||
border-color: #CD7F32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 排名徽章
|
||||
.rank-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
z-index: 10;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&.rank-1 {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
}
|
||||
|
||||
&.rank-2 {
|
||||
background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);
|
||||
}
|
||||
|
||||
&.rank-3 {
|
||||
background: linear-gradient(135deg, #CD7F32 0%, #B8722B 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 文章封面
|
||||
.article-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 45%;
|
||||
overflow: hidden;
|
||||
background: #F3F4F6;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.article-card:hover & img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48px;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.7) 100%);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.view-button {
|
||||
padding: 8px 20px;
|
||||
background: #E7000B;
|
||||
color: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 文章信息
|
||||
.article-info {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
height: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.article-tag {
|
||||
display: inline-flex;
|
||||
align-self: flex-start;
|
||||
padding: 4px 12px;
|
||||
background: #FEF2F2;
|
||||
border: 1px solid #FCA5A5;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
color: #E7000B;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.article-summary {
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #F3F4F6;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
|
||||
.meta-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&.author {
|
||||
margin-left: auto;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-time {
|
||||
font-size: 11px;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #9CA3AF;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
.pagination-container {
|
||||
height: 5%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -41,6 +41,7 @@ import { ref } from 'vue';
|
||||
import { ResourceSideBar, ResourceList, ResourceArticle } from './components';
|
||||
import { Search, CenterHead } from '@/components/base';
|
||||
import type { Resource, Tag } from '@/types/resource';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
|
||||
const showArticle = ref(false);
|
||||
const currentCategoryId = ref('tag_article_001');
|
||||
@@ -67,6 +68,8 @@ function handleListUpdated(list: Resource[]) {
|
||||
}
|
||||
|
||||
function handleResourceClick(resource: Resource) {
|
||||
// 增加浏览次数
|
||||
resourceApi.incrementViewCount(resource.resourceID || '');
|
||||
currentResourceId.value = resource.resourceID || '';
|
||||
showArticle.value = true;
|
||||
}
|
||||
@@ -100,6 +103,7 @@ async function handleArticleNavigate(direction: 'prev' | 'next', resourceId: str
|
||||
.resource-center-view {
|
||||
background: #F9F9F9;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
@@ -138,7 +142,6 @@ async function handleArticleNavigate(direction: 'prev' | 'next', resourceId: str
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
|
||||
@@ -26,9 +26,10 @@ import { ref, watch } from 'vue';
|
||||
import { ArticleShow } from '@/views/public/article';
|
||||
import { ResouceCollect, ResouceBottom } from '@/views/user/resource-center/components';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
import { userCollectionApi } from '@/apis/usercenter';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { CollectionType, type UserCollection } from '@/types';
|
||||
import { CollectionType, ResultDomain, type UserCollection } from '@/types';
|
||||
|
||||
interface Props {
|
||||
resourceId?: string;
|
||||
@@ -110,15 +111,15 @@ async function handleCollect(type: number) {
|
||||
collectionID: resourceID,
|
||||
collectionValue: type
|
||||
}
|
||||
const res = await resourceApi.resourceCollect(collect);
|
||||
if (res.success) {
|
||||
if (type === 1) {
|
||||
isCollected.value = true;
|
||||
ElMessage.success('收藏成功');
|
||||
} else if (type === -1) {
|
||||
isCollected.value = false;
|
||||
ElMessage.success('已取消收藏');
|
||||
}
|
||||
let res: ResultDomain<UserCollection> | null = null;
|
||||
if (type === 1) {
|
||||
res = await userCollectionApi.addCollection(collect);
|
||||
} else {
|
||||
res = await userCollectionApi.removeCollection(collect);
|
||||
}
|
||||
if (res && res.success) {
|
||||
isCollected.value = type === 1;
|
||||
ElMessage.success(type === 1 ? '收藏成功' : '已取消收藏');
|
||||
} else {
|
||||
ElMessage.error(type === 1 ? '收藏失败' : '取消收藏失败');
|
||||
}
|
||||
|
||||
@@ -27,12 +27,14 @@
|
||||
<div v-if="resources.length === 0 && !loading" class="empty">暂无数据</div>
|
||||
</div>
|
||||
|
||||
<div v-if="total > 0" class="pagination-wrapper">
|
||||
<div v-if="total > 0" class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="prev, pager, next, jumper"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -63,7 +65,7 @@ const resources = ref<Resource[]>([]);
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 10;
|
||||
const pageSize = ref(10);
|
||||
const listContainerRef = ref<HTMLElement>();
|
||||
|
||||
onMounted(() => {
|
||||
@@ -89,7 +91,7 @@ async function loadResources() {
|
||||
|
||||
const pageParam: PageParam = {
|
||||
pageNumber: currentPage.value,
|
||||
pageSize: pageSize
|
||||
pageSize: pageSize.value
|
||||
};
|
||||
|
||||
const res = await resourceApi.getResourcePage(pageParam, filter);
|
||||
@@ -119,6 +121,12 @@ function handlePageChange(page: number) {
|
||||
loadResources();
|
||||
}
|
||||
|
||||
function handleSizeChange(size: number) {
|
||||
pageSize.value = size;
|
||||
currentPage.value = 1;
|
||||
loadResources();
|
||||
}
|
||||
|
||||
function getResources() {
|
||||
return resources.value;
|
||||
}
|
||||
@@ -128,7 +136,7 @@ function getPageInfo() {
|
||||
}
|
||||
|
||||
async function loadNextPage() {
|
||||
const totalPages = Math.ceil(total.value / pageSize);
|
||||
const totalPages = Math.ceil(total.value / pageSize.value);
|
||||
if (currentPage.value < totalPages) {
|
||||
currentPage.value++;
|
||||
await loadResources();
|
||||
|
||||
@@ -243,13 +243,15 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-achievements {
|
||||
padding: 20px 0;
|
||||
// padding: 20px 0;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.achievements-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
height: 10%;
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
@@ -262,7 +264,7 @@ onMounted(() => {
|
||||
|
||||
.achievement-stats {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
gap: 20px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
@@ -289,10 +291,10 @@ onMounted(() => {
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
height: 5%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
margin: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -303,6 +305,8 @@ onMounted(() => {
|
||||
|
||||
.achievements-grid {
|
||||
display: grid;
|
||||
height: 80%;
|
||||
overflow-y: auto;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div
|
||||
class="filter-tab"
|
||||
v-for="filter in filters"
|
||||
:key="filter.key"
|
||||
:key="String(filter.key)"
|
||||
:class="{ active: activeFilter === filter.key }"
|
||||
@click="activeFilter = filter.key"
|
||||
>
|
||||
@@ -15,17 +15,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="favorites-grid">
|
||||
<div v-loading="loading" class="favorites-grid">
|
||||
<div class="favorite-item" v-for="item in filteredFavorites" :key="item.id">
|
||||
<div class="item-thumbnail">
|
||||
<img :src="item.thumbnail" :alt="item.title" />
|
||||
<div class="item-type">{{ item.typeName }}</div>
|
||||
<img v-if="getThumbnail(item)" :src="getThumbnail(item)" :alt="getTitle(item)" />
|
||||
<div v-else class="thumbnail-placeholder">
|
||||
<el-icon :size="48"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="item-type">{{ getTypeName(item) }}</div>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<p class="item-summary">{{ item.summary }}</p>
|
||||
<h3>{{ getTitle(item) }}</h3>
|
||||
<p class="item-summary">{{ getSummary(item) }}</p>
|
||||
<div class="item-footer">
|
||||
<span class="item-date">收藏于 {{ item.favoriteDate }}</span>
|
||||
<span class="item-date">收藏于 {{ formatDate(item.collectionTime) }}</span>
|
||||
<div class="item-actions">
|
||||
<el-button size="small" @click="viewItem(item)">查看</el-button>
|
||||
<el-button size="small" type="danger" @click="removeFavorite(item)">取消收藏</el-button>
|
||||
@@ -33,41 +36,173 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && filteredFavorites.length === 0" class="empty-state">
|
||||
<el-icon :size="64" class="empty-icon"><Star /></el-icon>
|
||||
<p>暂无收藏内容</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElButton, ElMessage, ElMessageBox, ElIcon } from 'element-plus';
|
||||
import { Document, Star } from '@element-plus/icons-vue';
|
||||
import { userCollectionApi } from '@/apis/usercenter';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
import type { UserCollectionVO } from '@/types';
|
||||
import { CollectionType } from '@/types/enums';
|
||||
|
||||
const activeFilter = ref('all');
|
||||
const favorites = ref<any[]>([]);
|
||||
defineOptions({
|
||||
name: 'MyFavoritesView'
|
||||
});
|
||||
|
||||
const filters = [
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
|
||||
const activeFilter = ref<'all' | number>('all');
|
||||
const favorites = ref<UserCollectionVO[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const filters: Array<{ key: 'all' | number; label: string }> = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'article', label: '文章' },
|
||||
{ key: 'video', label: '视频' },
|
||||
{ key: 'audio', label: '音频' },
|
||||
{ key: 'course', label: '课程' }
|
||||
{ key: CollectionType.RESOURCE, label: '资源' },
|
||||
{ key: CollectionType.COURSE, label: '课程' }
|
||||
];
|
||||
|
||||
const filteredFavorites = computed(() => {
|
||||
if (activeFilter.value === 'all') return favorites.value;
|
||||
return favorites.value.filter(item => item.type === activeFilter.value);
|
||||
return favorites.value.filter(item => item.collectionType === activeFilter.value);
|
||||
});
|
||||
|
||||
// 获取显示标题
|
||||
const getTitle = (item: UserCollectionVO): string => {
|
||||
if (item.collectionType === CollectionType.RESOURCE) {
|
||||
return item.title || '未命名资源';
|
||||
} else if (item.collectionType === CollectionType.COURSE) {
|
||||
return item.courseName || '未命名课程';
|
||||
}
|
||||
return '未知类型';
|
||||
};
|
||||
|
||||
// 获取显示简介
|
||||
const getSummary = (item: UserCollectionVO): string => {
|
||||
if (item.collectionType === CollectionType.RESOURCE) {
|
||||
return item.summary || '暂无简介';
|
||||
} else if (item.collectionType === CollectionType.COURSE) {
|
||||
return item.description || '暂无简介';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 获取显示缩略图
|
||||
const getThumbnail = (item: UserCollectionVO): string => {
|
||||
if (item.coverImage) {
|
||||
return `${FILE_DOWNLOAD_URL}${item.coverImage}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 获取类型名称
|
||||
const getTypeName = (item: UserCollectionVO): string => {
|
||||
if (item.collectionType === CollectionType.RESOURCE) {
|
||||
return '资源';
|
||||
} else if (item.collectionType === CollectionType.COURSE) {
|
||||
return '课程';
|
||||
}
|
||||
return '未知';
|
||||
};
|
||||
|
||||
// 获取当前用户ID
|
||||
const currentUser = computed(() => store.getters['auth/user']);
|
||||
|
||||
onMounted(() => {
|
||||
// TODO: 加载收藏数据
|
||||
loadFavorites();
|
||||
});
|
||||
|
||||
function viewItem(item: any) {
|
||||
// TODO: 跳转到详情页
|
||||
// 加载收藏列表
|
||||
async function loadFavorites() {
|
||||
if (!currentUser.value?.id) {
|
||||
ElMessage.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await userCollectionApi.getUserCollections(currentUser.value.id);
|
||||
|
||||
if (result.success && result.dataList) {
|
||||
// 后端已返回扁平化的VO,包含详情,无需再次查询
|
||||
favorites.value = result.dataList;
|
||||
} else {
|
||||
favorites.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载收藏列表失败:', error);
|
||||
ElMessage.error('加载收藏列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeFavorite(item: any) {
|
||||
// TODO: 取消收藏
|
||||
ElMessage.success('已取消收藏');
|
||||
// 查看详情
|
||||
function viewItem(item: UserCollectionVO) {
|
||||
if (item.collectionType === CollectionType.RESOURCE && item.resourceID) {
|
||||
router.push(`/article/show?articleId=${item.resourceID}`);
|
||||
} else if (item.collectionType === CollectionType.COURSE && item.courseID) {
|
||||
// TODO: 跳转到课程详情页
|
||||
ElMessage.info('课程详情页开发中');
|
||||
}
|
||||
}
|
||||
|
||||
// 取消收藏
|
||||
async function removeFavorite(item: UserCollectionVO) {
|
||||
if (!currentUser.value?.id || !item.collectionType || !item.collectionID) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要取消收藏吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
const result = await userCollectionApi.removeCollection(
|
||||
{
|
||||
collectionType: item.collectionType,
|
||||
collectionID: item.collectionID
|
||||
}
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('已取消收藏');
|
||||
// 从列表中移除
|
||||
favorites.value = favorites.value.filter(f => f.id !== item.id);
|
||||
} else {
|
||||
ElMessage.error(result.message || '取消收藏失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('取消收藏失败:', error);
|
||||
ElMessage.error('取消收藏失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr?: string): string {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -172,6 +307,7 @@ function removeFavorite(item: any) {
|
||||
margin-bottom: 16px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -193,5 +329,35 @@ function removeFavorite(item: any) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.thumbnail-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #999;
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 16px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -51,24 +51,27 @@ const menus = computed(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-center-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
// overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-card-wrapper {
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
|
||||
height: 75%;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ onMounted(async () => {
|
||||
.user-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 10px;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
min-height: 190px;
|
||||
padding: 20px;
|
||||
|
||||
Reference in New Issue
Block a user