From d0fb44216e39ed0be7aea14b5cb625c8748c84ff Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Tue, 15 Jul 2025 19:04:11 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=A2=84?= =?UTF-8?q?=E7=BD=AE=E8=99=9A=E6=8B=9F=E8=A1=A8=E6=A0=BC=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=EF=BC=88VIRTUALIZED=5FTABLE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/control/grid/grid/index.ts | 2 + src/control/grid/virtualized-table/index.ts | 17 + .../virtualized-table.provider.ts | 11 + .../virtualized-table/virtualized-table.scss | 269 ++++++++++++ .../virtualized-table/virtualized-table.tsx | 407 ++++++++++++++++++ .../virtualized-table.util.ts | 389 +++++++++++++++++ 6 files changed, 1095 insertions(+) create mode 100644 src/control/grid/virtualized-table/index.ts create mode 100644 src/control/grid/virtualized-table/virtualized-table.provider.ts create mode 100644 src/control/grid/virtualized-table/virtualized-table.scss create mode 100644 src/control/grid/virtualized-table/virtualized-table.tsx create mode 100644 src/control/grid/virtualized-table/virtualized-table.util.ts diff --git a/src/control/grid/grid/index.ts b/src/control/grid/grid/index.ts index f4583e929..18ee108b4 100644 --- a/src/control/grid/grid/index.ts +++ b/src/control/grid/grid/index.ts @@ -10,6 +10,7 @@ import { IBizDynamicGridFieldEditColumn, } from '../grid-column'; import { IBizRowEditPopover } from '../row-edit-popover/row-edit-popover'; +import { IBizVirtualizedTableControl } from '../virtualized-table'; import { GridControl } from './grid'; import { GridProvider } from './grid.provider'; @@ -24,6 +25,7 @@ export const IBizGridControl = withInstall(GridControl, (v: App) => { v.use(IBizGridFieldEditColumn); v.use(IBizGridGroupColumn); v.use(IBizDynamicGridFieldEditColumn); + v.use(IBizVirtualizedTableControl); registerControlProvider(ControlType.GRID, () => new GridProvider()); }); diff --git a/src/control/grid/virtualized-table/index.ts b/src/control/grid/virtualized-table/index.ts new file mode 100644 index 000000000..3297fe0f1 --- /dev/null +++ b/src/control/grid/virtualized-table/index.ts @@ -0,0 +1,17 @@ +import { withInstall } from '@ibiz-template/vue3-util'; +import { registerControlProvider } from '@ibiz-template/runtime'; +import { VirtualizedTableControl } from './virtualized-table'; +import { VirtualizedTableProvider } from './virtualized-table.provider'; + +export const IBizVirtualizedTableControl = withInstall( + VirtualizedTableControl, + v => { + v.component(VirtualizedTableControl.name!, VirtualizedTableControl); + registerControlProvider( + 'GRID_VIRTUALIZED_TABLE', + () => new VirtualizedTableProvider(), + ); + }, +); + +export default IBizVirtualizedTableControl; diff --git a/src/control/grid/virtualized-table/virtualized-table.provider.ts b/src/control/grid/virtualized-table/virtualized-table.provider.ts new file mode 100644 index 000000000..90bf60127 --- /dev/null +++ b/src/control/grid/virtualized-table/virtualized-table.provider.ts @@ -0,0 +1,11 @@ +import { IControlProvider } from '@ibiz-template/runtime'; + +/** + * @description 虚拟表格适配器 + * @export + * @class VirtualizedTableProvider + * @implements {IControlProvider} + */ +export class VirtualizedTableProvider implements IControlProvider { + component: string = 'IBizVirtualizedTableControl'; +} diff --git a/src/control/grid/virtualized-table/virtualized-table.scss b/src/control/grid/virtualized-table/virtualized-table.scss new file mode 100644 index 000000000..71423a6fd --- /dev/null +++ b/src/control/grid/virtualized-table/virtualized-table.scss @@ -0,0 +1,269 @@ +/* stylelint-disable no-descending-specificity */ +$control-virtualized-table: ( + text-color: getCssVar(color, text, 0), + bg-color: transparent, + padding: getCssVar('spacing', 'none'), + cell-padding: getCssVar(spacing, tight), + selection-padding: getCssVar(spacing, base-loose), + row-edit-bg-color: getCssVar(color, fill, 2), + row-bg-color: getCssVar(color, bg, 1), + row-bg-color-2: getCssVar(color, bg, 0), + row-hover-color: rgba(#{getCssVar(grey, 1)}, 1), + row-select-color: getCssVar(color, primary, light, default), + row-expand-icon-margin: getCssVar('spacing', 'none') + getCssVar('spacing', 'none') getCssVar('spacing', 'none') + getCssVar(spacing, tight), + now-header-height: 54px, + scrollbar-width: 4px, + scrollbar-bg-color: #{getCssVar(color, fill, 2)}, + scrollbar-bg-hover-opacity: 0.5, + scrollbar-bg-hover-color: #{getCssVar(color, fill, 2)}, +); + +$control-virtualized-table-header: ( + text-color: getCssVar(color, text, 2), + cell-padding: calc(getCssVar('spacing', 'base', 'tight') / 2) + getCssVar('spacing', 'base', 'tight'), + height: auto, + min-height: 54px, + bg-color: rgba(#{getCssVar(grey, 1)}, 1), + font-size: getCssVar(font-size, regular), + font-weight: getCssVar(font-weight, regular), +); + +$control-virtualized-table-content: ( + text-color: getCssVar(control-virtualized-table, text-color), + row-height: 48px, +); + +$control-virtualized-table-page: ( + text-color: getCssVar(color, text, 2), + height: 50px, + padding: getCssVar('spacing', 'none') getCssVar(spacing, base-tight), +); + +$control-virtualized-table-footer: ( + text-color: getCssVar(color, text, 2), + bg-color: rgba(#{getCssVar(grey, 1)}, 1), + font-size: getCssVar(font-size, regular), + font-weight: getCssVar(font-weight, bold), +); + +@include b('control-virtualized-table') { + @include set-component-css-var( + control-virtualized-table, + $control-virtualized-table + ); + @include set-component-css-var( + control-virtualized-table-header, + $control-virtualized-table-header + ); + @include set-component-css-var( + control-virtualized-table-content, + $control-virtualized-table-content + ); + @include set-component-css-var( + control-virtualized-table-page, + $control-virtualized-table-page + ); + @include set-component-css-var( + control-virtualized-table-footer, + $control-virtualized-table-footer + ); + + position: relative; + width: 100%; + height: 100%; + padding: getCssVar(control-virtualized-table, padding); + background-color: getCssVar(control-virtualized-table, bg-color); + + // 支持分页时高度调整 + @include when('enable-page') { + .el-auto-resizer { + height: calc( + 100% - getCssVar(control-virtualized-table, page-height) + ) !important; + } + + .el-table-v2__empty { + height: calc(100% - getCssVar(control-virtualized-table, page-height)); + } + } + + @include e('table') { + width: 100%; + height: 100%; + + &.el-table-v2 { + --el-table-header-bg-color: #{getCssVar( + control-virtualized-table, + header, + bg-color + )}; + --el-table-border-color: transparent; + } + + .el-table-v2__empty { + height: 100%; + } + + // 清除边框线 + .el-table-v2__header-row, + .el-table-v2__row { + border: none; + } + + // 清除element单元格的padding + &.el-table-v2 { + .el-table-v2__header-cell, + .el-table-v2__row-cell { + padding: getCssVar(control-virtualized-table, cell-padding); + + &:not(.is-selection) { + cursor: pointer; + } + } + } + + // 行和列的背景色 + .el-table-v2__main { + background-color: transparent; + } + + &.el-table-v2 .el-table-v2__body .el-table-v2__row { + // 普通行列背景色 + .el-table-v2__row-cell { + background: getCssVar(control-grid, row-bg-color); + } + + // 偶数行列的背景色 + &:nth-child(2n) { + .el-table-v2__row-cell { + background-color: getCssVar(control-grid, row-bg-color-2); + } + } + + // 悬浮行列激活色 + &:hover .el-table-v2__row-cell { + background-color: getCssVar(control-grid, row-hover-color); + } + + &.is-selected .el-table-v2__row-cell { + background-color: getCssVar( + control-virtualized-table, + row, + select-color + ); + } + } + + // 表格body样式 + .el-table-v2__body { + color: getCssVar(control-grid, content, text-color); + #{getCssVarName(color, text, 0)}: getCssVar( + control-grid, + content, + text-color + ); + #{getCssVarName(color, text, 1)}: getCssVar( + control-grid, + content, + text-color + ); + #{getCssVarName(color, text, 2)}: getCssVar( + control-grid, + content, + text-color + ); + #{getCssVarName(color, text, 3)}: getCssVar( + control-grid, + content, + text-color + ); + border-bottom: 0.1px solid getCssVar(color, border); + } + + // 表格头样式 + @include e('header-cell') { + font-size: getCssVar(control-grid, header, font-size); + font-weight: getCssVar(control-grid, header, font-weight); + color: getCssVar(control-grid, header, text-color); + @include overflow-wrap; + } + } + + @include e('caret-wrapper') { + display: inline-flex; + flex-direction: column; + gap: 2px; + align-items: center; + justify-content: center; + width: 24px; + height: 14px; + overflow: initial; + line-height: 23px; + vertical-align: middle; + cursor: pointer; + @include m('sort-caret') { + width: 0; + height: 0; + border: solid 5px transparent; + opacity: 0.6; + } + @include m('asc') { + border-bottom-color: getCssVar(color, text, 2); + @include when(active) { + border-bottom-color: getCssVar(color, primary); + } + } + @include m('desc') { + border-top-color: getCssVar(color, text, 2); + @include when(active) { + border-top-color: getCssVar(color, primary); + } + } + } + + @include e('quick-toolbar') { + justify-content: center; + } + + @include e('batch-toolbar') { + position: absolute; + top: 0; + left: 54px; + z-index: 99; + display: none; + width: calc(100% - 54px); + height: getCssVar(control-virtualized-table, now-header-height); + font-size: getCssVar(font-size, regular); + font-weight: getCssVar(font-weight, regular); + background-color: getCssVar(control-virtualized-table, header, bg-color); + + @include when('show') { + @include flex(row, flex-start, center); + } + + @include m('separator') { + padding: 0 getCssVar(spacing, base-tight); + } + @include m('content') { + height: 100%; + @include flex(row, flex-start, center); + } + } + + @include when('enable-customized') { + @include e('setting-box') { + @include flex(row, flex-start, center); + + position: absolute; + top: 0; + right: 0; + z-index: 1; + height: getCssVar(control-virtualized-table, now-header-height); + padding-right: getCssVar(spacing, extra-tight); + background-color: getCssVar(control-virtualized-table, header, bg-color); + } + } +} diff --git a/src/control/grid/virtualized-table/virtualized-table.tsx b/src/control/grid/virtualized-table/virtualized-table.tsx new file mode 100644 index 000000000..fc122f84b --- /dev/null +++ b/src/control/grid/virtualized-table/virtualized-table.tsx @@ -0,0 +1,407 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable prefer-destructuring */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + useUIStore, + useNamespace, + IBizCustomRender, + useControlController, + hasEmptyPanelRenderer, +} from '@ibiz-template/vue3-util'; +import { VNode, PropType, onUnmounted, defineComponent } from 'vue'; +import { IDEGrid } from '@ibiz/model-core'; +import { + ControlVO, + GridController, + IControlProvider, +} from '@ibiz-template/runtime'; +import { IColumn, useVirtualizedTable } from './virtualized-table.util'; +import { usePagination } from '../../../util'; +import './virtualized-table.scss'; + +export const VirtualizedTableControl = defineComponent({ + name: 'IBizVirtualizedTableControl', + props: { + /** + * @description 表格模型数据 + */ + modelData: { type: Object as PropType, required: true }, + /** + * @description 应用上下文对象 + */ + context: { type: Object as PropType, required: true }, + /** + * @description 视图参数对象 + * @default {} + */ + params: { type: Object as PropType, default: () => ({}) }, + /** + * @description 部件适配器 + */ + provider: { type: Object as PropType }, + /** + * @description 部件行数据默认激活模式,值为0:不激活,值为1:单击激活,值为2:双击激活 + */ + mdctrlActiveMode: { type: Number, default: undefined }, + /** + * @description 是否单选 + */ + singleSelect: { type: Boolean, default: undefined }, + /** + * @description 是否启用行编辑 + */ + rowEditOpen: { type: Boolean, default: undefined }, + /** + * @description 是否是简单模式,即直接传入数据,不加载数据 + */ + isSimple: { type: Boolean, required: false }, + /** + * @description 简单模式下传入的数据 + */ + data: { type: Array, required: false }, + /** + * @description 是否默认加载数据 + * @default true + */ + loadDefault: { type: Boolean, default: true }, + }, + setup(props) { + const c = useControlController( + (...args) => new GridController(...args), + ); + const ns = useNamespace(`control-virtualized-table`); + + const { zIndex } = useUIStore(); + c.state.zIndex = zIndex.increment(); + + const { + tableRef, + tableData, + sortValue, + columnModel, + isSelected, + isAllSelected, + handleRowClick, + handleSortClick, + handleSelectAll, + calcColumnWidth, + handleDbRowClick, + handleHeaderCellClick, + handleSelectionChange, + } = useVirtualizedTable(c, props); + + const { onPageChange, onPageRefresh, onPageSizeChange } = usePagination(c); + + onUnmounted(() => { + zIndex.decrement(); + }); + + /** + * @description 计算行样式 + * @param {IParams} params + * @returns {*} {string} + */ + const calcRowClass = (params: IParams): string => { + const { rowData } = params; + return `${ns.is('selected', rowData && isSelected(rowData))}`; + }; + + /** + * @description 绘制无数据 + * @returns {*} {(VNode | null)} + */ + const renderNoData = (): VNode | null => { + // 未加载不显示无数据 + const { isLoaded } = c.state; + if (isLoaded) { + const quickToolbar = c.model.controls?.find( + item => item.name === `${c.model.name}_quicktoolbar`, + ); + if (quickToolbar) { + return ( + + ); + } + const noDataSlots: IParams = {}; + if (hasEmptyPanelRenderer(c)) { + Object.assign(noDataSlots, { + customRender: () => ( + + ), + }); + } + return ( + + {noDataSlots} + + ); + } + // 给null 表格会绘制默认的无数据 + return
; + }; + + /** + * @description 绘制批操作工具栏 + * @returns {*} {(VNode | undefined)} + */ + const renderBatchToolBar = (): VNode | undefined => { + const batchToolbar = c.model.controls?.find(item => { + return item.name === `${c.model.name!}_batchtoolbar`; + }); + if (!batchToolbar || c.state.singleSelect || !c.state.selectedData.length) + return; + return ( +
0), + ]} + > +
+
+ {ibiz.i18n.t('control.common.itemsSelected', { + length: c.state.selectedData.length, + })} +
+
|
+ +
+
+ ); + }; + + /** + * @description 绘制头部单元格 + * @param {IParams} params + * @returns {*} + */ + const renderHeaderCell = (params: IParams) => { + const { column } = params; + const { type, enableSort, key, title, align } = column as IColumn; + const { prop, order } = sortValue.value; + // 预置选择列 + if (type === 'selection') + return ( +
+ handleSelectAll(val)} + indeterminate={ + c.state.selectedData.length > 0 && !isAllSelected() + } + /> +
+ ); + return ( +
handleHeaderCellClick(e, column)} + > + {title} + {enableSort && ( + + handleSortClick(e, column, 'asc')} + /> + handleSortClick(e, column, 'desc')} + /> + + )} +
+ ); + }; + + /** + * @description 绘制内容区单元格 + * @param {IParams} params + * @returns {*} + */ + const renderBodyCell = (params: IParams) => { + const { column, rowData } = params; + const { type, key, align } = column as IColumn; + // 预置选择列 + if (type === 'selection') + return ( +
evt.stopPropagation()} + > + handleSelectionChange(rowData)} + modelValue={isSelected(rowData)} + /> +
+ ); + const controller = c.columns[key]; + const { columnType } = controller.model; + const row = c.findRowState(rowData); + return ( +
+ {columnType === 'DEFGRIDCOLUMN' ? ( + + ) : ( + + )} +
+ ); + }; + + return { + c, + ns, + tableRef, + tableData, + columnModel, + onPageChange, + onPageRefresh, + onPageSizeChange, + renderNoData, + calcRowClass, + renderBodyCell, + handleRowClick, + calcColumnWidth, + handleDbRowClick, + renderHeaderCell, + renderBatchToolBar, + }; + }, + render() { + if (!this.c.state.isCreated) return; + return ( + + + + {{ + default: ({ + height, + width, + }: { + height: number; + width: number; + }) => { + const columns = this.calcColumnWidth(this.columnModel, width); + return ( + this.handleRowClick(event, rowData), + onDblclick: ({ + event, + rowData, + }: { + event: MouseEvent; + rowData: ControlVO; + }) => this.handleDbRowClick(event, rowData), + }} + > + {{ + empty: this.renderNoData, + cell: this.renderBodyCell, + 'header-cell': this.renderHeaderCell, + }} + + ); + }, + }} + + {/* {this.c.state.enablePagingBar && ( + + )} */} + {this.c.model.enableCustomized && !this.c.state.hideHeader && ( +
+ +
+ )} + {this.renderBatchToolBar()} +
+
+ ); + }, +}); diff --git a/src/control/grid/virtualized-table/virtualized-table.util.ts b/src/control/grid/virtualized-table/virtualized-table.util.ts new file mode 100644 index 000000000..864ae5c82 --- /dev/null +++ b/src/control/grid/virtualized-table/virtualized-table.util.ts @@ -0,0 +1,389 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { computed, ComputedRef, ref, Ref, watch } from 'vue'; +import { + Srfuf, + ControlVO, + GridRowState, + GridController, +} from '@ibiz-template/runtime'; + +// 排序方向 +type orderDir = 'asc' | 'desc' | undefined; + +/** + * @description 虚拟表格列模型 + * @export + * @interface IColumn + */ +export interface IColumn { + /** + * @description 列唯一标识(列模型代码标识) + * @type {string} + * @memberof IColumn + */ + key: string; + /** + * @description 列类型 + * @type {string} + * @memberof IColumn + */ + type: string; + /** + * @description 列宽 + * @type {number} + * @memberof IColumn + */ + width: number; + /** + * @description 宽度单位 + * @type {string} + * @memberof IColumn + */ + widthUnit: string; + /** + * @description 是否隐藏列 + * @type {boolean} + * @memberof IColumn + */ + hidden: boolean; + /** + * @description 列标题 + * @type {string} + * @memberof IColumn + */ + title?: string; + /** + * @description 表格单元格内容对齐方式 + * @type {('left' | 'center' | 'right')} + * @memberof IColumn + */ + align?: string; + /** + * @description 列的类名 + * @type {string} + * @memberof IColumn + */ + class?: string; + /** + * @description 头部单元格样式 + * @type {string} + * @memberof IColumn + */ + headerClass?: string; + /** + * @description 固定列位置 + * @type {('left' | 'right')} + * @memberof IColumn + */ + fixed?: 'left' | 'right'; + /** + * @description 设置列是否排序 + * @type {boolean} + * @memberof IColumn + */ + enableSort?: boolean; +} + +export function useVirtualizedTable( + c: GridController, + props: IData, +): { + tableRef: Ref; + tableData: ComputedRef; + sortValue: ComputedRef<{ + prop?: string; + order: orderDir; + }>; + columnModel: ComputedRef; + isAllSelected(): boolean; + isSelected(data: ControlVO): boolean; + handleSelectAll(state: boolean): void; + handleSelectionChange(data: ControlVO): void; + handleSortClick(event: MouseEvent, column: IColumn, newOrder: orderDir): void; + calcColumnWidth(_columns: IColumn[], bodyWidth: number): IColumn[]; + handleRowClick(event: MouseEvent, data: ControlVO): Promise; + handleDbRowClick(event: MouseEvent, data: ControlVO): Promise; + handleHeaderCellClick(event: MouseEvent, column: IColumn): void; +} { + const initSimpleData = (): void => { + if (!props.data) return; + c.state.items = props.data; + c.state.rows = c.state.items.map(item => { + const row = new GridRowState(new ControlVO(item), c); + return row; + }); + c.calcAggResult(c.state.items); + c.calcTotalData(); + }; + + c.evt.on('onCreated', async () => { + if (c.state.isSimple) initSimpleData(); + }); + + watch( + () => props.data, + () => { + if (c.state.isSimple) initSimpleData(); + }, + { + deep: true, + }, + ); + + // 表格引用 + const tableRef = ref(); + // 表格数据 + const tableData = computed(() => { + // TODO 树表未支持 + return c.state.rows.map(row => row.data); + }); + + // 排序值 + const sortValue = computed<{ prop?: string; order: orderDir }>(() => { + const [ + prop = c.model.minorSortAppDEFieldId, + order = c.model.minorSortDir?.toLowerCase(), + ] = c.state.sortQuery.split(','); + + return { + prop, + order: order as orderDir, + }; + }); + + // 列模型 + const columnModel = computed(() => { + // TODO 多级表头未支持 + const { columnStates, singleSelect } = c.state; + const columns: IColumn[] = []; + // 不是单选时添加选择列 + if (!singleSelect) + columns.push({ + key: c.id, + width: 55, + align: 'center', + type: 'selection', + widthUnit: 'PX', + hidden: false, + class: 'is-selection', + headerClass: 'is-selection', + }); + const { degridColumns } = c.model; + degridColumns?.forEach(model => { + const { + align, + width, + caption, + codeName, + widthUnit, + columnType, + cellSysCss, + enableSort, + } = model; + const state = columnStates.find(s => s.key === codeName); + columns.push({ + key: codeName!, + title: caption, + width: width || 160, + hidden: state?.hidden === true, + enableSort: !!enableSort, + class: cellSysCss?.cssName, + widthUnit: widthUnit || 'PX', + type: columnType!.toLowerCase(), + headerClass: cellSysCss?.cssName, + align: align?.toLowerCase() || 'center', + }); + }); + return columns; + }); + + /** + * @description 切换排序 + * @param {orderDir} order + */ + function toggleOrder(order: orderDir): orderDir { + const sortOrders: orderDir[] = ['asc', 'desc', undefined]; + if (order === undefined) return sortOrders[0]; + const index = sortOrders.indexOf(order || null); + return sortOrders[index > sortOrders.length - 2 ? 0 : index + 1]; + } + + /** + * @description 处理排序改变 + * @param {string} prop + * @param {orderDir} order + * @returns {*} {void} + */ + function handleSortChange(prop: string, order: orderDir): void { + // TODO 本地排序未支持 + const fieldName = c.fieldColumns[prop].model.appDEFieldId; + const sortQuery = `${fieldName},${order}`; + // 如果排序条件相同,不触发查询 + if (sortQuery === c.state.sortQuery) return; + c.setSort(fieldName, order); + c.load({ + isInitialLoad: c.model.pagingMode === 2 || c.model.pagingMode === 3, + }); + } + + /** + * @description 处理行单击 + * @param {MouseEvent} event + * @param {ControlVO} data + * @returns {*} {Promise} + */ + async function handleRowClick( + event: MouseEvent, + data: ControlVO, + ): Promise { + // 新建行拦截行点击事件 + if (data.srfuf === Srfuf.CREATE) return; + await c.onRowClick(data); + } + + /** + * @description 处理行双击 + * @param {MouseEvent} event + * @param {ControlVO} data + * @returns {*} {Promise} + */ + async function handleDbRowClick( + event: MouseEvent, + data: ControlVO, + ): Promise { + // 新建行拦截行双击事件 + if (data.srfuf === Srfuf.CREATE) return; + await c.onDbRowClick(data); + } + + /** + * @description 处理头部单元格点击 + * @param {MouseEvent} event + * @param {IColumn} column + */ + function handleHeaderCellClick(event: MouseEvent, column: IColumn): void { + // TODO 本地排序未支持 + const { enableSort, key } = column; + if (enableSort) { + const { prop, order } = sortValue.value; + let newOrder: orderDir = 'asc'; + // 如果点击已排序表头,则切换排序 + if (prop === key) newOrder = toggleOrder(order); + handleSortChange(key, newOrder); + } + } + + /** + * @description 处理排序点击 + * @param {MouseEvent} event + * @param {IColumn} column + * @param {orderDir} newOrder + */ + function handleSortClick( + event: MouseEvent, + column: IColumn, + newOrder: orderDir, + ): void { + event.stopPropagation(); + const { prop, order } = sortValue.value; + const { key } = column; + if (prop === key && newOrder === order) newOrder = undefined; + handleSortChange(key, newOrder); + } + + /** + * @description 判断是否选中 + * @param {ControlVO} data + * @returns {*} {boolean} + */ + function isSelected(data: ControlVO): boolean { + return !!c.state.selectedData.find(x => x.srfkey === data.srfkey); + } + + /** + * @description 判断是否全选 + * @returns {*} {boolean} + */ + function isAllSelected(): boolean { + // TODO 未支持树表格 + return ( + tableData.value.length > 0 && + tableData.value.every( + (data: IData) => + !!c.state.selectedData.find( + selected => data.srfkey === selected.srfkey, + ), + ) + ); + } + + /** + * @description 处理选择所有 + */ + function handleSelectAll(state: boolean): void { + c.setSelection(state ? [...c.state.items] : []); + } + + /** + * @description 处理选中改变 + * @param {ControlVO} data + */ + function handleSelectionChange(data: ControlVO): void { + const selection = [...c.state.selectedData]; + const index = selection.findIndex( + selected => selected.srfkey === data.srfkey, + ); + index === -1 ? selection.push(data) : selection.splice(index, 1); + c.setSelection([...selection]); + } + + /** + * @description 计算列宽 + * @param {IColumn[]} _columns + * @param {number} bodyWidth + * @returns {*} {IColumn[]} + */ + function calcColumnWidth(_columns: IColumn[], bodyWidth: number): IColumn[] { + const columns = [..._columns]; + // 显示列 + const showColumns = columnModel.value.filter(model => !model.hidden); + // 显示列总宽 + const totalWidth = showColumns.reduce( + (accumulator, currentValue) => accumulator + currentValue.width, + 0, + ); + // 表格可视宽度大于总列宽时,手动计算自适应列宽 + if (bodyWidth > totalWidth && showColumns.length) { + // 自适应列 + let adaptiveColumn = showColumns.filter( + model => model.widthUnit === 'STAR', + ); + // 如果没有配置自适应列,则最后一列变为自适应列 + if (!adaptiveColumn.length) + adaptiveColumn = [showColumns[showColumns.length - 1]]; + const width = (bodyWidth - totalWidth) / adaptiveColumn.length - 6; + adaptiveColumn.forEach(column => + Object.assign(column, { width: column.width + width }), + ); + } + return columns; + } + + return { + tableRef, + tableData, + sortValue, + columnModel, + isSelected, + isAllSelected, + handleRowClick, + handleSortClick, + handleSelectAll, + calcColumnWidth, + handleDbRowClick, + handleHeaderCellClick, + handleSelectionChange, + }; +} -- Gitee From 8d8c7a6cf9749f0e2eeea75653e62c0affd8986c Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Tue, 15 Jul 2025 19:04:33 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f53c4fd21..2281d1f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [Unreleased] +### Added + +- 新增预置虚拟表格插件(VIRTUALIZED_TABLE) + ## [0.7.41-alpha.12] - 2025-07-15 ### Added -- Gitee From 2218c34a255ffd2e16d30f1e6264134c685251e2 Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Tue, 15 Jul 2025 19:05:07 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E7=9C=8B=E6=9D=BF=E9=83=A8?= =?UTF-8?q?=E4=BB=B6=E6=96=B0=E5=A2=9E=E7=AE=80=E5=8D=95=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + src/control/kanban/kanban.tsx | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2281d1f91..48294231f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Added - 新增预置虚拟表格插件(VIRTUALIZED_TABLE) +- 看板部件新增简单模式 ## [0.7.41-alpha.12] - 2025-07-15 diff --git a/src/control/kanban/kanban.tsx b/src/control/kanban/kanban.tsx index 363b4a439..7fae9eede 100644 --- a/src/control/kanban/kanban.tsx +++ b/src/control/kanban/kanban.tsx @@ -9,6 +9,7 @@ import { ref, Ref, VNode, + watch, PropType, computed, onMounted, @@ -23,6 +24,7 @@ import { IUIActionGroupDetail, } from '@ibiz/model-core'; import { + ControlVO, IDragChangeInfo, IControlProvider, KanbanController, @@ -63,6 +65,14 @@ export const KanbanControl = defineComponent({ * @description 是否单选 */ singleSelect: { type: Boolean, default: undefined }, + /** + * @description 是否是简单模式,即直接传入数据,不加载数据 + */ + isSimple: { type: Boolean, required: false }, + /** + * @description 简单模式下传入的数据 + */ + data: { type: Array, required: false }, /** * @description 是否默认加载数据 * @default true @@ -78,6 +88,33 @@ export const KanbanControl = defineComponent({ return !c.state.draggable || c.state.updating; }); + // 本地数据模式 + const initSimpleData = (): void => { + if (!props.data) { + return; + } + c.state.items = (props.data as IData[]).map(item => new ControlVO(item)); + c.afterLoad({}, c.state.items); + }; + + c.evt.on('onCreated', async () => { + if (props.isSimple) { + initSimpleData(); + c.state.isSimple = true; + c.state.isLoaded = true; + } + }); + + watch( + () => props.data, + () => { + if (props.isSimple) initSimpleData(); + }, + { + deep: true, + }, + ); + const batchKey = computed(() => { return c.state.batching ? c.state.selectGroupKey : ''; }); -- Gitee From efb8a5c81a9ffd7d11e88d684cd6fe4639b66008 Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Tue, 15 Jul 2025 19:05:49 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=B1=9E=E6=80=A7=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 +++ .../grid-field-column/grid-field-column.tsx | 28 ++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48294231f..9464c6535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - 新增预置虚拟表格插件(VIRTUALIZED_TABLE) - 看板部件新增简单模式 +### Changed + +- 优化表格属性列 + ## [0.7.41-alpha.12] - 2025-07-15 ### Added diff --git a/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx b/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx index 08b08d981..39e1a9be9 100644 --- a/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx +++ b/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx @@ -1,11 +1,15 @@ import { ILayoutPanel, IPanel, IUIActionGroupDetail } from '@ibiz/model-core'; -import { computed, defineComponent, Ref, ref, VNode, watch } from 'vue'; -import { useNamespace, useCodeListListen } from '@ibiz-template/vue3-util'; +import { computed, defineComponent, ref, VNode } from 'vue'; import { + useNamespace, + computedAsync, + useCodeListListen, +} from '@ibiz-template/vue3-util'; +import { + ValueExUtil, CodeListItem, - GridFieldColumnController, GridRowState, - ValueExUtil, + GridFieldColumnController, } from '@ibiz-template/runtime'; import { isNotNil } from 'ramda'; import { showTitle } from '@ibiz-template/core'; @@ -60,18 +64,10 @@ export const GridFieldColumn = defineComponent({ } }; - const CustomHtml: Ref = ref(''); - - watch( - () => props.row, - async () => { - CustomHtml.value = await props.controller.getCustomHtml(props.row); - }, - { - deep: true, - immediate: true, - }, - ); + const CustomHtml = computedAsync(async () => { + const html = await props.controller.getCustomHtml(props.row); + return html; + }); const fieldValue = computed( () => props.row.data[props.controller.fieldName], -- Gitee