KnowledgeBase.vue 32 KB

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