From 4d614484bd52f9549978098287d9c3655e40d20b Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Wed, 19 Mar 2025 16:54:16 +0800 Subject: [PATCH 01/52] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=AF=BC=E5=85=A5=E8=B7=AF=E5=BE=84=E5=92=8C=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E6=96=B9=E6=B3=95=E5=8F=82=E6=95=B0=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/body/index.ts | 2 +- packages/vue/src/grid/src/body/src/body.vue | 840 ++++++++++++++++++++ packages/vue/src/grid/src/cell/src/cell.ts | 20 +- 3 files changed, 851 insertions(+), 11 deletions(-) create mode 100644 packages/vue/src/grid/src/body/src/body.vue diff --git a/packages/vue/src/grid/src/body/index.ts b/packages/vue/src/grid/src/body/index.ts index 4d8c119b88..c634a3b87f 100644 --- a/packages/vue/src/grid/src/body/index.ts +++ b/packages/vue/src/grid/src/body/index.ts @@ -22,7 +22,7 @@ * SOFTWARE. * */ -import Body from './src/body' +import Body from './src/body.vue' Body.install = function (Vue) { Vue.component(Body.name, Body) diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue new file mode 100644 index 0000000000..590423c0b4 --- /dev/null +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -0,0 +1,840 @@ + + + diff --git a/packages/vue/src/grid/src/cell/src/cell.ts b/packages/vue/src/grid/src/cell/src/cell.ts index 71639b9ee9..43adf0c506 100644 --- a/packages/vue/src/grid/src/cell/src/cell.ts +++ b/packages/vue/src/grid/src/cell/src/cell.ts @@ -28,7 +28,7 @@ import { getColumnConfig, getFuncText, formatText } from '@opentiny/vue-renderle import { Renderer } from '../../adapter' import { getCellLabel, warn } from '../../tools' import GLOBAL_CONFIG from '../../config' -import { hooks, isVnode } from '@opentiny/vue-common' +import { hooks, isVnode, h } from '@opentiny/vue-common' import { iconCheckedSur, iconHalfselect, @@ -285,7 +285,7 @@ export const Cell = { return [h('div', { class: 'tiny-grid-cell-text' }, [formatText(getFuncText(own.title), 1)])] }, - renderCell(h, params) { + renderCell({ params }) { let { $table, row, column } = params let { slots, renderer } = column const format = column.format || {} @@ -392,7 +392,7 @@ export const Cell = { renderTreeIndexCell(h, params) { return Cell.renderTreeIcon(h, params).concat(Cell.renderIndexCell(h, params)) }, - renderIndexCell(h, params) { + renderIndexCell({ params }) { const { $table, column, row, seq, $seq, level } = params // startIndex:序号列的起始值 const { startIndex, treeConfig, scrollYLoad, treeOrdered } = $table @@ -423,7 +423,7 @@ export const Cell = { return [formatText(value, 1)] }, - renderRadioCell(h, params) { + renderRadioCell({ params }) { let { $table, column: { slots }, @@ -521,7 +521,7 @@ export const Cell = { return [vnode, dropdownVnode] }, - renderSelectionCell(h, params) { + renderSelectionCell({ params }) { let { $table, column, row } = params let { slots } = column let { selectConfig = {}, treeConfig, treeIndeterminates, vSize } = $table @@ -599,7 +599,7 @@ export const Cell = { return Cell.renderTreeIcon(h, params).concat(Cell.renderSelectionCell(h, params)) }, // TODO: 与renderSelectionCell代码方法高度相似,待提取公共逻辑。 - renderSelectionCellByProp(h, params) { + renderSelectionCellByProp({ params }) { let { $table, column, row } = params let { slots } = column let { selectConfig = {}, treeConfig, treeIndeterminates, vSize } = $table @@ -850,10 +850,10 @@ export const Cell = { return vNodes }, renderTreeRowEdit(h, params) { - return Cell.renderTreeIcon(h, params).concat(Cell.renderRowEdit(h, params)) + return Cell.renderTreeIcon(h, params).concat(Cell.renderRowEdit({ params })) }, // 行格编辑模式 - renderRowEdit(h, params) { + renderRowEdit({ params }) { let { actived } = params.$table.editStore const { editConfig } = params.$table @@ -864,7 +864,7 @@ export const Cell = { return Cell.renderTreeIcon(h, params).concat(Cell.renderCellEdit(h, params)) }, // 单元格编辑模式 - renderCellEdit(h, params) { + renderCellEdit({ params }) { let { actived } = params.$table.editStore return Cell.runRenderer(h, params, this, actived && actived.row === params.row && actived.column === params.column) @@ -912,7 +912,7 @@ export const Cell = { return [cellValue] } - return Cell.renderCell.call(_vm, h, params) + return Cell.renderCell.call(_vm, { params }) }, getSuffixCls(params) { return params.$table.headerSuffixIconAbsolute ? ['suffix-icon-1', 'suffix-icon-0'] : ['', ''] From f649f748509b6cf7f6e2ef67882fea45ffbb78e6 Mon Sep 17 00:00:00 2001 From: zzcr <894103554@qq.com> Date: Wed, 19 Mar 2025 19:26:13 +0800 Subject: [PATCH 02/52] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E7=B1=BB?= =?UTF-8?q?=E5=90=8D=E6=98=A0=E5=B0=84=EF=BC=8C=E7=AE=80=E5=8C=96=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E7=BB=91=E5=AE=9A=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/body/src/body.vue | 47 ++++++--------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue index 590423c0b4..c5d45612b4 100644 --- a/packages/vue/src/grid/src/body/src/body.vue +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -2,7 +2,7 @@
@@ -83,10 +83,10 @@ class="tiny-grid-body__row" :class="[ { [`row__level-${rowLevel}`]: treeConfig }, - { [classMap.rowNew]: editStore.insertList.includes(row) }, - { [classMap.rowSelected]: selection.includes(row) }, - { [classMap.rowRadio]: selectRow === row }, - { [classMap.rowActived]: rowActived }, + { 'row__new': editStore.insertList.includes(row) }, + { 'row__selected': selection.includes(row) }, + { 'row__radio': selectRow === row }, + { 'row__actived': rowActived }, rowClassName ? typeof rowClassName === 'function' ? rowClassName({ $table: $table as TableInstance, $seq: 0, seq: 0, rowLevel, row, rowIndex: 0 }) @@ -103,15 +103,15 @@ :class="[ column.id, { [`col__${cellAlign}`]: cellAlign }, - { [classMap.colEdit]: editor }, - { [classMap.colIndex]: column.type === 'index' }, - { [classMap.colRadio]: column.type === 'radio' }, - { [classMap.colSelection]: column.type === 'selection' }, - { [classMap.colEllipsis]: hasEllipsis }, - { [classMap.editVisible]: editor && editor.type === 'visible' }, - { [classMap.fixedColumn]: fixedHiddenColumn }, - { [classMap.colDirty]: isDirty }, - { [classMap.colActived]: columnActived }, + { 'col__edit': editor }, + { 'col__index': column.type === 'index' }, + { 'col__radio': column.type === 'radio' }, + { 'col__selection': column.type === 'selection' }, + { 'col__ellipsis': hasEllipsis }, + { 'edit__visible': editor && editor.type === 'visible' }, + { 'fixed__column': fixedHiddenColumn }, + { 'col__dirty': isDirty }, + { 'col__actived': columnActived }, { 'col__valid-error': validError && validated }, { 'col__valid-success': columnActived ? !validError && !validated : isDirty && !validated }, { 'col__treenode': column.treeNode }, @@ -342,24 +342,6 @@ const isOperateMouse = ($table: TableConfig) => let renderRowFlag = false -// 解决静态扫描驼峰变量问题 -const classMap = { - colEdit: 'col__edit', - colIndex: 'col__index', - colRadio: 'col__radio', - colSelection: 'col__selection', - colEllipsis: 'col__ellipsis', - editVisible: 'edit__visible', - fixedColumn: 'fixed__column', - colDirty: 'col__dirty', - colActived: 'col__actived', - rowNew: 'row__new', - rowSelected: 'row__selected', - rowRadio: 'row__radio', - rowActived: 'row__actived', - isScrollload: 'is__scrollload' -} - export default defineComponent({ name: 'TinyGridBody', props: { @@ -574,7 +556,6 @@ export default defineComponent({ return { slots, rowSortable, - classMap, GlobalConfig } }, From 7b33a4e0444676bb77e5a2cd058c7a960a389623 Mon Sep 17 00:00:00 2001 From: zzcr <894103554@qq.com> Date: Wed, 19 Mar 2025 19:38:05 +0800 Subject: [PATCH 03/52] =?UTF-8?q?feat(grid):=20=E5=A2=9E=E5=8A=A0=E8=A1=8C?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=92=8C=E7=B4=A2=E5=BC=95=E7=9A=84=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E6=96=B0=E5=B1=9E=E6=80=A7=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E5=8A=A0=E8=BD=BD=E5=92=8C=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E8=A1=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/body/src/body.vue | 52 +++++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue index c5d45612b4..3756482c8f 100644 --- a/packages/vue/src/grid/src/body/src/body.vue +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -147,10 +147,22 @@ row, column, $table: $table as TableInstance, - $seq: row.$seq, - seq: row.seq, - rowIndex: row.rowIndex, - $rowIndex: row.$rowIndex + $seq: isOrdered + ? seqCount.value + : scrollYLoad + ? rowIndex + startIndex + 1 + : hasVirtualRow && !virtualRow + ? afterFullData.indexOf(row) + 1 + : rowIndex + 1, + seq: isOrdered + ? seqCount.value + : scrollYLoad + ? rowIndex + startIndex + 1 + : hasVirtualRow && !virtualRow + ? afterFullData.indexOf(row) + 1 + : rowIndex + 1, + rowIndex: ($table as TableInstance).getRowIndex(row), + $rowIndex: tableData.indexOf(row) }" />
@@ -540,6 +552,38 @@ export default defineComponent({ find: { type: Function as PropType, default: (array: T[], predicate: (item: T) => boolean) => array.find(predicate) + }, + isOrdered: { + type: Boolean, + default: false + }, + seqCount: { + type: Object as PropType<{ value: number }>, + default: () => ({ value: 0 }) + }, + scrollYLoad: { + type: Boolean, + default: false + }, + rowIndex: { + type: Number, + default: 0 + }, + startIndex: { + type: Number, + default: 0 + }, + hasVirtualRow: { + type: Boolean, + default: false + }, + virtualRow: { + type: Object as PropType, + default: null + }, + afterFullData: { + type: Array as PropType, + default: () => [] } }, setup(props, { slots }) { From 7fd013e0b2690d3e5a6de7d67767d34903382137 Mon Sep 17 00:00:00 2001 From: zzcr <894103554@qq.com> Date: Wed, 19 Mar 2025 21:30:51 +0800 Subject: [PATCH 04/52] =?UTF-8?q?feat(grid):=20=E6=9B=B4=E6=96=B0=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=AE=9E=E4=BE=8B=E7=B1=BB=E5=9E=8B=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=A1=8C=E5=88=97=E7=B4=A2=E5=BC=95=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=9B=B4=E7=81=B5=E6=B4=BB=E7=9A=84?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/body/src/body.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue index 3756482c8f..82c63bcc83 100644 --- a/packages/vue/src/grid/src/body/src/body.vue +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -260,7 +260,7 @@ type FindFn = (array: T[], predicate: (item: T) => boolean) => T | undefined // 定义组件参数类型 interface ComponentParams { - $table: any + $table: TableInstance $seq: number seq: number rowLevel: number @@ -284,6 +284,8 @@ interface ValidationRule { interface ValidationStore { rule?: ValidationRule content?: string + row?: TableRow + column?: TableColumn [key: string]: any } @@ -318,8 +320,7 @@ interface Editor { // 定义表格实例类型 interface TableInstance { isShapeTable?: boolean - _rowGroupTargetColumn?: TableColumn - getColumnIndex?: (column: TableColumn) => number + scrollLoad?: boolean _isResize?: boolean lastScrollTime?: number optimizeOpts?: { @@ -345,6 +346,9 @@ interface TableInstance { tableListeners?: { [key: string]: any } + getRowIndex?: (row: TableRow) => number + getColumnIndex?: (column: TableColumn) => number + _rowGroupTargetColumn?: TableColumn [key: string]: any } From b040c73fbfdfff2b5cfe4b0719bf497015ab1900 Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Thu, 20 Mar 2025 16:22:42 +0800 Subject: [PATCH 05/52] =?UTF-8?q?feat(grid):=20=E4=BC=98=E5=8C=96=E8=A1=8C?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=A1=A8=E6=A0=BC=E5=AE=9E=E4=BE=8B=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E7=AE=80=E5=8C=96=E8=A1=8C=E5=BA=8F=E5=88=97=E5=92=8C?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E7=9A=84=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/body/src/body.vue | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue index 82c63bcc83..260958ea06 100644 --- a/packages/vue/src/grid/src/body/src/body.vue +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -147,20 +147,8 @@ row, column, $table: $table as TableInstance, - $seq: isOrdered - ? seqCount.value - : scrollYLoad - ? rowIndex + startIndex + 1 - : hasVirtualRow && !virtualRow - ? afterFullData.indexOf(row) + 1 - : rowIndex + 1, - seq: isOrdered - ? seqCount.value - : scrollYLoad - ? rowIndex + startIndex + 1 - : hasVirtualRow && !virtualRow - ? afterFullData.indexOf(row) + 1 - : rowIndex + 1, + $seq: ($table as TableInstance).getRowIndex(row) + 1, + seq: ($table as TableInstance).getRowIndex(row) + 1, rowIndex: ($table as TableInstance).getRowIndex(row), $rowIndex: tableData.indexOf(row) }" From 93eb58ee9670fc51d63cc8c9be24a736e09e3a52 Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Tue, 25 Mar 2025 16:12:02 +0800 Subject: [PATCH 06/52] =?UTF-8?q?fix(grid):=20=E6=9B=B4=E6=96=B0=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E7=BB=84=E4=BB=B6=E5=AF=BC=E5=85=A5=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=AD=A3=E4=B8=BATable.vue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/table/index.ts | 2 +- packages/vue/src/grid/src/table/src/Table.vue | 1015 +++++++++++++++++ 2 files changed, 1016 insertions(+), 1 deletion(-) create mode 100644 packages/vue/src/grid/src/table/src/Table.vue diff --git a/packages/vue/src/grid/src/table/index.ts b/packages/vue/src/grid/src/table/index.ts index 41bf1c9ff1..2fa5ae9d3e 100644 --- a/packages/vue/src/grid/src/table/index.ts +++ b/packages/vue/src/grid/src/table/index.ts @@ -9,7 +9,7 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import Table from './src/table' +import Table from './src/Table.vue' import TINYGrid from '../adapter' Table.install = function (Vue) { diff --git a/packages/vue/src/grid/src/table/src/Table.vue b/packages/vue/src/grid/src/table/src/Table.vue new file mode 100644 index 0000000000..32b0f6649c --- /dev/null +++ b/packages/vue/src/grid/src/table/src/Table.vue @@ -0,0 +1,1015 @@ + + + From d8af2fe4b7a52d8e8bc2a6e906dead4eb5c9ecdb Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Thu, 27 Mar 2025 14:43:09 +0800 Subject: [PATCH 07/52] =?UTF-8?q?feat(grid):=20=E9=87=8D=E6=9E=84=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8ref=E6=9B=BF=E4=BB=A3reactive=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E6=80=A7=E8=83=BD=E5=92=8C=E5=8F=AF=E7=BB=B4=E6=8A=A4?= =?UTF-8?q?=E6=80=A7=EF=BC=8C=E5=90=8C=E6=97=B6=E7=A7=BB=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/table/src/Table.vue | 501 ++++++++++-------- .../vue/src/grid/src/table/src/methods.ts | 1 - 2 files changed, 277 insertions(+), 225 deletions(-) diff --git a/packages/vue/src/grid/src/table/src/Table.vue b/packages/vue/src/grid/src/table/src/Table.vue index 32b0f6649c..d4bf2d2973 100644 --- a/packages/vue/src/grid/src/table/src/Table.vue +++ b/packages/vue/src/grid/src/table/src/Table.vue @@ -74,7 +74,7 @@ From 8f14aefbf54b273c87a70f44cee868713cc08ae6 Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Mon, 31 Mar 2025 11:41:09 +0800 Subject: [PATCH 10/52] =?UTF-8?q?chore:=20=E7=BB=99=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BF=85=E8=A6=81=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vue/src/grid/src/table/src/methods.ts | 257 ++++++++++++++++-- 1 file changed, 227 insertions(+), 30 deletions(-) diff --git a/packages/vue/src/grid/src/table/src/methods.ts b/packages/vue/src/grid/src/table/src/methods.ts index 486c4be80e..1a72f85080 100644 --- a/packages/vue/src/grid/src/table/src/methods.ts +++ b/packages/vue/src/grid/src/table/src/methods.ts @@ -314,9 +314,14 @@ const Methods = { // 对全量数据进行筛选、排序、虚拟滚动切割数据等一系列操作 this.handleTableData(true) - // reserveCheckSelection:处理分页切换保留选中状态的逻辑,checkSelectionStatus:处理全选、半选等选中状态 + // reserveCheckSelection:处理分页切换保留选中状态的逻辑 + // checkSelectionStatus:处理全选、半选等选中状态 run(['reserveCheckSelection', 'checkSelectionStatus'], this) + + // 定义第一个处理函数:如果不是notRefresh模式,则重新计算表格尺寸和布局 let first = () => !notRefresh && this.recalculate() + + // 定义第二个处理函数:尝试恢复滚动位置 let second = () => { // 让表格滚动条滚动到最后一次滚动到的位置 if (lastScrollLeft || lastScrollTop) { @@ -327,71 +332,103 @@ const Methods = { headerElem && (headerElem.scrollLeft = 0) } } + + // 链式执行两个处理函数 return this.$nextTick().then(first).then(second) }, - // 重新加载数据 + + // 重新加载数据:先清空所有状态,然后加载新数据,最后处理默认设置 reloadData(datas) { return this.clearAll() .then(() => this.loadTableData(datas)) .then(() => this.handleDefault()) }, - // 加载全量数据 + + // 加载全量数据:直接加载数据而不清空状态 loadData(datas) { return new Promise((resolve) => { this.loadTableData(datas) resolve() }) }, + + // 重新加载指定行的数据 + // row: 目标行数据对象 + // record: 新的行数据对象 + // field: 指定字段名(如果只更新某个字段) reloadRow(row, record, field) { let { tableData, tableSourceData } = this let rowIndex = this.getRowIndex(row) let originRow = tableSourceData[rowIndex] let hasSrc = originRow && row let hasSrcNoField = hasSrc && !field + + // 如果指定了字段,只更新该字段值 if (hasSrc && field) { set(originRow, field, get(record || row, field)) } + + // 如果有源数据且提供了新记录,但没有指定字段,则用新记录替换整行 if (hasSrcNoField && record) { tableSourceData[rowIndex] = record clear(row, undefined) Object.assign(row, this.defineField({ ...record })) this.updateCache(true) } + + // 如果有源数据但没有新记录也没有指定字段,则用源数据替换当前行 if (hasSrcNoField && !record) { destructuring(originRow, clone(row, true)) } + + // 触发表格重新渲染 this.tableData = tableData.slice(0) return this.$nextTick() }, - // 从新加载列配置 + + // 重新加载列配置:先清除所有状态,然后加载新列配置 reloadColumn(columns) { return this.clearAll().then(() => this.loadColumn(columns)) }, + + // 加载列配置 loadColumn(columns) { return new Promise((resolve) => { + // 通过mapTree函数处理每个列配置,创建列实例 this.collectColumn = mapTree(columns, (column) => Cell.createColumn(this, column), headerProps) resolve() }).then(() => this.$nextTick()) }, - // 更新数据的 Map + + // 更新数据的映射缓存(优化查询效率) updateCache(source) { let { fullAllDataRowIdData, fullAllDataRowMap, fullDataRowIdData, fullDataRowMap, tableFullData, treeConfig } = this let rowKey = getTableRowKey(this) + + // 构建行数据缓存的函数 let buildRowCache = (row, index) => { + // 获取行ID,如果没有则生成 let rowId = getRowid(this, row) if (isNull(rowId) || rowId === '') { rowId = getRowUniqueId() set(row, rowKey, rowId) } + + // 创建行缓存对象 let rowCache = { row, rowid: rowId, index } + + // 根据source参数决定是否更新完整数据缓存 if (source) { fullDataRowIdData[rowId] = rowCache fullDataRowMap.set(row, rowCache) } + + // 更新全部数据缓存 fullAllDataRowIdData[rowId] = rowCache fullAllDataRowMap.set(row, rowCache) } + // 清空缓存的函数 let clearCache = () => { fullAllDataRowIdData = {} this.fullAllDataRowIdData = fullAllDataRowIdData @@ -402,7 +439,11 @@ const Methods = { fullDataRowMap.clear() } } + + // 执行清空缓存 clearCache() + + // 根据数据是否为树结构,使用不同方式遍历数据并构建缓存 if (treeConfig) { eachTree(tableFullData, buildRowCache, treeConfig) } else { @@ -414,27 +455,33 @@ const Methods = { let { fullColumnMap, tableFullColumn: fullColumn } = this let fullColumnIdData = {} this.fullColumnIdData = fullColumnIdData + // 清空列映射 Map.prototype.clear.apply(fullColumnMap) + // 为每列创建缓存对象 fullColumn.forEach((column, index) => { let colCache = { colid: column.id, column, index } fullColumnIdData[column.id] = colCache fullColumnMap.set(column, colCache) }) }, - // 通过tr的dom元素获取行数据等相关信息 + // 通过tr的DOM元素获取对应的行数据及相关信息 getRowNode(tr) { if (!tr) { return null } const { fullAllDataRowIdData, tableFullData, treeConfig } = this + // 获取行DOM元素上存储的行ID属性 const dataRowid = tr.getAttribute('data-rowid') + + // 如果是树形结构,使用findTree查找对应的行数据 if (treeConfig) { const matches = findTree(tableFullData, (row) => getRowid(this, row) === dataRowid, treeConfig) if (matches) { return matches } } else { + // 如果是普通表格,直接从缓存中获取 if (fullAllDataRowIdData[dataRowid]) { const rowCache = fullAllDataRowIdData[dataRowid] return { @@ -446,19 +493,25 @@ const Methods = { } return null }, + + // 通过单元格DOM元素获取对应的列信息 getColumnNode(cell) { if (!cell) { return null } const { isGroup, fullColumnIdData, tableFullColumn } = this + // 获取单元格DOM元素上存储的列ID属性 const dataColid = cell.getAttribute('data-colid') const colCache = fullColumnIdData?.[dataColid] + + // 如果是分组表头,使用findTree查找对应的列 if (isGroup) { let matches = findTree(tableFullColumn, (column) => column.id === dataColid, headerProps) if (matches) { return matches } } else if (colCache) { + // 如果是普通表头,直接从缓存中获取 return { index: colCache.index, item: colCache.column, @@ -467,27 +520,37 @@ const Methods = { } return null }, + + // 获取行在数据数组中的索引 getRowIndex(row) { let { fullDataRowMap } = this return fullDataRowMap.has(row) ? fullDataRowMap.get(row).index : -1 }, + + // 获取列在列数组中的索引 getColumnIndex(column) { let { fullColumnMap } = this return fullColumnMap.has(column) ? fullColumnMap.get(column).index : -1 }, + + // 判断列是否为序号列 hasIndexColumn(column) { return column && column.type === 'index' }, + + // 定义行数据字段,确保行数据具有所有必要字段 defineField(row, copy) { if (!row || typeof row !== 'object') { return row } + // 如果需要复制,则创建行数据的深拷贝 if (copy) { row = clone(row, true) } let { visibleColumn } = this let rowKey = getTableRowKey(this) + // 为每个可见列,如果行中没有对应的属性,则设置默认值 visibleColumn.forEach(({ property, editor }) => { let propNotExist = property && !has(row, property) let propDefaultValue = editor && !isUndefined(editor.defaultValue) ? editor.defaultValue : null @@ -495,27 +558,35 @@ const Methods = { set(row, property, propDefaultValue) } }) - // 如果行数据的唯一主键不存在,则生成 + + // 如果行数据的唯一主键不存在,则生成新的唯一ID const rowId = get(row, rowKey) if (isNull(rowId) || rowId === '') { set(row, rowKey, getRowUniqueId()) } return row }, + + // 判断行是否为临时行(如新增的未保存行) isTemporaryRow(row) { let rowid = getRowid(this, row) return find(this.temporaryRows, (r) => rowid === getRowid(this, r)) }, + + // 创建新的数据记录 createData(records, copy) { let isArr = isArray(records) if (!isArr) { records = [records] } + // 对每条记录应用defineField处理 let tmp = records.map((record) => this.defineField(record, copy)) return new Promise((resolve) => { resolve(isArr ? tmp : tmp[0]) }) }, + + // 创建新行(会创建副本) createRow(records) { return this.createData(records, true) }, @@ -528,11 +599,14 @@ const Methods = { * 如果还额外传了field,则清空指定单元格内容; */ clearData(rows, field) { + // 根据参数决定清空范围 rows = !arguments.length ? this.tableFullData : rows && !isArray(rows) ? [rows] : rows rows.forEach((row) => { if (field) { + // 清空指定字段 set(row, field, null) } else { + // 清空行的所有字段 this.visibleColumn.forEach((column) => { column.property && setCellValue(row, column, null) }) @@ -540,9 +614,13 @@ const Methods = { }) return this.$nextTick() }, + + // 判断行是否为插入的新行 hasRowInsert(row) { return ~this.editStore.insertList.indexOf(row) }, + + // 比较行数据的指定字段值是否相等 compareRow(row, originalRow, field) { const value = get(row, field) const originalValue = get(originalRow, field) @@ -562,16 +640,21 @@ const Methods = { return result }, + + // 判断行数据是否有变更 hasRowChange(row, field) { const { tableSourceData, treeConfig, visibleColumn, backupMap, editConfig } = this const insertChanged = editConfig?.insertChanged ?? false const argsLength = arguments.length const rowId = getRowid(this, row) let originRow - // 新增的数据不需要检测 + + // 新增的数据不需要检测变更,直接根据配置返回 if (this.isTemporaryRow(row)) { return insertChanged } + + // 处理树形结构的行比较 if (treeConfig) { const children = treeConfig.children const cacheRow = backupMap.get(row) @@ -582,13 +665,17 @@ const Methods = { originRow = { ...cacheRow, [children]: null } } } else { + // 获取原始行数据 originRow = find(tableSourceData, (item) => rowId === getRowid(this, item)) } + if (originRow) { + // 如果指定了字段,只比较该字段 if (argsLength > 1) { return !this.compareRow(row, originRow, field) } + // 否则比较所有可见列的字段 for (let i = 0; i < visibleColumn.length; i++) { let { property } = visibleColumn[i] if (property && !this.compareRow(row, originRow, property)) { @@ -598,22 +685,28 @@ const Methods = { } return false }, + // 获取表格所有列 getColumns(columnIndex) { let { visibleColumn: columns } = this let argsLength = arguments.length return argsLength ? columns[columnIndex] : columns.slice(0) }, + + // 根据列ID获取列对象 getColumnById(colid) { let { fullColumnIdData } = this let colCache = fullColumnIdData[colid] return colCache ? colCache.column : null }, + + // 根据字段名获取列对象 getColumnByField(field) { let { visibleColumn: columns } = this return typeof field === 'string' && field ? find(columns, (column) => column.property === field) : null }, - // 获取当前表格的列(完整的全量表头列、处理条件之后的全量表头列、当前渲染中的表头列) + + // 获取当前表格的列配置(完整的全量表头列、处理条件之后的全量表头列、当前渲染中的表头列) getTableColumn() { let { collectColumn, tableColumn, tableFullColumn, visibleColumn } = this return { @@ -623,6 +716,7 @@ const Methods = { collectColumn: collectColumn.slice(0) } }, + // 获取表格所有数据 getData(rowIndex) { let tableSynchData = this.data || this.tableSynchData @@ -1650,7 +1744,8 @@ const Methods = { }) }) }, - // 更新横向 X 可视渲染上下剩余空间大小 + // 更新横向 X 可视渲染上下剩余空间大小(续) + // 设置主表头/主表体/主表尾表格元素的marginLeft(已滚动出渲染范围的列,不渲染但是保留宽度占位,保证对齐) updateScrollXSpace() { const { elemStore, scrollXLoad, scrollXStore, scrollbarWidth, tableWidth, visibleColumn } = this const { startIndex } = scrollXStore @@ -1673,6 +1768,8 @@ const Methods = { headerElem && (headerElem.style.marginLeft = marginLeft) bodyElem.style.marginLeft = marginLeft footerElem && (footerElem.style.marginLeft = marginLeft) + + // 设置各个区域的横向占位元素宽度 const layouts = ['header', 'body', 'footer'] layouts.forEach((layout) => { const xSpaceElem = elemStore[`main-${layout}-xSpace`] @@ -1686,11 +1783,14 @@ const Methods = { this.$nextTick(this.updateStyle) }, + // 使用requestAnimationFrame实现防抖 debounceRaf(handlerKey, callback) { + // 如果已有请求帧,先取消 if (this[handlerKey]) { cancelAnimationFrame(this[handlerKey]) } + // 请求新的动画帧 this[handlerKey] = requestAnimationFrame(() => { this[handlerKey] = null callback() @@ -1700,12 +1800,12 @@ const Methods = { updateScrollYData() { // 更新DOM样式保证表格滚动时的对齐 this.updateScrollYSpace() - // 节流更新响应数据 + // 使用requestAnimationFrame优化渲染 this.debounceRaf('updateScrollYDataHandler', () => { this.handleTableData().then(() => this.$nextTick(this.updateStyle)) }) }, - // 更新纵向虚拟滚动 Y 可视渲染上下剩余空间大小(使用tiny-grid-body__y-space元素撑开足够空间) + // 更新纵向虚拟滚动 Y 可视渲染上下剩余空间大小 updateScrollYSpace() { let { $grid, elemStore, scrollLoad, scrollLoadStore, scrollYLoad } = this let { rowHeight, startIndex } = this.scrollYStore @@ -1715,75 +1815,92 @@ const Methods = { let isVScrollOrLoad = scrollYLoad || scrollLoad let { marginTop, ySpaceHeight } = {} - // 通过开始渲染下标startIndex和表格的行高度来计算marginTop,虚滚场景为已滚动出渲染范围的行的总高度,滚动分页场景为空 + // 计算marginTop和空间高度 marginTop = isVScrollOrLoad && scrollYLoad ? `${Math.max(startIndex * rowHeight, 0)}px` : '' - // 虚滚场景的滚动高度,滚动分页场景的视口高度 ySpaceHeight = isVScrollOrLoad ? `${bodyHeight}px` : '' - // 滚动分页场景的视口高度和滚动高度缓存 + // 存储滚动分页相关数据 scrollLoadStore.bodyHeight = bodyHeight scrollLoadStore.scrollHeight = scrollHeight const tableElem = elemStore['main-body-table'] - // 这里最好使用transform3D,使用gpu加速,防止页面重绘 + // 使用transform设置表格偏移位置,更高效 if (tableElem) { tableElem.style.transform = marginTop ? `translateY(${marginTop})` : '' } + // 设置Y轴空间元素高度 const ySpaceElem = elemStore['main-body-ySpace'] ySpaceElem && (ySpaceElem.style.height = ySpaceHeight) - // 滚动分页加载逻辑 + // 滚动分页加载逻辑设置 if (ySpaceElem && scrollLoad && $grid) { Object.assign(scrollLoadStore, { bodyHeight, scrollHeight }) ySpaceElem.firstChild.style.height = `${scrollHeight}px` ySpaceElem.onscroll = this.debounceScrollLoad } }, + // 更新滚动加载条位置 updateScrollLoadBar(event) { let { $el, elemStore, scrollLoad, scrollLoadStore } = this if (scrollLoad && $el.contains(event.target)) { + // 处理鼠标滚轮事件,更新滚动位置 let wheelDelta = event.wheelDelta ? event.wheelDelta : -event.detail * 40 let scrollElm = elemStore['main-body-ySpace'] let { scrollHeight, bodyHeight } = scrollLoadStore let max = scrollHeight - bodyHeight let top = scrollElm.scrollTop - wheelDelta + + // 确保滚动位置在有效范围内 top = max < top ? max : top top = top < 0 ? 0 : top scrollElm.scrollTop = top } }, + // 滚动到指定位置 scrollTo(scrollLeft, scrollTop) { const { elemStore } = this const tableBodyElem = elemStore['main-body-wrapper'] const tableHeaderElem = elemStore['main-header-wrapper'] const tableFooterElem = elemStore['main-footer-wrapper'] + + // 滚动到指定的水平位置 if (isNumber(scrollLeft)) { tableBodyElem && (tableBodyElem.scrollLeft = scrollLeft) tableFooterElem && (tableFooterElem.scrollLeft = scrollLeft) tableHeaderElem && (tableHeaderElem.scrollLeft = scrollLeft) } + + // 滚动到指定的垂直位置 if (isNumber(scrollTop)) { tableBodyElem && (tableBodyElem.scrollTop = scrollTop) } + return this.$nextTick() }, + // 滚动到指定行 scrollToRow(row, column, isDelay, move) { let hasRowCache = this.fullAllDataRowMap.has(row) let isDelayArg = isDelay || isBoolean(column) + // 如果存在行,滚动到可见位置 row && hasRowCache && rowToVisible(this, row) return this.scrollToColumn(column, isDelayArg, move) }, + // 滚动到树形结构的指定行 scrollToTreeRow(row) { let { tableFullData, treeConfig, treeOpts } = this if (!treeConfig) { return this.$nextTick() } + + // 查找目标行在树中的路径 let matchObj = findTree(tableFullData, (item) => item === row, treeOpts) if (!matchObj) { return this.$nextTick() } + + // 展开路径上的所有节点 let nodes = matchObj.nodes nodes.forEach((row, index) => { if (index === nodes.length - 1 || this.hasTreeExpand(row)) { @@ -1791,62 +1908,79 @@ const Methods = { } this.setTreeExpansion(row, true) }) + return this.$nextTick() }, + // 滚动到指定列 scrollToColumn(column, isDelay, move) { let hasColCache = this.fullColumnMap.has(column) + // 如果列存在,滚动到可见位置 column && hasColCache && colToVisible(this, column, move) - // 虚滚场景 DOM 元素会延时渲染,DOM 元素不存在时校验会显示异常 + // 虚拟滚动场景下,DOM元素延迟渲染,需要额外等待 return isDelay && (this.scrollXLoad || this.scrollYLoad) ? new Promise((resolve) => setTimeout(() => resolve(this.$nextTick()), 50)) : this.$nextTick() }, + // 重置滚动顶部位置 resetScrollTop() { this.lastScrollTop = 0 }, + // 清除滚动状态 clearScroll() { let { scrollXStore, scrollYStore, elemStore } = this + // 重置滚动位置记录 Object.assign(this, { lastScrollLeft: 0, lastScrollTop: 0 }) + // 重置虚拟滚动存储 Object.assign(scrollXStore, { startIndex: 0, visibleIndex: 0 }) Object.assign(scrollYStore, { startIndex: 0, visibleIndex: 0 }) + this.$nextTick(() => { - // 从缓存中拿 DOM 元素 + // 从缓存中获取DOM元素 const tableBodyElem = elemStore['main-body-wrapper'] const tableHeaderElem = elemStore['main-header-wrapper'] const tableFooterElem = elemStore['main-footer-wrapper'] + // 重置所有区域的滚动位置 if (this.afterMounted) { tableBodyElem && Object.assign(tableBodyElem, { scrollLeft: 0, scrollTop: 0 }) tableFooterElem && Object.assign(tableFooterElem, { scrollLeft: 0 }) tableHeaderElem && Object.assign(tableHeaderElem, { scrollLeft: 0 }) } }) + return this.$nextTick() }, - // 更新表尾合计 + // 更新表尾合计行 updateFooter() { let { afterFullData, footerMethod, showFooter, summaryConfig, tableColumn } = this + + // 如果设置了自定义表尾方法 if (footerMethod && showFooter) { let data = footerMethod({ columns: tableColumn, data: afterFullData }) + // 如果返回的不是二维数组,转换为二维数组 if (data.length && data.some((value) => !isArray(value))) { data = [data] } this.footerData = tableColumn.length ? data : [] } + + // 如果设置了汇总配置 if (summaryConfig) { let { fields, fraction, text, truncate } = summaryConfig + // 构建汇总行数据 let summary = tableColumn.map((column, columnIndex) => { if (columnIndex === 0) { - return text || '' + return text || '' // 第一列显示汇总文本 } if (~fields.indexOf(column.property)) { - return toDecimal(sum(this.afterFullData, column.property), fraction, truncate) + return toDecimal(sum(this.afterFullData, column.property), fraction, truncate) // 计算汇总值 } return null }) this.footerData = [summary] } + return this.$nextTick() }, // 更新列状态:如果组件值v-model发生change,调用该函数更新列的编辑状态。如果单元格配置了校验规则,则进行校验 @@ -1859,15 +1993,19 @@ const Methods = { return } + // 如果设置了始终验证 if (renderOpts && renderOpts.isValidAlways) { validStore.visible = true } let { column, row } = scope let type = 'change' + // 如果没有相关校验规则,直接返回 if (!this.hasCellRules(type, row, column)) { return } + + // 获取单元格元素并执行校验 let rowIndex = tableData.indexOf(row) getCell(this, { row, rowIndex, column }).then((cell) => { if (!cell) { @@ -1875,22 +2013,26 @@ const Methods = { } return this.validCellRules(type, row, column, cellValue) .then(() => { + // 校验通过,设置新值并清除验证提示 customValue && validStore.visible && setCellValue(row, column, cellValue) this.clearValidate() }) .catch(({ rule }) => { + // 校验失败,设置新值并显示验证提示 customValue && setCellValue(row, column, cellValue) this.showValidTooltip({ rule, row, column, cell }) }) }) }) }, - /* X/Y 方向滚动 */ + /* X/Y 方向滚动状态更新 */ updateScrollStatus() { + // 防抖处理滚动状态更新 if (!this.tasks.updateScrollStatus) { this.tasks.updateScrollStatus = debounce(AsyncCollectTimeout, () => { const { scrollXLoad, scrollYLoad, isAsyncColumn } = this + // 如果存在异步列并且开启了虚拟滚动 if (isAsyncColumn && (scrollXLoad || scrollYLoad)) { const { tableData, scrollXStore, scrollYStore, tableFullData, scrollDirection = 'N' } = this const isInit = @@ -1911,85 +2053,114 @@ const Methods = { getAsyncColumnUniqueKey(property, row) { return `${property}_${row[this.rowId]}` }, + // 获取异步列名称 getAsyncColumnName(property) { return GlobalConfig.constant.asyncPrefix + property }, + // 收集异步列 collectAsyncColumn(tableData) { const fetchColumns = [] const { rowId, asyncRenderMap, tableColumn } = this + + // 确保rowId存在 if (!rowId) { warn('The (grid-props:rowId) is required for the asynchronous column.') return fetchColumns } + + // 遍历所有列,找出需要异步渲染的列 tableColumn.forEach((col) => { const { async } = col.format || {} const { fetch, splitConfig = {} } = async || {} + if (typeof fetch === 'function') { const columnValues = [] + + // 收集列中所有行的值 tableData.forEach((row) => { let cellValue = row[col.property] if (typeof cellValue !== 'string' || (typeof cellValue === 'string' && !cellValue)) { cellValue = ' ' } + let cellValuesCount = 1 let cellValues = [cellValue] const uniqueKey = this.getAsyncColumnUniqueKey(col.property, row) - // 默认不开启 + + // 支持值分割配置 if (splitConfig.enabled === true) { cellValues = cellValue.split(splitConfig.valueSplit || ',') cellValuesCount = cellValues.length } + + // 缓存单元格值计数,避免重复加载 if (!asyncRenderMap[uniqueKey]) { - // 以行主键、列名作为缓存的 Key 防止重复加载(缓存单元格显示值的个数) asyncRenderMap[uniqueKey] = cellValuesCount - // 单元格多值支持 + // 添加所有单元格值到列值集合 cellValues.forEach((value) => columnValues.push(value)) } }) + + // 如果有需要异步处理的值,将列添加到结果中 if (columnValues.length) { fetchColumns.push({ ...col, columnValues }) } } }) + return fetchColumns }, - // fetchData 执行 + + // 处理异步列数据加载 handleAsyncColumn(tableData) { if (this.isAsyncColumn && tableData.length) { // 每次请求都需要清空加载缓存 this.asyncRenderMap = {} + // 收集并处理异步列 this.handleResolveColumn(tableData, this.collectAsyncColumn(tableData)) } }, - // 查询异步列 + + // 处理异步列数据解析 handleResolveColumn(tableData, fetchColumns) { const { tableColumn, scrollYStore, asyncRenderMap, scrollXLoad, scrollYLoad } = this const { startIndex } = scrollYStore const isScrollLoad = scrollXLoad || scrollYLoad + + // 如果没有需要处理的列,直接返回 if (fetchColumns.length === 0) { return } + + // 创建所有列数据获取的Promise const promises = mapFetchColumnPromise({ _vm: this, fetchColumns, tableColumn }) + + // 处理所有Promise结果 Promise.all(promises).then( handleAllColumnPromises({ startIndex, fetchColumns, tableData, asyncRenderMap, isScrollLoad }, this) ) }, - // Publish methods 与工具栏对接 + + // 与工具栏组件对接 connect({ toolbar }) { this.$toolbar = toolbar }, + // 检查触发源是否属于目标节点 getEventTargetNode, + // 可见性改变事件处理 handleVisibilityChange(visible, entry) { if (visible) { + // 可见时更新高度和布局 this.updateParentHeight() this.updateTableBodyHeight() this.recalculate() } + // 触发可见性变化事件 emitEvent(this, 'visible-change', [{ $table: this, visible, entry }]) }, @@ -1997,6 +2168,7 @@ const Methods = { updateTableBodyHeight() { if (!this.tasks.updateTableBodyHeight) { this.tasks.updateTableBodyHeight = () => { + // 使用fastdom避免布局抖动 fastdom.measure(() => { const tableBodyElem = this.elemStore['main-body-wrapper'] this.tableBodyHeight = tableBodyElem ? tableBodyElem.clientHeight : 0 @@ -2006,13 +2178,17 @@ const Methods = { this.tasks.updateTableBodyHeight() }, + // 按顺序切换列的排序状态(null --> asc --> desc --> null --> ...) toggleColumnOrder(column) { return column.order ? (column.order === 'asc' ? 'desc' : null) : 'asc' }, + + // 为Vue3监听数据变化 watchDataForVue3() { if (isVue2) return + // 创建侦听器,监控数组及其长度变化 const stopWatch = hooks.watch( [() => this.data, () => this.data && this.data.length], ([newData, newLength], [oldData, oldLength]) => { @@ -2023,28 +2199,37 @@ const Methods = { } ) + // 组件卸载时清除侦听器 hooks.onBeforeUnmount(() => stopWatch()) }, + + // 获取特定名称的vm实例 getVm(name) { return this.$grid.getVm(name) }, + + // 组装列配置 assembleColumns() { // 如果没有初始化任何列实例就不进行列组装 if (!this.isTagUsageSence) return assemColumn(this) }, + + // 验证列名是否有效 isValidCustomColumn(columnName) { return columnName && this.columnNames.includes(columnName) }, + + // 计算列集合的唯一键 computeCollectKey() { const columnIds = [] + // 遍历列树结构收集列ID const traverse = (columns) => { if (Array.isArray(columns) && columns.length > 0) { columns.forEach((column) => { columnIds.push(column.columnConfig.id) - traverse(column.childColumns) }) } @@ -2052,12 +2237,15 @@ const Methods = { traverse(this.childColumns) + // 生成逗号分隔的ID字符串作为键 return columnIds.join(',') }, + // 获取所有多选数据状态 getAllSelection() { return this.selection }, + // 尝试恢复滚动位置,规范了最大滚动位置的取值 attemptRestoreScoll(options) { let { lastScrollTop, lastScrollLeft } = options || this @@ -2065,18 +2253,24 @@ const Methods = { const { scrollXLoad, scrollYLoad, elemStore } = this const tableBodyElem = elemStore['main-body-wrapper'] + // 如果有上次滚动位置并且表格主体存在 if ((lastScrollTop || lastScrollLeft) && tableBodyElem) { + // 使用fastdom避免布局抖动 fastdom.measure(() => { + // 计算最大滚动范围 const maxScrollTop = tableBodyElem.scrollHeight - tableBodyElem.offsetHeight const maxScrollLeft = tableBodyElem.scrollWidth - tableBodyElem.offsetWidth + // 确保滚动位置不超过最大范围 lastScrollTop = Math.min(lastScrollTop, maxScrollTop) lastScrollLeft = Math.min(lastScrollLeft, maxScrollLeft) + // 设置滚动位置 fastdom.mutate(() => { this.restoreScollFlag = true this.scrollTo(lastScrollLeft, lastScrollTop) + // 触发滚动事件更新虚拟滚动渲染 scrollXLoad && this.triggerScrollXEvent() scrollYLoad && this.triggerScrollYEvent({ target: { scrollTop: lastScrollTop } }) }) @@ -2086,9 +2280,12 @@ const Methods = { return this.$nextTick() } } + +// 添加所有功能方法 funcs.forEach((name) => { Methods[name] = function (...args) { return this[`_${name}`] ? this[`_${name}`](...args) : null } }) + export default Methods From 20b65ef03823928832ea61cfb305ca38e44c3453 Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Mon, 31 Mar 2025 15:51:15 +0800 Subject: [PATCH 11/52] =?UTF-8?q?feat(grid):=20=E5=A2=9E=E5=BC=BAGrid?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E6=B3=A8=E9=87=8A=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=AE=89=E8=A3=85=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E8=A1=A8=E6=A0=BC=E6=B8=B2=E6=9F=93=E5=92=8C?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=9B=B4=E7=81=B5=E6=B4=BB=E7=9A=84=E9=85=8D=E7=BD=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/adapter/index.ts | 27 ++ .../vue/src/grid/src/adapter/src/renderer.ts | 260 +++++++++++++++++- .../vue/src/grid/src/adapter/src/setup.ts | 8 + packages/vue/src/grid/src/table/src/Table.vue | 166 +++++++++-- .../grid/src/table/src/utils/autoCellWidth.ts | 114 ++++++-- 5 files changed, 526 insertions(+), 49 deletions(-) diff --git a/packages/vue/src/grid/src/adapter/index.ts b/packages/vue/src/grid/src/adapter/index.ts index 53a4beea31..66228bcf7c 100644 --- a/packages/vue/src/grid/src/adapter/index.ts +++ b/packages/vue/src/grid/src/adapter/index.ts @@ -13,27 +13,54 @@ import { Interceptor, StoreMap } from '@opentiny/vue-renderless/grid/core' import Setup from './src/setup' import Renderer from './src/renderer' +// 存储已安装的插件列表,避免重复安装 const installedPlugins = [] +// 按钮和菜单的存储映射,用于管理表格的按钮和菜单配置 const Buttons = StoreMap const Menus = StoreMap +/** + * Grid表格核心对象,提供插件安装、配置管理等功能 + */ export const Grid = { + /** + * 安装Grid插件 + * @param {Object} Plugin - 需要安装的插件对象,必须包含install方法 + * @param {Object} options - 插件的配置选项 + * @returns {Object} 返回Grid实例,支持链式调用 + */ use(Plugin, options) { + // 检查插件是否有效且包含install方法 if (Plugin && Plugin.install) { + // 检查插件是否已安装,避免重复安装 if (!installedPlugins.includes(Plugin)) { + // 调用插件的install方法进行安装 Plugin.install(this, options) + // 将插件添加到已安装列表 installedPlugins.push(Plugin) } } return this }, + + // 全局配置设置函数 setup: Setup, + + // 拦截器,用于拦截和处理表格的各种事件 interceptor: Interceptor, + + // 渲染器,用于自定义表格的渲染逻辑 renderer: Renderer, + + // 按钮配置存储 buttons: Buttons, + + // 菜单配置存储 menus: Menus, + + // 是否启用tooltip功能的标志 _tooltip: true } diff --git a/packages/vue/src/grid/src/adapter/src/renderer.ts b/packages/vue/src/grid/src/adapter/src/renderer.ts index 776a7ea3bc..a06449e79a 100644 --- a/packages/vue/src/grid/src/adapter/src/renderer.ts +++ b/packages/vue/src/grid/src/adapter/src/renderer.ts @@ -26,6 +26,13 @@ import { set, assign, objectMap, get, each, isObject, isFunction } from '@openti import { getCellValue, setCellValue } from '@opentiny/vue-renderless/grid/utils' import { hooks } from '@opentiny/vue-common' +/** + * 获取组件属性 + * @param name - 组件名称 + * @param attrs - 属性对象或函数 + * @param params - 参数对象 + * @returns 处理后的属性对象 + */ function getAttrs({ name, attrs }, params) { let props = attrs @@ -33,6 +40,7 @@ function getAttrs({ name, attrs }, params) { props = attrs(params) } + // 如果是input组件,默认设置type为text if (name === 'input') { props = { type: 'text', ...props } } @@ -40,8 +48,19 @@ function getAttrs({ name, attrs }, params) { return props } +/** + * 判断是否需要同步单元格数据 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ const isSyncCell = (renderOpts, params, context) => renderOpts.type === 'visible' || context.$type === 'cell' +/** + * 判断是否需要自动刷新编辑器 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + */ function autoRefresh(renderOpts, params) { let { refresh = false } = renderOpts let { $table, row } = params @@ -55,79 +74,163 @@ function autoRefresh(renderOpts, params) { ) } +/** + * 获取组件事件处理器 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + * @returns 事件处理器对象 + */ function getEvents(renderOpts, params, context) { + // 从渲染选项中获取组件和事件配置 let { component = {}, events = {} } = renderOpts + // 判断是否为原生组件(字符串形式) let native = typeof component === 'string' + // 获取组件的model配置 let editorModel = component.model || {} + // 根据组件类型确定事件类型: + // - select组件使用change事件 + // - 原生组件使用input事件 + // - 其他组件使用model中定义的事件或默认的update:modelValue let type = component === 'select' ? 'change' : native ? 'input' : editorModel.event || 'update:modelValue' + // 解构出input和change事件处理函数 let { input, change, ...other } = events + // 获取表格、列和行数据 let { $table, column, row } = params + // 获取列的model对象 let { model } = column + // 定义事件处理器对象 let on = { + // 动态事件名称 [type](event) { + // 获取单元格的值: + // - 原生组件从event.target.value获取 + // - 其他组件直接使用event作为值 let cellValue = native ? event.target.value : event + // 根据配置决定如何更新数据: + // 1. 如果不需要始终验证且需要同步单元格,直接设置单元格值 if (!renderOpts.isValidAlways && isSyncCell(renderOpts, params, context)) { setCellValue(row, column, cellValue) } else { + // 2. 否则更新数据和状态: + // - 非原生组件设置属性值 + // - 更新model状态 + // - 更新表格状态 native || set(row, column.property, cellValue) model.update = true model.value = cellValue $table.updateStatus(params, cellValue, renderOpts) } + // 对原生组件调用input和change回调 if (native) { input && input.apply(null, [params].concat.apply(params, arguments)) change && change.apply(null, [params].concat.apply(params, arguments)) } + // 如果需要自动刷新,增加刷新计数器 if (autoRefresh(renderOpts, params, context)) { $table.editStore.editorAutoRefreshKey++ } } } + // 根据是否为原生组件选择事件对象 + // - 原生组件使用除input/change外的其他事件 + // - 非原生组件使用完整的events对象 let evts = native ? other : events + + // 创建事件处理器包装函数 + // 将每个事件处理器包装成一个新函数,添加params参数 let mapHandler = (cb) => function () { + // 调用原始事件处理器 + // 将params作为第一个参数 + // 将原始事件参数展开作为后续参数 cb.apply(null, [params].concat.apply(params, arguments)) } + // 将包装后的事件处理器对象合并到on中 + // objectMap遍历evts中的每个事件处理器并用mapHandler包装 + // 最后通过assign合并到on对象 assign(on, objectMap(evts, mapHandler)) return on } +/** + * 渲染下拉选项 + * @param h - 渲染函数 + * @param options - 选项数组 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ function renderOptions(h, options, renderOpts, params, context) { + // 从渲染选项中获取选项属性配置,如果没有则使用空对象 const { optionProps = {} } = renderOpts + + // 获取标签和值的属性名,默认分别为'label'和'value' const labelProp = optionProps.label || 'label' const valueProp = optionProps.value || 'value' + + // 从参数中获取列和行数据 const { column, row } = params + + // 获取列的格式化配置 const { formatConfig } = column.own + + // 获取单元格的值: + // - 如果需要同步单元格,则从行数据中获取 + // - 否则使用列模型中的值 const cellValue = isSyncCell(renderOpts, params, context) ? getCellValue(row, column) : column.model.value + // 如果没有传入选项数据,但有格式化配置数据,则使用格式化配置数据 if (!options && formatConfig && formatConfig.data) { options = formatConfig.data } + + // 标记是否有选中项 let hasSelected = false + + // 遍历选项数组,生成option元素列表 const optionsList = options.map((item, index) => { + // 判断当前选项是否被选中 const selected = item.value === cellValue if (selected) { hasSelected = true } + + // 构造option元素的属性 const attrs = { - domProps: { value: item[valueProp], selected }, - key: index + domProps: { + value: item[valueProp], // 选项值 + selected // 是否选中 + }, + key: index // 唯一key } + + // 创建option元素,显示选项标签文本 return h('option', attrs, item[labelProp]) }) + + // 如果有选项但没有选中项,在开头添加一个空的占位选项 if (options.length && !hasSelected) { optionsList.unshift(h('option', { style: 'display:none', selected: true }, '')) } + return optionsList } +/** + * 渲染分组下拉选项 + * @param h - 渲染函数 + * @param options - 选项配置 + * @param params - 参数对象 + * @param context - 上下文 + */ function renderOptgroups(h, options, params, context) { let { optionGroups, optionGroupProps = {} } = options let groupLabel = optionGroupProps.label || 'label' @@ -141,6 +244,13 @@ function renderOptgroups(h, options, params, context) { }) } +/** + * 处理筛选确认事件 + * @param context - 上下文 + * @param column - 列配置 + * @param checked - 是否选中 + * @param item - 选项项 + */ function handleConfirmFilter(context, column, checked, item) { let key = column.filterMultiple ? 'changeMultipleOption' : 'changeRadioOption' let method = context[key] @@ -149,28 +259,49 @@ function handleConfirmFilter(context, column, checked, item) { } function getFilterEvents(item, renderOpts, params, context) { + // 从渲染选项中解构出事件配置和组件名称 let { events, name } = renderOpts + // 从参数中解构出列配置 let { column } = params + // 根据组件名称确定事件类型,select用change事件,其他用input事件 let type = name === 'select' ? 'change' : 'input' + + // 定义基础事件处理对象 let on = { + // 动态事件名称 [type](event) { + // 更新选项数据为目标元素的值 item.data = event.target.value + // 调用筛选确认处理函数 handleConfirmFilter(context, column, !!item.data, item) } } + // 如果配置了额外的事件处理函数 if (events) { + // 定义事件处理函数包装器 let mapHandler = (cb) => function () { + // 将params作为第一个参数,并将原始参数展开作为后续参数 cb.apply(null, [params].concat.apply(params, arguments)) } + // 将包装后的事件处理函数合并到on对象中 assign(on, objectMap(events, mapHandler)) } + // 返回所有事件处理函数 return on } +/** + * 默认的筛选器渲染函数 + * 用于渲染筛选器组件 + * @param h - 渲染函数 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ function defaultFilterRender(h, renderOpts, params, context) { let { name } = renderOpts let { column } = params @@ -188,12 +319,27 @@ function defaultFilterRender(h, renderOpts, params, context) { }) } +/** + * 默认的筛选器方法 + * 用于比较选项数据和单元格值 + * @param option - 选项数据 + * @param row - 行数据 + * @param column - 列配置 + */ function defaultFilterMethod({ option, row, column }) { let cellValue = get(row, column.property) let data = option.data return cellValue == data } +/** + * 渲染选择编辑组件 + * 用于渲染选择编辑组件 + * @param h - 渲染函数 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ function renderSelectEdit(h, renderOpts, params, context) { let props = { class: 'tiny-grid-default-select', @@ -207,23 +353,54 @@ function renderSelectEdit(h, renderOpts, params, context) { } /** - * 内置渲染器。支持原生的 input、textarea、select。 + * 默认的编辑器渲染函数 + * 支持原生input、textarea、select等组件的渲染 + * @param h - 渲染函数 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 */ function defaultEditRender(h, renderOpts, params, context) { + // 解构获取表格实例、列配置和行数据 let { $table, column, row } = params + + // 获取列的格式化函数和配置信息 + // formatValue用于格式化单元格值,默认直接返回原值 let { formatValue = ({ cellValue }) => cellValue, own } = column + + // 获取格式化配置 let formatOpt = own.formatConfig || {} + + // 获取渲染选项中的子组件和组件配置 let { children, component = {} } = renderOpts + + // 获取组件属性 let attrs = getAttrs(renderOpts, params, formatOpt) + + // 获取单元格值 + // 如果需要同步单元格,则从行数据中获取,否则使用列模型的值 let value = isSyncCell(renderOpts, params, context) ? getCellValue(row, column) : column.model.value + + // 格式化单元格值 let cellValue = formatValue({ cellValue: value, ...params }) + + // 判断是否为原生HTML标签(input/textarea/select) let isTag = ~['input', 'textarea', 'select'].indexOf(component) + + // 获取组件的model配置 let editorModel = component.model || {} + + // 确定model属性名 + // 如果是字符串组件使用value,否则使用modelValue或自定义的prop let modelProps = typeof component === 'string' ? 'value' : editorModel.prop || 'modelValue' + // 获取行的唯一标识作为key const key = row[$table.rowId] + + // 构建组件选项 let options = { key, + // 如果是原生标签则添加默认类名 class: isTag ? `tiny-grid-default-${component}` : '', attrs: { formatOpt, @@ -231,55 +408,83 @@ function defaultEditRender(h, renderOpts, params, context) { ...attrs, [modelProps]: cellValue }, + // 绑定事件处理函数 on: getEvents(renderOpts, params, context) } + // 如果需要自动刷新,添加刷新key if (autoRefresh(renderOpts, params, context)) { options.attrs.editorAutoRefreshKey = $table.editStore.editorAutoRefreshKey } + // 渲染插槽内容 let slot = children ? children({ props: options, ...params }, h) : null + + // 创建组件VNode let cell = [h(hooks.toRaw(component), options, slot)] + // 如果是原生标签直接返回,否则包裹一层编辑器容器 return isTag ? cell : [h('div', { class: 'tiny-grid-editor' }, cell)] } +/** + * 构建渲染器映射表 + * 包含了input、textarea、select等基础组件的渲染配置 + */ let buildRenderMap = () => { + // 定义input类型渲染器的基础配置数组 + // 包含自动聚焦、编辑渲染、默认渲染、过滤渲染和过滤方法等配置项 let renderMapInput = [ - ['autofocus', 'input'], - ['renderEdit', defaultEditRender], - ['renderDefault', defaultEditRender], - ['renderFilter', defaultFilterRender], - ['filterMethod', defaultFilterMethod] + ['autofocus', 'input'], // 自动聚焦配置 + ['renderEdit', defaultEditRender], // 编辑模式下的渲染函数 + ['renderDefault', defaultEditRender], // 默认渲染函数 + ['renderFilter', defaultFilterRender], // 过滤器的渲染函数 + ['filterMethod', defaultFilterMethod] // 过滤方法 ] + + // 将配置数组转换为对象的工具函数 let mapHandler = (mapArr) => { let obj = {} + // 遍历数组,将每一项转换为对象的键值对 mapArr.forEach((item) => (obj[item[0]] = item[1])) return obj } + + // 创建渲染器映射对象 let renderMap = {} + // 配置input类型渲染器 renderMap.input = mapHandler(renderMapInput) + // 配置textarea类型渲染器 + // 将autofocus的值改为textarea renderMapInput[0][1] = 'textarea' renderMap.textarea = mapHandler(renderMapInput) - renderMapInput.splice(0, 1) + // 配置select类型渲染器 + renderMapInput.splice(0, 1) // 移除autofocus配置 + // 设置select的编辑和默认渲染函数 renderMapInput[0][1] = renderMapInput[1][1] = renderSelectEdit + // 自定义select的过滤渲染函数 renderMapInput[2][1] = function (h, renderOpts, params, context) { let { attrs } = renderOpts let { column } = params + // 遍历过滤器配置,渲染select选项 return column.filters.map((item) => { + // 构建select的属性配置 let props = { attrs, class: 'tiny-grid-default-select', on: getFilterEvents(item, renderOpts, params, context) } + + // 根据是否有选项组决定渲染方式 let children = renderOpts.optionGroups - ? renderOptgroups(h, renderOpts, params) - : renderOptions(h, renderOpts.options, renderOpts, params) + ? renderOptgroups(h, renderOpts, params) // 渲染选项组 + : renderOptions(h, renderOpts.options, renderOpts, params) // 渲染选项列表 + // 创建select元素 return h('select', props, children) }) } @@ -290,38 +495,66 @@ let buildRenderMap = () => { const renderMap = buildRenderMap() +/** + * 渲染器工厂函数 + * 提供了添加、获取、删除渲染器的方法 + */ let buildRenderer = () => { let Renderer = {} + /** + * 混入新的渲染器配置 + * @param map - 渲染器配置映射 + */ Renderer.mixin = function (map) { each(map, (options, name) => Renderer.add(name, options)) return Renderer } - // 支持动态组件:function (resolve, reject) { setTimeout(function () { resolve(xxx) }, 1) } + /** + * 获取指定名称的渲染器 + * @param name - 渲染器名称 + */ Renderer.get = function (name) { return isObject(name) || isFunction(name) ? renderMap.input : renderMap[name] || null } + /** + * 添加新的渲染器 + * @param name - 渲染器名称 + * @param options - 渲染器配置 + */ Renderer.add = function (name, options) { + // 声明变量用于存储已有的渲染器 let renders + // 检查name和options参数是否都存在 let flag = name && options + // 如果参数不完整则直接返回Renderer对象 if (!flag) { return Renderer } + // 获取renderMap中已存在的同名渲染器 renders = renderMap[name] + // 如果已存在同名渲染器 if (renders) { + // 将新的配置合并到已有渲染器中 Object.assign(renders, options) } else { + // 如果不存在则直接添加新的渲染器配置 renderMap[name] = options } + // 返回Renderer对象以支持链式调用 return Renderer } + /** + * 删除指定的渲染器 + * @param name - 渲染器名称 + */ Renderer.delete = function (name) { delete renderMap[name] return Renderer @@ -331,7 +564,8 @@ let buildRenderer = () => { } /** - * 全局渲染器 + * 导出全局渲染器实例 + * 用于管理和扩展表格的渲染功能 */ export const Renderer = buildRenderer() diff --git a/packages/vue/src/grid/src/adapter/src/setup.ts b/packages/vue/src/grid/src/adapter/src/setup.ts index 739ee8873a..d0dcd73a1d 100644 --- a/packages/vue/src/grid/src/adapter/src/setup.ts +++ b/packages/vue/src/grid/src/adapter/src/setup.ts @@ -27,16 +27,24 @@ import GlobalConfig from '../../config' // 全局参数设置 const setup = (options = {}) => { + // 从全局配置中解构出图标和菜单配置 let { icon, menu } = GlobalConfig + // 如果传入了菜单配置,则将其与全局菜单配置深度合并 + // 使用extend(true, ...)进行深拷贝,避免修改原始对象 if (options.menu) { menu = extend(true, {}, menu, options.menu) } + // 如果传入了图标配置,则将其与全局图标配置深度合并 + // 使用extend(true, ...)进行深拷贝,避免修改原始对象 if (options.icon) { icon = extend(true, {}, icon, options.icon) } + // 将所有配置项深度合并到全局配置中 + // 注意:icon和menu需要单独传入以保持其引用不变 + // 这样可以确保其他地方对这些对象的引用依然有效 extend(true, GlobalConfig, options, { icon, menu }) } diff --git a/packages/vue/src/grid/src/table/src/Table.vue b/packages/vue/src/grid/src/table/src/Table.vue index 5b3dff242f..f16b96a4e0 100644 --- a/packages/vue/src/grid/src/table/src/Table.vue +++ b/packages/vue/src/grid/src/table/src/Table.vue @@ -1,14 +1,14 @@ From 8e812774a464b67484cd9597cacc758cb4293089 Mon Sep 17 00:00:00 2001 From: Gimmy <975402925@qq.com> Date: Tue, 8 Apr 2025 16:58:42 +0800 Subject: [PATCH 17/52] fix: sync aui table optimization --- packages/renderless/src/grid/utils/dom.ts | 4 +- packages/theme-saas/src/grid/body.less | 6 + packages/theme-saas/src/grid/header.less | 53 -- packages/theme-saas/src/grid/table.less | 122 +++- packages/theme/src/grid/body.less | 6 + packages/theme/src/grid/header.less | 2 - packages/theme/src/grid/table.less | 91 ++- packages/vue-locale/src/format.ts | 10 + packages/vue-locale/src/vue2.7/index.ts | 4 +- packages/vue-locale/src/vue2/index.ts | 4 +- packages/vue-locale/src/vue3/index.ts | 4 +- .../vue/src/grid/src/adapter/src/renderer.ts | 28 +- packages/vue/src/grid/src/body/src/body.tsx | 4 - packages/vue/src/grid/src/body/src/body.vue | 76 +-- .../vue/src/grid/src/body/src/useHeader.ts | 114 ++++ packages/vue/src/grid/src/body/src/usePool.ts | 82 +++ .../vue/src/grid/src/checkbox/src/methods.ts | 10 +- packages/vue/src/grid/src/composable/index.ts | 1 + .../src/grid/src/composable/useCellStatus.ts | 50 ++ .../src/grid/src/composable/useDrag/index.ts | 11 + .../vue/src/grid/src/dragger/src/methods.ts | 2 +- packages/vue/src/grid/src/edit/src/methods.ts | 9 +- packages/vue/src/grid/src/footer/index.ts | 32 - .../vue/src/grid/src/footer/src/footer.ts | 359 ----------- packages/vue/src/grid/src/header/index.ts | 32 - .../vue/src/grid/src/header/src/header.ts | 579 ------------------ .../vue/src/grid/src/keyboard/src/methods.ts | 16 +- .../vue/src/grid/src/mobile-first/index.vue | 3 +- .../vue/src/grid/src/resize/src/methods.ts | 1 - packages/vue/src/grid/src/table/src/Table.vue | 226 ++++--- .../vue/src/grid/src/table/src/methods.ts | 118 ++-- .../vue/src/grid/src/table/src/strategy.ts | 4 +- .../src/table/src/utils/computeScrollLoad.ts | 2 +- .../vue/src/grid/src/tooltip/src/methods.ts | 5 +- 34 files changed, 670 insertions(+), 1400 deletions(-) create mode 100644 packages/vue/src/grid/src/body/src/useHeader.ts create mode 100644 packages/vue/src/grid/src/body/src/usePool.ts create mode 100644 packages/vue/src/grid/src/composable/useCellStatus.ts delete mode 100644 packages/vue/src/grid/src/footer/index.ts delete mode 100644 packages/vue/src/grid/src/footer/src/footer.ts delete mode 100644 packages/vue/src/grid/src/header/index.ts delete mode 100644 packages/vue/src/grid/src/header/src/header.ts diff --git a/packages/renderless/src/grid/utils/dom.ts b/packages/renderless/src/grid/utils/dom.ts index f8cd4fb266..f319dc4007 100644 --- a/packages/renderless/src/grid/utils/dom.ts +++ b/packages/renderless/src/grid/utils/dom.ts @@ -36,8 +36,8 @@ export const isPx = (val) => val && /^\d+(px)?$/.test(val) export const isScale = (val) => val && /^\d+%$/.test(val) -export const updateCellTitle = (event) => { - const cellEl = event.currentTarget.querySelector(CELL_CLS) +export const updateCellTitle = (event, td) => { + const cellEl = td ? td.querySelector(CELL_CLS) : event.currentTarget.querySelector(CELL_CLS) const content = cellEl.innerText if (cellEl.getAttribute('title') !== content) { diff --git a/packages/theme-saas/src/grid/body.less b/packages/theme-saas/src/grid/body.less index b14ed9fdd7..a45c2e2983 100644 --- a/packages/theme-saas/src/grid/body.less +++ b/packages/theme-saas/src/grid/body.less @@ -10,6 +10,12 @@ @apply border-b border-b-color-bg-3; @apply overflow-y-auto; @apply overflow-x-auto; + + &.no-data { + @apply overflow-y-hidden; + @apply flex; + @apply flex-col; + } } .@{grid-prefix-cls}__borders { diff --git a/packages/theme-saas/src/grid/header.less b/packages/theme-saas/src/grid/header.less index cfa48b981e..ac0170ce15 100644 --- a/packages/theme-saas/src/grid/header.less +++ b/packages/theme-saas/src/grid/header.less @@ -4,8 +4,6 @@ @grid-header-prefix-cls: ~'@{css-prefix}grid-header'; @grid-cell-prefix-cls: ~'@{css-prefix}grid-cell'; @grid-checkbox-prefix-cls: ~'@{css-prefix}grid-checkbox'; -@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; -@cell-tooltip: ~'@{grid-prefix-cls}-cell__tooltip'; .@{grid-prefix-cls}__header-wrapper { @apply bg-color-fill-8; @@ -167,57 +165,6 @@ } } -.@{grid-prefix-cls}__header { - .@{header-suffix} { - @apply relative; - min-height: 16px; - - .suffix-icon-1 { - @apply absolute; - @apply right-3; - } - - .suffix-icon-0 { - @apply absolute; - @apply right-0; - } - } - - .col__ellipsis { - &.is__editable.is__sortable.is__filter { - .@{header-suffix}.@{cell-tooltip} { - @apply pr-7; - } - } - - &.is__editable.is__sortable:not(.is__filter), - &.is__editable.is__filter:not(.is__sortable) { - .@{header-suffix}.@{cell-tooltip} { - @apply ~'pr-3.5'; - } - } - - &:not(.is__sortable):not(.is__filter) { - .@{header-suffix}.@{cell-tooltip} { - @apply pr-2; - } - } - - &.is__sortable.is__filter:not(.is__editable) { - .@{header-suffix}.@{cell-tooltip} { - padding-right: 26px; - } - } - - &.is__sortable:not(.is__filter):not(.is__editable), - &.is__filter:not(.is__sortable):not(.is__editable) { - .@{header-suffix}.@{cell-tooltip} { - @apply pr-3; - } - } - } -} - .@{grid-prefix-cls} { th.col__selection > .@{grid-cell-prefix-cls} { @apply relative; diff --git a/packages/theme-saas/src/grid/table.less b/packages/theme-saas/src/grid/table.less index 43cc06ad16..6c21a20592 100644 --- a/packages/theme-saas/src/grid/table.less +++ b/packages/theme-saas/src/grid/table.less @@ -7,6 +7,8 @@ @input-prefix-cls: ~'@{css-prefix}input'; @select-prefix-cls: ~'@{css-prefix}select'; @pager-prefix-cls: ~'@{css-prefix}pager'; +@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; +@cell-tooltip: ~'@{grid-prefix-cls}-cell__tooltip'; // table .@{grid-prefix-cls} { @@ -372,7 +374,7 @@ } &&__group-saas { - .@{grid-prefix-cls}__header { + .@{grid-prefix-cls}__body thead { @apply relative; &::before { @@ -418,7 +420,7 @@ } &&__border-vertical { - .@{grid-prefix-cls}__body { + .@{grid-prefix-cls}__body tbody { @apply relative; &::before { @@ -861,37 +863,12 @@ } & &__empty-block { - @apply hidden; - @apply opacity-0; - @apply h-full; - @apply ~"min-h-[theme('spacing.16')]"; - @apply py-16 px-0; - @apply justify-center; - @apply items-center; - @apply text-center; - - &.is__visible { - @apply flex; - @apply flex-col; - @apply opacity-100; - &.is__center { - @apply opacity-0; - } - } - } - - .empty-center-block { - @apply ~'z-[1]'; @apply flex; @apply flex-col; - @apply justify-center; - @apply text-center; - @apply absolute; - @apply w-full; - - .@{grid-prefix-cls}__empty-text { - @apply w-full; - } + @apply flex-auto; + @apply items-center; + @apply sticky; + @apply left-0; } & &__empty-img { @@ -904,8 +881,8 @@ & &__empty-text { @apply block; - @apply mt-2; - @apply ~'w-1/2'; + @apply w-full; + @apply text-center; } & &-body__column { @@ -1012,12 +989,12 @@ & &__body-wrapper { &.body__wrapper.is__scrollload { @apply overflow-y-hidden; - @apply static; } } & .is__scrollload &-body__y-space { @apply absolute; @apply right-0; + @apply bottom-0; @apply w-3; @apply overflow-y-scroll; @@ -1375,4 +1352,81 @@ } } } + + .@{grid-prefix-cls}__body { + .tiny-grid-header__column { + @apply sticky; + /* --tiny-color-fill-8 真实对应 rgba(31, 85, 181, .05) */ + background-color: var(--tiny-color-fill-8-solid, #f4f6fb); + } + + .tiny-grid-header__column:last-child { + contain: layout; + } + + .tiny-grid-header__column .tiny-grid-thead-partition, + .tiny-grid-header__column .tiny-grid-resizable { + transform: translateX(calc(50% - 1px)); + } + + .tiny-grid-custom-footer { + @apply w-full; + @apply sticky; + @apply bottom-0; + } + + .tiny-grid-footer__column { + @apply sticky; + @apply bg-color-bg-1; + } + + .@{header-suffix} { + @apply relative; + min-height: 16px; + + .suffix-icon-1 { + @apply absolute; + @apply right-3; + } + + .suffix-icon-0 { + @apply absolute; + @apply right-0; + } + } + + .col__ellipsis { + &.is__editable.is__sortable.is__filter { + .@{header-suffix}.@{cell-tooltip} { + @apply pr-7; + } + } + + &.is__editable.is__sortable:not(.is__filter), + &.is__editable.is__filter:not(.is__sortable) { + .@{header-suffix}.@{cell-tooltip} { + @apply ~'pr-3.5'; + } + } + + &:not(.is__sortable):not(.is__filter) { + .@{header-suffix}.@{cell-tooltip} { + @apply pr-2; + } + } + + &.is__sortable.is__filter:not(.is__editable) { + .@{header-suffix}.@{cell-tooltip} { + padding-right: 26px; + } + } + + &.is__sortable:not(.is__filter):not(.is__editable), + &.is__filter:not(.is__sortable):not(.is__editable) { + .@{header-suffix}.@{cell-tooltip} { + @apply pr-3; + } + } + } + } } diff --git a/packages/theme/src/grid/body.less b/packages/theme/src/grid/body.less index df05526d6c..17dff090bd 100644 --- a/packages/theme/src/grid/body.less +++ b/packages/theme/src/grid/body.less @@ -21,6 +21,12 @@ .@{grid-prefix-cls}__fixed-right-body-wrapper { overflow-y: auto; overflow-x: auto; + + &.no-data { + overflow-y: hidden; + display: flex; + flex-direction: column; + } } // 鼠标配置项开启后,选中单元格的边框样式(position:absolute) diff --git a/packages/theme/src/grid/header.less b/packages/theme/src/grid/header.less index 5044bd2296..746cb2ab70 100644 --- a/packages/theme/src/grid/header.less +++ b/packages/theme/src/grid/header.less @@ -16,8 +16,6 @@ @grid-header-prefix-cls: ~'@{css-prefix}grid-header'; @grid-cell-prefix-cls: ~'@{css-prefix}grid-cell'; @grid-checkbox-prefix-cls: ~'@{css-prefix}grid-checkbox'; -@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; -@cell-tooltip: ~'@{grid-prefix-cls}-cell__tooltip'; .@{grid-prefix-cls}__header-wrapper { background-color: var(--tv-Grid-header-bg-color); diff --git a/packages/theme/src/grid/table.less b/packages/theme/src/grid/table.less index 46a9371c0e..80cc958d55 100644 --- a/packages/theme/src/grid/table.less +++ b/packages/theme/src/grid/table.less @@ -19,6 +19,7 @@ @input-prefix-cls: ~'@{css-prefix}input'; @select-prefix-cls: ~'@{css-prefix}select'; @pager-prefix-cls: ~'@{css-prefix}pager'; +@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; // table .@{grid-prefix-cls} { @@ -312,7 +313,11 @@ .@{grid-prefix-cls}-header__column, .@{grid-prefix-cls}-body__column, .@{grid-prefix-cls}-footer__column { - background-image: linear-gradient(-90deg, var(--tv-Grid-border-color-divider), var(--tv-Grid-border-color-divider)), + background-image: linear-gradient( + -90deg, + var(--tv-Grid-border-color-divider), + var(--tv-Grid-border-color-divider) + ), linear-gradient(-180deg, var(--tv-Grid-border-color-divider), var(--tv-Grid-border-color-divider)); background-repeat: no-repeat; background-size: @@ -708,38 +713,12 @@ // 暂无数据 & &__empty-block { - display: none; - opacity: 0; - height: 100%; - min-height: 60px; - padding: 60px 0; - justify-content: center; - align-items: center; - text-align: center; - - &.is__visible { - display: flex; - flex-flow: column wrap; - opacity: 1; - &.is__center { - opacity: 0; - } - } - } - - .empty-center-block { - z-index: 1; display: flex; + align-items: center; + position: sticky; + left: 0; + flex: auto; flex-direction: column; - justify-content: center; - text-align: center; - position: absolute; - width: 100%; - height: calc(100% - 60px); - - .@{grid-prefix-cls}__empty-text { - width: 100%; - } } // 表格无数据背景图 @@ -753,7 +732,8 @@ & &__empty-text { display: block; margin-top: 8px; - width: 50%; + text-align: center; + width: 100%; } // 校验不通过 @@ -862,13 +842,13 @@ & &__body-wrapper { &.body__wrapper.is__scrollload { overflow-y: hidden; - position: static; } } & .is__scrollload &-body__y-space { position: absolute; right: 0; + bottom: 0; width: 12px; overflow-y: scroll; @@ -1210,6 +1190,51 @@ } } } + + .@{grid-prefix-cls}__body { + .tiny-grid-header__column { + background-color: var(--tv-Grid-header-bg-color); + position: sticky; + } + + .tiny-grid-header__column:last-child { + contain: layout; + } + + .tiny-grid-header__column .tiny-grid-thead-partition, + .tiny-grid-header__column .tiny-grid-resizable { + transform: translateX(calc(50% - 1px)); + } + + .tiny-grid-custom-footer { + width: 100%; + position: sticky; + bottom: 0; + } + + .tiny-grid-footer__column { + position: sticky; + background-color: var(--tv-Grid-bg-color); + } + + .@{grid-prefix-cls}-cell-text { + font-weight: var(--tv-Grid-header-font-weight); + } + .@{header-suffix} { + position: relative; + min-height: 16px; + + .suffix-icon-1 { + position: absolute; + right: 12px; + } + + .suffix-icon-0 { + position: absolute; + right: 0; + } + } + } } // 表格全屏样式 diff --git a/packages/vue-locale/src/format.ts b/packages/vue-locale/src/format.ts index 0719fc5bf9..8d7f2b8edd 100644 --- a/packages/vue-locale/src/format.ts +++ b/packages/vue-locale/src/format.ts @@ -25,3 +25,13 @@ export default function (string, ...args) { } }) } + +export const memoize = (callback) => { + const cache = {} + + return (key, ...args) => { + cache[key] = cache[key] || callback(key, ...args) + + return cache[key] + } +} diff --git a/packages/vue-locale/src/vue2.7/index.ts b/packages/vue-locale/src/vue2.7/index.ts index d4113c3745..cb9a62a458 100644 --- a/packages/vue-locale/src/vue2.7/index.ts +++ b/packages/vue-locale/src/vue2.7/index.ts @@ -1,6 +1,6 @@ import zhCN from '../lang/zh-CN' import enUS from '../lang/en' -import format from '../format' +import format, { memoize } from '../format' import { extend as _extend } from '@opentiny/utils' let lang = zhCN @@ -57,7 +57,7 @@ export const initI18n = ({ app, createI18n, messages = {}, i18n = {} as any, mer messages: merge({ lang, i18n, messages }) }) - i18nHandler = (key, value) => vueI18n.global.t(key, value) + i18nHandler = memoize((key, value) => vueI18n.global.t(key, value)) return vueI18n } diff --git a/packages/vue-locale/src/vue2/index.ts b/packages/vue-locale/src/vue2/index.ts index d24c481b24..19ff7172f2 100644 --- a/packages/vue-locale/src/vue2/index.ts +++ b/packages/vue-locale/src/vue2/index.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import zhCN from '../lang/zh-CN' import enUS from '../lang/en' -import format from '../format' +import format, { memoize } from '../format' import { extend as _extend } from '@opentiny/utils' let lang = zhCN @@ -70,7 +70,7 @@ export const initI18n = ({ VueI18n, messages = {}, i18n = {} as any, merge }) => messages: merge({ lang, i18n, messages }) }) - i18nHandler = (key, value) => vueI18n.t(key, value) + i18nHandler = memoize((key, value) => vueI18n.t(key, value)) return vueI18n } diff --git a/packages/vue-locale/src/vue3/index.ts b/packages/vue-locale/src/vue3/index.ts index 76f7fbb14e..43e58971b8 100644 --- a/packages/vue-locale/src/vue3/index.ts +++ b/packages/vue-locale/src/vue3/index.ts @@ -1,6 +1,6 @@ import zhCN from '../lang/zh-CN' import enUS from '../lang/en' -import format from '../format' +import format, { memoize } from '../format' import { extend as _extend } from '@opentiny/utils' let lang = zhCN @@ -67,7 +67,7 @@ export const initI18n = ({ app, createI18n, messages = {}, i18n = {} as any, mer messages: merge({ lang, i18n, messages }) }) - i18nHandler = (key, value) => vueI18n.global.t(key, value) + i18nHandler = memoize((key, value) => vueI18n.global.t(key, value)) return vueI18n } diff --git a/packages/vue/src/grid/src/adapter/src/renderer.ts b/packages/vue/src/grid/src/adapter/src/renderer.ts index a06449e79a..b65322113f 100644 --- a/packages/vue/src/grid/src/adapter/src/renderer.ts +++ b/packages/vue/src/grid/src/adapter/src/renderer.ts @@ -22,8 +22,8 @@ * SOFTWARE. * */ -import { set, assign, objectMap, get, each, isObject, isFunction } from '@opentiny/vue-renderless/grid/static/' -import { getCellValue, setCellValue } from '@opentiny/vue-renderless/grid/utils' +import { assign, objectMap, get, each, isObject, isFunction } from '@opentiny/vue-renderless/grid/static/' +import { getCellValue, getRowid, setCellValue } from '@opentiny/vue-renderless/grid/utils' import { hooks } from '@opentiny/vue-common' /** @@ -109,21 +109,17 @@ function getEvents(renderOpts, params, context) { // - 其他组件直接使用event作为值 let cellValue = native ? event.target.value : event - // 根据配置决定如何更新数据: - // 1. 如果不需要始终验证且需要同步单元格,直接设置单元格值 - if (!renderOpts.isValidAlways && isSyncCell(renderOpts, params, context)) { - setCellValue(row, column, cellValue) - } else { - // 2. 否则更新数据和状态: - // - 非原生组件设置属性值 - // - 更新model状态 - // - 更新表格状态 - native || set(row, column.property, cellValue) + if (!isSyncCell(renderOpts, params, context)) { model.update = true model.value = cellValue - $table.updateStatus(params, cellValue, renderOpts) } + setCellValue(row, column, cellValue) + + Promise.resolve().then(() => { + $table.updateStatus(params, cellValue, renderOpts) + }) + // 对原生组件调用input和change回调 if (native) { input && input.apply(null, [params].concat.apply(params, arguments)) @@ -341,7 +337,10 @@ function defaultFilterMethod({ option, row, column }) { * @param context - 上下文 */ function renderSelectEdit(h, renderOpts, params, context) { + const { column, $table, row } = params + const editorKey = `editor-${getRowid($table, row)}-${column.id}` let props = { + ref: editorKey, class: 'tiny-grid-default-select', on: getEvents(renderOpts, params, context) } @@ -394,11 +393,14 @@ function defaultEditRender(h, renderOpts, params, context) { // 如果是字符串组件使用value,否则使用modelValue或自定义的prop let modelProps = typeof component === 'string' ? 'value' : editorModel.prop || 'modelValue' + const editorKey = `editor-${getRowid($table, row)}-${column.id}` + // 获取行的唯一标识作为key const key = row[$table.rowId] // 构建组件选项 let options = { + ref: editorKey, key, // 如果是原生标签则添加默认类名 class: isTag ? `tiny-grid-default-${component}` : '', diff --git a/packages/vue/src/grid/src/body/src/body.tsx b/packages/vue/src/grid/src/body/src/body.tsx index 7a406d072a..f7fddc0aa5 100644 --- a/packages/vue/src/grid/src/body/src/body.tsx +++ b/packages/vue/src/grid/src/body/src/body.tsx @@ -894,10 +894,6 @@ export default defineComponent({ beforeUnmount() { this.rowSortable && this.rowSortable.destroy() }, - updated() { - const { $parent: $table, fixedType } = this - !fixedType && $table.updateTableBodyHeight() - }, setup(props, { slots }) { hooks.onBeforeUnmount(() => { const table = hooks.getCurrentInstance().proxy diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue index 9fb75565be..f418bbfd7d 100644 --- a/packages/vue/src/grid/src/body/src/body.vue +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -14,7 +14,15 @@ - +
@@ -110,10 +118,14 @@ { 'col__ellipsis': hasEllipsis }, { 'edit__visible': editor && editor.type === 'visible' }, { 'fixed__column': fixedHiddenColumn }, - { 'col__dirty': isDirty }, + { 'col__dirty': getIsDirty(row, column) }, { 'col__actived': columnActived }, { 'col__valid-error': validError && validated }, - { 'col__valid-success': columnActived ? !validError && !validated : isDirty && !validated }, + { + 'col__valid-success': columnActived + ? !validError && !validated + : getIsDirty(row, column) && !validated + }, { 'col__treenode': column.treeNode }, { 'fixed-left-last__column': column.fixed === 'left' && leftList[leftList.length - 1] === column }, { 'fixed-right-first__column': column.fixed === 'right' && rightList[0] === column } @@ -216,6 +228,7 @@
import type { PropType } from 'vue' -import { defineComponent, ref, onBeforeUnmount, getCurrentInstance } from 'vue' -import { updateCellTitle, emitEvent } from '@opentiny/vue-renderless/grid/utils' +import { $prefix, defineComponent, hooks } from '@opentiny/vue-common' +import { isObject, isNull, isFunction, removeClass, addClass } from '@opentiny/utils' +import { isBoolean, find } from '@opentiny/vue-renderless/grid/static' +import { + updateCellTitle, + emitEvent, + getClass, + getFuncText, + getRowid, + formatText, + getOffsetPos +} from '@opentiny/vue-renderless/grid/utils' +import { getCellLabel } from '../../tools' import GlobalConfig from '../../config' -import { handleRowGroupFold } from '../../table/src/strategy' +import { handleRowGroupFold, getTreeChildrenKey, getTreeShowKey, isVirtualRow } from '../../table/src/strategy' import { generateFixedClassName } from '../../table/src/utils/handleFixedColumn' import type { TableColumn, TableRow, TableConfig, EventParams, GridBodyInstance } from './types' // 定义工具函数类型 type FormatTextFn = (text: string) => string -type GetFuncTextFn = (text: string) => string -type GetCellLabelFn = (row: TableRow, column: TableColumn, params: any) => string -type FindFn = (array: T[], predicate: (item: T) => boolean) => T | undefined // 定义组件参数类型 interface ComponentParams { @@ -323,7 +344,6 @@ interface TableInstance { row?: boolean } rowDrop?: (el: HTMLElement) => any - updateTableBodyHeight?: () => void triggerScrollXEvent?: (event: Event) => void triggerScrollYEvent?: (event: Event) => void triggerTooltipEvent?: (event: MouseEvent, params: any) => void @@ -348,7 +368,7 @@ const isOperateMouse = ($table: TableConfig) => let renderRowFlag = false export default defineComponent({ - name: 'TinyGridBody', + name: $prefix + 'GridBody', props: { collectColumn: { type: Array as PropType, @@ -458,10 +478,6 @@ export default defineComponent({ type: Boolean, default: false }, - isDirty: { - type: Boolean, - default: false - }, columnActived: { type: Boolean, default: false @@ -534,18 +550,6 @@ export default defineComponent({ type: Function as PropType, default: (text: string) => text }, - getFuncText: { - type: Function as PropType, - default: (text: string) => text - }, - getCellLabel: { - type: Function as PropType, - default: (row: TableRow, column: TableColumn, params: any) => '' - }, - find: { - type: Function as PropType, - default: (array: T[], predicate: (item: T) => boolean) => array.find(predicate) - }, isOrdered: { type: Boolean, default: false @@ -580,10 +584,10 @@ export default defineComponent({ } }, setup(props, { slots }) { - const rowSortable = ref(null) + const rowSortable = hooks.ref(null) - onBeforeUnmount(() => { - const table = getCurrentInstance()?.proxy + hooks.onBeforeUnmount(() => { + const table = hooks.getCurrentInstance()?.proxy if (table) { table.$el._onscroll = null table.$el.onscroll = null @@ -631,12 +635,6 @@ export default defineComponent({ beforeUnmount() { this.rowSortable?.destroy() }, - updated() { - const { $parent: $table, fixedType } = this as GridBodyInstance - if ($table && !fixedType) { - $table.updateTableBodyHeight() - } - }, methods: { // 滚动处理 scrollEvent(event: Event) { @@ -881,6 +879,12 @@ export default defineComponent({ $table._isResize || ($table.lastScrollTime && Date.now() < $table.lastScrollTime + $table.optimizeOpts.delayHover) ) + }, + + // ----------- columns相关方法 ----------- + getIsDirty(row, column) { + const $table = this.$parent + return $table.getCellStatus(row, column)?.isDirty } } }) diff --git a/packages/vue/src/grid/src/body/src/useHeader.ts b/packages/vue/src/grid/src/body/src/useHeader.ts new file mode 100644 index 0000000000..ea474db3b6 --- /dev/null +++ b/packages/vue/src/grid/src/body/src/useHeader.ts @@ -0,0 +1,114 @@ +import { hooks } from '@opentiny/vue-common' + +const calcHeader = (collectColumn) => { + let maxLevel = 0 + const leafColumns = [] + const parentMap = new WeakMap() + const levelMap = new WeakMap() + + const traverseTree = (tree, level, parent) => { + if (Array.isArray(tree) && tree.length > 0) { + if (level > maxLevel) { + maxLevel = level + } + + tree.forEach((item) => { + if (parent) { + parentMap.set(item, parent) + } + + levelMap.set(item, level) + + traverseTree(item.children, level + 1, item) + }) + } else { + leafColumns.push(parent) + } + } + + traverseTree(collectColumn, 0, null) + + const headerTable = [] + const rowspanMap = new WeakMap() + + for (let i = 0; i <= maxLevel; i++) { + headerTable[i] = new Array(leafColumns.length).fill(0) + } + + leafColumns.forEach((column, index) => { + const level = levelMap.get(column) + + rowspanMap.set(column, maxLevel - level + 1) + headerTable[level][index] = column + + for (let l = level - 1; l >= 0; l--) { + column = headerTable[l][index] = parentMap.get(column) + } + }) + + return { leafColumns, headerTable, rowspanMap } +} + +const calcSpan = (tableColumn, header, rowHeight) => { + const indices = tableColumn.map((c) => header.leafColumns.indexOf(c)) + const subTable = [] + + header.headerTable.forEach((cols, i) => { + const countMap = new WeakMap() + + subTable[i] = indices + .map((j) => cols[j]) + .reduce((p, col) => { + if (col) { + if (!p.includes(col)) { + p.push(col) + } + + if (countMap.has(col)) { + countMap.set(col, countMap.get(col) + 1) + } else { + countMap.set(col, 1) + } + } + return p + }, []) + .map((column) => { + const rowspan = header.rowspanMap.get(column) || 1 + return { + id: column.id, + column, + colspan: countMap.get(column), + rowspan, + height: rowspan * rowHeight, + top: i * rowHeight + } + }) + }) + + return subTable +} + +export const useHeader = (props, vm, rowHeight) => { + const headerTable = hooks.ref([]) + const { showHeader } = vm.$parent + + let header + + if (showHeader) { + hooks.watch( + () => props.collectColumn, + () => { + header = calcHeader(props.collectColumn) + } + ) + + hooks.watch( + () => props.tableColumn, + () => { + headerTable.value = calcSpan(props.tableColumn, header, rowHeight.value) + } + ) + } + + return { headerTable } +} diff --git a/packages/vue/src/grid/src/body/src/usePool.ts b/packages/vue/src/grid/src/body/src/usePool.ts new file mode 100644 index 0000000000..e14b9b1fb8 --- /dev/null +++ b/packages/vue/src/grid/src/body/src/usePool.ts @@ -0,0 +1,82 @@ +import { hooks } from '@opentiny/vue-common' + +const difference = (arr, other) => arr.filter((i) => other.findIndex((j) => i.id === j.id) === -1) + +let uid = 0 + +const createPool = (array) => { + const context = { + pool: [], + idViewMap: new Map(), + unusedViews: [], + array + } + + array.forEach((item) => { + const view = { id: ++uid, used: true, item } + context.pool.push(view) + context.idViewMap.set(item.id, view) + }) + + return context +} + +const updatePool = (array, context) => { + const expires = difference(context.array, array) + const indices = new WeakMap() + + expires.forEach((item) => { + const view = context.idViewMap.get(item.id) + + view.used = false + + context.idViewMap.delete(item.id) + context.unusedViews.push(view) + }) + + array.forEach((item, i) => { + indices.set(item, i) + + let view = context.idViewMap.get(item.id) + + if (!view) { + if (context.unusedViews.length > 0) { + view = context.unusedViews.shift() + } else { + view = { id: ++uid, used: true, item } + context.pool.push(view) + } + + context.idViewMap.set(item.id, view) + } + + view.used = true + view.item = item + }) + + context.array = array + context.pool.sort((a, b) => (a.used ? (b.used ? indices.get(a.item) - indices.get(b.item) : -1) : b.used ? 1 : 0)) + + return context +} + +export const usePool = (props) => { + const columnPool = hooks.ref([]) + + let columnContext + + hooks.watch( + () => props.tableColumn, + () => { + if (columnContext) { + updatePool(props.tableColumn, columnContext) + } else { + columnContext = createPool(props.tableColumn) + } + + columnPool.value = columnContext.pool + } + ) + + return { columnPool } +} diff --git a/packages/vue/src/grid/src/checkbox/src/methods.ts b/packages/vue/src/grid/src/checkbox/src/methods.ts index a47cdda26a..9828aa6242 100644 --- a/packages/vue/src/grid/src/checkbox/src/methods.ts +++ b/packages/vue/src/grid/src/checkbox/src/methods.ts @@ -210,14 +210,16 @@ export default { let selected = this.getSelectRecords() let position = typeof selectToolbar === 'object' ? selectToolbar.position : '' if (selectColumn && selected && selected.length) { - let selectTh = this.$el.querySelector('th.tiny-grid-header__column.col__selection') - let headerWrapper = this.$el.querySelector('.tiny-grid>.tiny-grid__header-wrapper') + const { tinyTheme, vSize, $el } = this + // TODO: 适配不同主题行高 + const rowHeight = 36 + let selectTh = $el.querySelector('th.tiny-grid-header__column.col__selection') let tr = selectTh.parentNode let thArr = toArray(tr.childNodes) let range = document.createRange() let rangeBoundingRect - let headerBoundingRect = headerWrapper.getBoundingClientRect() - let layout = { width: 0, height: 0, left: 0, top: 0, zIndex: 1 } + let headerBoundingRect = { width: $el.getBoundingClientRect().width, height: rowHeight } + let layout = { width: 0, height: 0, left: 0, top: 0, zIndex: 20 } let adjust = 1 if (selectColumn.fixed === 'right') { range.setStart(tr, thArr.indexOf(selectTh)) diff --git a/packages/vue/src/grid/src/composable/index.ts b/packages/vue/src/grid/src/composable/index.ts index bd25c3d097..194676a005 100644 --- a/packages/vue/src/grid/src/composable/index.ts +++ b/packages/vue/src/grid/src/composable/index.ts @@ -1,2 +1,3 @@ export * from './useDrag' export * from './useRowGroup' +export * from './useCellStatus' diff --git a/packages/vue/src/grid/src/composable/useCellStatus.ts b/packages/vue/src/grid/src/composable/useCellStatus.ts new file mode 100644 index 0000000000..adafe05cf1 --- /dev/null +++ b/packages/vue/src/grid/src/composable/useCellStatus.ts @@ -0,0 +1,50 @@ +import { getRowid } from '@opentiny/vue-renderless/grid/utils' + +const isCellDirty = ($table, row, column) => { + const { editConfig } = $table + const { showStatus = false, relationFields = true } = editConfig || {} + // 关联字段配置为true,或者配置包含当前字段时,支持脏数据检查 + const canChange = + relationFields === true || (Array.isArray(relationFields) && relationFields.includes(column.property)) + + let isDirty + + // 冻结表格方案:主表的固定隐藏列不进行脏数据检查。改为粘性布局后:主表的所有列都应去掉此限制。 + if (editConfig && showStatus && column.property && (column.editor || (relationFields && canChange))) { + isDirty = $table.hasRowChange(row, column.property) + } + + return isDirty +} + +const getCellKey = ($table, row, column) => { + const rowid = getRowid($table, row) + return `${rowid}-${column.id}` +} + +const updateCellStatus = ($table, row, column) => { + const cellKey = getCellKey($table, row, column) + const isDirty = isCellDirty($table, row, column) + const map = $table.cellStatus + + if (map.has(cellKey)) { + map.get(cellKey).isDirty = isDirty + } else { + map.set(cellKey, { isDirty }) + } +} + +export const updateRowStatus = ($table, row) => { + $table.tableFullColumn.forEach((column) => updateCellStatus($table, row, column)) +} + +export const getCellStatus = ($table, row, column) => { + const cellKey = getCellKey($table, row, column) + const map = $table.cellStatus + + if (map.has(cellKey)) { + return map.get(cellKey) + } else { + return { isDirty: false } + } +} diff --git a/packages/vue/src/grid/src/composable/useDrag/index.ts b/packages/vue/src/grid/src/composable/useDrag/index.ts index 25b1c8022f..a1e92c9523 100644 --- a/packages/vue/src/grid/src/composable/useDrag/index.ts +++ b/packages/vue/src/grid/src/composable/useDrag/index.ts @@ -9,6 +9,8 @@ const headerTh = 'th.tiny-grid-header__column:not(.col__gutter):not(.fixed__hidd const groupKey = 'dndGroup' const idKey = 'colid' const pidKey = 'pColid' +let timer = null +const time = 2000 let dndGroup = 0 @@ -74,6 +76,8 @@ const getColidMap = (treeArray) => { } const createDragHander = (state, $table) => { + const dropConfig = $table.dropConfig || {} + const dropable = dropConfig.column && dropConfig.schema === 'v2' // 开始拖拽处理 const dragStart = (dragTarget) => { const dragColid = dragTarget.dataset.colid @@ -84,6 +88,7 @@ const createDragHander = (state, $table) => { const dragIndex = dragParentChildren.indexOf(dragColumn) $table.$emit('column-drag-start', { dragParentChildren, dragColumn, dragIndex }) + clearTimeout(timer) } // 放置结束处理 @@ -123,6 +128,12 @@ const createDragHander = (state, $table) => { scrollYLoad && $table.triggerScrollYEvent({ target: { scrollTop: lastScrollTop } }) } }) + + if ($table.getVm('toolbar') && dropable) { + timer = setTimeout(() => { + $table.getVm('toolbar').$refs.custom.saveSetting('drag') + }, time) + } } }) } diff --git a/packages/vue/src/grid/src/dragger/src/methods.ts b/packages/vue/src/grid/src/dragger/src/methods.ts index 1b55e6caed..4d15f9faa6 100644 --- a/packages/vue/src/grid/src/dragger/src/methods.ts +++ b/packages/vue/src/grid/src/dragger/src/methods.ts @@ -3,7 +3,7 @@ export default { // 处理列拖拽 columnDrop(headerEl) { const { plugin, onBeforeMove, filter } = this.dropConfig || {} - const columnDropContainer = headerEl.querySelector('.tiny-grid__header .tiny-grid-header__row') + const columnDropContainer = headerEl.querySelector('.tiny-grid-header__row') const columnDropOptions = { handle: '.tiny-grid-header__column:not(.col__fixed)', diff --git a/packages/vue/src/grid/src/edit/src/methods.ts b/packages/vue/src/grid/src/edit/src/methods.ts index b89d59fd63..a2aac845ca 100644 --- a/packages/vue/src/grid/src/edit/src/methods.ts +++ b/packages/vue/src/grid/src/edit/src/methods.ts @@ -300,6 +300,8 @@ export default { destructuring(row, oRow) } } + + this.updateRowStatus(row) } if (arguments.length) { @@ -420,6 +422,7 @@ export default { } if (isActived) { + this.updateRowStatus(row) this.updateFooter() // 处理数字输入框返回string类型数据,导致还原初始数字还是编辑状态的问题 @@ -589,7 +592,7 @@ export default { return this.$nextTick() } // 如果配置了批量选中功能,则为批量选中状态 - let headerElem = elemStore['main-header-list'] + let headerElem = elemStore['main-body-headerList'] this.handleChecked([[cell]]) @@ -597,8 +600,8 @@ export default { return this.$nextTick() } - this.handleHeaderChecked([[headerElem.querySelector(`.${column && column.id}`)]]) - this.handleIndexChecked([[cell && cell.parentNode && cell.parentNode.querySelector('.col__index')]]) + this.handleHeaderChecked([[headerElem?.querySelector(`.${column?.id}`)]]) + this.handleIndexChecked([[cell?.parentNode?.querySelector('.col__index')]]) return this.$nextTick() } diff --git a/packages/vue/src/grid/src/footer/index.ts b/packages/vue/src/grid/src/footer/index.ts deleted file mode 100644 index 4f71716b40..0000000000 --- a/packages/vue/src/grid/src/footer/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import Footer from './src/footer' - -Footer.install = function (Vue) { - Vue.component(Footer.name, Footer) -} - -export default Footer diff --git a/packages/vue/src/grid/src/footer/src/footer.ts b/packages/vue/src/grid/src/footer/src/footer.ts deleted file mode 100644 index 6d462f6555..0000000000 --- a/packages/vue/src/grid/src/footer/src/footer.ts +++ /dev/null @@ -1,359 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { isFunction } from '@opentiny/vue-renderless/grid/static/' -import { getClass, emitEvent, formatText, updateCellTitle } from '@opentiny/vue-renderless/grid/utils' -import { isNull } from '@opentiny/utils' -import { h, $prefix, defineComponent } from '@opentiny/vue-common' - -const classMap = { - fixedHidden: 'fixed__column', - colEllipsis: 'col__ellipsis', - filterActive: 'filter__active', - cellSummary: 'cell__summary', - fixedLeftLast: 'fixed-left-last__column', - fixedRightFirst: 'fixed-right-first__column', - colRadio: 'col__radio', - colSelection: 'col__selection' -} - -function doFooterSpan({ attrs, footerData, footerSpanMethod, params }) { - if (footerSpanMethod) { - let { rowspan = 1, colspan = 1 } = footerSpanMethod({ data: footerData, ...params }) || {} - - if (!rowspan || !colspan) { - return null - } - - attrs.rowspan = rowspan - attrs.colspan = colspan - } -} - -function addListenerDblclick({ $table, params, tableListeners, tfOns }) { - if (tableListeners['footer-cell-dblclick']) { - tfOns.dblclick = (event) => { - emitEvent($table, 'footer-cell-dblclick', [{ cell: event.currentTarget, ...params }, event]) - } - } -} - -function addListenerClick({ $table, params, tableListeners, tfOns }) { - if (tableListeners['footer-cell-click']) { - tfOns.click = (event) => { - emitEvent($table, 'footer-cell-click', [{ cell: event.currentTarget, ...params }, event]) - } - } -} - -function addListenerMouseout({ $table, showTooltip, tfOns }) { - if (showTooltip) { - tfOns.mouseout = () => { - $table.clostTooltip() - } - } -} - -function addListenerMouseover({ $table, params, showTitle, showTooltip, tfOns }) { - if (showTitle || showTooltip) { - tfOns.mouseover = (event) => { - if (showTitle) { - updateCellTitle(event) - } else if (showTooltip) { - $table.triggerFooterTooltipEvent(event, params) - } - } - } -} - -function renderColgroup(tableColumn) { - return h( - 'colgroup', - { ref: 'colgroup' }, - tableColumn - .map((column, columnIndex) => h('col', { attrs: { name: column.id }, key: columnIndex })) - .concat([h('col', { attrs: { name: 'col_gutter' } })]) - ) -} - -const renderfoots = (opt) => { - const { $table, allAlign, allColumnOverflow, allFooterAlign, buildParamFunc, columnKey, columnStore } = opt - const { - footerCellClassName, - footerData, - footerRowClassName, - footerSpanMethod, - overflowX, - tableColumn, - tableListeners - } = opt - const { scrollbarWidth } = $table - return (list, $rowIndex) => - h( - 'tr', - { - class: [ - 'tiny-grid-footer__row', - footerRowClassName - ? isFunction(footerRowClassName) - ? footerRowClassName({ $table, $rowIndex }) - : footerRowClassName - : '' - ] - }, - tableColumn - .map((column, $columnIndex) => { - const arg1 = { $columnIndex, $rowIndex, $table, allAlign, allColumnOverflow, allFooterAlign } - const arg2 = { column, footerData, footerSpanMethod, overflowX, tableListeners } - const { - attrs, - columnIndex, - fixedHiddenColumn, - footAlign, - footerClassName, - hasEllipsis, - params, - tfOns, - isShowEllipsis, - isShowTitle, - showTooltip - } = buildParamFunc(Object.assign(arg1, arg2)) - const { leftList, rightList } = columnStore - const { left: leftPosition, right } = column.style || {} - // 表尾右侧冻结列,当有表体有滚动条时,需要加上滚动条的偏移量 - const rightPosition = right >= 0 ? right + scrollbarWidth : '' - return h( - 'td', - { - class: [ - 'tiny-grid-footer__column', - column.id, - { - [`col__${footAlign}`]: footAlign, - [classMap.fixedHidden]: fixedHiddenColumn, - [classMap.colEllipsis]: hasEllipsis, - [classMap.filterActive]: column.filter && column.filter.hasFilter, - [classMap.fixedLeftLast]: column.fixed === 'left' && leftList[leftList.length - 1] === column, - [classMap.fixedRightFirst]: column.fixed === 'right' && rightList[0] === column, - [classMap.colRadio]: column.type === 'radio', - [classMap.colSelection]: column.type === 'selection' - }, - getClass(footerClassName, params), - getClass(footerCellClassName, params) - ], - style: fixedHiddenColumn - ? { - left: `${leftPosition}px`, - right: `${rightPosition}px` - } - : null, - attrs, - on: tfOns, - key: columnKey ? column.id : columnIndex - }, - [ - h( - 'div', - { - class: [ - 'tiny-grid-cell', - { - [classMap.cellSummary]: $table.summaryConfig, - 'tiny-grid-cell__title': isShowTitle, - 'tiny-grid-cell__tooltip': showTooltip || column.showTip, - 'tiny-grid-cell__ellipsis': isShowEllipsis - } - ] - }, - // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容 - $table.isShapeTable ? formatText(list[$table.tableColumn.indexOf(column)], 1) : null - ) - ] - ) - }) - .concat([h('td', { class: 'col__gutter' })]) - ) -} - -function renderTfoot(opt) { - return h('tfoot', { ref: 'tfoot' }, opt.footerData.map(renderfoots(opt))) -} - -export default defineComponent({ - name: `${$prefix}GridFooter`, - props: { - fixedColumn: Array, - fixedType: String, - footerData: Array, - size: String, - tableColumn: Array, - visibleColumn: Array - }, - mounted() { - let { $el, $parent: $table, $refs } = this - let { elemStore } = $table - let keyPrefix = 'main-footer-' - - elemStore[`${keyPrefix}wrapper`] = $el - elemStore[`${keyPrefix}table`] = $refs.table - elemStore[`${keyPrefix}colgroup`] = $refs.colgroup - elemStore[`${keyPrefix}list`] = $refs.tfoot - elemStore[`${keyPrefix}x-space`] = $refs.xSpace - }, - render() { - let { $parent: $table, buildParamFunc, fixedColumn, fixedType, footerData, tableColumn } = this - let { - align: allAlign, - columnKey, - footerAlign: allFooterAlign, - footerCellClassName, - footerRowClassName, - footerSpanMethod, - columnStore - } = $table - let { overflowX, showOverflow: allColumnOverflow, tableLayout, tableListeners, renderFooter } = $table - - let tableAttrs = { cellspacing: 0, cellpadding: 0, border: 0 } - let colgroupVNode = renderColgroup(tableColumn) - let arg1 = { $table, allAlign, allColumnOverflow, allFooterAlign, buildParamFunc, columnKey, columnStore } - let arg2 = { - footerCellClassName, - footerData, - footerRowClassName, - footerSpanMethod, - overflowX, - tableColumn, - tableListeners - } - let tfootVNode = renderTfoot(Object.assign(arg1, arg2)) - - const renderParams = { $table, columns: tableColumn, footerData, fixedColumns: fixedColumn, fixedType } - - return h( - 'div', - { - class: ['tiny-grid__footer-wrapper', 'body__wrapper'], - on: { scroll: this.scrollEvent } - }, - [ - h('div', { class: 'tiny-grid-body__x-space', ref: 'xSpace' }), - typeof renderFooter === 'function' - ? renderFooter(renderParams, h) - : h( - 'table', - { - class: 'tiny-grid__footer', - style: { tableLayout }, - attrs: tableAttrs, - ref: 'table' - }, - [ - // 列宽 - colgroupVNode, - // 底部 - tfootVNode - ] - ) - ] - ) - }, - methods: { - scrollEvent(event) { - // 滚动处理: 如果存在列固定左侧,同步更新滚动状态;如果存在列固定右侧,同步更新滚动状态。 - let { $parent: $table } = this - let { $refs, lastScrollLeft, scrollXLoad } = $table - let { tableBody, tableFooter, tableHeader } = $refs - let headerElem = tableHeader ? tableHeader.$el : null - let bodyElem = tableBody ? tableBody.$el : null - let footerElem = tableFooter ? tableFooter.$el : null - let scrollLeft = footerElem.scrollLeft - let isX = scrollLeft !== lastScrollLeft - let setElemScrollLeft = (elem, scrollLeft) => { - if (elem) { - elem.scrollLeft = scrollLeft - } - } - let eventParams = [{ $table, isX, isY: false, scrollLeft, scrollTop: bodyElem.scrollTop, type: 'footer' }, event] - - $table.lastScrollTime = Date.now() - $table.lastScrollLeft = scrollLeft - - setElemScrollLeft(headerElem, scrollLeft) - setElemScrollLeft(bodyElem, scrollLeft) - - if (scrollXLoad && isX) { - $table.triggerScrollXEvent(event) - } - - emitEvent($table, 'scroll', eventParams) - }, - buildParamFunc(opt) { - let { $columnIndex, $rowIndex, $table, allAlign, allColumnOverflow, allFooterAlign } = opt - let { column, footerData, footerSpanMethod, tableListeners } = opt - let { showOverflow, footerAlign, align, footerClassName } = column - let fixedHiddenColumn = column.fixed - let cellOverflowValue = isNull(showOverflow) ? allColumnOverflow : showOverflow - let footAlign = footerAlign || align || allFooterAlign || allAlign - let isShowEllipsis = cellOverflowValue === 'ellipsis' - let isShowTitle = cellOverflowValue === 'title' - let showTooltip = cellOverflowValue === true || cellOverflowValue === 'tooltip' - let hasEllipsis = isShowTitle || showTooltip || isShowEllipsis - let attrs = { 'data-colid': column.id } - let tfOns = {} - let columnIndex = $table.getColumnIndex(column) - let params = { - $table, - $rowIndex, - column, - columnIndex, - $columnIndex - } - - addListenerMouseover({ $table, params, showTitle: isShowTitle, showTooltip, tfOns }) - - addListenerMouseout({ $table, showTooltip, tfOns }) - - addListenerClick({ $table, params, tableListeners, tfOns }) - - addListenerDblclick({ $table, params, tableListeners, tfOns }) - // 处理行或者列的合并 - doFooterSpan({ attrs, footerData, footerSpanMethod, params }) - - return { - attrs, - columnIndex, - fixedHiddenColumn, - footAlign, - footerClassName, - hasEllipsis, - isShowEllipsis, - isShowTitle, - showTooltip, - params, - tfOns - } - } - } -}) diff --git a/packages/vue/src/grid/src/header/index.ts b/packages/vue/src/grid/src/header/index.ts deleted file mode 100644 index b58f841032..0000000000 --- a/packages/vue/src/grid/src/header/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import Header from './src/header' - -Header.install = function (Vue) { - Vue.component(Header.name, Header) -} - -export default Header diff --git a/packages/vue/src/grid/src/header/src/header.ts b/packages/vue/src/grid/src/header/src/header.ts deleted file mode 100644 index 5f4b8679f8..0000000000 --- a/packages/vue/src/grid/src/header/src/header.ts +++ /dev/null @@ -1,579 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { isObject, isNull } from '@opentiny/utils' -import { removeClass, addClass } from '@opentiny/utils' -import { isBoolean, isFunction } from '@opentiny/vue-renderless/grid/static/' -import { updateCellTitle, emitEvent, getClass } from '@opentiny/vue-renderless/grid/utils' -import { h, $prefix, defineComponent } from '@opentiny/vue-common' -import { random } from '@opentiny/utils' - -function addListenerMousedown({ $table, mouseConfig, params, thOns }) { - if (mouseConfig.checked) { - thOns.mousedown = (event) => - $table.triggerHeaderCellMousedownEvent(event, { - cell: event.currentTarget, - ...params - }) - } -} - -function addListenerDblclick({ $table, params, tableListeners, thOns }) { - if (tableListeners['header-cell-dblclick']) { - thOns.dblclick = (event) => - emitEvent($table, 'header-cell-dblclick', [{ cell: event.currentTarget, ...params }, event]) - } -} - -function addListenerClick({ $table, highlightCurrentColumn, mouseConfig, params, sortOpts, tableListeners, thOns }) { - if ( - highlightCurrentColumn || - tableListeners['header-cell-click'] || - mouseConfig.checked || - sortOpts.trigger === 'cell' - ) { - thOns.click = (event) => - $table.triggerHeaderCellClickEvent(event, { - cell: event.currentTarget, - ...params - }) - } -} - -function addListenerMouseout({ $table, showHeaderTip, showTooltip, thOns }) { - if (showTooltip || showHeaderTip) { - thOns.mouseout = () => { - if ($table._isResize) { - return - } - - $table.clostTooltip() - } - } -} - -function addListenerMouseover({ $table, params, showHeaderTip, showTitle, showTooltip, thOns }) { - if (showTitle || showTooltip || showHeaderTip) { - thOns.mouseover = (event) => { - if ($table._isResize) { - return - } - - if (showTitle) { - updateCellTitle(event) - } else if (showTooltip || showHeaderTip) { - $table.triggerHeaderTooltipEvent(event, { showHeaderTip, ...params }) - } - } - } -} - -function modifyHeadAlign({ column, headAlign }) { - if (~['radio', 'selection', 'index'].indexOf(column.type)) { - headAlign = headAlign || 'center' - } - - return headAlign -} - -function computeDragLeft(args) { - let { dragMinLeft, resizableConfig, scrollLeft, column, startColumnLeft, left } = args - - let dragLeft = Math.max(left, dragMinLeft) - - if (resizableConfig?.limit instanceof Function) { - let currentMouseLeft = dragLeft - scrollLeft - let width = resizableConfig.limit({ field: column.own.field, width: currentMouseLeft - startColumnLeft }) - dragLeft = startColumnLeft + width - } - - return { left, dragMinLeft, dragLeft } -} - -function renderTableColgroup(tableColumn) { - return h( - 'colgroup', - { - ref: 'colgroup' - }, - tableColumn - .map((column, columnIndex) => h('col', { attrs: { name: column.id }, key: columnIndex })) - .concat([h('col', { attrs: { name: 'col_gutter' } })]) - ) -} - -function renderRepair() { - return h('div', { class: 'tiny-grid__repair', ref: 'repair' }) -} - -function renderXSpace() { - return h('div', { class: 'tiny-grid-body__x-space', ref: 'xSpace' }) -} - -const classMap = { - colFixed: 'col__fixed', - colIndex: 'col__index', - colRadio: 'col__radio', - colSelection: 'col__selection', - colGroup: 'col__group', - colEllipsis: 'col__ellipsis', - fixedHidden: 'fixed__column', - isSortable: 'is__sortable', - isEditable: 'is__editable', - isFilter: 'is__filter', - filterActive: 'filter__active' -} - -function getThPropsArg(args) { - let { column, columnIndex, columnKey, fixedHiddenColumn, hasEllipsis, headAlign, columnStore } = args - let { headerCellClassName, headerClassName, isColGroup, isDragHeaderSorting, params, thOns, scrollbarWidth } = args - const { leftList, rightList } = columnStore - - return { - class: [ - 'tiny-grid-header__column', - column.id, - { - [`col__${headAlign}`]: headAlign, - [classMap.colFixed]: column.fixed, - [classMap.colIndex]: column.type === 'index', - [classMap.colRadio]: column.type === 'radio', - [classMap.colSelection]: column.type === 'selection', - [classMap.colGroup]: isColGroup, - [classMap.colEllipsis]: hasEllipsis, - [classMap.fixedHidden]: fixedHiddenColumn, - [classMap.isSortable]: !['index', 'radio', 'selection'].includes(column.type) && column.sortable, - [classMap.isEditable]: column.editor, - [classMap.isFilter]: isObject(column.filter), - [classMap.filterActive]: column.filter && column.filter.hasFilter, - 'fixed-left-last__column': - column.fixed === 'left' && (leftList[leftList.length - 1] === column || column.isFixedLeftLast), - 'fixed-right-first__column': column.fixed === 'right' && (rightList[0] === column || column.isFixedRightFirst) - }, - getClass(headerClassName, params), - getClass(headerCellClassName, params) - ], - attrs: { - 'data-colid': column.id, - colspan: column.colSpan, - rowspan: column.rowSpan - }, - style: fixedHiddenColumn - ? { - left: `${column.style?.left}px`, - right: `${column.style?.right + scrollbarWidth}px` - } - : null, - on: thOns, - key: isDragHeaderSorting ? random() : columnKey || isColGroup ? column.id : columnIndex - } -} - -function renderThPartition({ border, column, isColGroup, resizable }) { - let res = null - - const classMap = { - isLine: 'is__line' - } - - if (!isColGroup && !(isBoolean(column.resizable) ? column.resizable : resizable) && column.type !== 'index') { - res = h('div', { - class: ['tiny-grid-thead-partition', { [classMap.isLine]: !border }] - }) - } - - return res -} - -function renderThCell(args) { - let { column, fixedHiddenColumn, headerSuffixIconAbsolute, params, $table } = args - let { showEllipsis, showHeaderTip, showTitle, showTooltip } = args - - return h( - 'div', - { - class: [ - 'tiny-grid-cell', - { - 'tiny-grid-cell__title': showTitle, - 'tiny-grid-cell__tooltip': showTooltip || showHeaderTip, - 'tiny-grid-cell__ellipsis': showEllipsis, - 'tiny-grid-cell__header-suffix': headerSuffixIconAbsolute - } - ] - }, - // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容 - $table.isShapeTable ? column.renderHeader(h, { isHidden: fixedHiddenColumn, ...params }) : null - ) -} -function renderThResize({ _vm, border, column, fixedHiddenColumn, isColGroup, params, resizable, isColResize }) { - let res = null - - const classMap = { - isLine: 'is__line' - } - - // 删除fixedHiddenColumn,冻结表头放开可以拖拽调节宽度。 - if (!isColGroup && isColResize && (isBoolean(column.resizable) ? column.resizable : resizable)) { - res = h('div', { - class: ['tiny-grid-resizable', { [classMap.isLine]: !border }], - on: { - mousedown: (event) => _vm.resizeMousedown(event, { isHidden: fixedHiddenColumn, ...params }) - } - }) - } - - return res -} - -function getThHandler(args) { - let { - $rowIndex, - $table, - _vm, - allAlign, - allColumnHeaderOverflow, - allHeaderAlign, - border, - columnKey, - headerCellClassName - } = args - let { - headerSuffixIconAbsolute, - highlightCurrentColumn, - isDragHeaderSorting, - mouseConfig, - resizable, - sortOpts, - tableListeners - } = args - - let { operationColumnResizable } = $table - - return (column, $columnIndex) => { - let { showHeaderOverflow, showHeaderTip, headerAlign, align, headerClassName } = column - let isColGroup = column.children && column.children.length - let fixedHiddenColumn = column.fixed - let headOverflow = isNull(showHeaderOverflow) ? allColumnHeaderOverflow : showHeaderOverflow - let showEllipsis = headOverflow === 'ellipsis' - let showTitle = headOverflow === 'title' - let headAlign = headerAlign || align || allHeaderAlign || allAlign - let showTooltip = headOverflow === true || headOverflow === 'tooltip' - let thOns = {} - let hasEllipsis = showTitle || showTooltip || showEllipsis - const { columnStore, scrollbarWidth } = $table - - // type为index或radio或selection的列使用operationColumnResizable控制是否可拖动列宽,其它列默认是true - let isColResize = ['index', 'radio', 'selection'].includes(column.type) ? operationColumnResizable : true - - // 索引列、选择列如果不配置对齐方式则默认为居中对齐 - headAlign = modifyHeadAlign({ column, headAlign }) - // 确保表格索引的准确性 - let columnIndex = $table.getColumnIndex(column) - let params = { $table, $rowIndex, column } - Object.assign(params, { columnIndex, $columnIndex }) - addListenerMouseover({ $table, params, showHeaderTip, showTitle, showTooltip, thOns }) - addListenerMouseout({ $table, showHeaderTip, showTooltip, thOns }) - - let args1 = { $table, highlightCurrentColumn, mouseConfig, params } - Object.assign(args1, { sortOpts, tableListeners, thOns }) - addListenerClick(args1) - addListenerDblclick({ $table, params, tableListeners, thOns }) - - // 按下事件处理 - addListenerMousedown({ $table, mouseConfig, params, thOns }) - args1 = { column, columnIndex, columnKey, fixedHiddenColumn, hasEllipsis, headAlign, columnStore, scrollbarWidth } - Object.assign(args1, { headerCellClassName, headerClassName, isColGroup, isDragHeaderSorting, params, thOns }) - let args2 = { column, fixedHiddenColumn, headerSuffixIconAbsolute, params, $table } - Object.assign(args2, { showEllipsis, showHeaderTip, showTitle, showTooltip }) - - return h('th', getThPropsArg(args1), [ - renderThPartition({ border, column, isColGroup, resizable }), - renderThCell(args2), - // 列宽拖动 - renderThResize({ _vm, border, column, fixedHiddenColumn, isColGroup, params, resizable, isColResize }) - ]) - } -} - -function renderTableThead(args) { - let { $table, _vm, allAlign, allColumnHeaderOverflow } = args - let { allHeaderAlign, border, columnKey } = args - let { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute } = args - let { highlightCurrentColumn, isDragHeaderSorting, mouseConfig } = args - let { overflowX, resizable, sortOpts, tableListeners } = args - - return h( - 'thead', - { - ref: 'thead' - }, - headerColumn.map((cols, $rowIndex) => { - let args1 = { $rowIndex, $table, _vm, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } - - Object.assign(args1, { headerCellClassName, headerSuffixIconAbsolute, highlightCurrentColumn }) - Object.assign(args1, { isDragHeaderSorting, mouseConfig, overflowX, resizable, sortOpts, tableListeners }) - - return h( - 'tr', - { - class: [ - 'tiny-grid-header__row', - headerRowClassName - ? isFunction(headerRowClassName) - ? headerRowClassName({ $table, $rowIndex }) - : headerRowClassName - : '' - ] - }, - cols.map(getThHandler(args1)).concat([h('th', { class: 'col__gutter' })]) - ) - }) - ) -} - -function updateResizableToolbar($table) { - const toolbarVm = $table.getVm('toolbar') - - if (toolbarVm) { - toolbarVm.updateResizable() - } -} - -function renderTable(args) { - let { $table, _vm, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } = args - let { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute } = args - let { highlightCurrentColumn, isDragHeaderSorting, mouseConfig, overflowX, resizable, sortOpts } = args - let { tableColumn, tableLayout, tableListeners } = args - let args1 = { $table, _vm, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } - - Object.assign(args1, { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute }) - Object.assign(args1, { highlightCurrentColumn, isDragHeaderSorting, mouseConfig }) - Object.assign(args1, { overflowX, resizable, sortOpts, tableListeners }) - - return h( - 'table', - { - class: 'tiny-grid__header', - style: { tableLayout }, - attrs: { cellspacing: 0, cellpadding: 0, border: 0 }, - ref: 'table' - }, - [ - // 列宽 - renderTableColgroup(tableColumn), - // 头部 - renderTableThead(args1) - ] - ) -} - -const documentOnmouseup = function ({ - oldMousemove, - oldMouseup, - column, - dragPosLeft, - dragLeft, - resizeBarElem, - $table, - params -}) { - document.onmousemove = oldMousemove - document.onmouseup = oldMouseup - - let resizeWidth = column.renderWidth + dragLeft - dragPosLeft - resizeWidth = typeof resizeWidth === 'number' ? resizeWidth : parseInt(resizeWidth, 10) || 40 - column.resizeWidth = resizeWidth < 40 ? 40 : resizeWidth - - resizeBarElem.style.display = 'none' - removeClass($table.$el, 'tiny-grid-cell__resize') - Object.assign($table, { _isResize: false, _lastResizeTime: Date.now() }) - - $table.analyColumnWidth() - $table.recalculate().then(() => { - // 拖拽后,需要同步表头的scrollLeft - const { tableBody, tableFooter, tableHeader } = $table.$refs || {} - const headerElm = tableHeader?.$el - const bodyElm = tableBody?.$el - const footerElm = tableFooter?.$el - if (!headerElm) { - return - } - const elemStore = $table.elemStore - if (bodyElm) { - bodyElm.scrollLeft = headerElm.scrollLeft - } - if (footerElm) { - footerElm.scrollLeft = headerElm.scrollLeft - } - - if (!elemStore['main-header-repair']) { - return - } - elemStore['main-body-xSpace'].style.width = elemStore['main-header-repair'].style.width - if (elemStore['main-footer-xSpace']) { - elemStore['main-footer-xSpace'].style.width = elemStore['main-header-repair'].style.width - } - }) - updateResizableToolbar($table) - emitEvent($table, 'resizable-change', [params]) -} - -export default defineComponent({ - name: `${$prefix}GridHeader`, - props: { - collectColumn: Array, - fixedColumn: Array, - size: String, - isGroup: Boolean, - tableColumn: Array, - tableData: Array, - visibleColumn: Array, - resizableConfig: Object - }, - watch: { - tableColumn() { - this.uploadColumn() - } - }, - data() { - return { - headerColumn: [] - } - }, - mounted() { - const { $el, $parent: $table, $refs } = this - const { elemStore, dropConfig } = $table - const keyPrefix = 'main-header-' - - elemStore[`${keyPrefix}wrapper`] = $el - elemStore[`${keyPrefix}table`] = $refs.table - elemStore[`${keyPrefix}colgroup`] = $refs.colgroup - elemStore[`${keyPrefix}list`] = $refs.thead - elemStore[`${keyPrefix}x-space`] = $refs.xSpace - elemStore[`${keyPrefix}repair`] = $refs.repair - - if (dropConfig) { - const { plugin, column = true, scheme } = dropConfig - - if (scheme !== 'v2') { - plugin && column && (this.columnSortable = $table.columnDrop(this.$el)) - } - } - }, - beforeUnmount() { - this.columnSortable && this.columnSortable.destroy() - }, - created() { - this.uploadColumn() - }, - render() { - let { $parent: $table, headerColumn, tableColumn } = this - let { align: allAlign, border, columnKey, headerAlign: allHeaderAlign } = $table - let { headerCellClassName, headerRowClassName, headerSuffixIconAbsolute } = $table - let { highlightCurrentColumn, isDragHeaderSorting, mouseConfig = {}, overflowX } = $table - let { resizable, showHeaderOverflow: allColumnHeaderOverflow } = $table - let { sortOpts, tableLayout, tableListeners } = $table - - let args = { $table, _vm: this, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } - - Object.assign(args, { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute }) - Object.assign(args, { highlightCurrentColumn, isDragHeaderSorting, mouseConfig, overflowX, resizable, sortOpts }) - Object.assign(args, { tableColumn, tableLayout, tableListeners }) - - return h( - 'div', - { - class: ['tiny-grid__header-wrapper', 'body__wrapper'] - }, - [ - // 表格主体内容x轴方向虚拟滚动条占位元素,在表头中属于无效元素,待删除 - renderXSpace(), - renderTable(args), - // x轴方向虚拟滚动适配线 - renderRepair() - ] - ) - }, - methods: { - uploadColumn() { - this.headerColumn = this.isGroup ? this.$parent._sliceColumnTree(this.tableColumn) : [this.tableColumn] - }, - resizeMousedown(event, params) { - let { $el, $parent: $table, resizableConfig } = this - let { clientX: dragClientX, target: dragBtnElem } = event - let { column } = params - let { dragLeft = 0, minInterval = 36, fixedOffsetWidth = 0 } = {} - let { resizeBar: resizeBarElem, tableBody } = $table.$refs - let { cell = dragBtnElem.parentNode, dragBtnWidth = dragBtnElem.clientWidth } = {} - let startColumnLeft = cell.offsetLeft - let dragBtnOffsetWidth = Math.floor(dragBtnWidth / 2) - const tableBodyElem = tableBody.$el - const btnLeft = dragBtnElem?.getBoundingClientRect().left - $el?.getBoundingClientRect().left - let dragMinLeft = btnLeft - cell.clientWidth + dragBtnWidth + minInterval - let dragPosLeft = btnLeft + dragBtnOffsetWidth - let { oldMousemove = document.onmousemove, oldMouseup = document.onmouseup } = {} - - // 处理拖动事件 - let handleMousemoveEvent = function (event) { - event.stopPropagation() - event.preventDefault() - - let { offsetX = event.clientX - dragClientX, left = offsetX + dragPosLeft } = {} - let scrollLeft = tableBodyElem.scrollLeft - let args = { - cell, - dragMinLeft, - dragPosLeft, - fixedOffsetWidth, - resizableConfig, - scrollLeft, - column, - dragBtnOffsetWidth, - startColumnLeft - } - Object.assign(args, { left, minInterval, tableBodyElem }) - - let ret = computeDragLeft(args) - dragMinLeft = ret.dragMinLeft - dragLeft = ret.dragLeft - - let currentLeft = ret.dragLeft - scrollLeft - - resizeBarElem.style.left = `${currentLeft}px` - } - - resizeBarElem.style.display = 'block' - addClass($table.$el, 'tiny-grid-cell__resize') - $table._isResize = true - - document.onmousemove = handleMousemoveEvent - document.onmouseup = () => { - documentOnmouseup({ oldMousemove, oldMouseup, column, dragPosLeft, dragLeft, resizeBarElem, $table, params }) - } - handleMousemoveEvent(event) - } - } -}) diff --git a/packages/vue/src/grid/src/keyboard/src/methods.ts b/packages/vue/src/grid/src/keyboard/src/methods.ts index a221cfee15..af82856e75 100644 --- a/packages/vue/src/grid/src/keyboard/src/methods.ts +++ b/packages/vue/src/grid/src/keyboard/src/methods.ts @@ -242,7 +242,7 @@ export default { // 表头按下事件 triggerHeaderCellMousedownEvent(event, params) { let { $el, elemStore, mouseConfig = {}, tableData } = this - let headerList = elemStore['main-header-list'].children + let headerList = elemStore['main-body-headerList'].children let bodyList = elemStore['main-body-list'].children let cell = params.cell let column = params.column @@ -317,12 +317,12 @@ export default { let isIndex = column.type === 'index' let startCellNode = getCellNodeIndex(cell) - let headerList = elemStore['main-header-list'].children + let headerList = elemStore['main-body-headerList'].children let bodyList = elemStore['main-body-list'].children let cellFirstElementChild = cell.parentNode.firstElementChild let cellLastElementChild = cell.parentNode.lastElementChild let colIndex = Array.from(cell.parentNode.children).indexOf(cell) - let headStart = headerList[0].children[colIndex] + let headStart = headerList?.[0].children[colIndex] args = { $el, _vm: this, bodyList, cell, cellFirstElementChild } Object.assign(args, { cellLastElementChild, headStart, headerList, isIndex, startCellNode }) @@ -354,7 +354,7 @@ export default { } let bodyElem = elemStore['main-body-list'] - let headerElem = elemStore['main-header-list'] + let headerElem = elemStore['main-body-headerList'] if (bodyElem) { let elem = bodyElem.querySelector('.col__selected') @@ -477,9 +477,9 @@ export default { let column = find(visibleColumn, (col) => col.type === 'index') || visibleColumn[0] let selectorColumnId = `.${column.id}` - let headerListElem = elemStore['main-header-list'] - let headerList = headerListElem.children - let cell = headerListElem.querySelector(selectorColumnId) + let headerListElem = elemStore['main-body-headerList'] + let headerList = headerListElem?.children + let cell = headerListElem?.querySelector(selectorColumnId) let bodyList = elemStore['main-body-list'].children let firstTrElem = bodyList[0] let firstCell = firstTrElem.querySelector(selectorColumnId) @@ -546,7 +546,7 @@ export default { this.editStore.titles.rowNodes = rowNodes }, _clearHeaderChecked() { - let headerElem = this.elemStore['main-header-list'] + let headerElem = this.elemStore['main-body-headerList'] if (headerElem) { let eachHandler = (colNode) => removeClass(colNode, 'col__title-checked') diff --git a/packages/vue/src/grid/src/mobile-first/index.vue b/packages/vue/src/grid/src/mobile-first/index.vue index e714333d58..50d0b081c3 100644 --- a/packages/vue/src/grid/src/mobile-first/index.vue +++ b/packages/vue/src/grid/src/mobile-first/index.vue @@ -45,7 +45,7 @@ diff --git a/packages/vue/src/grid/src/column-anchor/src/methods.ts b/packages/vue/src/grid/src/column-anchor/src/methods.ts index 8d833f486f..a212c76d74 100644 --- a/packages/vue/src/grid/src/column-anchor/src/methods.ts +++ b/packages/vue/src/grid/src/column-anchor/src/methods.ts @@ -1,35 +1,4 @@ -import { iconMarkOn } from '@opentiny/vue-icon' -import { h } from '@opentiny/vue-common' - export default { - renderColumnAnchor(params, _vm) { - const { anchors = [], action = () => {} } = params || {} - const { viewType } = _vm - - return h( - 'div', - { - class: ['tiny-grid__column-anchor', _vm.viewCls('columnAnchor')], - style: viewType === 'default' ? 'display:flex' : '', - key: _vm.columnAnchorKey, - ref: 'tinyGridColumnAnchor' - }, - anchors.map((anchor) => { - const { active = false, label = '', field = '', render } = anchor - - if (typeof render === 'function') { - return render({ h, anchor, action }) - } - - const itemClass = { 'tiny-grid__column-anchor-item': true, 'tiny-grid__column-anchor-item--active': active } - const itemOn = { click: (e) => action(field, e) } - const iconVnode = active ? h(iconMarkOn(), { class: 'tiny-grid__column-anchor-item-icon' }) : null - const spanVnode = h('span', label) - - return h('div', { class: itemClass, on: itemOn }, [iconVnode, spanVnode]) - }) - ) - }, buildColumnAnchor({ property, label, anchors, activeAnchor }) { let visibleColumn = this.getColumns() let column = visibleColumn.find((col) => !col.type && col.property === property) diff --git a/packages/vue/src/grid/src/grid/grid.vue b/packages/vue/src/grid/src/grid/grid.vue index 4751fe3fee..8fdcfca438 100644 --- a/packages/vue/src/grid/src/grid/grid.vue +++ b/packages/vue/src/grid/src/grid/grid.vue @@ -15,7 +15,7 @@ - + { + // 获取组件实例的DOM元素和refs引用 const { $el, $refs } = this + // 获取表格和列锚点的ref引用 const { tinyTable, tinyGridColumnAnchor } = $refs + // 获取工具栏组件实例 const toolbarVm = this.getVm('toolbar') if (tinyTable) { + // 初始化列锚点高度为0 let columnAnchorHeight = 0 + + // 如果存在列锚点,计算其实际占用的总高度(包含margin) if (tinyGridColumnAnchor) { const { height, marginTop, marginBottom } = getComputedStyle(tinyGridColumnAnchor) columnAnchorHeight = toNumber(height) + toNumber(marginTop) + toNumber(marginBottom) } + + // 计算表格的父容器高度: + // 父节点总高度 - 工具栏高度(如果有) - 列锚点高度 - 分页器高度(如果有) tinyTable.parentHeight = $el.parentNode.clientHeight - (toolbarVm ? toolbarVm.$el.clientHeight : 0) - @@ -523,6 +515,7 @@ export default defineComponent({ }) } + // 执行更新父容器高度的任务 this.tasks.updateParentHeight() }, From d46424edfbbcc7490dbe0954ed3e9999b1fa461f Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Thu, 17 Apr 2025 15:57:15 +0800 Subject: [PATCH 30/52] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A0=8F=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E5=BC=8F=E5=92=8C=E6=8F=92?= =?UTF-8?q?=E6=A7=BD=E5=BC=8F=E4=BD=BF=E7=94=A8=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=88=97=E5=90=8D=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/grid/grid.vue | 33 +++++++++++-------- packages/vue/src/grid/src/table/src/Table.vue | 23 ++++++++++++- .../vue/src/grid/src/toolbar/src/methods.ts | 20 ----------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/vue/src/grid/src/grid/grid.vue b/packages/vue/src/grid/src/grid/grid.vue index 8fdcfca438..60f88b83a4 100644 --- a/packages/vue/src/grid/src/grid/grid.vue +++ b/packages/vue/src/grid/src/grid/grid.vue @@ -10,9 +10,21 @@ }" > - + + + + + + @@ -324,7 +336,7 @@ export default defineComponent({ }, mounted() { - const { columns, fetchOption, autoLoad, pagerSlot, prefetch } = this + const { columns, fetchOption, autoLoad, prefetch } = this // 处理列配置 if (columns && columns.length) { @@ -398,14 +410,6 @@ export default defineComponent({ updateRenderComponents() { this.updateTableOptions() this.updateTableEvents() - - this.renderedToolbar = this.getRenderedToolbar({ - $slots: this.$slots, - _vm: this, - loading: this.loading, - tableLoading: this.tableLoading, - toolbar: this.toolbar - }) }, // 更新表格选项 @@ -532,8 +536,9 @@ export default defineComponent({ this.$pageSizeChangeCallback = callback } else if (type === 'updateCustomsCallback') { // 表格可能有多个工具栏,因此工具栏个性化配置的回调应该是个数组 - this.$updateCustomsCallback = this.$updateCustomsCallback || [] - this.$updateCustomsCallback.push(callback) + this.$updateCustomsCallback = this.$updateCustomsCallback + ? this.$updateCustomsCallback.concat(callback) + : [callback] } }, diff --git a/packages/vue/src/grid/src/table/src/Table.vue b/packages/vue/src/grid/src/table/src/Table.vue index 5f03a8af57..fe88fe8aa1 100644 --- a/packages/vue/src/grid/src/table/src/Table.vue +++ b/packages/vue/src/grid/src/table/src/Table.vue @@ -912,6 +912,26 @@ export default defineComponent({ const sortOpts = hooks.computed(() => { return extend(true, {}, GlobalConfig.sortConfig, props.sortConfig) }) + + const columnNames = hooks.computed(() => { + const { customColumnNames } = props + const columnNames = [defaultColumnName] + + const pushIfNot = (columnName) => { + if (typeof columnName === 'string' && !columnNames.includes(columnName)) { + columnNames.push(columnName) + } + } + + if (Array.isArray(customColumnNames) && customColumnNames.length > 0) { + customColumnNames.forEach(pushIfNot) + } else if (typeof customColumnNames === 'string') { + pushIfNot(customColumnNames) + } + + return columnNames + }) + // 初始化列 const initColumns = () => { // 初始化表格实例的插槽 @@ -1241,7 +1261,8 @@ export default defineComponent({ selectToolbarStyle, staticClass, emptyText, - sortOpts + sortOpts, + columnNames } } }) diff --git a/packages/vue/src/grid/src/toolbar/src/methods.ts b/packages/vue/src/grid/src/toolbar/src/methods.ts index 215042911b..f13d712817 100644 --- a/packages/vue/src/grid/src/toolbar/src/methods.ts +++ b/packages/vue/src/grid/src/toolbar/src/methods.ts @@ -3,7 +3,6 @@ import { error } from '../../tools' import Modal from '@opentiny/vue-modal' import GlobalConfig from '../../config' import { emitEvent } from '@opentiny/vue-renderless/grid/utils' -import { h, hooks } from '@opentiny/vue-common' import { extend } from '@opentiny/utils' export function setBodyRecords({ body, insertRecords, pendingRecords }) { @@ -71,25 +70,6 @@ export function invokeSaveDataApi({ _vm, args, body, code, removeRecords, resolv } export default { - // 表格工具栏渲染器 - getRenderedToolbar({ $slots, _vm, loading, tableLoading, toolbar }) { - return (_vm.renderedToolbar = (() => { - let res = null - - if ($slots.toolbar) { - res = $slots.toolbar() - } else if (toolbar) { - res = h(hooks.toRaw(toolbar.component), { - ref: 'toolbar', - props: { loading: loading || tableLoading, ...toolbar }, - class: _vm.viewCls('toolbar'), - scopedSlots: toolbar.slots || {} - }) - } - - return res - })()) - }, handleSave(code, args) { let { saveData, isMsg } = this From b268e58cee12373404cf323568b9e843a9270cfc Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Thu, 17 Apr 2025 16:43:40 +0800 Subject: [PATCH 31/52] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A0=8F=E7=BB=84=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8F=92=E6=A7=BD=E4=BD=BF=E7=94=A8=E6=96=B9=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=B3=A8=E9=87=8A=E4=BB=A5=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid-toolbar/src/index.ts | 1 - packages/vue/src/grid/src/grid/grid.vue | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/vue/src/grid-toolbar/src/index.ts b/packages/vue/src/grid-toolbar/src/index.ts index 3e43b1b197..3eb8588f55 100644 --- a/packages/vue/src/grid-toolbar/src/index.ts +++ b/packages/vue/src/grid-toolbar/src/index.ts @@ -399,7 +399,6 @@ export default defineComponent({ } const defaultSlot = () => (typeof $slots.default === 'function' ? $slots.default() : $slots.default) - let childrenArg = [ renderButtonWrapper({ _vm: this, $buttons, $grid, table, buttons, vSize }), setting ? renderCustomWrapper(args) : null, diff --git a/packages/vue/src/grid/src/grid/grid.vue b/packages/vue/src/grid/src/grid/grid.vue index 60f88b83a4..79fca9807c 100644 --- a/packages/vue/src/grid/src/grid/grid.vue +++ b/packages/vue/src/grid/src/grid/grid.vue @@ -10,7 +10,7 @@ }" > - + - + From 4ee698cb43c346ed94d23aa026e8d003fb6e0f58 Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Wed, 23 Apr 2025 17:38:52 +0800 Subject: [PATCH 32/52] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E7=9A=84=E9=87=8D=E6=96=B0=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=8F=AF=E8=AF=BB=E6=80=A7=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E4=B8=AD=E9=81=AE=E7=BD=A9=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/src/grid/src/body/src/body.vue | 22 ++++------ packages/vue/src/grid/src/edit/src/methods.ts | 8 ++-- .../vue/src/grid/src/filter/src/methods.ts | 2 +- packages/vue/src/grid/src/grid/grid.vue | 2 +- .../vue/src/grid/src/resize/src/methods.ts | 1 - packages/vue/src/grid/src/table/src/Table.vue | 44 +++++++++++++------ packages/vue/src/grid/src/table/src/events.ts | 1 - .../vue/src/grid/src/table/src/methods.ts | 30 +++---------- .../vue/src/grid/src/table/src/strategy.ts | 2 +- .../grid/src/table/src/utils/autoCellWidth.ts | 32 ++------------ .../vue/src/grid/src/toolbar/src/methods.ts | 1 - packages/vue/src/grid/src/tree/src/methods.ts | 8 ++-- .../vue/src/grid/src/validator/src/methods.ts | 2 - 13 files changed, 59 insertions(+), 96 deletions(-) diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue index f418bbfd7d..832a7c2ae1 100644 --- a/packages/vue/src/grid/src/body/src/body.vue +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -25,7 +25,12 @@ >
- + @@ -248,20 +253,9 @@