Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,16 +630,18 @@ System 接口由资源管理服务提供;本地开源后端没有对应扩展
标签查询暂保留在 `workspace/knowledge/knowledge.ts`,`getKnowledgeTags(knowledgeId)` 返回非分页 `KnowledgeTagGroup[]`;
标签值中的可选 `doc_count` 表示关联文档数量。`postKnowledgeTags` 直接提交
`KnowledgeTagPayload[]`(`key`、`value`),用于创建标签和新增标签值;
`putKnowledgeTag` 向 `/<tagId>` 提交 `KnowledgeTagUpdatePayload`(`id`、`key`、`value`),仅用于已有的单项编辑。
`putKnowledgeTag` 向 `/<tagId>` 提交完整 `KnowledgeTagUpdatePayload`(`id`、`key`、`value`),
路径 `tagId` 与载荷 `id` 保持一致。重命名提交新 `key` 与该行原 `value`,服务端更新当前标签所属同名组;
编辑单个值提交原 `key` 与新 `value`,保留同组重复值校验,不提交其他行的值。
`deleteKnowledgeTag` 按路径末尾 `key` 删除整个标签、`one` 删除单个标签值;
`putBatchDeleteKnowledgeTags` 使用 PUT `batch_delete`,请求体直接为标签 ID 数组,不包装为 `id_list`。
`postImportKnowledgeTags(knowledgeId, file)` POST 到 `import`,使用 FormData 的单个 `file` 字段上传 XLS 或 XLSX,
响应解包为 `void`。文件第一行为“标签、标签值”,每行对应一个标签与值;同名标签可重复出现,
服务端跳过文件内及知识库已有的重复组合,校验所有工作表后统一写入。空行跳过,标签与值分别限制为 64、128 字。
`exportKnowledgeTagTemplate(knowledgeId)` GET 到 `template/export`,复用 `getExportFile` 下载“标签模板.xlsx”,
服务端返回与提供的模板一致的表头和示例数据。两个接口均校验目标知识库的工作空间及标签创建权限。
标签类型统一维护在 `types/knowledge.ts`。多行编辑等待 V3 新协议,不组合旧接口模拟整组保存,
也不声明推测的新接口。
标签类型统一维护在 `types/knowledge.ts`。创建支持多行,编辑仅支持重命名或单个值修改,
不声明多行编辑接口,也不组合旧接口模拟多行保存。

文档与标签的关联由 `workspace/knowledge/document.ts` 维护,不使用标签删除接口代替取消关联。
`getDocumentTags` 查询 `document/<documentId>/tags`,返回分组标签;
Expand Down
5 changes: 1 addition & 4 deletions ui/src/api/admin/workspace/knowledge/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import { getWorkspaceId } from '@/utils/resource-context'

const getPrefix = (knowledgeId: string) => `/workspace/${getWorkspaceId()}/knowledge/${knowledgeId}/tags`

// TODO:接入标签整组/多行编辑接口。
// 请求路径、参数和响应类型以 V3 接口协议为准。

/** 创建知识库标签或为已有标签新增值。 */
const postKnowledgeTags = (knowledgeId: string, tags: KnowledgeTagPayload[]) => post<KnowledgeTagPayload[], void>(getPrefix(knowledgeId), tags)

/** 编辑单个知识库标签,不支持多行编辑。 */
/** 重命名知识库标签或编辑单个标签值。 */
const putKnowledgeTag = (knowledgeId: string, tagId: string, tag: KnowledgeTagUpdatePayload) =>
put<KnowledgeTagUpdatePayload, void>(`${getPrefix(knowledgeId)}/${tagId}`, tag)

