Files
schoolNews/schoolNewsWeb/src/views/admin/manage/content/ColumnManagementView.vue

1273 lines
35 KiB
Vue
Raw Normal View History

2025-10-16 18:03:46 +08:00
<template>
2025-10-28 19:04:35 +08:00
<AdminLayout
2025-10-31 19:13:21 +08:00
title="栏目管理"
subtitle="管理首页展示的推荐内容"
2025-10-28 19:04:35 +08:00
>
<div class="column-management">
2025-10-31 19:13:21 +08:00
<!-- 标签页 -->
<div class="tab-list">
<button
class="tab-button"
:class="{ active: activeTab === 'hot-articles' }"
@click="activeTab = 'hot-articles'"
>
<img src="@/assets/imgs/hot.svg" alt="统计" class="tab-icon" />
热门文章统计
</button>
<button
class="tab-button"
:class="{ active: activeTab === 'top-resources' }"
@click="activeTab = 'top-resources'"
>
TOP资源推荐
</button>
<button
class="tab-button"
:class="{ active: activeTab === 'ideological' }"
@click="activeTab = 'ideological'"
>
思政新闻概览
</button>
2025-10-28 19:04:35 +08:00
</div>
2025-10-16 18:03:46 +08:00
2025-10-31 19:13:21 +08:00
<!-- 内容卡片 -->
<div class="content-card">
<!-- 热门文章统计 -->
<div v-if="activeTab === 'hot-articles'" class="tab-content">
<div class="header-section">
<div class="header-info">
<h2 class="section-title">热门文章统计</h2>
<p class="section-desc">查看和分析热门文章的访问统计数据</p>
</div>
<div class="filter-controls">
<el-select v-model="hotArticleLimit" @change="loadHotArticles" placeholder="显示数量">
<el-option label="前10名" :value="10" />
<el-option label="前20名" :value="20" />
<el-option label="前50名" :value="50" />
<el-option label="全部" :value="999" />
</el-select>
</div>
</div>
<!-- 热门文章列表 -->
<div v-loading="loading" class="hot-articles-list">
<div class="statistics-header">
<div class="stat-rank">排名</div>
<div class="stat-title">文章标题</div>
<div class="stat-tag">分类</div>
<div class="stat-author">作者</div>
<div class="stat-views">浏览量</div>
<div class="stat-likes">点赞数</div>
<div class="stat-time">发布时间</div>
2025-11-17 15:16:11 +08:00
<div class="stat-actions">操作</div>
2025-10-31 19:13:21 +08:00
</div>
<div
v-for="(article, index) in hotArticles"
2025-11-17 15:16:11 +08:00
:key="article.resource?.resourceID"
2025-10-31 19:13:21 +08:00
class="hot-article-row"
:class="{ 'top-three': index < 3 }"
>
<div class="stat-rank">
<div class="rank-badge" :class="`rank-${index + 1}`">
{{ index + 1 }}
</div>
</div>
<div class="stat-title">
2025-11-17 15:16:11 +08:00
<span class="article-title" :title="article.resource?.title">{{ article.resource?.title }}</span>
2025-10-31 19:13:21 +08:00
</div>
<div class="stat-tag">
2025-11-17 15:16:11 +08:00
<span v-if="article.resource?.tagID" class="tag-name">{{ getTagName(article.resource.tagID) }}</span>
2025-10-31 19:13:21 +08:00
<span v-else class="tag-empty">-</span>
</div>
<div class="stat-author">
2025-11-17 15:16:11 +08:00
<span>{{ article.resource?.author || '-' }}</span>
2025-10-31 19:13:21 +08:00
</div>
<div class="stat-views">
<div class="stat-number hot">
<img src="@/assets/imgs/hot.svg" alt="浏览" class="stat-icon" />
2025-11-17 15:16:11 +08:00
{{ formatNumber(article.resource?.viewCount) }}
2025-10-31 19:13:21 +08:00
</div>
</div>
<div class="stat-likes">
<div class="stat-number">
2025-11-17 15:16:11 +08:00
{{ formatNumber(article.resource?.likeCount) }}
2025-10-31 19:13:21 +08:00
</div>
</div>
<div class="stat-time">
2025-11-17 15:16:11 +08:00
<span class="time-text">{{ formatDate(article.resource?.publishTime) }}</span>
</div>
<div class="stat-actions">
<button
v-if="article.resource?.resourceID && !article.isTopRecommend"
class="add-to-top-btn"
@click="addToTopRecommends(article.resource.resourceID)"
:disabled="!!article.resource?.resourceID && addingToTop.has(article.resource.resourceID)"
>
{{ article.resource?.resourceID && addingToTop.has(article.resource.resourceID) ? '添加中...' : '添加至TOP' }}
</button>
<span v-else-if="article.isTopRecommend" class="already-top">已在TOP</span>
<!-- 添加至思政 -->
<button
v-if="article.resource?.resourceID && !article.isIdeologicalRecommend"
class="add-to-ideological-btn"
@click="addToIdeologicalRecommends(article.resource.resourceID)"
:disabled="!!article.resource?.resourceID && addingToIdeological.has(article.resource.resourceID)"
>
{{ article.resource?.resourceID && addingToIdeological.has(article.resource.resourceID) ? '添加中...' : '添加至思政' }}
</button>
<span v-else-if="article.isIdeologicalRecommend" class="already-ideological">已在思政</span>
2025-10-31 19:13:21 +08:00
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && hotArticles.length === 0" class="empty-state">
<p>暂无热门文章数据</p>
</div>
</div>
</div>
<!-- TOP资源推荐 -->
<div v-if="activeTab === 'top-resources'" class="tab-content">
<div class="header-section">
<div class="header-info">
<h2 class="section-title">TOP资源推荐</h2>
<p class="section-desc">管理首页展示的TOP资源推荐列表</p>
</div>
<button class="add-button" @click="showAddResourceDialog(1)">
<img src="@/assets/imgs/plus.svg" alt="添加" />
手动添加推荐
</button>
</div>
<!-- 推荐列表 -->
<div v-loading="loading" class="recommend-list">
<div
v-for="(item, index) in topRecommends"
:key="item.id"
class="recommend-card"
>
<div class="recommend-content">
<div class="rank-badge">{{ index + 1 }}</div>
<div class="recommend-info">
<h3 class="recommend-title">{{ item.title }}</h3>
<div v-if="item.tagID" class="tag-badge">
{{ getTagName(item.tagID) }}
</div>
</div>
</div>
<div class="recommend-actions">
<button
class="action-btn"
:disabled="index === 0"
@click="moveUp(item, index, 1)"
>
上移
</button>
<button
class="action-btn"
:disabled="index === topRecommends.length - 1"
@click="moveDown(item, index, 1)"
>
下移
</button>
<button class="action-btn icon-btn" @click="editRecommend(item)">
<img src="@/assets/imgs/edit.svg" alt="编辑" />
</button>
<button class="action-btn icon-btn" @click="deleteRecommend(item)">
<img src="@/assets/imgs/trashbin.svg" alt="删除" />
</button>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && topRecommends.length === 0" class="empty-state">
<p>暂无TOP资源推荐</p>
<button class="add-button-secondary" @click="showAddResourceDialog(1)">
添加推荐
</button>
</div>
</div>
</div>
<!-- 思政新闻概览 -->
<div v-if="activeTab === 'ideological'" class="tab-content">
<div class="header-section">
<div class="header-info">
<h2 class="section-title">思政新闻概览</h2>
<p class="section-desc">管理首页展示的思政新闻推荐列表</p>
</div>
<button class="add-button" @click="showAddResourceDialog(2)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 3.33334V12.6667" stroke="white" stroke-width="1.33333" stroke-linecap="round"/>
<path d="M3.33334 8H12.6667" stroke="white" stroke-width="1.33333" stroke-linecap="round"/>
</svg>
手动添加推荐
</button>
</div>
<!-- 推荐列表 -->
<div v-loading="loading" class="recommend-list">
<div
v-for="(item, index) in ideologicalRecommends"
:key="item.id"
class="recommend-card"
>
<div class="recommend-content">
<div class="rank-badge">{{ index + 1 }}</div>
<div class="recommend-info">
<h3 class="recommend-title">{{ item.title }}</h3>
<div v-if="item.tagID" class="tag-badge">
{{ getTagName(item.tagID) }}
</div>
</div>
</div>
<div class="recommend-actions">
<button
class="action-btn"
:disabled="index === 0"
@click="moveUp(item, index, 2)"
>
上移
</button>
<button
class="action-btn"
:disabled="index === ideologicalRecommends.length - 1"
@click="moveDown(item, index, 2)"
>
下移
</button>
<button class="action-btn icon-btn" @click="editRecommend(item)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 11.3333V14H4.66667L12.3333 6.33333L9.66667 3.66667L2 11.3333Z" stroke="#E7000B" stroke-width="1.33333"/>
<path d="M11 2L14 5" stroke="#E7000B" stroke-width="1.33333" stroke-linecap="round"/>
</svg>
</button>
<button class="action-btn icon-btn" @click="deleteRecommend(item)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 4H14" stroke="#E7000B" stroke-width="1.33333" stroke-linecap="round"/>
<path d="M5.33333 4V2.66667C5.33333 2.29848 5.63181 2 6 2H10C10.3682 2 10.6667 2.29848 10.6667 2.66667V4" stroke="#E7000B" stroke-width="1.33333"/>
<path d="M3.33334 4V13.3333C3.33334 13.7015 3.63182 14 4.00001 14H12C12.3682 14 12.6667 13.7015 12.6667 13.3333V4" stroke="#E7000B" stroke-width="1.33333"/>
</svg>
</button>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && ideologicalRecommends.length === 0" class="empty-state">
<p>暂无思政新闻推荐</p>
<button class="add-button-secondary" @click="showAddResourceDialog(2)">
添加推荐
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 添加推荐对话框 -->
<el-dialog
v-model="addDialogVisible"
:title="`添加${currentRecommendType === 1 ? 'TOP资源' : '思政新闻'}推荐`"
width="800px"
:close-on-click-modal="false"
>
<div class="add-dialog-content">
<el-input
v-model="searchKeyword"
placeholder="搜索文章标题或内容..."
clearable
@input="searchResources"
class="search-input"
>
<template #prefix>
<el-icon><Search /></el-icon>
2025-10-28 19:04:35 +08:00
</template>
2025-10-31 19:13:21 +08:00
</el-input>
<div v-loading="searchLoading" class="resource-list">
<div
v-for="resource in availableResources"
:key="resource.resourceID"
class="resource-item"
:class="{ selected: resource.resourceID && selectedResources.includes(resource.resourceID) }"
@click="resource.resourceID && toggleResourceSelection(resource.resourceID)"
>
<el-checkbox
:model-value="resource.resourceID && selectedResources.includes(resource.resourceID)"
@change="resource.resourceID && toggleResourceSelection(resource.resourceID)"
/>
<div class="resource-info">
<h4>{{ resource.title }}</h4>
<p>{{ resource.summary }}</p>
</div>
</div>
<!-- 空状态 -->
<div v-if="!searchLoading && availableResources.length === 0" class="empty-state">
<p>未找到相关文章</p>
</div>
</div>
<div class="dialog-footer">
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="confirmAddRecommends"
:disabled="selectedResources.length === 0"
>
确认添加 ({{ selectedResources.length }})
</el-button>
</div>
</div>
</el-dialog>
<!-- 编辑推荐对话框 -->
<el-dialog
v-model="editDialogVisible"
title="编辑推荐"
width="600px"
:close-on-click-modal="false"
>
<el-form :model="editForm" label-width="100px">
<el-form-item label="文章标题">
<el-input v-model="editForm.title" disabled />
</el-form-item>
<el-form-item label="推荐理由">
<el-input
v-model="editForm.reason"
type="textarea"
:rows="3"
placeholder="请输入推荐理由(可选)"
/>
</el-form-item>
<el-form-item label="排序号">
<el-input-number v-model="editForm.orderNum" :min="1" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmEditRecommend">确认</el-button>
2025-10-28 19:04:35 +08:00
</template>
2025-10-31 19:13:21 +08:00
</el-dialog>
2025-10-28 19:04:35 +08:00
</AdminLayout>
2025-10-16 18:03:46 +08:00
</template>
<script setup lang="ts">
2025-10-31 19:13:21 +08:00
import { ref, onMounted, watch } from 'vue';
import { ElDialog, ElInput, ElButton, ElCheckbox, ElIcon, ElForm, ElFormItem, ElInputNumber, ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus';
import { Search } from '@element-plus/icons-vue';
2025-10-28 19:04:35 +08:00
import { AdminLayout } from '@/views/admin';
2025-10-31 19:13:21 +08:00
import { resourceRecommendApi, resourceApi, resourceTagApi } from '@/apis/resource';
2025-11-17 15:16:11 +08:00
import type { ResourceRecommendVO, Resource, ResourceVO, Tag } from '@/types';
2025-10-28 19:04:35 +08:00
defineOptions({
name: 'ColumnManagementView'
});
2025-10-16 18:03:46 +08:00
2025-10-31 19:13:21 +08:00
// 当前激活的标签页
const activeTab = ref<'hot-articles' | 'top-resources' | 'ideological'>('top-resources');
// 加载状态
const loading = ref(false);
const searchLoading = ref(false);
// 推荐列表
const topRecommends = ref<ResourceRecommendVO[]>([]);
const ideologicalRecommends = ref<ResourceRecommendVO[]>([]);
2025-10-16 18:03:46 +08:00
2025-10-31 19:13:21 +08:00
// 热门文章列表
2025-11-17 15:16:11 +08:00
const hotArticles = ref<ResourceVO[]>([]);
2025-10-31 19:13:21 +08:00
const hotArticleLimit = ref<number>(20);
2025-11-17 15:16:11 +08:00
// 添加至TOP的加载状态
const addingToTop = ref<Set<string>>(new Set());
// 添加至思政的加载状态
const addingToIdeological = ref<Set<string>>(new Set());
2025-10-31 19:13:21 +08:00
// 标签列表
const tags = ref<Tag[]>([]);
// 数据加载标记(避免重复加载)
const loadedTabs = ref<Set<string>>(new Set());
// 添加推荐对话框
const addDialogVisible = ref(false);
const currentRecommendType = ref<1 | 2>(1); // 1-TOP资源2-思政新闻
const searchKeyword = ref('');
const availableResources = ref<Resource[]>([]);
const selectedResources = ref<string[]>([]);
// 编辑推荐对话框
const editDialogVisible = ref(false);
const editForm = ref({
id: '',
title: '',
reason: '',
orderNum: 1
});
2025-11-17 15:16:11 +08:00
// 挂载时加载标签数据和默认tab的数据
2025-10-16 18:03:46 +08:00
onMounted(() => {
2025-10-31 19:13:21 +08:00
loadTags();
loadTabData(activeTab.value);
});
// 监听tab切换加载对应数据
watch(activeTab, (newTab) => {
loadTabData(newTab);
2025-10-16 18:03:46 +08:00
});
2025-10-31 19:13:21 +08:00
// 加载标签列表
async function loadTags() {
try {
const result = await resourceTagApi.getTagList({});
if (result.success && result.dataList) {
tags.value = result.dataList;
}
} catch (error) {
console.error('加载标签失败:', error);
}
}
// 加载tab数据
async function loadTabData(tab: string) {
// 如果已经加载过,不再重复加载
if (loadedTabs.value.has(tab)) {
return;
}
loading.value = true;
try {
if (tab === 'top-resources') {
// 加载TOP资源推荐
const topResult = await resourceRecommendApi.getRecommendsByType(1);
if (topResult.success && topResult.dataList) {
topRecommends.value = topResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
}
loadedTabs.value.add(tab);
} else if (tab === 'ideological') {
// 加载思政新闻推荐
const ideologicalResult = await resourceRecommendApi.getRecommendsByType(2);
if (ideologicalResult.success && ideologicalResult.dataList) {
ideologicalRecommends.value = ideologicalResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
}
loadedTabs.value.add(tab);
} else if (tab === 'hot-articles') {
// 热门文章统计
await loadHotArticles();
loadedTabs.value.add(tab);
}
} catch (error) {
console.error('加载数据失败:', error);
ElMessage.error('加载数据失败');
} finally {
loading.value = false;
}
}
// 加载热门文章
async function loadHotArticles() {
loading.value = true;
try {
// 获取已发布的文章,按浏览量排序
2025-11-17 15:16:11 +08:00
const result = await resourceApi.getResourcePageOrderByViewCount(
2025-10-31 19:13:21 +08:00
{
pageNumber: 1,
pageSize: hotArticleLimit.value
},
{
status: 1 // 只显示已发布的
}
);
if (result.success && result.pageDomain?.dataList) {
// 按浏览量降序排序
hotArticles.value = result.pageDomain.dataList
2025-11-17 15:16:11 +08:00
.sort((a, b) => (b.resource?.viewCount || 0) - (a.resource?.viewCount || 0));
2025-10-31 19:13:21 +08:00
}
} catch (error) {
console.error('加载热门文章失败:', error);
ElMessage.error('加载热门文章失败');
} finally {
loading.value = false;
}
}
// 刷新当前tab的数据
async function refreshCurrentTab() {
// 移除已加载标记,强制重新加载
loadedTabs.value.delete(activeTab.value);
await loadTabData(activeTab.value);
}
// 获取标签名称
function getTagName(tagID: string): string {
const tag = tags.value.find(t => t.tagID === tagID);
return tag?.name || '未知标签';
}
// 格式化数字(千位分隔)
function formatNumber(num?: number): string {
if (num === undefined || num === null) return '0';
return num.toLocaleString('zh-CN');
}
// 格式化日期
function formatDate(dateStr?: string): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
2025-10-16 18:03:46 +08:00
}
2025-10-31 19:13:21 +08:00
// 显示添加推荐对话框
async function showAddResourceDialog(recommendType: 1 | 2) {
currentRecommendType.value = recommendType;
selectedResources.value = [];
searchKeyword.value = '';
addDialogVisible.value = true;
await searchResources();
2025-10-16 18:03:46 +08:00
}
2025-10-31 19:13:21 +08:00
// 搜索资源
async function searchResources() {
searchLoading.value = true;
try {
2025-11-17 15:16:11 +08:00
const result = await resourceApi.getResourcePageOrderByViewCount(
2025-10-31 19:13:21 +08:00
{ pageNumber: 1, pageSize: 50 },
{
keyword: searchKeyword.value,
status: 1 // 只显示已发布的
}
);
if (result.success && result.pageDomain?.dataList) {
// 过滤掉已经推荐的资源
const existingIds = currentRecommendType.value === 1
? topRecommends.value.map(r => r.resourceID)
: ideologicalRecommends.value.map(r => r.resourceID);
2025-11-17 15:16:11 +08:00
availableResources.value = result.pageDomain.dataList
.map(vo => vo.resource)
.filter(r => r && !existingIds.includes(r.resourceID)) as Resource[];
2025-10-31 19:13:21 +08:00
}
} catch (error) {
console.error('搜索资源失败:', error);
ElMessage.error('搜索资源失败');
} finally {
searchLoading.value = false;
}
2025-10-16 18:03:46 +08:00
}
2025-10-31 19:13:21 +08:00
// 切换资源选择
function toggleResourceSelection(resourceID: string) {
const index = selectedResources.value.indexOf(resourceID);
if (index > -1) {
selectedResources.value.splice(index, 1);
} else {
selectedResources.value.push(resourceID);
}
}
// 确认添加推荐
async function confirmAddRecommends() {
try {
await resourceRecommendApi.batchAddRecommends(
selectedResources.value,
currentRecommendType.value
);
ElMessage.success('添加推荐成功');
addDialogVisible.value = false;
await refreshCurrentTab();
} catch (error) {
console.error('添加推荐失败:', error);
ElMessage.error('添加推荐失败');
}
}
// 上移
async function moveUp(item: ResourceRecommendVO, index: number, type: 1 | 2) {
if (index === 0) return;
const list = type === 1 ? topRecommends.value : ideologicalRecommends.value;
const prevItem = list[index - 1];
try {
// 交换排序号
await resourceRecommendApi.updateRecommendOrder(item.id!, prevItem.orderNum!);
await resourceRecommendApi.updateRecommendOrder(prevItem.id!, item.orderNum!);
await refreshCurrentTab();
ElMessage.success('上移成功');
} catch (error) {
console.error('上移失败:', error);
ElMessage.error('上移失败');
}
}
// 下移
async function moveDown(item: ResourceRecommendVO, index: number, type: 1 | 2) {
const list = type === 1 ? topRecommends.value : ideologicalRecommends.value;
if (index === list.length - 1) return;
const nextItem = list[index + 1];
try {
// 交换排序号
await resourceRecommendApi.updateRecommendOrder(item.id!, nextItem.orderNum!);
await resourceRecommendApi.updateRecommendOrder(nextItem.id!, item.orderNum!);
await refreshCurrentTab();
ElMessage.success('下移成功');
} catch (error) {
console.error('下移失败:', error);
ElMessage.error('下移失败');
}
}
// 编辑推荐
function editRecommend(item: ResourceRecommendVO) {
editForm.value = {
id: item.id || '',
title: item.title || '',
reason: item.reason || '',
orderNum: item.orderNum || 1
};
editDialogVisible.value = true;
}
// 确认编辑推荐
async function confirmEditRecommend() {
try {
await resourceRecommendApi.updateRecommend({
id: editForm.value.id,
reason: editForm.value.reason,
orderNum: editForm.value.orderNum
});
ElMessage.success('编辑成功');
editDialogVisible.value = false;
await refreshCurrentTab();
} catch (error) {
console.error('编辑失败:', error);
ElMessage.error('编辑失败');
}
}
// 删除推荐
async function deleteRecommend(item: ResourceRecommendVO) {
try {
await ElMessageBox.confirm(
`确定要删除推荐"${item.title}"吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
await resourceRecommendApi.deleteRecommend(item.id!);
2025-10-16 18:03:46 +08:00
ElMessage.success('删除成功');
2025-10-31 19:13:21 +08:00
await refreshCurrentTab();
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除失败:', error);
ElMessage.error('删除失败');
}
}
2025-10-16 18:03:46 +08:00
}
2025-11-17 15:16:11 +08:00
// 检查资源是否已在TOP推荐中
function isInTopRecommends(resourceID?: string): boolean {
if (!resourceID) return false;
return topRecommends.value.some(r => r.resourceID === resourceID);
}
// 检查资源是否已在思政推荐中
function isInIdeologicalRecommends(resourceID?: string): boolean {
if (!resourceID) return false;
return ideologicalRecommends.value.some(r => r.resourceID === resourceID);
}
// 根据ResourceVO获取resourceID
function getResourceID(articleVO: ResourceVO): string | undefined {
return articleVO.resource?.resourceID;
}
// 添加至TOP推荐
async function addToTopRecommends(resourceID?: string) {
if (!resourceID) return;
addingToTop.value.add(resourceID);
try {
await resourceRecommendApi.batchAddRecommends([resourceID], 1);
ElMessage.success('添加至TOP推荐成功');
// 重新加载TOP推荐列表
loadedTabs.value.delete('top-resources');
const topResult = await resourceRecommendApi.getRecommendsByType(1);
if (topResult.success && topResult.dataList) {
topRecommends.value = topResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
}
// 重新加载热门文章统计以更新按钮状态
loadedTabs.value.delete('hot-articles');
await loadHotArticles();
} catch (error) {
console.error('添加至TOP推荐失败:', error);
ElMessage.error('添加至TOP推荐失败');
} finally {
addingToTop.value.delete(resourceID);
}
}
// 添加至思政推荐
async function addToIdeologicalRecommends(resourceID?: string) {
if (!resourceID) return;
addingToIdeological.value.add(resourceID);
try {
await resourceRecommendApi.batchAddRecommends([resourceID], 2);
ElMessage.success('添加至思政推荐成功');
// 重新加载思政推荐列表
loadedTabs.value.delete('ideological');
const ideologicalResult = await resourceRecommendApi.getRecommendsByType(2);
if (ideologicalResult.success && ideologicalResult.dataList) {
ideologicalRecommends.value = ideologicalResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
}
// 重新加载热门文章统计以更新按钮状态
loadedTabs.value.delete('hot-articles');
await loadHotArticles();
} catch (error) {
console.error('添加至思政推荐失败:', error);
ElMessage.error('添加至思政推荐失败');
} finally {
addingToIdeological.value.delete(resourceID);
}
}
2025-10-16 18:03:46 +08:00
</script>
<style lang="scss" scoped>
.column-management {
2025-10-31 19:13:21 +08:00
// 标签页列表
.tab-list {
display: flex;
gap: 8px;
margin-bottom: 32px;
background: #F9FAFB;
padding: 4px;
border-radius: 10px;
width: fit-content;
}
.tab-button {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
height: 27px;
background: transparent;
border: 1px solid transparent;
border-radius: 14px;
font-size: 14px;
font-weight: 500;
color: #0A0A0A;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
.tab-icon {
width: 16px;
height: 16px;
}
&:hover {
background: rgba(255, 255, 255, 0.5);
}
&.active {
background: #FFFFFF;
border-color: rgba(0, 0, 0, 0.1);
}
}
// 内容卡片
.content-card {
2025-10-28 19:04:35 +08:00
background: #FFFFFF;
2025-10-31 19:13:21 +08:00
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 24px 0 24px 24px;
}
.tab-content {
display: flex;
flex-direction: column;
gap: 48px;
}
// 头部区域
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 24px;
}
.header-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.section-title {
font-size: 16px;
font-weight: 400;
color: #101828;
margin: 0;
}
.section-desc {
font-size: 14px;
color: #4A5565;
margin: 0;
}
.add-button {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 16px;
background: #E7000B;
border: none;
border-radius: 8px;
color: #FFFFFF;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #c00009;
}
svg {
flex-shrink: 0;
}
}
// 推荐列表
.recommend-list {
display: flex;
flex-direction: column;
gap: 12px;
padding-right: 24px;
min-height: 300px;
}
.recommend-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #F9FAFB;
border: 1px solid rgba(0, 0, 0, 0.1);
2025-10-28 19:04:35 +08:00
border-radius: 14px;
2025-10-16 18:03:46 +08:00
}
2025-10-31 19:13:21 +08:00
.recommend-content {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
min-width: 0;
}
.rank-badge {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #E7000B;
border-radius: 50%;
color: #FFFFFF;
font-size: 16px;
font-weight: 400;
flex-shrink: 0;
}
.recommend-info {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 0;
}
.recommend-title {
font-size: 16px;
font-weight: 400;
color: #101828;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tag-badge {
display: inline-flex;
padding: 4px 9px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
font-size: 12px;
color: #0A0A0A;
width: fit-content;
}
.recommend-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
height: 32px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
color: #0A0A0A;
cursor: pointer;
transition: all 0.2s;
&:hover:not(:disabled) {
background: #F9FAFB;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.icon-btn {
width: 38px;
padding: 0;
}
}
// 空状态
.empty-state, .empty-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 60px 20px;
color: #4A5565;
text-align: center;
p {
margin: 0;
font-size: 14px;
}
}
.add-button-secondary {
padding: 8px 16px;
background: #FFFFFF;
border: 1px solid #E7000B;
border-radius: 8px;
color: #E7000B;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #FEF2F2;
}
}
// 筛选控件
.filter-controls {
display: flex;
gap: 12px;
2025-11-17 15:16:11 +08:00
min-width: 150px;
2025-10-31 19:13:21 +08:00
}
// 热门文章列表
.hot-articles-list {
display: flex;
flex-direction: column;
padding-right: 24px;
min-height: 400px;
}
.statistics-header {
display: grid;
2025-11-17 15:16:11 +08:00
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
2025-10-31 19:13:21 +08:00
gap: 16px;
padding: 12px 16px;
background: #F9FAFB;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
font-size: 14px;
font-weight: 500;
color: #4A5565;
}
.hot-article-row {
display: grid;
2025-11-17 15:16:11 +08:00
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
2025-10-31 19:13:21 +08:00
gap: 16px;
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
align-items: center;
transition: all 0.2s;
&:hover {
background: #F9FAFB;
}
&.top-three {
background: linear-gradient(90deg, rgba(231, 0, 11, 0.02) 0%, rgba(231, 0, 11, 0) 100%);
}
}
.stat-rank, .stat-title, .stat-tag, .stat-author, .stat-views, .stat-likes, .stat-time {
display: flex;
align-items: center;
}
2025-11-17 15:16:11 +08:00
.stat-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
2025-10-31 19:13:21 +08:00
.rank-badge {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 16px;
font-weight: 500;
color: #FFFFFF;
background: #9CA3AF;
&.rank-1 {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
box-shadow: 0 4px 8px rgba(255, 215, 0, 0.3);
font-size: 18px;
font-weight: 600;
}
&.rank-2 {
background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);
box-shadow: 0 4px 8px rgba(192, 192, 192, 0.3);
font-size: 17px;
font-weight: 600;
}
&.rank-3 {
background: linear-gradient(135deg, #CD7F32 0%, #B8722B 100%);
box-shadow: 0 4px 8px rgba(205, 127, 50, 0.3);
font-size: 16px;
font-weight: 600;
}
}
.article-title {
font-size: 14px;
color: #101828;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
&:hover {
color: #E7000B;
}
}
.tag-name {
display: inline-flex;
padding: 4px 8px;
background: #F9FAFB;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
font-size: 12px;
color: #4A5565;
}
.tag-empty {
color: #9CA3AF;
}
.stat-number {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
color: #101828;
&.hot {
color: #E7000B;
}
.stat-icon {
width: 16px;
height: 16px;
}
}
.time-text {
font-size: 13px;
color: #6B7280;
}
2025-11-17 15:16:11 +08:00
.add-to-top-btn {
padding: 6px 12px;
background: #E7000B;
border: none;
border-radius: 6px;
color: #FFFFFF;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&:hover:not(:disabled) {
background: #c00009;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.already-top {
font-size: 13px;
color: #10B981;
font-weight: 500;
}
.add-to-ideological-btn {
padding: 6px 12px;
background: #E7000B;
border: none;
border-radius: 6px;
color: #FFFFFF;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&:hover:not(:disabled) {
background: #c00009;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.already-ideological {
font-size: 13px;
color: #10B981;
font-weight: 500;
}
2025-10-31 19:13:21 +08:00
}
// 对话框样式
.add-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
.search-input {
width: 100%;
}
.resource-list {
max-height: 400px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-item {
display: flex;
gap: 12px;
padding: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #F9FAFB;
}
&.selected {
background: #FEF2F2;
border-color: #E7000B;
}
.resource-info {
flex: 1;
h4 {
margin: 0 0 4px;
font-size: 14px;
font-weight: 500;
color: #101828;
}
p {
margin: 0;
font-size: 12px;
color: #4A5565;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
2025-10-16 18:03:46 +08:00
}
</style>