diff --git a/packages/dag/src/components/form/index.ts b/packages/dag/src/components/form/index.ts
index 30d294716..b4fb7a8b9 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 000000000..a23889233
--- /dev/null
+++ b/packages/dag/src/components/form/key-value-editor/index.tsx
@@ -0,0 +1,228 @@
+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)
+ }
+ placeholder={`'bucket' = '8'\n"file.format" = "parquet"\ncompression=snappy`}
+ disabled={props.disabled}
+ autosize
+ />
+
+
+ {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 000000000..3a929d316
--- /dev/null
+++ b/packages/dag/src/components/form/key-value-editor/style.scss
@@ -0,0 +1,38 @@
+.key-value-editor-form-item {
+ .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 000000000..bde9e970c
--- /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 7e08e2832..fc7f64ad1 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 000000000..e69de29bb
diff --git a/packages/form/src/style.scss b/packages/form/src/style.scss
index 02ef45eea..5f8e808e4 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 15a5185f6..1081d0829 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 5875fb081..4dc61e29e 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 12c116f65..c21fa2097 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/ldp/src/TablePreview.vue b/packages/ldp/src/TablePreview.vue
index 60bd2c449..d40e093e3 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 5c80993c2..0b55049d6 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({
)}
diff --git a/packages/types/src/daas-auto-imports.d.ts b/packages/types/src/daas-auto-imports.d.ts
index 312c1de2d..76859eff5 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