Expand Down
22 changes: 13 additions & 9 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ Dialog。新增或重命名文件时,应同步更新所有导入和页面功
| `home/index.vue` | Workspace 首页 |
| `knowledge/index.vue` | 工作空间知识库目录与知识库卡片页面 |
| `knowledge-detail/index.vue` | 知识库详情页面 |
| `knowledge-detail/library/tags/index.vue` | 标签分组查询、创建、单值编辑、删除与关联文档管理 |
| `knowledge-detail/library/tags/index.vue` | 标签分组查询、创建、重命名、单值编辑、删除与关联文档管理 |
| `knowledge-detail/setting/base/index.vue` | 知识库基本信息、来源配置与上传限制设置 |
| `knowledge-detail/setting/document-strategy/index.vue` | Web 知识库文档处理策略回填、校验与保存 |
| `knowledge-detail/setting/scheduled-sync/index.vue` | Web、飞书与工作流知识库定时同步设置与日志 |
Expand Down Expand Up @@ -874,15 +874,19 @@ library 的接口调整已撤回,文档和标签沿用原有调用方式,Sys
标签名旁提供新增值、编辑标签、删除标签;行末提供编辑值和删除值,
数量链接打开同目录 `TagDocumentDrawer.vue`。

`library/tags/components/TagFormDialog.vue` 为标签管理和后续文档入口共用的创建、编辑弹窗,
`library/tags/components/CreateTagDialog.vue` 为顶部创建和标签旁新增值共用的创建弹窗,
接收知识库 ID 与完整 Tags API,参考添加成员流程复用 `MkDialog`、`el-form` 和 `MkFormList`。
创建允许多行标签名与值;新增值锁定标签名;编辑值回填单项并锁定标签名。
标签名最多 64 字、标签值最多 128 字,校验必填、纯空白和表单内重复组合。
创建与单个已有标签值编辑使用已确认接口;整组编辑或编辑中增删行走独立请求入口,
保留编辑前快照、已有 ID 与清理首尾空格后的当前草稿,交由请求入口按 V3 协议映射参数。
所有分支共用校验、loading、成功关闭和刷新;多行接口未接入时仅提示并保留草稿,
不即时删除、不拼接多次旧请求、不模拟成功。
常驻弹窗在 `closed` 清理草稿和校验。
创建允许多行标签名与值;新增值通过 `open(key)` 回填并锁定标签名,新增行沿用该名称。
顶部创建标题为“创建标签”,传入标签名时标题为“添加标签值”。
两个入口统一通过 `postKnowledgeTags` 提交标签数组,校验表单内重复组合。
同目录 `EditTagDialog.vue` 共用标签重命名和单值编辑,通过 `open(tag, field)` 回填独立草稿。
重命名只显示标签名;编辑值只显示一行并锁定标签名。两个入口均提交完整 `id`、`key`、`value`,
请求路径 ID 与载荷 ID 一致,仅修改当前编辑字段,未编辑字段原样保留。
两种编辑均使用 `putKnowledgeTag`,不提供增删行或多行编辑,不组合多个请求模拟保存。
两个弹窗均保留 `MkDialog` 默认 600px 宽度,标签名最多 64 字、标签值最多 128 字,
校验必填与纯空白,提交时清理可编辑字段首尾空格。各自维护 loading、防重复提交与关闭保护;
成功后提示、关闭并发出 `refresh`,失败保留草稿;常驻实例在 `closed` 清理草稿和校验。
后续文档入口直接复用上述弹窗,不复制创建和编辑逻辑。

`library/tags/components/ButtonImportTags.vue` 在同一组件内维护导入按钮与弹窗,
复用 `MkDialog`、`el-form` 和 `MkDragUpload`,不再单独拆分导入 Dialog。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const documentData = ref<DocumentItem[]>([])
const paginationConfig = ref({ currentPage: 1, pageSize: 10, total: 0 })
const documentTableRef = ref<{ clearSelection: () => void }>()
const selectedDocuments = ref<DocumentItem[]>([])
const selectedDocumentIds = computed(() => new Set(selectedDocuments.value.map(({ id }) => id)))

