Files
schoolNews/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue
2026-01-14 11:25:51 +08:00

419 lines
12 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="article-management">
<div class="action-bar">
<el-button type="primary" @click="showCreateDialog">+ 新增文章</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索文章..."
style="width: 300px"
clearable
@keyup.enter="handleSearch"
@clear="handleSearch"
>
<template #append>
<el-button @click="handleSearch">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
</div>
<el-table :data="articles" style="width: 100%">
<el-table-column prop="title" label="文章标题" min-width="200" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="author" label="作者" width="120" />
<el-table-column prop="publishTime" label="发布日期" width="120" />
<el-table-column prop="views" label="阅读量" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="isInKnowledge" label="知识库" width="100">
<template #default="{ row }">
<el-tag :type="row.isInKnowledge ? 'success' : 'info'" size="small">
{{ row.isInKnowledge ? '已导入' : '未导入' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewArticle(row)">查看</el-button>
<el-button
size="small"
:type="getActionButtonType(row.status)"
@click="changeArticleStatus(row)"
>
{{ getActionButtonText(row.status) }}
</el-button>
<el-button
v-if="row.status === ResourceStatus.SENSITIVE_FAILED && canForcePublish"
size="small"
type="warning"
@click="forcePublishArticle(row)"
>
强制发布
</el-button>
<el-button size="small" @click="editArticle(row)">编辑</el-button>
<el-button
size="small"
type="success"
:disabled="row.isInKnowledge || row.status !== 1"
:loading="importingIds.has(row.resourceID)"
@click="importToKnowledge(row)"
>
{{ row.isInKnowledge ? '已导入' : '导入知识库' }}
</el-button>
<el-button size="small" type="danger" @click="deleteArticle(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 文章查看弹窗 -->
<ArticleShowView
v-model="showViewDialog"
:as-dialog="true"
title="文章详情"
width="900px"
:article-data="currentArticle"
:category-list="categoryList"
:show-edit-button="true"
@edit="handleEditFromView"
@close="showViewDialog = false"
/>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'ArticleManagementView'
});
import { ref, onMounted, computed } from 'vue';
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage, ElMessageBox, ElIcon } from 'element-plus';
import { Search } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router';
import { resourceApi, resourceTagApi } from '@/apis/resource'
import type { PageParam, ResourceSearchParams, Resource, Tag } from '@/types';
import { ArticleShowView } from '@/views/public/article';
import { ResourceStatus } from '@/types/enums';
import { usePermission } from '@/utils/permission';
const router = useRouter();
// 权限检查
const { hasPermission } = usePermission();
const canForcePublish = computed(() => hasPermission('admin:article:force-publish'));
const searchKeyword = ref('');
const pageParam = ref<PageParam>({
pageNumber: 1,
pageSize: 10
});
const filter = ref<ResourceSearchParams>({
title: searchKeyword.value
});
const total = ref<number>(0);
const articles = ref<Resource[]>([]);
const showViewDialog = ref(false);
const currentArticle = ref<any>(null);
const categoryList = ref<Tag[]>([]); // 改为使用Tag类型tagType=1表示文章分类
const importingIds = ref<Set<string>>(new Set()); // 正在导入的文章ID集合
onMounted(() => {
loadArticles();
loadCategories();
});
async function loadCategories() {
try {
// 使用新的标签API获取文章分类标签tagType=1
const res = await resourceTagApi.getTagsByType(1); // 1 = 文章分类标签
if (res.success && res.dataList) {
categoryList.value = res.dataList;
}
} catch (error) {
console.error('加载分类列表失败:', error);
}
}
async function loadArticles() {
filter.value.title = searchKeyword.value;
const res = await resourceApi.getResourcePage(pageParam.value, filter.value);
if (res.success) {
articles.value = res.pageDomain?.dataList || [];
total.value = res.pageDomain?.pageParam.totalElements || 0;
}
}
function handleSearch() {
pageParam.value.pageNumber = 1; // 搜索时重置到第一页
loadArticles();
}
function showCreateDialog() {
// 尝试跳转
router.push('/article/add')
.then(() => {
// console.log('路由跳转成功!');
// console.log('跳转后路由:', router.currentRoute.value.fullPath);
})
.catch(err => {
console.error('路由跳转失败:', err);
});
}
function handleDataCollection() {
// TODO: 数据采集功能
ElMessage.info('数据采集功能开发中');
}
async function viewArticle(row: any) {
try {
const res = await resourceApi.getResourceById(row.resourceID);
if (res.success && res.data) {
// 将 ResourceVO 转换为 ArticleShowView 期望的格式
const resourceVO = res.data;
currentArticle.value = {
...resourceVO.resource,
tags: resourceVO.tags || []
};
showViewDialog.value = true;
} else {
ElMessage.error('获取文章详情失败');
}
} catch (error) {
ElMessage.error('获取文章详情失败');
console.error(error);
}
}
function editArticle(row: any) {
router.push('/article/add?id=' + row.resourceID);
}
async function changeArticleStatus(row: Resource) {
try {
// status: 0-草稿, 1-已发布, 2-已下架, 3-审核中, 4-敏感词未通过
if (row.status === ResourceStatus.DRAFT || row.status === ResourceStatus.OFFLINE || row.status === ResourceStatus.SENSITIVE_FAILED) {
// 草稿或下架状态 -> 发布
const res = await resourceApi.publishResource(row.resourceID!);
if (res.success) {
ElMessage.success('发布成功');
loadArticles();
} else {
ElMessage.error('发布失败');
}
} else if (row.status === ResourceStatus.PUBLISHED) {
// 已发布状态 -> 下架
const res = await resourceApi.unpublishResource(row.resourceID!);
if (res.success) {
ElMessage.success('下架成功');
loadArticles();
} else {
ElMessage.error('下架失败');
}
}
} catch (error) {
console.error('操作失败:', error);
ElMessage.error('操作失败');
}
}
async function forcePublishArticle(row: Resource) {
try {
await ElMessageBox.confirm(
`确定要强制发布文章「${row.title}」吗?此操作将跳过敏感词校验。`,
'强制发布确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
const res = await resourceApi.forcePublishResource(row.resourceID!);
if (res.success) {
ElMessage.success('强制发布成功');
loadArticles();
} else {
ElMessage.error(res.message || '强制发布失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('强制发布失败:', error);
ElMessage.error('强制发布失败');
}
}
}
function handleEditFromView() {
if (currentArticle.value?.resourceID) {
showViewDialog.value = false;
router.push('/article/add?id=' + currentArticle.value.resourceID);
}
}
async function deleteArticle(row: Resource) {
try {
await ElMessageBox.confirm(
`确定要删除文章「${row.title}」吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
const res = await resourceApi.deleteResource(row.resourceID!);
if (res.success) {
ElMessage.success('删除成功');
loadArticles();
} else {
ElMessage.error(res.message || '删除失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error);
ElMessage.error('删除失败');
}
}
}
async function importToKnowledge(row: Resource) {
if (!row.resourceID) return;
// 检查文章状态
if (row.status !== ResourceStatus.PUBLISHED) {
ElMessage.warning('只有已发布的文章才能导入知识库');
return;
}
if (row.isInKnowledge) {
ElMessage.info('文章已导入知识库');
return;
}
try {
await ElMessageBox.confirm(
`确定要将文章「${row.title}」导入知识库吗?`,
'导入确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
);
importingIds.value.add(row.resourceID);
const res = await resourceApi.importToKnowledge(row.resourceID);
if (res.success) {
ElMessage.success('导入知识库成功');
loadArticles();
} else {
ElMessage.error(res.message || '导入知识库失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('导入知识库失败:', error);
ElMessage.error('导入知识库失败');
}
} finally {
importingIds.value.delete(row.resourceID);
}
}
function getStatusType(status: number) {
const typeMap: Record<number, any> = {
[ResourceStatus.DRAFT]: 'info',
[ResourceStatus.PUBLISHED]: 'success',
[ResourceStatus.OFFLINE]: 'warning',
[ResourceStatus.REVIEWING]: 'primary',
[ResourceStatus.SENSITIVE_FAILED]: 'danger'
};
return typeMap[status] || 'info';
}
function getStatusText(status: number) {
const textMap: Record<number, string> = {
[ResourceStatus.DRAFT]: '草稿',
[ResourceStatus.PUBLISHED]: '已发布',
[ResourceStatus.OFFLINE]: '已下架',
[ResourceStatus.REVIEWING]: '审核中',
[ResourceStatus.SENSITIVE_FAILED]: '敏感词未通过'
};
return textMap[status] || '未知';
}
function getActionButtonType(status: number) {
// 草稿、下架或敏感词未通过状态显示主要按钮(发布), 已发布状态显示警告按钮(下架)
if (status === ResourceStatus.DRAFT || status === ResourceStatus.OFFLINE || status === ResourceStatus.SENSITIVE_FAILED) {
return 'primary';
} else if (status === ResourceStatus.PUBLISHED) {
return 'warning';
}
return '';
}
function getActionButtonText(status: number) {
// 草稿、下架或敏感词未通过状态显示"发布", 已发布状态显示"下架", 审核中状态不可操作
if (status === ResourceStatus.DRAFT || status === ResourceStatus.OFFLINE || status === ResourceStatus.SENSITIVE_FAILED) {
return '发布';
} else if (status === ResourceStatus.PUBLISHED) {
return '下架';
} else if (status === ResourceStatus.REVIEWING) {
return '审核中';
}
return '操作';
}
function handleSizeChange(val: number) {
pageParam.value.pageSize = val;
loadArticles();
}
function handleCurrentChange(val: number) {
pageParam.value.pageNumber = val;
loadArticles();
}
</script>
<style lang="scss" scoped>
.article-management {
background: #FFFFFF;
padding: 24px;
border-radius: 14px;
}
.action-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.el-table {
margin-bottom: 20px;
}
</style>