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
2 changes: 1 addition & 1 deletion ui/src/api/admin/workspace/knowledge/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const getDocumentPage = (knowledgeId: string, page: ParamsPage, query?: Dict<unk
const putBatchCreateDocuments = (knowledgeId: string, documents: DocumentQuickCreatePayload[] | DocumentImportPayload[]) =>
put<DocumentQuickCreatePayload[] | DocumentImportPayload[], DocumentItem[]>(`${getPrefix(knowledgeId)}/batch_create`, documents)

/** 按处理策略解析文本文件,返回分段与原文件关联信息。 */
/** 分段预览 */
const postSplitDocuments = (knowledgeId: string, files: File[], strategy: DocumentStrategy) => {
const data = new FormData()
files.forEach((file) => data.append('file', file))
Expand Down
5 changes: 5 additions & 0 deletions ui/src/styles/STYLE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,11 @@ Tailwind v4 的 `border` 只设置边框宽度和样式,不提供 `--tw-border
`height: auto`。Element Plus 默认尺寸未定义 `--el-button-size`,必须回退到 32px 的
`--el-component-size`,避免高度由内容和内边距撑开;业务需要的内边距和禁用颜色单独保留。

树形 Table 的名称列统一通过 `el-table-column` 的 `class-name="expand-name-column"`
启用同行布局。样式维护在 `element-plus.scss` 的 table 分组,令单元格使用纵向居中的 Flex,
并禁止展开图标、缩进和占位元素收缩,使长名称截断时保持目录层级。
资源授权 `PermissionTable` 和飞书导入表格共用该类,页面不再重复声明对应的 `:deep()` 样式。

Tree 的 `.el-tree-node__content` 统一保留 4px 底部间距,使同级节点及展开后的父子节点行
保持一致间距;规则维护在 `element-plus.scss` 的 tree 分组。

Expand Down
12 changes: 12 additions & 0 deletions ui/src/styles/element-plus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,18 @@
border-right: 1px solid var(--el-table-border-color);
}

/* 树形名称列:展开图标、缩进与业务内容同行。 */
.expand-name-column > .cell {
align-items: center;
display: flex;

.el-table__expand-icon,
.el-table__indent,
.el-table__placeholder {
flex-shrink: 0;
}
}

[class*='el-table__row--level'] .el-table__expand-icon {
align-items: center;
display: inline-flex;
Expand Down
11 changes: 8 additions & 3 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1068,14 +1068,19 @@ Web、飞书和工作流知识库的非共享文档页在标题栏组合
不传完整 `knowledge`,不重复查询知识库。
入口按类型挂载独立的 `WebImportDocumentDrawer.vue`、`LarkImportDocumentDrawer.vue`、
`WorkflowImportDocumentDrawer.vue`,关闭动画结束后卸载。
Web 和飞书使用全屏底部 Drawer,分别通过地址表单和飞书懒加载文件树进入文档处理策略;
Web 和飞书使用全屏底部 Drawer,分别通过地址表单和飞书 `MkTable` 懒加载目录表格进入文档处理策略;
策略复用 `DocumentStrategyForm`,使用组件默认策略,不回显知识库策略,上一步保留草稿。
Web 抽屉只接收完整 Document API 与知识库 ID,地址和选择器初始为空;
飞书抽屉额外接收文件夹 Token,工作流抽屉只接收知识库 ID 和图数据;所有导入组件均不接收 `KnowledgeDetail`。
Web 地址按行去空白、去重并校验 HTTP/HTTPS;空选择器提交 `body`。
飞书按 `meta.folder_token` 查询目录并读取全部分页,已导入文档勾选且不可重复导入;
全选覆盖当前已加载节点,选择文件夹时由树懒加载子节点,加载期间禁止进入下一步。
正式导入只提交未导入的文件,不将文件夹作为文档。
表格使用原生 `type="selection"` 选择列和名称列,表头全选覆盖当前已加载节点;
初始化仅加载根目录,文件夹默认收起,展开与收起由 Table 维护。
不重复保存表格选择状态,在下一步通过 `getSelectionRows()` 获取待导入文档。
文件夹持续保留懒加载标记,由 Table 维护层级和缩进,避免与普通树数据混用导致层级丢失。
原生选择维护已加载节点的父子联动与半选,勾选文件夹不请求子目录,只有展开时加载。
展开已选文件夹时新加载的节点继承勾选,加载期间禁止进入下一步。
正式导入沿用 v2,仅过滤已导入项,保留所选文件夹的名称、Token 和类型,不在前端递归展开文件夹。
两者提交成功关闭并刷新列表,失败保留输入、选择和策略。
工作流导入 Drawer 与 DebugDrawer 使用相同的三步 Action,直接引用
`views/workflow/knowledge/debug/action/` 的 DataSource、KnowledgeBase 和 Result,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, nextTick, ref, useTemplateRef } from 'vue'
import type { ElTree, LoadFunction } from 'element-plus'
import { nextTick, ref, useTemplateRef } from 'vue'
import type { TableInstance, TreeNode } from 'element-plus'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import type { LarkDocumentNode } from '@/api/types'
import DocumentStrategyForm from '@/views/knowledge/create-knowledge/components/DocumentStrategyForm.vue'
Expand All @@ -11,80 +11,80 @@ defineOptions({ name: 'LarkImportDocumentDrawer' })
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; folderToken: string }>()
const emit = defineEmits<{ refresh: []; closed: [] }>()

/* 飞书文件树与选择状态:只导入未导入的文件,文件夹用于导航。 */
/* 飞书目录与选择:仅展开时加载,文件夹沿用 v2 导入协议。 */
interface LarkDocumentRow extends LarkDocumentNode {
hasChildren: boolean
}
const visible = ref(false)
const loading = ref(false)
const pendingLoads = ref(0)
const activeStep = ref(0)
const strategyMounted = ref(false)
const loadedNodes = ref<LarkDocumentNode[]>([])
const checkedTokens = ref<string[]>([])
const documentRows = ref<LarkDocumentRow[]>([])
const documentTableRef = useTemplateRef<{ tableRef?: TableInstance }>('documentTableRef')
const selectedDocuments = ref<LarkDocumentNode[]>([])
const treeRef = useTemplateRef<InstanceType<typeof ElTree>>('treeRef')
const strategyRef = useTemplateRef<InstanceType<typeof DocumentStrategyForm>>('strategyRef')
const selectableNodes = computed(() => loadedNodes.value.filter((node) => !node.is_exist))
const allChecked = computed(() => selectableNodes.value.length > 0 && selectableNodes.value.every((node) => checkedTokens.value.includes(node.token)))
const indeterminate = computed(() => !allChecked.value && selectableNodes.value.some((node) => checkedTokens.value.includes(node.token)))
const treeProps = {
label: 'name',
isLeaf: (node: LarkDocumentNode) => node.type !== 'folder',
disabled: (node: LarkDocumentNode) => node.is_exist,
const folderRequests = new Map<string, Promise<LarkDocumentRow[]>>()
function selectImportedDocuments(documents: LarkDocumentRow[]) {
documents.filter((document) => document.is_exist).forEach((document) => documentTableRef.value?.tableRef?.toggleRowSelection(document, true, true))
}
function open() {
visible.value = true
}
function handleCheck() {
checkedTokens.value = (treeRef.value?.getCheckedKeys() ?? []).map(String)
}
function handleAllCheck(checked: unknown) {
const tokens = loadedNodes.value.filter((node) => node.is_exist || Boolean(checked)).map((node) => node.token)
treeRef.value?.setCheckedKeys(tokens)
handleCheck()
void loadFolder(props.folderToken)
.then((documents) => {
documentRows.value = documents
return nextTick(() => selectImportedDocuments(documents))
})
.catch(() => {
/* 请求层提示错误,可通过重新打开抽屉重试。 */
})
}

/* 懒加载目录:一次读完该目录分页,失败恢复可重试状态。 */
const loadNode: LoadFunction = (node, resolve, reject) => {
const token = node.level === 0 ? props.folderToken : String(node.data.token)
if (!token) {
resolve([])
return
}
/* 懒加载目录:读取全部分页,同一目录请求复用,失败可重试。 */
function loadFolder(token: string): Promise<LarkDocumentRow[]> {
if (!token) return Promise.resolve([])
const existingRequest = folderRequests.get(token)
if (existingRequest) return existingRequest
pendingLoads.value += 1
const folderDocuments: LarkDocumentNode[] = []
const folderDocuments: LarkDocumentRow[] = []
function loadPage(pageToken?: string): Promise<void> {
return props.api.getLarkDocumentList(props.knowledgeId, token, pageToken ? { page_token: pageToken } : {}).then((page) => {
folderDocuments.push(...page.files)
folderDocuments.push(...page.files.map((document) => ({ ...document, hasChildren: document.type === 'folder' })))
if (page.has_more && page.next_page_token) return loadPage(page.next_page_token)
})
}
void loadPage()
.then(() => {
const selectLoaded = allChecked.value
loadedNodes.value.push(...folderDocuments)
resolve(folderDocuments)
return nextTick(() => {
folderDocuments.forEach((document) => {
if (document.is_exist || selectLoaded) treeRef.value?.setChecked(document.token, true, false)
})
handleCheck()
})
})
.catch(() => {
reject?.()
const request = loadPage()
.then(() => folderDocuments)
.catch((error: unknown) => {
folderRequests.delete(token)
throw error
})
.finally(() => {
pendingLoads.value -= 1
})
folderRequests.set(token, request)
return request
}
function loadNode(document: LarkDocumentRow, treeNode: TreeNode, resolve: (documents: LarkDocumentRow[]) => void) {
void loadFolder(document.token)
.then((documents) => {
resolve(documents)
return nextTick(() => selectImportedDocuments(documents))
})
.catch(() => {
/* Table 无 reject 回调,恢复加载标记以便再次展开重试。 */
treeNode.loading = false
})
}

/* 处理策略与正式导入 */
function handleNext() {
if (loading.value || pendingLoads.value) return
selectedDocuments.value = ((treeRef.value?.getCheckedNodes(true) ?? []) as LarkDocumentNode[]).filter(
(node) => node.type !== 'folder' && !node.is_exist,
selectedDocuments.value = ((documentTableRef.value?.tableRef?.getSelectionRows() ?? []) as LarkDocumentNode[]).filter(
(document) => !document.is_exist,
)
if (!selectedDocuments.value.length) {
MsgWarning('请选择需要导入的文档')
MsgWarning('请选择文档')
return
}
strategyMounted.value = true
Expand All @@ -109,7 +109,7 @@ function handleSubmit() {
})),
)
.then(() => {
MsgSuccess('导入任务提交成功')
MsgSuccess('导入成功')
visible.value = false
emit('refresh')
})
Expand Down Expand Up @@ -146,36 +146,32 @@ defineExpose({ open })
</ol>
</el-alert>

<div class="mk-gray-card-lg mb-3">
<el-checkbox :model-value="allChecked" :indeterminate="indeterminate" :disabled="pendingLoads > 0" @change="handleAllCheck">
全部文档
</el-checkbox>
</div>

<el-tree
ref="treeRef"
:props="treeProps"
<MkTable
ref="documentTableRef"
v-loading="pendingLoads > 0"
:data="documentRows"
row-key="token"
:load="loadNode"
node-key="token"
:max-table-height="430"
lazy
show-checkbox
style="--el-tree-node-content-height: 44px"
@check="handleCheck"
>
<template #default="{ data }: { data: LarkDocumentNode }">
<div class="flex-align-center gap-2 py-2">
<img v-if="data.type === 'folder'" src="@/assets/file-type/file-icon.svg" alt="" class="size-5" />
<img
v-else
:src="getFileIconUrl(data.type === 'docx' ? `${data.name}.docx` : data.type === 'sheet' ? `${data.name}.xlsx` : data.name)"
alt=""
class="size-5"
/>
<span :title="data.name" class="truncate">{{ data.name }}</span>
<span v-if="data.is_exist" class="text-N600 text-sm">已导入</span>
</div>
</template>
</el-tree>
<!-- 选择文档或文件夹,全选由表格维护 -->
<el-table-column type="selection" width="40" reserve-selection :selectable="(row: LarkDocumentRow) => !row.is_exist" />
<el-table-column class-name="expand-name-column" label="全部文档" prop="name">
<template #default="{ row }: { row: LarkDocumentRow }">
<div class="flex-align-center min-w-0 flex-1 gap-2">
<img v-if="row.type === 'folder'" src="@/assets/file-type/file-icon.svg" alt="" class="w-4.5 shrink-0" />
<img
v-else
:src="getFileIconUrl(row.type === 'docx' ? `${row.name}.docx` : row.type === 'sheet' ? `${row.name}.xlsx` : row.name)"
alt=""
class="w-4.5 shrink-0"
/>
<span :title="row.name" class="min-w-0 truncate">{{ row.name }}</span>
</div>
</template>
</el-table-column>
</MkTable>
</section>
<section v-if="strategyMounted" v-show="activeStep === 1">
<h4 class="mb-4 mk-title-decoration">文档处理策略</h4>
Expand All @@ -184,9 +180,9 @@ defineExpose({ open })
</div>
<template #footer>
<!-- 取消导入 -->
<el-button :disabled="loading" @click="visible = false">取消</el-button>
<el-button plain :disabled="loading" @click="visible = false">取消</el-button>
<!-- 返回文档选择 -->
<el-button v-if="activeStep === 1" :disabled="loading" @click="activeStep = 0">上一步</el-button>
<el-button plain v-if="activeStep === 1" :disabled="loading" @click="activeStep = 0">上一步</el-button>
<!-- 进入文档处理策略 -->
<el-button v-if="activeStep === 0" type="primary" :disabled="!folderToken || pendingLoads > 0" @click="handleNext">下一步</el-button>
<!-- 提交飞书文档导入 -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ defineExpose({ open })
</div>
<template #footer>
<!-- 取消导入 -->
<el-button :disabled="loading" @click="visible = false">取消</el-button>
<el-button plain :disabled="loading" @click="visible = false">取消</el-button>
<!-- 返回基本信息 -->
<el-button v-if="activeStep === 1" :disabled="loading" @click="activeStep = 0">上一步</el-button>
<el-button plain v-if="activeStep === 1" :disabled="loading" @click="activeStep = 0">上一步</el-button>
<!-- 进入文档处理策略 -->
<el-button v-if="activeStep === 0" type="primary" @click="handleNext">下一步</el-button>
<!-- 提交 Web 文档导入 -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ watch(
</el-table-column>
<el-table-column label="同步内容" min-width="400" show-overflow-tooltip>
<template #default="{ row }">
<MkTooltip :disabled="!row.message" :content="row.message" placement="top">
<span>{{ getSyncContent(row) }}</span>
</MkTooltip>
<span>{{ getSyncContent(row) }}</span>
</template>
</el-table-column>
<el-table-column label="同步方式" width="140">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ watch(
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" reserve-selection :selectable="() => !disabled" />
<el-table-column class-name="resource-name-column" label="名称" min-width="260" prop="name">
<el-table-column class-name="expand-name-column" label="名称" min-width="260" prop="name">
<template #default="{ row }: { row: ResourcePermissionItem }">
<div class="flex-align-center min-w-0 flex-1 gap-2">
<MkIcon v-if="row.resource_type === 'folder'" name="icon_file-folder_colorful" :size="18" />
Expand Down Expand Up @@ -212,16 +212,3 @@ watch(
<BatchSetPermissionDialog ref="batchPermissionDialogRef" :disabled="disabled" @submit="handleBatchSubmit" />
</div>
</template>

<style scoped lang="scss">
:deep(.resource-name-column .cell) {
align-items: center;
display: flex;

.el-table__expand-icon,
.el-table__indent,
.el-table__placeholder {
flex-shrink: 0;
}
}
</style>
Loading