From f89e9747c02817847a1dddcc366974606ba7055c Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 1 Jun 2026 13:54:27 +0800 Subject: [PATCH 01/26] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B8=A6?= =?UTF-8?q?=E6=9C=89=E7=9B=AE=E6=A0=87=E7=BB=84=E4=BB=B6=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E5=90=8E=EF=BC=8C?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=9C=A8=E4=BA=8B=E4=BB=B6=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E4=B8=AD=E6=9F=A5=E7=9C=8B=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/monaco-editor/monaco-editor.component.tsx | 3 ++- .../method-manager/composition/build-method.ts | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/designer/src/components/components/monaco-editor/monaco-editor.component.tsx b/packages/designer/src/components/components/monaco-editor/monaco-editor.component.tsx index fd37bd04b..3a72e2019 100644 --- a/packages/designer/src/components/components/monaco-editor/monaco-editor.component.tsx +++ b/packages/designer/src/components/components/monaco-editor/monaco-editor.component.tsx @@ -23,7 +23,8 @@ export default defineComponent({ folding: true, readOnly: props.readOnly, automaticLayout: true, - mouseWheelZoom: true // 启用滚轮缩放 + mouseWheelZoom: true, // 启用滚轮缩放 + scrollBeyondLastLine: false // 开启此选项后,会在最后一行下方额外增加约一屏高度的滚动区域。目前关闭 }; async function initMonacoEditor() { diff --git a/packages/designer/src/components/components/view-model-designer/method-manager/composition/build-method.ts b/packages/designer/src/components/components/view-model-designer/method-manager/composition/build-method.ts index aaa941fee..c3a3a6ad1 100644 --- a/packages/designer/src/components/components/view-model-designer/method-manager/composition/build-method.ts +++ b/packages/designer/src/components/components/view-model-designer/method-manager/composition/build-method.ts @@ -126,10 +126,10 @@ export class MethodBuilder { let requestCommandsMetadata = webCmdInfos.map((commandInfo: FormWebCmd) => { const existedController = this.controllers?.find(controller => controller.Id === commandInfo.id); if (existedController) { - if(!(existedController.Extends&&Object.prototype.hasOwnProperty.call(existedController.Extends,'IsCommon')&&!existedController.Extends.IsCommon)){ + if (!(existedController.Extends && Object.prototype.hasOwnProperty.call(existedController.Extends, 'IsCommon') && !existedController.Extends.IsCommon)) { existedCommands.push(existedController); return; - } + } } return new MetadataService().getRefMetadata(relativePath, commandInfo.id).catch(error => { this.messageService.warning(`获取元数据${commandInfo.name}失败,请检查。`, ''); @@ -462,6 +462,8 @@ export class MethodBuilder { } Object.assign(this.commandToBePasted.value, newCommandData, newCommandCodeName); + this.commandToBePasted.value.isNewGenerated = false; + this.commandToBePasted.value.targetComponent = undefined; // 1、复制的命令来源于内置控制器 if (this.commandToBePasted.value.isFromPresetController) { @@ -523,7 +525,7 @@ export class MethodBuilder { * 转换命令格式 */ private transformComamnd(command: Command): Command { - const { controllerCode, handlerName, params} = command; + const { controllerCode, handlerName, params } = command; command.params = transformCommandParams(controllerCode, handlerName, params); return command; } -- Gitee From 4103de660617140eef4301c1d27f62d8839ee7d8 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 1 Jun 2026 13:57:02 +0800 Subject: [PATCH 02/26] =?UTF-8?q?fix:=20=E6=94=BE=E5=BC=80=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E6=8F=92=E4=BB=B6=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designer/src/components/designer.component.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/designer/src/components/designer.component.tsx b/packages/designer/src/components/designer.component.tsx index efae383e9..14992da8e 100644 --- a/packages/designer/src/components/designer.component.tsx +++ b/packages/designer/src/components/designer.component.tsx @@ -461,12 +461,12 @@ export default defineComponent({ onBeforeMount(async () => { // 加载并初始插件 - // const pluginService = formDesignerPluginService(useFormSchemaComposition, designerPluginContext); - // try { - // await pluginService.loadPluginsFromConfig(); - // } catch (error) { - // notifyService.error('插件初始化失败', error); - // } + const pluginService = formDesignerPluginService(useFormSchemaComposition, designerPluginContext); + try { + await pluginService.loadPluginsFromConfig(); + } catch (error) { + notifyService.error('插件初始化失败', error); + } // 加载表单元数据 const formSchema = await useFormMetadataComposition.queryMetadata(); @@ -504,7 +504,7 @@ export default defineComponent({ formUpgradeTip.value.resolveAndShowTipContent(); } // 应用插件扩展 - // pluginService.initPluginsAfterMount(); + pluginService.initPluginsAfterMount(); }); -- Gitee From 657a70dbda8e2c88a29bd71f09ff258e9268fa90 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Tue, 2 Jun 2026 17:46:49 +0800 Subject: [PATCH 03/26] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E7=9A=84=E7=BC=93=E5=AD=98=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/composition/plugins/plugin.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/designer/src/components/composition/plugins/plugin.service.ts b/packages/designer/src/components/composition/plugins/plugin.service.ts index 8794e65ce..2b90d5214 100644 --- a/packages/designer/src/components/composition/plugins/plugin.service.ts +++ b/packages/designer/src/components/composition/plugins/plugin.service.ts @@ -113,7 +113,7 @@ export function formDesignerPluginService(useFormSchema: UseFormSchema, designer try { // 加载插件模块(SystemJS 格式) - const pluginModule = await System.import(pluginUrl); + const pluginModule = await System.import(`${pluginUrl}?v=${Date.now()}`); if (!pluginModule?.default) { messageBoxService.error(`插件[${pluginId}]未正确定义definePlugin函数`); -- Gitee From 3c765ee3e0ec076ef673461d2f624ab8ef546f18 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Sat, 27 Jun 2026 16:57:54 +0800 Subject: [PATCH 04/26] feature: merge v1.8.3 --- .../designer/public/assets/rtc/property-rule.json | 14 ++++++++++++-- .../form-designer/form-designer.component.tsx | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/designer/public/assets/rtc/property-rule.json b/packages/designer/public/assets/rtc/property-rule.json index 86fdfaca9..b0f374525 100644 --- a/packages/designer/public/assets/rtc/property-rule.json +++ b/packages/designer/public/assets/rtc/property-rule.json @@ -92,7 +92,8 @@ "events.dictPicking", "events.beforeSelectData", "events.dictPicked", - "events.beforeLoadData" + "events.beforeLoadData", + "onlySelectLeaf" ], "check-box": [ "type", @@ -166,7 +167,16 @@ "onSelectionChange", "fitColumns", "customRowStyle", - "customCellStyle" + "customCellStyle", + "displayType", + "showCardHeader", + "cardMainTitle", + "cardTitleTemplate", + "cardHeaderTemplate", + "cardToolbar", + "showCardFooter", + "cardFooterTemplate", + "rules" ], "data-grid-column": [ "id", diff --git a/packages/designer/src/components/components/form-designer/form-designer.component.tsx b/packages/designer/src/components/components/form-designer/form-designer.component.tsx index 675566738..696474c61 100644 --- a/packages/designer/src/components/components/form-designer/form-designer.component.tsx +++ b/packages/designer/src/components/components/form-designer/form-designer.component.tsx @@ -312,7 +312,7 @@ export default defineComponent({ }); - const shouldRenderExternalComponentPanel = ref(designerMode !== DesignerMode.PC_RTC && !designerContext.responsiveForm); + const shouldRenderExternalComponentPanel = ref(!designerContext.responsiveForm); function renderExternalComponentPanel() { return shouldRenderExternalComponentPanel.value && ( -- Gitee From 85d2191a48e73bb073ac75f56253240fb3c35113 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Tue, 7 Jul 2026 17:45:02 +0800 Subject: [PATCH 05/26] =?UTF-8?q?feature:=20=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E5=AE=9A=E5=88=B6=E6=94=AF=E6=8C=81=E6=8F=90=E5=8F=96=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E5=8C=BA=E5=9F=9F=E4=B8=AD=E7=9A=84=E5=B8=AE=E5=8A=A9?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../composition/form-metadata-rtc.service.tsx | 9 +++- .../runtime/runtime-component-diff.service.ts | 47 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/designer/src/components/composition/form-metadata-rtc.service.tsx b/packages/designer/src/components/composition/form-metadata-rtc.service.tsx index bd8a9df53..6e8b9ebe5 100644 --- a/packages/designer/src/components/composition/form-metadata-rtc.service.tsx +++ b/packages/designer/src/components/composition/form-metadata-rtc.service.tsx @@ -90,7 +90,14 @@ export function useRtcFormMetadata( const schemaDiffService = new RuntimeSchemaDiffService(schemaService); const componentsDiffService = new RuntimeComponentDiffService(); const { addedFields, modifiedFields } = schemaDiffService.getChanges(previousMetadataContent.Contents.module, currentFormModule); - const { newLookupConfigs, deletedLookupConfigs, modifiedLookupConfigs } = componentsDiffService.getLookupConfigsChanges(previousMetadataContent.Contents.module.components, currentFormModule.components); + + // 统一对比 components 和 externalComponents 中的 lookup 配置 + const { newLookupConfigs, deletedLookupConfigs, modifiedLookupConfigs } = componentsDiffService.getLookupConfigsChanges( + previousMetadataContent.Contents.module.components, + currentFormModule.components, + previousMetadataContent.Contents.module.externalComponents, + currentFormModule.externalComponents + ); const voChanges = { added: addedFields, modified: modifiedFields, diff --git a/packages/designer/src/components/composition/runtime/runtime-component-diff.service.ts b/packages/designer/src/components/composition/runtime/runtime-component-diff.service.ts index 2bc32d8e4..99de6f1f6 100644 --- a/packages/designer/src/components/composition/runtime/runtime-component-diff.service.ts +++ b/packages/designer/src/components/composition/runtime/runtime-component-diff.service.ts @@ -10,6 +10,9 @@ class LookupDiffService { return this.lookupConfigs; } + /** + * 从 components 数组中查找帮助配置 + */ public findLookupConfigs(components: any[]) { if (components && components.length) { components.forEach(component => { @@ -24,7 +27,24 @@ class LookupDiffService { } this.findLookupConfigs(component['contents']); }); + } + } + /** + * 从 externalComponents 数组中查找类型为 lookup 的帮助配置 + * externalComponents 中 lookup 的数据结构为顶层对象,包含 type、dataSource、helpId 等属性 + */ + public findExternalLookupConfigs(externalComponents: any[]) { + if (externalComponents && externalComponents.length) { + externalComponents.forEach(component => { + if (component.type === 'lookup' && component.dataSource?.uri) { + const uri = component.dataSource.uri.replace('.', '/'); + const lookupId = component.helpId; + if (!this.lookupConfigs.has(uri)) { + this.lookupConfigs.set(uri, { uri, lookupId }); + } + } + }); } } @@ -75,14 +95,35 @@ class LookupDiffService { */ export class RuntimeComponentDiffService { - public getLookupConfigsChanges(previous: any[], current: any[]): any { + /** + * 获取帮助配置的变更信息(包含 components 和 externalComponents) + * @param previousComponents 之前的 components 数组 + * @param currentComponents 当前的 components 数组 + * @param previousExternalComponents 之前的 externalComponents 数组(可选) + * @param currentExternalComponents 当前的 externalComponents 数组(可选) + */ + public getLookupConfigsChanges( + previousComponents: any[], + currentComponents: any[], + previousExternalComponents?: any[], + currentExternalComponents?: any[] + ): any { + // 处理 components 中的 lookup 配置 const currentLookupDiff = new LookupDiffService(); - currentLookupDiff.findLookupConfigs(current); + currentLookupDiff.findLookupConfigs(currentComponents); + // 处理 externalComponents 中的 lookup 配置 + if (currentExternalComponents) { + currentLookupDiff.findExternalLookupConfigs(currentExternalComponents); + } const currentLookupConfigs = currentLookupDiff.getLookupConfigs(); const previousLookupDiff = new LookupDiffService(); - previousLookupDiff.findLookupConfigs(previous); + previousLookupDiff.findLookupConfigs(previousComponents); + // 处理 externalComponents 中的 lookup 配置 + if (previousExternalComponents) { + previousLookupDiff.findExternalLookupConfigs(previousExternalComponents); + } const previousLookupConfigs = previousLookupDiff.getLookupConfigs(); const newLookupConfigs: any = []; -- Gitee From 1dd34c001aa21329c4b032d5c6b379c6b607c78c Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 11:37:29 +0800 Subject: [PATCH 06/26] =?UTF-8?q?fix:=20=E6=95=B4=E7=90=86=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E7=9A=84=E6=89=A9=E5=B1=95=E8=A1=A8=E5=8D=95=E5=BC=80?= =?UTF-8?q?=E6=94=BE=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/assets/rtc/property-rule.json | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/designer/public/assets/rtc/property-rule.json b/packages/designer/public/assets/rtc/property-rule.json index b0f374525..b549f265e 100644 --- a/packages/designer/public/assets/rtc/property-rule.json +++ b/packages/designer/public/assets/rtc/property-rule.json @@ -166,6 +166,7 @@ "visible", "onSelectionChange", "fitColumns", + "fitMode", "customRowStyle", "customCellStyle", "displayType", @@ -188,7 +189,6 @@ "valign", "hAlign", "binding", - "resizable", "draggable", "width", "showTips", @@ -197,8 +197,13 @@ "decimal", "thousand", "prefix", + "suffix", "data", - "dateFormat" + "dateFormat", + "formatterEnumData", + "customFormat", + "trueText", + "falseText" ], "tree-grid": [ "id", @@ -214,6 +219,7 @@ "striped", "onSelectionChange", "fitColumns", + "fitMode", "customRowStyle", "customCellStyle" ], @@ -227,7 +233,6 @@ "valign", "hAlign", "binding", - "resizable", "draggable", "width", "showTips", @@ -236,8 +241,13 @@ "decimal", "thousand", "prefix", + "suffix", "data", - "dateFormat" + "dateFormat", + "formatterEnumData", + "customFormat", + "trueText", + "falseText" ], "response-toolbar-item": [ "id", -- Gitee From 6b94fb980c7fda2858d251a7c9b105a99582123a Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 15:39:59 +0800 Subject: [PATCH 07/26] feat(table): add table schema, types, cell factory and default grid resolver --- .../table/src/composition/td-factory.ts | 34 ++++++++++++++ .../components/table/src/composition/types.ts | 38 ++++++++++++++++ .../table/src/schema/schema-mapper.ts | 5 +++ .../table/src/schema/schema-resolver.ts | 9 ++++ .../table/src/schema/table.schema.json | 44 +++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 packages/ui-vue/components/table/src/composition/td-factory.ts create mode 100644 packages/ui-vue/components/table/src/composition/types.ts create mode 100644 packages/ui-vue/components/table/src/schema/schema-mapper.ts create mode 100644 packages/ui-vue/components/table/src/schema/schema-resolver.ts create mode 100644 packages/ui-vue/components/table/src/schema/table.schema.json diff --git a/packages/ui-vue/components/table/src/composition/td-factory.ts b/packages/ui-vue/components/table/src/composition/td-factory.ts new file mode 100644 index 000000000..9cdeb71f4 --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/td-factory.ts @@ -0,0 +1,34 @@ +import { TableCell, TableRow } from './types'; + +/** 与 Angular `new IdService().generate()` 等价的唯一 id 生成。 */ +export function generateId(): string { + return 'x' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4); +} + +/** 对齐 Angular `TableTdSchema` 的默认单元格(等价 cloneDeep(TableTdSchema) + 新 id)。 */ +export function createTableTdMetadata(): TableCell { + return { + id: generateId(), + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null + }; +} + +/** 生成默认 R×C 稠密网格(全部为独立锚点,markCol/markRow 缺省即视为 0)。 */ +export function createDefaultTableRows(rowCount: number, colCount: number): TableRow[] { + const rows: TableRow[] = []; + for (let r = 0; r < rowCount; r++) { + const columns: TableCell[] = []; + for (let c = 0; c < colCount; c++) { columns.push(createTableTdMetadata()); } + rows.push({ id: generateId(), type: 'TableRow', columns }); + } + return rows; +} diff --git a/packages/ui-vue/components/table/src/composition/types.ts b/packages/ui-vue/components/table/src/composition/types.ts new file mode 100644 index 000000000..fe2f562b0 --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/types.ts @@ -0,0 +1,38 @@ +export interface TableCellStaticText { text: string; require: boolean; } +export interface TableCellEditor { type: string | null; binding: any; id?: string; } + +export interface TableCell { + id: string; + type: string; // Angular 用 'TableTd' + tdType: 'editor' | 'staticText'; + staticText: TableCellStaticText; + editor: TableCellEditor; + colspan: number; + rowspan: number; + align: 'left' | 'center' | 'right'; + valign: 'top' | 'middle' | 'bottom'; + appearance: { class: string; style: string; }; + size?: any; + // 合并区域内偏移量(锚点=0,0);缺省即视为 0 + markRow?: number; + markCol?: number; + invisible?: boolean; +} + +export interface TableRow { + id: string; + type: string; // Angular 用 'TableRow' + columns: TableCell[]; +} + +export interface TableSchema { + id: string; + type: string; // Angular 用 'Table' + devMode: 'advanced' | 'simple'; + rows: TableRow[]; + border: { width: number; enableColor: boolean; color: string; }; + font: { size: number; enableColor: boolean; color: string; family: string; }; + size: { width?: number; height?: number } | null; + appearance: { class: string; style: string; }; + visible: boolean; +} diff --git a/packages/ui-vue/components/table/src/schema/schema-mapper.ts b/packages/ui-vue/components/table/src/schema/schema-mapper.ts new file mode 100644 index 000000000..bbc05c95a --- /dev/null +++ b/packages/ui-vue/components/table/src/schema/schema-mapper.ts @@ -0,0 +1,5 @@ +import { resolveAppearance, MapperFunction } from '@farris/ui-vue/components/dynamic-resolver'; + +export const schemaMapper = new Map([ + ['appearance', resolveAppearance] +]); diff --git a/packages/ui-vue/components/table/src/schema/schema-resolver.ts b/packages/ui-vue/components/table/src/schema/schema-resolver.ts new file mode 100644 index 000000000..2347a870d --- /dev/null +++ b/packages/ui-vue/components/table/src/schema/schema-resolver.ts @@ -0,0 +1,9 @@ +import { DynamicResolver } from '@farris/ui-vue/components/dynamic-resolver'; +import { createDefaultTableRows } from '../composition/td-factory'; + +export function schemaResolver(resolver: DynamicResolver, schema: Record, context: Record): Record { + if (!schema.rows || schema.rows.length === 0) { + schema.rows = createDefaultTableRows(2, 2); + } + return schema; +} diff --git a/packages/ui-vue/components/table/src/schema/table.schema.json b/packages/ui-vue/components/table/src/schema/table.schema.json new file mode 100644 index 000000000..b213b8b8d --- /dev/null +++ b/packages/ui-vue/components/table/src/schema/table.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://farris-design.gitee.io/table.schema.json", + "title": "table", + "description": "A Farris design-time layout table", + "type": "object", + "properties": { + "id": { "type": "string" }, + "type": { "type": "string", "default": "table" }, + "devMode": { "type": "string", "default": "advanced" }, + "appearance": { + "type": "object", + "properties": { "class": { "type": "string" }, "style": { "type": "string" } }, + "default": {} + }, + "border": { + "type": "object", + "properties": { + "width": { "type": "number" }, + "enableColor": { "type": "boolean" }, + "color": { "type": "string" } + }, + "default": { "width": 1, "enableColor": false, "color": "#d4d6d9" } + }, + "font": { + "type": "object", + "properties": { + "size": { "type": "number" }, + "enableColor": { "type": "boolean" }, + "color": { "type": "string" }, + "family": { "type": "string" } + }, + "default": { "size": 12, "enableColor": false, "color": "#333", "family": "" } + }, + "size": { + "type": "object", + "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, + "default": null + }, + "rows": { "type": "array", "default": [] }, + "visible": { "type": "boolean", "default": true } + }, + "required": ["id", "type", "rows"] +} -- Gitee From 169bb428077618f975a55895711d41375e5c39a2 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 15:52:41 +0800 Subject: [PATCH 08/26] feat(table): port Angular TableTdWidgetHelper geometry (offset model, TDD) --- .../table/src/composition/td-helper.spec.ts | 101 ++++++++++++++++++ .../table/src/composition/td-helper.ts | 98 +++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 packages/ui-vue/components/table/src/composition/td-helper.spec.ts create mode 100644 packages/ui-vue/components/table/src/composition/td-helper.ts diff --git a/packages/ui-vue/components/table/src/composition/td-helper.spec.ts b/packages/ui-vue/components/table/src/composition/td-helper.spec.ts new file mode 100644 index 000000000..e64be83d9 --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/td-helper.spec.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TableTdWidgetHelper } from './td-helper'; +import { createDefaultTableRows, createTableTdMetadata } from './td-factory'; + +const helper = new TableTdWidgetHelper(); + +describe('TableTdWidgetHelper(搬运自 Angular)', () => { + it('createTableTdMetadata 生成唯一 id 与默认形状', () => { + const a = createTableTdMetadata(); + const b = createTableTdMetadata(); + expect(a.id).not.toBe(b.id); + expect(a.colspan).toBe(1); + expect(a.rowspan).toBe(1); + expect(a.tdType).toBe('editor'); + }); + + it('insertColumns 在非合并区插入:每行加一个可见新格', () => { + const rows = createDefaultTableRows(2, 2); + helper.insertColumns(rows, 1); + expect(rows[0].columns.length).toBe(3); + expect(rows[1].columns.length).toBe(3); + expect(!!rows[0].columns[1].invisible).toBe(false); + }); + + it('markTableItem 写入区域内相对偏移', () => { + const rows = createDefaultTableRows(1, 2); + rows[0].columns[0].colspan = 2; + rows[0].columns[1].invisible = true; + helper.markTableItem(0, 0, rows); + expect(rows[0].columns[0].markCol).toBe(0); + expect(rows[0].columns[0].markRow).toBe(0); + expect(rows[0].columns[1].markCol).toBe(1); + expect(rows[0].columns[1].markRow).toBe(0); + }); + + it('findPrevColIndex 向左走到区域锚点列(markCol 为假处停)', () => { + const rows = createDefaultTableRows(1, 2); + rows[0].columns[0].colspan = 2; + rows[0].columns[1].invisible = true; + helper.markTableItem(0, 0, rows); + expect(helper.findPrevColIndex(0, 1, rows)).toBe(0); + expect(helper.findPrevColIndex(0, 0, rows)).toBe(0); + }); + + it('findNextColIndex 向右越过覆盖格到区域右边界', () => { + const rows = createDefaultTableRows(1, 3); + rows[0].columns[0].colspan = 2; + rows[0].columns[1].invisible = true; + helper.markTableItem(0, 0, rows); + expect(helper.findNextColIndex(0, 1, rows)).toBe(2); + }); + + it('insertColumns 命中合并区内部:锚点 colspan++ 并插入 invisible 槽 + 重标偏移', () => { + const rows = createDefaultTableRows(1, 2); + rows[0].columns[0].colspan = 2; + rows[0].columns[1].invisible = true; + helper.markTableItem(0, 0, rows); + helper.insertColumns(rows, 1); + expect(rows[0].columns.length).toBe(3); + expect(rows[0].columns[0].colspan).toBe(3); + expect(!!rows[0].columns[1].invisible).toBe(true); + expect(!!rows[0].columns[2].invisible).toBe(true); + expect(rows[0].columns[0].markCol).toBe(0); + expect(rows[0].columns[1].markCol).toBe(1); + expect(rows[0].columns[2].markCol).toBe(2); + }); + + it('insertRow 在非纵向合并区插入:加入整行可见新格', () => { + const rows = createDefaultTableRows(2, 3); + helper.insertRow(1, rows); + expect(rows.length).toBe(3); + expect(rows[1].columns.length).toBe(3); + rows[1].columns.forEach(c => expect(!!c.invisible).toBe(false)); + }); + + it('insertRow 命中纵向合并区:锚点 rowspan++ 并插入 invisible 槽', () => { + const rows = createDefaultTableRows(2, 1); + rows[0].columns[0].rowspan = 2; + rows[1].columns[0].invisible = true; + helper.markTableItem(0, 0, rows); + helper.insertRow(1, rows); + expect(rows.length).toBe(3); + expect(rows[0].columns[0].rowspan).toBe(3); + expect(!!rows[1].columns[0].invisible).toBe(true); + expect(!!rows[2].columns[0].invisible).toBe(true); + }); +}); diff --git a/packages/ui-vue/components/table/src/composition/td-helper.ts b/packages/ui-vue/components/table/src/composition/td-helper.ts new file mode 100644 index 000000000..a6acbe9e8 --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/td-helper.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TableRow } from './types'; +import { generateId, createTableTdMetadata } from './td-factory'; + +/** + * 搬运自 Angular `TableTdWidgetHelper` + * (table/component/td-manager/td-helper.ts)。 + * 算法未改:markCol/markRow 为合并区域内偏移(锚点=0); + * find* 递归在 `!markCol`/`!markRow`(区域边界)处停。 + * 仅将 `new IdService().generate()` → `generateId()`, + * `cloneDeep(TableTdSchema)` → `createTableTdMetadata()`。 + */ +export class TableTdWidgetHelper { + + private getTableTdMetadata(): any { + return createTableTdMetadata(); + } + + insertColumns(rows: any[], targetColumnIndex: number) { + for (let e = 0; e < rows.length; e++) { + const n = this.findPrevColIndex(e, targetColumnIndex, rows); + if (n !== targetColumnIndex && rows[e].columns[targetColumnIndex].invisible) { + const tdMetadata = this.getTableTdMetadata(); + tdMetadata.invisible = true; + rows[e].columns.splice(targetColumnIndex, 0, tdMetadata); + const i = rows[e].columns[n].colspan; + rows[e].columns[n].colspan = i + 1; + for (let o = 0; o < i + 1; o++) { + rows[e].columns[n + o].markCol = o; + } + } else { + const tdMetadata = this.getTableTdMetadata(); + rows[e].columns.splice(targetColumnIndex, 0, tdMetadata); + } + } + } + + findPrevColIndex(t: number, e: number, rows: any[]): number { + return e >= rows[t].columns.length || !rows[t].columns[e].markCol ? e : this.findPrevColIndex(t, e - 1, rows); + } + findNextColIndex(t: number, e: number, rows: any[]): number { + return e >= rows[t].columns.length || !rows[t].columns[e].markCol ? e : this.findNextColIndex(t, e + 1, rows); + } + findPrevRowIndex(t: number, e: number, rows: any[]): number { + return t >= rows.length || !rows[t].columns[e].markRow ? t : this.findPrevRowIndex(t - 1, e, rows); + } + findNextRowIndex(t: number, e: number, rows: any[]): number { + return t >= rows.length || !rows[t].columns[e].markRow ? t : this.findNextRowIndex(t + 1, e, rows); + } + + insertRow(t: number, rows: any[]) { + const n = []; + for (let i = 0; i < rows[0].columns.length; i++) { + const o = this.findPrevRowIndex(t, i, rows); + const tdMetadata = this.getTableTdMetadata(); + if (o !== t && rows[t].columns[i].invisible) { + tdMetadata.invisible = true; + n.push(tdMetadata); + rows[o].columns[i].rowspan = rows[o].columns[i].rowspan + 1; + } else { + n.push(tdMetadata); + } + } + rows.splice(t, 0, { id: generateId(), type: 'TableRow', columns: n }); + + for (let e = 0; e < rows[t].columns.length; e++) { + if (rows[t].columns[e].invisible) { + const i = this.findPrevRowIndex(t - 1, e, rows); + if (!rows[i].columns[e].invisible) { + this.markTableItem(i, e, rows); + } + } + } + } + + markTableItem(t: number, e: number, rows: any[]) { + for (let n = rows[t].columns[e].colspan, i = rows[t].columns[e].rowspan, o = 0; o < i; o++) { + for (let r = 0; r < n; r++) { + rows[t + o].columns[e + r].markCol = r; + rows[t + o].columns[e + r].markRow = o; + } + } + } +} -- Gitee From 4edab1f22e394ed51bc33759b57df35c85907058 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 15:56:08 +0800 Subject: [PATCH 09/26] fix(table): remove unused TableRow import from td-helper --- packages/ui-vue/components/table/src/composition/td-helper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui-vue/components/table/src/composition/td-helper.ts b/packages/ui-vue/components/table/src/composition/td-helper.ts index a6acbe9e8..c1d204c0f 100644 --- a/packages/ui-vue/components/table/src/composition/td-helper.ts +++ b/packages/ui-vue/components/table/src/composition/td-helper.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TableRow } from './types'; import { generateId, createTableTdMetadata } from './td-factory'; /** -- Gitee From e253a03f8140d94b43e3853ec110f8b8e373a115 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 16:11:41 +0800 Subject: [PATCH 10/26] feat(table): add runtime table (native table + FDynamicFormGroup editor cells) --- .../src/composition/use-table-cell-render.tsx | 66 +++++++++++++++++++ .../components/table/src/table.component.tsx | 46 +++++++++++++ .../components/table/src/table.props.ts | 25 +++++++ 3 files changed, 137 insertions(+) create mode 100644 packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx create mode 100644 packages/ui-vue/components/table/src/table.component.tsx create mode 100644 packages/ui-vue/components/table/src/table.props.ts diff --git a/packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx b/packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx new file mode 100644 index 000000000..09ce7b034 --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx @@ -0,0 +1,66 @@ +import { FDynamicFormGroup } from '@farris/ui-vue/components/dynamic-form'; +import { TableCell } from './types'; + +export function getTableStyle(props: any): Record { + const style: Record = { 'box-shadow': '0px 0px 0px 1px #eaecf3' }; + if (props.size?.height) { style.height = `${props.size.height}px`; } + return style; +} + +export function getCellStyle(props: any, cell: TableCell): Record { + const b = props.border || {}; + const f = props.font || {}; + const style: Record = { + border: `${b.width ?? 1}px solid ${b.enableColor ? b.color : '#eaecf3'}`, + 'text-align': cell.align || 'left', + 'vertical-align': cell.valign || 'middle' + }; + if (f.size) { style['font-size'] = `${f.size}px`; } + if (f.enableColor && f.color) { style.color = f.color; } + if (f.family) { style['font-family'] = f.family; } + if (cell.appearance?.style) { + cell.appearance.style.split(';').forEach((s: string) => { + const [k, v] = s.split(':'); + if (k && v) { style[k.trim()] = v.trim(); } + }); + } + return style; +} + +/** label 单元格:自绘标签 DOM(对齐 Angular 类名)。 */ +export function renderLabelCell(cell: TableCell) { + return ( +
+ +
+ ); +} + +/** editor 单元格:复用 FDynamicFormGroup(showLabel=false,v-model 直连记录字段)。 */ +export function renderEditorCell( + cell: TableCell, + opts: { design?: boolean; data?: Record; onChange?: (field: string, value: any) => void } +) { + const field = cell.editor?.binding?.field; + if (!field) { + return 未绑定字段; + } + if (opts.design) { + return ; + } + const data = opts.data || {}; + return ( + opts.onChange && opts.onChange(field, value)} /> + ); +} diff --git a/packages/ui-vue/components/table/src/table.component.tsx b/packages/ui-vue/components/table/src/table.component.tsx new file mode 100644 index 000000000..26d44abc1 --- /dev/null +++ b/packages/ui-vue/components/table/src/table.component.tsx @@ -0,0 +1,46 @@ +import { SetupContext, defineComponent, ref, watch } from 'vue'; +import { TablePropsType, tableProps } from './table.props'; +import { TableRow, TableCell } from './composition/types'; +import { getTableStyle, getCellStyle, renderLabelCell, renderEditorCell } from './composition/use-table-cell-render'; + +export default defineComponent({ + name: 'FTable', + props: tableProps, + emits: ['update:modelValue', 'change'], + setup(props: TablePropsType, context: SetupContext) { + const data = ref>(props.modelValue || {}); + watch(() => props.modelValue, (v) => { data.value = v || {}; }); + + function onCellChange(field: string, value: any) { + context.emit('update:modelValue', data.value); + context.emit('change', { field, value, modelValue: data.value }); + } + + function renderCell(cell: TableCell) { + const isLabel = cell.tdType === 'staticText'; + return ( + + {isLabel ? renderLabelCell(cell) : renderEditorCell(cell, { data: data.value, onChange: onCellChange })} + + ); + } + + return () => { + if (!props.visible) { return null; } + const rows = (props.rows as TableRow[]) || []; + return ( + + + {rows.map(row => ( + + {row.columns.filter(c => !c.invisible).map(cell => renderCell(cell))} + + ))} + +
+ ); + }; + } +}); diff --git a/packages/ui-vue/components/table/src/table.props.ts b/packages/ui-vue/components/table/src/table.props.ts new file mode 100644 index 000000000..7735fd31d --- /dev/null +++ b/packages/ui-vue/components/table/src/table.props.ts @@ -0,0 +1,25 @@ +import { ExtractPropTypes, PropType } from 'vue'; +import { createPropsResolver, getPropsResolverGenerator } from '@farris/ui-vue/components/dynamic-resolver'; +import { schemaMapper } from './schema/schema-mapper'; +import tableSchema from './schema/table.schema.json'; +import { schemaResolver } from './schema/schema-resolver'; +import { TableRow } from './composition/types'; + +export const tableProps = { + id: { type: String, default: '' }, + componentId: { type: String, default: '' }, + customClass: { type: String, default: '' }, + customStyle: { type: String, default: '' }, + devMode: { type: String, default: 'advanced' }, + rows: { type: Array as PropType, default: () => [] }, + border: { type: Object as PropType, default: () => ({ width: 1, enableColor: false, color: '#d4d6d9' }) }, + font: { type: Object as PropType, default: () => ({ size: 12, enableColor: false, color: '#333', family: '' }) }, + size: { type: Object as PropType, default: null }, + visible: { type: Boolean, default: true }, + modelValue: { type: Object as PropType>, default: () => ({}) } +} as Record; + +export type TablePropsType = ExtractPropTypes; + +export const propsResolver = createPropsResolver(tableProps, tableSchema, schemaMapper, schemaResolver); +export const propsResolverGenerator = getPropsResolverGenerator(tableProps, tableSchema, schemaMapper, schemaResolver); -- Gitee From 529cc5fa9af799b7bd37e6a5c984c3b2337cfb47 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 16:17:49 +0800 Subject: [PATCH 11/26] feat(table): add reusable context-menu component for cells --- .../src/components/context-menu.component.tsx | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 packages/ui-vue/components/table/src/components/context-menu.component.tsx diff --git a/packages/ui-vue/components/table/src/components/context-menu.component.tsx b/packages/ui-vue/components/table/src/components/context-menu.component.tsx new file mode 100644 index 000000000..53ce8ee81 --- /dev/null +++ b/packages/ui-vue/components/table/src/components/context-menu.component.tsx @@ -0,0 +1,168 @@ +import { defineComponent, ref, nextTick, onMounted, onBeforeUnmount } from "vue"; + +export interface ContextMenuOption { + id: string; + label: string; + icon?: string; + disabled?: boolean; + danger?: boolean; + onClick: () => void; +} + +export interface ContextMenuProps { + visible: boolean; + position: { x: number; y: number }; + options: ContextMenuOption[]; + onClose: () => void; +} + +export const contextMenuProps = { + visible: { type: Boolean, default: false }, + position: { + type: Object as () => { x: number; y: number }, + default: () => ({ x: 0, y: 0 }) + }, + options: { + type: Array as () => ContextMenuOption[], + default: () => [] + }, + onClose: { type: Function, required: true } +}; + +export default defineComponent({ + name: 'FTableContextMenu', + props: contextMenuProps, + setup(props: ContextMenuProps) { + const menuRef = ref(); + const adjustedPosition = ref({ x: 0, y: 0 }); + const hoveredId = ref(null); + + const adjustPosition = () => { + if (!menuRef.value || !props.visible) { + return; + } + + nextTick(() => { + const menu = menuRef.value!; + const rect = menu.getBoundingClientRect(); + const viewport = { + width: window.innerWidth, + height: window.innerHeight + }; + + let { x, y } = props.position; + + if (x + rect.width > viewport.width) { + x = viewport.width - rect.width - 10; + } + + if (y + rect.height > viewport.height) { + y = viewport.height - rect.height - 10; + } + + x = Math.max(10, x); + y = Math.max(10, y); + + adjustedPosition.value = { x, y }; + }); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.value && !menuRef.value.contains(event.target as Node)) { + props.onClose(); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + props.onClose(); + } + }; + + const handleOptionClick = (option: ContextMenuOption) => { + if (!option.disabled) { + option.onClick(); + props.onClose(); + } + }; + + onMounted(() => { + document.addEventListener('click', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + }); + + onBeforeUnmount(() => { + document.removeEventListener('click', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }); + + (() => { + if (props.visible) { + adjustedPosition.value = props.position; + adjustPosition(); + } + })(); + + return () => { + if (!props.visible) { + return null; + } + + return ( +
+
e.stopPropagation()} + > + {props.options.map((option) => ( +
handleOptionClick(option)} + onMouseenter={() => { hoveredId.value = option.id; }} + onMouseleave={() => { hoveredId.value = null; }} + > + {option.icon && ( + {option.icon} + )} + {option.label} +
+ ))} +
+
+ ); + }; + } +}); -- Gitee From 90c57d22111ae65d5dbed6eadad4722ff5fdaca0 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 16:26:49 +0800 Subject: [PATCH 12/26] feat(table): port Angular TableTdManager menu operations (services mapped, TDD) --- .../table/src/composition/td-manager.spec.ts | 102 +++++ .../table/src/composition/td-manager.ts | 364 ++++++++++++++++++ 2 files changed, 466 insertions(+) create mode 100644 packages/ui-vue/components/table/src/composition/td-manager.spec.ts create mode 100644 packages/ui-vue/components/table/src/composition/td-manager.ts diff --git a/packages/ui-vue/components/table/src/composition/td-manager.spec.ts b/packages/ui-vue/components/table/src/composition/td-manager.spec.ts new file mode 100644 index 000000000..c3c85cc32 --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/td-manager.spec.ts @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TableTdManager } from './td-manager'; +import { createDefaultTableRows } from './td-factory'; + +function makeManager(rows: any[]) { + const redraw = jest.fn(); + const canvas = jest.fn(); + const services = { + designViewModelUtils: { getDgViewModel: () => ({ removeField: jest.fn() }) }, + formSchemaUtils: { getExpressions: () => [], getComponents: () => [], getControlsInCmpWidthBinding: () => [] }, + messageBox: { question: (_m: string, _d: string, ok: () => void) => ok() } + }; + const mgr = new TableTdManager({ id: 't', rows }, services, redraw, canvas); + return { mgr, redraw, canvas }; +} + +describe('TableTdManager (ported from Angular)', () => { + it('mergeColumnToRight merges (0,0) with right neighbor', () => { + const rows = createDefaultTableRows(1, 3); + const { mgr, canvas } = makeManager(rows); + mgr.setTarget(0, 0); + mgr.mergeColumnToRight(); + expect(rows[0].columns[0].colspan).toBe(2); + expect(!!rows[0].columns[1].invisible).toBe(true); + expect(canvas).toHaveBeenCalled(); + }); + + it('showMergeColumnToRight returns false for last column', () => { + const rows = createDefaultTableRows(1, 2); + const { mgr } = makeManager(rows); + mgr.setTarget(0, 1); + expect(mgr.showMergeColumnToRight()).toBe(false); + }); + + it('splitColumns restores merged anchor', () => { + const rows = createDefaultTableRows(1, 3); + const { mgr } = makeManager(rows); + mgr.setTarget(0, 0); + mgr.mergeColumnToRight(); + mgr.setTarget(0, 0); + mgr.splitColumns(); + expect(rows[0].columns[0].colspan).toBe(1); + expect(!!rows[0].columns[1].invisible).toBe(false); + }); + + it('mergeRowToBottom merges (0,0) with bottom neighbor', () => { + const rows = createDefaultTableRows(3, 1); + const { mgr } = makeManager(rows); + mgr.setTarget(0, 0); + mgr.mergeRowToBottom(); + expect(rows[0].columns[0].rowspan).toBe(2); + expect(!!rows[1].columns[0].invisible).toBe(true); + }); + + it('insertColumnToRight adds a column', () => { + const rows = createDefaultTableRows(2, 2); + const { mgr } = makeManager(rows); + mgr.setTarget(0, 0); + mgr.insertColumnToRight(); + expect(rows[0].columns.length).toBe(3); + expect(rows[1].columns.length).toBe(3); + }); + + it('removeColumn removes a normal column', () => { + const rows = createDefaultTableRows(2, 3); + const { mgr } = makeManager(rows); + mgr.setTarget(0, 1); + mgr.removeColumn(); + expect(rows[0].columns.length).toBe(2); + }); + + it('showRemoveColumn returns false for colspan>1 anchor', () => { + const rows = createDefaultTableRows(1, 3); + const { mgr } = makeManager(rows); + mgr.setTarget(0, 0); + mgr.mergeColumnToRight(); + mgr.setTarget(0, 0); + expect(mgr.showRemoveColumn()).toBe(false); + }); + + it('removeRow removes a normal row', () => { + const rows = createDefaultTableRows(3, 2); + const { mgr } = makeManager(rows); + mgr.setTarget(1, 0); + mgr.removeRow(); + expect(rows.length).toBe(2); + }); +}); diff --git a/packages/ui-vue/components/table/src/composition/td-manager.ts b/packages/ui-vue/components/table/src/composition/td-manager.ts new file mode 100644 index 000000000..e505620bc --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/td-manager.ts @@ -0,0 +1,364 @@ +import { TableRow } from './types'; +import { TableTdWidgetHelper } from './td-helper'; +import { ContextMenuOption } from '../components/context-menu.component'; + +/** Services needed by TableTdManager (ported from Angular DI). */ +export interface TableTdManagerServices { + /** Angular DomService → designer inject('useFormSchema'): getExpressions/getComponents/getControlsInCmpWidthBinding */ + formSchemaUtils: any; + /** Angular DesignViewModelService → inject('designViewModelUtils'): getDgViewModel(id).removeField([...]) */ + designViewModelUtils: any; + /** Angular MessagerService → default FMessageBoxService; injectable stub for testing */ + messageBox?: { question: (message: string, detail: string, ok: () => void, cancel?: () => void) => void }; +} + +export interface TableTdManagerComponent { id: string; rows: TableRow[]; } + +export interface MenuItem { id: string; title: string; disable?: boolean; handle?: () => void; } +export type MenuConfig = (MenuItem | '-')[]; + +/** + * Ported from Angular `TableTdManager` (table/component/td-manager/td-manager.ts). + * Merge/split/insert/delete logic unchanged; service mappings updated for Vue. + */ +export class TableTdManager { + tableTdHelper = new TableTdWidgetHelper(); + selectedColumnIndex = 0; + selectedRowIndex = 0; + targetColumn: any; + targetRow: any; + viewModelId = ''; + menuConfig: MenuConfig = []; + + constructor( + public component: TableTdManagerComponent, + private services: TableTdManagerServices, + /** redrawTable.next replacement; payload may contain { clearPropertyPanel } */ + private redraw: (payload?: any) => void, + /** refreshSchamaTree.next replacement */ + private emitCanvasChanged: () => void + ) { } + + /** Called by design component on right-click / widget-panel icon click to set the current target cell. */ + setTarget(rowIndex: number, colIndex: number) { + this.selectedRowIndex = rowIndex; + this.selectedColumnIndex = colIndex; + this.targetRow = this.component.rows[rowIndex]; + this.targetColumn = this.component.rows[rowIndex].columns[colIndex]; + } + + getMenuConfig(): MenuConfig { + this.menuConfig = [ + { id: 'insertColumnToLeft', title: '左插入列', handle: () => this.insertColumnToLeft() }, + { id: 'insertColumnToRight', title: '右插入列', handle: () => this.insertColumnToRight() }, + { id: 'insertRowToTop', title: '上插入行', handle: () => this.insertRowToTop() }, + { id: 'insertRowToBottom', title: '下插入行', handle: () => this.insertRowToBottom() }, + '-', + { id: 'mergeColumnToRight', title: '向右合并', handle: () => this.mergeColumnToRight(), disable: !this.showMergeColumnToRight() }, + { id: 'mergeRowToBottom', title: '向下合并', handle: () => this.mergeRowToBottom(), disable: !this.showMergeRowToBottom() }, + '-', + { id: 'splitColumns', title: '拆分成列', handle: () => this.splitColumns(), disable: !this.showSplitColumns() }, + { id: 'splitRows', title: '拆分成行', handle: () => this.splitRows(), disable: !this.showSplitRows() }, + '-', + { id: 'removeColumn', title: '删除当前列', handle: () => this.removeColumn(), disable: !this.showRemoveColumn() }, + { id: 'removeRow', title: '删除当前行', handle: () => this.removeRow(), disable: !this.showRemoveRow() } + ]; + return this.menuConfig; + } + + private question(msg: string, ok: () => void) { + if (this.services.messageBox) { + this.services.messageBox.question(msg, '', ok); + } else { + // Lazy-require to avoid module-resolution cascade in unit tests + const FMessageBoxService = require('@farris/ui-vue/components/message-box').FMessageBoxService; + FMessageBoxService.question(msg, '', ok); + } + } + + // ===== Insert operations ===== + insertColumnToLeft() { + this.tableTdHelper.insertColumns(this.component.rows, this.selectedColumnIndex); + this.selectedColumnIndex++; + this.redraw(); + } + insertColumnToRight() { + const targetColumnIndex = this.targetColumn.colspan > 1 + ? this.tableTdHelper.findNextColIndex(this.selectedRowIndex, this.selectedColumnIndex + 1, this.component.rows) + : this.selectedColumnIndex + 1; + this.tableTdHelper.insertColumns(this.component.rows, targetColumnIndex); + this.redraw(); + } + insertRowToTop() { + this.tableTdHelper.insertRow(this.selectedRowIndex, this.component.rows); + this.selectedRowIndex++; + this.redraw(); + } + insertRowToBottom() { + const targetRowIndex = this.targetColumn.rowspan > 1 + ? this.tableTdHelper.findNextRowIndex(this.selectedRowIndex + 1, this.selectedColumnIndex, this.component.rows) + : this.selectedRowIndex + 1; + this.tableTdHelper.insertRow(targetRowIndex, this.component.rows); + this.redraw(); + } + + // ===== Merge right ===== + showMergeColumnToRight(): boolean { + const rowspan = this.targetColumn.rowspan; + const colspan = this.targetColumn.colspan; + if (this.selectedColumnIndex + colspan < this.targetRow.columns.length) { + const n = this.targetRow.columns[colspan + this.selectedColumnIndex]; + if (!n.invisible && rowspan === n.rowspan) { return true; } + } + return false; + } + mergeColumnToRight() { + const targetColspan = this.targetColumn.colspan; + const o = this.selectedColumnIndex + targetColspan; + const rightColumn = this.component.rows[this.selectedRowIndex].columns[o]; + if (rightColumn.editor && rightColumn.editor.type) { + this.question('合并后将重置右侧单元格的编辑器,确定合并?', () => { + this.removeDesignViewModelFieldInColumns([rightColumn]); + this.confirmToMergeColumnToRight(rightColumn, targetColspan); + }); + } else { + this.confirmToMergeColumnToRight(rightColumn, targetColspan); + } + } + confirmToMergeColumnToRight(rightColumn: any, targetColspan: number) { + const r = rightColumn.colspan; + this.targetColumn.colspan = targetColspan + r; + rightColumn.invisible = true; + this.resetTd(rightColumn); + this.tableTdHelper.markTableItem(this.selectedRowIndex, this.selectedColumnIndex, this.component.rows); + this.redraw(); + this.emitCanvasChanged(); + } + + // ===== Merge down ===== + showMergeRowToBottom(): boolean { + const t = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan; + const e = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan; + if (t + this.selectedRowIndex < this.component.rows.length) { + const n = this.component.rows[t + this.selectedRowIndex].columns[this.selectedColumnIndex]; + if (!n.invisible && e === n.colspan) { return true; } + } + return false; + } + mergeRowToBottom() { + const targetRowspan = this.targetColumn.rowspan; + const o = this.selectedRowIndex + targetRowspan; + const bottomColumn = this.component.rows[o].columns[this.selectedColumnIndex]; + if (bottomColumn.editor && bottomColumn.editor.type) { + this.question('合并后将重置下方单元格的编辑器,确定合并?', () => { + this.removeDesignViewModelFieldInColumns([bottomColumn]); + this.confirmToMergeRowToBottom(bottomColumn, targetRowspan); + }); + } else { + this.confirmToMergeRowToBottom(bottomColumn, targetRowspan); + } + } + confirmToMergeRowToBottom(bottomColumn: any, targetRowspan: number) { + this.targetColumn.rowspan = targetRowspan + bottomColumn.rowspan; + bottomColumn.invisible = true; + this.resetTd(bottomColumn); + this.tableTdHelper.markTableItem(this.selectedRowIndex, this.selectedColumnIndex, this.component.rows); + this.redraw(); + this.emitCanvasChanged(); + } + + private resetTd(column: any) { + column.staticText = { text: '', require: false }; + column.editor = { type: null, binding: null }; + } + + // ===== Field cleanup (removeField is REQUIRED; expression removal from data-grid pattern) ===== + removeDesignViewModelFieldInColumns(columns: any[]) { + const dgViewModel = this.services.designViewModelUtils.getDgViewModel(this.viewModelId); + columns.forEach(co => { + if (co.editor && co.editor.binding && co.editor.binding.field) { + const fieldId = co.editor.binding.field; + dgViewModel.removeField([fieldId]); + this.removeExpression(co, fieldId); + // BusinessControlResolver / ProcessControlledRuleUtil: not applicable in Vue → skip + } + }); + } + + /** + * References data-grid `grid-field-editor.component.tsx` removeExpression pattern: + * uses formSchemaUtils.getExpressions() keyed by .target, deduplicates via checkExpressionOccupiedRepeated. + */ + private removeExpression(cell: any, fieldId: string) { + const dom = this.services.formSchemaUtils; + const expressions = dom.getExpressions ? dom.getExpressions() : []; + if (!expressions || !expressions.length) { return; } + const expIndex = expressions.findIndex((e: any) => e.target === fieldId); + if (expIndex < 0) { return; } + (expressions[expIndex].rules || []).forEach((rule: any) => { + const propertyInSchema = rule.type === 'visible' ? cell[rule.type] : cell.editor?.[rule.type]; + const configured = ['visible', 'readonly', 'required'].includes(rule.type) + ? (propertyInSchema && propertyInSchema.type === 'Expression' && propertyInSchema.expressionId === rule.id) + : true; + if (configured && !this.checkExpressionOccupiedRepeated(fieldId, rule, cell.id)) { + rule.needDelete = true; + } + }); + expressions[expIndex].rules = (expressions[expIndex].rules || []).filter((rule: any) => !rule.needDelete); + } + + /** References data-grid: checks if expression is reused by other controls before deletion. */ + private checkExpressionOccupiedRepeated(bindingFieldId: string, rule: any, cellId: string): boolean { + const dom = this.services.formSchemaUtils; + if (!dom.getComponents || !dom.getControlsInCmpWidthBinding) { return false; } + const target = dom.getComponents().find((component: any) => { + const controls = dom.getControlsInCmpWidthBinding(component.viewModel, bindingFieldId) + .filter((control: any) => { + if (control.id === cellId) { return false; } + const propertyInSchema = rule.type === 'visible' ? control[rule.type] : control.editor?.[rule.type]; + return propertyInSchema && propertyInSchema.type === 'Expression' && propertyInSchema.expressionId === rule.id; + }); + return controls.length > 0; + }); + return !!target; + } + + // ===== Split operations ===== + showSplitColumns(): boolean { + return this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan > 1; + } + splitColumns() { + const colspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan; + const rowspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan; + for (let i = 0; i < colspan; i++) { + this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex + i].invisible = false; + this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex + i].colspan = 1; + this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex + i].rowspan = rowspan; + this.tableTdHelper.markTableItem(this.selectedRowIndex, this.selectedColumnIndex + i, this.component.rows); + } + this.redraw(); + } + showSplitRows(): boolean { + return this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan > 1; + } + splitRows() { + const colspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan; + const rowspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan; + for (let i = 0; i < rowspan; i++) { + this.component.rows[this.selectedRowIndex + i].columns[this.selectedColumnIndex].invisible = false; + this.component.rows[this.selectedRowIndex + i].columns[this.selectedColumnIndex].rowspan = 1; + this.component.rows[this.selectedRowIndex + i].columns[this.selectedColumnIndex].colspan = colspan; + this.tableTdHelper.markTableItem(this.selectedRowIndex, this.selectedColumnIndex + i, this.component.rows); + } + this.redraw(); + } + + // ===== Remove column ===== + showRemoveColumn(): boolean { + const rows = this.component.rows; + if (rows[this.selectedRowIndex].columns.length > 1) { + const t = rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan; + if (t === 1) { return true; } + } + return false; + } + removeColumn() { + if (this.checkIfHasEditorInColumn()) { + this.question('确定移除当前列所有的编辑器配置?', () => this.confirmRemoveColumn()); + } else { + this.confirmRemoveColumn(); + } + } + confirmRemoveColumn() { + const rows = this.component.rows; + const deletedColumns: any[] = []; + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + const colspan = rows[rowIndex].columns[this.selectedColumnIndex].colspan; + const rowspan = rows[rowIndex].columns[this.selectedColumnIndex].rowspan; + if (rows[rowIndex].columns[this.selectedColumnIndex].invisible) { + const r = this.tableTdHelper.findPrevColIndex(rowIndex, this.selectedColumnIndex, rows); + if (r !== this.selectedColumnIndex && rows[rowIndex].columns[r].colspan > 1) { + rows[rowIndex].columns[r].colspan = rows[rowIndex].columns[r].colspan - 1; + } + } else { + if (colspan > 1) { + rows[rowIndex].columns[this.selectedColumnIndex + 1].invisible = false; + rows[rowIndex].columns[this.selectedColumnIndex + 1].colspan = colspan - 1; + rows[rowIndex].columns[this.selectedColumnIndex + 1].rowspan = rowspan; + this.tableTdHelper.markTableItem(rowIndex, this.selectedColumnIndex + 1, rows); + } + } + deletedColumns.push(rows[rowIndex].columns[this.selectedColumnIndex]); + rows[rowIndex].columns.splice(this.selectedColumnIndex, 1); + } + this.removeDesignViewModelFieldInColumns(deletedColumns); + this.redraw({ clearPropertyPanel: true }); + this.emitCanvasChanged(); + } + checkIfHasEditorInColumn(): boolean { + for (const row of this.component.rows) { + const column = row.columns[this.selectedColumnIndex]; + if (column && column.tdType === 'editor' && column.editor && column.editor.binding) { return true; } + } + return false; + } + + // ===== Remove row ===== + showRemoveRow(): boolean { + const rows = this.component.rows; + if (rows.length > 1) { + const t = rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan; + if (t === 1) { return true; } + } + return false; + } + removeRow() { + if (this.checkIfHasEditorInRow()) { + this.question('确定移除当前行所有的编辑器配置?', () => this.confirmRemoveRow()); + } else { + this.confirmRemoveRow(); + } + } + checkIfHasEditorInRow(): boolean { + for (const column of this.component.rows[this.selectedRowIndex].columns) { + if (column && column.tdType === 'editor' && column.editor && column.editor.binding) { return true; } + } + return false; + } + confirmRemoveRow() { + const rows = this.component.rows; + for (let e = 0; e < rows[this.selectedRowIndex].columns.length; e++) { + const n = rows[this.selectedRowIndex].columns[e].rowspan; + const o = rows[this.selectedRowIndex].columns[e].colspan; + if (rows[this.selectedRowIndex].columns[e].invisible) { + const r = this.tableTdHelper.findPrevRowIndex(this.selectedRowIndex, e, rows); + if (r !== this.selectedRowIndex && rows[r].columns[e].rowspan > 1) { + rows[r].columns[e].rowspan = rows[r].columns[e].rowspan - 1; + } + } else { + if (n > 1) { + rows[this.selectedRowIndex + 1].columns[e].invisible = false; + rows[this.selectedRowIndex + 1].columns[e].rowspan = n - 1; + rows[this.selectedRowIndex + 1].columns[e].colspan = o; + this.tableTdHelper.markTableItem(this.selectedRowIndex + 1, e, rows); + } + } + } + this.removeDesignViewModelFieldInColumns(rows[this.selectedRowIndex].columns); + rows.splice(this.selectedRowIndex, 1); + this.redraw({ clearPropertyPanel: true }); + this.emitCanvasChanged(); + } +} + +/** Maps the ported menuConfig to FTableContextMenu options (filters out '-' separators). */ +export function menuConfigToOptions(menu: MenuConfig, onAfter: () => void): ContextMenuOption[] { + return menu + .filter((m): m is MenuItem => m !== '-') + .map(item => ({ + id: item.id, + label: item.title, + disabled: !!item.disable, + onClick: () => { if (item.handle) { item.handle(); } onAfter(); } + })); +} -- Gitee From 5628c9c44b139c241fe3edead212480fd44ead11 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 16:32:47 +0800 Subject: [PATCH 13/26] fix(table): use static FMessageBoxService import (jest.mock in spec) instead of runtime require --- .../table/src/composition/td-manager.spec.ts | 4 ++++ .../components/table/src/composition/td-manager.ts | 10 +++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ui-vue/components/table/src/composition/td-manager.spec.ts b/packages/ui-vue/components/table/src/composition/td-manager.spec.ts index c3c85cc32..60a3f3921 100644 --- a/packages/ui-vue/components/table/src/composition/td-manager.spec.ts +++ b/packages/ui-vue/components/table/src/composition/td-manager.spec.ts @@ -16,6 +16,10 @@ import { TableTdManager } from './td-manager'; import { createDefaultTableRows } from './td-factory'; +jest.mock('@farris/ui-vue/components/message-box', () => ({ + FMessageBoxService: { question: jest.fn() } +})); + function makeManager(rows: any[]) { const redraw = jest.fn(); const canvas = jest.fn(); diff --git a/packages/ui-vue/components/table/src/composition/td-manager.ts b/packages/ui-vue/components/table/src/composition/td-manager.ts index e505620bc..3bb4271d6 100644 --- a/packages/ui-vue/components/table/src/composition/td-manager.ts +++ b/packages/ui-vue/components/table/src/composition/td-manager.ts @@ -1,3 +1,4 @@ +import { FMessageBoxService } from '@farris/ui-vue/components/message-box'; import { TableRow } from './types'; import { TableTdWidgetHelper } from './td-helper'; import { ContextMenuOption } from '../components/context-menu.component'; @@ -67,13 +68,8 @@ export class TableTdManager { } private question(msg: string, ok: () => void) { - if (this.services.messageBox) { - this.services.messageBox.question(msg, '', ok); - } else { - // Lazy-require to avoid module-resolution cascade in unit tests - const FMessageBoxService = require('@farris/ui-vue/components/message-box').FMessageBoxService; - FMessageBoxService.question(msg, '', ok); - } + const svc = this.services.messageBox || FMessageBoxService; + svc.question(msg, '', ok); } // ===== Insert operations ===== -- Gitee From f5547d3ce9604cd02b42284705c545759bb625af Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 16:42:09 +0800 Subject: [PATCH 14/26] feat(table): add table & cell property configs and designer rules --- .../table/src/designer/use-designer-rules.ts | 23 ++++++++ .../table-td.property-config.ts | 56 +++++++++++++++++++ .../property-config/table.property-config.ts | 30 ++++++++++ 3 files changed, 109 insertions(+) create mode 100644 packages/ui-vue/components/table/src/designer/use-designer-rules.ts create mode 100644 packages/ui-vue/components/table/src/property-config/table-td.property-config.ts create mode 100644 packages/ui-vue/components/table/src/property-config/table.property-config.ts diff --git a/packages/ui-vue/components/table/src/designer/use-designer-rules.ts b/packages/ui-vue/components/table/src/designer/use-designer-rules.ts new file mode 100644 index 000000000..34365714e --- /dev/null +++ b/packages/ui-vue/components/table/src/designer/use-designer-rules.ts @@ -0,0 +1,23 @@ +import { + DesignerHostService, DraggingResolveContext, UseDesignerRules, + ComponentSchema, DesignerItemContext +} from '@farris/ui-vue/components/designer-canvas'; +import { TableProperty } from '../property-config/table.property-config'; + +export function useDesignerRulesForTable( + designItemContext: DesignerItemContext, designerHostService?: DesignerHostService +): UseDesignerRules { + const schema = designItemContext.schema as ComponentSchema; + + // Phase 1: table manages its own cells internally, does not accept toolbox/field drag-in + function canAccepts(_draggingContext: DraggingResolveContext): boolean { + return false; + } + function checkCanMoveComponent(): boolean { return true; } + function checkCanDeleteComponent(): boolean { return true; } + function getPropsConfig(componentId: string) { + const componentProp = new TableProperty(componentId, designerHostService); + return componentProp.getPropertyConfig(designItemContext.schema); + } + return { canAccepts, checkCanMoveComponent, checkCanDeleteComponent, getPropsConfig }; +} diff --git a/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts new file mode 100644 index 000000000..ed66b7fdb --- /dev/null +++ b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts @@ -0,0 +1,56 @@ +import { BaseControlProperty } from '../../../property-panel/src/composition/entity/base-property'; + +export class TableTdProperty extends BaseControlProperty { + constructor(componentId: string, designerHostService: any) { + super(componentId, designerHostService); + } + public getPropertyConfig(cell: any) { + this.propertyConfig.categories['basic'] = { + title: '基本信息', description: '', + properties: { + id: { title: '标识', type: 'string', readonly: true }, + tdType: { + title: '单元格类型', type: 'select', + editor: { + type: 'combo-list', textField: 'name', valueField: 'value', idField: 'value', editable: false, + data: [{ value: 'staticText', name: '静态文本' }, { value: 'editor', name: '编辑器' }] + } + } + } + }; + this.propertyConfig.categories['staticText'] = { + title: '文本', description: '', + properties: { + 'staticText.text': { title: '文本内容', type: 'string', description: '' }, + 'staticText.require': { title: '必填标识', type: 'boolean', description: '' } + } + }; + this.propertyConfig.categories['editor'] = { + title: '绑定', description: '', + properties: { + 'editor.binding': { + title: '绑定字段', type: 'binding', + editor: { type: 'binding-selector' }, + description: '为单元格绑定实体字段' + } + } + }; + this.propertyConfig.categories['align'] = { + title: '对齐', description: '', + properties: { + align: { + title: '水平对齐', type: 'select', + editor: { type: 'combo-list', textField: 'name', valueField: 'value', idField: 'value', editable: false, + data: [{ value: 'left', name: '左' }, { value: 'center', name: '中' }, { value: 'right', name: '右' }] } + }, + valign: { + title: '垂直对齐', type: 'select', + editor: { type: 'combo-list', textField: 'name', valueField: 'value', idField: 'value', editable: false, + data: [{ value: 'top', name: '上' }, { value: 'middle', name: '中' }, { value: 'bottom', name: '下' }] } + } + } + }; + this.propertyConfig.categories['appearance'] = this.getAppearanceConfig(cell); + return this.propertyConfig; + } +} diff --git a/packages/ui-vue/components/table/src/property-config/table.property-config.ts b/packages/ui-vue/components/table/src/property-config/table.property-config.ts new file mode 100644 index 000000000..01ba9e68e --- /dev/null +++ b/packages/ui-vue/components/table/src/property-config/table.property-config.ts @@ -0,0 +1,30 @@ +import { BaseControlProperty } from '../../../property-panel/src/composition/entity/base-property'; + +export class TableProperty extends BaseControlProperty { + constructor(componentId: string, designerHostService: any) { + super(componentId, designerHostService); + } + public getPropertyConfig(propertyData: any) { + this.propertyConfig.categories['basic'] = this.getBasicPropConfig(propertyData); + this.propertyConfig.categories['appearance'] = this.getAppearanceConfig(propertyData); + this.propertyConfig.categories['behavior'] = this.getBehaviorConfig(propertyData); + this.propertyConfig.categories['border'] = { + title: '边框', description: '', + properties: { + 'border.width': { title: '边框宽度', type: 'int', description: '' }, + 'border.enableColor': { title: '启用边框色', type: 'boolean', description: '' }, + 'border.color': { title: '边框颜色', type: 'string', editor: { type: 'color-picker' }, description: '' } + } + }; + this.propertyConfig.categories['font'] = { + title: '字体', description: '', + properties: { + 'font.size': { title: '字号', type: 'int', description: '' }, + 'font.enableColor': { title: '启用字体色', type: 'boolean', description: '' }, + 'font.color': { title: '字体颜色', type: 'string', editor: { type: 'color-picker' }, description: '' }, + 'font.family': { title: '字体', type: 'string', description: '' } + } + }; + return this.propertyConfig; + } +} -- Gitee From 91d85cd42f12baaf5705ceb8dc2d042fc1a5f698 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 16:46:53 +0800 Subject: [PATCH 15/26] feat(table): add design-time table with cell selection and right-click menu --- .../src/designer/table.design.component.tsx | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 packages/ui-vue/components/table/src/designer/table.design.component.tsx diff --git a/packages/ui-vue/components/table/src/designer/table.design.component.tsx b/packages/ui-vue/components/table/src/designer/table.design.component.tsx new file mode 100644 index 000000000..4e33e46da --- /dev/null +++ b/packages/ui-vue/components/table/src/designer/table.design.component.tsx @@ -0,0 +1,142 @@ +import { SetupContext, defineComponent, inject, onMounted, ref, computed, triggerRef } from 'vue'; +import { getCustomClass } from '@farris/ui-vue/components/common'; +import { + DesignerItemContext, useDesignerComponent, DesignerHostService, canvasChanged +} from '@farris/ui-vue/components/designer-canvas'; +import { tableProps, TablePropsType } from '../table.props'; +import { TableCell, TableRow, TableSchema } from '../composition/types'; +import { TableTdManager, menuConfigToOptions } from '../composition/td-manager'; +import { getTableStyle, getCellStyle, renderLabelCell, renderEditorCell } from '../composition/use-table-cell-render'; +import FTableContextMenu, { ContextMenuOption } from '../components/context-menu.component'; +import { useDesignerRulesForTable } from './use-designer-rules'; +import { TableTdProperty } from '../property-config/table-td.property-config'; + +export default defineComponent({ + name: 'FTableDesign', + props: tableProps, + emits: [], + setup(props: TablePropsType, context: SetupContext) { + const elementRef = ref(); + const designerHostService = inject('designer-host-service'); + const designItemContext = inject('design-item-context') as DesignerItemContext; + const designViewModelUtils = inject('designViewModelUtils') as any; + const formSchemaUtils = (inject('useFormSchema') as any) || (designerHostService as any)?.formSchemaUtils; + const rules = useDesignerRulesForTable(designItemContext, designerHostService); + const componentInstance = useDesignerComponent(elementRef, designItemContext, rules); + + const schema = designItemContext.schema as unknown as TableSchema; + if (!schema.rows) { schema.rows = []; } + + const rowsRef = ref(schema.rows); + const selectedCellId = ref(''); + + const menuVisible = ref(false); + const menuPosition = ref({ x: 0, y: 0 }); + const menuOptions = ref([]); + + function refresh(payload?: any) { + triggerRef(rowsRef); + canvasChanged.value++; + if (payload && payload.clearPropertyPanel) { selectedCellId.value = ''; } + } + + const componentId = (componentInstance.value as any).belongedComponentId || props.componentId; + const tdManager = new TableTdManager( + { id: schema.id, rows: schema.rows }, + { formSchemaUtils, designViewModelUtils }, + (payload?: any) => refresh(payload), + () => (designItemContext as any).setupContext?.emit('canvasChanged') + ); + tdManager.viewModelId = formSchemaUtils?.getViewModelIdByComponentId?.(componentId) || ''; + + function buildCellComponentInstance(cell: TableCell) { + const cid = (componentInstance.value as any).belongedComponentId || props.componentId; + return { + schema: cell, + getPropConfig: () => new TableTdProperty(cid as string, designerHostService).getPropertyConfig(cell), + onPropertyChanged: () => refresh() + }; + } + + function onCellClick(evt: MouseEvent, cell: TableCell) { + evt.stopPropagation(); + selectedCellId.value = cell.id; + const cid = (componentInstance.value as any).belongedComponentId || props.componentId; + (designItemContext as any).setupContext?.emit( + 'selectionChange', 'table-td', cell, cid, buildCellComponentInstance(cell) + ); + } + + function openMenuAt(x: number, y: number, r: number, c: number) { + tdManager.setTarget(r, c); + const closeMenu = () => { menuVisible.value = false; }; + menuOptions.value = menuConfigToOptions(tdManager.getMenuConfig(), closeMenu); + menuPosition.value = { x, y }; + menuVisible.value = true; + } + + function onCellContextMenu(evt: MouseEvent, r: number, c: number) { + evt.preventDefault(); + evt.stopPropagation(); + openMenuAt(evt.clientX, evt.clientY, r, c); + } + + function onWidgetClick(evt: MouseEvent, cell: TableCell, r: number, c: number) { + evt.preventDefault(); + evt.stopPropagation(); + onCellClick(evt, cell); + const rect = (evt.currentTarget as HTMLElement).getBoundingClientRect(); + openMenuAt(rect.right, rect.bottom, r, c); + } + + onMounted(() => { + elementRef.value.componentInstance = componentInstance; + }); + context.expose(componentInstance.value); + + const wrapperClass = computed(() => getCustomClass({ 'f-table-design': true, 'drag-container': true }, props.customClass)); + + return () => { + const rows = rowsRef.value; + return ( +
+ + + {rows.map((row, r) => ( + + {row.columns.map((cell, c) => cell.invisible ? null : ( + + ))} + + ))} + +
onCellClick(e, cell)} + onContextmenu={(e: MouseEvent) => onCellContextMenu(e, r, c)}> +
+ {cell.tdType === 'staticText' ? renderLabelCell(cell) : renderEditorCell(cell, { design: true })} +
onWidgetClick(e, cell, r, c)}> + +
+
+
+ { menuVisible.value = false; }} /> +
+ ); + }; + } +}); -- Gitee From a44b9adf8073e777ebbf50ca1c9a3c82871c5961 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 16:57:21 +0800 Subject: [PATCH 16/26] feat(table): register table component (runtime + designer + dg-control) --- packages/ui-vue/components/components.ts | 1 + .../designer-canvas/src/components/maps.ts | 2 + .../src/composition/dg-control.ts | 2 + .../dynamic-view/src/components/maps.ts | 3 ++ packages/ui-vue/components/table/index.ts | 39 +++++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 packages/ui-vue/components/table/index.ts diff --git a/packages/ui-vue/components/components.ts b/packages/ui-vue/components/components.ts index dfb905c81..6d9e13d00 100644 --- a/packages/ui-vue/components/components.ts +++ b/packages/ui-vue/components/components.ts @@ -134,3 +134,4 @@ export { FilterConditionEditor, ConditionActions, FilterConditionGrid, SortCondi export { ExpressionEditor, FExpressionTextbox } from './expression-editor'; export { FCarouselComponent, default as FCarousel } from './carousel'; export { ContentContainer as FContentContainer } from './content-container'; +export { Table as FTable } from './table'; diff --git a/packages/ui-vue/components/designer-canvas/src/components/maps.ts b/packages/ui-vue/components/designer-canvas/src/components/maps.ts index 0a66f9809..a9797626a 100644 --- a/packages/ui-vue/components/designer-canvas/src/components/maps.ts +++ b/packages/ui-vue/components/designer-canvas/src/components/maps.ts @@ -11,6 +11,7 @@ import FCheckboxGroup from '@farris/ui-vue/components/checkbox-group/designer'; import FColorPicker from '@farris/ui-vue/components/color-picker'; import FComboList from '@farris/ui-vue/components/combo-list'; import FContentContainer from '@farris/ui-vue/components/content-container'; +import FTable from '@farris/ui-vue/components/table'; import FDataGrid from '@farris/ui-vue/components/data-grid/designer'; import FDatePicker from '@farris/ui-vue/components/date-picker/designer'; import FDropdown from '@farris/ui-vue/components/dropdown'; @@ -100,6 +101,7 @@ function loadDesignerRegister() { FColorPicker.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter, registerContext); FComboList.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter, registerContext); FContentContainer.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter, registerContext); + FTable.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter, registerContext); FDataGrid.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter, registerContext); FDatePicker.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter, registerContext); FDropdown.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter); diff --git a/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts b/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts index f7aaa1000..aad2087b4 100644 --- a/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts +++ b/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts @@ -7,6 +7,8 @@ export const DgControl = { 'content-container': { type: 'content-container', name: '容器' }, + 'table': { type: 'table', name: '表格布局' }, + 'input-group': { type: 'input-group', name: '文本' }, 'textarea': { type: 'textarea', name: '多行文本' }, diff --git a/packages/ui-vue/components/dynamic-view/src/components/maps.ts b/packages/ui-vue/components/dynamic-view/src/components/maps.ts index b77879d82..6c104a2e2 100644 --- a/packages/ui-vue/components/dynamic-view/src/components/maps.ts +++ b/packages/ui-vue/components/dynamic-view/src/components/maps.ts @@ -13,6 +13,7 @@ import FComboTree from '@farris/ui-vue/components/combo-tree'; import FComponent from '@farris/ui-vue/components/component'; import FColorPicker from '@farris/ui-vue/components/color-picker'; import FContentContainer from '@farris/ui-vue/components/content-container'; +import FTable from '@farris/ui-vue/components/table'; import FDatePicker from '@farris/ui-vue/components/date-picker/designer'; import FDataGrid from '@farris/ui-vue/components/data-grid/designer'; import FDropdown from '@farris/ui-vue/components/dropdown'; @@ -114,6 +115,7 @@ const componentsForRegister = [ FTabs, FListView, FContentContainer, + FTable, FHtmlTemplate, FCalendar ]; @@ -145,6 +147,7 @@ async function loadRegister() { FComboTree.register(componentMap, componentPropsConverter, componentPropertyConfigConverter, resolverMap, registerContext); FComponent.register(componentMap, componentPropsConverter, componentPropertyConfigConverter, resolverMap); FContentContainer.register(componentMap, componentPropsConverter, componentPropertyConfigConverter, resolverMap, registerContext); + FTable.register(componentMap, componentPropsConverter, componentPropertyConfigConverter, resolverMap, registerContext); FColorPicker.register(componentMap, componentPropsConverter, componentPropertyConfigConverter, resolverMap, registerContext); FDatePicker.register(componentMap, componentPropsConverter, componentPropertyConfigConverter, resolverMap, registerContext); FDataGrid.register(componentMap, componentPropsConverter, componentPropertyConfigConverter, resolverMap, registerContext); diff --git a/packages/ui-vue/components/table/index.ts b/packages/ui-vue/components/table/index.ts new file mode 100644 index 000000000..78eef3a8f --- /dev/null +++ b/packages/ui-vue/components/table/index.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { App } from 'vue'; +import Table from './src/table.component'; +import TableDesign from './src/designer/table.design.component'; +import { propsResolver, propsResolverGenerator } from './src/table.props'; +import { RegisterContext } from '../common'; + +export * from './src/table.props'; +export { Table, TableDesign }; + +export default { + install(app: App): void { + app.component(Table.name as string, Table); + }, + register(componentMap: Record, propsResolverMap: Record, configResolverMap: Record, + resolverMap: Record, registerContext: RegisterContext): void { + componentMap['table'] = Table; + propsResolverMap['table'] = propsResolverGenerator(registerContext); + }, + registerDesigner(componentMap: Record, propsResolverMap: Record, configResolverMap: Record, + registerContext: RegisterContext): void { + componentMap['table'] = TableDesign; + propsResolverMap['table'] = propsResolverGenerator(registerContext); + } +}; -- Gitee From c23965a42cf63cca7459736765175a187dc33864 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 17:00:29 +0800 Subject: [PATCH 17/26] fix(table): type insertRow accumulator as any[] to satisfy strict mode --- packages/ui-vue/components/table/src/composition/td-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-vue/components/table/src/composition/td-helper.ts b/packages/ui-vue/components/table/src/composition/td-helper.ts index c1d204c0f..ec9836d5c 100644 --- a/packages/ui-vue/components/table/src/composition/td-helper.ts +++ b/packages/ui-vue/components/table/src/composition/td-helper.ts @@ -62,7 +62,7 @@ export class TableTdWidgetHelper { } insertRow(t: number, rows: any[]) { - const n = []; + const n: any[] = []; for (let i = 0; i < rows[0].columns.length; i++) { const o = this.findPrevRowIndex(t, i, rows); const tdMetadata = this.getTableTdMetadata(); -- Gitee From ac84900c4db1c8c84cc694bfc1492cd1c93b8d78 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 9 Jul 2026 17:31:53 +0800 Subject: [PATCH 18/26] fix(table): position context menu via watch and guard missing dgViewModel service --- .../src/components/context-menu.component.tsx | 19 ++++++++++++------- .../table/src/composition/td-manager.ts | 3 ++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/ui-vue/components/table/src/components/context-menu.component.tsx b/packages/ui-vue/components/table/src/components/context-menu.component.tsx index 53ce8ee81..06fb7f4d4 100644 --- a/packages/ui-vue/components/table/src/components/context-menu.component.tsx +++ b/packages/ui-vue/components/table/src/components/context-menu.component.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, nextTick, onMounted, onBeforeUnmount } from "vue"; +import { defineComponent, ref, nextTick, onMounted, onBeforeUnmount, watch } from "vue"; export interface ContextMenuOption { id: string; @@ -96,12 +96,17 @@ export default defineComponent({ document.removeEventListener('keydown', handleKeyDown); }); - (() => { - if (props.visible) { - adjustedPosition.value = props.position; - adjustPosition(); - } - })(); + // 该组件在设计组件中常驻挂载,setup 仅执行一次;用 watch 保证每次打开都按最新位置定位。 + watch( + () => [props.visible, props.position.x, props.position.y], + () => { + if (props.visible) { + adjustedPosition.value = props.position; + adjustPosition(); + } + }, + { immediate: true } + ); return () => { if (!props.visible) { diff --git a/packages/ui-vue/components/table/src/composition/td-manager.ts b/packages/ui-vue/components/table/src/composition/td-manager.ts index 3bb4271d6..106fa8e51 100644 --- a/packages/ui-vue/components/table/src/composition/td-manager.ts +++ b/packages/ui-vue/components/table/src/composition/td-manager.ts @@ -170,7 +170,8 @@ export class TableTdManager { // ===== Field cleanup (removeField is REQUIRED; expression removal from data-grid pattern) ===== removeDesignViewModelFieldInColumns(columns: any[]) { - const dgViewModel = this.services.designViewModelUtils.getDgViewModel(this.viewModelId); + const dgViewModel = this.services.designViewModelUtils?.getDgViewModel?.(this.viewModelId); + if (!dgViewModel) { return; } columns.forEach(co => { if (co.editor && co.editor.binding && co.editor.binding.field) { const fieldId = co.editor.binding.field; -- Gitee From fcb50a6d29383e8148d00a50678371bea1befd28 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 13 Jul 2026 20:05:30 +0800 Subject: [PATCH 19/26] =?UTF-8?q?feature:=20=E6=94=AF=E6=8C=81table?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=A0=BC=E7=9A=84=E5=B1=9E=E6=80=A7=E7=BC=96?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designer/public/assets/icon/iconfont.css | 2 +- .../public/assets/template-rules/common.json | 43 +- .../composition/use-view-model-name.ts | 7 +- .../composition/command.service.tsx | 3 +- .../composition/form-metadata.service.tsx | 4 +- .../composition/use-event-parameter-data.ts | 38 -- .../components/composition/use-form-schema.ts | 2 +- .../src/binding-selector.component.tsx | 32 +- .../src/binding-selector.props.ts | 7 +- .../binding-selector-container.component.tsx | 15 +- .../src/composition/use-field-selection.ts | 85 ++-- packages/ui-vue/components/components.ts | 2 +- .../src/composition/dg-control.ts | 1 + .../src/composition/use-outline-node.ts | 3 + .../components/designer-outline/src/types.ts | 5 +- .../components/dynamic-form/designer.ts | 14 +- .../src/designer/lookup.design.component.tsx | 2 +- .../src/designer/spinner.design.component.tsx | 40 +- .../composition/entity/input-base-property.ts | 2 +- packages/ui-vue/components/table/index.ts | 11 +- .../src/components/context-menu.component.tsx | 131 +++--- .../table/src/composition/common.ts | 57 +++ .../table/src/composition/td-factory.ts | 7 +- .../table/src/composition/td-helper.ts | 2 +- .../table/src/composition/td-manager.ts | 92 +--- .../components/table/src/composition/types.ts | 8 +- .../src/composition/use-table-cell-render.tsx | 25 +- .../src/designer/table.design.component.tsx | 87 ++-- .../table/src/designer/use-designer-rules.ts | 53 ++- .../converter/border-converter.ts | 13 + .../converter/font-converter.ts | 13 + .../converter/static-text.converter.ts | 13 + .../table-td.property-config.ts | 438 ++++++++++++++++-- .../property-config/table.property-config.ts | 77 ++- .../table/src/schema/table-td.schema.json | 116 +++++ .../table/src/schema/table.schema.json | 85 +++- .../components/table/src/table.component.tsx | 2 +- .../components/table/src/table.props.ts | 37 +- .../designer/textarea.design.component.tsx | 2 +- .../designer/time-picker.design.component.tsx | 2 +- packages/ui-vue/demos/table/basic.tsx | 252 ++++++++++ 41 files changed, 1470 insertions(+), 360 deletions(-) create mode 100644 packages/ui-vue/components/table/src/composition/common.ts create mode 100644 packages/ui-vue/components/table/src/property-config/converter/border-converter.ts create mode 100644 packages/ui-vue/components/table/src/property-config/converter/font-converter.ts create mode 100644 packages/ui-vue/components/table/src/property-config/converter/static-text.converter.ts create mode 100644 packages/ui-vue/components/table/src/schema/table-td.schema.json create mode 100644 packages/ui-vue/demos/table/basic.tsx diff --git a/packages/designer/public/assets/icon/iconfont.css b/packages/designer/public/assets/icon/iconfont.css index 75ab8c3bb..966eef477 100644 --- a/packages/designer/public/assets/icon/iconfont.css +++ b/packages/designer/public/assets/icon/iconfont.css @@ -11,7 +11,7 @@ -moz-osx-font-smoothing: grayscale; } -.fd_pc-Table:before { +.fd_pc-table:before { content: "\e615"; } diff --git a/packages/designer/public/assets/template-rules/common.json b/packages/designer/public/assets/template-rules/common.json index 10c456d02..2e1549a28 100644 --- a/packages/designer/public/assets/template-rules/common.json +++ b/packages/designer/public/assets/template-rules/common.json @@ -266,7 +266,15 @@ "f-form-layout": { "description": "卡片面板", "canDelete": false, - "canAccept": true, + "canAccept": { + "invalidContext": [ + { + "firstLevelChild": { + "class": "f-table-has-form" + } + } + ] + }, "canMove": false }, "f-component-tabs": { @@ -300,6 +308,14 @@ "firstLevelChild": { "type": "component-ref" } + }, + { + "parent": { + "class": "f-struct-wrapper" + }, + "firstLevelChild": { + "class": "f-form-is-table" + } } ] }, @@ -320,6 +336,14 @@ "firstLevelChild": { "type": "component-ref" } + }, + { + "parent": { + "class": "f-struct-wrapper" + }, + "firstLevelChild": { + "class": "f-form-is-table" + } } ] }, @@ -340,6 +364,14 @@ "firstLevelChild": { "type": "component-ref" } + }, + { + "parent": { + "class": "f-struct-wrapper" + }, + "firstLevelChild": { + "class": "f-form-is-table" + } } ] } @@ -445,6 +477,9 @@ "parent": { "class": "f-page-content", "type": "content-container" + }, + "firstLevelChild": { + "class": "f-page" } } ] @@ -569,5 +604,11 @@ } ] } + }, + "f-form-is-table": { + "description": "table组件内的卡片面板", + "canMove": false, + "canDelete": false, + "canAccept": false } } \ No newline at end of file diff --git a/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts b/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts index 8f1f1f917..99d60aaee 100644 --- a/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts +++ b/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts @@ -64,8 +64,11 @@ export function useViewModelName(useFormSchema: UseFormSchema) { case ComponentType.calendar: { return getComponentParentTitle(formSchema, component.id) || '日历组件'; } - case ComponentType.modalFrame: { - return '弹窗页面组件'; + case ComponentType.calendar: { + return getComponentParentTitle(formSchema, component.id) || '日历组件'; + } + case ComponentType.table: { + return '表格布局组件'; } case ComponentType.form: { // 卡片组件取内部section的标题 diff --git a/packages/designer/src/components/composition/command.service.tsx b/packages/designer/src/components/composition/command.service.tsx index 04babd1c6..f456478d4 100644 --- a/packages/designer/src/components/composition/command.service.tsx +++ b/packages/designer/src/components/composition/command.service.tsx @@ -542,7 +542,7 @@ export function useFormCommandService( if (componentsItem['viewModel']) { viewModelId = componentsItem['viewModel']; } // 子级控件通用的连接关键字 - const childrenKeys = ['contents', 'columns', 'buttons', 'children']; + const childrenKeys = ['contents', 'columns', 'buttons', 'children', "rows"]; childrenKeys.map(key => { if (componentsItem[key]) { findBoundEvent(componentsItem[key], findEvents, viewModelId); @@ -578,6 +578,7 @@ export function useFormCommandService( findBoundEvent(componentsItem['footerToolbar']['buttons'], findEvents, viewModelId); } } + // 表格卡片展示模式 if (componentsItem['cardToolbar']) { findBoundEvent(componentsItem['cardToolbar'], findEvents, viewModelId); } diff --git a/packages/designer/src/components/composition/form-metadata.service.tsx b/packages/designer/src/components/composition/form-metadata.service.tsx index ffa4eae71..acc91ebf2 100644 --- a/packages/designer/src/components/composition/form-metadata.service.tsx +++ b/packages/designer/src/components/composition/form-metadata.service.tsx @@ -93,12 +93,12 @@ export function useFormMetadata(props: DesignerProps, useFormSchemaComposition: notifyService.globalConfig = { position: 'top-center' }; return new Promise((resolve, reject) => { - const rulesRequests = [axios.get(`assets/template-rules/common.json`).then(response => response).catch(error => { + const rulesRequests = [axios.get(`assets/template-rules/common.json?version=${new Date().getTime()}`).then(response => response).catch(error => { notifyService.error('获取表单公共控制规则失败。'); return; })]; if (templateId && templateRule) { - rulesRequests.push(axios.get(`assets/template-rules/${templateRule}.json`).then(response => response).catch(error => { + rulesRequests.push(axios.get(`assets/template-rules/${templateRule}.json?version=${new Date().getTime()}`).then(response => response).catch(error => { notifyService.error(`获取模板[${templateRule}]的控制规则失败。`); return; })); diff --git a/packages/designer/src/components/composition/use-event-parameter-data.ts b/packages/designer/src/components/composition/use-event-parameter-data.ts index bc40997d8..76ffadfd2 100644 --- a/packages/designer/src/components/composition/use-event-parameter-data.ts +++ b/packages/designer/src/components/composition/use-event-parameter-data.ts @@ -60,44 +60,6 @@ export function useEventParameterData( return '表格组件'; } - function getViewModelName(viewModelId: string, componentName: string) { - const component = useFormSchemaComposition.getComponentByViewModelId(viewModelId); - if (!component || component.fakeDel) { - return; - } - switch (component.componentType) { - case ComponentType.Frame: { - return '根组件'; - } - case ComponentType.dataGrid: { - return getDataGridComponentName(component); - } - case ComponentType.uploader: { - return '附件组件'; - } - case ComponentType.listView: { - return '列表视图组件'; - } - case ComponentType.calendar: { - return '日历组件'; - } - case ComponentType.modalFrame: { - return '弹窗页面组件'; - } - default: { - // 卡片组件取内部section的标题 - if (component.componentType.startsWith('form')) { - const section = component.contents.find(content => content.type === 'section'); - if (section && section.mainTitle) { - return section.mainTitle + '组件'; - } - - } - } - } - - return componentName + '组件'; - } // 构造actions data function buildActions() { const stateMachineMetadata = useFormStateMachineComposition.getStateMachineMetadata(); diff --git a/packages/designer/src/components/composition/use-form-schema.ts b/packages/designer/src/components/composition/use-form-schema.ts index 7439d082d..b1dd2dbf9 100644 --- a/packages/designer/src/components/composition/use-form-schema.ts +++ b/packages/designer/src/components/composition/use-form-schema.ts @@ -1244,7 +1244,7 @@ export function useFormSchema(): UseFormSchema { return {}; } const eventMap = Object.assign({}, schemaMap[componentSchema.type]?.events || {}); - if (componentSchema.type === 'form-group' && componentSchema.editor) { + if ((componentSchema.type === 'form-group' || componentSchema.type === 'table-td') && componentSchema.editor) { const editorType = componentSchema.editor.type; Object.assign(eventMap, schemaMap[editorType]?.events); } diff --git a/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx b/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx index 515935795..67fa390f5 100644 --- a/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx +++ b/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx @@ -22,7 +22,7 @@ export default defineComponent({ /** 文本框展示内容 */ const displayText = ref(bindingType.value === 'Form' ? props.modelValue?.path : props.modelValue?.fullPath); - const { updateViewModel, updateComponentSchema, checkFieldValiation, clearExpresssionConfig } = useFieldSelection(props); + const { updateViewModel, updateComponentSchema, checkFieldValiation, clearExpresssionConfig, fieldTypeChanged, clearViewModel, clearComponentSchema } = useFieldSelection(props); function onFieldSelected(selectedFields: Array>) { bindingData.value = selectedFields; @@ -32,8 +32,12 @@ export default defineComponent({ bindingType.value = newBindingType; } - function setDisplayText(setDisplayText: any) { - displayText.value = setDisplayText?.type === 'Form' ? setDisplayText.path : setDisplayText.fullPath; + function setDisplayText(field: any) { + if (!field) { + displayText.value = ''; + return; + } + displayText.value = field?.type === 'Form' ? field.path : field.fullPath; } function renderBindingSelector() { @@ -47,6 +51,7 @@ export default defineComponent({ editorParams={props.editorParams} bindingType={props.bindingType} showCustomFooter={false} + canChangeFieldType={props.editorParams.canChangeFieldType} >; } @@ -84,10 +89,11 @@ export default defineComponent({ clearExpresssionConfig(bindingData.value[0]); const newBindingValue = updateComponentSchema(bindingData.value[0], bindingType.value); setDisplayText(newBindingValue); - + // 触发变更 if (props.onFieldSelected && typeof props.onFieldSelected == 'function') { - props.onFieldSelected(newBindingValue, undefined,bindingData.value[0]); + const outParams = { fieldTypeChanged: fieldTypeChanged.value, ...bindingData.value[0] }; + props.onFieldSelected(newBindingValue, undefined, outParams); } return true; } @@ -97,6 +103,14 @@ export default defineComponent({ draggable: true }; + function onClearBinding() { + clearViewModel(); + clearExpresssionConfig(); + clearComponentSchema() + setDisplayText(null); + props.onFieldSelected(undefined, undefined); + } + return () => { return ( + enableTitle={true} + onClear={onClearBinding} > +
{renderBindingSelector()}
-
); + ); }; } }); diff --git a/packages/ui-vue/components/binding-selector/src/binding-selector.props.ts b/packages/ui-vue/components/binding-selector/src/binding-selector.props.ts index 927eacbf4..d7c34e830 100644 --- a/packages/ui-vue/components/binding-selector/src/binding-selector.props.ts +++ b/packages/ui-vue/components/binding-selector/src/binding-selector.props.ts @@ -74,8 +74,11 @@ export const bindingSelectorProps = { editorParams: { type: Object as PropType, default: null }, onFieldSelected: { type: Function, default: null }, /** 是否显示自定义的底部按钮区域 */ - showCustomFooter: { type: Boolean, default: false } - + showCustomFooter: { type: Boolean, default: false }, + /** 是否支持清除绑定 */ + enableClear: { type: Boolean, default: false }, + /** 是否可以变更字段类型 */ + canChangeFieldType: { type: Boolean, default: false } } as Record; export type BindingSelectorProps = ExtractPropTypes; diff --git a/packages/ui-vue/components/binding-selector/src/components/binding-selector-container.component.tsx b/packages/ui-vue/components/binding-selector/src/components/binding-selector-container.component.tsx index fda804060..46e493483 100644 --- a/packages/ui-vue/components/binding-selector/src/components/binding-selector-container.component.tsx +++ b/packages/ui-vue/components/binding-selector/src/components/binding-selector-container.component.tsx @@ -127,9 +127,13 @@ export default defineComponent({ return visualData; } + // 若外部要求不限制字段类型的变换,则直接返回 + if (props.editorParams?.canChangeFieldType) { + visualData.disabled = false; + return visualData; + } const fieldTypeNode = getRealFieldType(visualData, type); const editorType = getEditorType(); - if (editorType === 'combo-list' || editorType === 'lookup') { if (type !== 'Form') { visualData.disabled = fieldTypeNode !== 'Object'; @@ -150,6 +154,7 @@ export default defineComponent({ return visualData; } + // 1、由复杂类型字段切换成其他字段类型:新字段适配控件类型即可 // 2、原字段被删除或原本没有绑定信息:新字段适配控件类型即可 if (isOriginalBindComplexField || !originalFieldType) { @@ -223,10 +228,10 @@ export default defineComponent({ return (
{props.bindingType.enable && !isLanguageEditor.value && renderBindingType()}
- {/* {bindingType.value === 'Form' ? renderTreeGrids() :renderDataGrid()} */} -
{renderTreeGrids()}
-
{renderDataGrid()}
- + {/* {bindingType.value === 'Form' ? renderTreeGrids() :renderDataGrid()} */} +
{renderTreeGrids()}
+
{renderDataGrid()}
+
{ props.showCustomFooter ? diff --git a/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts b/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts index 2dadaff6a..406678c14 100644 --- a/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts +++ b/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts @@ -3,6 +3,7 @@ import { FormBindingType, SchemaDOMMapping as defaultSchemaDOMMapping } from "@f import { merge } from "lodash-es"; import { BindingSelectorProps } from "../binding-selector.props"; import { FormSchemaEntityField$Type, FormSchemaEntityFieldTypeName } from "@farris/ui-vue/components/common"; +import { ref } from "vue"; export function useFieldSelection(props: BindingSelectorProps) { @@ -11,6 +12,8 @@ export function useFieldSelection(props: BindingSelectorProps) { let fieldTreeData = []; let localeVariableTreeData = []; let formVariableTreeData = []; + /** 字段类型是否发生变化 */ + let fieldTypeChanged = ref(false); const notifyService: any = new FNotifyService(); notifyService.globalConfig = { position: 'top-center' }; @@ -60,22 +63,22 @@ export function useFieldSelection(props: BindingSelectorProps) { variablesTreeData.forEach((item: any) => { if (!item.nodeType && (!item.children || !item.children.length)) { if (item.data) { - variableItems.push({...item.data, fromSource: parentItem?.data?.name}); + variableItems.push({ ...item.data, fromSource: parentItem?.data?.name }); } } else if (item.children) { const childVariables = getLocaleVariables(item.children, item); variableItems.push(...childVariables); } }); - } + } - return variableItems; + return variableItems; } function getVariableData() { const localeVars = getLocaleVariables(loadLocaleVariableTreeData()); - const remoteVars = loadFormVariableTreeData().map((item: any) => ({...item.data, fromSource: '视图对象'})); + const remoteVars = loadFormVariableTreeData().map((item: any) => ({ ...item.data, fromSource: '视图对象' })); return [...localeVars, ...remoteVars]; } @@ -156,12 +159,12 @@ export function useFieldSelection(props: BindingSelectorProps) { * @param bindingData * @returns */ - function clearExpresssionConfig(bindingData: Record) { + function clearExpresssionConfig(bindingData?: Record) { if (!props.editorParams.needSyncToViewModel || !props.editorParams.viewModelId || !props.editorParams.designerHostService) { return; } const originalBinding = props.editorParams.componentSchema.binding; - if (!originalBinding || originalBinding.field === bindingData.id) { + if (!originalBinding || originalBinding.field === bindingData?.id) { return; } if (formSchemaUtils.getExpressions().length) { @@ -211,16 +214,16 @@ export function useFieldSelection(props: BindingSelectorProps) { props.editorParams.componentSchema.binding = {}; } if (bindingType === 'Form') { - props.editorParams.componentSchema.binding.path = bindingData.bindingField; - props.editorParams.componentSchema.binding.field = bindingData.id; - props.editorParams.componentSchema.binding.fullPath = bindingData.path; - + props.editorParams.componentSchema.binding.path = bindingData.bindingField; + props.editorParams.componentSchema.binding.field = bindingData.id; + props.editorParams.componentSchema.binding.fullPath = bindingData.path; + } else { if (bindingData.category === 'locale') { props.editorParams.componentSchema.binding.path = bindingData.viewModelId === viewModelId ? bindingData.code : 'root-component.' + bindingData.code; props.editorParams.componentSchema.binding.field = bindingData.id; props.editorParams.componentSchema.binding.fullPath = bindingData.code; - + } else { props.editorParams.componentSchema.binding.path = 'root-component.' + bindingData.code; props.editorParams.componentSchema.binding.field = bindingData.id; @@ -303,8 +306,8 @@ export function useFieldSelection(props: BindingSelectorProps) { } const controlFieldMapping = {}; - const schemaDOMMapping = designerContext && designerContext.schemaDOMMapping ? designerContext.schemaDOMMapping : defaultSchemaDOMMapping; - const { fieldControlTypeMapping } = schemaDOMMapping; + const schemaDOMMapping = designerContext && designerContext.schemaDOMMapping ? designerContext.schemaDOMMapping : defaultSchemaDOMMapping; + const { fieldControlTypeMapping } = schemaDOMMapping; for (const fieldType in fieldControlTypeMapping) { const controlTypes = fieldControlTypeMapping[fieldType]; @@ -386,24 +389,24 @@ export function useFieldSelection(props: BindingSelectorProps) { return true; } let result = true; - let fieldTypeChanged = false; + fieldTypeChanged.value = false; const { originalFieldType, isOriginalBindComplexField } = getOriginalFieldType(); - + // 新绑定类型 const selectedData = bindingData[0]; const { fieldType: newFieldType, fieldTypeName: newFieldTypeName } = getFieldType(selectedData, bindingType); - + const cannotChangeTo = '不可以切换成'; const reselect = '类型字段,请重新选择!'; - + const editorType = props.editorParams?.componentSchema?.editor?.type; const allowedBindingFieldTypes = resolveFieldTypesByEditorType(editorType); // 1、由复杂类型字段切换成其他字段类型:新字段适配控件类型即可 // 2、原字段被删除:新字段适配控件类型即可 if (isOriginalBindComplexField || !originalFieldType) { - fieldTypeChanged = true; + fieldTypeChanged.value = true; if (allowedBindingFieldTypes?.length && !allowedBindingFieldTypes.includes(newFieldType)) { notifyService.warning(`${cannotChangeTo}${newFieldTypeName}${reselect}`); @@ -414,7 +417,10 @@ export function useFieldSelection(props: BindingSelectorProps) { // 3、简单字段之间的切换:字段类型必须一致 if (originalFieldType && originalFieldType !== newFieldType) { - + if (props.editorParams.canChangeFieldType) { + fieldTypeChanged.value = true; + return true; + } if (editorType === 'combo-list' || editorType === 'lookup' || editorType === 'radio-group') { if (bindingType === FormBindingType.Form) { return allowedBindingFieldTypes.includes(newFieldType); @@ -422,18 +428,40 @@ export function useFieldSelection(props: BindingSelectorProps) { return newFieldType === 'String' || newFieldType === 'Object'; } } + notifyService.warning(`${cannotChangeTo}${newFieldTypeName}${reselect}`); + return false; - if (!props.editorParams.canChangeFieldType) { - notifyService.warning(`${cannotChangeTo}${newFieldTypeName}${reselect}`); - return false; - } else { - fieldTypeChanged = true; - return true; - } } return result; } + /** + * 清除视图模型中的绑定字段 + */ + function clearViewModel() { + if (!props.editorParams.needSyncToViewModel || !props.editorParams.viewModelId || !props.editorParams.designerHostService) { + return; + } + const originalBinding = props.editorParams.componentSchema?.binding; + if (!originalBinding?.field) { + return; + } + const dgViewModel = designViewModelUtils.getDgViewModel(viewModelId); + dgViewModel.removeField([originalBinding.field]); + } + + /** + * 清除组件schema的绑定信息 + */ + function clearComponentSchema() { + const componentSchema = props.editorParams.componentSchema; + if (!componentSchema) { + return; + } + componentSchema.binding = null; + componentSchema.path = ''; + } + return { initTreeData, getTreeDataSource, @@ -444,6 +472,9 @@ export function useFieldSelection(props: BindingSelectorProps) { checkFieldValiation, clearExpresssionConfig, getOriginalFieldType, - getVariableData + getVariableData, + fieldTypeChanged, + clearViewModel, + clearComponentSchema }; } diff --git a/packages/ui-vue/components/components.ts b/packages/ui-vue/components/components.ts index 6d9e13d00..55a7862bd 100644 --- a/packages/ui-vue/components/components.ts +++ b/packages/ui-vue/components/components.ts @@ -134,4 +134,4 @@ export { FilterConditionEditor, ConditionActions, FilterConditionGrid, SortCondi export { ExpressionEditor, FExpressionTextbox } from './expression-editor'; export { FCarouselComponent, default as FCarousel } from './carousel'; export { ContentContainer as FContentContainer } from './content-container'; -export { Table as FTable } from './table'; +export { default as FTable } from './table'; diff --git a/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts b/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts index aad2087b4..80eb70d2b 100644 --- a/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts +++ b/packages/ui-vue/components/designer-canvas/src/composition/dg-control.ts @@ -8,6 +8,7 @@ export const DgControl = { 'content-container': { type: 'content-container', name: '容器' }, 'table': { type: 'table', name: '表格布局' }, + 'table-td': { type: 'table-td', name: '单元格' }, 'input-group': { type: 'input-group', name: '文本' }, diff --git a/packages/ui-vue/components/designer-outline/src/composition/use-outline-node.ts b/packages/ui-vue/components/designer-outline/src/composition/use-outline-node.ts index a04228add..ccae4a353 100644 --- a/packages/ui-vue/components/designer-outline/src/composition/use-outline-node.ts +++ b/packages/ui-vue/components/designer-outline/src/composition/use-outline-node.ts @@ -131,6 +131,9 @@ export function useOutlineNode(designerHostService: DesignerHostService, context case ComponentType.calendar: { return '日历组件'; } + case ComponentType.table: { + return '表格布局组件'; + } case ComponentType.page: { return componentsItem.name || '页面'; } diff --git a/packages/ui-vue/components/designer-outline/src/types.ts b/packages/ui-vue/components/designer-outline/src/types.ts index 858e5fab0..ac7622d2b 100644 --- a/packages/ui-vue/components/designer-outline/src/types.ts +++ b/packages/ui-vue/components/designer-outline/src/types.ts @@ -35,7 +35,10 @@ export enum ComponentType { /** * 页面 */ - page = 'page' + page = 'page', + + /** 表格 */ + table = 'table' } /** diff --git a/packages/ui-vue/components/dynamic-form/designer.ts b/packages/ui-vue/components/dynamic-form/designer.ts index 436d6db82..33a0a64cc 100644 --- a/packages/ui-vue/components/dynamic-form/designer.ts +++ b/packages/ui-vue/components/dynamic-form/designer.ts @@ -1,17 +1,19 @@ import { Plugin } from 'vue'; -import { FResponseForm, FDynamicFormGroup, FDynamicFormInput,DynamicFormGroupPropsType, - dynamicFormGroupProps, DynamicFormInputProps, dynamicFormInputProps } from '@farris/ui-vue/components/dynamic-form'; +import { + FResponseForm, FDynamicFormGroup, FDynamicFormInput, DynamicFormGroupPropsType, + dynamicFormGroupProps, DynamicFormInputProps, dynamicFormInputProps +} from '@farris/ui-vue/components/dynamic-form'; import { createPropsResolver, createFormGroupEditorResolver } from '@farris/ui-vue/components/dynamic-resolver'; import FResponseFormDesign from './src/designer/response-form.design.component'; import FDynamicFormDesign from './src/designer/dynamic-form.design.component'; import FDynamicForm from './src/dynamic-form.component'; -import FDynamicFromGroupDesign from './src/designer/form-group.design.component'; +import FDynamicFormGroupDesign from './src/designer/form-group.design.component'; import { responseFormLayoutPropsResolver } from './src/designer/response-form-layout-setting.props'; import FResponseFormLayoutSetting from './src/designer/response-form-layout-setting.component'; import { schemaMapper } from './src/schema/schema-mapper'; import formGroupSchema from './src/schema/form-group.schema.json'; -import {dynamicFormPropsResolver, formPropsResolver} from './src/response-form.props'; +import { dynamicFormPropsResolver, formPropsResolver } from './src/response-form.props'; import { dynamicFormDesignPropsResolver, formDesignPropsResolver } from './src/designer/response-form.design.props'; import { createDynamicFormCallbackResolver } from './src/composition/callback-resolvers'; @@ -25,7 +27,7 @@ export const formInputPropsResolver = createPropsResolver dynamicFormInputProps, formGroupSchema, schemaMapper, undefined ); export { FResponseFormDesign }; - +export { FDynamicFormGroupDesign }; FResponseFormDesign.register = (componentMap: Record, propsResolverMap: Record, configResolverMap: Record, resolverMap: Record) => { componentMap['form-group'] = FDynamicFormGroup; @@ -45,7 +47,7 @@ FResponseFormDesign.register = (componentMap: Record, propsResolver }; FResponseFormDesign.registerDesigner = (componentMap: Record, propsResolverMap: Record, configResolverMap: Record) => { - componentMap['form-group'] = FDynamicFromGroupDesign; + componentMap['form-group'] = FDynamicFormGroupDesign; propsResolverMap['form-group'] = formGroupPropsResolver; componentMap['response-form'] = FResponseFormDesign; propsResolverMap['response-form'] = formDesignPropsResolver; diff --git a/packages/ui-vue/components/lookup/src/designer/lookup.design.component.tsx b/packages/ui-vue/components/lookup/src/designer/lookup.design.component.tsx index f753cb2f5..c6d09c14c 100644 --- a/packages/ui-vue/components/lookup/src/designer/lookup.design.component.tsx +++ b/packages/ui-vue/components/lookup/src/designer/lookup.design.component.tsx @@ -47,7 +47,7 @@ export default defineComponent({ return () => { return ( -
+
{ const classObject = { + 'f-cmp-inputgroup': true, + 'input-group-append': true, 'btn-group': true, 'btn-group-number': true } as Record; @@ -46,25 +48,25 @@ export default function (props: any) { return () => { return ( <> - { !!props.showButton &&
- - -
} - { - props.expandable && props.expandContent &&
-
-
- } + {!!props.showButton &&
+ + +
} + { + props.expandable && props.expandContent &&
+
+
+ } ); }; diff --git a/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts b/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts index fe93fd9be..a04e89bd2 100644 --- a/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts +++ b/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts @@ -300,7 +300,7 @@ export class InputBaseProperty extends BaseControlProperty { /** * 获取可选的编辑器类型 */ - private getAvailableEditorType(propertyData: any) { + public getAvailableEditorType(propertyData: any) { const canChangeControlType = this.checkCanChangeControlType(propertyData, this.viewModelId); if (!canChangeControlType) { return { diff --git a/packages/ui-vue/components/table/index.ts b/packages/ui-vue/components/table/index.ts index 78eef3a8f..7ce1d534d 100644 --- a/packages/ui-vue/components/table/index.ts +++ b/packages/ui-vue/components/table/index.ts @@ -14,26 +14,27 @@ * limitations under the License. */ import type { App } from 'vue'; -import Table from './src/table.component'; +import FTable from './src/table.component'; import TableDesign from './src/designer/table.design.component'; -import { propsResolver, propsResolverGenerator } from './src/table.props'; +import { propsResolverGenerator, TableCellPropsResolver } from './src/table.props'; import { RegisterContext } from '../common'; export * from './src/table.props'; -export { Table, TableDesign }; +export { FTable, TableDesign }; export default { install(app: App): void { - app.component(Table.name as string, Table); + app.component(FTable.name as string, FTable); }, register(componentMap: Record, propsResolverMap: Record, configResolverMap: Record, resolverMap: Record, registerContext: RegisterContext): void { - componentMap['table'] = Table; + componentMap['table'] = FTable; propsResolverMap['table'] = propsResolverGenerator(registerContext); }, registerDesigner(componentMap: Record, propsResolverMap: Record, configResolverMap: Record, registerContext: RegisterContext): void { componentMap['table'] = TableDesign; propsResolverMap['table'] = propsResolverGenerator(registerContext); + propsResolverMap['table-td'] = TableCellPropsResolver; } }; diff --git a/packages/ui-vue/components/table/src/components/context-menu.component.tsx b/packages/ui-vue/components/table/src/components/context-menu.component.tsx index 06fb7f4d4..5d208a648 100644 --- a/packages/ui-vue/components/table/src/components/context-menu.component.tsx +++ b/packages/ui-vue/components/table/src/components/context-menu.component.tsx @@ -9,10 +9,16 @@ export interface ContextMenuOption { onClick: () => void; } +export interface ContextMenuSeparator { + separator: true; +} + +export type ContextMenuItem = ContextMenuOption | ContextMenuSeparator; + export interface ContextMenuProps { visible: boolean; position: { x: number; y: number }; - options: ContextMenuOption[]; + options: ContextMenuItem[]; onClose: () => void; } @@ -23,7 +29,7 @@ export const contextMenuProps = { default: () => ({ x: 0, y: 0 }) }, options: { - type: Array as () => ContextMenuOption[], + type: Array as () => ContextMenuItem[], default: () => [] }, onClose: { type: Function, required: true } @@ -38,39 +44,30 @@ export default defineComponent({ const hoveredId = ref(null); const adjustPosition = () => { - if (!menuRef.value || !props.visible) { - return; + if (!menuRef.value) return; + const menu = menuRef.value; + const rect = menu.getBoundingClientRect(); + const vpW = window.innerWidth; + const vpH = window.innerHeight; + + let { x, y } = props.position; + const padding = 4; + + // 右侧空间不足 → 翻转到锚点左侧 + if (x + rect.width > vpW) { + x = props.position.x - rect.width; } - nextTick(() => { - const menu = menuRef.value!; - const rect = menu.getBoundingClientRect(); - const viewport = { - width: window.innerWidth, - height: window.innerHeight - }; - - let { x, y } = props.position; - - if (x + rect.width > viewport.width) { - x = viewport.width - rect.width - 10; - } - - if (y + rect.height > viewport.height) { - y = viewport.height - rect.height - 10; - } + // 下方空间不足 → 翻转到锚点上方 + if (y + rect.height > vpH) { + y = props.position.y - rect.height; + } - x = Math.max(10, x); - y = Math.max(10, y); + // 兜底:不超出视口 + x = Math.max(padding, Math.min(x, vpW - rect.width - padding)); + y = Math.max(padding, Math.min(y, vpH - rect.height - padding)); - adjustedPosition.value = { x, y }; - }); - }; - - const handleClickOutside = (event: MouseEvent) => { - if (menuRef.value && !menuRef.value.contains(event.target as Node)) { - props.onClose(); - } + adjustedPosition.value = { x, y }; }; const handleKeyDown = (event: KeyboardEvent) => { @@ -86,23 +83,26 @@ export default defineComponent({ } }; + const handleOverlayClick = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + props.onClose(); + }; + onMounted(() => { - document.addEventListener('click', handleClickOutside); document.addEventListener('keydown', handleKeyDown); }); onBeforeUnmount(() => { - document.removeEventListener('click', handleClickOutside); document.removeEventListener('keydown', handleKeyDown); }); - // 该组件在设计组件中常驻挂载,setup 仅执行一次;用 watch 保证每次打开都按最新位置定位。 watch( () => [props.visible, props.position.x, props.position.y], () => { if (props.visible) { adjustedPosition.value = props.position; - adjustPosition(); + void nextTick(() => adjustPosition()); } }, { immediate: true } @@ -115,7 +115,7 @@ export default defineComponent({ return (
{ e.preventDefault(); e.stopPropagation(); }} >
e.stopPropagation()} > - {props.options.map((option) => ( -
handleOptionClick(option)} - onMouseenter={() => { hoveredId.value = option.id; }} - onMouseleave={() => { hoveredId.value = null; }} - > - {option.icon && ( - {option.icon} - )} - {option.label} -
- ))} + {props.options.map((item, index) => { + if ('separator' in item) { + return ( +
+ ); + } + const option = item as ContextMenuOption; + return ( +
handleOptionClick(option)} + onMouseenter={() => { hoveredId.value = option.id; }} + onMouseleave={() => { hoveredId.value = null; }} + > + {option.icon && ( + {option.icon} + )} + {option.label} +
+ ); + })}
); diff --git a/packages/ui-vue/components/table/src/composition/common.ts b/packages/ui-vue/components/table/src/composition/common.ts new file mode 100644 index 000000000..683fbb75f --- /dev/null +++ b/packages/ui-vue/components/table/src/composition/common.ts @@ -0,0 +1,57 @@ +export class TableUtils { + constructor( + private designViewModelUtils: any, + private formSchemaUtils: any, + private viewModelId: string) { } + /** + * 移除单元格内的绑定、表达式等 + */ + public removeDesignViewModelFieldInColumns(columns: any[]) { + const dgViewModel = this.designViewModelUtils.getDgViewModel(this.viewModelId); + if (!dgViewModel) { return; } + columns.forEach(co => { + if (co.binding && co.binding.field) { + const fieldId = co.binding.field; + dgViewModel.removeField([fieldId]); + this.removeExpression(co, fieldId); + } + }); + } + + /** + * 移除表达式 + */ + private removeExpression(cell: any, fieldId: string) { + const dom = this.formSchemaUtils; + const expressions = dom.getExpressions ? dom.getExpressions() : []; + if (!expressions || !expressions.length) { return; } + const expIndex = expressions.findIndex((e: any) => e.target === fieldId); + if (expIndex < 0) { return; } + (expressions[expIndex].rules || []).forEach((rule: any) => { + const propertyInSchema = rule.type === 'visible' ? cell[rule.type] : cell.editor?.[rule.type]; + const configured = ['visible', 'readonly', 'required'].includes(rule.type) + ? (propertyInSchema && propertyInSchema.type === 'Expression' && propertyInSchema.expressionId === rule.id) + : true; + if (configured && !this.checkExpressionOccupiedRepeated(fieldId, rule, cell.id)) { + rule.needDelete = true; + } + }); + expressions[expIndex].rules = (expressions[expIndex].rules || []).filter((rule: any) => !rule.needDelete); + } + + /** References data-grid: checks if expression is reused by other controls before deletion. */ + private checkExpressionOccupiedRepeated(bindingFieldId: string, rule: any, cellId: string): boolean { + const dom = this.formSchemaUtils; + if (!dom.getComponents || !dom.getControlsInCmpWidthBinding) { return false; } + const target = dom.getComponents().find((component: any) => { + const controls = dom.getControlsInCmpWidthBinding(component.viewModel, bindingFieldId) + .filter((control: any) => { + if (control.id === cellId) { return false; } + const propertyInSchema = rule.type === 'visible' ? control[rule.type] : control.editor?.[rule.type]; + return propertyInSchema && propertyInSchema.type === 'Expression' && propertyInSchema.expressionId === rule.id; + }); + return controls.length > 0; + }); + return !!target; + } +} \ No newline at end of file diff --git a/packages/ui-vue/components/table/src/composition/td-factory.ts b/packages/ui-vue/components/table/src/composition/td-factory.ts index 9cdeb71f4..11fd1b74b 100644 --- a/packages/ui-vue/components/table/src/composition/td-factory.ts +++ b/packages/ui-vue/components/table/src/composition/td-factory.ts @@ -9,10 +9,11 @@ export function generateId(): string { export function createTableTdMetadata(): TableCell { return { id: generateId(), - type: 'TableTd', + type: 'table-td', tdType: 'editor', staticText: { text: '', require: false }, - editor: { type: null, binding: null }, + editor: { type: null }, + binding: null, colspan: 1, rowspan: 1, align: 'right', @@ -28,7 +29,7 @@ export function createDefaultTableRows(rowCount: number, colCount: number): Tabl for (let r = 0; r < rowCount; r++) { const columns: TableCell[] = []; for (let c = 0; c < colCount; c++) { columns.push(createTableTdMetadata()); } - rows.push({ id: generateId(), type: 'TableRow', columns }); + rows.push({ id: generateId(), type: 'table-row', columns }); } return rows; } diff --git a/packages/ui-vue/components/table/src/composition/td-helper.ts b/packages/ui-vue/components/table/src/composition/td-helper.ts index ec9836d5c..d6e404779 100644 --- a/packages/ui-vue/components/table/src/composition/td-helper.ts +++ b/packages/ui-vue/components/table/src/composition/td-helper.ts @@ -74,7 +74,7 @@ export class TableTdWidgetHelper { n.push(tdMetadata); } } - rows.splice(t, 0, { id: generateId(), type: 'TableRow', columns: n }); + rows.splice(t, 0, { id: generateId(), type: 'table-row', columns: n }); for (let e = 0; e < rows[t].columns.length; e++) { if (rows[t].columns[e].invisible) { diff --git a/packages/ui-vue/components/table/src/composition/td-manager.ts b/packages/ui-vue/components/table/src/composition/td-manager.ts index 106fa8e51..474b318bf 100644 --- a/packages/ui-vue/components/table/src/composition/td-manager.ts +++ b/packages/ui-vue/components/table/src/composition/td-manager.ts @@ -2,6 +2,7 @@ import { FMessageBoxService } from '@farris/ui-vue/components/message-box'; import { TableRow } from './types'; import { TableTdWidgetHelper } from './td-helper'; import { ContextMenuOption } from '../components/context-menu.component'; +import { TableUtils } from './common'; /** Services needed by TableTdManager (ported from Angular DI). */ export interface TableTdManagerServices { @@ -28,17 +29,21 @@ export class TableTdManager { selectedRowIndex = 0; targetColumn: any; targetRow: any; - viewModelId = ''; menuConfig: MenuConfig = []; + private tableUtils: TableUtils; + constructor( public component: TableTdManagerComponent, private services: TableTdManagerServices, /** redrawTable.next replacement; payload may contain { clearPropertyPanel } */ private redraw: (payload?: any) => void, /** refreshSchamaTree.next replacement */ - private emitCanvasChanged: () => void - ) { } + private emitCanvasChanged: () => void, + private viewModelId: string + ) { + this.tableUtils = new TableUtils(services.designViewModelUtils, services.formSchemaUtils, this.viewModelId); + } /** Called by design component on right-click / widget-panel icon click to set the current target cell. */ setTarget(rowIndex: number, colIndex: number) { @@ -114,7 +119,7 @@ export class TableTdManager { const rightColumn = this.component.rows[this.selectedRowIndex].columns[o]; if (rightColumn.editor && rightColumn.editor.type) { this.question('合并后将重置右侧单元格的编辑器,确定合并?', () => { - this.removeDesignViewModelFieldInColumns([rightColumn]); + this.tableUtils.removeDesignViewModelFieldInColumns([rightColumn]); this.confirmToMergeColumnToRight(rightColumn, targetColspan); }); } else { @@ -147,7 +152,7 @@ export class TableTdManager { const bottomColumn = this.component.rows[o].columns[this.selectedColumnIndex]; if (bottomColumn.editor && bottomColumn.editor.type) { this.question('合并后将重置下方单元格的编辑器,确定合并?', () => { - this.removeDesignViewModelFieldInColumns([bottomColumn]); + this.tableUtils.removeDesignViewModelFieldInColumns([bottomColumn]); this.confirmToMergeRowToBottom(bottomColumn, targetRowspan); }); } else { @@ -168,58 +173,6 @@ export class TableTdManager { column.editor = { type: null, binding: null }; } - // ===== Field cleanup (removeField is REQUIRED; expression removal from data-grid pattern) ===== - removeDesignViewModelFieldInColumns(columns: any[]) { - const dgViewModel = this.services.designViewModelUtils?.getDgViewModel?.(this.viewModelId); - if (!dgViewModel) { return; } - columns.forEach(co => { - if (co.editor && co.editor.binding && co.editor.binding.field) { - const fieldId = co.editor.binding.field; - dgViewModel.removeField([fieldId]); - this.removeExpression(co, fieldId); - // BusinessControlResolver / ProcessControlledRuleUtil: not applicable in Vue → skip - } - }); - } - - /** - * References data-grid `grid-field-editor.component.tsx` removeExpression pattern: - * uses formSchemaUtils.getExpressions() keyed by .target, deduplicates via checkExpressionOccupiedRepeated. - */ - private removeExpression(cell: any, fieldId: string) { - const dom = this.services.formSchemaUtils; - const expressions = dom.getExpressions ? dom.getExpressions() : []; - if (!expressions || !expressions.length) { return; } - const expIndex = expressions.findIndex((e: any) => e.target === fieldId); - if (expIndex < 0) { return; } - (expressions[expIndex].rules || []).forEach((rule: any) => { - const propertyInSchema = rule.type === 'visible' ? cell[rule.type] : cell.editor?.[rule.type]; - const configured = ['visible', 'readonly', 'required'].includes(rule.type) - ? (propertyInSchema && propertyInSchema.type === 'Expression' && propertyInSchema.expressionId === rule.id) - : true; - if (configured && !this.checkExpressionOccupiedRepeated(fieldId, rule, cell.id)) { - rule.needDelete = true; - } - }); - expressions[expIndex].rules = (expressions[expIndex].rules || []).filter((rule: any) => !rule.needDelete); - } - - /** References data-grid: checks if expression is reused by other controls before deletion. */ - private checkExpressionOccupiedRepeated(bindingFieldId: string, rule: any, cellId: string): boolean { - const dom = this.services.formSchemaUtils; - if (!dom.getComponents || !dom.getControlsInCmpWidthBinding) { return false; } - const target = dom.getComponents().find((component: any) => { - const controls = dom.getControlsInCmpWidthBinding(component.viewModel, bindingFieldId) - .filter((control: any) => { - if (control.id === cellId) { return false; } - const propertyInSchema = rule.type === 'visible' ? control[rule.type] : control.editor?.[rule.type]; - return propertyInSchema && propertyInSchema.type === 'Expression' && propertyInSchema.expressionId === rule.id; - }); - return controls.length > 0; - }); - return !!target; - } - // ===== Split operations ===== showSplitColumns(): boolean { return this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan > 1; @@ -288,14 +241,14 @@ export class TableTdManager { deletedColumns.push(rows[rowIndex].columns[this.selectedColumnIndex]); rows[rowIndex].columns.splice(this.selectedColumnIndex, 1); } - this.removeDesignViewModelFieldInColumns(deletedColumns); + this.tableUtils.removeDesignViewModelFieldInColumns(deletedColumns); this.redraw({ clearPropertyPanel: true }); this.emitCanvasChanged(); } checkIfHasEditorInColumn(): boolean { for (const row of this.component.rows) { const column = row.columns[this.selectedColumnIndex]; - if (column && column.tdType === 'editor' && column.editor && column.editor.binding) { return true; } + if (column && column.tdType === 'editor' && column.binding) { return true; } } return false; } @@ -318,7 +271,7 @@ export class TableTdManager { } checkIfHasEditorInRow(): boolean { for (const column of this.component.rows[this.selectedRowIndex].columns) { - if (column && column.tdType === 'editor' && column.editor && column.editor.binding) { return true; } + if (column && column.tdType === 'editor' && column.binding) { return true; } } return false; } @@ -341,21 +294,26 @@ export class TableTdManager { } } } - this.removeDesignViewModelFieldInColumns(rows[this.selectedRowIndex].columns); + this.tableUtils.removeDesignViewModelFieldInColumns(rows[this.selectedRowIndex].columns); rows.splice(this.selectedRowIndex, 1); this.redraw({ clearPropertyPanel: true }); this.emitCanvasChanged(); } } -/** Maps the ported menuConfig to FTableContextMenu options (filters out '-' separators). */ -export function menuConfigToOptions(menu: MenuConfig, onAfter: () => void): ContextMenuOption[] { - return menu - .filter((m): m is MenuItem => m !== '-') - .map(item => ({ +export type { ContextMenuOption }; + +/** Maps the ported menuConfig to context menu items, preserving separators. */ +export function menuConfigToOptions(menu: MenuConfig, onAfter: () => void): (ContextMenuOption | { separator: true })[] { + return menu.map(item => { + if (item === '-') { + return { separator: true }; + } + return { id: item.id, label: item.title, disabled: !!item.disable, onClick: () => { if (item.handle) { item.handle(); } onAfter(); } - })); + }; + }); } diff --git a/packages/ui-vue/components/table/src/composition/types.ts b/packages/ui-vue/components/table/src/composition/types.ts index fe2f562b0..c8af8462c 100644 --- a/packages/ui-vue/components/table/src/composition/types.ts +++ b/packages/ui-vue/components/table/src/composition/types.ts @@ -1,12 +1,13 @@ export interface TableCellStaticText { text: string; require: boolean; } -export interface TableCellEditor { type: string | null; binding: any; id?: string; } +export interface TableCellEditor { type: string | null; id?: string; } export interface TableCell { id: string; - type: string; // Angular 用 'TableTd' + type: string; tdType: 'editor' | 'staticText'; staticText: TableCellStaticText; editor: TableCellEditor; + binding: any, colspan: number; rowspan: number; align: 'left' | 'center' | 'right'; @@ -32,7 +33,6 @@ export interface TableSchema { rows: TableRow[]; border: { width: number; enableColor: boolean; color: string; }; font: { size: number; enableColor: boolean; color: string; family: string; }; - size: { width?: number; height?: number } | null; appearance: { class: string; style: string; }; visible: boolean; -} +} \ No newline at end of file diff --git a/packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx b/packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx index 09ce7b034..fce540aa4 100644 --- a/packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx +++ b/packages/ui-vue/components/table/src/composition/use-table-cell-render.tsx @@ -1,13 +1,18 @@ import { FDynamicFormGroup } from '@farris/ui-vue/components/dynamic-form'; +import { FDynamicFormGroupDesign } from '@farris/ui-vue/components/dynamic-form/designer'; import { TableCell } from './types'; -export function getTableStyle(props: any): Record { +export function getTableStyle(props: any, isInDesignerCanvas = false): Record { const style: Record = { 'box-shadow': '0px 0px 0px 1px #eaecf3' }; + if (isInDesignerCanvas) { + Object.assign(style, { width: 'inherit', height: 'inherit', background: 'inherit', 'margin-bottom': 0 }); + } if (props.size?.height) { style.height = `${props.size.height}px`; } + if (props.size?.width) { style.width = `${props.size.width}px`; } return style; } -export function getCellStyle(props: any, cell: TableCell): Record { +export function getCellStyle(props: any, cell: TableCell, isClicked = false): Record { const b = props.border || {}; const f = props.font || {}; const style: Record = { @@ -24,6 +29,12 @@ export function getCellStyle(props: any, cell: TableCell): Record { if (k && v) { style[k.trim()] = v.trim(); } }); } + + // 设计时,选中状态下的单元格需要设置边框宽度,以便于显示蓝色边框 + if (isClicked) { + const borderWidth = props.border?.width || 1; + style['border-width'] = `${borderWidth + 1}px !important`; + } return style; } @@ -44,12 +55,14 @@ export function renderEditorCell( cell: TableCell, opts: { design?: boolean; data?: Record; onChange?: (field: string, value: any) => void } ) { - const field = cell.editor?.binding?.field; + const field = cell.binding?.field; if (!field) { - return 未绑定字段; + return null; } if (opts.design) { - return ; + const editorInDesign = Object.assign({}, cell.editor, { required: false }); + return ; } const data = opts.data || {}; return ( @@ -57,7 +70,7 @@ export function renderEditorCell( id={cell.id} key={cell.id} showLabel={false} - label={cell.editor?.binding?.label || ''} + customClass="h-100" editor={cell.editor} required={(cell.editor as any)?.required} v-model={data[field]} diff --git a/packages/ui-vue/components/table/src/designer/table.design.component.tsx b/packages/ui-vue/components/table/src/designer/table.design.component.tsx index 4e33e46da..bf504a01c 100644 --- a/packages/ui-vue/components/table/src/designer/table.design.component.tsx +++ b/packages/ui-vue/components/table/src/designer/table.design.component.tsx @@ -1,4 +1,4 @@ -import { SetupContext, defineComponent, inject, onMounted, ref, computed, triggerRef } from 'vue'; +import { SetupContext, defineComponent, inject, onMounted, onBeforeUnmount, ref, computed, triggerRef } from 'vue'; import { getCustomClass } from '@farris/ui-vue/components/common'; import { DesignerItemContext, useDesignerComponent, DesignerHostService, canvasChanged @@ -7,7 +7,7 @@ import { tableProps, TablePropsType } from '../table.props'; import { TableCell, TableRow, TableSchema } from '../composition/types'; import { TableTdManager, menuConfigToOptions } from '../composition/td-manager'; import { getTableStyle, getCellStyle, renderLabelCell, renderEditorCell } from '../composition/use-table-cell-render'; -import FTableContextMenu, { ContextMenuOption } from '../components/context-menu.component'; +import FTableContextMenu, { ContextMenuItem } from '../components/context-menu.component'; import { useDesignerRulesForTable } from './use-designer-rules'; import { TableTdProperty } from '../property-config/table-td.property-config'; @@ -19,8 +19,8 @@ export default defineComponent({ const elementRef = ref(); const designerHostService = inject('designer-host-service'); const designItemContext = inject('design-item-context') as DesignerItemContext; - const designViewModelUtils = inject('designViewModelUtils') as any; - const formSchemaUtils = (inject('useFormSchema') as any) || (designerHostService as any)?.formSchemaUtils; + const designViewModelUtils = designerHostService?.designViewModelUtils + const formSchemaUtils = designerHostService?.formSchemaUtils; const rules = useDesignerRulesForTable(designItemContext, designerHostService); const componentInstance = useDesignerComponent(elementRef, designItemContext, rules); @@ -32,22 +32,26 @@ export default defineComponent({ const menuVisible = ref(false); const menuPosition = ref({ x: 0, y: 0 }); - const menuOptions = ref([]); + const menuOptions = ref([]); function refresh(payload?: any) { triggerRef(rowsRef); canvasChanged.value++; if (payload && payload.clearPropertyPanel) { selectedCellId.value = ''; } + if (rules.setComponentBasicInfoMap) { + rules.setComponentBasicInfoMap(); + } } const componentId = (componentInstance.value as any).belongedComponentId || props.componentId; + const viewModelId = formSchemaUtils?.getViewModelIdByComponentId?.(componentId) || ''; const tdManager = new TableTdManager( { id: schema.id, rows: schema.rows }, { formSchemaUtils, designViewModelUtils }, (payload?: any) => refresh(payload), - () => (designItemContext as any).setupContext?.emit('canvasChanged') + () => (designItemContext as any).setupContext?.emit('canvasChanged'), + viewModelId ); - tdManager.viewModelId = formSchemaUtils?.getViewModelIdByComponentId?.(componentId) || ''; function buildCellComponentInstance(cell: TableCell) { const cid = (componentInstance.value as any).belongedComponentId || props.componentId; @@ -57,16 +61,47 @@ export default defineComponent({ onPropertyChanged: () => refresh() }; } + /** 清除设计器已有的选中状态 */ + function clearCanvasSelection() { + const panel = document.querySelector('.editorPanel'); + if (!panel) return; + panel.querySelectorAll('.dgComponentSelected').forEach( + (el: Element) => el.classList.remove('dgComponentSelected') + ); + panel.querySelectorAll('.dgComponentFocused').forEach( + (el: Element) => el.classList.remove('dgComponentFocused') + ); + } + + function isCellSelected(cell: TableCell): boolean { + return selectedCellId.value === cell.id; + } function onCellClick(evt: MouseEvent, cell: TableCell) { evt.stopPropagation(); - selectedCellId.value = cell.id; + evt.preventDefault(); + if (!isCellSelected(cell)) { + clearCanvasSelection(); + selectedCellId.value = cell.id; + } const cid = (componentInstance.value as any).belongedComponentId || props.componentId; (designItemContext as any).setupContext?.emit( 'selectionChange', 'table-td', cell, cid, buildCellComponentInstance(cell) ); } + function onCellMouseDown(evt: MouseEvent) { + evt.stopPropagation(); + } + + function onDocMouseDown(evt: MouseEvent) { + const panel = document.querySelector('.editorPanel'); + if (!panel || !panel.contains(evt.target as Node)) return; + if (elementRef.value && !elementRef.value.contains(evt.target as Node)) { + selectedCellId.value = ''; + } + } + function openMenuAt(x: number, y: number, r: number, c: number) { tdManager.setTarget(r, c); const closeMenu = () => { menuVisible.value = false; }; @@ -75,12 +110,6 @@ export default defineComponent({ menuVisible.value = true; } - function onCellContextMenu(evt: MouseEvent, r: number, c: number) { - evt.preventDefault(); - evt.stopPropagation(); - openMenuAt(evt.clientX, evt.clientY, r, c); - } - function onWidgetClick(evt: MouseEvent, cell: TableCell, r: number, c: number) { evt.preventDefault(); evt.stopPropagation(); @@ -91,6 +120,10 @@ export default defineComponent({ onMounted(() => { elementRef.value.componentInstance = componentInstance; + document.addEventListener('mousedown', onDocMouseDown); + }); + onBeforeUnmount(() => { + document.removeEventListener('mousedown', onDocMouseDown); }); context.expose(componentInstance.value); @@ -100,8 +133,8 @@ export default defineComponent({ const rows = rowsRef.value; return (
- + data-dragref={`${designItemContext.schema.id}-container`}> +
{rows.map((row, r) => ( @@ -110,19 +143,21 @@ export default defineComponent({ colspan={cell.colspan} rowspan={cell.rowspan} class={[cell.tdType === 'staticText' ? 'farris-group-wrap--label' : 'farris-group-wrap--input', - cell.appearance?.class, - { 'f-table-td-selected': selectedCellId.value === cell.id }]} - style={{ ...getCellStyle(props, cell), position: 'relative' }} + cell.appearance?.class, + { 'dgComponentSelected': selectedCellId.value === cell.id }]} + style={{ ...getCellStyle(props, cell, selectedCellId.value === cell.id), position: 'relative' }} onClick={(e: MouseEvent) => onCellClick(e, cell)} - onContextmenu={(e: MouseEvent) => onCellContextMenu(e, r, c)}> + onMousedown={(e: MouseEvent) => onCellMouseDown(e)}>
{cell.tdType === 'staticText' ? renderLabelCell(cell) : renderEditorCell(cell, { design: true })} -
onWidgetClick(e, cell, r, c)}> - -
+ {selectedCellId.value === cell.id && ( +
onWidgetClick(e, cell, r, c)}> + +
+ )}
))} diff --git a/packages/ui-vue/components/table/src/designer/use-designer-rules.ts b/packages/ui-vue/components/table/src/designer/use-designer-rules.ts index 34365714e..db67b1abb 100644 --- a/packages/ui-vue/components/table/src/designer/use-designer-rules.ts +++ b/packages/ui-vue/components/table/src/designer/use-designer-rules.ts @@ -1,6 +1,6 @@ import { DesignerHostService, DraggingResolveContext, UseDesignerRules, - ComponentSchema, DesignerItemContext + ComponentSchema, DesignerItemContext, DgControl } from '@farris/ui-vue/components/designer-canvas'; import { TableProperty } from '../property-config/table.property-config'; @@ -13,11 +13,56 @@ export function useDesignerRulesForTable( function canAccepts(_draggingContext: DraggingResolveContext): boolean { return false; } - function checkCanMoveComponent(): boolean { return true; } - function checkCanDeleteComponent(): boolean { return true; } + function checkCanMoveComponent(): boolean { return false; } + function checkCanDeleteComponent(): boolean { return false; } + + function hideNestedPaddingInDesginerView() { + return true; + } function getPropsConfig(componentId: string) { const componentProp = new TableProperty(componentId, designerHostService); return componentProp.getPropertyConfig(designItemContext.schema); } - return { canAccepts, checkCanMoveComponent, checkCanDeleteComponent, getPropsConfig }; + function getStyles() { + return 'border:0'; + } + /** + * 配置表格的路径信息,用于事件交互面板显示“已有方法”的事件路径 + */ + function setComponentBasicInfoMap() { + if (!designerHostService || !designItemContext) { + return; + } + const { formSchemaUtils } = designerHostService; + + const controlBasicInfoMap = formSchemaUtils.getControlBasicInfoMap(); + const tableName = DgControl['table'].name; + const tableTdName = DgControl['table-td'].name; + + // 单元格的展示名称和路径 + if (schema.rows?.length) { + schema.rows.forEach(row => { + if (row.columns?.length) { + row.columns.forEach(cell => { + if (cell.tdType === 'editor' && cell.editor.type) { + const editorLabel = cell.editor.label || '单元格'; + controlBasicInfoMap.set(cell.id, { + showName: editorLabel, + parentPathName: `${tableName} > ${tableTdName} > ${editorLabel}` + }); + } + }) + } + }) + } + } + return { + canAccepts, + checkCanMoveComponent, + checkCanDeleteComponent, + getPropsConfig, + setComponentBasicInfoMap, + hideNestedPaddingInDesginerView, + getStyles + }; } diff --git a/packages/ui-vue/components/table/src/property-config/converter/border-converter.ts b/packages/ui-vue/components/table/src/property-config/converter/border-converter.ts new file mode 100644 index 000000000..bd1f6b212 --- /dev/null +++ b/packages/ui-vue/components/table/src/property-config/converter/border-converter.ts @@ -0,0 +1,13 @@ +import { ComponentSchema, PropertyConverter, SchemaService } from "@farris/ui-vue/components/designer"; + +export default { + convertTo: (schema: ComponentSchema, propertyKey: string, propertyValue: any, schemaService: SchemaService) => { + if (!schema.border) { + schema.border = {}; + } + schema.border[propertyKey] = propertyValue; + }, + convertFrom: (schema: ComponentSchema, propertyKey: string, schemaService: SchemaService) => { + return schema.border ? schema.border[propertyKey] : schema[propertyKey]; + } +} as PropertyConverter; diff --git a/packages/ui-vue/components/table/src/property-config/converter/font-converter.ts b/packages/ui-vue/components/table/src/property-config/converter/font-converter.ts new file mode 100644 index 000000000..7c17d9a96 --- /dev/null +++ b/packages/ui-vue/components/table/src/property-config/converter/font-converter.ts @@ -0,0 +1,13 @@ +import { ComponentSchema, PropertyConverter, SchemaService } from "@farris/ui-vue/components/designer"; + +export default { + convertTo: (schema: ComponentSchema, propertyKey: string, propertyValue: any, schemaService: SchemaService) => { + if (!schema.font) { + schema.font = {}; + } + schema.font[propertyKey] = propertyValue; + }, + convertFrom: (schema: ComponentSchema, propertyKey: string, schemaService: SchemaService) => { + return schema.font ? schema.font[propertyKey] : schema[propertyKey]; + } +} as PropertyConverter; diff --git a/packages/ui-vue/components/table/src/property-config/converter/static-text.converter.ts b/packages/ui-vue/components/table/src/property-config/converter/static-text.converter.ts new file mode 100644 index 000000000..120c08ecb --- /dev/null +++ b/packages/ui-vue/components/table/src/property-config/converter/static-text.converter.ts @@ -0,0 +1,13 @@ +import { ComponentSchema, PropertyConverter, SchemaService } from "@farris/ui-vue/components/designer"; + +export default { + convertTo: (schema: ComponentSchema, propertyKey: string, propertyValue: any, schemaService: SchemaService) => { + if (!schema.staticText) { + schema.staticText = {}; + } + schema.staticText[propertyKey] = propertyValue; + }, + convertFrom: (schema: ComponentSchema, propertyKey: string, schemaService: SchemaService) => { + return schema.staticText ? schema.staticText[propertyKey] : schema[propertyKey]; + } +} as PropertyConverter; diff --git a/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts index ed66b7fdb..ddebe5a9d 100644 --- a/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts +++ b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts @@ -1,56 +1,432 @@ -import { BaseControlProperty } from '../../../property-panel/src/composition/entity/base-property'; +import { FormBindingType, FormPropertyChangeObject, SchemaDOMMapping } from '@farris/ui-vue/components/property-panel'; +import { InputBaseProperty } from '../../../property-panel/src/composition/entity/input-base-property'; +import staticTextConverter from './converter/static-text.converter'; +import { canvasChanged, DesignerComponentInstance } from '@farris/ui-vue/components/designer-canvas'; +import { AvatarProperty } from '@farris/ui-vue/components/avatar/src/property-config/avatar.property-config'; +import { CheckGroupProperty } from '@farris/ui-vue/components/checkbox-group/src/property-config/check-group.property-config'; +import { CheckBoxProperty } from '@farris/ui-vue/components/checkbox/src/property-config/checkbox.property-config'; +import { ComboListProperty } from '@farris/ui-vue/components/combo-list/src/property-config/combo-list.property-config'; +import { DatePickerProperty } from '@farris/ui-vue/components/date-picker/src/property-config/date-picker.property-config'; +import { InputGroupProperty } from '@farris/ui-vue/components/input-group/src/property-config/input-group.property-config'; +import { LanuageTextBoxPropertyConfig } from '@farris/ui-vue/components/language-textbox/src/property-config/language-textbox.property-config'; +import { LookupPropertyConfig } from '@farris/ui-vue/components/lookup/src/property-config/lookup.property-config'; +import { NumberRangeProperty } from '@farris/ui-vue/components/number-range/src/property-config/number-range.property-config'; +import { NumberSpinnerProperty } from '@farris/ui-vue/components/number-spinner/src/property-config/number-spinner.property-config'; +import { RadioGroupProperty } from '@farris/ui-vue/components/radio-group/src/property-config/radio-group.property-config'; +import { SwitchProperty } from '@farris/ui-vue/components/switch/src/property-config/switch.property-config'; +import { TextareaProperty } from '@farris/ui-vue/components/textarea/src/property-config/textarea.property-config'; +import { TimePickerProperty } from '@farris/ui-vue/components/time-picker/src/property-config/time-picker.property-config'; +import { TableUtils } from '../composition/common'; +import { property } from 'lodash'; + +export class TableTdProperty extends InputBaseProperty { + /** 公共操作类 */ + private tableUtils: TableUtils; + /** 单元格编辑器的属性配置 */ + private editorTypeConfig: any; -export class TableTdProperty extends BaseControlProperty { constructor(componentId: string, designerHostService: any) { super(componentId, designerHostService); + this.tableUtils = new TableUtils(designerHostService.designViewModelUtils, designerHostService.formSchemaUtils, this.viewModelId); + } + + public getPropertyConfig(cell: any, componentInstance?: DesignerComponentInstance) { + this.setDesignViewModelField(cell); + this.getBasicTdPropConfig(cell); + this.propertyConfig.categories['appearance'] = this.getAppearanceConfig(cell); + this.getTdTypePropConfig(cell); + this.getStaticTdPropConfig(cell); + this.getEditorBindingAndTypePropConfig(cell); + this.getFieldEditorPropConfig(cell); + this.getExpandProperties(cell); + this.getEventPropConfig(cell); + + return this.propertyConfig; + } + + public setDesignViewModelField(propertyData: any) { + const bindingFieldId = propertyData.binding?.type === 'Form' && propertyData.binding?.field; + // 视图模型中[字段更新时机]属性现在要在控件上维护,所以在控件上复制一份属性值 + if (bindingFieldId) { + if (!this.designViewModelField) { + const dgViewModel = this.designViewModelUtils.getDgViewModel(this.viewModelId); + this.designViewModelField = dgViewModel.fields.find(f => f.id === bindingFieldId); + } + propertyData.updateOn = this.designViewModelField?.updateOn; + } } - public getPropertyConfig(cell: any) { + + private getBasicTdPropConfig(cell: any) { this.propertyConfig.categories['basic'] = { - title: '基本信息', description: '', + title: '基本信息', + description: '', + properties: { + id: { + title: '标识', + type: 'string', + readonly: true + } + } + }; + } + /** + * 获取单元格类型属性配置 + */ + private getTdTypePropConfig(cell: any) { + const self = this; + this.propertyConfig.categories['tdEditor'] = { + title: '单元格类型', + tabId: 'tdEditor', + tabName: '编辑器', properties: { - id: { title: '标识', type: 'string', readonly: true }, tdType: { - title: '单元格类型', type: 'select', + title: '单元格类型', + type: 'select', editor: { type: 'combo-list', textField: 'name', valueField: 'value', idField: 'value', editable: false, data: [{ value: 'staticText', name: '静态文本' }, { value: 'editor', name: '编辑器' }] - } + }, + refreshPanelAfterChanged: true + } + }, + setPropertyRelates(changeObject: FormPropertyChangeObject, propData, parameters) { + cell.staticText = { + text: '', + require: false + }; + if (cell.binding) { + // 清除字段绑定 + self.tableUtils.removeDesignViewModelFieldInColumns([cell]); } + cell.editor = { + type: null, + binding: null + }; + cell.editorLabel = undefined; + + // 切换单元格样式 + if (!cell.appearance) { + cell.appearance = { class: '' }; + } + cell.appearance.class = cell.appearance.class.replace('farris-group-wrap--label', '').replace('farris-group-wrap--input', ''); + cell.appearance.class += changeObject.propertyValue === 'staticText' ? + ' farris-group-wrap--label' : ' farris-group-wrap--input'; } }; + } + /** + * 获取文本单元格属性配置 + */ + private getStaticTdPropConfig(cell: any) { this.propertyConfig.categories['staticText'] = { - title: '文本', description: '', - properties: { - 'staticText.text': { title: '文本内容', type: 'string', description: '' }, - 'staticText.require': { title: '必填标识', type: 'boolean', description: '' } - } - }; - this.propertyConfig.categories['editor'] = { - title: '绑定', description: '', + title: '文本', + description: '', + hide: cell.tdType !== 'staticText', + $converter: staticTextConverter, + parentPropertyID: 'staticText', + tabId: 'tdEditor', + tabName: '编辑器', properties: { - 'editor.binding': { - title: '绑定字段', type: 'binding', - editor: { type: 'binding-selector' }, - description: '为单元格绑定实体字段' + text: { + title: '单元格文本', + type: 'string', + description: '' + }, + require: { + title: '是否显示必填图标', + type: 'boolean', + description: '' } } }; - this.propertyConfig.categories['align'] = { - title: '对齐', description: '', + } + /** + * 编辑器单元格绑定属性 + * @param propertyData 属性值 + * @param viewModelId Viewmodel id + */ + private getEditorBindingAndTypePropConfig(cell: any) { + const self = this; + + const { canChangeControlType, editorTypeList } = this.getAvailableEditorType(cell); + + this.propertyConfig.categories['tableTdEditor_editorBinding'] = { + title: '绑定', + description: '', + hide: cell.tdType !== 'editor', + tabId: 'tdEditor', + tabName: '编辑器', properties: { - align: { - title: '水平对齐', type: 'select', - editor: { type: 'combo-list', textField: 'name', valueField: 'value', idField: 'value', editable: false, - data: [{ value: 'left', name: '左' }, { value: 'center', name: '中' }, { value: 'right', name: '右' }] } + binding: { + description: "绑定字段", + title: "绑定", + editor: { + type: "binding-selector", + bindingType: { "enable": true }, + title: "绑定编辑器", + editorParams: { + componentSchema: cell, + needSyncToViewModel: true, + viewModelId: this.viewModelId, + designerHostService: this.designerHostService, + disableOccupiedFields: true, + enableClear: true, + canChangeFieldType: true + }, + textField: 'bindingField' + }, + refreshPanelAfterChanged: true, + readonly: this.formSchemaUtils.designerMode === 'PC_RTC' && !cell.isRtcControl + }, + type: { + title: '控件类型', + type: 'select', + description: '组件的类型', + $converter: '/converter/property-editor.converter', + editor: { + type: 'combo-list', + textField: 'value', + valueField: 'key', + idField: 'key', + editable: false, + data: editorTypeList + }, + readonly: !canChangeControlType, + visible: !!cell.binding?.field, + refreshPanelAfterChanged: true }, - valign: { - title: '垂直对齐', type: 'select', - editor: { type: 'combo-list', textField: 'name', valueField: 'value', idField: 'value', editable: false, - data: [{ value: 'top', name: '上' }, { value: 'middle', name: '中' }, { value: 'bottom', name: '下' }] } + editorLabel: { + title: '单元格标题', + type: 'string', + description: '单元格标题', + visible: !!(cell.binding?.field && cell.editorLabel) + } + }, + setPropertyRelates(changeObject: FormPropertyChangeObject, propData, parameters) { + switch (changeObject.propertyID) { + case 'binding': { + // 变更字段类型:修改原控件DOM结构 + self.changeControlAfterBindingChanged(changeObject, cell, parameters); + // 编辑器内自动为cell赋值Binding,但是td的binding是存在cell.editor.binding的,所以这里单独删除一份 + // delete cell.binding; + // 刷新实体树 + changeObject.needRefreshEntityTree = true; + break; + } + case 'type': { + if (cell.binding && changeObject.propertyValue) { + const bindingType = cell.binding.type; + + const newEditorInfo = self.createControl(bindingType, self.designViewModelField, changeObject.propertyValue, cell.editor); + if (newEditorInfo) { + cell.editor = newEditorInfo.editor; + } + // 触发刷新 + canvasChanged.value++; + } + + } } + } }; - this.propertyConfig.categories['appearance'] = this.getAppearanceConfig(cell); - return this.propertyConfig; + + } + + + private changeControlAfterBindingChanged(changeObject: FormPropertyChangeObject, propertyData: any, parameters: any) { + + // 清除绑定:触发界面更新 + if (!changeObject.propertyValue) { + propertyData.editorLabel = undefined; + propertyData.editor = { type: null, binding: null }; + return; + } + const fieldTypeChanged = parameters?.fieldTypeChanged + // 若变更字段类型,或新绑定字段,则走新建控件逻辑 + if (fieldTypeChanged || !propertyData.editor.type) { + const bindingType = changeObject.propertyValue.type; + const newEditorInfo = this.createControl(bindingType, parameters); + if (newEditorInfo) { + propertyData.editor = newEditorInfo.editor; + // 在table td上保存字段名称, 以便于交互面板显示事件路径 + propertyData.editorLabel = newEditorInfo.label; + } + + changeObject['controlEditorChanged'] = true; + } else { + // 没有变更字段类型:不需要清除原控件结构,只需要同步更新path属性和帮助相关属性 + // 没有变更字段类型,只同步单元格文本即可 + propertyData.editorLabel = parameters.name; + // this.updateControlDomAfterChangingBinding(changeObject, propertyData.editor, parameters, viewModelId); + } + + // 切换绑定信息后,需要使用新字段的受控规则 + + } + + private createControl(bindingType: string, selectedField: any, editorType?: string, oldEditor?: any) { + let newControl; + if (!bindingType) { + return; + } + + if (bindingType === FormBindingType.Form) { + newControl = this.controlCreatorUtils.setFormFieldProperty(selectedField, editorType); + } else { + // 暂不支持变量 + // newEditor = this.controlCreatorUtils.create(selectedField, editorType, this.componentId); + } + + + if (oldEditor) { + ['readonly', 'required', 'placeholder'].map((item) => { + if (Object.prototype.hasOwnProperty.call(oldEditor, item)) { + newControl.editor[item] = oldEditor[item]; + } + }); + } + + return { + editor: newControl.editor, + label: selectedField.name + }; + } + + /** + * 列编辑器属性 + * @param propertyData 列属性值 + * @param viewModelId viewModelId + */ + private getFieldEditorPropConfig(propertyData: any, showPosition = 'gridFieldEditor', isSimpleTable = false) { + if (propertyData.tdType !== 'editor' || !propertyData.binding?.field || !propertyData.editor?.type) { + return; + } + const editorType = this.formMetadataConverter.getRealEditorType(propertyData.editor.type); + this.editorTypeConfig = null; + + switch (editorType) { + case 'avatar': + this.editorTypeConfig = new AvatarProperty(this.componentId, this.designerHostService); + break; + case 'check-group': + this.editorTypeConfig = new CheckGroupProperty(this.componentId, this.designerHostService); + break; + case 'check-box': + this.editorTypeConfig = new CheckBoxProperty(this.componentId, this.designerHostService); + break; + case 'combo-list': + this.editorTypeConfig = new ComboListProperty(this.componentId, this.designerHostService); + break; + case 'date-picker': + this.editorTypeConfig = new DatePickerProperty(this.componentId, this.designerHostService); + break; + case 'lookup': + this.editorTypeConfig = new LookupPropertyConfig(this.componentId, this.designerHostService); + break; + case 'number-range': + this.editorTypeConfig = new NumberRangeProperty(this.componentId, this.designerHostService); + break; + case 'number-spinner': + this.editorTypeConfig = new NumberSpinnerProperty(this.componentId, this.designerHostService); + break; + case 'radio-group': + this.editorTypeConfig = new RadioGroupProperty(this.componentId, this.designerHostService); + break; + case 'switch': + this.editorTypeConfig = new SwitchProperty(this.componentId, this.designerHostService); + break; + case 'textarea': + this.editorTypeConfig = new TextareaProperty(this.componentId, this.designerHostService); + break; + case 'time-picker': + this.editorTypeConfig = new TimePickerProperty(this.componentId, this.designerHostService); + break; + case 'language-textbox': + this.editorTypeConfig = new LanuageTextBoxPropertyConfig(this.componentId, this.designerHostService); + break; + default: + this.editorTypeConfig = new InputGroupProperty(this.componentId, this.designerHostService); + } + + const self = this; + const notSupportProperty = ['id', 'type', 'class', 'style', 'binding', 'label', 'responseLayout', 'visible', 'fill', 'titleDataSource', 'titleSourceType']; + const categories = this.editorTypeConfig.getGridFieldEdtiorPropConfig(propertyData); + Object.keys(categories).map((categoryId: string) => { + const propertyCategory = categories[categoryId]; + const properties = {}; + Object.keys(propertyCategory.properties).map((propertyId) => { + // 此处属性还有visible和通过计算控制的情况--ToDo + if (notSupportProperty.findIndex((item) => item === propertyId) === -1 && propertyCategory.properties[propertyId] !== false) { + properties[propertyId] = propertyCategory.properties[propertyId]; + } + }); + if (Object.keys(properties).length !== 0) { + const title = propertyCategory?.title; + const tabId = 'tdEditor'; + const tabName = '编辑器'; + const description = propertyCategory?.description; + const $converter = propertyCategory?.$converter || ''; + self.propertyConfig.categories[showPosition + '_' + categoryId] = { + title, + hide: Object.prototype.hasOwnProperty.call(propertyCategory, 'hide') ? propertyCategory.hide : false, + hideTitle: Object.prototype.hasOwnProperty.call(propertyCategory, 'hideTitle') ? propertyCategory.hideTitle : false, + tabId: propertyCategory.tabId || tabId, + tabName: propertyCategory.tabName || tabName, + description, + $converter, + parentPropertyID: 'editor', + propertyData, + properties, + setPropertyRelates: propertyCategory?.setPropertyRelates + }; + } + }); + } + /** + * 组装事件类属性 + */ + private getEventPropConfig(cell: any) { + if (cell.tdType !== 'editor' || !cell.binding?.field || !cell.editor?.type || !this.editorTypeConfig) { + return; + } + const eventConfig = this.editorTypeConfig.getEventPropertyConfig(cell); + if (eventConfig) { + // 过滤掉 onClickLabel 事件 + const properties = eventConfig.properties; + if (properties) { + Object.values(properties).forEach((prop: any) => { + if (prop?.editor?.initialData?.getEventList) { + const originalGetEventList = prop.editor.initialData.getEventList; + prop.editor.initialData.getEventList = () => { + const originalResult = originalGetEventList(); + const filteredEvents = (originalResult.events || []).filter((item: any) => item.label !== 'onClickLabel'); + return { ...originalResult, events: filteredEvents }; + } + } + }); + } + this.propertyConfig.categories['eventsEditor'] = eventConfig; + } + } + + /** + * 扩展区域属性 + * @param cell + * @returns + */ + public getExpandProperties(cell: any) { + if (cell.tdType !== 'editor' || !cell.binding?.field || !cell.editor?.type) { + return; + } + const adaptableEditorTypes = ['time-picker', 'date-picker', 'lookup', 'combo-list', 'number-spinner', 'input-group', 'language-textbox']; + if (!adaptableEditorTypes.includes(cell.editor.type)) { + return; + } + const expandConfig = super.getExpandProperties(cell); + this.propertyConfig.categories['expandEditor'] = Object.assign({}, expandConfig, { + tabId: 'tdEditor', + tabName: '编辑器', + }); } } diff --git a/packages/ui-vue/components/table/src/property-config/table.property-config.ts b/packages/ui-vue/components/table/src/property-config/table.property-config.ts index 01ba9e68e..4c7d684ab 100644 --- a/packages/ui-vue/components/table/src/property-config/table.property-config.ts +++ b/packages/ui-vue/components/table/src/property-config/table.property-config.ts @@ -1,4 +1,6 @@ import { BaseControlProperty } from '../../../property-panel/src/composition/entity/base-property'; +import borderConverter from './converter/border-converter'; +import fontConverter from './converter/font-converter'; export class TableProperty extends BaseControlProperty { constructor(componentId: string, designerHostService: any) { @@ -9,20 +11,77 @@ export class TableProperty extends BaseControlProperty { this.propertyConfig.categories['appearance'] = this.getAppearanceConfig(propertyData); this.propertyConfig.categories['behavior'] = this.getBehaviorConfig(propertyData); this.propertyConfig.categories['border'] = { - title: '边框', description: '', + title: '边框', + $converter: borderConverter, properties: { - 'border.width': { title: '边框宽度', type: 'int', description: '' }, - 'border.enableColor': { title: '启用边框色', type: 'boolean', description: '' }, - 'border.color': { title: '边框颜色', type: 'string', editor: { type: 'color-picker' }, description: '' } + width: { + title: '边框宽度', + type: 'number', + editor: { + min: 0, + max: 10, + needValid: true, + precision: 0 + } + }, + enableColor: { + title: '启用自定义颜色', + type: 'boolean', + description: '', + refreshPanelAfterChanged: true + + }, + color: { + title: '自定义颜色', + type: 'string', + description: '统一设置表格边框颜色', + visible: propertyData.border?.enableColor + } } }; this.propertyConfig.categories['font'] = { - title: '字体', description: '', + title: '字体', + $converter: fontConverter, properties: { - 'font.size': { title: '字号', type: 'int', description: '' }, - 'font.enableColor': { title: '启用字体色', type: 'boolean', description: '' }, - 'font.color': { title: '字体颜色', type: 'string', editor: { type: 'color-picker' }, description: '' }, - 'font.family': { title: '字体', type: 'string', description: '' } + size: { + title: '字号', + type: 'number', + editor: { + min: 10, + max: 20, + needValid: true, + precision: 0 + } + }, + enableColor: { + title: '启用自定义颜色', + type: 'boolean', + description: '是否启用自定义颜色', + refreshPanelAfterChanged: true + }, + color: { + title: '自定义颜色', + type: 'string', + description: '设置字体框颜色', + visible: propertyData.font?.enableColor + }, + family: { + title: '字体', + type: 'select', + editor: { + type: 'combo-list', + textField: 'name', + valueField: 'value', + editable: false, + data: [ + { value: 'sans-serif', name: 'sans-serif' }, + { value: 'simsun', name: 'simsun' }, + { value: 'SimHei', name: 'SimHei' }, + { value: 'arial', name: 'arial' }, + { value: 'FangSong', name: 'FangSong' } + ] + } + } } }; return this.propertyConfig; diff --git a/packages/ui-vue/components/table/src/schema/table-td.schema.json b/packages/ui-vue/components/table/src/schema/table-td.schema.json new file mode 100644 index 000000000..cb9b54e8c --- /dev/null +++ b/packages/ui-vue/components/table/src/schema/table-td.schema.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://farris-design.gitee.io/table-td.schema.json", + "title": "table-td", + "description": "A Farris design-time layout table cell", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "default": "" + }, + "tdType": { + "type": "string", + "default": "staticText" + }, + "staticText": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "require": { + "type": "boolean" + } + }, + "default": { + "text": "", + "require": false + } + }, + "editor": { + "type": "object", + "properties": { + "type": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + } + }, + "default": { + "type": null + } + }, + "binding": { + "type": "object", + "default": {} + }, + "colspan": { + "type": "number", + "default": 1 + }, + "rowspan": { + "type": "number", + "default": 1 + }, + "align": { + "type": "string", + "default": "left" + }, + "valign": { + "type": "string", + "default": "middle" + }, + "appearance": { + "type": "object", + "properties": { + "class": { + "type": "string" + }, + "style": { + "type": "string" + } + }, + "default": { + "class": "", + "style": "" + } + }, + "size": { + "type": "object" + }, + "markRow": { + "type": "number", + "default": 0 + }, + "markCol": { + "type": "number", + "default": 0 + }, + "invisible": { + "type": "boolean", + "default": false + }, + "editorLabel": { + "type": "string", + "default": "" + } + }, + "required": [ + "id", + "type" + ], + "events": { + "onExpandClick": "扩展按钮点击事件", + "onClickLabel": "标签点击事件", + "fieldValueChanging": "绑定字段值变化前事件", + "fieldValueChanged": "绑定字段值变化后事件" + } +} \ No newline at end of file diff --git a/packages/ui-vue/components/table/src/schema/table.schema.json b/packages/ui-vue/components/table/src/schema/table.schema.json index b213b8b8d..b6fa750e8 100644 --- a/packages/ui-vue/components/table/src/schema/table.schema.json +++ b/packages/ui-vue/components/table/src/schema/table.schema.json @@ -5,40 +5,83 @@ "description": "A Farris design-time layout table", "type": "object", "properties": { - "id": { "type": "string" }, - "type": { "type": "string", "default": "table" }, - "devMode": { "type": "string", "default": "advanced" }, + "id": { + "type": "string" + }, + "type": { + "type": "string", + "default": "table" + }, + "devMode": { + "type": "string", + "default": "advanced" + }, "appearance": { "type": "object", - "properties": { "class": { "type": "string" }, "style": { "type": "string" } }, + "properties": { + "class": { + "type": "string" + }, + "style": { + "type": "string" + } + }, "default": {} }, "border": { "type": "object", "properties": { - "width": { "type": "number" }, - "enableColor": { "type": "boolean" }, - "color": { "type": "string" } + "width": { + "type": "number" + }, + "enableColor": { + "type": "boolean" + }, + "color": { + "type": "string" + } }, - "default": { "width": 1, "enableColor": false, "color": "#d4d6d9" } + "default": { + "width": 1, + "enableColor": false, + "color": "#eaecf3" + } }, "font": { "type": "object", "properties": { - "size": { "type": "number" }, - "enableColor": { "type": "boolean" }, - "color": { "type": "string" }, - "family": { "type": "string" } + "size": { + "type": "number" + }, + "enableColor": { + "type": "boolean" + }, + "color": { + "type": "string" + }, + "family": { + "type": "string" + } }, - "default": { "size": 12, "enableColor": false, "color": "#333", "family": "" } + "default": { + "size": 13, + "enableColor": false, + "color": "#2d2f33", + "family": "sans-serif" + } }, - "size": { - "type": "object", - "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, - "default": null + "rows": { + "type": "array", + "default": [] }, - "rows": { "type": "array", "default": [] }, - "visible": { "type": "boolean", "default": true } + "visible": { + "type": "boolean", + "default": true + } }, - "required": ["id", "type", "rows"] -} + "required": [ + "id", + "type", + "rows" + ] +} \ No newline at end of file diff --git a/packages/ui-vue/components/table/src/table.component.tsx b/packages/ui-vue/components/table/src/table.component.tsx index 26d44abc1..5d1c0d853 100644 --- a/packages/ui-vue/components/table/src/table.component.tsx +++ b/packages/ui-vue/components/table/src/table.component.tsx @@ -31,7 +31,7 @@ export default defineComponent({ if (!props.visible) { return null; } const rows = (props.rows as TableRow[]) || []; return ( -
+
{rows.map(row => ( diff --git a/packages/ui-vue/components/table/src/table.props.ts b/packages/ui-vue/components/table/src/table.props.ts index 7735fd31d..eab35f8b5 100644 --- a/packages/ui-vue/components/table/src/table.props.ts +++ b/packages/ui-vue/components/table/src/table.props.ts @@ -2,8 +2,11 @@ import { ExtractPropTypes, PropType } from 'vue'; import { createPropsResolver, getPropsResolverGenerator } from '@farris/ui-vue/components/dynamic-resolver'; import { schemaMapper } from './schema/schema-mapper'; import tableSchema from './schema/table.schema.json'; +import tableCellSchema from './schema/table-td.schema.json'; import { schemaResolver } from './schema/schema-resolver'; -import { TableRow } from './composition/types'; +import { TableCellEditor, TableCellStaticText, TableRow } from './composition/types'; + + export const tableProps = { id: { type: String, default: '' }, @@ -12,9 +15,8 @@ export const tableProps = { customStyle: { type: String, default: '' }, devMode: { type: String, default: 'advanced' }, rows: { type: Array as PropType, default: () => [] }, - border: { type: Object as PropType, default: () => ({ width: 1, enableColor: false, color: '#d4d6d9' }) }, - font: { type: Object as PropType, default: () => ({ size: 12, enableColor: false, color: '#333', family: '' }) }, - size: { type: Object as PropType, default: null }, + border: { type: Object as PropType, default: () => ({ width: 1, enableColor: false, color: '#eaecf3' }) }, + font: { type: Object as PropType, default: () => ({ size: 13, enableColor: false, color: '#2d2f33', family: 'sans-serif' }) }, visible: { type: Boolean, default: true }, modelValue: { type: Object as PropType>, default: () => ({}) } } as Record; @@ -23,3 +25,30 @@ export type TablePropsType = ExtractPropTypes; export const propsResolver = createPropsResolver(tableProps, tableSchema, schemaMapper, schemaResolver); export const propsResolverGenerator = getPropsResolverGenerator(tableProps, tableSchema, schemaMapper, schemaResolver); + + + +export const tableCellProps = { + id: { type: String, default: '' }, + type: { type: String, default: '' }, + tdType: { type: String as PropType<'editor' | 'staticText'>, default: 'staticText' }, + staticText: { type: Object as PropType, default: () => ({ text: '', require: false }) }, + editor: { type: Object as PropType, default: () => ({ type: null }) }, + binding: { type: Object as PropType, default: () => ({}) }, + colspan: { type: Number, default: 1 }, + rowspan: { type: Number, default: 1 }, + align: { type: String as PropType<'left' | 'center' | 'right'>, default: 'left' }, + valign: { type: String as PropType<'top' | 'middle' | 'bottom'>, default: 'middle' }, + appearance: { type: Object as PropType<{ class: string; style: string }>, default: () => ({ class: '', style: '' }) }, + size: { type: Object as PropType, default: null }, + markRow: { type: Number, default: 0 }, + markCol: { type: Number, default: 0 }, + invisible: { type: Boolean, default: false } +} as Record; + + +export type TableCellProps = ExtractPropTypes; +export const TableCellPropsResolver = createPropsResolver(tableCellProps, tableCellSchema, schemaMapper, schemaResolver); + + + diff --git a/packages/ui-vue/components/textarea/src/designer/textarea.design.component.tsx b/packages/ui-vue/components/textarea/src/designer/textarea.design.component.tsx index 0152c91a3..a6d329cc5 100644 --- a/packages/ui-vue/components/textarea/src/designer/textarea.design.component.tsx +++ b/packages/ui-vue/components/textarea/src/designer/textarea.design.component.tsx @@ -62,7 +62,7 @@ export default defineComponent({ }); context.expose(componentInstance.value); const textareaClass = computed(() => { - return getCustomClass({}, props.customClass); + return getCustomClass({ 'h-100': true }, props.customClass); }); return () => { return ( diff --git a/packages/ui-vue/components/time-picker/src/designer/time-picker.design.component.tsx b/packages/ui-vue/components/time-picker/src/designer/time-picker.design.component.tsx index e5e64413a..c4d5cbb25 100644 --- a/packages/ui-vue/components/time-picker/src/designer/time-picker.design.component.tsx +++ b/packages/ui-vue/components/time-picker/src/designer/time-picker.design.component.tsx @@ -26,7 +26,7 @@ export default defineComponent({ return () => { return ( -
+
>({ + name: '张三', + gender: 'Male', + birth: '1990-05-15', + telephone: '13800138000', + email: 'zhangsan@example.com' + }); + + const rows = ref([ + { + id: 'row-1', + type: 'TableRow', + columns: [ + { + id: 'cell-label-name', + type: 'TableTd', + tdType: 'staticText', + staticText: { text: '姓名', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--label', style: '' }, + size: null + }, + { + id: 'cell-editor-name', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: 'input-group', binding: { field: 'name', label: '姓名' } }, + colspan: 2, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null + } + ] + }, + { + id: 'row-2', + type: 'TableRow', + columns: [ + { + id: 'cell-label-gender', + type: 'TableTd', + tdType: 'staticText', + staticText: { text: '性别', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--label', style: '' }, + size: null + }, + { + id: 'cell-editor-gender', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: 'combo-list', binding: { field: 'gender', label: '性别' } }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null + }, + { + id: 'cell-label-birth', + type: 'TableTd', + tdType: 'staticText', + staticText: { text: '出生日期', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--label', style: '' }, + size: null + }, + { + id: 'cell-editor-birth', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: 'date-picker', binding: { field: 'birth', label: '出生日期' } }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null + } + ] + }, + { + id: 'row-3', + type: 'TableRow', + columns: [ + { + id: 'cell-label-phone', + type: 'TableTd', + tdType: 'staticText', + staticText: { text: '电话', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--label', style: '' }, + size: null + }, + { + id: 'cell-editor-phone', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: 'input-group', binding: { field: 'telephone', label: '电话' } }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null + }, + { + id: 'cell-label-email', + type: 'TableTd', + tdType: 'staticText', + staticText: { text: '邮箱', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--label', style: '' }, + size: null + }, + { + id: 'cell-editor-email', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: 'input-group', binding: { field: 'email', label: '邮箱' } }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null + } + ] + }, + { + id: 'row-4', + type: 'TableRow', + columns: [ + { + id: 'cell-label-remark', + type: 'TableTd', + tdType: 'staticText', + staticText: { text: '备注', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--label', style: '' }, + size: null + }, + { + id: 'cell-editor-remark', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: 'input-group', binding: { field: 'remark', label: '备注' } }, + colspan: 3, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null + }, + { + id: 'cell-editor-remark-inv1', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null, + invisible: true, + markCol: 1, + markRow: 0 + }, + { + id: 'cell-editor-remark-inv2', + type: 'TableTd', + tdType: 'editor', + staticText: { text: '', require: false }, + editor: { type: null, binding: null }, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'top', + appearance: { class: 'farris-group-wrap--input', style: '' }, + size: null, + invisible: true, + markCol: 2, + markRow: 0 + } + ] + } + ]); + + const border = { width: 1, enableColor: false, color: '#eaecf3' }; + const font = { size: 13, enableColor: true, color: '#2d2f33', family: 'sans-serif' }; + + return () => ( +
+
Table 运行时基础示例
+ console.log('table change:', e)} + /> +
+ 当前表单数据: +
{JSON.stringify(formData.value, null, 2)}
+
+
+ ); + } +}); -- Gitee From 57e32eb5cec36794c0d5d83a6ed4ed037489b3ae Mon Sep 17 00:00:00 2001 From: wang-xh Date: Thu, 16 Jul 2026 09:16:36 +0800 Subject: [PATCH 20/26] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3table=E5=86=85?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E8=8F=9C=E5=8D=95=E8=A2=AB=E9=81=AE=E6=8C=A1?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B=E8=A7=A3=E5=86=B3table?= =?UTF-8?q?=E5=86=85=E9=87=8D=E5=A4=8D=E7=82=B9=E5=87=BB=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=A0=BC=E5=90=8E=E4=B8=8D=E6=98=BE=E7=A4=BA=E8=93=9D=E8=89=B2?= =?UTF-8?q?=E8=BE=B9=E6=A1=86=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/assets/template-rules/common.json | 6 - .../src/composition/class/designer-canvas.css | 10 +- .../response-form-use-designer-rules.ts | 4 + .../src/components/context-menu.component.tsx | 153 +++++++++--------- .../src/designer/table.design.component.tsx | 20 +-- 5 files changed, 100 insertions(+), 93 deletions(-) diff --git a/packages/designer/public/assets/template-rules/common.json b/packages/designer/public/assets/template-rules/common.json index 2e1549a28..dc0231691 100644 --- a/packages/designer/public/assets/template-rules/common.json +++ b/packages/designer/public/assets/template-rules/common.json @@ -604,11 +604,5 @@ } ] } - }, - "f-form-is-table": { - "description": "table组件内的卡片面板", - "canMove": false, - "canDelete": false, - "canAccept": false } } \ No newline at end of file diff --git a/packages/ui-vue/components/designer-canvas/src/composition/class/designer-canvas.css b/packages/ui-vue/components/designer-canvas/src/composition/class/designer-canvas.css index 7f7cc119f..230155e00 100644 --- a/packages/ui-vue/components/designer-canvas/src/composition/class/designer-canvas.css +++ b/packages/ui-vue/components/designer-canvas/src/composition/class/designer-canvas.css @@ -231,7 +231,7 @@ } /** 解决由ng升级的vue带导航的卡片模板中,子表容器宽度不足的问题 */ -.editorDiv .f-page-navigate .f-page.f-page-card>.drag-container> .f-page-main:not(.f-page-child-fill)>.drag-container { +.editorDiv .f-page-navigate .f-page.f-page-card>.drag-container>.f-page-main:not(.f-page-child-fill)>.drag-container { display: block; overflow: unset; } @@ -358,5 +358,11 @@ .farris-component.farris-component-content-container.data-grid-container { min-height: 21.875rem; } +/** 解决table组件单元格操作按钮的显示问题 **/ +.f-table-has-form td .widget-panel { + display: none; +} - +.f-table-has-form td.dgComponentSelected .widget-panel { + display: block; +} \ No newline at end of file diff --git a/packages/ui-vue/components/dynamic-form/src/designer/response-form-use-designer-rules.ts b/packages/ui-vue/components/dynamic-form/src/designer/response-form-use-designer-rules.ts index 85b6fba5d..8f2977eac 100644 --- a/packages/ui-vue/components/dynamic-form/src/designer/response-form-use-designer-rules.ts +++ b/packages/ui-vue/components/dynamic-form/src/designer/response-form-use-designer-rules.ts @@ -612,6 +612,10 @@ export function useDesignerRules(designItemContext: DesignerItemContext, designe } function hideNestedPaddingInDesginerView() { + // 内部存放table组件时,不展示间距 + if (schema.appearance?.class?.includes('f-form-is-table')) { + return true; + } return false; } // 构造属性配置方法 diff --git a/packages/ui-vue/components/table/src/components/context-menu.component.tsx b/packages/ui-vue/components/table/src/components/context-menu.component.tsx index 5d208a648..a9ee806ff 100644 --- a/packages/ui-vue/components/table/src/components/context-menu.component.tsx +++ b/packages/ui-vue/components/table/src/components/context-menu.component.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, nextTick, onMounted, onBeforeUnmount, watch } from "vue"; +import { defineComponent, ref, onMounted, onBeforeUnmount, onUpdated, Teleport } from "vue"; export interface ContextMenuOption { id: string; @@ -40,34 +40,34 @@ export default defineComponent({ props: contextMenuProps, setup(props: ContextMenuProps) { const menuRef = ref(); - const adjustedPosition = ref({ x: 0, y: 0 }); const hoveredId = ref(null); + const displayPos = ref({ x: 0, y: 0 }); - const adjustPosition = () => { + const doAdjust = () => { if (!menuRef.value) return; const menu = menuRef.value; const rect = menu.getBoundingClientRect(); const vpW = window.innerWidth; const vpH = window.innerHeight; + const padding = 4; let { x, y } = props.position; - const padding = 4; // 右侧空间不足 → 翻转到锚点左侧 if (x + rect.width > vpW) { x = props.position.x - rect.width; } - // 下方空间不足 → 翻转到锚点上方 if (y + rect.height > vpH) { y = props.position.y - rect.height; } - // 兜底:不超出视口 x = Math.max(padding, Math.min(x, vpW - rect.width - padding)); y = Math.max(padding, Math.min(y, vpH - rect.height - padding)); - adjustedPosition.value = { x, y }; + if (displayPos.value.x !== x || displayPos.value.y !== y) { + displayPos.value = { x, y }; + } }; const handleKeyDown = (event: KeyboardEvent) => { @@ -97,89 +97,90 @@ export default defineComponent({ document.removeEventListener('keydown', handleKeyDown); }); - watch( - () => [props.visible, props.position.x, props.position.y], - () => { - if (props.visible) { - adjustedPosition.value = props.position; - void nextTick(() => adjustPosition()); - } - }, - { immediate: true } - ); + onUpdated(() => { + doAdjust(); + }); return () => { if (!props.visible) { + if (displayPos.value.x !== 0 || displayPos.value.y !== 0) { + displayPos.value = { x: 0, y: 0 }; + } return null; } + // displayPos 为 (0,0) 说明是首次渲染,直接用 props.position 定位 + const pos = (displayPos.value.x === 0 && displayPos.value.y === 0) ? props.position : displayPos.value; + return ( -
{ e.preventDefault(); e.stopPropagation(); }} - > +
e.stopPropagation()} + onClick={handleOverlayClick} + onContextmenu={(e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} > - {props.options.map((item, index) => { - if ('separator' in item) { +
e.stopPropagation()} + > + {props.options.map((item, index) => { + if ('separator' in item) { + return ( +
+ ); + } + const option = item as ContextMenuOption; return ( -
+
handleOptionClick(option)} + onMouseenter={() => { hoveredId.value = option.id; }} + onMouseleave={() => { hoveredId.value = null; }} + > + {option.icon && ( + {option.icon} + )} + {option.label} +
); - } - const option = item as ContextMenuOption; - return ( -
handleOptionClick(option)} - onMouseenter={() => { hoveredId.value = option.id; }} - onMouseleave={() => { hoveredId.value = null; }} - > - {option.icon && ( - {option.icon} - )} - {option.label} -
- ); - })} + })} +
-
+ ); }; } diff --git a/packages/ui-vue/components/table/src/designer/table.design.component.tsx b/packages/ui-vue/components/table/src/designer/table.design.component.tsx index bf504a01c..f8b275b69 100644 --- a/packages/ui-vue/components/table/src/designer/table.design.component.tsx +++ b/packages/ui-vue/components/table/src/designer/table.design.component.tsx @@ -1,4 +1,4 @@ -import { SetupContext, defineComponent, inject, onMounted, onBeforeUnmount, ref, computed, triggerRef } from 'vue'; +import { SetupContext, defineComponent, inject, onMounted, onBeforeUnmount, ref, computed, triggerRef, nextTick } from 'vue'; import { getCustomClass } from '@farris/ui-vue/components/common'; import { DesignerItemContext, useDesignerComponent, DesignerHostService, canvasChanged @@ -73,21 +73,23 @@ export default defineComponent({ ); } - function isCellSelected(cell: TableCell): boolean { - return selectedCellId.value === cell.id; - } - function onCellClick(evt: MouseEvent, cell: TableCell) { evt.stopPropagation(); evt.preventDefault(); - if (!isCellSelected(cell)) { - clearCanvasSelection(); + clearCanvasSelection(); + // 先清除再设置,确保 Vue 能检测到变化并重新渲染 dgComponentSelected + const prevId = selectedCellId.value; + selectedCellId.value = ''; + if (prevId !== cell.id) { selectedCellId.value = cell.id; + } else { + void nextTick(() => { selectedCellId.value = cell.id; }); } const cid = (componentInstance.value as any).belongedComponentId || props.componentId; (designItemContext as any).setupContext?.emit( 'selectionChange', 'table-td', cell, cid, buildCellComponentInstance(cell) ); + } function onCellMouseDown(evt: MouseEvent) { @@ -114,8 +116,8 @@ export default defineComponent({ evt.preventDefault(); evt.stopPropagation(); onCellClick(evt, cell); - const rect = (evt.currentTarget as HTMLElement).getBoundingClientRect(); - openMenuAt(rect.right, rect.bottom, r, c); + // 使用事件坐标而非 getBoundingClientRect,避免 absolute 定位元素尺寸为 0 的问题 + openMenuAt(evt.clientX + 4, evt.clientY + 4, r, c); } onMounted(() => { -- Gitee From 4ac0b4f24cecc5e7230e6b1277cf331296b62af8 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Fri, 17 Jul 2026 16:45:15 +0800 Subject: [PATCH 21/26] =?UTF-8?q?fix:=20=E6=94=AF=E6=8C=81=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E5=88=9B=E5=BB=BAtable=E7=BB=84=E4=BB=B6=EF=BC=9B?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81=E5=AE=A1=E6=9F=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../composition/use-view-model-name.ts | 3 - .../components/types/toolbox/pc-toolbox.json | 6 + .../src/binding-selector.component.tsx | 2 +- .../src/composition/use-field-selection.ts | 4 +- .../components/button/src/button.props.ts | 2 +- .../base-form.property-config.ts | 17 +- .../composition/entity/input-base-property.ts | 2 +- packages/ui-vue/components/table/index.ts | 3 +- .../src/components/context-menu.component.tsx | 2 +- .../table/src/composition/common.ts | 2 +- .../table/src/composition/td-factory.ts | 15 +- .../table/src/composition/td-manager.spec.ts | 2 +- .../table/src/composition/td-manager.ts | 20 +- .../components/table/src/composition/types.ts | 7 +- .../src/designer/table.design.component.tsx | 8 +- .../table/src/designer/use-designer-rules.ts | 8 +- .../table/src/designer/use-table-creator.ts | 192 +++++++++++++ .../converter/border-converter.ts | 2 +- .../converter/font-converter.ts | 2 +- .../table-td.property-config.ts | 14 +- .../property-config/table.property-config.ts | 6 +- .../table/src/schema/schema-resolver.ts | 32 ++- .../table/src/schema/table-row.schema.json | 25 ++ .../table/src/schema/table-td.schema.json | 24 +- .../table/src/schema/table.schema.json | 32 +-- .../components/table/src/table.props.ts | 15 +- packages/ui-vue/demos/table/basic.tsx | 252 ------------------ 27 files changed, 338 insertions(+), 361 deletions(-) create mode 100644 packages/ui-vue/components/table/src/designer/use-table-creator.ts create mode 100644 packages/ui-vue/components/table/src/schema/table-row.schema.json delete mode 100644 packages/ui-vue/demos/table/basic.tsx diff --git a/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts b/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts index 99d60aaee..b3af4fc1f 100644 --- a/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts +++ b/packages/designer/src/components/components/view-model-designer/method-manager/composition/use-view-model-name.ts @@ -64,9 +64,6 @@ export function useViewModelName(useFormSchema: UseFormSchema) { case ComponentType.calendar: { return getComponentParentTitle(formSchema, component.id) || '日历组件'; } - case ComponentType.calendar: { - return getComponentParentTitle(formSchema, component.id) || '日历组件'; - } case ComponentType.table: { return '表格布局组件'; } diff --git a/packages/designer/src/components/types/toolbox/pc-toolbox.json b/packages/designer/src/components/types/toolbox/pc-toolbox.json index 53dbc3571..babca2d5b 100644 --- a/packages/designer/src/components/types/toolbox/pc-toolbox.json +++ b/packages/designer/src/components/types/toolbox/pc-toolbox.json @@ -128,6 +128,12 @@ "name": "日历", "category": "dataCollection", "icon": "date-picker" + }, + { + "id": "table", + "type": "table", + "name": "表格布局", + "category": "dataCollection" } ] }, diff --git a/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx b/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx index 67fa390f5..df4f7cf53 100644 --- a/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx +++ b/packages/ui-vue/components/binding-selector/src/binding-selector.component.tsx @@ -106,7 +106,7 @@ export default defineComponent({ function onClearBinding() { clearViewModel(); clearExpresssionConfig(); - clearComponentSchema() + clearComponentSchema(); setDisplayText(null); props.onFieldSelected(undefined, undefined); } diff --git a/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts b/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts index 406678c14..754b68de8 100644 --- a/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts +++ b/packages/ui-vue/components/binding-selector/src/composition/use-field-selection.ts @@ -13,7 +13,7 @@ export function useFieldSelection(props: BindingSelectorProps) { let localeVariableTreeData = []; let formVariableTreeData = []; /** 字段类型是否发生变化 */ - let fieldTypeChanged = ref(false); + const fieldTypeChanged = ref(false); const notifyService: any = new FNotifyService(); notifyService.globalConfig = { position: 'top-center' }; @@ -454,7 +454,7 @@ export function useFieldSelection(props: BindingSelectorProps) { * 清除组件schema的绑定信息 */ function clearComponentSchema() { - const componentSchema = props.editorParams.componentSchema; + const { componentSchema } = props.editorParams; if (!componentSchema) { return; } diff --git a/packages/ui-vue/components/button/src/button.props.ts b/packages/ui-vue/components/button/src/button.props.ts index 40b4889f8..0faa131c6 100644 --- a/packages/ui-vue/components/button/src/button.props.ts +++ b/packages/ui-vue/components/button/src/button.props.ts @@ -104,4 +104,4 @@ export const buttonProps = { * Button 组件 props 的推导类型。 */ export type ButtonProps = ExtractPropTypes; -export const propsResolver = createPropsResolver(buttonProps, componentSchema, schemaMapper, schemaResolver); +export const propsResolver = createPropsResolver(buttonProps, componentSchema, schemaMapper, schemaResolver); diff --git a/packages/ui-vue/components/dynamic-form/src/property-config/base-form.property-config.ts b/packages/ui-vue/components/dynamic-form/src/property-config/base-form.property-config.ts index 567907ae1..02efef674 100644 --- a/packages/ui-vue/components/dynamic-form/src/property-config/base-form.property-config.ts +++ b/packages/ui-vue/components/dynamic-form/src/property-config/base-form.property-config.ts @@ -9,11 +9,14 @@ export class BaseFormProperty extends BaseControlProperty { constructor(componentId: string, designerHostService: any) { super(componentId, designerHostService); } - public getBaseFormAppearanceConfig(formType:'dynamic'|'default',propertyData,componentInstance: DesignerComponentInstance){ + public getBaseFormAppearanceConfig(formType: 'dynamic' | 'default', propertyData, componentInstance: DesignerComponentInstance) { const self = this; const { checkIsInFormComponent, assembleUnifiedLayoutContext, changeFormControlsByUnifiedLayoutConfig, changeControlClassInByColumn } = useResponseFormLayoutSetting(this.formSchemaUtils, this.componentId); const appearanceCategory = this.getAppearanceConfig(propertyData); - // const issInFormComponent = checkIsInFormComponent(this.componentId); + const isInFormComponent = checkIsInFormComponent(this.componentId); + if (!isInFormComponent) { + return appearanceCategory; + } appearanceCategory.properties['adaptForLanguage'] = { title: "控件布局响应国际化", description: "启用国际化后:简体中文、繁体中文环境下控件标签与输入框在一行展示,其他语言环境下控件标签与输入框上下排列。", @@ -41,7 +44,7 @@ export class BaseFormProperty extends BaseControlProperty { switch (changeObject && changeObject.propertyID) { case 'unifiedLayout': { propertyData.layout = changeControlClassInByColumn('', changeObject.propertyValue); - if(formType === 'dynamic'){ + if (formType === 'dynamic') { propertyData.layout = changeControlClassInByColumn('', changeObject.propertyValue); } else { changeFormControlsByUnifiedLayoutConfig(null, changeObject.propertyValue, propertyData.id); @@ -59,14 +62,14 @@ export class BaseFormProperty extends BaseControlProperty { return appearanceCategory; } - public getBaseFormBehaviorConfig(formType:'dynamic'|'default',propertyData: any) { + public getBaseFormBehaviorConfig(formType: 'dynamic' | 'default', propertyData: any) { const self = this; // 外观 const fieldsEditor = this.getPropertyEditorParams(propertyData, ['Variable', 'Custom'], 'fields', {}, { newVariablePrefix: '', newVariableType: 'Array' }); const formDataEditor = this.getPropertyEditorParams(propertyData, ['Variable', 'Custom'], 'formData', {}, { newVariablePrefix: '', newVariableType: 'Object' }); let extendBehavior = {}; - if(formType === 'dynamic'){ - extendBehavior= { + if (formType === 'dynamic') { + extendBehavior = { fields: { title: '动态控件列表', type: 'string', @@ -85,7 +88,7 @@ export class BaseFormProperty extends BaseControlProperty { type: "enum", editor: { default: "quick", - readonly:true, + readonly: true, data: [ { id: "quick", name: "快速模式" }, { id: "full", name: "完整模式" } diff --git a/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts b/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts index a04e89bd2..83cb25674 100644 --- a/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts +++ b/packages/ui-vue/components/property-panel/src/composition/entity/input-base-property.ts @@ -306,7 +306,7 @@ export class InputBaseProperty extends BaseControlProperty { return { canChangeControlType: false, editorTypeList: [{ - key: propertyData.editor.type, + key: propertyData.editor?.type, value: DgControl[propertyData.editor.type]?.name || propertyData.editor.type }] }; diff --git a/packages/ui-vue/components/table/index.ts b/packages/ui-vue/components/table/index.ts index 7ce1d534d..8b21b1df8 100644 --- a/packages/ui-vue/components/table/index.ts +++ b/packages/ui-vue/components/table/index.ts @@ -16,7 +16,7 @@ import type { App } from 'vue'; import FTable from './src/table.component'; import TableDesign from './src/designer/table.design.component'; -import { propsResolverGenerator, TableCellPropsResolver } from './src/table.props'; +import { propsResolverGenerator, TableCellPropsResolver, TableRowPropsResolver } from './src/table.props'; import { RegisterContext } from '../common'; export * from './src/table.props'; @@ -35,6 +35,7 @@ export default { registerContext: RegisterContext): void { componentMap['table'] = TableDesign; propsResolverMap['table'] = propsResolverGenerator(registerContext); + propsResolverMap['table-row'] = TableRowPropsResolver; propsResolverMap['table-td'] = TableCellPropsResolver; } }; diff --git a/packages/ui-vue/components/table/src/components/context-menu.component.tsx b/packages/ui-vue/components/table/src/components/context-menu.component.tsx index a9ee806ff..2dd4ca40d 100644 --- a/packages/ui-vue/components/table/src/components/context-menu.component.tsx +++ b/packages/ui-vue/components/table/src/components/context-menu.component.tsx @@ -44,7 +44,7 @@ export default defineComponent({ const displayPos = ref({ x: 0, y: 0 }); const doAdjust = () => { - if (!menuRef.value) return; + if (!menuRef.value) { return; }; const menu = menuRef.value; const rect = menu.getBoundingClientRect(); const vpW = window.innerWidth; diff --git a/packages/ui-vue/components/table/src/composition/common.ts b/packages/ui-vue/components/table/src/composition/common.ts index 683fbb75f..d2f1a8a17 100644 --- a/packages/ui-vue/components/table/src/composition/common.ts +++ b/packages/ui-vue/components/table/src/composition/common.ts @@ -54,4 +54,4 @@ export class TableUtils { }); return !!target; } -} \ No newline at end of file +} diff --git a/packages/ui-vue/components/table/src/composition/td-factory.ts b/packages/ui-vue/components/table/src/composition/td-factory.ts index 11fd1b74b..71cf0f010 100644 --- a/packages/ui-vue/components/table/src/composition/td-factory.ts +++ b/packages/ui-vue/components/table/src/composition/td-factory.ts @@ -1,26 +1,21 @@ import { TableCell, TableRow } from './types'; +import { getSchemaByType } from "@farris/ui-vue/components/dynamic-resolver"; /** 与 Angular `new IdService().generate()` 等价的唯一 id 生成。 */ export function generateId(): string { return 'x' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4); } -/** 对齐 Angular `TableTdSchema` 的默认单元格(等价 cloneDeep(TableTdSchema) + 新 id)。 */ export function createTableTdMetadata(): TableCell { - return { + const tdMetadata = getSchemaByType('table-td') as TableCell; + return Object.assign(tdMetadata, { id: generateId(), - type: 'table-td', tdType: 'editor', staticText: { text: '', require: false }, editor: { type: null }, binding: null, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null - }; + appearance: { class: 'farris-group-wrap--input', style: '' } + }); } /** 生成默认 R×C 稠密网格(全部为独立锚点,markCol/markRow 缺省即视为 0)。 */ diff --git a/packages/ui-vue/components/table/src/composition/td-manager.spec.ts b/packages/ui-vue/components/table/src/composition/td-manager.spec.ts index 60a3f3921..ea882f94e 100644 --- a/packages/ui-vue/components/table/src/composition/td-manager.spec.ts +++ b/packages/ui-vue/components/table/src/composition/td-manager.spec.ts @@ -28,7 +28,7 @@ function makeManager(rows: any[]) { formSchemaUtils: { getExpressions: () => [], getComponents: () => [], getControlsInCmpWidthBinding: () => [] }, messageBox: { question: (_m: string, _d: string, ok: () => void) => ok() } }; - const mgr = new TableTdManager({ id: 't', rows }, services, redraw, canvas); + const mgr = new TableTdManager({ id: 't', rows }, services, redraw, canvas, 'test-vm-id'); return { mgr, redraw, canvas }; } diff --git a/packages/ui-vue/components/table/src/composition/td-manager.ts b/packages/ui-vue/components/table/src/composition/td-manager.ts index 474b318bf..78e6f4787 100644 --- a/packages/ui-vue/components/table/src/composition/td-manager.ts +++ b/packages/ui-vue/components/table/src/composition/td-manager.ts @@ -105,8 +105,7 @@ export class TableTdManager { // ===== Merge right ===== showMergeColumnToRight(): boolean { - const rowspan = this.targetColumn.rowspan; - const colspan = this.targetColumn.colspan; + const { rowspan, colspan } = this.targetColumn; if (this.selectedColumnIndex + colspan < this.targetRow.columns.length) { const n = this.targetRow.columns[colspan + this.selectedColumnIndex]; if (!n.invisible && rowspan === n.rowspan) { return true; } @@ -178,8 +177,7 @@ export class TableTdManager { return this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan > 1; } splitColumns() { - const colspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan; - const rowspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan; + const { colspan, rowspan } = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex]; for (let i = 0; i < colspan; i++) { this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex + i].invisible = false; this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex + i].colspan = 1; @@ -192,8 +190,7 @@ export class TableTdManager { return this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan > 1; } splitRows() { - const colspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan; - const rowspan = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan; + const { colspan, rowspan } = this.component.rows[this.selectedRowIndex].columns[this.selectedColumnIndex]; for (let i = 0; i < rowspan; i++) { this.component.rows[this.selectedRowIndex + i].columns[this.selectedColumnIndex].invisible = false; this.component.rows[this.selectedRowIndex + i].columns[this.selectedColumnIndex].rowspan = 1; @@ -205,7 +202,7 @@ export class TableTdManager { // ===== Remove column ===== showRemoveColumn(): boolean { - const rows = this.component.rows; + const { rows } = this.component; if (rows[this.selectedRowIndex].columns.length > 1) { const t = rows[this.selectedRowIndex].columns[this.selectedColumnIndex].colspan; if (t === 1) { return true; } @@ -220,11 +217,10 @@ export class TableTdManager { } } confirmRemoveColumn() { - const rows = this.component.rows; + const { rows } = this.component; const deletedColumns: any[] = []; for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { - const colspan = rows[rowIndex].columns[this.selectedColumnIndex].colspan; - const rowspan = rows[rowIndex].columns[this.selectedColumnIndex].rowspan; + const { colspan, rowspan } = rows[rowIndex].columns[this.selectedColumnIndex]; if (rows[rowIndex].columns[this.selectedColumnIndex].invisible) { const r = this.tableTdHelper.findPrevColIndex(rowIndex, this.selectedColumnIndex, rows); if (r !== this.selectedColumnIndex && rows[rowIndex].columns[r].colspan > 1) { @@ -255,7 +251,7 @@ export class TableTdManager { // ===== Remove row ===== showRemoveRow(): boolean { - const rows = this.component.rows; + const { rows } = this.component; if (rows.length > 1) { const t = rows[this.selectedRowIndex].columns[this.selectedColumnIndex].rowspan; if (t === 1) { return true; } @@ -276,7 +272,7 @@ export class TableTdManager { return false; } confirmRemoveRow() { - const rows = this.component.rows; + const { rows } = this.component; for (let e = 0; e < rows[this.selectedRowIndex].columns.length; e++) { const n = rows[this.selectedRowIndex].columns[e].rowspan; const o = rows[this.selectedRowIndex].columns[e].colspan; diff --git a/packages/ui-vue/components/table/src/composition/types.ts b/packages/ui-vue/components/table/src/composition/types.ts index c8af8462c..c209da2f1 100644 --- a/packages/ui-vue/components/table/src/composition/types.ts +++ b/packages/ui-vue/components/table/src/composition/types.ts @@ -10,10 +10,9 @@ export interface TableCell { binding: any, colspan: number; rowspan: number; - align: 'left' | 'center' | 'right'; - valign: 'top' | 'middle' | 'bottom'; + align?: 'left' | 'center' | 'right'; + valign?: 'top' | 'middle' | 'bottom'; appearance: { class: string; style: string; }; - size?: any; // 合并区域内偏移量(锚点=0,0);缺省即视为 0 markRow?: number; markCol?: number; @@ -35,4 +34,4 @@ export interface TableSchema { font: { size: number; enableColor: boolean; color: string; family: string; }; appearance: { class: string; style: string; }; visible: boolean; -} \ No newline at end of file +} diff --git a/packages/ui-vue/components/table/src/designer/table.design.component.tsx b/packages/ui-vue/components/table/src/designer/table.design.component.tsx index f8b275b69..c6e12850f 100644 --- a/packages/ui-vue/components/table/src/designer/table.design.component.tsx +++ b/packages/ui-vue/components/table/src/designer/table.design.component.tsx @@ -19,7 +19,7 @@ export default defineComponent({ const elementRef = ref(); const designerHostService = inject('designer-host-service'); const designItemContext = inject('design-item-context') as DesignerItemContext; - const designViewModelUtils = designerHostService?.designViewModelUtils + const designViewModelUtils = designerHostService?.designViewModelUtils; const formSchemaUtils = designerHostService?.formSchemaUtils; const rules = useDesignerRulesForTable(designItemContext, designerHostService); const componentInstance = useDesignerComponent(elementRef, designItemContext, rules); @@ -64,7 +64,7 @@ export default defineComponent({ /** 清除设计器已有的选中状态 */ function clearCanvasSelection() { const panel = document.querySelector('.editorPanel'); - if (!panel) return; + if (!panel) { return; }; panel.querySelectorAll('.dgComponentSelected').forEach( (el: Element) => el.classList.remove('dgComponentSelected') ); @@ -89,7 +89,7 @@ export default defineComponent({ (designItemContext as any).setupContext?.emit( 'selectionChange', 'table-td', cell, cid, buildCellComponentInstance(cell) ); - + } function onCellMouseDown(evt: MouseEvent) { @@ -98,7 +98,7 @@ export default defineComponent({ function onDocMouseDown(evt: MouseEvent) { const panel = document.querySelector('.editorPanel'); - if (!panel || !panel.contains(evt.target as Node)) return; + if (!panel || !panel.contains(evt.target as Node)) { return; }; if (elementRef.value && !elementRef.value.contains(evt.target as Node)) { selectedCellId.value = ''; } diff --git a/packages/ui-vue/components/table/src/designer/use-designer-rules.ts b/packages/ui-vue/components/table/src/designer/use-designer-rules.ts index db67b1abb..c811e8904 100644 --- a/packages/ui-vue/components/table/src/designer/use-designer-rules.ts +++ b/packages/ui-vue/components/table/src/designer/use-designer-rules.ts @@ -44,16 +44,16 @@ export function useDesignerRulesForTable( schema.rows.forEach(row => { if (row.columns?.length) { row.columns.forEach(cell => { - if (cell.tdType === 'editor' && cell.editor.type) { - const editorLabel = cell.editor.label || '单元格'; + if (cell.tdType === 'editor' && cell.editor?.type) { + const editorLabel = cell.editor?.label || '单元格'; controlBasicInfoMap.set(cell.id, { showName: editorLabel, parentPathName: `${tableName} > ${tableTdName} > ${editorLabel}` }); } - }) + }); } - }) + }); } } return { diff --git a/packages/ui-vue/components/table/src/designer/use-table-creator.ts b/packages/ui-vue/components/table/src/designer/use-table-creator.ts new file mode 100644 index 000000000..666eb1120 --- /dev/null +++ b/packages/ui-vue/components/table/src/designer/use-table-creator.ts @@ -0,0 +1,192 @@ +import { ComponentBuildInfo } from "@farris/ui-vue/components/component"; +import { ComponentSchema, DesignerHostService, DgControl } from "@farris/ui-vue/components/designer-canvas"; +import { DynamicResolver } from "@farris/ui-vue/components/dynamic-resolver"; +const ROOT_VIEW_MODEL_ID = 'root-viewmodel'; + +export function useTableCreator( + resolver: DynamicResolver, + designerHostService: DesignerHostService) { + + const { + formSchemaUtils, + designViewModelUtils, + } = designerHostService; + + /** + * 添加viewModel节点 + */ + function createViewModeNode(buildInfo: ComponentBuildInfo): any { + const viewModelNode = { + id: `${buildInfo.componentId}-component-viewmodel`, + code: `${buildInfo.componentId}-component-viewmodel`, + name: buildInfo.componentName, + bindTo: buildInfo.bindTo, + parent: ROOT_VIEW_MODEL_ID, + fields: [], + commands: [], + states: [], + enableValidation: true + }; + return viewModelNode; + } + + function createComponentRefNode(buildInfo: ComponentBuildInfo): any { + const componentRefNode = resolver.getSchemaByType('component-ref') as ComponentSchema; + Object.assign(componentRefNode, { + id: `${buildInfo.componentId}-component-ref`, + component: `${buildInfo.componentId}-component`, + }); + return componentRefNode; + } + + function createLabelTd(field: any) { + const td = resolver.getSchemaByType('table-td') as ComponentSchema; + Object.assign(td, { + id: `td_${Math.random().toString(36).substr(2, 6)}`, + appearance: { + class: 'farris-group-wrap--label' + }, + tdType: 'staticText', + staticText: { + text: '', + require: false + } + }); + if (field) { + td.staticText.text = field.name; + td.staticText.require = field.require; + } + return [td]; + } + + function createEditorTd(field: any) { + const editorTd = resolver.getSchemaByType('table-td') as ComponentSchema; + const controlCreatorUtils = designerHostService?.controlCreatorUtils; + // 可编辑单元格 + Object.assign(editorTd, { + id: `td_${Math.random().toString(36).substr(2, 6)}`, + appearance: { + class: 'farris-group-wrap--input' + }, + colspan: 2, + tdType: 'editor', + editor: { + type: null, + binding: null + } + }); + // 可编辑单元格 + if (field) { + const formGroupSchema = controlCreatorUtils.setFormFieldProperty(field); + Object.assign(editorTd, { + editorLabel: field.name, + editor: formGroupSchema?.editor, + binding: formGroupSchema?.binding, + path: formGroupSchema?.path + }); + } + + + // 隐藏单元格 + const invisibleTd = resolver.getSchemaByType('table-td') as ComponentSchema; + invisibleTd.id = `td_${Math.random().toString(36).substr(2, 6)}`; + invisibleTd.invisible = true; + invisibleTd.markCol = 1; + invisibleTd.markRow = 0; + return [editorTd, invisibleTd]; + } + + function resolveTableColumns(selectedFields: any[], table: any) { + table.rows = []; + + // 计算table行数 + let rowCounts = selectedFields.length / 2 + 0.5; + rowCounts = parseInt(rowCounts + '', 10); + + for (let rowIndex = 0, fieldCount = 0; rowIndex < rowCounts; rowIndex++) { + let columns: any[] = []; + for (let index = 0; index < 2; index++) { + const field = fieldCount < selectedFields.length ? selectedFields[fieldCount] : null; + const labelTds = createLabelTd(field); + const editorTds = createEditorTd(field); + columns = columns.concat(labelTds).concat(editorTds); + + fieldCount++; + } + + table.rows.push({ + id: `row_${Math.random().toString(36).substr(2, 6)}`, + type: 'table-row', + columns + }); + } + } + /** + * 创建table组件内层级结构 + */ + function createTableComponentContents(buildInfo: ComponentBuildInfo) { + const table = resolver.getSchemaByType('table') as ComponentSchema; + const section = resolver.getSchemaByType('section') as ComponentSchema; + const responseForm = resolver.getSchemaByType('response-form') as ComponentSchema; + resolveTableColumns(buildInfo.selectedFields || [], table); + + Object.assign(table, { + id: buildInfo.componentId, + appearance: { + class: 'table table-bordered f-table-has-form' + } + }); + Object.assign(responseForm, { + id: buildInfo.componentId + '-form', + appearance: { + class: 'f-form-layout farris-form f-form-is-table' + }, + contents: [table] + }); + Object.assign(section, { + id: buildInfo.componentId + '-section', + appearance: { + class: 'f-section-oa-table f-section-in-mainsubcard' + }, + showHeader: false, + contents: [responseForm] + }); + + + return [section]; + } + + function createComponentNode(buildInfo: ComponentBuildInfo): any { + const componentNode = resolver.getSchemaByType('component') as ComponentSchema; + const contents = createTableComponentContents(buildInfo); + + Object.assign(componentNode, { + id: `${buildInfo.componentId}-component`, + viewModel: `${buildInfo.componentId}-component-viewmodel`, + componentType: buildInfo.componentType, + contents, + appearance: { + class: 'f-struct-wrapper' + } + }); + return componentNode; + } + + function createComponent(buildInfo: ComponentBuildInfo) { + const componentRefNode = createComponentRefNode(buildInfo); + + const componentNode = createComponentNode(buildInfo); + + const viewModelNode = createViewModeNode(buildInfo); + + const formSchema = formSchemaUtils.getFormSchema(); + formSchema.module.viewmodels.push(viewModelNode); + formSchema.module.components.push(componentNode); + + designViewModelUtils.assembleDesignViewModel(); + + return componentRefNode; + } + + return { createComponent }; +} diff --git a/packages/ui-vue/components/table/src/property-config/converter/border-converter.ts b/packages/ui-vue/components/table/src/property-config/converter/border-converter.ts index bd1f6b212..b06f36c50 100644 --- a/packages/ui-vue/components/table/src/property-config/converter/border-converter.ts +++ b/packages/ui-vue/components/table/src/property-config/converter/border-converter.ts @@ -3,7 +3,7 @@ import { ComponentSchema, PropertyConverter, SchemaService } from "@farris/ui-vu export default { convertTo: (schema: ComponentSchema, propertyKey: string, propertyValue: any, schemaService: SchemaService) => { if (!schema.border) { - schema.border = {}; + schema.border = { width: 1, enableColor: false, color: '#eaecf3' }; } schema.border[propertyKey] = propertyValue; }, diff --git a/packages/ui-vue/components/table/src/property-config/converter/font-converter.ts b/packages/ui-vue/components/table/src/property-config/converter/font-converter.ts index 7c17d9a96..15e6dd189 100644 --- a/packages/ui-vue/components/table/src/property-config/converter/font-converter.ts +++ b/packages/ui-vue/components/table/src/property-config/converter/font-converter.ts @@ -3,7 +3,7 @@ import { ComponentSchema, PropertyConverter, SchemaService } from "@farris/ui-vu export default { convertTo: (schema: ComponentSchema, propertyKey: string, propertyValue: any, schemaService: SchemaService) => { if (!schema.font) { - schema.font = {}; + schema.font = { size: 13, enableColor: false, color: '#2d2f33', family: 'sans-serif' }; } schema.font[propertyKey] = propertyValue; }, diff --git a/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts index ddebe5a9d..c4688b122 100644 --- a/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts +++ b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts @@ -147,7 +147,9 @@ export class TableTdProperty extends InputBaseProperty { */ private getEditorBindingAndTypePropConfig(cell: any) { const self = this; - + if (cell.tdType !== 'editor' || !cell.editor?.type) { + return; + } const { canChangeControlType, editorTypeList } = this.getAvailableEditorType(cell); this.propertyConfig.categories['tableTdEditor_editorBinding'] = { @@ -207,8 +209,6 @@ export class TableTdProperty extends InputBaseProperty { case 'binding': { // 变更字段类型:修改原控件DOM结构 self.changeControlAfterBindingChanged(changeObject, cell, parameters); - // 编辑器内自动为cell赋值Binding,但是td的binding是存在cell.editor.binding的,所以这里单独删除一份 - // delete cell.binding; // 刷新实体树 changeObject.needRefreshEntityTree = true; break; @@ -242,9 +242,9 @@ export class TableTdProperty extends InputBaseProperty { propertyData.editor = { type: null, binding: null }; return; } - const fieldTypeChanged = parameters?.fieldTypeChanged + const fieldTypeChanged = parameters?.fieldTypeChanged; // 若变更字段类型,或新绑定字段,则走新建控件逻辑 - if (fieldTypeChanged || !propertyData.editor.type) { + if (fieldTypeChanged || !propertyData.editor?.type) { const bindingType = changeObject.propertyValue.type; const newEditorInfo = this.createControl(bindingType, parameters); if (newEditorInfo) { @@ -393,7 +393,7 @@ export class TableTdProperty extends InputBaseProperty { const eventConfig = this.editorTypeConfig.getEventPropertyConfig(cell); if (eventConfig) { // 过滤掉 onClickLabel 事件 - const properties = eventConfig.properties; + const { properties } = eventConfig; if (properties) { Object.values(properties).forEach((prop: any) => { if (prop?.editor?.initialData?.getEventList) { @@ -402,7 +402,7 @@ export class TableTdProperty extends InputBaseProperty { const originalResult = originalGetEventList(); const filteredEvents = (originalResult.events || []).filter((item: any) => item.label !== 'onClickLabel'); return { ...originalResult, events: filteredEvents }; - } + }; } }); } diff --git a/packages/ui-vue/components/table/src/property-config/table.property-config.ts b/packages/ui-vue/components/table/src/property-config/table.property-config.ts index 4c7d684ab..f043b92bb 100644 --- a/packages/ui-vue/components/table/src/property-config/table.property-config.ts +++ b/packages/ui-vue/components/table/src/property-config/table.property-config.ts @@ -29,13 +29,13 @@ export class TableProperty extends BaseControlProperty { type: 'boolean', description: '', refreshPanelAfterChanged: true - + }, color: { title: '自定义颜色', type: 'string', description: '统一设置表格边框颜色', - visible: propertyData.border?.enableColor + visible: !!propertyData.border?.enableColor } } }; @@ -63,7 +63,7 @@ export class TableProperty extends BaseControlProperty { title: '自定义颜色', type: 'string', description: '设置字体框颜色', - visible: propertyData.font?.enableColor + visible: !!propertyData.font?.enableColor }, family: { title: '字体', diff --git a/packages/ui-vue/components/table/src/schema/schema-resolver.ts b/packages/ui-vue/components/table/src/schema/schema-resolver.ts index 2347a870d..dc32f4277 100644 --- a/packages/ui-vue/components/table/src/schema/schema-resolver.ts +++ b/packages/ui-vue/components/table/src/schema/schema-resolver.ts @@ -1,9 +1,31 @@ import { DynamicResolver } from '@farris/ui-vue/components/dynamic-resolver'; -import { createDefaultTableRows } from '../composition/td-factory'; +import { DesignerComponentInstance, DesignerHostService } from "@farris/ui-vue/components/designer-canvas"; +import { ComponentBuildInfo } from "@farris/ui-vue/components/component"; +import { useTableCreator } from '../designer/use-table-creator'; -export function schemaResolver(resolver: DynamicResolver, schema: Record, context: Record): Record { - if (!schema.rows || schema.rows.length === 0) { - schema.rows = createDefaultTableRows(2, 2); +export function schemaResolver( + resolver: DynamicResolver, + schema: Record, + context: Record, + designerHostService?: DesignerHostService): Record { + const parentComponentInstance = context.parentComponentInstance as DesignerComponentInstance; + if (parentComponentInstance && designerHostService) { + const radomNumber = Math.random().toString(36).slice(2, 6); + const componentBuildInfo: ComponentBuildInfo = { + componentId: `table-${radomNumber}`, + componentName: context.bindingSourceContext?.entityTitle || context.bindingSourceContext?.bindingEntity?.name || `表格-${radomNumber}`, + componentType: 'table', + parentContainerId: parentComponentInstance.schema.id, + parentComponentInstance: parentComponentInstance, + bindTo: context.bindingSourceContext?.bindTo || '', + dataSource: context.bindingSourceContext?.bindingEntity?.label, + selectedFields: context.bindingSourceContext?.bindingEntityFields + }; + const { createComponent } = useTableCreator(resolver, designerHostService); + const componentRefNode = createComponent(componentBuildInfo); + + return componentRefNode; + } else { + return schema; } - return schema; } diff --git a/packages/ui-vue/components/table/src/schema/table-row.schema.json b/packages/ui-vue/components/table/src/schema/table-row.schema.json new file mode 100644 index 000000000..50f2e8f59 --- /dev/null +++ b/packages/ui-vue/components/table/src/schema/table-row.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://farris-design.gitee.io/table-row.schema.json", + "title": "table-row", + "description": "A Farris design-time layout table row", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "default": "table-row" + }, + "columns": { + "type": "array", + "default": [] + } + }, + "required": [ + "id", + "type", + "columns" + ] +} \ No newline at end of file diff --git a/packages/ui-vue/components/table/src/schema/table-td.schema.json b/packages/ui-vue/components/table/src/schema/table-td.schema.json index cb9b54e8c..992e4f51d 100644 --- a/packages/ui-vue/components/table/src/schema/table-td.schema.json +++ b/packages/ui-vue/components/table/src/schema/table-td.schema.json @@ -10,7 +10,7 @@ }, "type": { "type": "string", - "default": "" + "default": "table-td" }, "tdType": { "type": "string", @@ -23,12 +23,9 @@ "type": "string" }, "require": { - "type": "boolean" + "type": "boolean", + "default": false } - }, - "default": { - "text": "", - "require": false } }, "editor": { @@ -39,13 +36,7 @@ "string", "null" ] - }, - "id": { - "type": "string" } - }, - "default": { - "type": null } }, "binding": { @@ -83,9 +74,6 @@ "style": "" } }, - "size": { - "type": "object" - }, "markRow": { "type": "number", "default": 0 @@ -105,7 +93,11 @@ }, "required": [ "id", - "type" + "type", + "colspan", + "rowspan", + "align", + "valign" ], "events": { "onExpandClick": "扩展按钮点击事件", diff --git a/packages/ui-vue/components/table/src/schema/table.schema.json b/packages/ui-vue/components/table/src/schema/table.schema.json index b6fa750e8..12f0dfc93 100644 --- a/packages/ui-vue/components/table/src/schema/table.schema.json +++ b/packages/ui-vue/components/table/src/schema/table.schema.json @@ -32,42 +32,38 @@ "type": "object", "properties": { "width": { - "type": "number" + "type": "number", + "default": 1 }, "enableColor": { - "type": "boolean" + "type": "boolean", + "default": false }, "color": { - "type": "string" + "type": "string", + "default": "#eaecf3" } - }, - "default": { - "width": 1, - "enableColor": false, - "color": "#eaecf3" } }, "font": { "type": "object", "properties": { "size": { - "type": "number" + "type": "number", + "default": 13 }, "enableColor": { - "type": "boolean" + "type": "boolean", + "default": false }, "color": { - "type": "string" + "type": "string", + "default": "#2d2f33" }, "family": { - "type": "string" + "type": "string", + "default": "sans-serif" } - }, - "default": { - "size": 13, - "enableColor": false, - "color": "#2d2f33", - "family": "sans-serif" } }, "rows": { diff --git a/packages/ui-vue/components/table/src/table.props.ts b/packages/ui-vue/components/table/src/table.props.ts index eab35f8b5..2dc75c5fa 100644 --- a/packages/ui-vue/components/table/src/table.props.ts +++ b/packages/ui-vue/components/table/src/table.props.ts @@ -3,10 +3,9 @@ import { createPropsResolver, getPropsResolverGenerator } from '@farris/ui-vue/c import { schemaMapper } from './schema/schema-mapper'; import tableSchema from './schema/table.schema.json'; import tableCellSchema from './schema/table-td.schema.json'; +import tableRowSchema from './schema/table-row.schema.json'; import { schemaResolver } from './schema/schema-resolver'; -import { TableCellEditor, TableCellStaticText, TableRow } from './composition/types'; - - +import { TableCell, TableCellEditor, TableCellStaticText, TableRow } from './composition/types'; export const tableProps = { id: { type: String, default: '' }, @@ -26,8 +25,6 @@ export type TablePropsType = ExtractPropTypes; export const propsResolver = createPropsResolver(tableProps, tableSchema, schemaMapper, schemaResolver); export const propsResolverGenerator = getPropsResolverGenerator(tableProps, tableSchema, schemaMapper, schemaResolver); - - export const tableCellProps = { id: { type: String, default: '' }, type: { type: String, default: '' }, @@ -51,4 +48,12 @@ export type TableCellProps = ExtractPropTypes; export const TableCellPropsResolver = createPropsResolver(tableCellProps, tableCellSchema, schemaMapper, schemaResolver); +export const tableRowProps = { + id: { type: String, default: '' }, + type: { type: String, default: '' }, + rows: { type: Array as PropType, default: () => [] } +} as Record; + +export type TableRowProps = ExtractPropTypes; +export const TableRowPropsResolver = createPropsResolver(tableCellProps, tableRowSchema, schemaMapper, schemaResolver); diff --git a/packages/ui-vue/demos/table/basic.tsx b/packages/ui-vue/demos/table/basic.tsx deleted file mode 100644 index cf02d2b6d..000000000 --- a/packages/ui-vue/demos/table/basic.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { defineComponent, ref } from 'vue'; -import FTable from '../../components/table/src/table.component'; -import { createDefaultTableRows } from '../../components/table/src/composition/td-factory'; -import { TableRow } from '../../components/table/src/composition/types'; - -export default defineComponent({ - name: 'TableBasicDemo', - setup() { - const formData = ref>({ - name: '张三', - gender: 'Male', - birth: '1990-05-15', - telephone: '13800138000', - email: 'zhangsan@example.com' - }); - - const rows = ref([ - { - id: 'row-1', - type: 'TableRow', - columns: [ - { - id: 'cell-label-name', - type: 'TableTd', - tdType: 'staticText', - staticText: { text: '姓名', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--label', style: '' }, - size: null - }, - { - id: 'cell-editor-name', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: 'input-group', binding: { field: 'name', label: '姓名' } }, - colspan: 2, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null - } - ] - }, - { - id: 'row-2', - type: 'TableRow', - columns: [ - { - id: 'cell-label-gender', - type: 'TableTd', - tdType: 'staticText', - staticText: { text: '性别', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--label', style: '' }, - size: null - }, - { - id: 'cell-editor-gender', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: 'combo-list', binding: { field: 'gender', label: '性别' } }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null - }, - { - id: 'cell-label-birth', - type: 'TableTd', - tdType: 'staticText', - staticText: { text: '出生日期', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--label', style: '' }, - size: null - }, - { - id: 'cell-editor-birth', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: 'date-picker', binding: { field: 'birth', label: '出生日期' } }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null - } - ] - }, - { - id: 'row-3', - type: 'TableRow', - columns: [ - { - id: 'cell-label-phone', - type: 'TableTd', - tdType: 'staticText', - staticText: { text: '电话', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--label', style: '' }, - size: null - }, - { - id: 'cell-editor-phone', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: 'input-group', binding: { field: 'telephone', label: '电话' } }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null - }, - { - id: 'cell-label-email', - type: 'TableTd', - tdType: 'staticText', - staticText: { text: '邮箱', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--label', style: '' }, - size: null - }, - { - id: 'cell-editor-email', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: 'input-group', binding: { field: 'email', label: '邮箱' } }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null - } - ] - }, - { - id: 'row-4', - type: 'TableRow', - columns: [ - { - id: 'cell-label-remark', - type: 'TableTd', - tdType: 'staticText', - staticText: { text: '备注', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--label', style: '' }, - size: null - }, - { - id: 'cell-editor-remark', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: 'input-group', binding: { field: 'remark', label: '备注' } }, - colspan: 3, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null - }, - { - id: 'cell-editor-remark-inv1', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null, - invisible: true, - markCol: 1, - markRow: 0 - }, - { - id: 'cell-editor-remark-inv2', - type: 'TableTd', - tdType: 'editor', - staticText: { text: '', require: false }, - editor: { type: null, binding: null }, - colspan: 1, - rowspan: 1, - align: 'right', - valign: 'top', - appearance: { class: 'farris-group-wrap--input', style: '' }, - size: null, - invisible: true, - markCol: 2, - markRow: 0 - } - ] - } - ]); - - const border = { width: 1, enableColor: false, color: '#eaecf3' }; - const font = { size: 13, enableColor: true, color: '#2d2f33', family: 'sans-serif' }; - - return () => ( -
-
Table 运行时基础示例
- console.log('table change:', e)} - /> -
- 当前表单数据: -
{JSON.stringify(formData.value, null, 2)}
-
-
- ); - } -}); -- Gitee From dbe287b851023b0415adcb6e567f3b2ef2631366 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 20 Jul 2026 09:26:00 +0800 Subject: [PATCH 22/26] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/src/composition/td-factory.ts | 4 ++-- .../src/designer/table.design.component.tsx | 2 +- .../table/src/designer/use-table-creator.ts | 21 +++++++++++-------- .../table/src/schema/schema-resolver.ts | 6 +++--- .../components/table/src/table.props.ts | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/ui-vue/components/table/src/composition/td-factory.ts b/packages/ui-vue/components/table/src/composition/td-factory.ts index 71cf0f010..8f7ad2e48 100644 --- a/packages/ui-vue/components/table/src/composition/td-factory.ts +++ b/packages/ui-vue/components/table/src/composition/td-factory.ts @@ -9,7 +9,7 @@ export function generateId(): string { export function createTableTdMetadata(): TableCell { const tdMetadata = getSchemaByType('table-td') as TableCell; return Object.assign(tdMetadata, { - id: generateId(), + id: `td_${generateId()}`, tdType: 'editor', staticText: { text: '', require: false }, editor: { type: null }, @@ -24,7 +24,7 @@ export function createDefaultTableRows(rowCount: number, colCount: number): Tabl for (let r = 0; r < rowCount; r++) { const columns: TableCell[] = []; for (let c = 0; c < colCount; c++) { columns.push(createTableTdMetadata()); } - rows.push({ id: generateId(), type: 'table-row', columns }); + rows.push({ id: `tr_${generateId()}`, type: 'table-row', columns }); } return rows; } diff --git a/packages/ui-vue/components/table/src/designer/table.design.component.tsx b/packages/ui-vue/components/table/src/designer/table.design.component.tsx index b07d188d6..852fccd8d 100644 --- a/packages/ui-vue/components/table/src/designer/table.design.component.tsx +++ b/packages/ui-vue/components/table/src/designer/table.design.component.tsx @@ -64,7 +64,7 @@ export default defineComponent({ /** 清除设计器已有的选中状态 */ function clearCanvasSelection() { const panel = document.querySelector('.editorPanel'); - if (!panel) { return; }; + if (!panel) { return; } panel.querySelectorAll('.dgComponentSelected').forEach( (el: Element) => el.classList.remove('dgComponentSelected') ); diff --git a/packages/ui-vue/components/table/src/designer/use-table-creator.ts b/packages/ui-vue/components/table/src/designer/use-table-creator.ts index 666eb1120..8e4733cc7 100644 --- a/packages/ui-vue/components/table/src/designer/use-table-creator.ts +++ b/packages/ui-vue/components/table/src/designer/use-table-creator.ts @@ -1,6 +1,7 @@ import { ComponentBuildInfo } from "@farris/ui-vue/components/component"; import { ComponentSchema, DesignerHostService, DgControl } from "@farris/ui-vue/components/designer-canvas"; import { DynamicResolver } from "@farris/ui-vue/components/dynamic-resolver"; +import { generateId } from "../composition/td-factory"; const ROOT_VIEW_MODEL_ID = 'root-viewmodel'; export function useTableCreator( @@ -15,7 +16,7 @@ export function useTableCreator( /** * 添加viewModel节点 */ - function createViewModeNode(buildInfo: ComponentBuildInfo): any { + function createViewModelNode(buildInfo: ComponentBuildInfo): any { const viewModelNode = { id: `${buildInfo.componentId}-component-viewmodel`, code: `${buildInfo.componentId}-component-viewmodel`, @@ -42,7 +43,7 @@ export function useTableCreator( function createLabelTd(field: any) { const td = resolver.getSchemaByType('table-td') as ComponentSchema; Object.assign(td, { - id: `td_${Math.random().toString(36).substr(2, 6)}`, + id: `td_${generateId()}`, appearance: { class: 'farris-group-wrap--label' }, @@ -62,9 +63,12 @@ export function useTableCreator( function createEditorTd(field: any) { const editorTd = resolver.getSchemaByType('table-td') as ComponentSchema; const controlCreatorUtils = designerHostService?.controlCreatorUtils; + if (!controlCreatorUtils) { + return []; + } // 可编辑单元格 Object.assign(editorTd, { - id: `td_${Math.random().toString(36).substr(2, 6)}`, + id: `td_${generateId()}`, appearance: { class: 'farris-group-wrap--input' }, @@ -89,7 +93,7 @@ export function useTableCreator( // 隐藏单元格 const invisibleTd = resolver.getSchemaByType('table-td') as ComponentSchema; - invisibleTd.id = `td_${Math.random().toString(36).substr(2, 6)}`; + invisibleTd.id = `td_${generateId()}`; invisibleTd.invisible = true; invisibleTd.markCol = 1; invisibleTd.markRow = 0; @@ -99,9 +103,8 @@ export function useTableCreator( function resolveTableColumns(selectedFields: any[], table: any) { table.rows = []; - // 计算table行数 - let rowCounts = selectedFields.length / 2 + 0.5; - rowCounts = parseInt(rowCounts + '', 10); + // 计算table行数(向上取整) + const rowCounts = Math.ceil(selectedFields.length / 2); for (let rowIndex = 0, fieldCount = 0; rowIndex < rowCounts; rowIndex++) { let columns: any[] = []; @@ -115,7 +118,7 @@ export function useTableCreator( } table.rows.push({ - id: `row_${Math.random().toString(36).substr(2, 6)}`, + id: `tr_${generateId()}`, type: 'table-row', columns }); @@ -177,7 +180,7 @@ export function useTableCreator( const componentNode = createComponentNode(buildInfo); - const viewModelNode = createViewModeNode(buildInfo); + const viewModelNode = createViewModelNode(buildInfo); const formSchema = formSchemaUtils.getFormSchema(); formSchema.module.viewmodels.push(viewModelNode); diff --git a/packages/ui-vue/components/table/src/schema/schema-resolver.ts b/packages/ui-vue/components/table/src/schema/schema-resolver.ts index dc32f4277..10cabe9e8 100644 --- a/packages/ui-vue/components/table/src/schema/schema-resolver.ts +++ b/packages/ui-vue/components/table/src/schema/schema-resolver.ts @@ -10,10 +10,10 @@ export function schemaResolver( designerHostService?: DesignerHostService): Record { const parentComponentInstance = context.parentComponentInstance as DesignerComponentInstance; if (parentComponentInstance && designerHostService) { - const radomNumber = Math.random().toString(36).slice(2, 6); + const randomNumber = Math.random().toString(36).slice(2, 6); const componentBuildInfo: ComponentBuildInfo = { - componentId: `table-${radomNumber}`, - componentName: context.bindingSourceContext?.entityTitle || context.bindingSourceContext?.bindingEntity?.name || `表格-${radomNumber}`, + componentId: `table-${randomNumber}`, + componentName: context.bindingSourceContext?.entityTitle || context.bindingSourceContext?.bindingEntity?.name || `表格-${randomNumber}`, componentType: 'table', parentContainerId: parentComponentInstance.schema.id, parentComponentInstance: parentComponentInstance, diff --git a/packages/ui-vue/components/table/src/table.props.ts b/packages/ui-vue/components/table/src/table.props.ts index 70c2ceeb2..2bd5dcb84 100644 --- a/packages/ui-vue/components/table/src/table.props.ts +++ b/packages/ui-vue/components/table/src/table.props.ts @@ -62,4 +62,4 @@ export const tableRowProps = { export type TableRowProps = ExtractPropTypes; -export const TableRowPropsResolver = createPropsResolver(tableCellProps, tableRowSchema, schemaMapper, schemaResolver); +export const TableRowPropsResolver = createPropsResolver(tableRowProps, tableRowSchema, schemaMapper, schemaResolver); -- Gitee From ce1ef6f137268bd8671d5d8f526de6e2174ab927 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 20 Jul 2026 09:48:32 +0800 Subject: [PATCH 23/26] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/src/designer/use-table-creator.ts | 42 ++++++++++--------- .../table/src/schema/schema-resolver.ts | 3 +- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/ui-vue/components/table/src/designer/use-table-creator.ts b/packages/ui-vue/components/table/src/designer/use-table-creator.ts index 8e4733cc7..b289276e4 100644 --- a/packages/ui-vue/components/table/src/designer/use-table-creator.ts +++ b/packages/ui-vue/components/table/src/designer/use-table-creator.ts @@ -32,8 +32,8 @@ export function useTableCreator( } function createComponentRefNode(buildInfo: ComponentBuildInfo): any { - const componentRefNode = resolver.getSchemaByType('component-ref') as ComponentSchema; - Object.assign(componentRefNode, { + const componentRefNodeTemplate = resolver.getSchemaByType('component-ref') as ComponentSchema; + const componentRefNode = Object.assign({}, componentRefNodeTemplate, { id: `${buildInfo.componentId}-component-ref`, component: `${buildInfo.componentId}-component`, }); @@ -41,8 +41,8 @@ export function useTableCreator( } function createLabelTd(field: any) { - const td = resolver.getSchemaByType('table-td') as ComponentSchema; - Object.assign(td, { + const tdTemplate = resolver.getSchemaByType('table-td') as ComponentSchema; + const td = Object.assign({}, tdTemplate, { id: `td_${generateId()}`, appearance: { class: 'farris-group-wrap--label' @@ -55,19 +55,19 @@ export function useTableCreator( }); if (field) { td.staticText.text = field.name; - td.staticText.require = field.require; + td.staticText.require = field.require || false; } return [td]; } function createEditorTd(field: any) { - const editorTd = resolver.getSchemaByType('table-td') as ComponentSchema; + const editorTdTemplate = resolver.getSchemaByType('table-td') as ComponentSchema; const controlCreatorUtils = designerHostService?.controlCreatorUtils; if (!controlCreatorUtils) { return []; } // 可编辑单元格 - Object.assign(editorTd, { + const editorTd = Object.assign({}, editorTdTemplate, { id: `td_${generateId()}`, appearance: { class: 'farris-group-wrap--input' @@ -92,11 +92,13 @@ export function useTableCreator( // 隐藏单元格 - const invisibleTd = resolver.getSchemaByType('table-td') as ComponentSchema; - invisibleTd.id = `td_${generateId()}`; - invisibleTd.invisible = true; - invisibleTd.markCol = 1; - invisibleTd.markRow = 0; + const invisibleTdTemplate = resolver.getSchemaByType('table-td') as ComponentSchema; + const invisibleTd = Object.assign({}, invisibleTdTemplate, { + id: `td_${generateId()}`, + invisible: true, + markCol: 1, + markRow: 0 + }); return [editorTd, invisibleTd]; } @@ -128,9 +130,11 @@ export function useTableCreator( * 创建table组件内层级结构 */ function createTableComponentContents(buildInfo: ComponentBuildInfo) { - const table = resolver.getSchemaByType('table') as ComponentSchema; - const section = resolver.getSchemaByType('section') as ComponentSchema; - const responseForm = resolver.getSchemaByType('response-form') as ComponentSchema; + const tableTemplate = resolver.getSchemaByType('table') as ComponentSchema; + const sectionTemplate = resolver.getSchemaByType('section') as ComponentSchema; + const responseFormTemplate = resolver.getSchemaByType('response-form') as ComponentSchema; + + const table = Object.assign({}, tableTemplate); resolveTableColumns(buildInfo.selectedFields || [], table); Object.assign(table, { @@ -139,14 +143,14 @@ export function useTableCreator( class: 'table table-bordered f-table-has-form' } }); - Object.assign(responseForm, { + const responseForm = Object.assign({}, responseFormTemplate, { id: buildInfo.componentId + '-form', appearance: { class: 'f-form-layout farris-form f-form-is-table' }, contents: [table] }); - Object.assign(section, { + const section = Object.assign({}, sectionTemplate, { id: buildInfo.componentId + '-section', appearance: { class: 'f-section-oa-table f-section-in-mainsubcard' @@ -160,10 +164,10 @@ export function useTableCreator( } function createComponentNode(buildInfo: ComponentBuildInfo): any { - const componentNode = resolver.getSchemaByType('component') as ComponentSchema; + const componentNodeTemplate = resolver.getSchemaByType('component') as ComponentSchema; const contents = createTableComponentContents(buildInfo); - Object.assign(componentNode, { + const componentNode = Object.assign({}, componentNodeTemplate, { id: `${buildInfo.componentId}-component`, viewModel: `${buildInfo.componentId}-component-viewmodel`, componentType: buildInfo.componentType, diff --git a/packages/ui-vue/components/table/src/schema/schema-resolver.ts b/packages/ui-vue/components/table/src/schema/schema-resolver.ts index 10cabe9e8..b6c74a76f 100644 --- a/packages/ui-vue/components/table/src/schema/schema-resolver.ts +++ b/packages/ui-vue/components/table/src/schema/schema-resolver.ts @@ -2,6 +2,7 @@ import { DynamicResolver } from '@farris/ui-vue/components/dynamic-resolver'; import { DesignerComponentInstance, DesignerHostService } from "@farris/ui-vue/components/designer-canvas"; import { ComponentBuildInfo } from "@farris/ui-vue/components/component"; import { useTableCreator } from '../designer/use-table-creator'; +import { generateId } from '../composition/td-factory'; export function schemaResolver( resolver: DynamicResolver, @@ -10,7 +11,7 @@ export function schemaResolver( designerHostService?: DesignerHostService): Record { const parentComponentInstance = context.parentComponentInstance as DesignerComponentInstance; if (parentComponentInstance && designerHostService) { - const randomNumber = Math.random().toString(36).slice(2, 6); + const randomNumber = generateId(); const componentBuildInfo: ComponentBuildInfo = { componentId: `table-${randomNumber}`, componentName: context.bindingSourceContext?.entityTitle || context.bindingSourceContext?.bindingEntity?.name || `表格-${randomNumber}`, -- Gitee From 8843bc470ffbc254fcb92522e112bbf63e6ca021 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 20 Jul 2026 10:10:05 +0800 Subject: [PATCH 24/26] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/table/src/designer/table.design.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-vue/components/table/src/designer/table.design.component.tsx b/packages/ui-vue/components/table/src/designer/table.design.component.tsx index 852fccd8d..021593bed 100644 --- a/packages/ui-vue/components/table/src/designer/table.design.component.tsx +++ b/packages/ui-vue/components/table/src/designer/table.design.component.tsx @@ -97,7 +97,7 @@ export default defineComponent({ function onDocMouseDown(evt: MouseEvent) { const panel = document.querySelector('.editorPanel'); - if (!panel || !panel.contains(evt.target as Node)) { return; }; + if (!panel || !panel.contains(evt.target as Node)) { return; } if (elementRef.value && !elementRef.value.contains(evt.target as Node)) { selectedCellId.value = ''; } -- Gitee From e7f595501010b4254062f68ac7d36ecca9a12a63 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 20 Jul 2026 10:49:35 +0800 Subject: [PATCH 25/26] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui-vue/components/table/src/composition/td-factory.ts | 6 +++++- packages/ui-vue/components/table/src/composition/types.ts | 4 ++-- packages/ui-vue/components/table/src/table.props.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ui-vue/components/table/src/composition/td-factory.ts b/packages/ui-vue/components/table/src/composition/td-factory.ts index 8f7ad2e48..da5af975b 100644 --- a/packages/ui-vue/components/table/src/composition/td-factory.ts +++ b/packages/ui-vue/components/table/src/composition/td-factory.ts @@ -8,12 +8,16 @@ export function generateId(): string { export function createTableTdMetadata(): TableCell { const tdMetadata = getSchemaByType('table-td') as TableCell; - return Object.assign(tdMetadata, { + return Object.assign({}, tdMetadata, { id: `td_${generateId()}`, tdType: 'editor', staticText: { text: '', require: false }, editor: { type: null }, binding: null, + colspan: 1, + rowspan: 1, + align: 'right', + valign: 'middle', appearance: { class: 'farris-group-wrap--input', style: '' } }); } diff --git a/packages/ui-vue/components/table/src/composition/types.ts b/packages/ui-vue/components/table/src/composition/types.ts index c209da2f1..5428cedf4 100644 --- a/packages/ui-vue/components/table/src/composition/types.ts +++ b/packages/ui-vue/components/table/src/composition/types.ts @@ -10,8 +10,8 @@ export interface TableCell { binding: any, colspan: number; rowspan: number; - align?: 'left' | 'center' | 'right'; - valign?: 'top' | 'middle' | 'bottom'; + align: 'left' | 'center' | 'right'; + valign: 'top' | 'middle' | 'bottom'; appearance: { class: string; style: string; }; // 合并区域内偏移量(锚点=0,0);缺省即视为 0 markRow?: number; diff --git a/packages/ui-vue/components/table/src/table.props.ts b/packages/ui-vue/components/table/src/table.props.ts index 2bd5dcb84..cf8c7eb28 100644 --- a/packages/ui-vue/components/table/src/table.props.ts +++ b/packages/ui-vue/components/table/src/table.props.ts @@ -57,7 +57,7 @@ export const TableCellPropsResolver = createPropsResolver(tableC export const tableRowProps = { id: { type: String, default: '' }, type: { type: String, default: '' }, - rows: { type: Array as PropType, default: () => [] } + columns: { type: Array as PropType, default: () => [] } } as Record; -- Gitee From 5fb1ff485522adcdf78587f8d6a1a69d3a1c9541 Mon Sep 17 00:00:00 2001 From: wang-xh Date: Mon, 20 Jul 2026 15:41:35 +0800 Subject: [PATCH 26/26] =?UTF-8?q?fix:=20=E6=94=AF=E6=8C=81=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=A0=BC=E7=BB=91=E5=AE=9A=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../use-pc-control-creator.service.ts | 162 ++++++++++++++++-- .../src/components/designer.component.tsx | 2 +- .../src/components/types/designer-context.ts | 6 +- .../designer/src/components/types/metadata.ts | 2 + .../composition/entity/schema-dom-mapping.ts | 23 +++ .../table-td.property-config.ts | 19 +- 6 files changed, 187 insertions(+), 27 deletions(-) diff --git a/packages/designer/src/components/composition/control-creator/use-pc-control-creator.service.ts b/packages/designer/src/components/composition/control-creator/use-pc-control-creator.service.ts index c59e46f3c..6afad0798 100644 --- a/packages/designer/src/components/composition/control-creator/use-pc-control-creator.service.ts +++ b/packages/designer/src/components/composition/control-creator/use-pc-control-creator.service.ts @@ -1,16 +1,16 @@ -import { getSchemaByType } from "@farris/ui-vue/components"; +import { getSchemaByType, SchemaDOMMapping } from "@farris/ui-vue/components"; import { omit } from "lodash-es"; -import { FormBindingType, FormSchemaEntityField, FormSchemaEntityFieldTypeName, UseControlCreator, UseFormStateMachine, UseSchemaService } from "../../types"; +import { FormBindingType, FormSchemaEntityField, FormSchemaEntityFieldTypeName, FormVariable, FormVariableCategory, UseControlCreator, UseFormSchema, UseFormStateMachine, UseSchemaService } from "../../types"; import { FormMetadataConverter } from "../form-metadata-converter"; -export function usePCControlCreator(schemaService: UseSchemaService, formStateMachineUtils: UseFormStateMachine): UseControlCreator { +export function usePCControlCreator(schemaService: UseSchemaService, formStateMachineUtils: UseFormStateMachine, useFormSchema: UseFormSchema): UseControlCreator { /** * 获取控件的只读属性值 */ - function resolveControlReadonly(field: FormSchemaEntityField): any { + function resolveControlReadonly(field?: FormSchemaEntityField): any { // 若字段为只读,则控件为只读 - if (field.readonly) { + if (field?.readonly) { return true; } // 根据当前页面状态机中是否包含editable状态,判断控件的只读 @@ -108,12 +108,12 @@ export function usePCControlCreator(schemaService: UseSchemaService, formStateMa } // 图像 if (formEditor.type === 'image') { - formGroupMetadata.showLabelType='none'; + formGroupMetadata.showLabelType = 'none'; formEditor.rootId = 'default-root'; } // 头像 if (formEditor.type === 'avatar') { - formGroupMetadata.showLabelType='none'; + formGroupMetadata.showLabelType = 'none'; } // 帮助类型 if (formEditor.type === 'lookup' && field.editor?.$type === 'lookup') { @@ -305,23 +305,161 @@ export function usePCControlCreator(schemaService: UseSchemaService, formStateMa formEditor.textField = 'name'; break; } - case 'image':{ - formGroupMetadata.showLabelType='none'; + case 'image': { + formGroupMetadata.showLabelType = 'none'; formEditor.rootId = 'default-root'; break; } - case 'avatar':{ - formGroupMetadata.showLabelType='none'; + case 'avatar': { + formGroupMetadata.showLabelType = 'none'; break; } } return formGroupMetadata; } + function getViewModelIdOfVariable(variableId: string) { + const viewModels = useFormSchema.getViewModels(); + if (!viewModels || viewModels.length === 0) { + return; + } + + for (const viewModel of viewModels) { + const variable = viewModel.states.find(v => v.id === variableId); + if (variable) { + return viewModel.id; + } + } + } + /** + * 获取表单变量在schema中的记录。 + */ + function getRemoteVariableInSchema(variableId: string): FormSchemaEntityField | undefined { + + const schema = useFormSchema.getSchemas(); + const voVariables = schema?.variables || []; + + return voVariables?.find(v => v.id === variableId); + } + /** + * 组装变量的绑定信息 + */ + function resolveBindingForVariable(varField: FormVariable, componentId?: string): any { + if (!varField.category || varField.category === FormVariableCategory.locale) { + // 本地变量 + // 判断本地变量是否为当前组件的,若不是,需要修改path属性 + let isVaribleAndControlInSameComponent = true; + if (componentId) { + const variableViewModelId = getViewModelIdOfVariable(varField.id); + const cmp = useFormSchema.getComponentById(componentId); + isVaribleAndControlInSameComponent = variableViewModelId === cmp?.viewModel; + } + return { + type: FormBindingType.Variable, + path: isVaribleAndControlInSameComponent ? varField.code : 'root-component.' + varField.code, + field: varField.id, + fullPath: varField.code + }; + } else { + // 后端变量 + return { + type: FormBindingType.Variable, + path: 'root-component.' + varField.code, + field: varField.id, + fullPath: varField.code + }; + } + } + /** + * 根据变量创建输入控件 + * @param varField 变量 + * @param editorType 设定的编辑器类型 + * @param controlClass 控件 样式 + * @param componentId 输入控件所在组件id + * @returns form-group控件 + */ + function createFormGroupWithVariable(varField: FormVariable, editorType?: string, controlClass = 'col-12 col-md-6 col-xl-3 col-el-2', componentId?: string): any { + + const formGroupMetadata = getSchemaByType('form-group') || {}; + formGroupMetadata.id = varField.id.length > 8 ? varField.id.slice(0, 8) : varField.id; + formGroupMetadata.id = varField.code + '_' + formGroupMetadata.id.replace(/-/g, '_') + '_' + Math.random().toString(36).substring(2, 4); + formGroupMetadata.label = varField.name; + formGroupMetadata.appearance = { class: controlClass || '' }; + formGroupMetadata.binding = resolveBindingForVariable(varField, componentId); + + const resolvedEditorType = editorType || SchemaDOMMapping.getEditorTypeByVariableType(varField.type); + const formEditor = getSchemaByType(resolvedEditorType) || {}; + formGroupMetadata.editor = formEditor?.type ? formEditor : { type: 'input-group' }; + formEditor.readonly = resolveControlReadonly(); + + // 表单变量在schema中的记录 + const varInSchema = getRemoteVariableInSchema(varField.id); + + switch (varField.type) { + // 数字类型 + case 'Number': { + if (formEditor.type === 'check-box' || formEditor.type === 'switch') { + formEditor.trueValue = 1; + formEditor.falseValue = 0; + } else { + formEditor.precision = varInSchema?.type?.precision || 0; + formEditor.nullable = true; + + } + if (varInSchema?.type?.length) { + formEditor.maxLength = varInSchema.type.length; + } + break; + } + case 'String': { + if (formEditor.type === 'check-box' || formEditor.type === 'switch') { + formEditor.trueValue = 'true'; + formEditor.falseValue = 'false'; + } + if (varInSchema?.type?.length) { + formEditor.maxLength = varInSchema.type.length; + } + break; + } + } + switch (formEditor.type) { + // 多行文本 + case 'textarea': { + formEditor.resizable = true; + break; + } + // 日期 + case 'date-picker': { + formEditor.fieldType = varField.type; + + // 日期时间类型字段:启用时间选择属性 + if (varField.type === 'DateTime') { + formEditor.showTime = true; + formEditor.displayFormat = 'yyyy-MM-dd HH:mm:ss'; + formEditor.valueFormat = 'yyyy-MM-dd HH:mm:ss'; + } + break; + } + // 图像 + case 'image': { + formGroupMetadata.showLabelType = 'none'; + formEditor.rootId = 'default-root'; + break; + } + // 头像 + case 'avatar': { + formGroupMetadata.showLabelType = 'none'; + break; + } + } + + return formGroupMetadata; + } return { setFormFieldProperty, setGridFieldProperty, createFormGroupWithoutField, mapControlType2GridFieldType, - setGridFieldFormatter + setGridFieldFormatter, + createFormGroupWithVariable }; } diff --git a/packages/designer/src/components/designer.component.tsx b/packages/designer/src/components/designer.component.tsx index 14992da8e..236f4635b 100644 --- a/packages/designer/src/components/designer.component.tsx +++ b/packages/designer/src/components/designer.component.tsx @@ -84,7 +84,7 @@ export default defineComponent({ // metadatFullPath const metadataPath: string = inject(MetadataPathToken, ''); // 控件创建服务 - const controlCreatorService = designerContext.useControlCreator(schemaService, useFormStateMachineComposition); + const controlCreatorService = designerContext.useControlCreator(schemaService, useFormStateMachineComposition, useFormSchemaComposition); provide('controlCreatorUtils', controlCreatorService); provide('formMetadataConverter', new FormMetadataConverter()); diff --git a/packages/designer/src/components/types/designer-context.ts b/packages/designer/src/components/types/designer-context.ts index 8aaed3a0b..829f54e66 100644 --- a/packages/designer/src/components/types/designer-context.ts +++ b/packages/designer/src/components/types/designer-context.ts @@ -42,14 +42,14 @@ export interface UseDesignerContext { dgControl?: any; uiModel?: any; instances?: Record; - responsiveForm?:boolean; + responsiveForm?: boolean; designerMode: DesignerMode; terminalRef: Ref; toolboxItems: Ref; componentsToRegister: any[]; supportedControllers: any; controllCategories: any; - useControlCreator: (schemaService: UseSchemaService, useFormStateMachineComposition: UseFormStateMachine) => UseControlCreator; + useControlCreator: (schemaService: UseSchemaService, useFormStateMachineComposition: UseFormStateMachine, useFormSchema: UseFormSchema) => UseControlCreator; getPageComponents: (useFormSchema: UseFormSchema) => FormComponent[]; /** 查询元数据内容 */ @@ -77,5 +77,5 @@ export interface UseDesignerContext { schemaDOMMapping?: Record; - dynamicScriptService?: DynamicScriptService|undefined; + dynamicScriptService?: DynamicScriptService | undefined; } diff --git a/packages/designer/src/components/types/metadata.ts b/packages/designer/src/components/types/metadata.ts index d1856be4c..02b6b27c4 100644 --- a/packages/designer/src/components/types/metadata.ts +++ b/packages/designer/src/components/types/metadata.ts @@ -337,6 +337,8 @@ export interface UseControlCreator { createFormGroupWithoutField: (editorType: string, controlClass: string) => any; setGridFieldFormatter: (gridFieldType: string, metadata: any, schemaField: any) => void; mapControlType2GridFieldType: (field: FormSchemaEntityField) => string; + createFormGroupWithVariable(varField: FormVariable, editorType?: string, controlClass?: string, componentId?: string): any; + } export interface UseFormStateMachine { diff --git a/packages/ui-vue/components/property-panel/src/composition/entity/schema-dom-mapping.ts b/packages/ui-vue/components/property-panel/src/composition/entity/schema-dom-mapping.ts index 2a08d4fe2..7dbb8c3e2 100644 --- a/packages/ui-vue/components/property-panel/src/composition/entity/schema-dom-mapping.ts +++ b/packages/ui-vue/components/property-panel/src/composition/entity/schema-dom-mapping.ts @@ -1,6 +1,7 @@ import { DgControl } from "@farris/ui-vue/components/designer-canvas"; import { FormPropertyChangeObject } from "./property-entity"; +import { GSPElementDataType } from "@farris/ui-vue/components/common"; export class SchemaDOMMapping { @@ -177,4 +178,26 @@ export class SchemaDOMMapping { } return array; } + /** + * 根据变量类型获取默认控件编辑器类型 + */ + static getEditorTypeByVariableType(type: string) { + switch (type) { + case 'String': { + return DgControl['input-group'].type; + } + case 'Number': { + return DgControl['number-spinner'].type; + } + case 'Boolean': { + return DgControl['check-box'].type; + } + case 'Date': case 'DateTime': { + return DgControl['date-picker'].type; + } + case 'Text': { + return DgControl['textarea'].type; + } + } + } } diff --git a/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts index c4688b122..2bd945146 100644 --- a/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts +++ b/packages/ui-vue/components/table/src/property-config/table-td.property-config.ts @@ -147,7 +147,7 @@ export class TableTdProperty extends InputBaseProperty { */ private getEditorBindingAndTypePropConfig(cell: any) { const self = this; - if (cell.tdType !== 'editor' || !cell.editor?.type) { + if (cell.tdType !== 'editor') { return; } const { canChangeControlType, editorTypeList } = this.getAvailableEditorType(cell); @@ -207,7 +207,6 @@ export class TableTdProperty extends InputBaseProperty { setPropertyRelates(changeObject: FormPropertyChangeObject, propData, parameters) { switch (changeObject.propertyID) { case 'binding': { - // 变更字段类型:修改原控件DOM结构 self.changeControlAfterBindingChanged(changeObject, cell, parameters); // 刷新实体树 changeObject.needRefreshEntityTree = true; @@ -233,7 +232,9 @@ export class TableTdProperty extends InputBaseProperty { } - + /** + * 切换绑定后,需要更新控件结构 + */ private changeControlAfterBindingChanged(changeObject: FormPropertyChangeObject, propertyData: any, parameters: any) { // 清除绑定:触发界面更新 @@ -255,16 +256,13 @@ export class TableTdProperty extends InputBaseProperty { changeObject['controlEditorChanged'] = true; } else { - // 没有变更字段类型:不需要清除原控件结构,只需要同步更新path属性和帮助相关属性 // 没有变更字段类型,只同步单元格文本即可 propertyData.editorLabel = parameters.name; - // this.updateControlDomAfterChangingBinding(changeObject, propertyData.editor, parameters, viewModelId); } - - // 切换绑定信息后,需要使用新字段的受控规则 - } - + /** + * 创建新控件 + */ private createControl(bindingType: string, selectedField: any, editorType?: string, oldEditor?: any) { let newControl; if (!bindingType) { @@ -274,8 +272,7 @@ export class TableTdProperty extends InputBaseProperty { if (bindingType === FormBindingType.Form) { newControl = this.controlCreatorUtils.setFormFieldProperty(selectedField, editorType); } else { - // 暂不支持变量 - // newEditor = this.controlCreatorUtils.create(selectedField, editorType, this.componentId); + newControl = this.controlCreatorUtils.createFormGroupWithVariable(selectedField, editorType, '', this.componentId); } -- Gitee