diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e5528b3f5efa6af7ff8b4b9ba0bc9b98027424..a7351bf0e7c790f4aa3f41f947b1131531fa57f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [Unreleased] +### Added + +- 新增重复器表格样式2组件 + ## [0.7.41-alpha.37] - 2025-11-14 ### 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 016531b214a6697af4a53802eb54ee3f6ad772aa..301f0fa0a8a8c5e6de3564a4931541636c5dac5f 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 @@ -4,6 +4,7 @@ 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'; +import { RepeaterGrid2 } from './repeater-grid2/repeater-grid2'; export const FormMDCtrlRepeater = defineComponent({ name: 'IBizFormMDCtrlRepeater', @@ -32,6 +33,16 @@ export const FormMDCtrlRepeater = defineComponent({ > ); case 'Grid': + const gridStyle = this.controller.model.ctrlParams?.gridStyle; + if (gridStyle === 'style2') { + return ( + + ); + } return ( = + defineComponent({ + name: 'IBizRepeaterGrid2', + props: { + controller: { + type: FormMDCtrlRepeaterController, + required: true, + }, + }, + emits: { + change: (_value: IData[]) => true, + }, + setup(props, { emit }) { + const ns = useNamespace('repeater-grid2'); + const formItems: IDEFormItem[] = []; + const tableRef = ref(); + const tableKey = ref(createUUID()); + const chunkSize = props.controller.model.ctrlParams?.chunkSize + ? Number(props.controller.model.ctrlParams?.chunkSize) + : 100; + const { renderItems, loadMore, updateTotalItems } = useLoadMore( + props.controller.value as IData[], + chunkSize, + ); + // 遍历所有的项,如果有逻辑的话加入 + 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 ctx = useCtx(); + const formControllers = reactive([]); + 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 as IData[] | null, + newVal => { + if (newVal && newVal.length > 0) { + 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); + } + }); + } + updateTotalItems(newVal || []); + }, + { immediate: true, deep: true }, + ); + + let sortable: Sortable | undefined; + + const rowDrop = () => { + const wrapper = tableRef.value?.$el?.querySelector( + '.el-table__body-wrapper tbody', + ); + if (!wrapper || !props.controller.enableSort) return; + sortable = Sortable.create(wrapper, { + animation: 150, + handle: `.${ns.e('drag-icon')}`, + ghostClass: `${ns.e('sortable-ghost')}`, + onEnd({ newIndex, oldIndex }) { + props.controller.dragChange(oldIndex!, newIndex!); + tableKey.value = createUUID(); + }, + }); + }; + + watch( + () => tableRef.value, + () => { + if (!props.controller.enableSort) return; + // eslint-disable-next-line no-unused-expressions + tableRef.value ? rowDrop() : sortable?.destroy(); + }, + ); + + onUnmounted(() => sortable?.destroy()); + + const renderRemoveBtn = (index: number) => { + if (ibiz.config.form.mdCtrlConfirmBeforeRemove) { + return ( + { + props.controller.remove(index); + formControllers.splice(index, 1); + }} + > + {{ + reference: () => { + return ( + + ); + }, + }} + + ); + } + return ( + { + props.controller.remove(index); + formControllers.splice(index, 1); + }} + > + ); + }; + + return { + ns, + tableRef, + tableKey, + formItems, + renderItems, + formControllers, + renderRemoveBtn, + loadMore, + }; + }, + render() { + const tableHeight = this.controller.model.layoutPos?.height; + const heightObject = tableHeight ? { height: tableHeight } : {}; + const isEmpty = this.renderItems.length === 0; + return ( +
+ { + const shouldShowIndex = this.controller.enableSort + ? columnIndex === 1 + : columnIndex === 0; + return shouldShowIndex ? this.ns.b('index') : ''; + }} + {...heightObject} + > + {{ + default: (): VNodeArrayChildren => { + return [ + this.controller.enableSort && ( + + {{ + default: () => { + if (isEmpty) { + return ''; + } + return ( + + + + + + + + ); + }, + }} + + ), + + {{ + default: (opts: IData) => { + if (isEmpty) { + return ''; + } + const { $index } = opts; + return {$index + 1}; + }, + }} + , + this.formItems.length > 0 && + this.formItems.map(item => { + // 重复器表格列自适应(表单项label宽度配置为1) + const width = item.labelWidth; + let widthName = 'width'; + let columnWidth = ''; + if (typeof width === 'number') { + if (width === 1) { + widthName = 'min-width'; + } + columnWidth = `${width}px`; + } + return ( + + {{ + default: (opts: IData) => { + if (isEmpty) { + return ''; + } + const { $index } = opts; + 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} + + ); + }, + }} +
+ ); + }), + + {{ + default: (opts: IData) => { + const { $index } = opts; + return ( +
+ {this.controller.enableCreate && ( + { + this.controller.create( + isEmpty ? 0 : $index + 1, + ); + }} + > + )} + {this.controller.enableDelete && + !isEmpty && + this.renderRemoveBtn($index)} +
+ ); + }, + }} +
, + ]; + }, + append: () => { + return [ + tableHeight && ( +
this.loadMore()} + infinite-scroll-distance={20} + >
+ ), + ]; + }, + }} +
+
+ ); + }, + });