/**
* 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')
}
}
}
}