419 lines
12 KiB
Vue
419 lines
12 KiB
Vue
<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>
|
||
|