2025-10-16 18:03:46 +08:00
|
|
|
|
<template>
|
2025-10-28 19:04:35 +08:00
|
|
|
|
<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"
|
2025-11-21 16:49:37 +08:00
|
|
|
|
onkeydown=""
|
2025-10-28 19:04:35 +08:00
|
|
|
|
clearable
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-10-16 18:03:46 +08:00
|
|
|
|
|
|
|
|
|
|
<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" />
|
2025-10-20 15:08:41 +08:00
|
|
|
|
<el-table-column prop="publishTime" label="发布日期" width="120" />
|
2025-10-16 18:03:46 +08:00
|
|
|
|
<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>
|
2025-10-24 18:28:35 +08:00
|
|
|
|
<el-table-column label="操作" width="250" fixed="right">
|
2025-10-16 18:03:46 +08:00
|
|
|
|
<template #default="{ row }">
|
2025-10-20 15:08:41 +08:00
|
|
|
|
<el-button size="small" @click="viewArticle(row)">查看</el-button>
|
2025-10-24 18:28:35 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:type="getActionButtonType(row.status)"
|
|
|
|
|
|
@click="changeArticleStatus(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ getActionButtonText(row.status) }}
|
|
|
|
|
|
</el-button>
|
2025-10-16 18:03:46 +08:00
|
|
|
|
<el-button size="small" @click="editArticle(row)">编辑</el-button>
|
2025-10-20 15:08:41 +08:00
|
|
|
|
<el-button size="small" type="danger" @click="deleteArticle()">删除</el-button>
|
2025-10-16 18:03:46 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
2025-10-27 13:42:34 +08:00
|
|
|
|
v-model:current-page="pageParam.pageNumber"
|
|
|
|
|
|
v-model:page-size="pageParam.pageSize"
|
2025-10-16 18:03:46 +08:00
|
|
|
|
:total="total"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
/>
|
2025-10-20 15:08:41 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 文章查看弹窗 -->
|
|
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2025-10-28 19:04:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</AdminLayout>
|
2025-10-16 18:03:46 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-10-28 19:04:35 +08:00
|
|
|
|
import { AdminLayout } from '@/views/admin';
|
|
|
|
|
|
|
|
|
|
|
|
defineOptions({
|
|
|
|
|
|
name: 'ArticleManagementView'
|
|
|
|
|
|
});
|
2025-10-16 18:03:46 +08:00
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
|
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
|
2025-10-18 18:19:19 +08:00
|
|
|
|
import { useRouter } from 'vue-router';
|
2025-10-27 16:21:00 +08:00
|
|
|
|
import { resourceApi, resourceTagApi } from '@/apis/resource'
|
|
|
|
|
|
import type { PageParam, ResourceSearchParams, Resource, Tag } from '@/types';
|
2025-10-27 17:29:25 +08:00
|
|
|
|
import { ArticleShowView } from '@/views/public/article';
|
2025-10-24 18:28:35 +08:00
|
|
|
|
import { ArticleStatus } from '@/types/enums';
|
2025-10-16 18:03:46 +08:00
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
const router = useRouter();
|
2025-10-16 18:03:46 +08:00
|
|
|
|
const searchKeyword = ref('');
|
2025-10-20 15:08:41 +08:00
|
|
|
|
const pageParam = ref<PageParam>({
|
2025-10-27 13:42:34 +08:00
|
|
|
|
pageNumber: 1,
|
|
|
|
|
|
pageSize: 10
|
2025-10-20 15:08:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
const filter = ref<ResourceSearchParams>({
|
|
|
|
|
|
keyword: searchKeyword.value
|
|
|
|
|
|
});
|
|
|
|
|
|
const total = ref<number>(0);
|
|
|
|
|
|
const articles = ref<Resource[]>([]);
|
|
|
|
|
|
const showViewDialog = ref(false);
|
|
|
|
|
|
const currentArticle = ref<any>(null);
|
2025-10-27 16:21:00 +08:00
|
|
|
|
const categoryList = ref<Tag[]>([]); // 改为使用Tag类型(tagType=1表示文章分类)
|
2025-10-16 18:03:46 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadArticles();
|
2025-10-20 15:08:41 +08:00
|
|
|
|
loadCategories();
|
2025-10-16 18:03:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-20 15:08:41 +08:00
|
|
|
|
async function loadCategories() {
|
|
|
|
|
|
try {
|
2025-10-27 16:21:00 +08:00
|
|
|
|
// 使用新的标签API获取文章分类标签(tagType=1)
|
|
|
|
|
|
const res = await resourceTagApi.getTagsByType(1); // 1 = 文章分类标签
|
2025-10-20 15:08:41 +08:00
|
|
|
|
if (res.success && res.dataList) {
|
|
|
|
|
|
categoryList.value = res.dataList;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载分类列表失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadArticles() {
|
|
|
|
|
|
const res = await resourceApi.getResourcePage(pageParam.value, filter.value);
|
|
|
|
|
|
if (res.success) {
|
|
|
|
|
|
articles.value = res.pageDomain?.dataList || [];
|
2025-10-24 18:28:35 +08:00
|
|
|
|
total.value = res.pageDomain?.pageParam.totalElements || 0;
|
2025-10-20 15:08:41 +08:00
|
|
|
|
}
|
2025-10-16 18:03:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
function showCreateDialog() {
|
|
|
|
|
|
// 尝试跳转
|
|
|
|
|
|
router.push('/article/add')
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
console.log('路由跳转成功!');
|
|
|
|
|
|
console.log('跳转后路由:', router.currentRoute.value.fullPath);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
console.error('路由跳转失败:', err);
|
|
|
|
|
|
});
|
2025-10-16 18:03:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDataCollection() {
|
|
|
|
|
|
// TODO: 数据采集功能
|
|
|
|
|
|
ElMessage.info('数据采集功能开发中');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 15:08:41 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 18:03:46 +08:00
|
|
|
|
function editArticle(row: any) {
|
2025-10-20 15:08:41 +08:00
|
|
|
|
router.push('/article/add?id=' + row.resourceID);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 18:28:35 +08:00
|
|
|
|
async function changeArticleStatus(row: Resource) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// status: 0-草稿, 1-已发布, 2-已下架
|
|
|
|
|
|
if (row.status === ArticleStatus.DRAFT || row.status === ArticleStatus.OFFLINE) {
|
|
|
|
|
|
// 草稿或下架状态 -> 发布
|
|
|
|
|
|
const res = await resourceApi.publishResource(row.resourceID!);
|
|
|
|
|
|
if (res.success) {
|
|
|
|
|
|
ElMessage.success('发布成功');
|
|
|
|
|
|
loadArticles();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error('发布失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (row.status === ArticleStatus.PUBLISHED) {
|
|
|
|
|
|
// 已发布状态 -> 下架
|
|
|
|
|
|
const res = await resourceApi.unpublishResource(row.resourceID!);
|
|
|
|
|
|
if (res.success) {
|
|
|
|
|
|
ElMessage.success('下架成功');
|
|
|
|
|
|
loadArticles();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error('下架失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('操作失败:', error);
|
|
|
|
|
|
ElMessage.error('操作失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 15:08:41 +08:00
|
|
|
|
function handleEditFromView() {
|
|
|
|
|
|
if (currentArticle.value?.resourceID) {
|
|
|
|
|
|
showViewDialog.value = false;
|
|
|
|
|
|
router.push('/article/add?id=' + currentArticle.value.resourceID);
|
|
|
|
|
|
}
|
2025-10-16 18:03:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 15:08:41 +08:00
|
|
|
|
function deleteArticle() {
|
2025-10-16 18:03:46 +08:00
|
|
|
|
// TODO: 删除文章
|
2025-10-20 15:08:41 +08:00
|
|
|
|
ElMessage.info('删除功能开发中');
|
2025-10-16 18:03:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 18:28:35 +08:00
|
|
|
|
function getStatusType(status: number) {
|
|
|
|
|
|
const typeMap: Record<number, any> = {
|
|
|
|
|
|
[ArticleStatus.DRAFT]: 'info',
|
|
|
|
|
|
[ArticleStatus.PUBLISHED]: 'success',
|
2025-11-19 15:11:30 +08:00
|
|
|
|
[ArticleStatus.OFFLINE]: 'warning',
|
|
|
|
|
|
[ArticleStatus.FAILED]: 'danger'
|
2025-10-16 18:03:46 +08:00
|
|
|
|
};
|
|
|
|
|
|
return typeMap[status] || 'info';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 18:28:35 +08:00
|
|
|
|
function getStatusText(status: number) {
|
|
|
|
|
|
const textMap: Record<number, string> = {
|
|
|
|
|
|
[ArticleStatus.DRAFT]: '草稿',
|
|
|
|
|
|
[ArticleStatus.PUBLISHED]: '已发布',
|
2025-11-19 15:11:30 +08:00
|
|
|
|
[ArticleStatus.OFFLINE]: '已下架',
|
|
|
|
|
|
[ArticleStatus.FAILED]: '审核失败'
|
2025-10-16 18:03:46 +08:00
|
|
|
|
};
|
2025-10-24 18:28:35 +08:00
|
|
|
|
return textMap[status] || '未知';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getActionButtonType(status: number) {
|
|
|
|
|
|
// 草稿或下架状态显示主要按钮(发布), 已发布状态显示警告按钮(下架)
|
2025-11-19 15:11:30 +08:00
|
|
|
|
if (status === ArticleStatus.DRAFT || status === ArticleStatus.OFFLINE || status === ArticleStatus.FAILED) {
|
2025-10-24 18:28:35 +08:00
|
|
|
|
return 'primary';
|
|
|
|
|
|
} else if (status === ArticleStatus.PUBLISHED) {
|
|
|
|
|
|
return 'warning';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getActionButtonText(status: number) {
|
|
|
|
|
|
// 草稿或下架状态显示"发布", 已发布状态显示"下架"
|
2025-11-19 15:11:30 +08:00
|
|
|
|
if (status === ArticleStatus.DRAFT || status === ArticleStatus.OFFLINE || status === ArticleStatus.FAILED) {
|
2025-10-24 18:28:35 +08:00
|
|
|
|
return '发布';
|
|
|
|
|
|
} else if (status === ArticleStatus.PUBLISHED) {
|
|
|
|
|
|
return '下架';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '操作';
|
2025-10-16 18:03:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleSizeChange(val: number) {
|
2025-10-27 13:42:34 +08:00
|
|
|
|
pageParam.value.pageSize = val;
|
2025-10-16 18:03:46 +08:00
|
|
|
|
loadArticles();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleCurrentChange(val: number) {
|
2025-10-27 13:42:34 +08:00
|
|
|
|
pageParam.value.pageNumber = val;
|
2025-10-16 18:03:46 +08:00
|
|
|
|
loadArticles();
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.article-management {
|
2025-10-28 19:04:35 +08:00
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
border-radius: 14px;
|
2025-10-16 18:03:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|