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
29 changes: 22 additions & 7 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
- `action-dropdown/` 目录目前只优先用于 `application`、`knowledge`、`model`、`tool` 四类特殊
资源,用于维护跨 Workspace、System 资源管理或 System 共享资源复用的卡片 Action。其他业务
暂不主动创建这类目录,操作入口和配套流程优先留在所属页面或组件中;出现明确的跨范围共享需求
后再评估是否采用相同结构。
后再评估是否采用相同结构。按明确需求,文档页使用 `knowledge-detail/document/action-dropdown/`
集中维护行操作、批量操作及其专属弹窗;不因此扩展其他普通页面的目录规则。
- 仅供一个页面使用的代码放在该页面功能目录;同一功能下多个页面复用的代码放在它们最近的
共同功能目录中。
- 不要为了复用一个页面内部实现而提前移动到 `src/components`。
Expand Down Expand Up @@ -322,8 +323,9 @@ Workspace 的自定义工具和工作流工具菜单
维护逐方法接口类型;`ApplyStoreToolDialog` 内聚商店应用请求,提交时通过
`isSystemSharedResource()` 选择 System 共享资源或 Workspace 的 Tool API 和 Tool Store API,
覆盖工作流模板及商店工具两个分支;历史内置工具由 ToolStore 提供,不再从数据库加载。
工具启用状态属于固定的卡片内交互,由 `ToolStatusSwitch`
使用卡片传入的 Tool API 更新,并通过 `update` 事件通知页面替换列表数据。工具批量选择状态、
工具启用状态由 Workspace 工具页和 System 共享工具页通过 `ToolCard` 的 `actions` 插槽组合
`ToolStatusSwitch`,页面直接传入对应 Tool API 和操作 loading,并通过 `update` 事件替换列表数据。
`ToolCard` 不再接收 Tool API,仅提供操作插槽;版本更新仍通过卡片的 loading 和 update 通道处理。工具批量选择状态、
批量移动和批量删除流程由 `ToolView` 管理;`MoveToolAction` 复用公共 `MoveToDialog` 完成单个工具移动,
在具体目录中通过 `delete` 通知页面局部移除卡片,在“全部工具”中保留卡片。`ToolCard` 只把选择模式与选中状态传给 `MkSourceCard`。
工具商店列表由 `ToolView` 统一加载并经 `ToolCard` 传给 `ButtonUpdateVersion`,卡片和更新按钮
Expand Down Expand Up @@ -677,12 +679,25 @@ Workspace API 内部通过 `getWorkspaceId()` 读取当前路由工作空间。
选中后通过 `footer-batch-actions` 展示批量操作。右侧固定操作列提供启停、向量化、分词索引和更多菜单,
运行或排队中的任务切换为取消入口。Web、飞书提供同步,普通与工作流文档提供原文下载和替换。
单项与批量操作共用页面请求状态,删除和同步先确认,成功后清空选择并刷新,导出和下载保留选择。
文档操作入口与专属弹窗合并放在 `document/components/`:`ButtonDocumentTask` 负责向量化分段范围与生成问题配置,
`ButtonDocumentSetting` 负责召回及来源设置,`ButtonMigrateDocument` 选择目标知识库,
`ButtonDocumentTags` 添加已有标签和移除文档标签关联。组件接收知识库 ID、文档 ID 或文档数组及禁用状态,
文档操作统一放在 `document/action-dropdown/`,通过该目录 `index.ts` 导出,页面显式组合各 Action。
`EmbeddingDocumentAction` 负责向量化分段范围与取消向量化,`GenerateDocumentQuestionsAction` 负责生成问题配置,`SettingDocumentAction` 负责召回及来源设置,
`MigrateDocumentAction` 选择目标知识库,`DocumentTagsAction` 添加已有标签和移除文档标签关联;
这些 Action 继续将操作入口与专属弹窗放在同一组件内。组件接收完整 Document API、知识库 ID、文档 ID 或文档数组及禁用状态,
点击时快照本次操作对象,内部管理弹窗和请求,成功后通过 `refresh` 通知页面刷新,失败保留输入。
任务组件通过 `action` 区分向量化与生成问题,通过 `batch` 切换批量按钮;行内入口使用 `display` 区分菜单与图标按钮。
向量化和生成问题使用独立 Action,通过 `batch` 切换批量按钮;行内入口使用 `display` 区分菜单与图标按钮。
单项向量化传入 `document`,由 `EmbeddingDocumentAction` 内部判断排队或执行状态并切换取消入口,
取消请求通过 `v-model:loading` 共用页面操作状态;页面不再拆分向量化和取消向量化两个入口。
设置组件通过 `batch` 区分单项与批量,标签组件通过 `manageTags` 区分标签管理与批量添加。
`CancelDocumentTaskAction`、`TokenizeDocumentAction`、`SyncDocumentAction`、
`ExportDocumentAction`、`DownloadDocumentAction`、`ReplaceDocumentAction` 和 `DeleteDocumentAction`
分别负责任务取消、分词、同步、导出、下载、替换与删除,单项和批量入口复用对应组件。
直接请求的 Action 通过 `v-model:loading` 共用页面操作状态,各 Action 自行管理请求、
防重复提交、成功提示与失败处理,不额外抽取统一请求封装;需要刷新时发出 `refresh`,导出与下载不发出。
删除后分页总数和页码回退由页面重新查询服务端处理,不在 Action 中维护分页数据。
启停开关直接使用页面内的 `el-switch`,由页面 `handleChangeActive` 管理请求,
接口成功后刷新列表回显,不在请求前切换状态,不单独封装 Action。
页面保留列表、多选、任务状态展示判断与刷新方法,不维护各 Action 的请求流程。
`DocumentStatus`、`DocumentTags` 等纯展示组件继续留在 `document/components/`。
页面不再维护这些弹窗 Ref 和打开方法;承载入口的 `MkTableMoreDropdown` 使用 `persistent`,避免菜单收起时卸载弹窗。
不新增前端权限判断;共享文档不展示选择列及操作入口。
文档分页接口维护在 `workspace/knowledge/document.ts`,页面管理 loading。文件状态表头使用
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup lang="ts">
import { MsgSuccess } from '@/utils/message'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import type { DocumentTaskType } from '@/api/types'

