成就等界面接口调整

This commit is contained in:
2025-10-31 19:13:21 +08:00
parent 9ad9507a72
commit 16754b527e
61 changed files with 4748 additions and 592 deletions

View File

@@ -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>

View 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>

View File

@@ -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 {

View File

@@ -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 ? '收藏失败' : '取消收藏失败');
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -87,7 +87,6 @@ onMounted(async () => {
.user-card {
background: #FFFFFF;
border-radius: 10px;
max-width: 1200px;
width: 100%;
min-height: 190px;
padding: 20px;