KnowledgeBase.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. <template>
  2. <div class="kb-container">
  3. <div class="header-section">
  4. <div class="title-info">
  5. <h2>知识库管理</h2>
  6. <p class="subtitle">管理所有知识库和相关内容</p>
  7. </div>
  8. <div class="action-buttons">
  9. <el-select v-model="queryParams.status" placeholder="所有状态" clearable style="width: 120px; margin-right: 12px" @change="handleSearch">
  10. <el-option label="正常" value="normal" />
  11. <el-option label="已禁用" value="disabled" />
  12. </el-select>
  13. <el-input
  14. v-model="queryParams.keyword"
  15. placeholder="搜索知识库名称..."
  16. style="width: 240px; margin-right: 12px"
  17. clearable
  18. @keyup.enter="handleSearch"
  19. @clear="handleSearch"
  20. />
  21. <el-button type="primary" @click="handleSearch">
  22. <el-icon><Search /></el-icon> 查询
  23. </el-button>
  24. <el-button type="primary" @click="handleAdd">
  25. <el-icon><Plus /></el-icon> 新建知识库
  26. </el-button>
  27. </div>
  28. </div>
  29. <el-card class="table-card" shadow="never">
  30. <el-table :data="tableData" v-loading="loading" style="width: 100%" @selection-change="handleSelectionChange">
  31. <el-table-column type="selection" width="55" />
  32. <el-table-column label="知识库名称" min-width="200">
  33. <template #default="{ row }">
  34. <div class="kb-name-cell">
  35. <el-icon class="kb-icon" :size="24" :style="{ color: getIconColor(row.name) }"><Collection /></el-icon>
  36. <div class="kb-info">
  37. <div class="name">{{ row.name }}</div>
  38. <div class="desc">{{ row.description || '暂无描述' }}</div>
  39. </div>
  40. </div>
  41. </template>
  42. </el-table-column>
  43. <el-table-column prop="collection_name" label="知识库表名" min-width="150" />
  44. <el-table-column prop="document_count" label="文档数量" width="100" />
  45. <el-table-column prop="status" label="状态" width="100">
  46. <template #default="{ row }">
  47. <el-tag :type="getStatusType(row.status)" effect="light" round>{{ getStatusText(row.status) }}</el-tag>
  48. </template>
  49. </el-table-column>
  50. <el-table-column prop="created_by" label="创建人" width="100" />
  51. <el-table-column prop="created_at" label="创建时间" width="160">
  52. <template #default="{ row }">
  53. {{ formatTime(row.created_at) }}
  54. </template>
  55. </el-table-column>
  56. <el-table-column prop="updated_by" label="修改人" width="100" />
  57. <el-table-column prop="updated_at" label="修改时间" width="160">
  58. <template #default="{ row }">
  59. {{ formatTime(row.updated_at) }}
  60. </template>
  61. </el-table-column>
  62. <el-table-column label="操作" width="180" fixed="right">
  63. <template #default="{ row }">
  64. <el-tooltip content="查看" placement="top">
  65. <el-button link type="primary" @click="handleView(row)">
  66. <el-icon><View /></el-icon>
  67. </el-button>
  68. </el-tooltip>
  69. <el-tooltip content="编辑" placement="top">
  70. <el-button link type="primary" @click="handleEdit(row)">
  71. <el-icon><Edit /></el-icon>
  72. </el-button>
  73. </el-tooltip>
  74. <el-tooltip :content="row.is_synced ? '已同步' : '同步到Milvus'" placement="top">
  75. <el-button link type="warning" @click="handleSync(row)" :disabled="row.is_synced">
  76. <el-icon><Refresh /></el-icon>
  77. </el-button>
  78. </el-tooltip>
  79. <el-tooltip content="删除" placement="top">
  80. <el-button link type="danger" @click="handleDelete(row)">
  81. <el-icon><Delete /></el-icon>
  82. </el-button>
  83. </el-tooltip>
  84. <el-dropdown @command="(cmd) => handleCommand(cmd, row)" trigger="click">
  85. <el-button link type="info">
  86. <el-icon><MoreFilled /></el-icon>
  87. </el-button>
  88. <template #dropdown>
  89. <el-dropdown-menu>
  90. <el-dropdown-item command="toggleStatus">
  91. {{ row.status === 'disabled' ? '启用' : '禁用' }}
  92. </el-dropdown-item>
  93. </el-dropdown-menu>
  94. </template>
  95. </el-dropdown>
  96. </template>
  97. </el-table-column>
  98. </el-table>
  99. <div class="pagination-container">
  100. <div class="pagination-info">
  101. 显示 {{ (pagination.page - 1) * pagination.pageSize + 1 }} 到
  102. {{ Math.min(pagination.page * pagination.pageSize, pagination.total) }} 条,
  103. 共 {{ pagination.total }} 条记录
  104. </div>
  105. <el-pagination
  106. v-model:current-page="pagination.page"
  107. v-model:page-size="pagination.pageSize"
  108. :total="pagination.total"
  109. :page-sizes="[10, 20, 50, 100]"
  110. layout="prev, pager, next, sizes"
  111. @size-change="handleSizeChange"
  112. @current-change="handleCurrentChange"
  113. />
  114. </div>
  115. </el-card>
  116. <!-- Create/Edit Dialog -->
  117. <el-dialog
  118. v-model="dialogVisible"
  119. :title="dialogType === 'create' ? '新建知识库' : '编辑知识库'"
  120. width="700px"
  121. @close="resetForm"
  122. class="kb-dialog"
  123. >
  124. <el-form ref="formRef" :model="formData" :rules="rules" label-width="110px" class="kb-form">
  125. <div class="form-section">
  126. <div class="section-title">基本信息</div>
  127. <el-row :gutter="20">
  128. <el-col :span="12">
  129. <el-form-item label="知识库名称" prop="name">
  130. <el-input v-model="formData.name" placeholder="请输入知识库名称" />
  131. </el-form-item>
  132. </el-col>
  133. <el-col :span="12">
  134. <el-form-item label="集合名称" prop="collection_name" v-if="dialogType === 'create'">
  135. <el-input v-model="formData.collection_name" placeholder="Milvus集合名(英文)" />
  136. </el-form-item>
  137. <el-form-item label="集合名称" v-else>
  138. <el-input v-model="formData.collection_name" disabled />
  139. </el-form-item>
  140. </el-col>
  141. </el-row>
  142. <el-form-item label="描述" prop="description">
  143. <el-input
  144. v-model="formData.description"
  145. type="textarea"
  146. rows="3"
  147. placeholder="请输入关于此知识库的详细描述..."
  148. />
  149. </el-form-item>
  150. <el-row :gutter="20">
  151. <el-col :span="12" v-if="dialogType === 'create'">
  152. <el-form-item label="向量维度" prop="dimension">
  153. <el-input-number v-model="formData.dimension" :min="1" :step="1" style="width: 100%" />
  154. </el-form-item>
  155. </el-col>
  156. <el-col :span="12">
  157. <el-form-item label="状态" prop="status">
  158. <el-radio-group v-model="formData.status">
  159. <el-radio-button label="normal">正常</el-radio-button>
  160. <el-radio-button label="disabled">禁用</el-radio-button>
  161. </el-radio-group>
  162. </el-form-item>
  163. </el-col>
  164. </el-row>
  165. </div>
  166. <div class="form-section">
  167. <div class="section-header">
  168. <div class="section-title">元数据字段定义</div>
  169. <div class="section-desc">定义文档的额外属性,用于精确检索过滤</div>
  170. </div>
  171. <div class="metadata-fields-container">
  172. <div v-for="(field, index) in formData.metadata_fields" :key="index" class="metadata-field-card">
  173. <div class="field-header">
  174. <span class="field-index">字段 {{ index + 1 }}</span>
  175. <el-button
  176. v-if="formData.metadata_fields.length > 1"
  177. type="danger"
  178. link
  179. :icon="Delete"
  180. @click="removeMetadataField(index)"
  181. size="small"
  182. >删除</el-button>
  183. </div>
  184. <el-row :gutter="15">
  185. <el-col :span="6">
  186. <el-form-item label-width="0" style="margin-bottom: 12px;">
  187. <el-select
  188. v-model="field.field_zh_name"
  189. placeholder="中文名称"
  190. filterable
  191. allow-create
  192. default-first-option
  193. @change="(val) => handleFieldChange(val, index)"
  194. >
  195. <el-option
  196. v-for="opt in availableFields"
  197. :key="opt.value"
  198. :label="opt.label"
  199. :value="opt.value"
  200. :disabled="getDisabledOptions(index).includes(opt.value)"
  201. />
  202. </el-select>
  203. </el-form-item>
  204. </el-col>
  205. <el-col :span="6">
  206. <el-form-item label-width="0" style="margin-bottom: 12px;">
  207. <el-input v-model="field.field_en_name" placeholder="英文标识 (如: year)" />
  208. </el-form-item>
  209. </el-col>
  210. <el-col :span="5">
  211. <el-form-item label-width="0" style="margin-bottom: 12px;">
  212. <el-select v-model="field.field_type" placeholder="类型">
  213. <el-option label="文本" value="text" />
  214. <el-option label="数字" value="num" />
  215. </el-select>
  216. </el-form-item>
  217. </el-col>
  218. <el-col :span="7">
  219. <el-form-item label-width="0" style="margin-bottom: 12px;">
  220. <el-input v-model="field.remark" placeholder="备注说明" />
  221. </el-form-item>
  222. </el-col>
  223. </el-row>
  224. </div>
  225. <div class="add-field-btn">
  226. <el-button type="primary" plain :icon="Plus" @click="addMetadataField" style="width: 100%">添加元数据字段</el-button>
  227. </div>
  228. </div>
  229. </div>
  230. </el-form>
  231. <template #footer>
  232. <span class="dialog-footer">
  233. <el-button @click="dialogVisible = false">取消</el-button>
  234. <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
  235. </span>
  236. </template>
  237. </el-dialog>
  238. </div>
  239. </template>
  240. <script setup lang="ts">
  241. import { ref, reactive, onMounted, computed } from 'vue'
  242. import { Search, Plus, Collection, View, Edit, Delete, MoreFilled, Refresh } from '@element-plus/icons-vue'
  243. import { ElMessage, ElMessageBox } from 'element-plus'
  244. import type { FormInstance, FormRules } from 'element-plus'
  245. import {
  246. getKnowledgeBases,
  247. getKnowledgeBaseMetadata,
  248. createKnowledgeBase,
  249. updateKnowledgeBase,
  250. updateKnowledgeBaseStatus,
  251. deleteKnowledgeBase,
  252. syncKnowledgeBase,
  253. type KnowledgeBase
  254. } from '@/api/knowledge-base'
  255. import dayjs from 'dayjs'
  256. // Query Parameters
  257. const queryParams = reactive({
  258. page: 1,
  259. pageSize: 10,
  260. keyword: '',
  261. status: ''
  262. })
  263. // Table Data
  264. const loading = ref(false)
  265. const tableData = ref<KnowledgeBase[]>([])
  266. const pagination = reactive({
  267. page: 1,
  268. pageSize: 10,
  269. total: 0
  270. })
  271. // Dialog
  272. const dialogVisible = ref(false)
  273. const dialogType = ref<'create' | 'edit'>('create')
  274. const submitLoading = ref(false)
  275. const formRef = ref<FormInstance>()
  276. const formData = reactive({
  277. id: '',
  278. name: '',
  279. collection_name: '',
  280. description: '',
  281. dimension: 768,
  282. status: 'normal',
  283. metadata_fields: [{ field_zh_name: '', field_en_name: '', field_type: 'text', remark: '' }]
  284. })
  285. // 可选字段列表
  286. const availableFields = [
  287. { label: "文件名称", value: "文件名称" },
  288. { label: "标准编号", value: "标准编号" },
  289. { label: "发布单位", value: "发布单位" },
  290. { label: "文件类型", value: "文件类型" },
  291. { label: "专业领域", value: "专业领域" },
  292. { label: "时效性", value: "时效性" },
  293. { label: "文档层级信息", value: "文档层级信息" },
  294. { label: "文件URL", value: "文件URL" },
  295. { label: "施工方案类型适配", value: "施工方案类型适配" }
  296. ]
  297. // 计算每个字段下拉框的禁用选项
  298. const getDisabledOptions = (currentIndex: number) => {
  299. // 获取所有已被选中的字段名
  300. const selectedValues = formData.metadata_fields
  301. .map((f, idx) => idx !== currentIndex ? f.field_zh_name : null)
  302. .filter(v => v !== null && v !== '')
  303. return selectedValues
  304. }
  305. const rules: FormRules = {
  306. name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
  307. collection_name: [
  308. { required: true, message: '请输入集合名称', trigger: 'blur' },
  309. { pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: '必须以字母或下划线开头,只能包含字母、数字和下划线', trigger: 'blur' }
  310. ],
  311. dimension: [{ required: true, message: '请输入维度', trigger: 'blur' }]
  312. }
  313. // Methods
  314. const loadData = async () => {
  315. loading.value = true
  316. try {
  317. const res = await getKnowledgeBases({
  318. page: pagination.page,
  319. page_size: pagination.pageSize,
  320. keyword: queryParams.keyword,
  321. status: queryParams.status
  322. })
  323. tableData.value = res.data
  324. pagination.total = res.meta?.total || 0
  325. } catch (error) {
  326. console.error(error)
  327. } finally {
  328. loading.value = false
  329. }
  330. }
  331. const handleSearch = () => {
  332. pagination.page = 1
  333. loadData()
  334. }
  335. const handleSizeChange = (val: number) => {
  336. pagination.pageSize = val
  337. loadData()
  338. }
  339. const handleCurrentChange = (val: number) => {
  340. pagination.page = val
  341. loadData()
  342. }
  343. const handleSelectionChange = (val: KnowledgeBase[]) => {
  344. // console.log(val)
  345. }
  346. const handleFieldChange = (val: string, index: number) => {
  347. // 自动映射中英文名称和类型
  348. const map: Record<string, any> = {
  349. '文件名称': { en: 'file_name', type: 'text' },
  350. '标准编号': { en: 'standard_number', type: 'text' },
  351. '发布单位': { en: 'issuing_authority', type: 'text' },
  352. '文件类型': { en: 'document_type', type: 'text' },
  353. '专业领域': { en: 'professional_field', type: 'text' },
  354. '时效性': { en: 'validity', type: 'text' },
  355. '文档层级信息': { en: 'hierarchy', type: 'text' },
  356. '文件URL': { en: 'file_url', type: 'text' },
  357. '施工方案类型适配': { en: 'plan_type_list', type: 'text' }
  358. }
  359. if (map[val]) {
  360. formData.metadata_fields[index].field_en_name = map[val].en
  361. formData.metadata_fields[index].field_type = map[val].type
  362. }
  363. }
  364. const handleAdd = () => {
  365. dialogType.value = 'create'
  366. formData.id = ''
  367. formData.name = ''
  368. formData.collection_name = ''
  369. formData.description = ''
  370. formData.dimension = 768
  371. formData.status = 'normal'
  372. formData.metadata_fields = [{ field_zh_name: '', field_en_name: '', field_type: 'text', remark: '' }]
  373. dialogVisible.value = true
  374. }
  375. const handleEdit = async (row: KnowledgeBase) => {
  376. dialogType.value = 'edit'
  377. formData.id = row.id
  378. formData.name = row.name
  379. formData.collection_name = row.collection_name
  380. formData.description = row.description || ''
  381. formData.status = row.status
  382. // 加载元数据字段,允许编辑
  383. try {
  384. const res = await getKnowledgeBaseMetadata(row.id)
  385. if (res.code === 0 && res.data) {
  386. // res.data.metadata_fields 是从 t_samp_metadata 获取的
  387. if (res.data.metadata_fields && res.data.metadata_fields.length > 0) {
  388. formData.metadata_fields = res.data.metadata_fields
  389. } else {
  390. formData.metadata_fields = [{ field_zh_name: '', field_en_name: '', field_type: 'text', remark: '' }]
  391. }
  392. }
  393. } catch (error) {
  394. console.error("加载元数据失败", error)
  395. formData.metadata_fields = [{ field_zh_name: '', field_en_name: '', field_type: 'text', remark: '' }]
  396. }
  397. dialogVisible.value = true
  398. }
  399. const handleSync = async (row: KnowledgeBase) => {
  400. try {
  401. await syncKnowledgeBase(row.id)
  402. ElMessage.success('同步成功,Milvus集合已创建')
  403. loadData()
  404. } catch (error) {
  405. // error handled by request interceptor
  406. }
  407. }
  408. const handleView = async (row: KnowledgeBase) => {
  409. // 复用编辑时的逻辑加载元数据,但不打开编辑弹窗
  410. try {
  411. const res = await getKnowledgeBaseMetadata(row.id)
  412. let metadataInfo = ''
  413. let schemaInfo = ''
  414. if (res.code === 0 && res.data) {
  415. if (res.data.metadata_fields && res.data.metadata_fields.length > 0) {
  416. metadataInfo = res.data.metadata_fields.map((f: any) =>
  417. `<li>${f.field_zh_name} (${f.field_en_name}): ${f.field_type === 'text' ? '文本' : '数字'} - ${f.remark || '无备注'}</li>`
  418. ).join('')
  419. }
  420. if (res.data.custom_schemas && res.data.custom_schemas.length > 0) {
  421. schemaInfo = res.data.custom_schemas.map((s: any) =>
  422. `<li>${s.field_name}: ${s.field_type} ${s.max_length ? `(Max: ${s.max_length})` : ''} - ${s.description || '无描述'}</li>`
  423. ).join('')
  424. }
  425. }
  426. const content = `
  427. <div style="text-align: left;">
  428. <p><strong>名称:</strong> ${row.name}</p>
  429. <p><strong>集合:</strong> ${row.collection_name}</p>
  430. <p><strong>描述:</strong> ${row.description || '暂无'}</p>
  431. <p><strong>状态:</strong> ${getStatusText(row.status)}</p>
  432. <p><strong>文档数:</strong> ${row.document_count}</p>
  433. <p><strong>创建时间:</strong> ${formatTime(row.created_at)}</p>
  434. <br>
  435. <p><strong>自定义 Schema (Milvus):</strong></p>
  436. <ul>
  437. <li>pk: INT64 (主键)</li>
  438. <li>text: VARCHAR (65535) - 内容</li>
  439. <li>vector: FLOAT_VECTOR (768) - 向量列</li>
  440. <li>sparse: BM25 - 关键字检索</li>
  441. <li>document_id: VARCHAR (128) - 文档ID</li>
  442. <li>parent_id: VARCHAR (128) - 父段ID</li>
  443. <li>index: INT64 - 索引序号</li>
  444. <li>tag_list: VARCHAR (2048) - 标签</li>
  445. <li>permission: JSON - 权限</li>
  446. <li>metadata: JSON - 元数据</li>
  447. <li>is_deleted: BOOL - 删除标志</li>
  448. <li>created_by: VARCHAR (128) - 创建人</li>
  449. <li>created_time: INT64 - 创建时间</li>
  450. <li>updated_by: VARCHAR (128) - 修改人</li>
  451. <li>updated_time: INT64 - 修改时间</li>
  452. </ul>
  453. <br>
  454. <p><strong>元数据字段 (Metadata):</strong></p>
  455. <ul>${metadataInfo || '<li>无元数据字段</li>'}</ul>
  456. </div>
  457. `
  458. ElMessageBox.alert(content, '知识库详情', {
  459. dangerouslyUseHTMLString: true,
  460. confirmButtonText: '关闭',
  461. customStyle: { maxWidth: '600px' }
  462. })
  463. } catch (error) {
  464. console.error("加载详情失败", error)
  465. ElMessage.error("获取详情失败")
  466. }
  467. }
  468. const handleDelete = (row: KnowledgeBase) => {
  469. // 预先检查,提升用户体验
  470. if (row.document_count > 0) {
  471. ElMessage.warning(`知识库 "${row.name}" 中仍有 ${row.document_count} 条文档,请先清空文档后再删除`)
  472. return
  473. }
  474. ElMessageBox.confirm(
  475. `确定要删除知识库 "${row.name}" 吗?此操作不可恢复,且会删除对应的Milvus集合。`,
  476. '警告',
  477. {
  478. confirmButtonText: '确定',
  479. cancelButtonText: '取消',
  480. type: 'warning',
  481. }
  482. )
  483. .then(async () => {
  484. try {
  485. await deleteKnowledgeBase(row.id)
  486. ElMessage.success('删除成功')
  487. loadData()
  488. } catch (error) {
  489. // error handled by request interceptor
  490. }
  491. })
  492. .catch(() => {})
  493. }
  494. const handleCommand = async (command: string, row: KnowledgeBase) => {
  495. if (command === 'toggleStatus') {
  496. const newStatus = row.status === 'disabled' ? 'normal' : 'disabled'
  497. try {
  498. await updateKnowledgeBaseStatus(row.id, newStatus)
  499. ElMessage.success('状态更新成功')
  500. loadData()
  501. } catch (error) {
  502. console.error(error)
  503. }
  504. }
  505. }
  506. const addMetadataField = () => {
  507. formData.metadata_fields.push({ field_zh_name: '', field_en_name: '', field_type: 'text', remark: '' })
  508. }
  509. const removeMetadataField = (index: number) => {
  510. formData.metadata_fields.splice(index, 1)
  511. }
  512. const handleSubmit = async () => {
  513. if (!formRef.value) return
  514. await formRef.value.validate(async (valid) => {
  515. if (valid) {
  516. submitLoading.value = true
  517. try {
  518. if (dialogType.value === 'create') {
  519. await createKnowledgeBase({
  520. name: formData.name,
  521. collection_name: formData.collection_name,
  522. description: formData.description,
  523. dimension: formData.dimension,
  524. metadata_fields: formData.metadata_fields.filter(f => f.field_zh_name && f.field_en_name)
  525. })
  526. ElMessage.success('创建成功')
  527. } else {
  528. await updateKnowledgeBase(formData.id, {
  529. name: formData.name,
  530. description: formData.description,
  531. status: formData.status,
  532. metadata_fields: formData.metadata_fields.filter(f => f.field_zh_name && f.field_en_name)
  533. })
  534. ElMessage.success('更新成功')
  535. }
  536. dialogVisible.value = false
  537. loadData()
  538. } catch (error) {
  539. console.error(error)
  540. } finally {
  541. submitLoading.value = false
  542. }
  543. }
  544. })
  545. }
  546. const resetForm = () => {
  547. if (formRef.value) {
  548. formRef.value.resetFields()
  549. }
  550. }
  551. // Helpers
  552. const getStatusType = (status: string) => {
  553. const map: Record<string, string> = {
  554. normal: 'success',
  555. test: 'info',
  556. disabled: 'danger'
  557. }
  558. return map[status] || 'info'
  559. }
  560. const getStatusText = (status: string) => {
  561. const map: Record<string, string> = {
  562. normal: '正常',
  563. test: '测试',
  564. disabled: '已禁用'
  565. }
  566. return map[status] || status
  567. }
  568. const formatTime = (time: string) => {
  569. if (!time) return '-'
  570. return dayjs(time).format('YYYY-MM-DD HH:mm')
  571. }
  572. // 为图标生成随机颜色或基于名称的固定颜色
  573. const getIconColor = (name: string) => {
  574. const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']
  575. let hash = 0
  576. for (let i = 0; i < name.length; i++) {
  577. hash = name.charCodeAt(i) + ((hash << 5) - hash)
  578. }
  579. const index = Math.abs(hash) % colors.length
  580. return colors[index]
  581. }
  582. onMounted(() => {
  583. loadData()
  584. })
  585. </script>
  586. <style scoped>
  587. .kb-container {
  588. padding: 20px;
  589. }
  590. .header-section {
  591. display: flex;
  592. justify-content: space-between;
  593. align-items: center;
  594. margin-bottom: 20px;
  595. }
  596. .title-info h2 {
  597. margin: 0;
  598. font-size: 20px;
  599. font-weight: 600;
  600. color: #303133;
  601. }
  602. .subtitle {
  603. margin: 8px 0 0;
  604. color: #909399;
  605. font-size: 14px;
  606. }
  607. .table-card {
  608. border-radius: 8px;
  609. }
  610. .kb-name-cell {
  611. display: flex;
  612. align-items: flex-start;
  613. padding: 8px 0;
  614. }
  615. .kb-icon {
  616. margin-right: 12px;
  617. margin-top: 4px;
  618. }
  619. .kb-info {
  620. flex: 1;
  621. }
  622. .kb-info .name {
  623. font-weight: 600;
  624. font-size: 14px;
  625. color: #303133;
  626. margin-bottom: 4px;
  627. }
  628. .kb-info .desc {
  629. font-size: 12px;
  630. color: #909399;
  631. line-height: 1.4;
  632. display: -webkit-box;
  633. -webkit-box-orient: vertical;
  634. -webkit-line-clamp: 2;
  635. overflow: hidden;
  636. }
  637. .pagination-container {
  638. display: flex;
  639. justify-content: space-between;
  640. align-items: center;
  641. margin-top: 20px;
  642. }
  643. .pagination-info {
  644. color: #909399;
  645. font-size: 13px;
  646. }
  647. .kb-dialog :deep(.el-dialog__body) {
  648. padding: 20px 30px;
  649. }
  650. .kb-form {
  651. padding-right: 10px;
  652. }
  653. .form-section {
  654. margin-bottom: 24px;
  655. }
  656. .section-title {
  657. font-size: 15px;
  658. font-weight: 600;
  659. color: #303133;
  660. margin-bottom: 16px;
  661. padding-left: 10px;
  662. border-left: 3px solid #409eff;
  663. }
  664. .section-header {
  665. display: flex;
  666. align-items: baseline;
  667. margin-bottom: 16px;
  668. }
  669. .section-desc {
  670. font-size: 12px;
  671. color: #909399;
  672. margin-left: 10px;
  673. }
  674. .metadata-field-card {
  675. background-color: #f8f9fa;
  676. border-radius: 4px;
  677. border: 1px solid #e4e7ed;
  678. padding: 12px 16px;
  679. margin-bottom: 12px;
  680. }
  681. .field-header {
  682. display: flex;
  683. justify-content: space-between;
  684. align-items: center;
  685. margin-bottom: 10px;
  686. }
  687. .field-index {
  688. font-size: 12px;
  689. font-weight: 600;
  690. color: #606266;
  691. background-color: #e6e8eb;
  692. padding: 2px 6px;
  693. border-radius: 2px;
  694. }
  695. .add-field-btn {
  696. margin-top: 10px;
  697. }
  698. .metadata-fields-container {
  699. padding: 0 5px;
  700. }
  701. .delete-btn {
  702. margin-left: 10px;
  703. }
  704. </style>