defineOptions({ name: 'CancelDocumentTaskAction' })
const props = defineProps<{
api: typeof DocumentApi
knowledgeId: string
documentIds: string[]
taskType: DocumentTaskType
label: string
display?: 'menu' | 'button'
divided?: boolean
}>()
const loading = defineModel<boolean>('loading', { default: false })
const emit = defineEmits<{ refresh: [] }>()

// 取消单个或选中文档的指定任务。
function handleCancelTask() {
const ids = [...props.documentIds]
if (!ids.length || loading.value) return
loading.value = true
return props.api
.putBatchCancelDocumentTask(props.knowledgeId, ids, props.taskType)
.then(() => {
MsgSuccess('操作成功')
emit('refresh')
})
.catch(() => {})
.finally(() => {
loading.value = false
})
}
</script>

<template>
<!-- 取消文档任务 -->
<MkAction
:label="label"
icon="icon_close_outlined"
:display="display"
:divided="divided"
:disabled="loading || !documentIds.length"
@click="handleCancelTask"
/>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import { MsgConfirm, MsgSuccess } from '@/utils/message'

defineOptions({ name: 'DeleteDocumentAction' })
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; documentIds: string[]; batch?: boolean }>()
const loading = defineModel<boolean>('loading', { default: false })
const emit = defineEmits<{ refresh: [] }>()

// 确认后删除单个或选中文档;刷新后的分页总数以服务端为准。
function handleDelete() {
if (!props.documentIds.length || loading.value) return
const ids = [...props.documentIds]
const documentId = ids[0]!
return MsgConfirm(`删除 ${ids.length} 个文档`, '删除后无法恢复,是否继续?')
.then(() => {
if (loading.value) return
loading.value = true
const request = props.batch
? props.api.putBatchDeleteDocuments(props.knowledgeId, ids)
: props.api.deleteDocument(props.knowledgeId, documentId)
return request
.then(() => {
MsgSuccess('操作成功')
emit('refresh')
})
.finally(() => {
loading.value = false
})
})
.catch(() => {})
}
</script>

