Skip to content

Commit fa710c3

Browse files
fix: tool switch
1 parent 2060047 commit fa710c3

24 files changed

Lines changed: 819 additions & 227 deletions

‎ui/src/views/VIEW_README.md‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
- `action-dropdown/` 目录目前只优先用于 `application`、`knowledge`、`model`、`tool` 四类特殊
2424
资源,用于维护跨 Workspace、System 资源管理或 System 共享资源复用的卡片 Action。其他业务
2525
暂不主动创建这类目录,操作入口和配套流程优先留在所属页面或组件中;出现明确的跨范围共享需求
26-
后再评估是否采用相同结构。
26+
后再评估是否采用相同结构。按明确需求,文档页使用 `knowledge-detail/document/action-dropdown/`
27+
集中维护行操作、批量操作及其专属弹窗;不因此扩展其他普通页面的目录规则。
2728
- 仅供一个页面使用的代码放在该页面功能目录;同一功能下多个页面复用的代码放在它们最近的
2829
共同功能目录中。
2930
- 不要为了复用一个页面内部实现而提前移动到 `src/components`。
@@ -322,8 +323,9 @@ Workspace 的自定义工具和工作流工具菜单
322323
维护逐方法接口类型;`ApplyStoreToolDialog` 内聚商店应用请求,提交时通过
323324
`isSystemSharedResource()` 选择 System 共享资源或 Workspace 的 Tool API 和 Tool Store API,
324325
覆盖工作流模板及商店工具两个分支;历史内置工具由 ToolStore 提供,不再从数据库加载。
325-
工具启用状态属于固定的卡片内交互,由 `ToolStatusSwitch`
326-
使用卡片传入的 Tool API 更新,并通过 `update` 事件通知页面替换列表数据。工具批量选择状态、
326+
工具启用状态由 Workspace 工具页和 System 共享工具页通过 `ToolCard` 的 `actions` 插槽组合
327+
`ToolStatusSwitch`,页面直接传入对应 Tool API 和操作 loading,并通过 `update` 事件替换列表数据。
328+
`ToolCard` 不再接收 Tool API,仅提供操作插槽;版本更新仍通过卡片的 loading 和 update 通道处理。工具批量选择状态、
327329
批量移动和批量删除流程由 `ToolView` 管理;`MoveToolAction` 复用公共 `MoveToDialog` 完成单个工具移动,
328330
在具体目录中通过 `delete` 通知页面局部移除卡片,在“全部工具”中保留卡片。`ToolCard` 只把选择模式与选中状态传给 `MkSourceCard`。
329331
工具商店列表由 `ToolView` 统一加载并经 `ToolCard` 传给 `ButtonUpdateVersion`,卡片和更新按钮
@@ -677,12 +679,25 @@ Workspace API 内部通过 `getWorkspaceId()` 读取当前路由工作空间。
677679
选中后通过 `footer-batch-actions` 展示批量操作。右侧固定操作列提供启停、向量化、分词索引和更多菜单,
678680
运行或排队中的任务切换为取消入口。Web、飞书提供同步,普通与工作流文档提供原文下载和替换。
679681
单项与批量操作共用页面请求状态,删除和同步先确认,成功后清空选择并刷新,导出和下载保留选择。
680-
文档操作入口与专属弹窗合并放在 `document/components/`:`ButtonDocumentTask` 负责向量化分段范围与生成问题配置,
681-
`ButtonDocumentSetting` 负责召回及来源设置,`ButtonMigrateDocument` 选择目标知识库,
682-
`ButtonDocumentTags` 添加已有标签和移除文档标签关联。组件接收知识库 ID、文档 ID 或文档数组及禁用状态,
682+
文档操作统一放在 `document/action-dropdown/`,通过该目录 `index.ts` 导出,页面显式组合各 Action。
683+
`EmbeddingDocumentAction` 负责向量化分段范围与取消向量化,`GenerateDocumentQuestionsAction` 负责生成问题配置,`SettingDocumentAction` 负责召回及来源设置,
684+
`MigrateDocumentAction` 选择目标知识库,`DocumentTagsAction` 添加已有标签和移除文档标签关联;
685+
这些 Action 继续将操作入口与专属弹窗放在同一组件内。组件接收完整 Document API、知识库 ID、文档 ID 或文档数组及禁用状态,
683686
点击时快照本次操作对象,内部管理弹窗和请求,成功后通过 `refresh` 通知页面刷新,失败保留输入。
684-
任务组件通过 `action` 区分向量化与生成问题,通过 `batch` 切换批量按钮;行内入口使用 `display` 区分菜单与图标按钮。
687+
向量化和生成问题使用独立 Action,通过 `batch` 切换批量按钮;行内入口使用 `display` 区分菜单与图标按钮。
688+
单项向量化传入 `document`,由 `EmbeddingDocumentAction` 内部判断排队或执行状态并切换取消入口,
689+
取消请求通过 `v-model:loading` 共用页面操作状态;页面不再拆分向量化和取消向量化两个入口。
685690
设置组件通过 `batch` 区分单项与批量,标签组件通过 `manageTags` 区分标签管理与批量添加。
691+
`CancelDocumentTaskAction`、`TokenizeDocumentAction`、`SyncDocumentAction`、
692+
`ExportDocumentAction`、`DownloadDocumentAction`、`ReplaceDocumentAction` 和 `DeleteDocumentAction`
693+
分别负责任务取消、分词、同步、导出、下载、替换与删除,单项和批量入口复用对应组件。
694+
直接请求的 Action 通过 `v-model:loading` 共用页面操作状态,各 Action 自行管理请求、
695+
防重复提交、成功提示与失败处理,不额外抽取统一请求封装;需要刷新时发出 `refresh`,导出与下载不发出。
696+
删除后分页总数和页码回退由页面重新查询服务端处理,不在 Action 中维护分页数据。
697+
启停开关直接使用页面内的 `el-switch`,由页面 `handleChangeActive` 管理请求,
698+
接口成功后刷新列表回显,不在请求前切换状态,不单独封装 Action。
699+
页面保留列表、多选、任务状态展示判断与刷新方法,不维护各 Action 的请求流程。
700+
`DocumentStatus`、`DocumentTags` 等纯展示组件继续留在 `document/components/`。
686701
页面不再维护这些弹窗 Ref 和打开方法;承载入口的 `MkTableMoreDropdown` 使用 `persistent`,避免菜单收起时卸载弹窗。
687702
不新增前端权限判断;共享文档不展示选择列及操作入口。
688703
文档分页接口维护在 `workspace/knowledge/document.ts`,页面管理 loading。文件状态表头使用
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<script setup lang="ts">
2+
import { MsgSuccess } from '@/utils/message'
3+
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
4+
import type { DocumentTaskType } from '@/api/types'
5+
6+
defineOptions({ name: 'CancelDocumentTaskAction' })
7+
const props = defineProps<{
8+
api: typeof DocumentApi
9+
knowledgeId: string
10+
documentIds: string[]
11+
taskType: DocumentTaskType
12+
label: string
13+
display?: 'menu' | 'button'
14+
divided?: boolean
15+
}>()
16+
const loading = defineModel<boolean>('loading', { default: false })
17+
const emit = defineEmits<{ refresh: [] }>()
18+
19+
// 取消单个或选中文档的指定任务。
20+
function handleCancelTask() {
21+
const ids = [...props.documentIds]
22+
if (!ids.length || loading.value) return
23+
loading.value = true
24+
return props.api
25+
.putBatchCancelDocumentTask(props.knowledgeId, ids, props.taskType)
26+
.then(() => {
27+
MsgSuccess('操作成功')
28+
emit('refresh')
29+
})
30+
.catch(() => {})
31+
.finally(() => {
32+
loading.value = false
33+
})
34+
}
35+
</script>
36+
37+
<template>
38+
<!-- 取消文档任务 -->
39+
<MkAction
40+
:label="label"
41+
icon="icon_close_outlined"
42+
:display="display"
43+
:divided="divided"
44+
:disabled="loading || !documentIds.length"
45+
@click="handleCancelTask"
46+
/>
47+
</template>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
3+
import { MsgConfirm, MsgSuccess } from '@/utils/message'
4+
5+
defineOptions({ name: 'DeleteDocumentAction' })
6+
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; documentIds: string[]; batch?: boolean }>()
7+
const loading = defineModel<boolean>('loading', { default: false })
8+
const emit = defineEmits<{ refresh: [] }>()
9+
10+
// 确认后删除单个或选中文档;刷新后的分页总数以服务端为准。
11+
function handleDelete() {
12+
if (!props.documentIds.length || loading.value) return
13+
const ids = [...props.documentIds]
14+
const documentId = ids[0]!
15+
return MsgConfirm(`删除 ${ids.length} 个文档`, '删除后无法恢复,是否继续?')
16+
.then(() => {
17+
if (loading.value) return
18+
loading.value = true
19+
const request = props.batch
20+
? props.api.putBatchDeleteDocuments(props.knowledgeId, ids)
21+
: props.api.deleteDocument(props.knowledgeId, documentId)
22+
return request
23+
.then(() => {
24+
MsgSuccess('操作成功')
25+
emit('refresh')
26+
})
27+
.finally(() => {
28+
loading.value = false
29+
})
30+
})
31+
.catch(() => {})
32+
}
33+
</script>
34+
35+
<template>
36+
<!-- 删除单个或选中文档 -->
37+
<MkAction label="删除" icon="icon_delete-trash_outlined" divided :disabled="loading || !documentIds.length" @click="handleDelete" />
38+
</template>

