Compare commits
2 Commits
1697a71693
...
4547dc7777
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4547dc7777 | ||
|
|
f5ef075fb1 |
86
src/api/kb/favorite.ts
Normal file
86
src/api/kb/favorite.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { request } from '@/api/request';
|
||||||
|
|
||||||
|
/** 资源类型 */
|
||||||
|
export type ResourceType = 'document' | 'faq';
|
||||||
|
|
||||||
|
/** 收藏记录接口 */
|
||||||
|
export interface Favorite {
|
||||||
|
id: number;
|
||||||
|
created_at: string;
|
||||||
|
resource_type: ResourceType;
|
||||||
|
resource_id: number;
|
||||||
|
resource_name: string;
|
||||||
|
remarks: string;
|
||||||
|
is_deleted: boolean;
|
||||||
|
resource_data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收藏列表响应 */
|
||||||
|
export interface FavoriteListResponse {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
data: Favorite[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取收藏列表参数 */
|
||||||
|
export interface FetchFavoriteListParams {
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
resource_type?: ResourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收藏请求参数 */
|
||||||
|
export interface CollectParams {
|
||||||
|
resource_type: ResourceType;
|
||||||
|
resource_id: number;
|
||||||
|
remarks?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消收藏参数 */
|
||||||
|
export interface UncollectParams {
|
||||||
|
resource_type: ResourceType;
|
||||||
|
resource_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API响应包装类型 */
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取收藏列表
|
||||||
|
*/
|
||||||
|
export async function fetchFavoriteList(
|
||||||
|
params: FetchFavoriteListParams = {}
|
||||||
|
): Promise<ApiResponse<FavoriteListResponse>> {
|
||||||
|
return request.get<ApiResponse<FavoriteListResponse>>('/Kb/v1/favorite/list', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收藏资源
|
||||||
|
*/
|
||||||
|
export async function collectResource(
|
||||||
|
data: CollectParams
|
||||||
|
): Promise<ApiResponse<Favorite>> {
|
||||||
|
return request.post<ApiResponse<Favorite>>('/Kb/v1/favorite/collect', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消收藏
|
||||||
|
*/
|
||||||
|
export async function uncollectResource(
|
||||||
|
data: UncollectParams
|
||||||
|
): Promise<ApiResponse<string>> {
|
||||||
|
return request.post<ApiResponse<string>>('/Kb/v1/favorite/uncollect', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 资源类型选项 */
|
||||||
|
export const resourceTypeOptions = [
|
||||||
|
{ label: '文档', value: 'document' },
|
||||||
|
{ label: 'FAQ', value: 'faq' },
|
||||||
|
];
|
||||||
@@ -12,6 +12,162 @@ export interface ReviewStatsPayload {
|
|||||||
need_my_review_unreviewed_total?: number;
|
need_my_review_unreviewed_total?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 文档资源类型 */
|
||||||
|
export interface DocumentResource {
|
||||||
|
id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
doc_no: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
category_id: number;
|
||||||
|
sub_category: string;
|
||||||
|
author_id: number;
|
||||||
|
author_name: string;
|
||||||
|
reviewer_id: number;
|
||||||
|
reviewer_name: string;
|
||||||
|
reviewed_at: string | null;
|
||||||
|
published_at: string | null;
|
||||||
|
publisher_id: number;
|
||||||
|
view_count: number;
|
||||||
|
like_count: number;
|
||||||
|
comment_count: number;
|
||||||
|
download_count: number;
|
||||||
|
version: string;
|
||||||
|
version_notes: string;
|
||||||
|
tags: string;
|
||||||
|
attachments: string | null;
|
||||||
|
related_docs: string | null;
|
||||||
|
metadata: string | null;
|
||||||
|
keywords: string;
|
||||||
|
remarks: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** FAQ资源类型 */
|
||||||
|
export interface FaqResource {
|
||||||
|
id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
faq_no: string;
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
category_id: number;
|
||||||
|
sub_category: string;
|
||||||
|
problem_type: string;
|
||||||
|
solution: string;
|
||||||
|
process_steps: string;
|
||||||
|
prerequisites: string;
|
||||||
|
author_id: number;
|
||||||
|
author_name: string;
|
||||||
|
reviewer_id: number;
|
||||||
|
reviewer_name: string;
|
||||||
|
reviewed_at: string | null;
|
||||||
|
published_at: string | null;
|
||||||
|
view_count: number;
|
||||||
|
use_count: number;
|
||||||
|
helpful_count: number;
|
||||||
|
useless_count: number;
|
||||||
|
tags: string;
|
||||||
|
related_faqs: string | null;
|
||||||
|
related_docs: string | null;
|
||||||
|
related_links: string | null;
|
||||||
|
attachments: string | null;
|
||||||
|
keywords: string;
|
||||||
|
applicable_scope: string;
|
||||||
|
remarks: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核列表项(resource_type 为 all 时) */
|
||||||
|
export interface ReviewListItem {
|
||||||
|
type: "document" | "faq";
|
||||||
|
resource: DocumentResource | FaqResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页响应类型 */
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
data: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API响应包装类型 */
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取审核列表参数 */
|
||||||
|
export interface FetchReviewListParams {
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
resource_type?: ReviewStatsResourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核通过参数 */
|
||||||
|
export interface ApproveParams {
|
||||||
|
resource_type: "document" | "faq";
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核拒绝参数 */
|
||||||
|
export interface RejectParams {
|
||||||
|
resource_type: "document" | "faq";
|
||||||
|
id: number;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 按当前登录用户统计需要本人审核的数量(不含本人为作者的稿件) */
|
/** 按当前登录用户统计需要本人审核的数量(不含本人为作者的稿件) */
|
||||||
export const fetchReviewStats = (params?: { resource_type?: ReviewStatsResourceType }) =>
|
export const fetchReviewStats = (params?: { resource_type?: ReviewStatsResourceType }) =>
|
||||||
request.get("/Kb/v1/review/stats", params ? { params } : undefined);
|
request.get("/Kb/v1/review/stats", params ? { params } : undefined);
|
||||||
|
|
||||||
|
/** 获取待审核列表 */
|
||||||
|
export const fetchReviewList = (params?: FetchReviewListParams) =>
|
||||||
|
request.get<ApiResponse<PaginatedResponse<ReviewListItem | DocumentResource | FaqResource>>>("/Kb/v1/review/list", { params });
|
||||||
|
|
||||||
|
/** 审核通过 */
|
||||||
|
export const approveReview = (data: ApproveParams) =>
|
||||||
|
request.post<ApiResponse<string>>("/Kb/v1/review/approve", data);
|
||||||
|
|
||||||
|
/** 审核拒绝 */
|
||||||
|
export const rejectReview = (data: RejectParams) =>
|
||||||
|
request.post<ApiResponse<string>>("/Kb/v1/review/reject", data);
|
||||||
|
|
||||||
|
/** 获取文档详情 */
|
||||||
|
export const fetchDocumentDetail = (id: number) =>
|
||||||
|
request.get<ApiResponse<DocumentResource>>(`/Kb/v1/document/${id}`);
|
||||||
|
|
||||||
|
/** 获取FAQ详情 */
|
||||||
|
export const fetchFaqDetail = (id: number) =>
|
||||||
|
request.get<ApiResponse<FaqResource>>(`/Kb/v1/faq/${id}`);
|
||||||
|
|
||||||
|
/** 资源类型选项 */
|
||||||
|
export const resourceTypeOptions = [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '文档', value: 'document' },
|
||||||
|
{ label: 'FAQ', value: 'faq' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 获取资源类型文本 */
|
||||||
|
export const getResourceTypeText = (type: string): string => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
document: '文档',
|
||||||
|
faq: 'FAQ',
|
||||||
|
};
|
||||||
|
return typeMap[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取资源类型颜色 */
|
||||||
|
export const getResourceTypeColor = (type: string): string => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
document: 'arcoblue',
|
||||||
|
faq: 'green',
|
||||||
|
};
|
||||||
|
return colorMap[type] || 'gray';
|
||||||
|
};
|
||||||
|
|||||||
@@ -133,6 +133,16 @@ const OPS: AppRouteRecordRaw = {
|
|||||||
roles: ['*'],
|
roles: ['*'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'favorite',
|
||||||
|
name: 'Favorite',
|
||||||
|
component: () => import('@/views/ops/pages/favorite/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: '收藏管理',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
440
src/views/ops/pages/kb/favorite/index.vue
Normal file
440
src/views/ops/pages/kb/favorite/index.vue
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<a-card class="general-card" title="收藏管理">
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<a-table
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
row-key="id"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
>
|
||||||
|
<!-- 序号 -->
|
||||||
|
<template #index="{ rowIndex }">
|
||||||
|
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 资源类型 -->
|
||||||
|
<template #resource_type="{ record }">
|
||||||
|
<a-tag :color="record.resource_type === 'document' ? 'arc-blue' : 'arc-green'">
|
||||||
|
{{ record.resource_type === 'document' ? '文档' : 'FAQ' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 收藏时间 -->
|
||||||
|
<template #created_at="{ record }">
|
||||||
|
{{ formatDateTime(record.created_at) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<template #actions="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="record.is_deleted"
|
||||||
|
@click="handleView(record)"
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="record.is_deleted"
|
||||||
|
@click="handleDownload(record)"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
status="danger"
|
||||||
|
@click="handleUncollect(record)"
|
||||||
|
>
|
||||||
|
取消收藏
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 文档详情对话框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
title="文档详情"
|
||||||
|
:width="800"
|
||||||
|
:footer="false"
|
||||||
|
unmount-on-close
|
||||||
|
>
|
||||||
|
<div v-if="currentResource" class="detail-content">
|
||||||
|
<a-descriptions :column="2" bordered>
|
||||||
|
<a-descriptions-item label="文档编号">
|
||||||
|
{{ currentResource.doc_no || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="标题">
|
||||||
|
{{ currentResource.title || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="作者">
|
||||||
|
{{ currentResource.author_name || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态">
|
||||||
|
<a-tag :color="getDocStatusColor(currentResource.status)">
|
||||||
|
{{ getDocStatusText(currentResource.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="发布时间">
|
||||||
|
{{ formatDateTime(currentResource.published_at) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="浏览次数">
|
||||||
|
{{ currentResource.view_count || 0 }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="描述" :span="2">
|
||||||
|
{{ currentResource.description || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="内容" :span="2">
|
||||||
|
<div class="content-preview" v-html="currentResource.content || '-'"></div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- FAQ详情对话框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="faqDetailVisible"
|
||||||
|
title="FAQ详情"
|
||||||
|
:width="800"
|
||||||
|
:footer="false"
|
||||||
|
unmount-on-close
|
||||||
|
>
|
||||||
|
<div v-if="currentFaq" class="detail-content">
|
||||||
|
<a-descriptions :column="2" bordered>
|
||||||
|
<a-descriptions-item label="FAQ编号">
|
||||||
|
{{ currentFaq.faq_no || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态">
|
||||||
|
<a-tag color="green">{{ currentFaq.status || '已发布' }}</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="问题" :span="2">
|
||||||
|
{{ currentFaq.question || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="答案" :span="2">
|
||||||
|
<div class="content-preview" v-html="currentFaq.answer || '-'"></div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="浏览次数">
|
||||||
|
{{ currentFaq.view_count || 0 }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="有用次数">
|
||||||
|
{{ currentFaq.helpful_count || 0 }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue';
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||||
|
import {
|
||||||
|
fetchFavoriteList,
|
||||||
|
uncollectResource,
|
||||||
|
type Favorite,
|
||||||
|
type ResourceType,
|
||||||
|
} from '@/api/kb/favorite';
|
||||||
|
import { request } from '@/api/request';
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const loading = ref(false);
|
||||||
|
const tableData = ref<Favorite[]>([]);
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = computed<TableColumnData[]>(() => [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
dataIndex: 'index',
|
||||||
|
slotName: 'index',
|
||||||
|
width: 70,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '资源名称',
|
||||||
|
dataIndex: 'resource_name',
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '资源类型',
|
||||||
|
dataIndex: 'resource_type',
|
||||||
|
slotName: 'resource_type',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '备注',
|
||||||
|
dataIndex: 'remarks',
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收藏时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
slotName: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
slotName: 'actions',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 当前选中的资源
|
||||||
|
const currentResource = ref<any>(null);
|
||||||
|
const currentFaq = ref<any>(null);
|
||||||
|
|
||||||
|
// 对话框可见性
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const faqDetailVisible = ref(false);
|
||||||
|
|
||||||
|
// 获取收藏列表
|
||||||
|
const fetchFavorites = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.current,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res: any = await fetchFavoriteList(params);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
tableData.value = res.details?.data || [];
|
||||||
|
pagination.total = res.details?.total || 0;
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '获取收藏列表失败');
|
||||||
|
tableData.value = [];
|
||||||
|
pagination.total = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取收藏列表失败:', error);
|
||||||
|
Message.error('获取收藏列表失败');
|
||||||
|
tableData.value = [];
|
||||||
|
pagination.total = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (current: number) => {
|
||||||
|
pagination.current = current;
|
||||||
|
fetchFavorites();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = async (record: Favorite) => {
|
||||||
|
if (record.is_deleted) {
|
||||||
|
Message.warning('该资源已被删除,无法查看');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (record.resource_type === 'document') {
|
||||||
|
// 如果有resource_data直接使用,否则请求详情
|
||||||
|
if (record.resource_data) {
|
||||||
|
currentResource.value = record.resource_data;
|
||||||
|
detailVisible.value = true;
|
||||||
|
} else {
|
||||||
|
const res = await request.get<any>(`/Kb/v1/document/${record.resource_id}`);
|
||||||
|
if (res.code === 0) {
|
||||||
|
currentResource.value = res.details;
|
||||||
|
detailVisible.value = true;
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '获取文档详情失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (record.resource_type === 'faq') {
|
||||||
|
// FAQ详情
|
||||||
|
if (record.resource_data) {
|
||||||
|
currentFaq.value = record.resource_data;
|
||||||
|
faqDetailVisible.value = true;
|
||||||
|
} else {
|
||||||
|
const res = await request.get<any>(`/Kb/v1/faq/${record.resource_id}`);
|
||||||
|
if (res.code === 0) {
|
||||||
|
currentFaq.value = res.details;
|
||||||
|
faqDetailVisible.value = true;
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '获取FAQ详情失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取详情失败:', error);
|
||||||
|
Message.error('获取详情失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载文档
|
||||||
|
const handleDownload = async (record: Favorite) => {
|
||||||
|
if (record.is_deleted) {
|
||||||
|
Message.warning('该资源已被删除,无法下载');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (record.resource_type === 'document') {
|
||||||
|
// 调用文档下载接口
|
||||||
|
const res: any = await request.get<any>(`/Kb/v1/document/${record.resource_id}`);
|
||||||
|
if (res.code === 0) {
|
||||||
|
const doc = res.details;
|
||||||
|
// 创建下载内容
|
||||||
|
const content = `# ${doc.title || '无标题'}\n\n## 描述\n${doc.description || '无描述'}\n\n## 内容\n${doc.content || '无内容'}`;
|
||||||
|
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${doc.title || 'document'}.md`;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
Message.success('下载成功');
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '获取文档失败');
|
||||||
|
}
|
||||||
|
} else if (record.resource_type === 'faq') {
|
||||||
|
// FAQ下载
|
||||||
|
const res = await request.get<any>(`/Kb/v1/faq/${record.resource_id}`);
|
||||||
|
if (res.code === 0) {
|
||||||
|
const faq = res.details;
|
||||||
|
const content = `# ${faq.question || 'FAQ'}\n\n## 答案\n${faq.answer || '无答案'}`;
|
||||||
|
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `FAQ-${faq.faq_no || 'unknown'}.md`;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
Message.success('下载成功');
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '获取FAQ失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error);
|
||||||
|
Message.error('下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消收藏
|
||||||
|
const handleUncollect = (record: Favorite) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认取消收藏',
|
||||||
|
content: `确认取消收藏「${record.resource_name}」吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const res = await uncollectResource({
|
||||||
|
resource_type: record.resource_type,
|
||||||
|
resource_id: record.resource_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
Message.success('取消收藏成功');
|
||||||
|
fetchFavorites();
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || '取消收藏失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消收藏失败:', error);
|
||||||
|
Message.error('取消收藏失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
const formatDateTime = (dateStr?: string) => {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
// 处理多种日期格式
|
||||||
|
let date: Date;
|
||||||
|
if (dateStr.includes('T')) {
|
||||||
|
date = new Date(dateStr);
|
||||||
|
} else {
|
||||||
|
// 格式: YYYY-MM-DD HH:mm:ss
|
||||||
|
date = new Date(dateStr.replace(' ', 'T'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(date.getTime())) return dateStr;
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取文档状态颜色
|
||||||
|
const getDocStatusColor = (status?: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
draft: 'gray',
|
||||||
|
published: 'green',
|
||||||
|
reviewed: 'blue',
|
||||||
|
rejected: 'red',
|
||||||
|
};
|
||||||
|
return colorMap[status || ''] || 'gray';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取文档状态文本
|
||||||
|
const getDocStatusText = (status?: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
draft: '草稿',
|
||||||
|
published: '已发布',
|
||||||
|
reviewed: '已审核',
|
||||||
|
rejected: '已拒绝',
|
||||||
|
};
|
||||||
|
return textMap[status || ''] || '未知';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchFavorites();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'FavoriteManage',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-preview {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--color-fill-1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -90,14 +90,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-space v-if="currentDocument || isEditing">
|
<a-space v-if="currentDocument || isEditing">
|
||||||
<!-- 编辑模式切换 -->
|
<!-- 编辑模式切换(仅在编辑状态时显示) -->
|
||||||
<a-button-group>
|
<a-button-group v-if="isEditing">
|
||||||
<a-button
|
<a-button
|
||||||
:type="editorMode === 'edit' ? 'primary' : 'secondary'"
|
:type="editorMode === 'edit' ? 'primary' : 'secondary'"
|
||||||
size="small"
|
size="small"
|
||||||
@click="editorMode = 'edit'"
|
@click="editorMode = 'edit'"
|
||||||
>
|
>
|
||||||
编辑
|
源码
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
:type="editorMode === 'preview' ? 'primary' : 'secondary'"
|
:type="editorMode === 'preview' ? 'primary' : 'secondary'"
|
||||||
@@ -209,9 +209,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Markdown编辑/预览区域 -->
|
<!-- Markdown编辑/预览区域 -->
|
||||||
<div class="markdown-area" :class="editorMode">
|
<div class="markdown-area" :class="isEditing ? editorMode : 'preview'">
|
||||||
<!-- 编辑区 -->
|
<!-- 编辑区(仅在编辑状态时显示) -->
|
||||||
<div v-show="editorMode === 'edit' || editorMode === 'split'" class="editor-pane">
|
<div v-show="isEditing && (editorMode === 'edit' || editorMode === 'split')" class="editor-pane">
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="md-toolbar">
|
<div class="md-toolbar">
|
||||||
<a-space>
|
<a-space>
|
||||||
@@ -264,8 +264,8 @@
|
|||||||
class="md-editor"
|
class="md-editor"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 预览区 -->
|
<!-- 预览区(非编辑状态始终显示,编辑状态根据模式显示) -->
|
||||||
<div v-show="editorMode === 'preview' || editorMode === 'split'" class="preview-pane">
|
<div v-show="!isEditing || editorMode === 'preview' || editorMode === 'split'" class="preview-pane">
|
||||||
<div class="md-preview" v-html="renderedContent"></div>
|
<div class="md-preview" v-html="renderedContent"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -410,30 +410,31 @@ const fetchData = async () => {
|
|||||||
|
|
||||||
// 兼容多种API响应格式
|
// 兼容多种API响应格式
|
||||||
if (res?.code === 0) {
|
if (res?.code === 0) {
|
||||||
|
let rawData: any[] = []
|
||||||
|
|
||||||
// 格式1: res.details.data
|
// 格式1: res.details.data
|
||||||
if (res.details?.data) {
|
if (res.details?.data) {
|
||||||
documentList.value = res.details.data || []
|
rawData = res.details.data || []
|
||||||
total.value = res.details.total || 0
|
total.value = res.details.total || 0
|
||||||
}
|
}
|
||||||
// 格式2: res.data.data
|
// 格式2: res.data.data
|
||||||
else if (res.data?.data) {
|
else if (res.data?.data) {
|
||||||
documentList.value = res.data.data || []
|
rawData = res.data.data || []
|
||||||
total.value = res.data.total || 0
|
total.value = res.data.total || 0
|
||||||
}
|
}
|
||||||
// 格式3: res.data 是数组
|
// 格式3: res.data 是数组
|
||||||
else if (Array.isArray(res.data)) {
|
else if (Array.isArray(res.data)) {
|
||||||
documentList.value = res.data
|
rawData = res.data
|
||||||
total.value = res.data.length
|
total.value = res.data.length
|
||||||
}
|
}
|
||||||
// 格式4: res.details 是数组
|
// 格式4: res.details 是数组
|
||||||
else if (Array.isArray(res.details)) {
|
else if (Array.isArray(res.details)) {
|
||||||
documentList.value = res.details
|
rawData = res.details
|
||||||
total.value = res.details.length
|
total.value = res.details.length
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
documentList.value = []
|
// 处理数据:提取 resource 字段(document 和 faq 都作为文档处理)
|
||||||
total.value = 0
|
documentList.value = rawData.map((item: any) => item.resource)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
documentList.value = []
|
documentList.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
@@ -496,10 +497,12 @@ const handleSelectDocument = async (doc: Document) => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
const res: any = await fetchDocumentDetail(doc.id)
|
const res: any = await fetchDocumentDetail(doc.id)
|
||||||
if (res?.code === 0 && res.details) {
|
if (res?.code === 0 && res.details) {
|
||||||
currentDocument.value = res.details
|
// 兼容两种响应格式:直接返回文档对象 或 { type, resource } 格式
|
||||||
|
const docData = res.details.resource || res.details
|
||||||
|
currentDocument.value = docData
|
||||||
|
|
||||||
// 检查收藏状态
|
// 检查收藏状态
|
||||||
if (res.details.status === 'reviewed') {
|
if (docData.status === 'reviewed') {
|
||||||
try {
|
try {
|
||||||
const favRes: any = await checkFavorite(doc.id)
|
const favRes: any = await checkFavorite(doc.id)
|
||||||
isFavorited.value = favRes?.details?.is_favorited || false
|
isFavorited.value = favRes?.details?.is_favorited || false
|
||||||
@@ -932,6 +935,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
|
max-width: 80%;
|
||||||
.title-input {
|
.title-input {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -1,30 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Breadcrumb :items="['知识管理', '回收站']" />
|
<SearchTable
|
||||||
|
:form-model="searchForm"
|
||||||
<a-card class="general-card">
|
|
||||||
<!-- 搜索表单 -->
|
|
||||||
<SearchForm
|
|
||||||
:model-value="searchForm"
|
|
||||||
:form-items="filters"
|
:form-items="filters"
|
||||||
@update:model-value="handleFormModelUpdate"
|
|
||||||
@search="handleSearch"
|
|
||||||
@reset="handleReset"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<a-divider style="margin-top: 0" />
|
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
|
||||||
<DataTable
|
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
title="回收站"
|
||||||
:pagination="{
|
:pagination="{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
total,
|
total,
|
||||||
}"
|
}"
|
||||||
:show-download="false"
|
:show-download="false"
|
||||||
|
@update:form-model="handleFormModelUpdate"
|
||||||
|
@search="handleSearch"
|
||||||
|
@reset="handleReset"
|
||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
@refresh="fetchData"
|
@refresh="fetchData"
|
||||||
>
|
>
|
||||||
@@ -66,8 +57,7 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</SearchTable>
|
||||||
</a-card>
|
|
||||||
|
|
||||||
<!-- 恢复确认对话框 -->
|
<!-- 恢复确认对话框 -->
|
||||||
<a-modal
|
<a-modal
|
||||||
@@ -103,8 +93,7 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import SearchForm from '@/components/search-form/index.vue'
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
import DataTable from '@/components/data-table/index.vue'
|
|
||||||
import type { FormItem } from '@/components/search-form/types'
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
import type { TrashRecord, FetchTrashListParams } from '@/api/kb/trash'
|
import type { TrashRecord, FetchTrashListParams } from '@/api/kb/trash'
|
||||||
@@ -181,7 +170,7 @@ const filters = computed((): FormItem[] => [
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
placeholder: '请选择资源类型',
|
placeholder: '请选择资源类型',
|
||||||
options: resourceTypeOptions,
|
options: resourceTypeOptions,
|
||||||
span: 4,
|
span: 6,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -226,11 +215,11 @@ const fetchData = async () => {
|
|||||||
| undefined,
|
| undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetchTrashList(params)
|
const res: any = await fetchTrashList(params)
|
||||||
|
console.log('获取回收站列表成功:', res)
|
||||||
if (res?.code === 200 && res.data) {
|
if (res?.code === 0) {
|
||||||
// 如果有关键词,在前端过滤
|
// 如果有关键词,在前端过滤
|
||||||
let data = res.data.data || []
|
let data = res.details?.data || []
|
||||||
if (searchForm.keyword) {
|
if (searchForm.keyword) {
|
||||||
const keyword = searchForm.keyword.toLowerCase()
|
const keyword = searchForm.keyword.toLowerCase()
|
||||||
data = data.filter(
|
data = data.filter(
|
||||||
@@ -240,7 +229,7 @@ const fetchData = async () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
tableData.value = data
|
tableData.value = data
|
||||||
total.value = res.data.total || 0
|
total.value = res.details.total || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取回收站列表失败:', error)
|
console.error('获取回收站列表失败:', error)
|
||||||
@@ -288,7 +277,7 @@ const handleConfirmRestore = async () => {
|
|||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await restoreTrash({ id: recordToRestore.value.id })
|
const res = await restoreTrash({ id: recordToRestore.value.id })
|
||||||
if (res?.code === 200) {
|
if (res?.code === 0) {
|
||||||
Message.success('恢复成功')
|
Message.success('恢复成功')
|
||||||
restoreConfirmVisible.value = false
|
restoreConfirmVisible.value = false
|
||||||
recordToRestore.value = null
|
recordToRestore.value = null
|
||||||
@@ -317,7 +306,7 @@ const handleConfirmDelete = async () => {
|
|||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await deleteTrash({ id: recordToDelete.value.id })
|
const res = await deleteTrash({ id: recordToDelete.value.id })
|
||||||
if (res?.code === 200) {
|
if (res?.code === 0) {
|
||||||
Message.success('彻底删除成功')
|
Message.success('彻底删除成功')
|
||||||
deleteConfirmVisible.value = false
|
deleteConfirmVisible.value = false
|
||||||
recordToDelete.value = null
|
recordToDelete.value = null
|
||||||
@@ -339,12 +328,9 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.container {
|
.container {
|
||||||
padding: 0 20px 20px 20px;
|
margin-top: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.general-card {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<SearchTable
|
||||||
|
:form-model="searchForm"
|
||||||
|
:form-items="filters"
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="loading"
|
||||||
|
title="待审核列表"
|
||||||
|
:pagination="{
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
}"
|
||||||
|
:show-download="false"
|
||||||
|
@update:form-model="handleFormModelUpdate"
|
||||||
|
@search="handleSearch"
|
||||||
|
@reset="handleReset"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
@refresh="fetchData"
|
||||||
|
>
|
||||||
|
<!-- 序号列 -->
|
||||||
|
<template #index="{ rowIndex }">
|
||||||
|
{{ rowIndex + 1 + (page - 1) * pageSize }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 分类列 -->
|
||||||
|
<template #category="{ record }">
|
||||||
|
<a-tag :color="getResourceTypeColor(record.type)">
|
||||||
|
{{ getResourceTypeText(record.type) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 作者列 -->
|
||||||
|
<template #author="{ record }">
|
||||||
|
{{ record.resource?.author_name || '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 申请时间列 -->
|
||||||
|
<template #created_at="{ record }">
|
||||||
|
{{ formatTime(record.resource?.created_at) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 描述列 -->
|
||||||
|
<template #description="{ record }">
|
||||||
|
<a-tooltip :content="record.resource?.description || record.resource?.question || '-'">
|
||||||
|
<span class="description-text">
|
||||||
|
{{ record.resource?.description || record.resource?.question || '-' }}
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template #operation="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="text" size="small" @click="handleView(record)">
|
||||||
|
查看
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" status="success" @click="handleApprove(record)">
|
||||||
|
审核通过
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" status="danger" @click="handleReject(record)">
|
||||||
|
拒绝
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</SearchTable>
|
||||||
|
|
||||||
|
<!-- 详情弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
title="详情"
|
||||||
|
:width="720"
|
||||||
|
:footer="false"
|
||||||
|
>
|
||||||
|
<a-descriptions :column="2" bordered>
|
||||||
|
<a-descriptions-item label="类型">
|
||||||
|
<a-tag :color="getResourceTypeColor(currentRecord?.type || '')">
|
||||||
|
{{ getResourceTypeText(currentRecord?.type || '') }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="编号">
|
||||||
|
{{ getResourceNo(currentRecord) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="标题" :span="2">
|
||||||
|
{{ getResourceTitle(currentRecord) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="作者">
|
||||||
|
{{ currentRecord?.resource?.author_name || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="创建时间">
|
||||||
|
{{ formatTime(currentRecord?.resource?.created_at) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="分类">
|
||||||
|
{{ currentRecord?.resource?.sub_category || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态">
|
||||||
|
<a-tag :color="getStatusColor(currentRecord?.resource?.status)">
|
||||||
|
{{ getStatusText(currentRecord?.resource?.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="描述" :span="2">
|
||||||
|
{{ getResourceDescription(currentRecord) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="关键词" :span="2">
|
||||||
|
{{ currentRecord?.resource?.keywords || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="标签" :span="2">
|
||||||
|
<a-space wrap>
|
||||||
|
<a-tag v-for="tag in parseTags(currentRecord?.resource?.tags)" :key="tag">
|
||||||
|
{{ tag }}
|
||||||
|
</a-tag>
|
||||||
|
</a-space>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item v-if="currentRecord?.type === 'faq'" label="答案" :span="2">
|
||||||
|
<div class="content-preview">{{ getFaqAnswer(currentRecord) }}</div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item v-if="currentRecord?.type === 'document'" label="内容" :span="2">
|
||||||
|
<div class="content-preview">{{ getDocumentContent(currentRecord) }}</div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 拒绝原因对话框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="rejectVisible"
|
||||||
|
title="拒绝原因"
|
||||||
|
:ok-loading="rejectLoading"
|
||||||
|
@ok="handleConfirmReject"
|
||||||
|
@cancel="handleCancelReject"
|
||||||
|
>
|
||||||
|
<a-form :model="rejectForm" layout="vertical">
|
||||||
|
<a-form-item label="拒绝原因" required>
|
||||||
|
<a-textarea
|
||||||
|
v-model="rejectForm.reason"
|
||||||
|
placeholder="请输入拒绝原因"
|
||||||
|
:max-length="500"
|
||||||
|
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
|
import type { ReviewListItem, FetchReviewListParams } from '@/api/kb/review'
|
||||||
|
import {
|
||||||
|
fetchReviewList,
|
||||||
|
approveReview,
|
||||||
|
rejectReview,
|
||||||
|
getResourceTypeText,
|
||||||
|
getResourceTypeColor,
|
||||||
|
resourceTypeOptions,
|
||||||
|
} from '@/api/kb/review'
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = computed((): TableColumnData[] => [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
dataIndex: 'index',
|
||||||
|
slotName: 'index',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'category',
|
||||||
|
slotName: 'category',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '作者',
|
||||||
|
dataIndex: 'author',
|
||||||
|
slotName: 'author',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申请时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
slotName: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
slotName: 'description',
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'operation',
|
||||||
|
slotName: 'operation',
|
||||||
|
width: 280,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 搜索表单配置
|
||||||
|
const filters = computed((): FormItem[] => [
|
||||||
|
{
|
||||||
|
label: '资源类型',
|
||||||
|
field: 'resource_type',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择资源类型',
|
||||||
|
options: resourceTypeOptions,
|
||||||
|
span: 6,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 搜索表单数据
|
||||||
|
const searchForm = reactive({
|
||||||
|
resource_type: 'all',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理表单模型更新
|
||||||
|
const handleFormModelUpdate = (newFormModel: Record<string, any>) => {
|
||||||
|
Object.assign(searchForm, newFormModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<ReviewListItem[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const currentRecord = ref<ReviewListItem | null>(null)
|
||||||
|
|
||||||
|
// 拒绝对话框
|
||||||
|
const rejectVisible = ref(false)
|
||||||
|
const rejectLoading = ref(false)
|
||||||
|
const recordToReject = ref<ReviewListItem | null>(null)
|
||||||
|
const rejectForm = reactive({
|
||||||
|
reason: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const params: FetchReviewListParams = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
resource_type: searchForm.resource_type as 'all' | 'document' | 'faq',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await fetchReviewList(params)
|
||||||
|
|
||||||
|
if (res?.code === 0) {
|
||||||
|
tableData.value = res.details?.data || []
|
||||||
|
total.value = res.details?.total || 0
|
||||||
|
} else {
|
||||||
|
Message.error(res?.message || '获取审核列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取审核列表失败:', error)
|
||||||
|
Message.error('获取审核列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
page.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.resource_type = 'all'
|
||||||
|
page.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handlePageChange = (current: number) => {
|
||||||
|
page.value = current
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (time: string | null | undefined): string => {
|
||||||
|
return time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: string | undefined): string => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
draft: '草稿',
|
||||||
|
published: '待审核',
|
||||||
|
reviewed: '已审核',
|
||||||
|
rejected: '已拒绝',
|
||||||
|
}
|
||||||
|
return statusMap[status || ''] || status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status: string | undefined): string => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
draft: 'gray',
|
||||||
|
published: 'orange',
|
||||||
|
reviewed: 'green',
|
||||||
|
rejected: 'red',
|
||||||
|
}
|
||||||
|
return colorMap[status || ''] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析标签
|
||||||
|
const parseTags = (tags: string | null | undefined): string[] => {
|
||||||
|
if (!tags) return []
|
||||||
|
try {
|
||||||
|
return JSON.parse(tags)
|
||||||
|
} catch {
|
||||||
|
return tags.split(',').filter(Boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取资源编号
|
||||||
|
const getResourceNo = (record: ReviewListItem | null): string => {
|
||||||
|
if (!record?.resource) return '-'
|
||||||
|
const resource = record.resource as any
|
||||||
|
return resource.doc_no || resource.faq_no || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取资源标题
|
||||||
|
const getResourceTitle = (record: ReviewListItem | null): string => {
|
||||||
|
if (!record?.resource) return '-'
|
||||||
|
const resource = record.resource as any
|
||||||
|
return resource.title || resource.question || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取资源描述
|
||||||
|
const getResourceDescription = (record: ReviewListItem | null): string => {
|
||||||
|
if (!record?.resource) return '-'
|
||||||
|
const resource = record.resource as any
|
||||||
|
return resource.description || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取FAQ答案
|
||||||
|
const getFaqAnswer = (record: ReviewListItem | null): string => {
|
||||||
|
if (!record?.resource) return '-'
|
||||||
|
const resource = record.resource as any
|
||||||
|
return resource.answer || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文档内容
|
||||||
|
const getDocumentContent = (record: ReviewListItem | null): string => {
|
||||||
|
if (!record?.resource) return '-'
|
||||||
|
const resource = record.resource as any
|
||||||
|
return resource.content || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (record: ReviewListItem) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核通过
|
||||||
|
const handleApprove = async (record: ReviewListItem) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const res: any = await approveReview({
|
||||||
|
resource_type: record.type,
|
||||||
|
id: record.resource.id,
|
||||||
|
})
|
||||||
|
if (res?.code === 0) {
|
||||||
|
Message.success('审核通过')
|
||||||
|
await fetchData()
|
||||||
|
} else {
|
||||||
|
Message.error(res?.message || '审核失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('审核失败:', error)
|
||||||
|
Message.error('审核失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拒绝
|
||||||
|
const handleReject = (record: ReviewListItem) => {
|
||||||
|
recordToReject.value = record
|
||||||
|
rejectForm.reason = ''
|
||||||
|
rejectVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认拒绝
|
||||||
|
const handleConfirmReject = async () => {
|
||||||
|
if (!recordToReject.value) return
|
||||||
|
|
||||||
|
if (!rejectForm.reason.trim()) {
|
||||||
|
Message.warning('请输入拒绝原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rejectLoading.value = true
|
||||||
|
const res: any = await rejectReview({
|
||||||
|
resource_type: recordToReject.value.type,
|
||||||
|
id: recordToReject.value.resource.id,
|
||||||
|
reason: rejectForm.reason,
|
||||||
|
})
|
||||||
|
if (res?.code === 0) {
|
||||||
|
Message.success('已拒绝')
|
||||||
|
rejectVisible.value = false
|
||||||
|
recordToReject.value = null
|
||||||
|
rejectForm.reason = ''
|
||||||
|
await fetchData()
|
||||||
|
} else {
|
||||||
|
Message.error(res?.message || '拒绝失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('拒绝失败:', error)
|
||||||
|
Message.error('拒绝失败')
|
||||||
|
} finally {
|
||||||
|
rejectLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消拒绝
|
||||||
|
const handleCancelReject = () => {
|
||||||
|
rejectVisible.value = false
|
||||||
|
recordToReject.value = null
|
||||||
|
rejectForm.reason = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-preview {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user