From b93bdd9ea5a9cc051c55034d7fd2930efadf25c4 Mon Sep 17 00:00:00 2001 From: Feynman Date: Thu, 23 Apr 2026 13:19:57 +0800 Subject: [PATCH 1/3] feat: add key-value editor export, enhance form styles, and update i18n translations --- packages/dag/src/components/form/index.ts | 1 + .../form/key-value-editor/index.tsx | 227 ++++++++++++++++++ .../form/key-value-editor/style.scss | 41 ++++ .../form/src/components/array-items/index.ts | 173 +++++++++++++ packages/form/src/components/index.ts | 2 + .../src/components/key-value-editor/index.tsx | 0 packages/form/src/style.scss | 5 +- packages/i18n/src/locale/lang/en.js | 6 + packages/i18n/src/locale/lang/zh-CN.js | 6 + packages/i18n/src/locale/lang/zh-TW.js | 6 + packages/types/src/daas-auto-imports.d.ts | 1 + 11 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 packages/dag/src/components/form/key-value-editor/index.tsx create mode 100644 packages/dag/src/components/form/key-value-editor/style.scss create mode 100644 packages/form/src/components/array-items/index.ts create mode 100644 packages/form/src/components/key-value-editor/index.tsx diff --git a/packages/dag/src/components/form/index.ts b/packages/dag/src/components/form/index.ts index 30d2947165..b4fb7a8b96 100644 --- a/packages/dag/src/components/form/index.ts +++ b/packages/dag/src/components/form/index.ts @@ -35,3 +35,4 @@ export * from './source-database-node' export * from './data-preview' export * from './compare-schema' export * from './merge-table-cache' +export * from './key-value-editor' diff --git a/packages/dag/src/components/form/key-value-editor/index.tsx b/packages/dag/src/components/form/key-value-editor/index.tsx new file mode 100644 index 0000000000..4f6f53283d --- /dev/null +++ b/packages/dag/src/components/form/key-value-editor/index.tsx @@ -0,0 +1,227 @@ +import { observer } from '@formily/reactive-vue' +import { useField } from '@formily/vue' +import { FormItem } from '@tap/form' +import { useI18n } from '@tap/i18n' +import { ElMessage, ElSegmented } from 'element-plus' +import { computed, defineComponent, ref } from 'vue' +import './style.scss' + +interface KVItem { + [key: string]: string +} + +const VIEW_FORM = 'form' +const VIEW_TEXT = 'text' + +export const KeyValueEditor = observer( + defineComponent({ + name: 'KeyValueEditor', + inheritAttrs: false, + props: { + value: { + type: Array, + default: () => [], + }, + disabled: Boolean, + keyField: { + type: String, + default: 'propKey', + }, + valueField: { + type: String, + default: 'propValue', + }, + keyPlaceholder: { + type: String, + default: 'Key', + }, + valuePlaceholder: { + type: String, + default: 'Value', + }, + }, + emits: ['change'], + setup(props, { emit, attrs }) { + const { t } = useI18n() + const fieldRef = useField() + const viewMode = ref(VIEW_FORM) + const textContent = ref('') + + const viewOptions = computed(() => [ + { label: t('public_form_view'), value: VIEW_FORM }, + { label: t('public_text_view'), value: VIEW_TEXT }, + ]) + + const items = computed( + () => (Array.isArray(props.value) ? props.value : []) as KVItem[], + ) + + function emitChange(list: KVItem[]) { + emit('change', list) + } + + function handleAdd() { + emitChange([ + ...items.value, + { [props.keyField]: '', [props.valueField]: '' }, + ]) + } + + function handleRemove(index: number) { + const list = [...items.value] + list.splice(index, 1) + emitChange(list) + } + + function handleItemChange(index: number, field: string, val: string) { + const list = items.value.map((item: KVItem, i: number) => + i === index ? { ...item, [field]: val } : item, + ) + emitChange(list) + } + + // 序列化 value 为文本格式 + function serializeToText(list: KVItem[]): string { + if (!list?.length) return '' + return list + .map( + (item) => `'${item[props.keyField]}' = '${item[props.valueField]}'`, + ) + .join(',\n') + } + + // 解析文本为 KVItem[] + function parseText(text: string): KVItem[] { + return text + .split('\n') + .map((l) => l.trim()) + .filter(Boolean) + .map((line) => { + line = line.replace(/[,;]+$/, '').trim() + const match = line.match( + /^['"]?([^'"=]+?)['"]?\s*=\s*['"]?([^'"]*)['"]?$/, + ) + if (!match) return null + return { + [props.keyField]: match[1].trim(), + [props.valueField]: match[2].trim(), + } + }) + .filter(Boolean) as KVItem[] + } + + function handleViewChange(mode: string) { + if (mode === VIEW_TEXT) { + textContent.value = serializeToText(items.value) + } + viewMode.value = mode + } + + function handleApply() { + const parsed = parseText(textContent.value) + if (!parsed.length && textContent.value.trim()) { + ElMessage.warning(t('public_parse_failed')) + return + } + emitChange(parsed) + ElMessage.success(t('public_import_count', [parsed.length])) + viewMode.value = VIEW_FORM + } + + function handleCopy() { + navigator.clipboard.writeText(serializeToText(items.value)).then(() => { + ElMessage.success(t('public_message_copy_success')) + }) + } + + const renderLabel = () => ( +
+ {fieldRef.value.title} + +
+ ) + + return () => ( + +
+ {viewMode.value === VIEW_FORM ? ( +
+ {items.value.map((item: KVItem, index: number) => ( +
+ {index + 1} + + handleItemChange(index, props.keyField, val) + } + /> + + handleItemChange(index, props.valueField, val) + } + /> + handleRemove(index)} + icon={IconLucideTrash2} + > +
+ ))} + +
+ ) : ( +
+ + (textContent.value = val) + } + rows={8} + placeholder={`'bucket' = '8'\n"file.format" = "parquet"\ncompression=snappy`} + disabled={props.disabled} + /> +
+ + {t('public_button_copy')} + + + {t('public_apply_to_form')} + +
+
+ )} +
+
+ ) + }, + }), +) + +export default KeyValueEditor diff --git a/packages/dag/src/components/form/key-value-editor/style.scss b/packages/dag/src/components/form/key-value-editor/style.scss new file mode 100644 index 0000000000..4f7c5cf55a --- /dev/null +++ b/packages/dag/src/components/form/key-value-editor/style.scss @@ -0,0 +1,41 @@ +.key-value-editor-form-item { + .formily-element-plus-form-item-label-content { + flex: 1; + } + .formily-element-plus-form-item-colon { + display: none; + } + .formily-element-plus-space { + display: flex; + } +} + +.key-value-editor__text-box { + border-radius: 10px; + display: flex; + flex-direction: column; + gap: 8px; + padding-bottom: 8px; + overflow: hidden; + box-shadow: 0 0 0 1px var(--el-border-color) inset; + transition: box-shadow var(--el-transition-duration); + + &:focus-within { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; + } + + .el-textarea { + .el-textarea__inner { + border: none; + box-shadow: none; + background: transparent; + resize: none; + } + } +} + +.key-value-editor__text-actions { + display: flex; + justify-content: flex-end; + padding: 0 8px; +} diff --git a/packages/form/src/components/array-items/index.ts b/packages/form/src/components/array-items/index.ts new file mode 100644 index 0000000000..bde9e970c4 --- /dev/null +++ b/packages/form/src/components/array-items/index.ts @@ -0,0 +1,173 @@ +import { ArrayBase } from '@formily/element-plus/esm/array-base' +import { + composeExport, + stylePrefix, +} from '@formily/element-plus/lib/__builtins__' +import { observer } from '@formily/reactive-vue' +import { RecursionField, useField, useFieldSchema } from '@formily/vue' +import { defineComponent, h } from 'vue' + +import Draggable from 'vuedraggable' +import type { ArrayField } from '@formily/core' +import type { ISchema } from '@formily/json-schema' + +const isAdditionComponent = (schema: ISchema) => { + return schema['x-component']?.indexOf('Addition') > -1 +} + +export interface IArrayItemsItemProps { + type?: 'card' | 'divide' +} + +const ArrayItemsInner = observer( + defineComponent({ + name: 'FArrayItems', + inheritAttrs: false, + setup() { + console.log('Addition') + const fieldRef = useField() + const schemaRef = useFieldSchema() + + const prefixCls = `${stylePrefix}-array-items` + const { getKey, keyMap } = ArrayBase.useKey(schemaRef.value) + + return () => { + const field = fieldRef.value + const schema = schemaRef.value + const dataSource = Array.isArray(field.value) ? field.value.slice() : [] + + const renderItems = () => { + const itemSlot = ({ + element, + index, + }: { + element: any + index: number + }) => { + const items = Array.isArray(schema.items) + ? schema.items[index] || schema.items[0] + : schema.items + const key = getKey(element, index) + return h( + 'div', + {}, + h( + ArrayBase.Item, + { + key, + index, + record: element, + }, + { + default: () => + h( + 'div', + { + class: [`${prefixCls}-item-inner`], + index, + key, + }, + h(RecursionField, { + schema: items, + name: index, + }), + ), + }, + ), + ) + } + + return h( + Draggable, + { + class: [`${prefixCls}-list`], + value: [], + list: dataSource, + handle: `.${stylePrefix}-array-base-sort-handle`, + itemKey: (item: any, index: number) => getKey(item, index), + onChange(evt: any) { + if (evt.moved) { + const { oldIndex, newIndex } = evt.moved + if (Array.isArray(keyMap)) { + keyMap.splice(newIndex, 0, keyMap.splice(oldIndex, 1)[0]) + } + field.move(oldIndex, newIndex) + } + }, + }, + { item: itemSlot }, + ) + } + const renderAddition = () => { + return schema.reduceProperties((addition, schema) => { + if (isAdditionComponent(schema)) { + return h(RecursionField, { + schema, + name: 'addition', + }) + } + return addition + }, null) + } + + return h( + ArrayBase, + { + keyMap, + }, + { + default: () => + h( + 'div', + { + class: [prefixCls], + onChange: () => { + return + }, + }, + { + default: () => [renderItems(), renderAddition()], + }, + ), + }, + ) + } + }, + }), +) + +const ArrayItemsItem = defineComponent({ + name: 'FArrayItemsItem', + props: ['type'], + setup(props, { attrs, slots }) { + const prefixCls = `${stylePrefix}-array-items` + + return () => + h( + 'div', + { + class: [`${prefixCls}-${props.type || 'card'}`], + ...attrs, + onChange: () => { + return + }, + }, + slots, + ) + }, +}) + +export const ArrayItems = composeExport(ArrayItemsInner, { + Item: ArrayItemsItem, + Index: ArrayBase.Index, + SortHandle: ArrayBase.SortHandle, + Addition: ArrayBase.Addition, + Remove: ArrayBase.Remove, + MoveDown: ArrayBase.MoveDown, + MoveUp: ArrayBase.MoveUp, + useArray: ArrayBase.useArray, + useIndex: ArrayBase.useIndex, + useRecord: ArrayBase.useRecord, +}) + +export default ArrayItems diff --git a/packages/form/src/components/index.ts b/packages/form/src/components/index.ts index 7e08e28320..fc7f64ad19 100644 --- a/packages/form/src/components/index.ts +++ b/packages/form/src/components/index.ts @@ -3,6 +3,7 @@ import { ElDivider as Divider, ElLink as Link, } from 'element-plus' +import { ArrayItems } from './array-items' import { FormCollapse, FormCollapseItem } from './form-collapse' import { FormTab } from './form-tab' import { Input } from './input' @@ -15,6 +16,7 @@ import { Switch } from './switch' export { Alert, Divider, Link } // 覆盖 formily-element-plus export { + ArrayItems, FormCollapse, FormCollapseItem, FormTab, diff --git a/packages/form/src/components/key-value-editor/index.tsx b/packages/form/src/components/key-value-editor/index.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/form/src/style.scss b/packages/form/src/style.scss index 02ef45eea6..5f8e808e4e 100644 --- a/packages/form/src/style.scss +++ b/packages/form/src/style.scss @@ -141,7 +141,6 @@ $formNamespace: formily-element-plus; background-color: #bcbfc3; } } - .#{$formNamespace}-form-item-label .el-button { line-height: 1; @@ -154,6 +153,10 @@ $formNamespace: formily-element-plus; } } +.#{$formNamespace}-form-item-label .el-segmented__item-label { + line-height: normal; +} + .#{$formNamespace}-array-table { .el-table { diff --git a/packages/i18n/src/locale/lang/en.js b/packages/i18n/src/locale/lang/en.js index 15a5185f6f..1081d08295 100644 --- a/packages/i18n/src/locale/lang/en.js +++ b/packages/i18n/src/locale/lang/en.js @@ -653,4 +653,10 @@ export default { public_table_regex_example_2: 'Matches tables ending with _temp', public_table_regex_example_3: 'Matches tables containing test or demo', public_data_no_result: 'No Data Found', + public_form_view: 'Form View', + public_text_view: 'Text View', + public_parse_failed: 'Parse failed, please check the format', + public_import_count: '{0} items imported', + public_apply_to_form: 'Apply to Form', + public_copy_text: 'Copy Text', } diff --git a/packages/i18n/src/locale/lang/zh-CN.js b/packages/i18n/src/locale/lang/zh-CN.js index 5875fb081a..4dc61e29e9 100644 --- a/packages/i18n/src/locale/lang/zh-CN.js +++ b/packages/i18n/src/locale/lang/zh-CN.js @@ -645,4 +645,10 @@ export default { public_table_regex_example_2: '匹配以 _temp 结尾的表', public_table_regex_example_3: '匹配包含 test 或 demo 的表', public_data_no_result: '未找到数据', + public_form_view: '表单视图', + public_text_view: '文本视图', + public_parse_failed: '解析失败,请检查格式', + public_import_count: '已导入 {0} 条', + public_apply_to_form: '应用到表单', + public_copy_text: '复制文本', } diff --git a/packages/i18n/src/locale/lang/zh-TW.js b/packages/i18n/src/locale/lang/zh-TW.js index 12c116f652..c21fa20976 100644 --- a/packages/i18n/src/locale/lang/zh-TW.js +++ b/packages/i18n/src/locale/lang/zh-TW.js @@ -642,4 +642,10 @@ export default { public_table_regex_example_2: '匹配以 _temp 結尾的表', public_table_regex_example_3: '匹配包含 test 或 demo 的表', public_data_no_result: '未找到數據', + public_form_view: '表單檢視', + public_text_view: '文字檢視', + public_parse_failed: '解析失敗,請檢查格式', + public_import_count: '已匯入 {0} 條', + public_apply_to_form: '套用到表單', + public_copy_text: '複製文字', } diff --git a/packages/types/src/daas-auto-imports.d.ts b/packages/types/src/daas-auto-imports.d.ts index 312c1de2d7..76859eff5e 100644 --- a/packages/types/src/daas-auto-imports.d.ts +++ b/packages/types/src/daas-auto-imports.d.ts @@ -33,6 +33,7 @@ declare global { const IconLucideMaximize2: typeof import('~icons/lucide/maximize2').default const IconLucideMinimize2: typeof import('~icons/lucide/minimize2').default const IconLucidePlay: typeof import('~icons/lucide/play').default + const IconLucidePlus: typeof import('~icons/lucide/plus').default const IconLucideSettings2: typeof import('~icons/lucide/settings2').default const IconLucideSparkles: typeof import('~icons/lucide/sparkles').default const IconLucideTrash2: typeof import('~icons/lucide/trash2').default From b447a97d42509cd99ea41b7a9c70b8d3fb7c0b22 Mon Sep 17 00:00:00 2001 From: Feynman Date: Thu, 23 Apr 2026 14:22:38 +0800 Subject: [PATCH 2/3] refactor: update KeyValueEditor to use autosize and enhance Catalogue styles --- packages/dag/src/components/form/key-value-editor/index.tsx | 2 +- packages/ldp/src/TablePreview.vue | 6 +++--- packages/ldp/src/components/Catalogue.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dag/src/components/form/key-value-editor/index.tsx b/packages/dag/src/components/form/key-value-editor/index.tsx index 4f6f53283d..0065633ba7 100644 --- a/packages/dag/src/components/form/key-value-editor/index.tsx +++ b/packages/dag/src/components/form/key-value-editor/index.tsx @@ -199,9 +199,9 @@ export const KeyValueEditor = observer( onUpdate:modelValue={(val: string) => (textContent.value = val) } - rows={8} placeholder={`'bucket' = '8'\n"file.format" = "parquet"\ncompression=snappy`} disabled={props.disabled} + autosize />
diff --git a/packages/ldp/src/TablePreview.vue b/packages/ldp/src/TablePreview.vue index 60bd2c449d..d40e093e31 100644 --- a/packages/ldp/src/TablePreview.vue +++ b/packages/ldp/src/TablePreview.vue @@ -1571,9 +1571,9 @@ export default { } } - :deep(.table-preview-tabs > .el-tabs__content > .el-tab-pane) { - background-color: rgb(245, 248, 254); - } + // :deep(.table-preview-tabs > .el-tabs__content > .el-tab-pane) { + // background-color: rgb(245, 248, 254); + // } :deep(th .cell) { white-space: nowrap; diff --git a/packages/ldp/src/components/Catalogue.tsx b/packages/ldp/src/components/Catalogue.tsx index 5c80993c2f..0b55049d64 100644 --- a/packages/ldp/src/components/Catalogue.tsx +++ b/packages/ldp/src/components/Catalogue.tsx @@ -191,7 +191,7 @@ export default defineComponent({ return (
{ notLast && setTreeCurrent(node.data) }} @@ -266,7 +266,7 @@ export default defineComponent({ )}
From bfe00e0046976ea768bcaa8f6ae9167fe5ebb42b Mon Sep 17 00:00:00 2001 From: Feynman Date: Thu, 23 Apr 2026 14:58:50 +0800 Subject: [PATCH 3/3] style: adjust KeyValueEditor layout and clean up unused styles in SCSS --- packages/dag/src/components/form/key-value-editor/index.tsx | 1 + packages/dag/src/components/form/key-value-editor/style.scss | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dag/src/components/form/key-value-editor/index.tsx b/packages/dag/src/components/form/key-value-editor/index.tsx index 0065633ba7..a238892335 100644 --- a/packages/dag/src/components/form/key-value-editor/index.tsx +++ b/packages/dag/src/components/form/key-value-editor/index.tsx @@ -138,6 +138,7 @@ export const KeyValueEditor = observer(
{fieldRef.value.title}