1273 lines
35 KiB
Vue
1273 lines
35 KiB
Vue
<template>
|
||
<AdminLayout
|
||
title="栏目管理"
|
||
subtitle="管理首页展示的推荐内容"
|
||
>
|
||
<div class="column-management">
|
||
<!-- 标签页 -->
|
||
<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>
|
||
</div>
|
||
|
||
<!-- 内容卡片 -->
|
||
<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>
|
||
<div class="stat-actions">操作</div>
|
||
</div>
|
||
|
||
<div
|
||
v-for="(article, index) in hotArticles"
|
||
:key="article.resource?.resourceID"
|
||
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">
|
||
<span class="article-title" :title="article.resource?.title">{{ article.resource?.title }}</span>
|
||
</div>
|
||
<div class="stat-tag">
|
||
<span v-if="article.resource?.tagID" class="tag-name">{{ getTagName(article.resource.tagID) }}</span>
|
||
<span v-else class="tag-empty">-</span>
|
||
</div>
|
||
<div class="stat-author">
|
||
<span>{{ article.resource?.author || '-' }}</span>
|
||
</div>
|
||
<div class="stat-views">
|
||
<div class="stat-number hot">
|
||
<img src="@/assets/imgs/hot.svg" alt="浏览" class="stat-icon" />
|
||
{{ formatNumber(article.resource?.viewCount) }}
|
||
</div>
|
||
</div>
|
||
<div class="stat-likes">
|
||
<div class="stat-number">
|
||
{{ formatNumber(article.resource?.likeCount) }}
|
||
</div>
|
||
</div>
|
||
<div class="stat-time">
|
||
<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>
|
||
</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>
|
||
</template>
|
||
</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>
|
||
</template>
|
||
</el-dialog>
|
||
</AdminLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
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';
|
||
import { AdminLayout } from '@/views/admin';
|
||
import { resourceRecommendApi, resourceApi, resourceTagApi } from '@/apis/resource';
|
||
import type { ResourceRecommendVO, Resource, ResourceVO, Tag } from '@/types';
|
||
|
||
defineOptions({
|
||
name: 'ColumnManagementView'
|
||
});
|
||
|
||
// 当前激活的标签页
|
||
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[]>([]);
|
||
|
||
// 热门文章列表
|
||
const hotArticles = ref<ResourceVO[]>([]);
|
||
const hotArticleLimit = ref<number>(20);
|
||
|
||
// 添加至TOP的加载状态
|
||
const addingToTop = ref<Set<string>>(new Set());
|
||
|
||
// 添加至思政的加载状态
|
||
const addingToIdeological = ref<Set<string>>(new Set());
|
||
|
||
// 标签列表
|
||
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
|
||
});
|
||
|
||
// 挂载时加载标签数据和默认tab的数据
|
||
onMounted(() => {
|
||
loadTags();
|
||
loadTabData(activeTab.value);
|
||
});
|
||
|
||
// 监听tab切换,加载对应数据
|
||
watch(activeTab, (newTab) => {
|
||
loadTabData(newTab);
|
||
});
|
||
|
||
// 加载标签列表
|
||
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 {
|
||
// 获取已发布的文章,按浏览量排序
|
||
const result = await resourceApi.getResourcePageOrderByViewCount(
|
||
{
|
||
pageNumber: 1,
|
||
pageSize: hotArticleLimit.value
|
||
},
|
||
{
|
||
status: 1 // 只显示已发布的
|
||
}
|
||
);
|
||
|
||
if (result.success && result.pageDomain?.dataList) {
|
||
// 按浏览量降序排序
|
||
hotArticles.value = result.pageDomain.dataList
|
||
.sort((a, b) => (b.resource?.viewCount || 0) - (a.resource?.viewCount || 0));
|
||
}
|
||
} 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}`;
|
||
}
|
||
|
||
// 显示添加推荐对话框
|
||
async function showAddResourceDialog(recommendType: 1 | 2) {
|
||
currentRecommendType.value = recommendType;
|
||
selectedResources.value = [];
|
||
searchKeyword.value = '';
|
||
addDialogVisible.value = true;
|
||
await searchResources();
|
||
}
|
||
|
||
// 搜索资源
|
||
async function searchResources() {
|
||
searchLoading.value = true;
|
||
try {
|
||
const result = await resourceApi.getResourcePageOrderByViewCount(
|
||
{ 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);
|
||
|
||
availableResources.value = result.pageDomain.dataList
|
||
.map(vo => vo.resource)
|
||
.filter(r => r && !existingIds.includes(r.resourceID)) as Resource[];
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索资源失败:', error);
|
||
ElMessage.error('搜索资源失败');
|
||
} finally {
|
||
searchLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 切换资源选择
|
||
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!);
|
||
ElMessage.success('删除成功');
|
||
await refreshCurrentTab();
|
||
} catch (error: any) {
|
||
if (error !== 'cancel') {
|
||
console.error('删除失败:', error);
|
||
ElMessage.error('删除失败');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查资源是否已在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);
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.column-management {
|
||
// 标签页列表
|
||
.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 {
|
||
background: #FFFFFF;
|
||
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);
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.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;
|
||
min-width: 150px;
|
||
}
|
||
|
||
// 热门文章列表
|
||
.hot-articles-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-right: 24px;
|
||
min-height: 400px;
|
||
}
|
||
|
||
.statistics-header {
|
||
display: grid;
|
||
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
|
||
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;
|
||
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
|
||
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;
|
||
}
|
||
|
||
.stat-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
// 对话框样式
|
||
.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;
|
||
}
|
||
}
|
||
</style>
|