diff --git a/CHANGELOG.md b/CHANGELOG.md index 84bf226573430f3f5fdbefd8bc66bd8f75f0c36e..8a6700a6597175a35c8fa47dbacc055ca8ca7c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [Unreleased] +### Added + +- 新增重复器表单及重复器表格支持拖动排序配置 + ## [0.7.38-alpha.26] - 2024-11-19 ### Added 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 index 6397053722f6cde3d6c0a3d38547434658a638cb..c70bda4937e68fe1970778f93c9b345a848c8ee2 100644 --- 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 @@ -16,6 +16,19 @@ $repeater-grid: ( z-index: 2; } + @include e(drag-icon) { + cursor: move; + + >svg { + display: flex; + align-items: center; + justify-content: center; + width: getCssVar('spacing', 'base'); + height: getCssVar('spacing', 'base'); + fill: getCssVar(color, text, 3); + } + } + .el-table__row:hover { .#{bem(repeater-grid-index, text)}{ &:last-child{ 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 index a0b4f39a37b563f98ef889b3955a323bbdb710c2..b0d80ad102f9c0f7013fabb6fde1072d68d38d1c 100644 --- 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 @@ -1,7 +1,10 @@ +/* eslint-disable no-param-reassign */ import { defineComponent, h, + onUnmounted, reactive, + ref, resolveComponent, toRaw, watch, @@ -14,9 +17,10 @@ import { IEditFormController, } from '@ibiz-template/runtime'; import { useCtx, useNamespace } from '@ibiz-template/vue3-util'; -import { recursiveIterate, showTitle } from '@ibiz-template/core'; +import { NOOP, recursiveIterate, showTitle } from '@ibiz-template/core'; import { IDEFormDetail, IDEFormItem } from '@ibiz/model-core'; import './repeater-grid.scss'; +import { useGridDraggable } from './repeater-grid.util'; export const RepeaterGrid = defineComponent({ name: 'IBizRepeaterGrid', @@ -31,6 +35,16 @@ export const RepeaterGrid = defineComponent({ }, setup(props, { emit }) { const ns = useNamespace('repeater-grid'); + // 表格实例 + const tableRef = ref(''); + // 维护的响应式数据 + const items = ref([]); + // 处理拖拽监听事件 + const { cleanup = NOOP } = useGridDraggable( + tableRef, + props.controller, + items, + ); const formItems: IDEFormItem[] = []; // 遍历所有的项,如果有逻辑的话加入 recursiveIterate( @@ -81,6 +95,7 @@ export const RepeaterGrid = defineComponent({ () => props.controller.value as IData[] | null, newVal => { if (newVal && newVal.length > 0) { + items.value = [...newVal]; newVal.forEach((item, index) => { const formC = formControllers[index] as EditFormController; if (formC) { @@ -105,12 +120,21 @@ export const RepeaterGrid = defineComponent({ } }); } + } else { + items.value = []; } }, { immediate: true, deep: true }, ); - const renderRemoveBtn = (index: number) => { + onUnmounted(() => { + // 销毁监听事件 + if (cleanup !== NOOP) { + cleanup(); + } + }); + + const renderRemoveBtn = (index: number): JSX.Element | null => { if (!props.controller.enableDelete) { return null; } @@ -152,7 +176,60 @@ export const RepeaterGrid = defineComponent({ ); }; - return { ns, formItems, formControllers, renderRemoveBtn }; + // 绘制排序拖拽列 + const renderRowDragSort = (): JSX.Element => { + return ( + + {{ + default: () => { + return ( +
+ + + + + + + +
+ ); + }, + }} +
+ ); + }; + + return { + ns, + formItems, + formControllers, + tableRef, + items, + renderRemoveBtn, + renderRowDragSort, + }; }, render() { return ( @@ -168,14 +245,20 @@ export const RepeaterGrid = defineComponent({ )} { // 索引单元格样式 return columnIndex === 0 ? this.ns.b('index') : ''; }} + row-class-name={({ rowIndex }: IData) => { + // 用于行排序拖动过程中确定节点下标 + return `id-${rowIndex}`; + }} > + {this.controller.enableSort && this.renderRowDragSort()} {{ default: (opts: IData) => { diff --git a/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.util.ts b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5e28f852b0b05cb1f3197190a068db941888abc --- /dev/null +++ b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-grid/repeater-grid.util.ts @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { listenJSEvent } from '@ibiz-template/core'; +import { FormMDCtrlRepeaterController } from '@ibiz-template/runtime'; +import { nextTick, Ref, watch } from 'vue'; + +export function useGridDraggable( + tableRef: IData, + c: FormMDCtrlRepeaterController, + items: Ref, +): { + cleanup?: () => void; +} { + // 如果没有配置排序,则不执行以下监听拖拽节点 + if (!c.enableSort) { + return { cleanup: (): void => {} }; + } + + // 拖拽下标 + let dragIndex = 0; + + // 目标下标 + let dropIndex = 0; + + // 拖拽数据 + let draggingData: IData | null = null; + + // 目标数据 + let dropData: IData | null = null; + + // eslint-disable-next-line @typescript-eslint/ban-types + let cleanups: Function[] = []; + + const calcSrfKeyByClass = (classList: DOMTokenList): number => { + let result = ''; + classList.forEach((className: string) => { + if (className.startsWith('id-')) { + result = className.replace('id-', ''); + } + }); + return result ? Number(result) : -1; + }; + + const setRowDragEvent = (item: HTMLTableRowElement): void => { + item.setAttribute('draggable', 'true'); + const cleanDragStart = listenJSEvent( + item, + 'dragstart', + (event: DragEvent) => { + if (event.target) { + const draggingDom = event.target as HTMLElement; + // eslint-disable-next-line no-param-reassign + event.dataTransfer!.effectAllowed = 'move'; + dragIndex = calcSrfKeyByClass(draggingDom.classList); + if (items.value.length < 0) { + return; + } + draggingData = items.value[dragIndex]; + } + }, + ); + const cleanDragEnter = listenJSEvent( + item, + 'dragenter', + (event: DragEvent) => { + event.preventDefault(); + const targetDom = event.currentTarget as HTMLElement; + dropIndex = calcSrfKeyByClass(targetDom.classList); + if (items.value.length < 0 || dropIndex === -1) { + return; + } + + dropData = items.value[dropIndex]; + }, + ); + const cleanDragOver = listenJSEvent( + item, + 'dragover', + (event: DragEvent) => { + event.preventDefault(); + }, + ); + const cleanDragEnd = listenJSEvent(item, 'dragend', (event: DragEvent) => { + event.preventDefault(); + if (draggingData && dropData) { + items.value.splice(dragIndex, 1, dropData); // 移除 dragIndex 位置的数据,并插入 dropData + items.value.splice(dropIndex, 1, draggingData); // 移除 dropIndex 位置的数据,并插入 draggingData + c.setValue(items.value); + } + }); + cleanups.push(cleanDragStart); + cleanups.push(cleanDragEnter); + cleanups.push(cleanDragOver); + cleanups.push(cleanDragEnd); + }; + const cleanup = (): void => { + // eslint-disable-next-line no-shadow + cleanups.forEach(cleanup => { + cleanup(); + }); + cleanups = []; + }; + watch([() => tableRef.value, () => items.value], ([table]) => { + if (!table || !c.value || items.value.length !== c.value.length) { + return; + } + const grid = tableRef.value!.$el; + if (grid) { + cleanup(); + nextTick(() => { + const rows = grid.getElementsByClassName('el-table__row'); + rows.forEach((item: HTMLTableRowElement) => { + setRowDragEvent(item); + }); + }); + } + }); + + return { + cleanup, + }; +} diff --git a/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-multi-form/repeater-multi-form.tsx b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-multi-form/repeater-multi-form.tsx index 9fbb513ae0a78350a7c4ce519897efc860ec708b..86ae1fdcf2ea78a5e7b9d357eddab84c6ac233b5 100644 --- a/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-multi-form/repeater-multi-form.tsx +++ b/src/control/form/form-detail/form-mdctrl/form-mdctrl-repeater/repeater-multi-form/repeater-multi-form.tsx @@ -53,6 +53,7 @@ export const RepeaterMultiForm = defineComponent({ items={items} enableCreate={this.controller.enableCreate} enableDelete={this.controller.enableDelete} + enableSort={this.controller.enableSort} onAddClick={(): void => this.controller.create()} onRemoveClick={(_item: IData, index: number): void => this.controller.remove(index) @@ -62,7 +63,6 @@ export const RepeaterMultiForm = defineComponent({ item: ({ data, index }: { data: IData; index: number }) => { return ( { diff --git a/src/control/form/form-detail/form-mdctrl/mdctrl-container/icon/index.tsx b/src/control/form/form-detail/form-mdctrl/mdctrl-container/icon/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ff0750f137558187404f15ff97a1d02f5f951fbd --- /dev/null +++ b/src/control/form/form-detail/form-mdctrl/mdctrl-container/icon/index.tsx @@ -0,0 +1,19 @@ +/** + * 拖拽图标 + */ +export const dragIcon = (): JSX.Element => ( + + + + + + + +); diff --git a/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.scss b/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.scss index 9739136c29d5672ec060d0154d59d5888ccb0278..6b249d4c9a5090002334b4b6d551f143a87074ae 100644 --- a/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.scss +++ b/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.scss @@ -1,12 +1,49 @@ @include b(mdctrl-container) { + + @include e('content') { + @include when('sort') { + .#{bem('mdctrl-container-item')} { + padding-left: getCssVar('spacing', 'base'); + } + } + } } @include b(mdctrl-container-item) { @include flex(row); + position: relative; + @include e(form) { flex-grow: 1; } + + @include when(drag) { + background: getCssVar(color, border); + } + + @include e('icon-drag') { + position: absolute; + top: 50%; + left: 0; + cursor: move; + transform: translateY(-50%); + + >svg { + display: flex; + align-items: center; + justify-content: center; + width: getCssVar('spacing', 'base'); + height: getCssVar('spacing', 'base'); + fill: getCssVar(color, text, 3); + } + } + + &:has(.#{bem('control-form')}) { + .#{bem('mdctrl-container-item__icon-drag')} { + transform: translateY(calc(-50% - var(--ibiz-spacing-tight) / 2)); + } + } } @include b(mdctrl-container-item-actions) { diff --git a/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.tsx b/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.tsx index df341b05c8e75c3e546698c79cb5fe471875fd4a..0f0b7eeabb5cb6762c6ae050873fc6b9c391de5a 100644 --- a/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.tsx +++ b/src/control/form/form-detail/form-mdctrl/mdctrl-container/mdctrl-container.tsx @@ -1,10 +1,15 @@ -import { PropType, computed, defineComponent } from 'vue'; +import { PropType, computed, defineComponent, ref } from 'vue'; +import draggable from 'vuedraggable'; import { useNamespace } from '@ibiz-template/vue3-util'; import './mdctrl-container.scss'; import { showTitle } from '@ibiz-template/core'; +import { dragIcon } from './icon'; export const MDCtrlContainer = defineComponent({ name: 'IBizMDCtrlContainer', + components: { + draggable, + }, props: { enableCreate: { type: Boolean, @@ -21,20 +26,53 @@ export const MDCtrlContainer = defineComponent({ userStyle: { type: String, }, + enableSort: { + type: Boolean, + required: false, + }, + options: { + type: Object as PropType, + default: () => {}, + }, }, emits: { addClick: () => true, removeClick: (_data: IData, _index: number) => true, }, - setup(props, { emit }) { + setup(props, { emit, slots }) { const ns = useNamespace('mdctrl-container'); + // 拖拽中的项 + const draggingIndex = ref(-1); + + // 拖拽按钮类名 + const dragClssName = ns.be('item', 'icon-drag'); + + // 处理拖拽开始 + const handleDragStart = (index: number): void => { + draggingIndex.value = index; + }; + + // // 处理拖拽结束 + const handleDragEnd = (): void => { + draggingIndex.value = -1; + }; + /** 是否显示操作按钮 */ const showActions = computed(() => { return props.enableCreate || props.enableDelete; }); - const renderAddBtn = () => { + // 排序拖拽按钮 + const renderDragBtn = (): JSX.Element => { + return ( +
+ {dragIcon()} +
+ ); + }; + + const renderAddBtn = (): JSX.Element => { return ( { + if (props.userStyle === 'STYLE2') { + return ( +
+ {renderStyle2RemoveBtn(item, index)} +
+ ); + } + + return ( +
+ {index === 0 && props.enableCreate && renderAddBtn()} + {renderRemoveBtn(item, index)} +
+ ); + }; + + // 绘制项 + const renderItem = (item: IData): JSX.Element => { + const { element, index } = item; + const isDragging = index === draggingIndex.value; + + const formComponent = slots.item ? ( + slots.item({ data: element, index }) + ) : ( +
{ibiz.i18n.t('control.form.mdCtrlContainer.noSlot')}
+ ); + + return ( +
{ + handleDragStart(index); + }} + onDragend={() => { + handleDragEnd(); + }} + > + {formComponent} + {showActions.value && renderItemActions(element, index)} + {props.enableSort && renderDragBtn()} +
+ ); + }; + + // 绘制内容 + const renderContent = (): JSX.Element => { + return ( + + {{ + item: (item: IData) => renderItem(item), + }} + + ); + }; + return { ns, showActions, + renderContent, renderAddBtn, renderRemoveBtn, renderStyle2AddBtn, @@ -173,31 +281,12 @@ export const MDCtrlContainer = defineComponent({ return (
{this.items?.length ? ( -
- {this.showActions && + [ + this.showActions && this.enableCreate && - this.renderStyle2AddBtn()} - {this.items.map((item, index) => { - const formComponent = this.$slots.item ? ( - this.$slots.item({ data: item, index }) - ) : ( -
- {ibiz.i18n.t('control.form.mdCtrlContainer.noSlot')} -
- ); - - return ( -
- {formComponent} - {this.showActions && ( -
- {this.renderStyle2RemoveBtn(item, index)} -
- )} -
- ); - })} -
+ this.renderStyle2AddBtn(), + this.renderContent(), + ] ) : (
{this.enableCreate && ( @@ -213,25 +302,7 @@ export const MDCtrlContainer = defineComponent({ return (
{this.items?.length ? ( - this.items.map((item, index) => { - const formComponent = this.$slots.item ? ( - this.$slots.item({ data: item, index }) - ) : ( -
{ibiz.i18n.t('control.form.mdCtrlContainer.noSlot')}
- ); - - return ( -
- {formComponent} - {this.showActions && ( -
- {index === 0 && this.enableCreate && this.renderAddBtn()} - {this.renderRemoveBtn(item, index)} -
- )} -
- ); - }) + this.renderContent() ) : (
{this.enableCreate && (