fix: 修正 FAQ 表单与分类交互
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import { request } from '@/api/request'
|
||||
|
||||
/** FAQ 审核状态。 */
|
||||
export type FaqStatus = 'draft' | 'published' | 'reviewed' | 'rejected'
|
||||
/** FAQ 优先级。 */
|
||||
export type FaqPriority = 'low' | 'medium' | 'high'
|
||||
/** FAQ 列表可见范围。 */
|
||||
export type FaqScope = 'my' | 'all'
|
||||
|
||||
/** 知识库服务统一响应。 */
|
||||
export interface KbReply<T> {
|
||||
code: number
|
||||
message: string
|
||||
@@ -11,6 +15,7 @@ export interface KbReply<T> {
|
||||
timeseq: number
|
||||
}
|
||||
|
||||
/** FAQ 资源。 */
|
||||
export interface Faq {
|
||||
id: number
|
||||
created_at: string
|
||||
@@ -48,6 +53,7 @@ export interface Faq {
|
||||
is_favorited: boolean
|
||||
}
|
||||
|
||||
/** FAQ 分页结果。 */
|
||||
export interface FaqPage {
|
||||
total: number
|
||||
page: number
|
||||
@@ -55,6 +61,7 @@ export interface FaqPage {
|
||||
data: Faq[]
|
||||
}
|
||||
|
||||
/** FAQ 列表查询参数。 */
|
||||
export interface FaqListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
@@ -66,6 +73,7 @@ export interface FaqListParams {
|
||||
problem_type?: string
|
||||
}
|
||||
|
||||
/** FAQ 创建和编辑字段。 */
|
||||
export interface FaqFormData {
|
||||
question: string
|
||||
answer: string
|
||||
@@ -83,10 +91,12 @@ export interface FaqFormData {
|
||||
remarks: string
|
||||
}
|
||||
|
||||
/** FAQ 编辑请求。 */
|
||||
export interface UpdateFaqData extends FaqFormData {
|
||||
id: number
|
||||
}
|
||||
|
||||
/** 公共分类树节点。 */
|
||||
export interface CategoryTreeNode {
|
||||
id: number
|
||||
name: string
|
||||
@@ -104,6 +114,7 @@ export interface CategoryTreeNode {
|
||||
children: CategoryTreeNode[]
|
||||
}
|
||||
|
||||
/** FAQ 状态筛选项。 */
|
||||
export const faqStatusOptions: Array<{ label: string; value: FaqStatus }> = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '待审核', value: 'published' },
|
||||
@@ -111,12 +122,14 @@ export const faqStatusOptions: Array<{ label: string; value: FaqStatus }> = [
|
||||
{ label: '已拒绝', value: 'rejected' },
|
||||
]
|
||||
|
||||
/** FAQ 优先级筛选项。 */
|
||||
export const faqPriorityOptions: Array<{ label: string; value: FaqPriority }> = [
|
||||
{ label: '低', value: 'low' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '高', value: 'high' },
|
||||
]
|
||||
|
||||
/** 常用问题类型;接口仍允许自由字符串。 */
|
||||
export const faqProblemTypeOptions = [
|
||||
{ label: '故障', value: '故障' },
|
||||
{ label: '咨询', value: '咨询' },
|
||||
@@ -124,31 +137,65 @@ export const faqProblemTypeOptions = [
|
||||
{ label: '其他', value: '其他' },
|
||||
]
|
||||
|
||||
const faqStatusMeta: Record<FaqStatus, { text: string; color: string }> = {
|
||||
draft: { text: '草稿', color: 'gray' },
|
||||
published: { text: '待审核', color: 'orange' },
|
||||
reviewed: { text: '已审核', color: 'green' },
|
||||
rejected: { text: '已拒绝', color: 'red' },
|
||||
}
|
||||
|
||||
const faqPriorityMeta: Record<FaqPriority, { text: string; color: string }> = {
|
||||
low: { text: '低', color: 'gray' },
|
||||
medium: { text: '中', color: 'blue' },
|
||||
high: { text: '高', color: 'orange' },
|
||||
}
|
||||
|
||||
/** 获取 FAQ 状态文案。 */
|
||||
export const getFaqStatusText = (status: FaqStatus | string) => faqStatusMeta[status as FaqStatus]?.text || status || '-'
|
||||
|
||||
/** 获取 FAQ 状态标签颜色。 */
|
||||
export const getFaqStatusColor = (status: FaqStatus | string) => faqStatusMeta[status as FaqStatus]?.color || 'gray'
|
||||
|
||||
/** 获取 FAQ 优先级文案。 */
|
||||
export const getFaqPriorityText = (priority: FaqPriority | string) => faqPriorityMeta[priority as FaqPriority]?.text || priority || '-'
|
||||
|
||||
/** 获取 FAQ 优先级标签颜色。 */
|
||||
export const getFaqPriorityColor = (priority: FaqPriority | string) => faqPriorityMeta[priority as FaqPriority]?.color || 'gray'
|
||||
|
||||
/** 获取 FAQ 列表。 */
|
||||
export const fetchFaqList = (params: FaqListParams) =>
|
||||
request.get<KbReply<FaqPage>>('/Kb/v1/faq/list', {
|
||||
params,
|
||||
})
|
||||
|
||||
/** 获取 FAQ 详情。 */
|
||||
export const fetchFaqDetail = (id: number) => request.get<KbReply<Faq>>(`/Kb/v1/faq/${id}`)
|
||||
|
||||
/** 创建 FAQ 草稿。 */
|
||||
export const createFaq = (data: FaqFormData) => request.post<KbReply<Faq>>('/Kb/v1/faq/create', data)
|
||||
|
||||
/** 更新 FAQ。 */
|
||||
export const updateFaq = (data: UpdateFaqData) => request.post<KbReply<Faq>>('/Kb/v1/faq/update', data)
|
||||
|
||||
/** 提交 FAQ 审核。 */
|
||||
export const publishFaq = (id: number) => request.post<KbReply<string>>('/Kb/v1/faq/publish', { id })
|
||||
|
||||
/** 删除 FAQ 并移入回收站。 */
|
||||
export const deleteFaq = (id: number) => request.delete<KbReply<string>>(`/Kb/v1/faq/${id}`)
|
||||
|
||||
/** 收藏 FAQ。 */
|
||||
export const favoriteFaq = (id: number) =>
|
||||
request.post<KbReply<unknown>>('/Kb/v1/favorite/collect', {
|
||||
resource_type: 'faq',
|
||||
resource_id: id,
|
||||
})
|
||||
|
||||
/** 取消收藏 FAQ。 */
|
||||
export const unfavoriteFaq = (id: number) =>
|
||||
request.post<KbReply<string>>('/Kb/v1/favorite/uncollect', {
|
||||
resource_type: 'faq',
|
||||
resource_id: id,
|
||||
})
|
||||
|
||||
/** 获取公共分类树。 */
|
||||
export const fetchGeneralCategoryTree = () => request.get<KbReply<CategoryTreeNode[]>>('/Kb/v1/category/tree')
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<div class="faq-no">{{ faq.faq_no || `FAQ-${faq.id}` }}</div>
|
||||
<h2>{{ faq.question }}</h2>
|
||||
<a-space>
|
||||
<a-tag :color="statusColor(faq.status)">{{ statusText(faq.status) }}</a-tag>
|
||||
<a-tag :color="priorityColor(faq.priority)">{{ priorityText(faq.priority) }}</a-tag>
|
||||
<a-tag :color="getFaqStatusColor(faq.status)">{{ getFaqStatusText(faq.status) }}</a-tag>
|
||||
<a-tag :color="getFaqPriorityColor(faq.priority)">{{ getFaqPriorityText(faq.priority) }}</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-space wrap>
|
||||
@@ -42,7 +42,8 @@
|
||||
<h3>基本信息</h3>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="公共分类">{{ categoryName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="问题类型">{{ problemTypeText(faq.problem_type) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="子分类">{{ faq.sub_category || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="问题类型">{{ faq.problem_type || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="作者">{{ faq.author_name || `用户 ${faq.author_id}` }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ formatTime(faq.updated_at) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="审核人">{{ faq.reviewer_name || '-' }}</a-descriptions-item>
|
||||
@@ -105,7 +106,7 @@
|
||||
<script lang="ts" setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { IconStar, IconStarFill } from '@arco-design/web-vue/es/icon'
|
||||
import type { Faq, FaqPriority, FaqStatus } from '@/api/kb/faq'
|
||||
import { getFaqPriorityColor, getFaqPriorityText, getFaqStatusColor, getFaqStatusText, type Faq } from '@/api/kb/faq'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
@@ -145,40 +146,12 @@ function handleVisibleChange(visible: boolean) {
|
||||
if (!visible) close()
|
||||
}
|
||||
|
||||
/** 将后端时间统一为列表使用的分钟精度。 */
|
||||
function formatTime(value: string | null | undefined) {
|
||||
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'
|
||||
}
|
||||
|
||||
function statusText(status: FaqStatus) {
|
||||
return {
|
||||
draft: '草稿',
|
||||
published: '待审核',
|
||||
reviewed: '已审核',
|
||||
rejected: '已拒绝',
|
||||
}[status]
|
||||
}
|
||||
|
||||
function statusColor(status: FaqStatus) {
|
||||
return {
|
||||
draft: 'gray',
|
||||
published: 'orange',
|
||||
reviewed: 'green',
|
||||
rejected: 'red',
|
||||
}[status]
|
||||
}
|
||||
|
||||
function priorityText(priority: FaqPriority) {
|
||||
return { low: '低', medium: '中', high: '高' }[priority] || priority || '-'
|
||||
}
|
||||
|
||||
function priorityColor(priority: FaqPriority) {
|
||||
return { low: 'gray', medium: 'blue', high: 'orange' }[priority] || 'gray'
|
||||
}
|
||||
|
||||
function problemTypeText(type: string) {
|
||||
return { fault: '故障', consultation: '咨询', request: '请求', other: '其他' }[type] || type || '-'
|
||||
}
|
||||
|
||||
/** 提取最近一次审核拒绝原因。 */
|
||||
function rejectionReason(remarks: string) {
|
||||
const matches = [...remarks.matchAll(/\[审核拒绝 [^\]]+\]\s*([^\n]+)/g)]
|
||||
return matches.length ? matches[matches.length - 1][1] : remarks || '未填写原因'
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
:visible="visible"
|
||||
:width="760"
|
||||
:title="faq ? '编辑 FAQ' : '新建 FAQ'"
|
||||
:ok-loading="submitting"
|
||||
ok-text="保存"
|
||||
:closable="!submitting"
|
||||
:mask-closable="!submitting"
|
||||
:esc-to-close="!submitting"
|
||||
:on-before-cancel="canClose"
|
||||
unmount-on-close
|
||||
@ok="handleSubmit"
|
||||
@cancel="close"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
@@ -43,7 +44,7 @@
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="问题类型" field="problem_type">
|
||||
<a-select v-model="form.problem_type" placeholder="请选择或输入" allow-clear allow-create>
|
||||
<a-select v-model="form.problem_type" placeholder="请选择或输入" allow-clear allow-search allow-create>
|
||||
<a-option v-for="option in faqProblemTypeOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</a-option>
|
||||
@@ -51,6 +52,9 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="子分类" field="sub_category">
|
||||
<a-input v-model="form.sub_category" placeholder="请输入子分类或补充分类说明" :max-length="100" allow-clear />
|
||||
</a-form-item>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
@@ -106,6 +110,13 @@
|
||||
</a-form-item>
|
||||
</section>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button :disabled="submitting" @click="close">取消</a-button>
|
||||
<a-button type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">保存</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
@@ -114,6 +125,7 @@ import { reactive, ref, watch } from 'vue'
|
||||
import { Message, type FormInstance } from '@arco-design/web-vue'
|
||||
import { faqPriorityOptions, faqProblemTypeOptions, type Faq, type FaqFormData } from '@/api/kb/faq'
|
||||
|
||||
/** 表单和筛选使用的扁平分类节点。 */
|
||||
export interface FaqCategoryOption {
|
||||
id: number
|
||||
name: string
|
||||
@@ -121,6 +133,7 @@ export interface FaqCategoryOption {
|
||||
parentId: number
|
||||
level: number
|
||||
path: string
|
||||
status: 'active' | 'inactive'
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -144,6 +157,7 @@ type FaqEditorForm = Omit<FaqFormData, 'category_id'> & { category_id?: number }
|
||||
|
||||
const form = reactive<FaqEditorForm>(createEmptyForm())
|
||||
|
||||
/** 创建一份不共享引用的空表单。 */
|
||||
function createEmptyForm(): FaqEditorForm {
|
||||
return {
|
||||
question: '',
|
||||
@@ -163,6 +177,7 @@ function createEmptyForm(): FaqEditorForm {
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开抽屉时载入当前 FAQ,关闭后由 unmount-on-close 清理视图。 */
|
||||
function resetForm() {
|
||||
const faq = props.faq
|
||||
Object.assign(
|
||||
@@ -196,6 +211,7 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
/** 校验通过后交由父页面保存,接口成功前不关闭抽屉。 */
|
||||
async function handleSubmit() {
|
||||
if (props.submitting) return
|
||||
const errors = await formRef.value?.validate()
|
||||
@@ -214,6 +230,7 @@ async function handleSubmit() {
|
||||
question,
|
||||
answer,
|
||||
category_id: categoryId,
|
||||
sub_category: form.sub_category.trim(),
|
||||
problem_type: form.problem_type.trim(),
|
||||
solution: form.solution.trim(),
|
||||
process_steps: form.process_steps.trim(),
|
||||
@@ -226,10 +243,15 @@ async function handleSubmit() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交中拒绝关闭,避免中断请求或丢失输入。 */
|
||||
function close() {
|
||||
if (!props.submitting) emit('update:visible', false)
|
||||
}
|
||||
|
||||
function canClose() {
|
||||
return !props.submitting
|
||||
}
|
||||
|
||||
function handleVisibleChange(visible: boolean) {
|
||||
if (!visible) close()
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
:loading="categoryLoading"
|
||||
style="width: 220px"
|
||||
>
|
||||
<a-option v-for="category in categoryOptions" :key="category.id" :value="category.id">
|
||||
{{ category.label }}
|
||||
<a-option v-for="category in categoryFilterOptions" :key="category.id" :value="category.id">
|
||||
{{ category.label }}{{ category.status === 'inactive' ? '(停用)' : '' }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
@@ -53,7 +53,7 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="问题类型" field="problem_type">
|
||||
<a-select v-model="filters.problem_type" placeholder="全部类型" allow-clear style="width: 130px">
|
||||
<a-select v-model="filters.problem_type" placeholder="请选择或输入" allow-clear allow-search allow-create style="width: 150px">
|
||||
<a-option v-for="option in faqProblemTypeOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</a-option>
|
||||
@@ -86,10 +86,10 @@
|
||||
{{ categoryName(record.category_id) }}
|
||||
</template>
|
||||
<template #priority="{ record }">
|
||||
<a-tag :color="priorityColor(record.priority)">{{ priorityText(record.priority) }}</a-tag>
|
||||
<a-tag :color="getFaqPriorityColor(record.priority)">{{ getFaqPriorityText(record.priority) }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
|
||||
<a-tag :color="getFaqStatusColor(record.status)">{{ getFaqStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
<template #author="{ record }">
|
||||
{{ record.author_name || `用户 ${record.author_id}` }}
|
||||
@@ -138,7 +138,7 @@
|
||||
<FaqFormDrawer
|
||||
v-model:visible="formVisible"
|
||||
:faq="editingFaq"
|
||||
:categories="categoryOptions"
|
||||
:categories="categoryFormOptions"
|
||||
:submitting="formSubmitting"
|
||||
@submit="saveFaq"
|
||||
/>
|
||||
@@ -167,11 +167,8 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import { IconPlus, IconRefresh, IconStar, IconStarFill } from '@arco-design/web-vue/es/icon'
|
||||
import usePermissionCodes from '@/hooks/usePermissionCodes'
|
||||
import { useUserStore } from '@/store'
|
||||
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import {
|
||||
createFaq,
|
||||
deleteFaq,
|
||||
@@ -182,6 +179,10 @@ import {
|
||||
fetchFaqDetail,
|
||||
fetchFaqList,
|
||||
fetchGeneralCategoryTree,
|
||||
getFaqPriorityColor,
|
||||
getFaqPriorityText,
|
||||
getFaqStatusColor,
|
||||
getFaqStatusText,
|
||||
publishFaq,
|
||||
unfavoriteFaq,
|
||||
updateFaq,
|
||||
@@ -193,6 +194,9 @@ import {
|
||||
type FaqStatus,
|
||||
type KbReply,
|
||||
} from '@/api/kb/faq'
|
||||
import usePermissionCodes from '@/hooks/usePermissionCodes'
|
||||
import { useUserStore } from '@/store'
|
||||
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
|
||||
import FaqFormDrawer, { type FaqCategoryOption } from './components/FaqFormDrawer.vue'
|
||||
import FaqDetailDrawer from './components/FaqDetailDrawer.vue'
|
||||
|
||||
@@ -250,7 +254,8 @@ const pagination = reactive({
|
||||
let listRequestSequence = 0
|
||||
|
||||
const categoryLoading = ref(false)
|
||||
const categoryOptions = ref<FaqCategoryOption[]>([])
|
||||
const categoryFilterOptions = ref<FaqCategoryOption[]>([])
|
||||
const categoryFormOptions = computed(() => categoryFilterOptions.value.filter((category) => category.status === 'active'))
|
||||
const categoryLabels = ref(new Map<number, string>())
|
||||
|
||||
const formVisible = ref(false)
|
||||
@@ -264,12 +269,14 @@ let detailRequestSequence = 0
|
||||
|
||||
const actionState = reactive<{ id: number; type: ActionType | '' }>({ id: 0, type: '' })
|
||||
|
||||
/** 将登录信息里的 ID 收敛为可比较的数值。 */
|
||||
function numericId(value: unknown) {
|
||||
if (value === null || value === undefined || value === '') return 0
|
||||
const id = Number(value)
|
||||
return Number.isFinite(id) ? id : 0
|
||||
}
|
||||
|
||||
/** 按服务端规则判断当前用户能否修改 FAQ。 */
|
||||
function canMutate(faq: Faq) {
|
||||
if (faq.status === 'published') return false
|
||||
if (faq.status === 'reviewed') return canManage.value
|
||||
@@ -289,6 +296,7 @@ function canDelete(faq: Faq) {
|
||||
return canMutate(faq)
|
||||
}
|
||||
|
||||
/** 加载当前范围的 FAQ,过期响应不会覆盖新查询。 */
|
||||
async function fetchData() {
|
||||
const sequence = ++listRequestSequence
|
||||
loading.value = true
|
||||
@@ -322,6 +330,7 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载全部公共分类,同时为表单保留 active 子集。 */
|
||||
async function loadCategories() {
|
||||
categoryLoading.value = true
|
||||
try {
|
||||
@@ -331,10 +340,10 @@ async function loadCategories() {
|
||||
return
|
||||
}
|
||||
const labels = new Map<number, string>()
|
||||
const active: FaqCategoryOption[] = []
|
||||
flattenCategories(reply.details || [], [], labels, active)
|
||||
const categories: FaqCategoryOption[] = []
|
||||
flattenCategories(reply.details || [], [], labels, categories)
|
||||
categoryLabels.value = labels
|
||||
categoryOptions.value = active
|
||||
categoryFilterOptions.value = categories
|
||||
} catch (error) {
|
||||
showRequestError(error, '获取公共分类失败')
|
||||
} finally {
|
||||
@@ -342,22 +351,22 @@ async function loadCategories() {
|
||||
}
|
||||
}
|
||||
|
||||
function flattenCategories(nodes: CategoryTreeNode[], parentNames: string[], labels: Map<number, string>, active: FaqCategoryOption[]) {
|
||||
/** 展开分类树,并保留每个节点的父级、层级、路径和状态。 */
|
||||
function flattenCategories(nodes: CategoryTreeNode[], parentNames: string[], labels: Map<number, string>, categories: FaqCategoryOption[]) {
|
||||
nodes.forEach((node) => {
|
||||
const names = [...parentNames, node.name]
|
||||
const label = names.join(' / ')
|
||||
labels.set(node.id, label)
|
||||
if (node.status === 'active') {
|
||||
active.push({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
label,
|
||||
parentId: node.parent_id,
|
||||
level: node.level,
|
||||
path: node.path,
|
||||
})
|
||||
}
|
||||
flattenCategories(node.children || [], names, labels, active)
|
||||
labels.set(node.id, node.status === 'inactive' ? `${label}(停用)` : label)
|
||||
categories.push({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
label,
|
||||
parentId: node.parent_id,
|
||||
level: node.level,
|
||||
path: node.path,
|
||||
status: node.status,
|
||||
})
|
||||
flattenCategories(node.children || [], names, labels, categories)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -365,11 +374,17 @@ function categoryName(categoryId: number) {
|
||||
return categoryLabels.value.get(categoryId) || '-'
|
||||
}
|
||||
|
||||
function handleScopeChange() {
|
||||
/** 切换可见范围时清除范围不兼容的状态筛选。 */
|
||||
function changeScope(scope: FaqScope, load = true) {
|
||||
activeScope.value = scope
|
||||
pagination.current = 1
|
||||
filters.status = undefined
|
||||
handleDetailVisible(false)
|
||||
fetchData()
|
||||
if (load) fetchData()
|
||||
}
|
||||
|
||||
function handleScopeChange(scope: string | number) {
|
||||
changeScope(scope as FaqScope)
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
@@ -421,6 +436,7 @@ function openEditFromDetail(faq: Faq) {
|
||||
openEdit(faq)
|
||||
}
|
||||
|
||||
/** 保存表单;仅接口成功后关闭抽屉。 */
|
||||
async function saveFaq(values: FaqFormData) {
|
||||
const target = editingFaq.value
|
||||
if ((!target && !canCreate.value) || (target && !canEdit(target))) {
|
||||
@@ -435,8 +451,8 @@ async function saveFaq(values: FaqFormData) {
|
||||
Message.success(target ? 'FAQ 已保存' : 'FAQ 已创建')
|
||||
formVisible.value = false
|
||||
editingFaq.value = null
|
||||
if (!target) activeScope.value = 'my'
|
||||
pagination.current = 1
|
||||
if (!target) changeScope('my', false)
|
||||
else pagination.current = 1
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
showRequestError(error, target ? '保存 FAQ 失败' : '创建 FAQ 失败')
|
||||
@@ -445,6 +461,7 @@ async function saveFaq(values: FaqFormData) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取最新详情,关闭抽屉后自动废弃仍在途的响应。 */
|
||||
async function openDetail(faq: Faq) {
|
||||
const sequence = ++detailRequestSequence
|
||||
detailVisible.value = true
|
||||
@@ -579,12 +596,14 @@ function isActionLoading(id: number, type: ActionType) {
|
||||
return actionState.id === id && actionState.type === type
|
||||
}
|
||||
|
||||
/** 处理服务端返回的业务错误。 */
|
||||
function showReplyResult(reply: KbReply<unknown>, fallback: string) {
|
||||
if (reply.code === 0) return true
|
||||
Message.error(reply.message || fallback)
|
||||
return false
|
||||
}
|
||||
|
||||
/** 按 HTTP 状态展示鉴权与冲突错误。 */
|
||||
function showRequestError(error: unknown, fallback: string) {
|
||||
if (!axios.isAxiosError(error)) {
|
||||
Message.error(fallback)
|
||||
@@ -612,22 +631,6 @@ function formatTime(value: string) {
|
||||
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'
|
||||
}
|
||||
|
||||
function statusText(status: FaqStatus) {
|
||||
return { draft: '草稿', published: '待审核', reviewed: '已审核', rejected: '已拒绝' }[status]
|
||||
}
|
||||
|
||||
function statusColor(status: FaqStatus) {
|
||||
return { draft: 'gray', published: 'orange', reviewed: 'green', rejected: 'red' }[status]
|
||||
}
|
||||
|
||||
function priorityText(priority: FaqPriority) {
|
||||
return { low: '低', medium: '中', high: '高' }[priority] || priority || '-'
|
||||
}
|
||||
|
||||
function priorityColor(priority: FaqPriority) {
|
||||
return { low: 'gray', medium: 'blue', high: 'orange' }[priority] || 'gray'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCategories()
|
||||
fetchData()
|
||||
|
||||
Reference in New Issue
Block a user