| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021 |
- <template>
- <div class="snippet-container">
- <div class="header-section">
- <div class="title-info">
- <h2>知识片段管理</h2>
- <p class="subtitle">管理所有知识片段和相关内容</p>
- </div>
- <div class="action-buttons">
- <el-select v-model="queryParams.status" placeholder="所有状态" clearable style="width: 120px; margin-right: 12px" @change="handleSearch">
- <el-option label="启用" value="normal" />
- <el-option label="禁用" value="disabled" />
- </el-select>
- <el-select v-model="queryParams.kb" placeholder="所有知识库" clearable style="width: 150px; margin-right: 12px" @change="handleSearch">
- <el-option
- v-for="item in kbOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- >
- <span>{{ item.label }}</span>
- </el-option>
- </el-select>
- <el-input
- v-model="queryParams.keyword"
- placeholder="搜索知识片段..."
- style="width: 240px; margin-right: 12px"
- clearable
- @keyup.enter="handleSearch"
- @clear="handleSearch"
- />
- <el-button type="primary" @click="handleSearch">
- <el-icon><Search /></el-icon> 查询
- </el-button>
- <el-button type="primary" @click="handleAdd">
- <el-icon><Plus /></el-icon> 新建片段
- </el-button>
- </div>
- </div>
- <div v-if="!queryParams.kb" class="empty-state">
- <el-empty description="请先选择一个知识库以查看内容" />
- </div>
-
- <el-card v-else class="table-card" shadow="never">
- <el-table :data="tableData" v-loading="loading" style="width: 100%" @selection-change="handleSelectionChange">
- <el-table-column type="selection" width="55" />
-
- <el-table-column label="文档名称" min-width="150">
- <template #default="{ row }">
- <div class="doc-name-cell">
- <el-icon class="doc-icon"><Document /></el-icon>
- <span>{{ row.doc_name }}</span>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="code" label="片段编号" width="160">
- <template #default="{ row }">
- <span>{{ row.document_id || row.code || '-' }}</span>
- </template>
- </el-table-column>
- <el-table-column label="片段内容" min-width="300">
- <template #default="{ row }">
- <el-tooltip
- effect="dark"
- placement="top"
- :show-after="500"
- :disabled="!row.content || row.content.length <= 200"
- >
- <template #content>
- <div style="max-width: 400px; max-height: 300px; overflow-y: auto; white-space: pre-wrap;">
- {{ row.content }}
- </div>
- </template>
- <div class="content-cell">
- {{ row.content && row.content.length > 200 ? row.content.substring(0, 200) + '...' : row.content }}
- </div>
- </el-tooltip>
- </template>
- </el-table-column>
- <el-table-column prop="char_count" label="字符数量" width="100" />
- <el-table-column label="元数据信息" min-width="200">
- <template #default="{ row }">
- <el-tooltip
- v-if="formatMetaInfo(row) && formatMetaInfo(row).length > 50"
- effect="dark"
- :content="formatMetaInfo(row)"
- placement="top"
- >
- <span class="meta-info truncate">{{ formatMetaInfo(row) }}</span>
- </el-tooltip>
- <span v-else class="meta-info">{{ formatMetaInfo(row) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="标签" min-width="150">
- <template #default="{ row }">
- <div class="tags-container">
- <el-tag
- v-for="(tag, index) in parseTags(row.tag_list)"
- :key="index"
- size="small"
- class="snippet-tag"
- effect="plain"
- >
- {{ tag }}
- </el-tag>
- <span v-if="!row.tag_list" class="no-tags">-</span>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="status" label="状态" width="80">
- <template #default="{ row }">
- <el-tag :type="row.status === 'normal' ? 'success' : 'info'" effect="plain" class="status-tag">
- {{ row.status === 'normal' ? '启用' : '禁用' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="created_at" label="创建时间" width="160" />
- <el-table-column prop="updated_at" label="修改时间" width="160" />
- <el-table-column label="操作" width="120" fixed="right">
- <template #default="{ row }">
- <el-tooltip content="查看" placement="top">
- <el-button link type="primary" @click="handleView(row)">
- <el-icon><View /></el-icon>
- </el-button>
- </el-tooltip>
- <el-tooltip content="编辑" placement="top">
- <el-button link type="primary" @click="handleEdit(row)">
- <el-icon><Edit /></el-icon>
- </el-button>
- </el-tooltip>
- <el-tooltip content="删除" placement="top">
- <el-button link type="danger" @click="handleDelete(row)">
- <el-icon><Delete /></el-icon>
- </el-button>
- </el-tooltip>
- <el-dropdown trigger="click">
- <el-button link type="info">
- <el-icon><MoreFilled /></el-icon>
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item>更多操作</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </template>
- </el-table-column>
- </el-table>
- <div v-if="queryParams.kb" class="pagination-container">
- <div class="pagination-info">
- 显示 {{ (pagination.page - 1) * pagination.pageSize + 1 }} 到
- {{ Math.min(pagination.page * pagination.pageSize, pagination.total) }} 条,
- 共 {{ pagination.total }} 条记录
- </div>
- <el-pagination
- v-model:current-page="pagination.page"
- v-model:page-size="pagination.pageSize"
- :total="pagination.total"
- :page-sizes="[10, 20, 50, 100]"
- layout="prev, pager, next, sizes"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </el-card>
- <el-dialog
- v-model="dialogVisible"
- :title="dialogType === 'add' ? '新建片段' : '编辑片段'"
- width="800px"
- @close="resetForm"
- class="snippet-dialog"
- >
- <el-form :model="formData" label-width="100px" class="snippet-form">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="所属知识库" required>
- <el-select
- v-model="formData.collection_name"
- placeholder="请选择知识库"
- :disabled="dialogType === 'edit'"
- @change="handleKbChange"
- style="width: 100%"
- >
- <el-option
- v-for="item in kbOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="文档名称" required>
- <el-select
- v-model="formData.doc_name"
- placeholder="请输入文档名称进行搜索"
- filterable
- remote
- :remote-method="loadDocOptions"
- :loading="docLoading"
- style="width: 100%"
- @focus="() => loadDocOptions('')"
- @change="handleDocChange"
- >
- <el-option
- v-for="item in docOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-form-item label="父段ID">
- <el-input v-model="formData.parent_id" placeholder="可选: 输入父段ID" />
- </el-form-item>
- <el-form-item label="片段标签">
- <el-tree-select
- v-model="formData.tag_ids"
- :data="tagTreeData"
- multiple
- :render-after-expand="false"
- show-checkbox
- check-strictly
- node-key="id"
- :props="{ label: 'name', children: 'children' }"
- placeholder="请选择标签"
- style="width: 100%"
- />
- </el-form-item>
- <!-- 动态渲染元数据字段 -->
- <template v-if="currentKbSchema.length > 0">
- <el-divider content-position="left">元数据信息</el-divider>
- <el-row :gutter="20">
- <el-col :span="12" v-for="field in currentKbSchema" :key="field.field_en_name">
- <el-form-item :label="field.field_zh_name || field.field_en_name">
- <el-input
- v-if="field.field_type === 'text'"
- v-model="formData.custom_fields[field.field_en_name]"
- :placeholder="'请输入' + (field.field_zh_name || field.field_en_name)"
- />
- <el-input-number
- v-else-if="field.field_type === 'num'"
- v-model="formData.custom_fields[field.field_en_name]"
- :placeholder="'请输入' + (field.field_zh_name || field.field_en_name)"
- style="width: 100%"
- />
- <el-input
- v-else
- v-model="formData.custom_fields[field.field_en_name]"
- :placeholder="'请输入' + (field.field_zh_name || field.field_en_name)"
- />
- </el-form-item>
- </el-col>
- </el-row>
- </template>
- <el-form-item label="片段内容" required>
- <el-input
- v-model="formData.content"
- type="textarea"
- :rows="15"
- placeholder="请输入知识片段的具体内容..."
- resize="vertical"
- />
- </el-form-item>
- </el-form>
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="dialogVisible = false" size="large">取消</el-button>
- <el-button type="primary" @click="handleSubmit" :loading="submitLoading" size="large">
- 确定
- </el-button>
- </span>
- </template>
- </el-dialog>
- <!-- View Dialog -->
- <el-dialog
- v-model="viewDialogVisible"
- title="知识片段详情"
- width="700px"
- class="view-dialog"
- >
- <el-descriptions :column="2" border>
- <el-descriptions-item label="片段编号">{{ viewData.code }}</el-descriptions-item>
- <el-descriptions-item label="文档名称">{{ viewData.doc_name }}</el-descriptions-item>
- <el-descriptions-item label="所属知识库">{{ viewData.collection_name }}</el-descriptions-item>
- <el-descriptions-item label="字符数量">{{ viewData.char_count }}</el-descriptions-item>
- <el-descriptions-item label="状态">
- <el-tag :type="viewData.status === 'normal' ? 'success' : 'info'" effect="plain">
- {{ viewData.status === 'normal' ? '启用' : '禁用' }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="标签">
- <div class="tags-container">
- <el-tag
- v-for="(tag, index) in parseTags(viewData.tag_list)"
- :key="index"
- size="small"
- class="snippet-tag"
- >
- {{ tag }}
- </el-tag>
- </div>
- </el-descriptions-item>
- <el-descriptions-item label="元数据信息">{{ formatMetaInfo(viewData) }}</el-descriptions-item>
- <el-descriptions-item label="创建时间">{{ viewData.created_at || '-' }}</el-descriptions-item>
- <el-descriptions-item label="修改时间">{{ viewData.updated_at || '-' }}</el-descriptions-item>
- </el-descriptions>
-
- <div class="view-content-section">
- <div class="section-title">片段内容</div>
- <div class="content-box">
- {{ viewData.content }}
- </div>
- </div>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted } from 'vue'
- import { Search, Plus, Document, View, Edit, Delete, MoreFilled, Download, Filter } from '@element-plus/icons-vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import {
- getSnippets,
- createSnippet,
- updateSnippet,
- deleteSnippet,
- type Snippet
- } from '@/api/snippet'
- import { getKnowledgeBases, getKnowledgeBaseMetadata, type KnowledgeBase } from '@/api/knowledge-base'
- import { tagApi } from '@/api/tag'
- // Table Data
- const tableData = ref<Snippet[]>([])
- const queryParams = reactive({
- page: 1,
- pageSize: 10,
- keyword: '',
- status: '',
- kb: ''
- })
- const loading = ref(false)
- const pagination = reactive({
- page: 1,
- pageSize: 10,
- total: 0
- })
- // Dialog
- const dialogVisible = ref(false)
- const dialogType = ref<'add' | 'edit'>('add')
- const submitLoading = ref(false)
- const formData = reactive({
- id: '',
- collection_name: '',
- doc_name: '',
- selected_doc_id: '',
- selected_doc_name: '',
- parent_id: '',
- content: '',
- custom_fields: {} as Record<string, any>,
- tag_ids: [] as number[]
- })
- // 解析标签字符串
- const parseTags = (tagListStr: string | undefined) => {
- if (!tagListStr) return []
- if (tagListStr.includes(',')) {
- return tagListStr.split(',').filter(t => t)
- }
- return [tagListStr]
- }
- const getTagNamesByIds = (ids: number[]) => {
- const names: string[] = []
- const findName = (nodes: any[]) => {
- for (const node of nodes) {
- if (ids.includes(node.id)) {
- names.push(node.name)
- }
- if (node.children) {
- findName(node.children)
- }
- }
- }
- findName(tagTreeData.value)
- return names.join(',')
- }
- const getTagIdsByNames = (tagStr: string) => {
- if (!tagStr) return []
- const names = tagStr.split(',').filter(t => t)
- const ids: number[] = []
-
- const findId = (nodes: any[]) => {
- for (const node of nodes) {
- if (names.includes(node.name)) {
- ids.push(node.id)
- }
- if (node.children) {
- findId(node.children)
- }
- }
- }
- findId(tagTreeData.value)
- return ids
- }
- const docOptions = ref<{label: string, value: string}[]>([])
- const docLoading = ref(false)
- import { documentApi, type DocumentItem } from '@/api/document'
- // ...
- const loadDocOptions = async (query: string) => {
- // 只有当有输入或者初始化(空字符串)时才搜索
- // 这里允许空字符串查询,即显示最近的一些文档
- docLoading.value = true
- try {
- const res = await documentApi.getList({
- page: 1,
- size: 50, // 限制返回数量,作为搜索结果
- keyword: query, // 将输入作为关键字
- // whether_to_enter: 1 // 移除此过滤,搜索所有文档
- }, true)
- if (res.code === 0) {
- // 映射为 label (title) 和 value (id)
- docOptions.value = res.data.items.map(item => ({
- label: item.title,
- value: item.id
- }))
- }
- } catch (error) {
- console.error("加载文档列表失败", error)
- } finally {
- docLoading.value = false
- }
- }
- // View Dialog
- const viewDialogVisible = ref(false)
- const viewData = ref<Snippet>({} as Snippet)
- const currentKbSchema = ref<any[]>([]) // 当前选中知识库的自定义Schema
- // 当选择知识库变化时,加载对应的 Schema 字段
- const handleKbChange = async (collection_name: string) => {
- // 清空文档选择
- formData.doc_name = ''
- formData.selected_doc_id = ''
- formData.selected_doc_name = ''
- docOptions.value = []
-
- // 自动加载文档列表 (初始加载最近文档)
- loadDocOptions('')
- // 找到对应的 KB ID
- const kb = kbOptions.value.find(k => k.value === collection_name)
- if (!kb) {
- currentKbSchema.value = []
- return
- }
-
- // 加载知识库的元数据定义
- try {
- const res = await getKnowledgeBaseMetadata(kb.id)
- if (res.code === 0) {
- if (Array.isArray(res.data)) {
- currentKbSchema.value = res.data
- } else if (res.data && Array.isArray(res.data.metadata_fields)) {
- currentKbSchema.value = res.data.metadata_fields
- } else {
- currentKbSchema.value = []
- }
- }
- } catch (error) {
- console.error("加载元数据定义失败", error)
- currentKbSchema.value = []
- }
-
- // 初始化 custom_fields
- formData.custom_fields = {}
- }
- // Methods
- const loadData = async () => {
- // 强制检查是否选择了知识库
- if (!queryParams.kb) {
- tableData.value = []
- pagination.total = 0
- return
- }
- loading.value = true
- try {
- const res = await getSnippets({
- page: pagination.page,
- page_size: pagination.pageSize,
- keyword: queryParams.keyword,
- kb: queryParams.kb, // 知识库集合名称
- status: queryParams.status // 传递状态参数
- })
- if (res.code === 0) {
- tableData.value = res.data
- pagination.total = res.meta?.total || 0
- } else {
- // 如果返回需要选择知识库的提示,清空列表
- tableData.value = []
- pagination.total = 0
- if (res.message !== "请选择知识库") {
- ElMessage.warning(res.message)
- }
- }
- } catch (error) {
- console.error(error)
- // error handled by interceptor
- } finally {
- loading.value = false
- }
- }
- // 加载知识库列表供筛选
- const kbOptions = ref<{label: string, value: string, id: string}[]>([])
- const loadKbOptions = async () => {
- try {
- const res = await getKnowledgeBases({ page_size: 100 })
- if (res.code === 0) {
- kbOptions.value = res.data.map((item: any) => ({
- label: item.name,
- value: item.collection_name,
- id: item.id
- }))
- // 修改为默认不选中(查询所有),直接加载数据
- // if (!queryParams.kb) {
- // loadData()
- // }
-
- // 默认选中第一个知识库(如果存在),并加载数据
- if (kbOptions.value.length > 0 && !queryParams.kb) {
- queryParams.kb = kbOptions.value[0].value
- loadData()
- }
- }
- } catch (error) {
- console.error("加载知识库选项失败", error)
- }
- }
- // 标签树数据
- const tagTreeData = ref<any[]>([])
- // 递归处理树数据,禁用非 label 节点
- const processTagTree = (nodes: any[]) => {
- return nodes.map(node => {
- // 如果不是 label 类型,禁用选择
- if (node.type !== 'label') {
- node.disabled = true
- }
-
- if (node.children && node.children.length > 0) {
- node.children = processTagTree(node.children)
- }
- return node
- })
- }
- // 加载标签树
- const loadTagTree = async () => {
- try {
- const res = await tagApi.getCategoryTree(false) // 不包含禁用
- if (res.code === 200) {
- tagTreeData.value = processTagTree(res.data)
- }
- } catch (error) {
- console.error("加载标签树失败", error)
- }
- }
- onMounted(() => {
- loadKbOptions()
- loadTagTree()
- })
- // Methods
- const handleSearch = () => {
- pagination.page = 1
- loadData()
- }
- const handleAdd = async () => {
- dialogType.value = 'add'
- resetForm()
- // 如果筛选栏选中了知识库,默认填入并加载元数据
- if (queryParams.kb) {
- formData.collection_name = queryParams.kb
- await handleKbChange(queryParams.kb)
- }
- dialogVisible.value = true
- }
- const handleEdit = async (row: Snippet) => {
- dialogType.value = 'edit'
- formData.id = row.id
- formData.collection_name = row.collection_name
-
- // 触发加载 Schema (会重置 doc_name 和 custom_fields)
- await handleKbChange(row.collection_name)
- // 恢复数据
- formData.doc_name = row.doc_name
- formData.content = row.content
-
- // 恢复 parent_id
- formData.parent_id = row.parent_id || (row.metadata && (row.metadata as any).parent_id) || ''
-
- // 恢复自定义字段
- if (row.metadata && typeof row.metadata === 'object') {
- formData.custom_fields = { ...row.metadata }
- } else {
- formData.custom_fields = {}
- }
- // 回显标签
- formData.tag_ids = getTagIdsByNames(row.tag_list || (row as any).tags)
-
- dialogVisible.value = true
- }
- const handleDocChange = async (val: string) => {
- // val 是选中的文档ID
- // 找到对应的文档名称用于显示(如果需要的话,但 formData.doc_name 已经绑定了 val 即 ID)
- // 这里我们需要注意:formData.doc_name 存储的是 ID 还是 Name?
- // 根据用户需求:"新建时选择的文档的id就是对应知识库的schema中的document_id的值"
- // 同时也需要显示文档名称
-
- // 实际上,Snippet 结构中 doc_name 应该存名称,document_id 存 ID
- // 但前端 el-select v-model 绑定的是 value (ID)
-
- // 我们需要调整 formData 结构或者在提交时处理
- // 让我们查找选中的项
- const selected = docOptions.value.find(item => item.value === val)
- if (selected) {
- // 将选中的文档ID保存到 custom_fields.document_id (如果 Schema 中有) 或者单独保存
- // 实际上 Snippet Create 接口需要 doc_name 和 document_id
- // 我们可以暂时将 ID 存入 formData 的一个临时字段,或者直接修改提交逻辑
- formData.selected_doc_id = val
- formData.selected_doc_name = selected.label
-
- // 自动填充元数据 (如果当前 KB 有定义的 Schema)
- if (currentKbSchema.value.length > 0) {
- try {
- // 获取文档详情
- const res = await documentApi.getDetail(val)
- if (res.code === 0 && res.data) {
- const doc = res.data
-
- // 定义元数据字段名与文档属性的映射关系
- const mapping: Record<string, keyof DocumentItem> = {
- 'file_name': 'title',
- 'title': 'title',
- 'standard_number': 'standard_no',
- 'standard_no': 'standard_no',
- 'issuing_authority': 'issuing_authority',
- 'document_type': 'document_type',
- 'professional_field': 'professional_field',
- 'validity': 'validity',
- 'file_url': 'file_url',
- 'plan_type_list': 'plan_category',
- 'plan_category': 'plan_category'
- }
-
- // 遍历当前 KB 定义的所有字段
- currentKbSchema.value.forEach(field => {
- const fieldName = field.field_en_name
- let docValue: any = null
-
- // 1. 尝试映射匹配
- if (mapping[fieldName] && doc[mapping[fieldName]] !== undefined) {
- docValue = doc[mapping[fieldName]]
- }
- // 2. 尝试直接属性名匹配
- else if (fieldName in doc) {
- docValue = doc[fieldName as keyof DocumentItem]
- }
-
- // 3. 特殊处理:层级信息 (hierarchy)
- if (fieldName === 'hierarchy') {
- const levels = [
- doc.level_1_classification,
- doc.level_2_classification,
- doc.level_3_classification,
- doc.level_4_classification
- ].filter(l => l) // 过滤掉空值
-
- if (levels.length > 0) {
- docValue = levels.join('/')
- }
- }
- // 如果找到了对应的值,且不为空,则填充
- if (docValue !== undefined && docValue !== null && docValue !== '') {
- formData.custom_fields[fieldName] = docValue
- }
- })
- ElMessage.success('已自动填充文档元数据')
- }
- } catch (error) {
- console.error("自动填充元数据失败", error)
- }
- }
- }
- }
- const handleSubmit = async () => {
- if (!formData.collection_name || !formData.content) {
- ElMessage.warning('请填写完整信息')
- return
- }
-
- const tagListStr = getTagNamesByIds(formData.tag_ids)
- submitLoading.value = true
- try {
- if (dialogType.value === 'add') {
- await createSnippet({
- collection_name: formData.collection_name,
- // 如果用户选择了文档,使用选择的名称;否则(兼容旧逻辑)使用输入值
- doc_name: formData.selected_doc_name || formData.doc_name || '手动添加',
- content: formData.content,
- meta_info: '',
- custom_fields: {
- ...formData.custom_fields,
- parent_id: formData.parent_id,
- // 传递 document_id
- document_id: formData.selected_doc_id,
- tag_list: tagListStr
- }
- })
- ElMessage.success('创建成功')
- } else {
- // 编辑模式下的处理...
- await updateSnippet(formData.id, {
- collection_name: formData.collection_name,
- doc_name: formData.doc_name, // 编辑时通常不改文档归属,或者需要同样的逻辑
- content: formData.content,
- custom_fields: {
- ...formData.custom_fields,
- parent_id: formData.parent_id,
- tag_list: tagListStr
- }
- })
- ElMessage.success('更新成功')
- }
- dialogVisible.value = false
- loadData()
- } catch (error) {
- console.error(error)
- } finally {
- submitLoading.value = false
- }
- }
- const resetForm = () => {
- formData.id = ''
- formData.collection_name = ''
- formData.doc_name = ''
- formData.selected_doc_id = ''
- formData.selected_doc_name = ''
- formData.parent_id = ''
- formData.content = ''
- formData.custom_fields = {}
- formData.tag_ids = []
- docOptions.value = []
- currentKbSchema.value = []
- }
- const handleDelete = (row: Snippet) => {
- ElMessageBox.confirm(
- '确定要删除该知识片段吗?',
- '警告',
- {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- }
- ).then(async () => {
- try {
- await deleteSnippet(row.id, row.collection_name)
- ElMessage.success('删除成功')
- loadData()
- } catch (error) {
- // handled
- }
- })
- }
- const handleView = (row: Snippet) => {
- viewData.value = { ...row }
- viewDialogVisible.value = true
- }
- const handleSelectionChange = (val: any) => {
- console.log(val)
- }
- const formatMetaInfo = (row: Snippet) => {
- // 优先展示 metadata 字段中的信息
- if (row.metadata) {
- // 如果是 JSON 对象,尝试格式化展示
- if (typeof row.metadata === 'object') {
- // 排除一些不需要展示的字段,如 doc_name, file_name, title (因为已有单独列)
- const displayParts = []
- for (const [key, value] of Object.entries(row.metadata)) {
- if (!['doc_name', 'file_name', 'title'].includes(key)) {
- // 简单格式化:Key: Value
- // 如果 value 也是对象,转字符串
- const valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
- displayParts.push(`${key}: ${valStr}`)
- }
- }
- if (displayParts.length > 0) return displayParts.join(' | ')
- } else if (typeof row.metadata === 'string') {
- try {
- // 尝试解析 JSON 字符串
- const metaObj = JSON.parse(row.metadata)
- const displayParts = []
- for (const [key, value] of Object.entries(metaObj)) {
- if (!['doc_name', 'file_name', 'title'].includes(key)) {
- const valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
- displayParts.push(`${key}: ${valStr}`)
- }
- }
- if (displayParts.length > 0) return displayParts.join(' | ')
- } catch (e) {
- return String(row.metadata)
- }
- }
- }
- // 其次尝试 parent_id
- if (row.parent_id && row.parent_id !== '0' && row.parent_id !== '') {
- return `ParentID: ${row.parent_id}`
- }
- // 最后尝试 meta_info (旧字段)
- if (row.meta_info && row.meta_info !== "ParentID: -" && row.meta_info !== "ParentID: ") {
- return row.meta_info
- }
-
- return '-'
- }
- const handleSizeChange = (val: number) => {
- pagination.pageSize = val
- loadData()
- }
- const handleCurrentChange = (val: number) => {
- pagination.page = val
- loadData()
- }
- </script>
- <style scoped>
- .snippet-container {
- padding: 20px;
- }
- .snippet-dialog :deep(.el-dialog__body) {
- padding: 20px 30px;
- }
- .header-section {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- }
- .title-info h2 {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- color: #303133;
- }
- .subtitle {
- margin: 8px 0 0;
- color: #909399;
- font-size: 14px;
- }
- .table-card {
- border-radius: 8px;
- }
- .doc-name-cell {
- display: flex;
- align-items: center;
- color: #606266;
- }
- .doc-icon {
- margin-right: 8px;
- font-size: 16px;
- }
- .content-cell {
- overflow: hidden;
- text-overflow: ellipsis;
- /* display: -webkit-box; 移除多行省略,改为单行截断逻辑由JS控制,或者保留样式但内容已截断 */
- /* -webkit-line-clamp: 2; */
- /* -webkit-box-orient: vertical; */
- color: #303133;
- line-height: 1.5;
- white-space: normal; /* 允许换行,但内容已被截断 */
- }
- .meta-info {
- color: #909399;
- font-size: 13px;
- display: inline-block;
- max-width: 100%;
- }
- .meta-info.truncate {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .status-tag {
- border-radius: 4px;
- padding: 0 12px;
- height: 24px;
- line-height: 22px;
- }
- .empty-state {
- padding: 40px;
- background: #fff;
- border-radius: 8px;
- }
- .pagination-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 20px;
- }
- .pagination-info {
- color: #909399;
- font-size: 13px;
- }
- .view-content-section {
- margin-top: 20px;
- }
- .view-content-section .section-title {
- font-weight: 600;
- margin-bottom: 10px;
- font-size: 15px;
- color: #303133;
- }
- .content-box {
- background-color: #f5f7fa;
- padding: 15px;
- border-radius: 4px;
- border: 1px solid #e4e7ed;
- color: #606266;
- line-height: 1.6;
- max-height: 400px;
- overflow-y: auto;
- white-space: pre-wrap;
- word-break: break-all;
- }
- .snippet-form {
- padding-right: 20px;
- }
- .tags-container {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- }
- .snippet-tag {
- margin-right: 0;
- }
- .no-tags {
- color: #909399;
- font-size: 12px;
- }
- </style>
|