Skip to content
Closed
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
25 changes: 23 additions & 2 deletions ui/src/styles/element-plus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -344,15 +344,21 @@
--el-checkbox-disabled-checked-input-fill: var(--mk-N400);
--el-checkbox-disabled-checked-input-border-color: var(--mk-N400);
--el-checkbox-disabled-checked-icon-color: white;
--el-checkbox-disabled-border-color: var(--mk-N400);
--el-checkbox-disabled-input-fill: var(--mk-N200);
--el-checkbox-checked-text-color: var(--el-text-color-primary);
margin: 0;
height: unset;
&__label {
line-height: 22px;
font-weight: 400;
}
&__inner:before {
top: 6px !important;
&__inner {
&.is-disabled {
}
&:before {
top: 6px !important;
}
}
}

Expand Down Expand Up @@ -785,6 +791,21 @@
&.is-checked + .el-radio__label {
color: var(--el-text-color-primary) !important;
}
&.is-disabled {
.el-radio__inner {
background-color: var(--mk-N200);
border-color: var(--mk-N400);
}
&.is-checked {
.el-radio__inner {
background-color: var(--mk-N400);
border-color: var(--mk-N400);
&::after {
background-color: var(--mk-N200);
}
}
}
}
}
&__inner {
&::after {
Expand Down
8 changes: 6 additions & 2 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1061,11 +1061,15 @@ Layout 保留标题栏固定位置和外层间距,页面负责内部布局。
不新增权限判断或修改共享文档的查询边界。

Web、飞书和工作流知识库的非共享文档页在标题栏组合
`document/import-document/ButtonImportDocument.vue`,复用详情上下文,不重复查询知识库。
`document/import-document/ButtonImportDocument.vue`,页面传入已有路由变量 `knowledgeId`、
`knowledgeType`(`KNOWLEDGE_TYPE_KEY` 标识),从详情上下文读取飞书 `folderToken` 和工作流图 `workflow`;
不传完整 `knowledge`,不重复查询知识库。
入口按类型挂载独立的 `WebImportDocumentDrawer.vue`、`LarkImportDocumentDrawer.vue`、
`WorkflowImportDocumentDrawer.vue`,关闭动画结束后卸载。
Web 和飞书使用全屏底部 Drawer,分别通过地址表单和飞书懒加载文件树进入文档处理策略;
策略复用 `DocumentStrategyForm`,首次挂载读取知识库策略,上一步保留草稿。
策略复用 `DocumentStrategyForm`,使用组件默认策略,不回显知识库策略,上一步保留草稿。
Web 抽屉只接收完整 Document API 与知识库 ID,地址和选择器初始为空;
飞书抽屉额外接收文件夹 Token,工作流抽屉只接收知识库 ID 和图数据;所有导入组件均不接收 `KnowledgeDetail`。
Web 地址按行去空白、去重并校验 HTTP/HTTPS;空选择器提交 `body`。
飞书按 `meta.folder_token` 查询目录并读取全部分页,已导入文档勾选且不可重复导入;
全选覆盖当前已加载节点,选择文件夹时由树懒加载子节点,加载期间禁止进入下一步。
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
<script setup lang="ts">
import { computed, nextTick, ref, useTemplateRef } from 'vue'
import { computed, nextTick, ref, useTemplateRef, type Component } from 'vue'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import type { KnowledgeDetail } from '@/api/types'
import { KNOWLEDGE_TYPE } from '@/api/enums'
import type LogicFlow from '@logicflow/core'
import { KNOWLEDGE_TYPE_KEY } from '@/constants/knowledge'
import WebImportDocumentDrawer from './WebImportDocumentDrawer.vue'
import LarkImportDocumentDrawer from './LarkImportDocumentDrawer.vue'
import WorkflowImportDocumentDrawer from './WorkflowImportDocumentDrawer.vue'

defineOptions({ name: 'ButtonImportDocument' })
const props = defineProps<{ api: typeof DocumentApi; knowledge: KnowledgeDetail }>()
const props = defineProps<{
api: typeof DocumentApi
knowledgeId: string
knowledgeType: string
folderToken?: string
workflow?: LogicFlow.GraphConfigData
}>()
const emit = defineEmits<{ refresh: [] }>()

/* 导入入口:按知识库类型挂载独立抽屉,关闭动画后卸载。 */
const drawerMounted = ref(false)
const drawerComponent = computed(() => {
switch (props.knowledge.type) {
case KNOWLEDGE_TYPE.WEB:
const drawerComponent = computed<Component | undefined>(() => {
switch (props.knowledgeType) {
case KNOWLEDGE_TYPE_KEY.WEB:
return WebImportDocumentDrawer
case KNOWLEDGE_TYPE.LARK:
case KNOWLEDGE_TYPE_KEY.LARK:
return LarkImportDocumentDrawer
case KNOWLEDGE_TYPE.WORKFLOW:
case KNOWLEDGE_TYPE_KEY.WORKFLOW:
return WorkflowImportDocumentDrawer
default:
return undefined
}
})
const drawerProps = computed(() => {
if (props.knowledgeType === KNOWLEDGE_TYPE_KEY.WORKFLOW) return { knowledgeId: props.knowledgeId, workflow: props.workflow }
if (props.knowledgeType === KNOWLEDGE_TYPE_KEY.LARK) {
return { api: props.api, knowledgeId: props.knowledgeId, folderToken: props.folderToken ?? '' }
}
return { api: props.api, knowledgeId: props.knowledgeId }
})
const drawerRef = useTemplateRef<{ open: () => void }>('drawerRef')
function handleOpenImport() {
if (!drawerComponent.value || drawerMounted.value) return
Expand All @@ -35,16 +48,15 @@ function handleOpenImport() {

<template>
<!-- 导入文档 -->
<el-button v-if="drawerComponent" type="primary" @click="handleOpenImport">
<MkIcon name="icon_import_outlined" class="mr-1" />
导入文档
<el-button type="primary" @click="handleOpenImport">
<MkIcon name="icon_import_outlined" />
<span>导入文档</span>
</el-button>
<component
:is="drawerComponent"
v-if="drawerMounted"
ref="drawerRef"
v-bind="knowledge.type === KNOWLEDGE_TYPE.WORKFLOW ? {} : { api }"
:knowledge="knowledge"
v-bind="drawerProps"
@refresh="emit('refresh')"
@closed="drawerMounted = false"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import { computed, nextTick, ref, useTemplateRef } from 'vue'
import type { ElTree, LoadFunction } from 'element-plus'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import type { KnowledgeDetail, LarkDocumentNode } from '@/api/types'
import type { LarkDocumentNode } from '@/api/types'
import DocumentStrategyForm from '@/views/knowledge/create-knowledge/components/DocumentStrategyForm.vue'
import { getFileIconUrl } from '@/utils/icon'
import { MsgSuccess, MsgWarning } from '@/utils/message'

defineOptions({ name: 'LarkImportDocumentDrawer' })
const props = defineProps<{ api: typeof DocumentApi; knowledge: KnowledgeDetail }>()
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; folderToken: string }>()
const emit = defineEmits<{ refresh: []; closed: [] }>()

/* 飞书文件树与选择状态:只导入未导入的文件,文件夹用于导航。 */
Expand All @@ -22,7 +22,6 @@ const checkedTokens = ref<string[]>([])
const selectedDocuments = ref<LarkDocumentNode[]>([])
const treeRef = useTemplateRef<InstanceType<typeof ElTree>>('treeRef')
const strategyRef = useTemplateRef<InstanceType<typeof DocumentStrategyForm>>('strategyRef')
const folderToken = computed(() => String(props.knowledge.meta?.folder_token ?? ''))
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)))
Expand All @@ -45,15 +44,15 @@ function handleAllCheck(checked: unknown) {

/* 懒加载目录:一次读完该目录分页,失败恢复可重试状态。 */
const loadNode: LoadFunction = (node, resolve, reject) => {
const token = node.level === 0 ? folderToken.value : String(node.data.token)
const token = node.level === 0 ? props.folderToken : String(node.data.token)
if (!token) {
resolve([])
return
}
pendingLoads.value += 1
const folderDocuments: LarkDocumentNode[] = []
function loadPage(pageToken?: string): Promise<void> {
return props.api.getLarkDocumentList(props.knowledge.id, token, pageToken ? { page_token: pageToken } : {}).then((page) => {
return props.api.getLarkDocumentList(props.knowledgeId, token, pageToken ? { page_token: pageToken } : {}).then((page) => {
folderDocuments.push(...page.files)
if (page.has_more && page.next_page_token) return loadPage(page.next_page_token)
})
Expand Down Expand Up @@ -91,9 +90,6 @@ function handleNext() {
strategyMounted.value = true
activeStep.value = 1
}
function handleStrategyMounted() {
strategyRef.value?.setStrategy(props.knowledge.doc_strategy)
}
function handleSubmit() {
if (loading.value || !strategyRef.value) return
loading.value = true
Expand All @@ -104,7 +100,7 @@ function handleSubmit() {
const strategy = strategyRef.value.getStrategy()
return props.api
.postImportLarkDocuments(
props.knowledge.id,
props.knowledgeId,
selectedDocuments.value.map(({ name, token, type }) => ({
name,
token,
Expand All @@ -129,7 +125,7 @@ defineExpose({ open })
</script>

<template>
<MkDrawer v-model="visible" direction="btt" size="100%" :show-close="!loading" @closed="emit('closed')">
<MkDrawer v-model="visible" direction="btt" @closed="emit('closed')">
<template #header>
<div class="flex w-full">
<h4>导入文档</h4>
Expand All @@ -149,42 +145,41 @@ defineExpose({ open })
<li>导入文档前,建议规范文档的分段标识。</li>
</ol>
</el-alert>
<el-alert v-if="!folderToken" type="warning" title="请先在知识库设置中配置飞书文件夹 Token" :closable="false" />
<template v-else>
<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"
:load="loadNode"
node-key="token"
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>
</template>

<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"
:load="loadNode"
node-key="token"
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>
</section>
<section v-if="strategyMounted" v-show="activeStep === 1">
<h4 class="mb-4 mk-title-decoration">文档处理策略</h4>
<DocumentStrategyForm ref="strategyRef" @vue:mounted="handleStrategyMounted" />
<DocumentStrategyForm ref="strategyRef" />
</section>
</div>
<template #footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@
import { computed, ref, useTemplateRef } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import type { KnowledgeDetail } from '@/api/types'
import DocumentStrategyForm from '@/views/knowledge/create-knowledge/components/DocumentStrategyForm.vue'
import { MsgSuccess } from '@/utils/message'

defineOptions({ name: 'WebImportDocumentDrawer' })
const props = defineProps<{ api: typeof DocumentApi; knowledge: KnowledgeDetail }>()
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string }>()
const emit = defineEmits<{ refresh: []; closed: [] }>()

/* Web 地址与处理策略 */
const visible = ref(false)
const loading = ref(false)
const activeStep = ref(0)
const strategyMounted = ref(false)
const form = ref({ addresses: '', selector: String(props.knowledge.meta?.selector ?? '') })
const form = ref({ source_url: '', selector: '' })
const sourceUrls = computed(() => [
...new Set(
form.value.addresses
form.value.source_url
.split(/\r?\n/)
.map((url) => url.trim())
.filter(Boolean),
Expand All @@ -27,23 +26,7 @@ const sourceUrls = computed(() => [
const formRef = ref<FormInstance>()
const strategyRef = useTemplateRef<InstanceType<typeof DocumentStrategyForm>>('strategyRef')
const rules: FormRules = {
addresses: [
{ required: true, whitespace: true, message: '请输入文档地址', trigger: 'blur' },
{
validator: (_rule, _value, callback) => {
const valid = sourceUrls.value.every((address) => {
try {
const url = new URL(address)
return ['http:', 'https:'].includes(url.protocol) && Boolean(url.hostname)
} catch {
return false
}
})
callback(valid ? undefined : new Error('请输入正确的文档地址,一行一个,仅支持 HTTP 或 HTTPS'))
},
trigger: 'blur',
},
],
source_url: [{ required: true, whitespace: true, message: '请输入文档地址', trigger: 'blur' }],
}
function open() {
visible.value = true
Expand All @@ -60,9 +43,6 @@ function handleNext() {
/* 表单展示校验错误。 */
})
}
function handleStrategyMounted() {
strategyRef.value?.setStrategy(props.knowledge.doc_strategy)
}
function handleSubmit() {
if (loading.value || !strategyRef.value) return
loading.value = true
Expand All @@ -71,13 +51,13 @@ function handleSubmit() {
.then((valid) => {
if (!valid || !strategyRef.value) return
return props.api
.postWebDocument(props.knowledge.id, {
.postWebDocument(props.knowledgeId, {
source_url_list: sourceUrls.value,
selector: form.value.selector.trim() || 'body',
doc_strategy: strategyRef.value.getStrategy(),
})
.then(() => {
MsgSuccess('导入任务提交成功')
MsgSuccess('导入成功')
visible.value = false
emit('refresh')
})
Expand All @@ -93,7 +73,7 @@ defineExpose({ open })
</script>

<template>
<MkDrawer v-model="visible" direction="btt" size="100%" :show-close="!loading" @closed="emit('closed')">
<MkDrawer v-model="visible" direction="btt" @closed="emit('closed')">
<template #header>
<div class="flex w-full">
<h4>导入文档</h4>
Expand All @@ -107,8 +87,8 @@ defineExpose({ open })
<section v-show="activeStep === 0">
<h4 class="mb-4 mk-title-decoration">基本信息</h4>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" require-asterisk-position="right" @submit.prevent>
<el-form-item label="文档地址" prop="addresses">
<el-input v-model="form.addresses" type="textarea" :rows="5" placeholder="请输入文档地址,一行一个,地址不正确会导入失败" />
<el-form-item label="文档地址" prop="source_url">
<el-input v-model="form.source_url" type="textarea" :rows="5" placeholder="请输入文档地址,一行一个,地址不正确文档会导入失败。" />
</el-form-item>
<el-form-item label="选择器">
<el-input v-model="form.selector" placeholder="默认为 body,可输入 .classname/#idname/tagname" />
Expand All @@ -117,7 +97,7 @@ defineExpose({ open })
</section>
<section v-if="strategyMounted" v-show="activeStep === 1">
<h4 class="mb-4 mk-title-decoration">文档处理策略</h4>
<DocumentStrategyForm ref="strategyRef" @vue:mounted="handleStrategyMounted" />
<DocumentStrategyForm ref="strategyRef" />
</section>
</div>
<template #footer>
Expand Down
Loading
Loading