ui/src/views/knowledge-detail/document/components/ButtonDocumentTags.vue renamed to ui/src/views/knowledge-detail/document/action-dropdown/DocumentTagsAction.vue

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<script setup lang="ts">
22
import { computed, ref } from 'vue'
33
import type { KnowledgeTagGroup } from '@/api/types'
4-
import DocumentApi from '@/api/admin/workspace/knowledge/document'
4+
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
55
import KnowledgeApi from '@/api/admin/workspace/knowledge/knowledge'
66
import { MsgSuccess } from '@/utils/message'
77
8-
defineOptions({ name: 'ButtonDocumentTags' })
9-
const props = defineProps<{ knowledgeId: string; documentIds: string[]; manageTags?: boolean; disabled?: boolean }>()
8+
defineOptions({ name: 'DocumentTagsAction' })
9+
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; documentIds: string[]; manageTags?: boolean; disabled?: boolean }>()
1010
const emit = defineEmits<{ refresh: [] }>()
1111
const visible = ref(false)
1212
const loading = ref(false)
@@ -30,7 +30,7 @@ function handleOpenDialog() {
3030
optionLoading.value = true
3131
Promise.all([
3232
KnowledgeApi.getKnowledgeTags(props.knowledgeId),
33-
manageTags && ids[0] ? DocumentApi.getDocumentTags(props.knowledgeId, ids[0]) : Promise.resolve([]),
33+
manageTags && ids[0] ? props.api.getDocumentTags(props.knowledgeId, ids[0]) : Promise.resolve([]),
3434
])
3535
.then(([groups, currentTags]) => {
3636
tagGroups.value = groups
@@ -47,13 +47,14 @@ function handleOpenDialog() {
4747
function handleAddTags() {
4848
if (loading.value || !selectedTagIds.value.length) return
4949
loading.value = true
50-
return DocumentApi.postAddDocumentTags(props.knowledgeId, targetDocumentIds.value, selectedTagIds.value)
50+
return props.api
51+
.postAddDocumentTags(props.knowledgeId, targetDocumentIds.value, selectedTagIds.value)
5152
.then(() => {
5253
MsgSuccess('添加成功')
5354
emit('refresh')
5455
selectedTagIds.value = []
5556
if (manage.value && targetDocumentIds.value[0]) {
56-
return DocumentApi.getDocumentTags(props.knowledgeId, targetDocumentIds.value[0]).then((groups) => {
57+
return props.api.getDocumentTags(props.knowledgeId, targetDocumentIds.value[0]).then((groups) => {
5758
documentTagGroups.value = groups
5859
})
5960
}
@@ -71,7 +72,8 @@ function handleDeleteTag(tagId: string) {
7172
const documentId = targetDocumentIds.value[0]
7273
if (loading.value || !documentId) return
7374
loading.value = true
74-
return DocumentApi.putDeleteDocumentTags(props.knowledgeId, documentId, [tagId])
75+
return props.api
76+
.putDeleteDocumentTags(props.knowledgeId, documentId, [tagId])
7577
.then(() => {
7678
MsgSuccess('移除成功')
7779
documentTagGroups.value = documentTagGroups.value.map((group) => ({ ...group, values: group.values.filter(({ id }) => id !== tagId) }))
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script setup lang="ts">
2+
import { MsgSuccess } from '@/utils/message'
3+
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
4+
import type { DocumentItem } from '@/api/types'
5+
6+
defineOptions({ name: 'DownloadDocumentAction' })
7+
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; document: DocumentItem }>()
8+
const loading = defineModel<boolean>('loading', { default: false })
9+
10+
// 下载原始文件,不清空勾选或刷新列表。
11+
function handleDownload() {
12+
if (loading.value) return
13+
loading.value = true
14+
return props.api
15+
.downloadDocumentSource(props.knowledgeId, props.document)
16+
.then(() => {
17+
MsgSuccess('操作成功')
18+
})
19+
.catch(() => {})
20+
.finally(() => {
21+
loading.value = false
22+
})
23+
}
24+
</script>
25+
26+
<template>
27+
<!-- 下载原文档 -->
28+
<MkAction label="下载原文档" icon="icon_download_outlined" :disabled="loading" @click="handleDownload" />
29+
</template>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<script setup lang="ts">
2+
import { computed, ref } from 'vue'
3+
import { DOCUMENT_TASK_STATE, DOCUMENT_TASK_TYPE } from '@/api/enums'
4+
import type { DocumentItem } from '@/api/types'
5+
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
6+
import { MsgSuccess } from '@/utils/message'
7+
8+
defineOptions({ name: 'EmbeddingDocumentAction' })
9+
const props = defineProps<{
10+
api: typeof DocumentApi
11+
knowledgeId: string
12+
documentIds: string[]
13+
document?: DocumentItem
14+
batch?: boolean
15+
display?: 'menu' | 'button'
16+
}>()
17+
const operationLoading = defineModel<boolean>('loading', { default: false })
18+
const emit = defineEmits<{ refresh: [] }>()
19+
const visible = ref(false)
20+
const loading = ref(false)
21+
const targetDocumentIds = ref<string[]>([])
22+
const scope = ref<'error' | 'all'>('error')
23+
24+
// 单项向量化根据任务状态切换为取消入口,批量入口始终打开向量化配置。
25+
const running = computed(() => {
26+
if (props.batch) return false
27+
const state = props.document?.status?.at(-DOCUMENT_TASK_TYPE.EMBEDDING)
28+
return state === DOCUMENT_TASK_STATE.PENDING || state === DOCUMENT_TASK_STATE.STARTED
29+
})
30+
31+
function handleEmbedding() {
32+
if (!running.value) {
33+
targetDocumentIds.value = [...props.documentIds]
34+
scope.value = 'error'
35+
visible.value = true
36+
return
37+
}
38+
// 正在排队或执行时,直接提交取消向量化请求。
39+
operationLoading.value = true
40+
return props.api
41+
.putBatchCancelDocumentTask(props.knowledgeId, [...props.documentIds], DOCUMENT_TASK_TYPE.EMBEDDING)
42+
.then(() => {
43+
MsgSuccess('操作成功')
44+
emit('refresh')
45+
})
46+
.catch(() => {})
47+
.finally(() => {
48+
operationLoading.value = false
49+
})
50+
}
51+
52+
// 根据选定的分段范围提交向量化,失败时保留配置。
53+
function handleSubmit() {
54+
if (loading.value) return
55+
loading.value = true
56+
const stateList = Object.values(DOCUMENT_TASK_STATE).filter((state) => scope.value === 'all' || state !== DOCUMENT_TASK_STATE.SUCCESS)
57+
return props.api
58+
.putBatchRefreshDocuments(props.knowledgeId, targetDocumentIds.value, stateList)
59+
.then(() => {
60+
MsgSuccess('任务已提交')
61+
visible.value = false
62+
emit('refresh')
63+
})
64+
.catch(() => {})
65+
.finally(() => {
66+
loading.value = false
67+
})
68+
}
69+
70+
function handleClosed() {
71+
targetDocumentIds.value = []
72+
scope.value = 'error'
73+
}
74+
</script>
75+
76+
<template>
77+
<!-- 批量向量化 -->
78+
<el-button v-if="batch" :disabled="operationLoading || loading || !documentIds.length" @click="handleEmbedding">向量化</el-button>
79+
<!-- 向量化或取消向量化 -->
80+
<MkAction
81+
v-else
82+
:display="display ?? 'menu'"
83+
:label="running ? '取消向量化' : '向量化'"
84+
:icon="running ? 'icon_close_outlined' : 'icon_refresh_outlined'"
85+
:disabled="operationLoading || loading || !documentIds.length"
86+
@click="handleEmbedding"
87+
/>
88+
<MkDialog v-model="visible" title="向量化" @closed="handleClosed">
89+
<el-form label-position="top" @submit.prevent>
90+
<el-form-item label="选择分段">
91+
<el-radio-group v-model="scope">
92+
<el-radio value="error">未成功的分段</el-radio>
93+
<el-radio value="all">全部分段</el-radio>
94+
</el-radio-group>
95+
</el-form-item>
96+
</el-form>
97+
<template #footer>
98+
<!-- 取消向量化配置 -->
99+
<el-button :disabled="loading" @click="visible = false">取消</el-button>
100+
<!-- 提交向量化 -->
101+
<el-button type="primary" :loading="loading" @click="handleSubmit">确认</el-button>
102+
</template>
103+
</MkDialog>
104+
</template>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script setup lang="ts">
2+
import { MsgSuccess } from '@/utils/message'
3+
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
4+
5+
defineOptions({ name: 'ExportDocumentAction' })
6+
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; documentIds: string[]; format: 'excel' | 'zip' }>()
7+
const loading = defineModel<boolean>('loading', { default: false })
8+
9+
// 导出单个或选中文档,保留列表和勾选状态。
10+
function handleExport() {
11+
const ids = [...props.documentIds]
12+
if (!ids.length || loading.value) return
13+
loading.value = true
14+
return props.api
15+
.exportDocuments(props.knowledgeId, ids, props.format)
16+
.then(() => {
17+
MsgSuccess('操作成功')
18+
})
19+
.catch(() => {})
20+
.finally(() => {
21+
loading.value = false
22+
})
23+
}
24+
</script>
25+
26+
<template>
27+
<!-- 导出文档 -->
28+
<MkAction
29+
:label="format === 'excel' ? '导出 Excel' : '导出 Zip'"
30+
icon="icon_export_outlined"
31+
:disabled="loading || !documentIds.length"
32+
@click="handleExport"
33+
/>
34+
</template>

0 commit comments

Comments
 (0)