成就等界面接口调整
This commit is contained in:
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