/* 两个页签复用同一文档查询,未关联页通过 tag_exclude 排除当前标签。 */
function loadDocuments(): Promise<void> {
Expand Down Expand Up @@ -78,10 +77,6 @@ function handleSelectionChange(selection: unknown[]) {
selectedDocuments.value = selection as DocumentItem[]
}

function getRowClass({ row }: { row: DocumentItem }) {
return selectedDocumentIds.value.has(row.id) ? '[&>td]:bg-primary/10!' : ''
}

function handleDocumentFilterChange() {
if (loading.value) return
paginationConfig.value.currentPage = 1
Expand Down Expand Up @@ -157,58 +152,54 @@ defineExpose({ open })
<MkSearchInput v-if="activeTab === 'linked'" v-model="searchName" class="w-60!" @change="handleDocumentFilterChange" />
<MkComplexSearch v-else :fields="[{ label: '名称', value: 'name' }]" @change="handleComplexSearchChange" />
</div>
<!-- 仅遮罩表格,指示器固定在表体靠上位置,避免随记录数量上下跳动。 -->
<div v-loading="loading" element-loading-custom-class="[&>.el-loading-spinner]:top-20!" class="relative">
<MkTable
ref="documentTableRef"
v-model:pagination-config="paginationConfig"
:data="documentData"
:row-class-name="getRowClass"
row-key="id"
@current-change="loadDocuments"
@size-change="loadDocuments"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" />
<el-table-column prop="name" label="文档名称" min-width="200">
<template #default="{ row }: { row: DocumentItem }">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(row.name)" alt="" class="size-5 shrink-0" />
<span class="truncate" :title="row.name">{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="is_active" label="启用状态" width="120">
<template #header>
<MkTableFilter
v-model="selectedDocumentActive"
mode="single"
label="启用状态"
:options="documentActiveOptions"
@change="handleDocumentFilterChange"
/>
</template>
<template #default="{ row }: { row: DocumentItem }">
<MkStatusLabel :active="row.is_active" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180">
<template #default="{ row }: { row: DocumentItem }">{{ datetimeFormat(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="70" fixed="right">
<template #default="{ row }: { row: DocumentItem }">
<!-- 关联或取消关联当前文档 -->
<MkAction
display="button"
:label="activeTab === 'linked' ? '取消关联' : '关联'"
:icon="activeTab === 'linked' ? 'icon_unlink_outlined' : 'icon_link-record_outlined'"
:disabled="loading"
@click="handleLinkDocuments([row.id])"
/>
</template>
</el-table-column>
</MkTable>
</div>
<MkTable
ref="documentTableRef"
v-model:pagination-config="paginationConfig"
v-loading="loading"
:data="documentData"
@current-change="loadDocuments"
@size-change="loadDocuments"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" />
<el-table-column prop="name" label="文档名称" min-width="200">
<template #default="{ row }: { row: DocumentItem }">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(row.name)" alt="" class="size-5 shrink-0" />
<span class="truncate" :title="row.name">{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="is_active" label="启用状态" width="120">
<template #header>
<MkTableFilter
v-model="selectedDocumentActive"
mode="single"
label="启用状态"
:options="documentActiveOptions"
@change="handleDocumentFilterChange"
/>
</template>
<template #default="{ row }: { row: DocumentItem }">
<MkStatusLabel :active="row.is_active" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180">
<template #default="{ row }: { row: DocumentItem }">{{ datetimeFormat(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="70" fixed="right">
<template #default="{ row }: { row: DocumentItem }">
<!-- 关联或取消关联当前文档 -->
<MkAction
display="button"
:label="activeTab === 'linked' ? '取消关联' : '关联'"
:icon="activeTab === 'linked' ? 'icon_unlink_outlined' : 'icon_link-record_outlined'"
:disabled="loading"
@click="handleLinkDocuments([row.id])"
/>
</template>
</el-table-column>
</MkTable>
</div>
</MkDrawer>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import type { FormInstance, FormItemRule } from 'element-plus'
import type TagsApi from '@/api/admin/workspace/knowledge/tags'
import type { KnowledgeTagPayload } from '@/api/types'
import { MsgSuccess } from '@/utils/message'

defineOptions({ name: 'CreateTagDialog' })

const props = defineProps<{ knowledgeId: string; api: typeof TagsApi }>()
const emit = defineEmits<{ refresh: [] }>()

const visible = ref(false)
const loading = ref(false)
const formRef = ref<FormInstance>()
const fixedTagKey = ref('')
const tagForm = reactive<{ tags: KnowledgeTagPayload[] }>({ tags: [{ key: '', value: '' }] })
const title = computed(() => (fixedTagKey.value ? '添加标签值' : '创建标签'))
const defaultTag = computed(() => ({ key: fixedTagKey.value, value: '' }))

/* 新增值只回填并锁定标签名,与创建标签共用提交流程。 */
function open(key?: string) {
if (key) {
fixedTagKey.value = key
tagForm.tags = [{ key, value: '' }]
}
visible.value = true
}

function getTagValueRules(index: number): FormItemRule[] {
return [
{ required: true, whitespace: true, message: '请输入标签值', trigger: 'blur' },
{
validator: (_rule, _value, callback) => {
const tag = tagForm.tags[index]
const duplicate =
tag &&
tagForm.tags.some(
(other, otherIndex) => otherIndex !== index && other.key.trim() === tag.key.trim() && other.value.trim() === tag.value.trim(),
)
callback(duplicate ? new Error('相同标签下的标签值不能重复') : undefined)
},
trigger: 'blur',
},
]
}

function handleRowsChange() {
formRef.value?.clearValidate()
}

/* 两个创建入口统一提交标签数组。 */
function handleSubmit() {
if (loading.value) return
return formRef.value?.validate((valid) => {
if (!valid || loading.value || !tagForm.tags.length) return
const tags = tagForm.tags.map((tag) => ({ key: fixedTagKey.value || tag.key.trim(), value: tag.value.trim() }))

loading.value = true
return props.api
.postKnowledgeTags(props.knowledgeId, tags)
.then(() => {
MsgSuccess('创建成功')
visible.value = false
emit('refresh')
})
.finally(() => {
loading.value = false
})
})
}

function handleBeforeClose(done: () => void) {
if (!loading.value) done()
}

function resetData() {
tagForm.tags = [{ key: '', value: '' }]
fixedTagKey.value = ''
loading.value = false
formRef.value?.clearValidate()
}

defineExpose({ open })
</script>

<template>
<MkDialog v-model="visible" :title="title" :before-close="handleBeforeClose" @closed="resetData">
<el-form ref="formRef" v-loading="loading" :model="tagForm" label-position="top" @submit.prevent>
<MkFormList v-model="tagForm.tags" :default-item="defaultTag" @update:model-value="handleRowsChange">
<template #default="{ index, item: tag }">
<el-form-item
class="min-w-0 flex-1"
:label="index === 0 ? '标签' : ''"
:prop="`tags.${index}.key`"
:rules="{ required: true, whitespace: true, message: '请输入标签', trigger: 'blur' }"
>
<el-input v-model="tag.key" :disabled="!!fixedTagKey" :maxlength="64" placeholder="请输入标签" />
</el-form-item>
<el-form-item class="min-w-0 flex-1" :label="index === 0 ? '标签值' : ''" :prop="`tags.${index}.value`" :rules="getTagValueRules(index)">
<el-input v-model="tag.value" :maxlength="128" placeholder="请输入标签值" />
</el-form-item>
</template>
</MkFormList>
</el-form>
<template #footer>
<!-- 取消创建标签 -->
<el-button plain :disabled="loading" @click="visible = false">取消</el-button>
<!-- 创建标签 -->
<el-button type="primary" :loading="loading" @click="handleSubmit">创建</el-button>
</template>
</MkDialog>
</template>
Loading
Loading