/** * FAQ 管理组件 * 支持 CRUD + 批量导入 + 导出 + 启用/禁用 */ import { listFaqs, createFaq, updateFaq, deleteFaq, toggleFaqStatus, batchImportFaqs, exportFaqs, getFaqStats } from '../js/api.js' import { toast } from '../js/utils.js' export default { template: `

❓ FAQ 精准匹配管理

总计: {{ stats.totalCount || 0 }} 启用: {{ stats.enabledCount || 0 }} 总命中: {{ stats.totalHitCount || 0 }}
问题 答案(摘要) 分类 优先级 命中 状态 操作
加载中...
暂无 FAQ
{{ f.question }} {{ f.answer }} {{ f.category || '-' }} {{ f.priority }} {{ f.hitCount }} {{ f.status === 'ENABLED' ? '✅ 启用' : '❌ 禁用' }}
第 {{ page }} / {{ Math.ceil(total / pageSize) }} 页(共 {{ total }} 条)

{{ editingFaq ? '编辑 FAQ' : '添加 FAQ' }}

批量导入 FAQ

使用 JSON 格式批量导入,每条包含 question、answer、similarQuestions(可选数组)、category(可选)

`, data() { return { faqs: [], loading: false, page: 1, pageSize: 20, total: 0, searchKeyword: '', filterCategory: '', filterStatus: '', categories: [], stats: null, showFormDialog: false, showImportDialog: false, editingFaq: null, form: { question: '', answer: '', similarQuestionsText: '', category: '', priority: 0 }, importJson: '', searchTimer: null } }, mounted() { this.loadList() this.loadStats() }, methods: { async loadList() { this.loading = true try { const res = await listFaqs(this.page, this.pageSize, this.searchKeyword || undefined, this.filterCategory || undefined, this.filterStatus || undefined) if (res.success) { this.faqs = res.data?.records || res.data || [] this.total = res.data?.total || res.total || 0 // 提取分类列表 const cats = new Set(this.faqs.map(f => f.category).filter(Boolean)) this.categories = [...cats] } } catch (e) { toast('加载失败: ' + e.message, 'error') } this.loading = false }, async loadStats() { try { const res = await getFaqStats() if (res.success) this.stats = res.data } catch { /* silent */ } }, debouncedSearch() { clearTimeout(this.searchTimer) this.searchTimer = setTimeout(() => { this.page = 1; this.loadList() }, 300) }, openAddDialog() { this.editingFaq = null this.form = { question: '', answer: '', similarQuestionsText: '', category: '', priority: 0 } this.showFormDialog = true }, openEditDialog(f) { this.editingFaq = f let similarText = '' try { const arr = typeof f.similarQuestions === 'string' ? JSON.parse(f.similarQuestions) : f.similarQuestions similarText = Array.isArray(arr) ? arr.join('\n') : '' } catch { similarText = f.similarQuestions || '' } this.form = { question: f.question, answer: f.answer, similarQuestionsText: similarText, category: f.category || '', priority: f.priority || 0 } this.showFormDialog = true }, async saveFaq() { const similarQuestions = this.form.similarQuestionsText.split('\n').map(s => s.trim()).filter(Boolean) const data = { question: this.form.question, answer: this.form.answer, similarQuestions: JSON.stringify(similarQuestions), category: this.form.category || null, priority: this.form.priority || 0 } try { let res if (this.editingFaq) { res = await updateFaq(this.editingFaq.id, data) } else { res = await createFaq(data) } if (res.success) { toast(this.editingFaq ? '修改成功' : '添加成功', 'success') this.showFormDialog = false this.loadList() this.loadStats() } else { toast(res.message || '操作失败', 'error') } } catch (e) { toast('操作失败: ' + e.message, 'error') } }, async toggleStatus(f) { const newStatus = f.status === 'ENABLED' ? 'DISABLED' : 'ENABLED' try { const res = await toggleFaqStatus(f.id, newStatus) if (res.success) { f.status = newStatus toast(`已${newStatus === 'ENABLED' ? '启用' : '禁用'}`, 'success') } else { toast(res.message || '操作失败', 'error') } } catch (e) { toast('操作失败: ' + e.message, 'error') } }, async removeFaq(id) { if (!confirm('确认删除该 FAQ?')) return try { const res = await deleteFaq(id) if (res.success) { toast('删除成功', 'success'); this.loadList(); this.loadStats() } else toast(res.message || '删除失败', 'error') } catch (e) { toast('删除失败: ' + e.message, 'error') } }, async doImport() { try { const faqs = JSON.parse(this.importJson) if (!Array.isArray(faqs) || faqs.length === 0) return toast('请输入有效的 FAQ 数组', 'error') // 处理 similarQuestions 字段 for (const f of faqs) { if (Array.isArray(f.similarQuestions)) f.similarQuestions = JSON.stringify(f.similarQuestions) else if (!f.similarQuestions) f.similarQuestions = '[]' } const res = await batchImportFaqs({ faqs }) if (res.success) { toast(`成功导入 ${res.data || faqs.length} 条`, 'success') this.showImportDialog = false this.importJson = '' this.loadList() this.loadStats() } else { toast(res.message || '导入失败', 'error') } } catch (e) { if (e instanceof SyntaxError) toast('JSON 格式错误', 'error') else toast('导入失败: ' + e.message, 'error') } }, async doExport() { try { const res = await exportFaqs() if (res.success) { const data = JSON.stringify(res.data, null, 2) const blob = new Blob([data], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = 'faq_export.json'; a.click() URL.revokeObjectURL(url) toast('导出成功', 'success') } else { toast(res.message || '导出失败', 'error') } } catch (e) { toast('导出失败: ' + e.message, 'error') } } } }