KnowledgeBaseView.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <template>
  2. <div class="app-page">
  3. <section class="glass-card section-card">
  4. <el-form :model="filters" inline class="filter-form">
  5. <el-form-item label="分类">
  6. <el-select v-model="filters.category" placeholder="全部分类" clearable style="width:150px">
  7. <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
  8. </el-select>
  9. </el-form-item>
  10. <el-form-item label="关键词">
  11. <el-input v-model="filters.keyword" placeholder="搜索关键词" clearable style="width:160px" @keyup.enter="loadData" />
  12. </el-form-item>
  13. <el-form-item label="状态">
  14. <el-select v-model="filters.status" placeholder="全部" clearable style="width:120px">
  15. <el-option label="已启用" value="enabled" />
  16. <el-option label="已禁用" value="disabled" />
  17. </el-select>
  18. </el-form-item>
  19. <el-form-item>
  20. <el-button type="primary" @click="loadData">查询</el-button>
  21. <el-button @click="resetFilters">重置</el-button>
  22. </el-form-item>
  23. </el-form>
  24. </section>
  25. <section class="glass-card section-card" style="padding:12px 24px">
  26. <div class="table-toolbar" style="margin-bottom:0">
  27. <div class="chip-list">
  28. <el-button type="primary" @click="openCreateCategory">新建分类</el-button>
  29. <el-button type="success" @click="openCreate">新增知识</el-button>
  30. <el-button @click="batchImport">批量导入</el-button>
  31. <el-button @click="doExport">导出</el-button>
  32. </div>
  33. <el-button @click="loadData">刷新</el-button>
  34. </div>
  35. </section>
  36. <section class="glass-card section-card">
  37. <div class="knowledge-layout">
  38. <div class="category-tree">
  39. <div class="category-tree__header">
  40. <h3>知识分类</h3>
  41. </div>
  42. <el-tree
  43. :data="categoryTree"
  44. :props="{ children: 'children', label: 'name' }"
  45. node-key="id"
  46. @node-click="handleCategoryClick"
  47. default-expand-all
  48. >
  49. <template #default="{ node, data }">
  50. <span class="category-node">
  51. <span>{{ data.name }}</span>
  52. <span class="category-node__count">({{ data.count }})</span>
  53. </span>
  54. </template>
  55. </el-tree>
  56. </div>
  57. <div class="knowledge-list">
  58. <el-table :data="filteredItems" style="width:100%" v-loading="loading">
  59. <el-table-column prop="keywords" label="关键词" width="200">
  60. <template #default="{ row }">
  61. <el-tag v-for="kw in row.keywords.slice(0, 3)" :key="kw" size="small" style="margin-right:4px">{{ kw }}</el-tag>
  62. <span v-if="row.keywords.length > 3" style="color:#999;font-size:12px">+{{ row.keywords.length - 3 }}</span>
  63. </template>
  64. </el-table-column>
  65. <el-table-column prop="question" label="标准问题" min-width="200" show-overflow-tooltip />
  66. <el-table-column prop="answer" label="标准答案" min-width="250" show-overflow-tooltip />
  67. <el-table-column prop="clicks" label="点击量" width="90" align="center" />
  68. <el-table-column prop="aiScore" label="AI匹配度" width="100" align="center">
  69. <template #default="{ row }">
  70. <el-progress :percentage="row.aiScore" :status="aiScoreStatus(row.aiScore)" />
  71. </template>
  72. </el-table-column>
  73. <el-table-column prop="status" label="状态" width="90" align="center">
  74. <template #default="{ row }">
  75. <el-tag :type="getKnowledgeStatus(row.status).type" size="small">
  76. {{ getKnowledgeStatus(row.status).label }}
  77. </el-tag>
  78. </template>
  79. </el-table-column>
  80. <el-table-column prop="updatedAt" label="更新时间" width="120" />
  81. <el-table-column label="操作" width="150" fixed="right">
  82. <template #default="{ row }">
  83. <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
  84. <el-button link :type="row.status === 'enabled' ? 'warning' : 'success'" @click="toggleStatus(row)">
  85. {{ row.status === 'enabled' ? '禁用' : '启用' }}
  86. </el-button>
  87. <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
  88. </template>
  89. </el-table-column>
  90. </el-table>
  91. </div>
  92. </div>
  93. </section>
  94. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" @closed="resetForm">
  95. <el-form :model="form" label-width="100px">
  96. <el-form-item label="所属分类" required>
  97. <el-cascader v-model="form.categoryId" :options="categoryOptions" :props="{ checkStrictly: true, label: 'name', value: 'id' }" placeholder="选择分类" style="width:100%" />
  98. </el-form-item>
  99. <el-form-item label="关键词" required>
  100. <el-select v-model="form.keywords" multiple filterable allow-create default-first-option placeholder="输入关键词后回车" style="width:100%">
  101. <el-option v-for="kw in form.keywords" :key="kw" :label="kw" :value="kw" />
  102. </el-select>
  103. </el-form-item>
  104. <el-form-item label="标准问题" required>
  105. <el-input v-model="form.question" type="textarea" :rows="2" placeholder="请输入标准问题" />
  106. </el-form-item>
  107. <el-form-item label="标准答案" required>
  108. <el-input v-model="form.answer" type="textarea" :rows="4" placeholder="请输入标准答案" />
  109. </el-form-item>
  110. <el-form-item label="状态">
  111. <el-radio-group v-model="form.status">
  112. <el-radio value="enabled">启用</el-radio>
  113. <el-radio value="disabled">禁用</el-radio>
  114. </el-radio-group>
  115. </el-form-item>
  116. </el-form>
  117. <template #footer>
  118. <el-button @click="dialogVisible = false">取消</el-button>
  119. <el-button type="primary" @click="saveKnowledge">保存</el-button>
  120. </template>
  121. </el-dialog>
  122. <el-dialog v-model="categoryDialogVisible" title="新建分类" width="400px">
  123. <el-form :model="categoryForm" label-width="80px">
  124. <el-form-item label="上级分类">
  125. <el-tree-select v-model="categoryForm.parentId" :data="categoryTree" :props="{ label: 'name', value: 'id' }" placeholder="选择上级分类(可选)" clearable check-strictly style="width:100%" />
  126. </el-form-item>
  127. <el-form-item label="分类名称" required>
  128. <el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
  129. </el-form-item>
  130. </el-form>
  131. <template #footer>
  132. <el-button @click="categoryDialogVisible = false">取消</el-button>
  133. <el-button type="primary" @click="saveCategory">保存</el-button>
  134. </template>
  135. </el-dialog>
  136. </div>
  137. </template>
  138. <script setup lang="ts">
  139. import { ref, computed, onMounted } from 'vue';
  140. import { ElMessage, ElMessageBox } from 'element-plus';
  141. import { api } from '@/api/services';
  142. import type { KnowledgeBaseItem, KnowledgeCategory } from '@/types/page';
  143. import { getKnowledgeStatus } from '@/utils/enumMappings';
  144. const loading = ref(false);
  145. const dialogVisible = ref(false);
  146. const categoryDialogVisible = ref(false);
  147. const isEdit = ref(false);
  148. const currentId = ref('');
  149. const filters = ref({ category: '', keyword: '', status: '' });
  150. const categories = ref<KnowledgeCategory[]>([
  151. { id: '1', name: '产品咨询', count: 45 },
  152. { id: '2', name: '物流问题', count: 32 },
  153. { id: '3', name: '支付问题', count: 18 },
  154. { id: '4', name: '退换货', count: 28 },
  155. { id: '5', name: '账户问题', count: 15 }
  156. ]);
  157. const categoryTree = ref<KnowledgeCategory[]>([
  158. { id: '1', name: '产品咨询', count: 45, children: [
  159. { id: '1-1', name: '产品规格', count: 20 },
  160. { id: '1-2', name: '产品材质', count: 15 },
  161. { id: '1-3', name: '使用方法', count: 10 }
  162. ]},
  163. { id: '2', name: '物流问题', count: 32, children: [
  164. { id: '2-1', name: '发货时间', count: 12 },
  165. { id: '2-2', name: '物流查询', count: 10 },
  166. { id: '2-3', name: '快递丢失', count: 10 }
  167. ]},
  168. { id: '3', name: '支付问题', count: 18 },
  169. { id: '4', name: '退换货', count: 28 },
  170. { id: '5', name: '账户问题', count: 15 }
  171. ]);
  172. const categoryOptions = computed(() => categoryTree.value);
  173. const items = ref<KnowledgeBaseItem[]>([]);
  174. const filteredItems = computed(() => {
  175. return items.value.filter(item => {
  176. if (filters.value.category && item.categoryId !== filters.value.category) return false;
  177. if (filters.value.keyword && !item.keywords.some(k => k.includes(filters.value.keyword)) && !item.question.includes(filters.value.keyword)) return false;
  178. if (filters.value.status && item.status !== filters.value.status) return false;
  179. return true;
  180. });
  181. });
  182. const form = ref<Partial<KnowledgeBaseItem>>({
  183. categoryId: '',
  184. keywords: [],
  185. question: '',
  186. answer: '',
  187. status: 'enabled'
  188. });
  189. const categoryForm = ref({ parentId: '', name: '' });
  190. const dialogTitle = computed(() => isEdit.value ? '编辑知识' : '新增知识');
  191. const loadData = async () => {
  192. loading.value = true;
  193. try {
  194. const res = await api.getKnowledgeBase();
  195. items.value = res.items ?? [];
  196. } finally {
  197. loading.value = false;
  198. }
  199. };
  200. const resetFilters = () => { filters.value = { category: '', keyword: '', status: '' }; };
  201. const handleCategoryClick = (data: KnowledgeCategory) => {
  202. filters.value.category = data.id;
  203. };
  204. const aiScoreStatus = (score: number) => {
  205. if (score >= 90) return 'success';
  206. if (score >= 70) return 'warning';
  207. return 'exception';
  208. };
  209. const openCreate = () => { isEdit.value = false; dialogVisible.value = true; };
  210. const openEdit = (row: KnowledgeBaseItem) => { isEdit.value = true; currentId.value = row.id; form.value = { ...row }; dialogVisible.value = true; };
  211. const openCreateCategory = () => { categoryForm.value = { parentId: '', name: '' }; categoryDialogVisible.value = true; };
  212. const saveKnowledge = () => {
  213. if (!form.value.categoryId || !form.value.question || !form.value.answer) {
  214. ElMessage.warning('请填写必填项');
  215. return;
  216. }
  217. if (isEdit.value) {
  218. const idx = items.value.findIndex(i => i.id === currentId.value);
  219. if (idx !== -1) items.value[idx] = { ...items.value[idx], ...form.value } as KnowledgeBaseItem;
  220. ElMessage.success('更新成功');
  221. } else {
  222. items.value.unshift({
  223. id: 'K' + Date.now(),
  224. category: categories.value.find(c => c.id === form.value.categoryId)?.name || '',
  225. ...form.value,
  226. clicks: 0,
  227. aiScore: 0,
  228. createdAt: new Date().toISOString().split('T')[0],
  229. updatedAt: new Date().toISOString().split('T')[0]
  230. } as KnowledgeBaseItem);
  231. ElMessage.success('创建成功');
  232. }
  233. dialogVisible.value = false;
  234. };
  235. const resetForm = () => { form.value = { categoryId: '', keywords: [], question: '', answer: '', status: 'enabled' }; };
  236. const saveCategory = () => {
  237. if (!categoryForm.value.name) {
  238. ElMessage.warning('请输入分类名称');
  239. return;
  240. }
  241. ElMessage.success('分类创建成功');
  242. categoryDialogVisible.value = false;
  243. };
  244. const toggleStatus = (row: KnowledgeBaseItem) => {
  245. row.status = row.status === 'enabled' ? 'disabled' : 'enabled';
  246. ElMessage.success(`已${row.status === 'enabled' ? '启用' : '禁用'}`);
  247. };
  248. const handleDelete = (row: KnowledgeBaseItem) => {
  249. ElMessageBox.confirm('确定删除该知识吗?', '提示', { type: 'warning' })
  250. .then(() => {
  251. items.value = items.value.filter(i => i.id !== row.id);
  252. ElMessage.success('删除成功');
  253. })
  254. .catch(() => {});
  255. };
  256. const batchImport = () => { ElMessage.info('批量导入功能开发中'); };
  257. const doExport = () => { ElMessage.info('导出开始'); };
  258. onMounted(loadData);
  259. </script>
  260. <style scoped>
  261. .filter-form :deep(.el-form-item) { margin-bottom: 0; }
  262. .knowledge-layout {
  263. display: grid;
  264. grid-template-columns: 240px 1fr;
  265. gap: 16px;
  266. min-height: 500px;
  267. }
  268. .category-tree {
  269. background: #fafafa;
  270. border-radius: 12px;
  271. padding: 16px;
  272. }
  273. .category-tree__header {
  274. margin-bottom: 16px;
  275. }
  276. .category-tree__header h3 {
  277. margin: 0;
  278. font-size: 15px;
  279. font-weight: 600;
  280. }
  281. .category-node {
  282. display: flex;
  283. align-items: center;
  284. gap: 4px;
  285. }
  286. .category-node__count {
  287. color: #999;
  288. font-size: 12px;
  289. }
  290. .knowledge-list {
  291. min-width: 0;
  292. }
  293. </style>