From f5c7ebc59c4d19a3d59cbacd09a68d8a6c06165f Mon Sep 17 00:00:00 2001 From: zxr <271055687@qq.com> Date: Wed, 22 Jul 2026 04:01:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E7=B4=A7=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E9=A1=B5=E9=9D=A2=E4=BA=A4=E4=BA=92=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/kb/category.ts | 17 +- src/views/ops/pages/kb/categories/index.vue | 172 +++++++++++++++----- src/views/ops/pages/kb/favorite/index.vue | 28 +++- src/views/ops/pages/kb/items/index.vue | 133 ++++++++++++--- src/views/ops/pages/kb/recycle/index.vue | 44 ++++- src/views/ops/pages/kb/review/index.vue | 99 +++++++++-- 6 files changed, 400 insertions(+), 93 deletions(-) diff --git a/src/api/kb/category.ts b/src/api/kb/category.ts index cabeee7..f12def6 100644 --- a/src/api/kb/category.ts +++ b/src/api/kb/category.ts @@ -27,8 +27,21 @@ export interface Category { remarks: string } -/** 公共分类树节点。 */ -export interface CategoryTreeNode extends Category { +/** 公共分类树节点;详情字段需通过详情接口获取。 */ +export interface CategoryTreeNode { + id: number + name: string + description: string + type: 'general' + icon: string + color: string + parent_id: number + level: 1 | 2 | 3 + path: string + sort_order: number + status: CategoryStatus + doc_count: number + faq_count: number children: CategoryTreeNode[] } diff --git a/src/views/ops/pages/kb/categories/index.vue b/src/views/ops/pages/kb/categories/index.vue index f9323b9..564370a 100644 --- a/src/views/ops/pages/kb/categories/index.vue +++ b/src/views/ops/pages/kb/categories/index.vue @@ -5,11 +5,11 @@ - + {{ selectedCategory?.name || '分类详情' }} - - {{ selectedCategory.name }} - - - {{ selectedCategory.status === 'active' ? '启用' : '停用' }} - - - 第 {{ selectedCategory.level }} 级 - {{ selectedCategory.sort_order }} - {{ selectedCategory.doc_count }} - {{ selectedCategory.faq_count }} - {{ selectedCategory.creator_name || '-' }} - {{ formatTime(selectedCategory.updated_at) }} - {{ selectedCategory.description || '-' }} - {{ selectedCategory.remarks || '-' }} - - + + + {{ selectedCategory.name }} + + + {{ selectedCategory.status === 'active' ? '启用' : '停用' }} + + + 第 {{ selectedCategory.level }} 级 + {{ selectedCategory.sort_order }} + {{ selectedCategory.doc_count }} + {{ selectedCategory.faq_count }} + {{ selectedCategory.creator_name || '-' }} + {{ formatTime(selectedCategory.updated_at) }} + {{ selectedCategory.description || '-' }} + {{ selectedCategory.remarks || '-' }} + + + @@ -103,6 +122,12 @@ + @@ -116,6 +141,7 @@ import type { FormInstance } from '@arco-design/web-vue' import { createCategory, deleteCategory, + fetchCategoryDetail, fetchCategoryTree, updateCategory, type Category, @@ -127,7 +153,7 @@ import usePermissionCodes from '@/hooks/usePermissionCodes' interface TreeViewNode { key: string title: string - category: Category + category: CategoryTreeNode children: TreeViewNode[] } @@ -140,16 +166,22 @@ interface ParentOption { const { hasPermission } = usePermissionCodes() const canManage = computed(() => hasPermission('kb:content:manage')) -const loading = ref(false) +const treeLoading = ref(false) +const detailLoading = ref(false) const tree = ref([]) const selectedKeys = ref([]) const selectedCategory = ref(null) -let requestSequence = 0 +const selectedTreeID = ref(0) +let treeSequence = 0 +let detailSequence = 0 const treeData = computed(() => tree.value.map((node) => toViewNode(node))) const formVisible = ref(false) const submitting = ref(false) +const deletingID = ref(0) +const deletePending = ref(false) +const writeBusy = computed(() => submitting.value || deletePending.value) const formRef = ref() const editingCategory = ref(null) const form = reactive({ @@ -214,42 +246,71 @@ function categorySubtreeHeight(id: number): number { /** 加载最新分类树,并保留仍存在的选中项。 */ async function loadTree() { - const sequence = ++requestSequence - loading.value = true + const sequence = ++treeSequence + ++detailSequence + detailLoading.value = false + selectedCategory.value = null + treeLoading.value = true try { const reply = await fetchCategoryTree() - if (sequence !== requestSequence) return + if (sequence !== treeSequence) return if (reply.code !== 0) throw new Error(reply.message || '获取分类树失败') tree.value = reply.details || [] - const selectedID = selectedCategory.value?.id + const selectedID = selectedTreeID.value const selected = selectedID ? findCategory(tree.value, selectedID) : tree.value[0] - selectedCategory.value = selected || null + selectedTreeID.value = selected?.id || 0 selectedKeys.value = selected ? [String(selected.id)] : [] + if (selected) await loadCategoryDetail(selected.id) } catch (error) { - if (sequence === requestSequence) { + if (sequence === treeSequence) { tree.value = [] selectedCategory.value = null + selectedTreeID.value = 0 selectedKeys.value = [] showRequestError(error, '获取分类树失败') } } finally { - if (sequence === requestSequence) loading.value = false + if (sequence === treeSequence) treeLoading.value = false } } function handleSelect(keys: Array) { const id = Number(keys[0]) - selectedCategory.value = Number.isFinite(id) ? findCategory(tree.value, id) || null : null + if (!Number.isFinite(id) || !findCategory(tree.value, id)) { + selectedTreeID.value = 0 + selectedCategory.value = null + return + } + selectedTreeID.value = id + loadCategoryDetail(id) +} + +/** 分类树只负责导航,完整详情始终由详情接口读取。 */ +async function loadCategoryDetail(id: number) { + const sequence = ++detailSequence + selectedCategory.value = null + detailLoading.value = true + try { + const reply = await fetchCategoryDetail(id) + if (sequence !== detailSequence || selectedTreeID.value !== id) return + if (reply.code !== 0) throw new Error(reply.message || '获取分类详情失败') + selectedCategory.value = reply.details + } catch (error) { + if (sequence === detailSequence && selectedTreeID.value === id) showRequestError(error, '获取分类详情失败') + } finally { + if (sequence === detailSequence) detailLoading.value = false + } } function openCreate(parentID: number) { + if (writeBusy.value) return editingCategory.value = null Object.assign(form, { name: '', description: '', parent_id: parentID, sort_order: 0, status: 'active', remarks: '' }) formVisible.value = true } function openEdit() { - if (!selectedCategory.value) return + if (!selectedCategory.value || writeBusy.value) return editingCategory.value = selectedCategory.value Object.assign(form, { name: selectedCategory.value.name, @@ -263,15 +324,17 @@ function openEdit() { } function closeForm() { + if (submitting.value) return formVisible.value = false editingCategory.value = null formRef.value?.clearValidate() } async function submitForm() { - if (await formRef.value?.validate()) return false + if (submitting.value) return submitting.value = true try { + if (await formRef.value?.validate()) return const payload = { name: form.name.trim(), description: form.description.trim(), @@ -285,20 +348,22 @@ async function submitForm() { const reply = editingCategory.value ? await updateCategory({ ...payload, id: editingCategory.value.id }) : await createCategory(payload) if (reply.code !== 0) throw new Error(reply.message || '保存分类失败') Message.success(editingCategory.value ? '分类已更新' : '分类已创建') - closeForm() + formVisible.value = false + editingCategory.value = null + formRef.value?.clearValidate() await loadTree() - return true } catch (error) { showRequestError(error, '保存分类失败') - return false } finally { submitting.value = false } } function confirmDelete() { - if (!selectedCategory.value) return + if (!selectedCategory.value || writeBusy.value) return const category = selectedCategory.value + deletePending.value = true + deletingID.value = category.id Modal.confirm({ title: '确认删除分类', content: `确认删除「${category.name}」吗?存在子分类或内容时服务端将拒绝删除。`, @@ -307,15 +372,33 @@ function confirmDelete() { const reply = await deleteCategory(category.id) if (reply.code !== 0) throw new Error(reply.message || '删除分类失败') Message.success('分类已删除') + selectedTreeID.value = 0 selectedCategory.value = null await loadTree() } catch (error) { showRequestError(error, '删除分类失败') + } finally { + deletePending.value = false + deletingID.value = 0 } }, + onCancel: finishDelete, }) } +function finishDelete() { + deletePending.value = false + deletingID.value = 0 +} + +function canCloseForm() { + return !submitting.value +} + +function handleFormVisibleChange(visible: boolean) { + if (!visible) closeForm() +} + function showRequestError(error: unknown, fallback: string) { if (axios.isAxiosError(error)) { const status = error.response?.status @@ -347,7 +430,8 @@ onMounted(loadTree) .detail-card { min-height: 560px; } -.tree-loading { +.tree-loading, +.detail-loading { width: 100%; } :deep(.arco-tree-node-title) { diff --git a/src/views/ops/pages/kb/favorite/index.vue b/src/views/ops/pages/kb/favorite/index.vue index e55bdf2..6e80dc7 100644 --- a/src/views/ops/pages/kb/favorite/index.vue +++ b/src/views/ops/pages/kb/favorite/index.vue @@ -3,7 +3,7 @@ - + @@ -12,7 +12,7 @@ FAQ - 查询 + 查询 {{ formatTime(record.created_at) }}