diff --git a/CHANGELOG.md b/CHANGELOG.md index 934f2fc994b4c7e021d10032b3e845234b255874..3356fb2b2f367c370a31d1275790b84e4d5e92c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ## [Unreleased] +### Added + +- 新增看板分页栏 +- 新增卡片拖拽 +- 新增列表新建和拖拽功能 +- 新增表格动态控件参数grouprowmode,控制分组行模式 + ## [0.7.41-alpha.16] - 2025-07-30 ### Added diff --git a/src/control/data-view/data-view.tsx b/src/control/data-view/data-view.tsx index 00eb9bb559a29938572aa48e55e89cabe33c0bb8..f31e59e7d7b2b8976ca490e1e306e4402134a251 100644 --- a/src/control/data-view/data-view.tsx +++ b/src/control/data-view/data-view.tsx @@ -27,13 +27,18 @@ import { IControlProvider, IMDControlGroupState, DataViewControlController, + IDragChangeInfo, } from '@ibiz-template/runtime'; import { createUUID } from 'qx-util'; +import draggable from 'vuedraggable'; import { usePagination } from '../../util'; import './data-view.scss'; export const DataViewControl = defineComponent({ name: 'IBizDataViewControl', + components: { + draggable, + }, props: { /** * @description 数据视图(卡片)模型数据 @@ -220,6 +225,33 @@ export const DataViewControl = defineComponent({ return c.onDbRowClick(item); }; + let cacheInfo: Partial | null = null; + const onDraggableChange = (evt: IData, groupKey?: string | number) => { + if (evt.moved) { + // 排序 + c.onDragChange({ + from: groupKey!, + to: groupKey!, + fromIndex: evt.moved.oldIndex, + toIndex: evt.moved.newIndex, + }); + } + if (evt.added) { + cacheInfo = { + to: groupKey, + toIndex: evt.added.newIndex, + }; + } + if (evt.removed) { + if (cacheInfo) { + cacheInfo.from = groupKey; + cacheInfo.fromIndex = evt.removed.oldIndex; + c.onDragChange(cacheInfo as IDragChangeInfo); + } + cacheInfo = null; + } + }; + /** * @description 绘制新建卡片项 * @param {IMDControlGroupState} [group] @@ -348,13 +380,24 @@ export const DataViewControl = defineComponent({ * @param {IData[]} items * @return {*} */ - const renderCardLayout = (items: IData[], group?: IMDControlGroupState) => { + const renderCardLayout = ( + items: IData[], + group?: IMDControlGroupState, + disabled: boolean = true, + ) => { const { cardColXS, cardColSM, cardColMD, cardColLG } = c.model; if (cardColXS || cardColSM || cardColMD || cardColLG) return ( - - {items.map(item => { - return ( + onDraggableChange(evt, group?.key)} + > + {{ + item: ({ element }: { element: IData }) => ( -
{renderCard(item)}
+
{renderCard(element)}
- ); - })} - {c.enableNew && !c.state.readonly && ( - -
{renderNewCard(group)}
-
- )} -
+ ), + footer: () => { + if (c.enableNew && !c.state.readonly) + return ( + +
+ {renderNewCard(group)} +
+
+ ); + }, + }} + ); return ( -
- {items.map(item => { - return
{renderCard(item)}
; - })} - {c.enableNew && !c.state.readonly && ( -
{renderNewCard(group)}
- )} -
+ onDraggableChange(evt, group?.key)} + > + {{ + item: ({ element }: { element: IData }) => ( +
{renderCard(element)}
+ ), + footer: () => { + if (c.enableNew && !c.state.readonly) + return ( +
{renderNewCard(group)}
+ ); + }, + }} +
); }; @@ -428,7 +488,7 @@ export const DataViewControl = defineComponent({ {group.children.length > 0 ? ( - renderCardLayout(group.children, group) + renderCardLayout(group.children, group, !c.state.draggable) ) : (
{ibiz.i18n.t('app.noData')} @@ -451,6 +511,8 @@ export const DataViewControl = defineComponent({ } return renderCardLayout( isCollapse.value ? c.state.items.slice(0, c.state.size) : c.state.items, + undefined, + !c.enableEditOrder, ); }; diff --git a/src/control/grid/grid/grid-control.util.ts b/src/control/grid/grid/grid-control.util.ts index c5193673d7c0b47bf71552b248434ef162cd349e..234ef18f0dc542e0efb3f259a6ab2a830edac5f9 100644 --- a/src/control/grid/grid/grid-control.util.ts +++ b/src/control/grid/grid/grid-control.util.ts @@ -378,7 +378,7 @@ export function useITableEvent(c: GridController): { ): Promise { // 新建行拦截行点击事件 if (data.srfuf === Srfuf.CREATE) { - if (c.editShowMode === 'row') { + if (c.editShowMode === 'row' && !data.isGroupRow) { const row = c.findRowState(data); // 新建行值被修改过就保存,否则取消 if (row) await c.switchRowEdit(row); @@ -406,6 +406,10 @@ export function useITableEvent(c: GridController): { _column: IData, event: MouseEvent, ): Promise { + if (data.isGroupRow) { + tableRef.value?.store.loadOrToggle(data); + return; + } // 非shift点击时需标记选中数据 if (!event.shiftKey) { const index = c.findRowStateIndex(data); @@ -444,9 +448,7 @@ export function useITableEvent(c: GridController): { function onDbRowClick(data: ControlVO): void { // 新建行拦截行双击事件 - if (data.srfuf === Srfuf.CREATE) { - return; - } + if (data.srfuf === Srfuf.CREATE || data.isGroupRow) return; c.onDbRowClick(data); } @@ -745,21 +747,28 @@ export function useAppGridBase( const tableData = computed(() => { const state = c.state; if (c.state.enableGroup) { + const grouprowmode = c.controlParams.grouprowmode; const result: IData[] = []; state.groups.forEach(item => { - if (!item.children.length) { - return; + if (!item.children.length) return; + if (grouprowmode === 'NEWROW') { + result.push({ + ...item, + tempsrfkey: item.key, + isGroupRow: true, + }); + } else { + const children = [...item.children]; + const first = children.shift(); + result.push({ + tempsrfkey: first?.tempsrfkey || item.caption, + srfkey: first?.srfkey || item.caption, + isGroupData: true, + caption: item.caption, + first, + children, + }); } - const children = [...item.children]; - const first = children.shift(); - result.push({ - tempsrfkey: first?.tempsrfkey || item.caption, - srfkey: first?.srfkey || item.caption, - isGroupData: true, - caption: item.caption, - first, - children, - }); }); return result; } @@ -967,6 +976,18 @@ export function useAppGridBase( colspan, }; } + // 设置分组行的合并 + if (row.isGroupRow) { + const total = c.state.singleSelect + ? renderColumns.value.length + : renderColumns.value.length + 1; + const index = c.state.singleSelect ? 0 : 1; + if (columnIndex === index) return { rowspan: 1, colspan: total }; + return { + rowspan: 0, + colspan: 0, + }; + } }; /** diff --git a/src/control/grid/grid/grid.scss b/src/control/grid/grid/grid.scss index f2d1852014782dd1e39968085b8c18a139118041..5d4712e8773e29c9fb99b6c50a1ce5c1929c2db3 100644 --- a/src/control/grid/grid/grid.scss +++ b/src/control/grid/grid/grid.scss @@ -138,6 +138,12 @@ $control-grid-footer: ( } } + @include when(group-row-mode) { + .el-table .el-table__body-wrapper .el-table__row .el-table__expand-icon { + margin: 0 0 0 20px; + } + } + @include e(add) { width: calc(100% - getCssVar(spacing, extra-tight)); margin: getCssVar(spacing, tight) getCssVar(spacing, extra-tight); diff --git a/src/control/grid/grid/grid.tsx b/src/control/grid/grid/grid.tsx index 1112ad39b30220cf701c35c893486a0620a5304e..b0ae02c9f8b25cf935d20d0f9b084543ac06ab67 100644 --- a/src/control/grid/grid/grid.tsx +++ b/src/control/grid/grid/grid.tsx @@ -255,11 +255,13 @@ export function renderColumn( }, default: ({ row }: IData): VNode | null => { let elRow = row; // element表格数据 - if (row.isGroupData) { - // 有第一条数据时,分组那一行绘制第一条数据 - elRow = row.first; - } - + if (elRow.isGroupRow) + return ( +
+ {row.caption} +
+ ); + if (row.isGroupData) elRow = row.first; const rowState = c.findRowState(elRow); if (rowState) { // 常规非业务单元格由表格绘制(性能优化) @@ -678,6 +680,10 @@ export const GridControl = defineComponent({ this.ns.is('single-select', state.singleSelect), this.ns.is('empty', state.items.length === 0), this.ns.is('enable-customized', this.c.model.enableCustomized), + this.ns.is( + 'group-row-mode', + this.c.controlParams.grouprowmode === 'NEWROW', + ), ]} controller={this.c} style={this.headerCssVars} diff --git a/src/control/kanban/kanban.scss b/src/control/kanban/kanban.scss index 08cf95fb20255fe4db32639b5b10f57d7db99113..5c372a2effa175f8741a2b8d5f54127bf635bf26 100644 --- a/src/control/kanban/kanban.scss +++ b/src/control/kanban/kanban.scss @@ -11,9 +11,21 @@ $control-kanban: ( @include set-component-css-var(control-kanban, $control-kanban); display: flex; + flex-direction: column; width: 100%; height: 100%; + @include when(enable-page) { + .#{bem(control-kanban, content)} { + height: calc(100% - 50px); + } + } + + @include e(content) { + display: flex; + flex-grow: 1; + } + @include m(row) { @include b(control-kanban-group-container) { @include flex(row); diff --git a/src/control/kanban/kanban.tsx b/src/control/kanban/kanban.tsx index 39947e8cba940482584eb26af3f8a13c5c35ff78..0995ad9fd1e2420587281112eee1acffd01a118c 100644 --- a/src/control/kanban/kanban.tsx +++ b/src/control/kanban/kanban.tsx @@ -32,6 +32,7 @@ import { } from '@ibiz-template/runtime'; import { NOOP, listenJSEvent } from '@ibiz-template/core'; import { SwimlaneKanban } from './swimlane-kanban/swimlane-kanban'; +import { usePagination } from '../../util'; import './kanban.scss'; export const KanbanControl = defineComponent({ @@ -84,9 +85,8 @@ export const KanbanControl = defineComponent({ const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); const kanban = ref(); const isFull: Ref = ref(false); - const disabled = computed(() => { - return !c.state.draggable || c.state.updating; - }); + + const { onPageChange, onPageRefresh, onPageSizeChange } = usePagination(c); // 本地数据模式 const initSimpleData = (): void => { @@ -560,7 +560,9 @@ export const KanbanControl = defineComponent({ modelValue={group.children} group={c.model.id} itemKey='srfkey' - disabled={disabled.value || c.state.readonly} + disabled={ + !c.state.draggable || c.state.updating || c.state.readonly + } onChange={(evt: IData) => onChange(evt, group.key)} > {{ @@ -611,8 +613,11 @@ export const KanbanControl = defineComponent({ ns, isFull, kanban, - onFullScreen, renderGroup, + onFullScreen, + onPageChange, + onPageRefresh, + onPageSizeChange, }; }, render() { @@ -627,42 +632,58 @@ export const KanbanControl = defineComponent({ this.ns.m(this.modelData.groupLayout?.toLowerCase()), this.ns.is('full', this.isFull), this.ns.is('swimlane', !!swimlaneAppDEFieldId), + this.ns.is('enable-page', this.c.state.enablePagingBar), ]} > - {swimlaneAppDEFieldId ? ( - - ) : ( - [ -
- {groups.length > 0 && - groups.map(group => { - if (group.hidden) return null; - return this.renderGroup(group); - })} -
, - groups.length > 0 && ( -
- {this.c.enableGroupHidden && ( - - )} - {this.c.enableFullScreen && ( - - - - )} -
- ), - ] +
+ {swimlaneAppDEFieldId ? ( + + ) : ( + [ +
+ {groups.length > 0 && + groups.map(group => { + if (group.hidden) return null; + return this.renderGroup(group); + })} +
, + groups.length > 0 && ( +
+ {this.c.enableGroupHidden && ( + + )} + {this.c.enableFullScreen && ( + + + + )} +
+ ), + ] + )} +
+ {this.c.state.enablePagingBar && ( + )} ); diff --git a/src/control/kanban/swimlane-kanban/swimlane-kanban.scss b/src/control/kanban/swimlane-kanban/swimlane-kanban.scss index a18cb279c82fff17f6b10d427d3c2dfb6cb8ec6b..9b17dc399dee633c040255593f4b73607d26529f 100644 --- a/src/control/kanban/swimlane-kanban/swimlane-kanban.scss +++ b/src/control/kanban/swimlane-kanban/swimlane-kanban.scss @@ -10,6 +10,7 @@ $swimlane-kanban: ( width: 100%; height: 100%; padding: getCssVar('spacing', 'tight'); + background-color: getCssVar('color', 'white'); @include e('default') { .#{bem('swimlane-kanban', 'cell')}:not(.is-collapsed) { diff --git a/src/control/kanban/swimlane-kanban/swimlane-kanban.tsx b/src/control/kanban/swimlane-kanban/swimlane-kanban.tsx index 09a2378d7d44d5c89f3e85875668e5bf80f28c65..ecc9b324ff4891de8473d4ac6476b6ee6f6bef28 100644 --- a/src/control/kanban/swimlane-kanban/swimlane-kanban.tsx +++ b/src/control/kanban/swimlane-kanban/swimlane-kanban.tsx @@ -1,5 +1,13 @@ /* eslint-disable no-nested-ternary */ -import { defineComponent, PropType, ref, computed } from 'vue'; +import { + ref, + Ref, + PropType, + computed, + onMounted, + defineComponent, + onBeforeUnmount, +} from 'vue'; import { useUIStore, useNamespace } from '@ibiz-template/vue3-util'; import { IUIActionGroupDetail } from '@ibiz/model-core'; import { @@ -9,7 +17,7 @@ import { IKanbanGroupState, } from '@ibiz-template/runtime'; import draggable from 'vuedraggable'; -import { showTitle } from '@ibiz-template/core'; +import { NOOP, listenJSEvent, showTitle } from '@ibiz-template/core'; import './swimlane-kanban.scss'; /** @@ -30,6 +38,7 @@ export const SwimlaneKanban = defineComponent({ const ns = useNamespace('swimlane-kanban'); const c = props.controller; const { zIndex } = useUIStore(); + const isFull: Ref = ref(false); /** * popper样式 */ @@ -45,6 +54,20 @@ export const SwimlaneKanban = defineComponent({ */ const dropdownKey = ref(); + const swimlaneKanban = ref(); + + let cleanup = NOOP; + + onMounted(() => { + cleanup = listenJSEvent(window, 'resize', () => { + isFull.value = c.getFullscreen(); + }); + }); + + onBeforeUnmount(() => { + if (cleanup !== NOOP) cleanup(); + }); + /** * 是否禁止拖拽 */ @@ -94,6 +117,14 @@ export const SwimlaneKanban = defineComponent({ */ let cacheInfo: Partial | null = null; + /** + * @description 全屏 + */ + const onFullScreen = () => { + const container = swimlaneKanban.value; + isFull.value = c.onFullScreen(container); + }; + /** * @description 拖拽改变 * @param {IData} evt @@ -364,14 +395,32 @@ export const SwimlaneKanban = defineComponent({ {ibiz.i18n.t('control.kanban.lane')}
- {c.enableGroupHidden && ( -
+
+ {c.enableGroupHidden && ( -
- )} + )} + {c.enableFullScreen && ( + + + + )} +
{c.state.groups.map(group => { @@ -652,11 +701,12 @@ export const SwimlaneKanban = defineComponent({ ); }; - return { ns, width, renderHeader, renderBody }; + return { ns, swimlaneKanban, width, renderHeader, renderBody }; }, render() { return (
| null = null; + + const onDraggableChange = (evt: IData, groupKey?: string | number) => { + if (evt.moved) { + // 排序 + c.onDragChange({ + from: groupKey!, + to: groupKey!, + fromIndex: evt.moved.oldIndex, + toIndex: evt.moved.newIndex, + }); + } + if (evt.added) { + cacheInfo = { + to: groupKey, + toIndex: evt.added.newIndex, + }; + } + if (evt.removed) { + if (cacheInfo) { + cacheInfo.from = groupKey; + cacheInfo.fromIndex = evt.removed.oldIndex; + c.onDragChange(cacheInfo as IDragChangeInfo); + } + cacheInfo = null; + } + }; + // 本地数据模式 const initSimpleData = (): void => { if (!props.data) { @@ -231,6 +264,25 @@ export const ListControl = defineComponent({ ); }; + /** + * @description 绘制新建项 + * @param {IMDControlGroupState} [group] + * @returns {*} + */ + const renderNewItem = (group?: IMDControlGroupState) => { + return ( +
{ + c.onClickNew(event, group?.key); + }} + > + +
+ ); + }; + // 绘制默认列表项 const renderDefaultItem = (item: IData): VNode => { const actionModel = c.getOptItemModel(); @@ -337,18 +389,38 @@ export const ListControl = defineComponent({ * * @param {IData[]} items */ - const renderListItems = (items: IData[]) => { + const renderListItems = ( + items: IData[], + group?: IMDControlGroupState, + disabled: boolean = true, + ) => { const { navAppViewId } = c.model; - return items.map(item => { - if (navAppViewId && c.state.showRowDetail) - return ( -
- {renderItem(item)} - {item.__isExpand && renderRowDetail(item)} -
- ); - return renderItem(item); - }); + return ( + onDraggableChange(evt, group?.key)} + > + {{ + item: ({ element }: { element: IData }) => { + if (navAppViewId && c.state.showRowDetail) + return ( +
+ {renderItem(element)} + {element.__isExpand && renderRowDetail(element)} +
+ ); + return renderItem(element); + }, + footer: () => { + if (c.enableNew && !c.state.readonly) return renderNewItem(group); + }, + }} +
+ ); }; /** @@ -378,7 +450,7 @@ export const ListControl = defineComponent({ }, default: () => group.children.length > 0 ? ( - renderListItems(group.children) + renderListItems(group.children, group, !c.state.draggable) ) : (
{ibiz.i18n.t('app.noData')} @@ -469,6 +541,8 @@ export const ListControl = defineComponent({ isCollapse.value ? c.state.items.slice(0, c.state.size) : c.state.items, + undefined, + !c.enableEditOrder, )}
);