<template>
<!-- 删除单个或选中文档 -->
<MkAction label="删除" icon="icon_delete-trash_outlined" divided :disabled="loading || !documentIds.length" @click="handleDelete" />
</template>
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { KnowledgeTagGroup } from '@/api/types'
import DocumentApi from '@/api/admin/workspace/knowledge/document'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import KnowledgeApi from '@/api/admin/workspace/knowledge/knowledge'
import { MsgSuccess } from '@/utils/message'

defineOptions({ name: 'ButtonDocumentTags' })
const props = defineProps<{ knowledgeId: string; documentIds: string[]; manageTags?: boolean; disabled?: boolean }>()
defineOptions({ name: 'DocumentTagsAction' })
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; documentIds: string[]; manageTags?: boolean; disabled?: boolean }>()
const emit = defineEmits<{ refresh: [] }>()
const visible = ref(false)
const loading = ref(false)
Expand All @@ -30,7 +30,7 @@ function handleOpenDialog() {
optionLoading.value = true
Promise.all([
KnowledgeApi.getKnowledgeTags(props.knowledgeId),
manageTags && ids[0] ? DocumentApi.getDocumentTags(props.knowledgeId, ids[0]) : Promise.resolve([]),
manageTags && ids[0] ? props.api.getDocumentTags(props.knowledgeId, ids[0]) : Promise.resolve([]),
])
.then(([groups, currentTags]) => {
tagGroups.value = groups
Expand All @@ -47,13 +47,14 @@ function handleOpenDialog() {
function handleAddTags() {
if (loading.value || !selectedTagIds.value.length) return
loading.value = true
return DocumentApi.postAddDocumentTags(props.knowledgeId, targetDocumentIds.value, selectedTagIds.value)
return props.api
.postAddDocumentTags(props.knowledgeId, targetDocumentIds.value, selectedTagIds.value)
.then(() => {
MsgSuccess('添加成功')
emit('refresh')
selectedTagIds.value = []
if (manage.value && targetDocumentIds.value[0]) {
return DocumentApi.getDocumentTags(props.knowledgeId, targetDocumentIds.value[0]).then((groups) => {
return props.api.getDocumentTags(props.knowledgeId, targetDocumentIds.value[0]).then((groups) => {
documentTagGroups.value = groups
})
}
Expand All @@ -71,7 +72,8 @@ function handleDeleteTag(tagId: string) {
const documentId = targetDocumentIds.value[0]
if (loading.value || !documentId) return
loading.value = true
return DocumentApi.putDeleteDocumentTags(props.knowledgeId, documentId, [tagId])
return props.api
.putDeleteDocumentTags(props.knowledgeId, documentId, [tagId])
.then(() => {
MsgSuccess('移除成功')
documentTagGroups.value = documentTagGroups.value.map((group) => ({ ...group, values: group.values.filter(({ id }) => id !== tagId) }))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { MsgSuccess } from '@/utils/message'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import type { DocumentItem } from '@/api/types'

defineOptions({ name: 'DownloadDocumentAction' })
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; document: DocumentItem }>()
const loading = defineModel<boolean>('loading', { default: false })

// 下载原始文件,不清空勾选或刷新列表。
function handleDownload() {
if (loading.value) return
loading.value = true
return props.api
.downloadDocumentSource(props.knowledgeId, props.document)
.then(() => {
MsgSuccess('操作成功')
})
.catch(() => {})
.finally(() => {
loading.value = false
})
}
</script>

<template>
<!-- 下载原文档 -->
<MkAction label="下载原文档" icon="icon_download_outlined" :disabled="loading" @click="handleDownload" />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { DOCUMENT_TASK_STATE, DOCUMENT_TASK_TYPE } from '@/api/enums'
import type { DocumentItem } from '@/api/types'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'
import { MsgSuccess } from '@/utils/message'

defineOptions({ name: 'EmbeddingDocumentAction' })
const props = defineProps<{
api: typeof DocumentApi
knowledgeId: string
documentIds: string[]
document?: DocumentItem
batch?: boolean
display?: 'menu' | 'button'
}>()
const operationLoading = defineModel<boolean>('loading', { default: false })
const emit = defineEmits<{ refresh: [] }>()
const visible = ref(false)
const loading = ref(false)
const targetDocumentIds = ref<string[]>([])
const scope = ref<'error' | 'all'>('error')

// 单项向量化根据任务状态切换为取消入口,批量入口始终打开向量化配置。
const running = computed(() => {
if (props.batch) return false
const state = props.document?.status?.at(-DOCUMENT_TASK_TYPE.EMBEDDING)
return state === DOCUMENT_TASK_STATE.PENDING || state === DOCUMENT_TASK_STATE.STARTED
})

function handleEmbedding() {
if (!running.value) {
targetDocumentIds.value = [...props.documentIds]
scope.value = 'error'
visible.value = true
return
}
// 正在排队或执行时,直接提交取消向量化请求。
operationLoading.value = true
return props.api
.putBatchCancelDocumentTask(props.knowledgeId, [...props.documentIds], DOCUMENT_TASK_TYPE.EMBEDDING)
.then(() => {
MsgSuccess('操作成功')
emit('refresh')
})
.catch(() => {})
.finally(() => {
operationLoading.value = false
})
}

// 根据选定的分段范围提交向量化,失败时保留配置。
function handleSubmit() {
if (loading.value) return
loading.value = true
const stateList = Object.values(DOCUMENT_TASK_STATE).filter((state) => scope.value === 'all' || state !== DOCUMENT_TASK_STATE.SUCCESS)
return props.api
.putBatchRefreshDocuments(props.knowledgeId, targetDocumentIds.value, stateList)
.then(() => {
MsgSuccess('任务已提交')
visible.value = false
emit('refresh')
})
.catch(() => {})
.finally(() => {
loading.value = false
})
}

function handleClosed() {
targetDocumentIds.value = []
scope.value = 'error'
}
</script>

<template>
<!-- 批量向量化 -->
<el-button v-if="batch" :disabled="operationLoading || loading || !documentIds.length" @click="handleEmbedding">向量化</el-button>
<!-- 向量化或取消向量化 -->
<MkAction
v-else
:display="display ?? 'menu'"
:label="running ? '取消向量化' : '向量化'"
:icon="running ? 'icon_close_outlined' : 'icon_refresh_outlined'"
:disabled="operationLoading || loading || !documentIds.length"
@click="handleEmbedding"
/>
<MkDialog v-model="visible" title="向量化" @closed="handleClosed">
<el-form label-position="top" @submit.prevent>
<el-form-item label="选择分段">
<el-radio-group v-model="scope">
<el-radio value="error">未成功的分段</el-radio>
<el-radio value="all">全部分段</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<!-- 取消向量化配置 -->
<el-button :disabled="loading" @click="visible = false">取消</el-button>
<!-- 提交向量化 -->
<el-button type="primary" :loading="loading" @click="handleSubmit">确认</el-button>
</template>
</MkDialog>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { MsgSuccess } from '@/utils/message'
import type DocumentApi from '@/api/admin/workspace/knowledge/document'

defineOptions({ name: 'ExportDocumentAction' })
const props = defineProps<{ api: typeof DocumentApi; knowledgeId: string; documentIds: string[]; format: 'excel' | 'zip' }>()
const loading = defineModel<boolean>('loading', { default: false })

// 导出单个或选中文档,保留列表和勾选状态。
function handleExport() {
const ids = [...props.documentIds]
if (!ids.length || loading.value) return
loading.value = true
return props.api
.exportDocuments(props.knowledgeId, ids, props.format)
.then(() => {
MsgSuccess('操作成功')
})
.catch(() => {})
.finally(() => {
loading.value = false
})
}
</script>

<template>
<!-- 导出文档 -->
<MkAction
:label="format === 'excel' ? '导出 Excel' : '导出 Zip'"
icon="icon_export_outlined"
:disabled="loading || !documentIds.length"
@click="handleExport"
/>
</template>
Loading
Loading