Files
schoolNews/schoolNewsWeb/src/views/admin/manage/content/ColumnManagementView.vue
2025-11-17 15:16:11 +08:00

1273 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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