From 6ae697a0520f70861301944fd013154007286d8c Mon Sep 17 00:00:00 2001 From: zhf <1204297681@qq.com> Date: Tue, 18 Nov 2025 23:53:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=99=A8=E8=A1=A8=E6=A0=BC=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 + .../form-mdctrl-repeater.tsx | 9 + .../repeater-grid/repeater-grid.scss | 84 +++++ .../repeater-grid/repeater-grid.tsx | 330 ++++++++++++++++++ src/locale/en/index.ts | 4 + src/locale/zh-CN/index.ts | 4 + 6 files changed, 435 insertions(+) create mode 100644 src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.scss create mode 100644 src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f45c806b6c1..f2e5eef5dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [Unreleased] +### Added + +- 新增重复器表格组件 + ## [0.7.41-alpha.20] - 2025-11-17 ### Added diff --git a/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/form-mdctrl-repeater.tsx b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/form-mdctrl-repeater.tsx index 59b936b9e05..b8941cd2027 100644 --- a/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/form-mdctrl-repeater.tsx +++ b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/form-mdctrl-repeater.tsx @@ -3,6 +3,7 @@ import { FormMDCtrlRepeaterController } from '@ibiz-template/runtime'; import { useNamespace } from '@ibiz-template/vue3-util'; import { RepeaterSingleForm } from './repeater-single-form/repeater-single-form'; import { RepeaterMultiForm } from './repeater-multi-form/repeater-multi-form'; +import { RepeaterGrid } from './repeater-grid/repeater-grid'; export const FormMDCtrlRepeater = defineComponent({ name: 'IBizFormMDCtrlRepeater', @@ -39,6 +40,14 @@ export const FormMDCtrlRepeater = defineComponent({ onChange={this.onDataChange} > ); + case 'Grid': + return ( + + ); default: return (
diff --git a/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.scss b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.scss new file mode 100644 index 00000000000..fea74ee76a2 --- /dev/null +++ b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.scss @@ -0,0 +1,84 @@ +$repeater-grid: ( + // 颜色 + th-color: getCssVar(color, text, 2), + td-color: getCssVar(color, text, 0), + btn-color: getCssVar(color, text, 2), + + // 间距 + cell-padding: getCssVar(spacing, extra-tight) getCssVar(spacing, tight), + btn-margin-left: getCssVar(spacing, tight), + + // 行高 + cell-line-height: getCssVar(editor, default, line-height), + + // 边框 + cell-border: 1px solid getCssVar(color, border), + btn-border: 1px solid getCssVar(color, border), + + // 大小 + btn-size: getCssVar(width-icon, large), +); + +@include b(repeater-grid) { + @include set-component-css-var(repeater-grid, $repeater-grid); + + width: 100%; + overflow: auto; + + table { + table-layout: fixed; + border-collapse: collapse; + } + + th { + min-width: 0; + padding: getCssVar(repeater-grid, cell-padding); + overflow: hidden; + line-height: getCssVar(repeater-grid, cell-line-height); + color: getCssVar(repeater-grid, th-color); + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + border: getCssVar(repeater-grid, cell-border); + } + + td { + min-width: 0; + padding: getCssVar(repeater-grid, cell-padding); + overflow: hidden; + line-height: getCssVar(repeater-grid, cell-line-height); + color: getCssVar(repeater-grid, td-color); + text-align: center; + border: getCssVar(repeater-grid, cell-border); + } + + @include b(repeater-grid-action-cell) { + border: none; + } +} + +@include b(repeater-grid-action-group) { + display: flex; + align-items: center; + justify-content: center; + height: getCssVar(repeater-grid, cell-line-height); + + ion-icon { + box-sizing: border-box; + font-size: getCssVar(repeater-grid, btn-size); + color: getCssVar(repeater-grid, btn-color); + cursor: pointer; + border: getCssVar(repeater-grid, btn-border); + + + ion-icon { + margin-left: getCssVar(repeater-grid, btn-margin-left); + } + } +} + +@include b(repeater-grid-empty-content) { + td { + color: getCssVar(repeater-grid, th-color); + } +} + diff --git a/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.tsx b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.tsx new file mode 100644 index 00000000000..c4d1d1fe94c --- /dev/null +++ b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.tsx @@ -0,0 +1,330 @@ +import { + defineComponent, + h, + onMounted, + onUnmounted, + reactive, + ref, + resolveComponent, + toRaw, + watch, +} from 'vue'; +import { + ControlVO, + EditFormController, + EventBase, + FormMDCtrlRepeaterController, + IEditFormController, +} from '@ibiz-template/runtime'; +import { useCtx, useNamespace } from '@ibiz-template/vue3-util'; +import { IDEFormDetail, IDEFormItem } from '@ibiz/model-core'; +import { recursiveIterate } from '@ibiz-template/core'; +import './repeater-grid.scss'; + +export const RepeaterGrid = defineComponent({ + name: 'IBizRepeaterGrid', + props: { + controller: { + type: FormMDCtrlRepeaterController, + required: true, + }, + }, + emits: { + change: (_value: IData[]) => true, + }, + setup(props, { emit }) { + const ns = useNamespace('repeater-grid'); + + // 表单项 + const formItems: IDEFormItem[] = []; + + // 数据 + const items = ref([]); + + // 上下文 + const ctx = useCtx(); + + // 表单控制器 + const formControllers = reactive([]); + + // 容器 + const container = ref(); + + // 表格 + const table = ref(); + + // 表头 + const thead = ref(); + + // 自适应宽度 + const flexWidth = ref(0); + + // 表格宽度 + const gridWidth = ref(''); + + // 容器大小变化监听器 + let resizeObserver: ResizeObserver | undefined; + + recursiveIterate( + props.controller.repeatedForm, + (item: IDEFormDetail) => { + if (item.detailType === 'FORMITEM') { + // 隐藏表单项不绘制 + if ((item as IDEFormItem).editor?.editorType !== 'HIDDEN') { + formItems.push(item); + } + } + }, + { + childrenFields: ['deformPages', 'deformTabPages', 'deformDetails'], + }, + ); + + // 单条数据改变时,更新数据 + const onSingleValueChange = (value: IData, index: number) => { + const arrData = [...((props.controller.value || []) as IData[])]; + arrData[index] = value; + emit('change', arrData); + }; + + // 添加表单控制器 + const addFormController = async (data: IData = {}) => { + const formC = new EditFormController( + props.controller.repeatedForm, + props.controller.context, + props.controller.params, + ctx, + ); + formC.state.isSimple = true; + await formC.created(); + formC.setSimpleData(data); + formControllers.push(formC); + props.controller.setRepeaterController( + `${formControllers.length - 1}`, + formC, + ); + formC.evt.on('onFormDataChange', (event: EventBase) => { + // 隔离抛出不一样的对象 + const item = event.data[0]; + const formData = item instanceof ControlVO ? item.clone() : { ...item }; + const index = formControllers.indexOf(formC); + onSingleValueChange(formData, index); + }); + }; + + watch( + () => props.controller.value, + newVal => { + if (Array.isArray(newVal)) { + newVal.forEach((item, index) => { + const formC = formControllers[index] as EditFormController; + if (formC) { + const changeVal = item || {}; + // 找有没有不一致的属性 + const find = Object.keys(formC.data).find(key => { + return changeVal[key] !== formC.data[key]; + }); + // 内外部数据不一致时,只能是外部修改了,这是更新数据并重走load + if (find) { + formC.setSimpleData(changeVal); + } + } else { + addFormController(item); + } + }); + items.value = newVal; + } else { + items.value = []; + } + }, + { + immediate: true, + deep: true, + }, + ); + + onMounted(() => { + if (!container.value || !table.value || !thead.value) { + return; + } + resizeObserver = new ResizeObserver(() => { + if (!container.value || !table.value || !thead.value) { + return; + } + + const containerWidth = container.value.offsetWidth; + const theadRow = thead.value.children[0]; + if (!theadRow) return; + const thElements = Array.from(theadRow.children) as HTMLElement[]; + if (thElements.length === 0) return; + + let fixedWidthSum = 0; + const flexThElements: HTMLElement[] = []; + + thElements.forEach(th => { + const isFlow = th.dataset.flex === 'true'; + if (isFlow) { + flexThElements.push(th); + } else { + const thWidth = th.style.width + ? parseFloat(th.style.width) + : th.offsetWidth; + fixedWidthSum += thWidth; + } + }); + + const availableSpace = containerWidth - fixedWidthSum - 1; + const flowColumnCount = flexThElements.length; + + if (flowColumnCount > 0 && availableSpace > 0) { + flexWidth.value = availableSpace / flowColumnCount; + gridWidth.value = `${containerWidth}px`; + } else { + flexWidth.value = 0; + gridWidth.value = `${fixedWidthSum}px`; + } + }); + resizeObserver.observe(container.value); + }); + + onUnmounted(() => { + if (resizeObserver) { + resizeObserver.disconnect(); + } + }); + + return { + ns, + formItems, + formControllers, + items, + container, + table, + thead, + flexWidth, + gridWidth, + }; + }, + render() { + const isEmpty = !this.items.length; + + // 表头行 + const headers = [ + + {ibiz.i18n.t('control.form.repeaterGrid.index')} + , + this.formItems.map(item => { + return ( + + {item.caption} + + ); + }), + (this.controller.enableCreate || this.controller.enableDelete) && ( + + ), + ]; + + // 表格行 + const rows = ( + isEmpty && this.controller.enableCreate ? [{}] : this.items + ).map((_row: IData, index: number) => { + return ( + + {isEmpty ? '' : index + 1} + {this.formItems.map(item => { + if (isEmpty) { + return ; + } + const formC = toRaw( + this.formControllers[index], + ) as EditFormController; + if (!formC || !formC.state.isLoaded) { + return ( + {ibiz.i18n.t('control.form.repeaterGrid.absentOrLoad')} + ); + } + const formItemC = formC.formItems.find(x => x.name === item.id)!; + + let editor = null; + if (!formItemC.editorProvider) { + editor = ; + } else { + const component = resolveComponent( + formItemC.editorProvider.formEditor, + ); + editor = h(component, { + value: formItemC.value, + data: formItemC.data, + controller: formItemC.editor, + disabled: formItemC.state.disabled, + readonly: formItemC.state.readonly, + onChange: (val: unknown, name?: string): void => { + formItemC.setDataValue(val, name); + }, + }); + } + return {editor}; + })} + {(this.controller.enableCreate || this.controller.enableDelete) && ( + +
+ {this.controller.enableCreate && ( + { + this.controller.create(isEmpty ? 0 : index + 1); + }} + > + )} + {this.controller.enableDelete && !isEmpty && ( + { + this.controller.remove(index); + this.formControllers.splice(index, 1); + }} + > + )} +
+ + )} + + ); + }); + + return ( +
+ + + {headers} + + {rows} + {isEmpty && !this.controller.enableCreate && ( + + + + )} +
+ {ibiz.i18n.t('control.form.repeaterGrid.noData')} +
+
+ ); + }, +}); diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index bb505c22cca..302a7f78840 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -122,6 +122,10 @@ export default { search: 'Search', reset: 'Reset', }, + repeaterGrid: { + index: 'Index', + noData: 'No data', + }, }, list: { expand: 'Expand', diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index f01e76ede2f..cd53e400a0f 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -104,6 +104,10 @@ export default { search: '查询', reset: '重置', }, + repeaterGrid: { + index: '序号', + noData: '暂无数据', + }, }, list: { expand: '展开', -- Gitee