Skip to content

Commit 4aee0bb

Browse files
feat: workflow debug
1 parent 1cfdcd3 commit 4aee0bb

10 files changed

Lines changed: 398 additions & 535 deletions

File tree

‎ui/src/components/COMPONENT_README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,17 @@ JSON 专用输入框,通过 `v-model` 接收并回传解析后的 JSON 值,
762762
表单配置加载、字段联动请求与 Tree 懒加载的 loading 由组件在请求前开启,并在 `finally` 中释放;
763763
不向 API 请求方法传递 loading,动态请求脚本也不再通过 `extra.loading` 获取状态。
764764

765+
动态表单 `Tree` 字段使用 `MkTable` 展示文档与文件夹,复用原生复选列、全选和父子选择联动。
766+
保留 `text_field`、`value_field`、`childrenField` 映射,选中值仍为原始节点对象数组;
767+
已导入节点自动选中且不可取消,`disabled` 节点不可选择。懒加载先查询根目录,展开时通过
768+
原有工具数据源接口提交 `current_node` 查询子节点;非懒加载使用 `option_list`。
769+
770+
动态表单 `LocalFileUpload` 字段复用 `MkDragUpload` 的多文件模式,将
771+
`file_type_list`、`file_size_limit`、`file_count_limit` 映射为格式、大小和数量限制。
772+
上传适配兼容上层返回的 Promise 或 `{ request, abort }`,响应支持地址字符串或 `{ data: 地址 }`;
773+
文件夹选择、进度、失败重试、取消和成功文件删除由 `MkDragUpload` 维护,文件值采用
774+
`DragUploadFile[]`,失败状态为 `fail`。
775+
765776
`MkDynamicsForm` 根据字段配置渲染动态表单,统一维护字段值、默认值、显隐规则和表单校验;
766777
`MkDynamicsFormConstructor` 用于新增或编辑单个字段配置。该组件族位于 `components` 直属目录,
767778
使用方必须从 `@/components/mk-dynamics-form` 手动导入,不安装为 Vue 插件,也不全局注册其内部
Lines changed: 145 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,167 @@
11
<script setup lang="ts">
2-
import type { DynamicFormValue } from '../../type'
3-
import { computed, ref, useAttrs, nextTick, inject } from 'vue'
4-
import type { FormField } from '@/components/mk-dynamics-form/type'
2+
import { computed, inject, nextTick, ref, toRaw, useAttrs, useTemplateRef, watch } from 'vue'
3+
import { cloneDeep, isEqual } from 'lodash'
4+
import { formItemContextKey, type TableInstance, type TreeNode } from 'element-plus'
5+
import type { Dict } from '@/api/types'
56
import { post } from '@/api/admin/core/request'
6-
import { cloneDeep } from 'lodash'
7-
import { formItemContextKey } from 'element-plus'
8-
import type { LoadFunction } from 'element-plus'
9-
const getExtra = inject('get_extra') as DynamicFormValue
10-
const elFormItem = inject(formItemContextKey, void 0)
7+
import { getFileIconUrl } from '@/utils/icon'
8+
import type { DynamicFormValue, FormField } from '../../type'
119
12-
defineOptions({ name: 'DynamicFormTree' })
10+
defineOptions({ name: 'DynamicFormTree', inheritAttrs: false })
1311
14-
const allCheck = ref<boolean>(false)
15-
16-
const handleAllCheckChange = (checked: boolean) => {
17-
if (checked) {
18-
const nodes = Object.values(treeRef.value?.store.nodesMap || {}) as DynamicFormValue[]
19-
nodes.forEach((node) => {
20-
if (!node.disabled) {
21-
treeRef.value?.setChecked(node.data, true, false)
22-
}
23-
})
24-
} else {
25-
treeRef.value?.setCheckedKeys([])
26-
}
12+
interface DocumentRow extends Dict<DynamicFormValue> {
13+
hasChildren: boolean
2714
}
28-
const textField = computed(() => {
29-
return props.formField.text_field ? props.formField.text_field : 'label'
30-
})
3115
32-
const valueField = computed(() => {
33-
return props.formField.value_field ? props.formField.value_field : 'value'
34-
})
35-
const childrenField = computed(() => {
36-
return props.formField.childrenField ? props.formField.childrenField : 'children'
37-
})
38-
const options = computed(() => {
39-
return props.formField.option_list ? props.formField.option_list : []
40-
})
41-
const propsData = computed(() => {
42-
return {
43-
label: textField,
44-
children: childrenField,
45-
isLeaf: (data: DynamicFormValue) => data.leaf,
46-
disabled: (data: DynamicFormValue) => data.disabled,
47-
}
16+
const props = withDefaults(defineProps<{ modelValue?: DynamicFormValue; formField: FormField; otherParams: DynamicFormValue }>(), {
17+
modelValue: () => [],
4818
})
19+
const emit = defineEmits<{ 'update:modelValue': [value: DynamicFormValue[]]; change: [value: DynamicFormValue[]] }>()
20+
const attrs = useAttrs()
21+
const getExtra = inject<() => Dict<DynamicFormValue>>('get_extra')
22+
const elFormItem = inject(formItemContextKey, undefined)
23+
24+
// 字段映射与表格数据,保留完整节点对象作为选中值。
25+
const textField = computed(() => props.formField.text_field || 'label')
26+
const valueField = computed(() => props.formField.value_field || 'value')
27+
const childrenField = computed(() => props.formField.childrenField || 'children')
28+
const options = computed(() => props.formField.option_list ?? [])
29+
const lazy = computed(() => Boolean(props.formField.attrs?.lazy ?? attrs.lazy))
30+
const documentRows = ref<DocumentRow[]>([])
31+
const documentTableRef = useTemplateRef<{ tableRef?: TableInstance }>('documentTableRef')
32+
const tableKey = ref(0)
33+
const pendingLoads = ref(0)
34+
const documentNodes = new Map<string | number, DocumentRow>()
35+
const originalNodes = new WeakMap<DocumentRow, Dict<DynamicFormValue>>()
36+
let restoringSelection = false
4937
50-
const attrs = useAttrs() as DynamicFormValue
51-
const treeRef = ref<DynamicFormValue>(null)
52-
function renderTemplate(template: string, data: DynamicFormValue) {
53-
return template.replace(/\$\{(\w+)\}/g, (match, key) => {
54-
return data[key] !== undefined ? data[key] : match
38+
function normalizeDocuments(documents: Dict<DynamicFormValue>[]): DocumentRow[] {
39+
return documents.map((document) => {
40+
const row: DocumentRow = {
41+
...document,
42+
hasChildren: lazy.value && (document.leaf === false || (document.leaf !== true && (document.type === 'folder' || !document.type))),
43+
}
44+
originalNodes.set(row, document)
45+
documentNodes.set(row[valueField.value], row)
46+
if (Array.isArray(document[childrenField.value])) {
47+
row[childrenField.value] = normalizeDocuments(document[childrenField.value])
48+
}
49+
return row
5550
})
5651
}
5752
58-
const loadNode: LoadFunction = (node, resolve) => {
59-
const url = renderTemplate(
60-
'/workspace/${current_workspace_id}/knowledge/${current_knowledge_id}/datasource/tool/${current_tool_id}/' + attrs.fetch_list_function,
61-
{
62-
...props.otherParams,
63-
...(getExtra ? getExtra() : {}),
64-
},
65-
)
66-
loading.value = true
67-
return post<{ current_node: DynamicFormValue }, DynamicFormValue[]>(url, { current_node: node.level === 0 ? undefined : node.data })
68-
.then((children) => {
69-
resolve(children)
70-
children.forEach((childNode) => {
71-
if (childNode.is_exist) {
72-
treeRef.value?.setChecked(childNode.token, true, false)
73-
}
74-
})
75-
})
53+
function handleSelectionChange(selection: unknown[]) {
54+
if (restoringSelection) return
55+
const selectedDocuments = cloneDeep((selection as DocumentRow[]).map((row) => originalNodes.get(toRaw(row)) ?? row))
56+
if (isEqual(selectedDocuments, props.modelValue ?? [])) return
57+
emit('update:modelValue', selectedDocuments)
58+
emit('change', selectedDocuments)
59+
nextTick(() => elFormItem?.validate('change').catch(() => {}))
60+
}
61+
62+
function restoreSelection() {
63+
const table = documentTableRef.value?.tableRef
64+
if (!table) return
65+
const selectedKeys = new Set((props.modelValue ?? []).map((document: Dict<DynamicFormValue>) => document[valueField.value]))
66+
restoringSelection = true
67+
table.clearSelection()
68+
documentNodes.forEach((row, key) => {
69+
if (row.is_exist || selectedKeys.has(key)) table.toggleRowSelection(row, true, true)
70+
})
71+
restoringSelection = false
72+
handleSelectionChange(table.getSelectionRows() as DocumentRow[])
73+
}
74+
75+
// 懒加载沿用工具数据源协议,根目录不传 current_node。
76+
function renderTemplate(template: string, data: Dict<DynamicFormValue>) {
77+
return template.replace(/\$\{(\w+)\}/g, (match, key) => (data[key] !== undefined ? String(data[key]) : match))
78+
}
79+
const requestUrl = computed(() =>
80+
renderTemplate(
81+
'/workspace/${current_workspace_id}/knowledge/${current_knowledge_id}/datasource/tool/${current_tool_id}/' +
82+
(props.formField.attrs?.fetch_list_function ?? attrs.fetch_list_function),
83+
{ ...props.otherParams, ...getExtra?.() },
84+
),
85+
)
86+
87+
function loadDocuments(currentNode?: Dict<DynamicFormValue>) {
88+
pendingLoads.value += 1
89+
return post<{ current_node?: Dict<DynamicFormValue> }, Dict<DynamicFormValue>[]>(requestUrl.value, { current_node: currentNode })
90+
.then(normalizeDocuments)
7691
.finally(() => {
77-
loading.value = false
92+
pendingLoads.value -= 1
7893
})
7994
}
80-
const props = withDefaults(defineProps<{ modelValue?: DynamicFormValue; formField: FormField; otherParams: DynamicFormValue }>(), {
81-
modelValue: () => [],
82-
})
8395
84-
const emit = defineEmits(['update:modelValue', 'change'])
96+
function loadNode(document: DocumentRow, treeNode: TreeNode, resolve: (documents: DocumentRow[]) => void) {
97+
return loadDocuments(originalNodes.get(toRaw(document)) ?? document)
98+
.then((documents) => {
99+
resolve(documents)
100+
return nextTick(restoreSelection)
101+
})
102+
.catch(() => {
103+
// Table 无 reject 回调,恢复加载标记以便再次展开重试。
104+
treeNode.loading = false
105+
})
106+
}
85107
86-
const modelValueProxy = computed({
87-
get: () => {
88-
if (!props.modelValue) {
89-
emit('update:modelValue', [])
108+
watch(
109+
[options, lazy, requestUrl],
110+
() => {
111+
documentNodes.clear()
112+
documentRows.value = []
113+
tableKey.value += 1
114+
if (!lazy.value) {
115+
documentRows.value = normalizeDocuments(options.value)
116+
return nextTick(restoreSelection)
90117
}
91-
return props.modelValue
92-
},
93-
set: (v: DynamicFormValue[]) => {
94-
emit('update:modelValue', v)
118+
return loadDocuments()
119+
.then((documents) => {
120+
documentRows.value = documents
121+
return nextTick(restoreSelection)
122+
})
123+
.catch(() => {
124+
// 请求层统一提示错误。
125+
})
95126
},
96-
})
97-
const change = () => {
98-
modelValueProxy.value = cloneDeep(treeRef.value?.getCheckedNodes() || [])
99-
nextTick(() => {
100-
if (elFormItem?.validate) {
101-
elFormItem.validate('change')
102-
}
103-
})
104-
}
105-
106-
const loading = ref<boolean>(false)
127+
{ immediate: true, deep: true },
128+
)
129+
watch(() => props.modelValue, restoreSelection, { deep: true })
107130
</script>
108131

109132
<template>
110-
<div v-loading="loading" class="w-full">
111-
<div class="card-never border-r-6 mb-16">
112-
<el-checkbox v-model="allCheck" label="全选" size="large" class="ml-24" @change="handleAllCheckChange" />
113-
</div>
114-
<div style="height: calc(100vh - 450px)">
115-
<el-scrollbar>
116-
<el-tree
117-
:data="options"
118-
@check-change="change"
119-
v-loading="loading"
120-
style="width: 100%"
121-
:props="propsData"
122-
:load="loadNode"
123-
:lazy="attrs.lazy"
124-
show-checkbox
125-
:node-key="valueField"
126-
ref="treeRef"
127-
>
128-
<template #default="{ node, data }">
129-
<div class="flex align-center lighter">
130-
<img :src="data.icon" alt="" height="20" v-if="data.icon" />
131-
<img src="@/assets/empty/no-data.svg" alt="" height="20" v-else-if="data.type === 'folder'" />
132-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.type === 'docx' || data.name.endsWith('.docx')" />
133-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.type === 'sheet' || data.name.endsWith('.xlsx')" />
134-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.name.endsWith('xls')" />
135-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.name.endsWith('csv')" />
136-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.name.endsWith('.pdf')" />
137-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.name.endsWith('.html')" />
138-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.name.endsWith('.txt')" />
139-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.name.endsWith('.zip')" />
140-
<img src="@/assets/empty/no-data.svg" alt="" height="22" v-else-if="data.name.endsWith('.md')" />
141-
142-
<span class="ml-4">{{ node.label }}</span>
143-
</div>
144-
</template>
145-
</el-tree>
146-
</el-scrollbar>
147-
</div>
148-
</div>
133+
<MkTable
134+
:key="tableKey"
135+
ref="documentTableRef"
136+
v-loading="pendingLoads > 0"
137+
:data="documentRows"
138+
:row-key="valueField"
139+
:tree-props="{ children: childrenField, hasChildren: 'hasChildren', checkStrictly: false }"
140+
:load="loadNode"
141+
:max-table-height="430"
142+
:lazy="lazy"
143+
@selection-change="handleSelectionChange"
144+
>
145+
<!-- 选择文档或文件夹,全选由表格维护 -->
146+
<el-table-column type="selection" width="40" reserve-selection :selectable="(row: DocumentRow) => !row.disabled && !row.is_exist" />
147+
<el-table-column class-name="expand-name-column" label="全部文档" :prop="textField">
148+
<template #default="{ row }: { row: DocumentRow }">
149+
<div class="flex-align-center min-w-0 flex-1 gap-2">
150+
<img v-if="row.icon" :src="row.icon" alt="" class="w-4.5 shrink-0" />
151+
<img v-else-if="row.type === 'folder'" src="@/assets/file-type/file-icon.svg" alt="" class="w-4.5 shrink-0" />
152+
<img
153+
v-else
154+
:src="
155+
getFileIconUrl(
156+
row.type === 'docx' ? `${row.name}.docx` : row.type === 'sheet' ? `${row.name}.xlsx` : String(row.name ?? row[textField] ?? ''),
157+
)
158+
"
159+
alt=""
160+
class="w-4.5 shrink-0"
161+
/>
162+
<span :title="row[textField]" class="min-w-0 truncate">{{ row[textField] }}</span>
163+
</div>
164+
</template>
165+
</el-table-column>
166+
</MkTable>
149167
</template>

0 commit comments

Comments
 (0)