From 85d763436f29930ca281e92164b85d5a4591e4ea Mon Sep 17 00:00:00 2001
From: ajaxzheng <894103554@qq.com>
Date: Tue, 17 Jun 2025 09:41:39 +0800
Subject: [PATCH] refactor: optimize table performance and refactor the table
---
.../pc/app/grid/custom/column-fixed.spec.js | 3 +-
.../pc/app/grid/custom/page-size.spec.js | 2 +-
.../grid/data-source/request-service.spec.js | 2 +-
.../app/grid/data-source/static-data.spec.js | 2 +-
.../pc/app/grid/editor/custom-edit.spec.js | 9 +-
.../grid/empty/empty-data-iscenter.spec.js | 2 +-
.../event/toolbar-button-click-event.spec.js | 2 +-
.../app/grid/expand/set-row-expansion.spec.js | 4 +
.../app/grid/filter/default-relation.spec.js | 2 +-
.../pc/app/grid/filter/server-filter.spec.js | 2 +-
.../footer/footer-summation-empty.spec.js | 1 -
.../grid/large-data/full-data-loading.spec.js | 6 +-
.../keyboard-navigation.spec.js | 5 +-
.../app/grid/renderer/inner-renderer.spec.js | 4 +
.../app/grid/size/max-min-grid-height.spec.js | 4 +-
.../app/grid/sort/combinations-sort.spec.js | 4 +-
.../demos/pc/app/grid/webdoc/grid-empty.js | 2 +-
.../sites/demos/pc/app/icon/iconGroups.js | 1 +
.../src/grid/plugins/exportExcel.ts | 3 +-
packages/renderless/src/grid/utils/column.ts | 187 +-
packages/renderless/src/grid/utils/common.ts | 121 +-
packages/renderless/src/grid/utils/dom.ts | 198 +-
packages/theme-saas/src/grid/body.less | 10 +
packages/theme-saas/src/grid/header.less | 53 -
packages/theme-saas/src/grid/table.less | 147 +-
.../src/svgs/delegated-processing.svg | 4 +
.../theme-saas/src/svgs/pushpin-solid.svg | 18 +-
packages/theme-saas/src/svgs/pushpin.svg | 21 +-
packages/theme/src/grid/body.less | 10 +
packages/theme/src/grid/header.less | 2 -
packages/theme/src/grid/table.less | 107 +-
.../theme/src/svgs/delegated-processing.svg | 4 +
packages/vue-icon-saas/index.ts | 4 +
packages/vue-icon/index.ts | 4 +
.../src/delegated-processing/index.ts | 15 +
packages/vue/src/grid-toolbar/src/index.ts | 10 +-
packages/vue/src/grid/index.ts | 2 +-
.../vue/src/grid/src/adapter/src/renderer.ts | 24 +-
packages/vue/src/grid/src/body/src/body.tsx | 1608 +++++++++--------
packages/vue/src/grid/src/body/src/usePool.ts | 108 ++
packages/vue/src/grid/src/cell/src/cell.ts | 6 +-
.../vue/src/grid/src/checkbox/src/methods.ts | 14 +-
.../src/grid/src/column-anchor/src/methods.ts | 6 +-
.../vue/src/grid/src/column/src/column.ts | 3 +-
packages/vue/src/grid/src/composable/index.ts | 5 +
.../src/grid/src/composable/useCellEvent.ts | 365 ++++
.../src/grid/src/composable/useCellSpan.ts | 245 +++
.../src/grid/src/composable/useCellStatus.ts | 50 +
.../vue/src/grid/src/composable/useData.ts | 126 ++
.../src/grid/src/composable/useDrag/index.ts | 120 +-
.../vue/src/grid/src/composable/useHeader.ts | 116 ++
.../src/grid/src/composable/useRowGroup.ts | 105 +-
packages/vue/src/grid/src/config.ts | 12 +
.../vue/src/grid/src/dragger/src/methods.ts | 2 +-
packages/vue/src/grid/src/edit/src/methods.ts | 17 +-
.../src/grid/src/fetch-data/src/methods.ts | 4 +-
packages/vue/src/grid/src/footer/index.ts | 32 -
.../vue/src/grid/src/footer/src/footer.ts | 359 ----
packages/vue/src/grid/src/grid/grid.ts | 117 +-
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 | 61 +-
packages/vue/src/grid/src/menu/src/methods.ts | 46 +-
.../vue/src/grid/src/mobile-first/index.vue | 3 +-
.../vue/src/grid/src/pager/src/methods.ts | 22 +-
.../vue/src/grid/src/resize/src/methods.ts | 20 +-
.../vue/src/grid/src/table/src/methods.ts | 1119 ++++++------
.../vue/src/grid/src/table/src/strategy.ts | 400 +---
packages/vue/src/grid/src/table/src/table.ts | 1073 +++++------
.../grid/src/table/src/utils/autoCellWidth.ts | 212 ++-
.../src/table/src/utils/computeScrollLoad.ts | 4 +-
.../src/table/src/utils/handleFixedColumn.ts | 27 -
.../grid/src/table/src/utils/updateStyle.ts | 265 +--
.../vue/src/grid/src/toolbar/src/methods.ts | 2 +-
packages/vue/src/grid/src/tools/index.ts | 7 +-
.../vue/src/grid/src/tooltip/src/methods.ts | 10 +-
packages/vue/src/grid/src/tree/src/methods.ts | 35 +-
77 files changed, 3923 insertions(+), 4415 deletions(-)
create mode 100644 packages/theme-saas/src/svgs/delegated-processing.svg
create mode 100644 packages/theme/src/svgs/delegated-processing.svg
create mode 100644 packages/vue-icon/src/delegated-processing/index.ts
create mode 100644 packages/vue/src/grid/src/body/src/usePool.ts
create mode 100644 packages/vue/src/grid/src/composable/useCellEvent.ts
create mode 100644 packages/vue/src/grid/src/composable/useCellSpan.ts
create mode 100644 packages/vue/src/grid/src/composable/useCellStatus.ts
create mode 100644 packages/vue/src/grid/src/composable/useData.ts
create mode 100644 packages/vue/src/grid/src/composable/useHeader.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
delete mode 100644 packages/vue/src/grid/src/table/src/utils/handleFixedColumn.ts
diff --git a/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js b/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js
index 8b4ad4a35b..4cb592db8d 100644
--- a/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js
+++ b/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js
@@ -3,10 +3,11 @@ import { test, expect } from '@playwright/test'
test('列冻结', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
const custom = page.locator('.tiny-grid-custom')
+ const demo = page.locator('#custom-column-fixed')
await page.goto('grid-custom#custom-column-fixed')
await page.locator('.tiny-grid-custom__setting-btn').click()
await custom.getByRole('row', { name: '员工数 ' }).getByTitle('未冻结').getByRole('img').click()
await custom.getByRole('row', { name: '员工数' }).getByTitle('左冻结').getByRole('img').click()
await page.getByRole('button', { name: '确定' }).click()
- await expect(page.getByRole('cell', { name: '员工数' })).toHaveCSS('right', '0px')
+ await expect(demo.locator('.tiny-grid-header__row th').nth(3)).toHaveText(/员工数/)
})
diff --git a/examples/sites/demos/pc/app/grid/custom/page-size.spec.js b/examples/sites/demos/pc/app/grid/custom/page-size.spec.js
index ec6bf31229..cb76ff4466 100644
--- a/examples/sites/demos/pc/app/grid/custom/page-size.spec.js
+++ b/examples/sites/demos/pc/app/grid/custom/page-size.spec.js
@@ -9,5 +9,5 @@ test('分页条数', async ({ page }) => {
await page.getByText('其他设置', { exact: true }).click()
await page.locator('label').filter({ hasText: '5' }).locator('path').nth(1).click()
await page.getByRole('button', { name: '确定' }).click()
- await expect(demo.locator('.tiny-grid-body__row')).toHaveCount(5)
+ await expect(demo.locator('.tiny-grid-body__row:visible')).toHaveCount(5)
})
diff --git a/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js b/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js
index 3b880a4557..a72cb6d9f7 100644
--- a/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js
+++ b/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js
@@ -6,5 +6,5 @@ test('开启服务请求', async ({ page }) => {
await page.getByRole('button', { name: '筛选华南区数据' }).click()
// 判断筛选华南区数据成功
- await expect(page.locator('.tiny-grid-body__row')).toHaveCount(3)
+ await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(3)
})
diff --git a/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js b/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js
index 34c32ef20e..dc14e1ff37 100644
--- a/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js
+++ b/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js
@@ -14,5 +14,5 @@ test('绑定静态数据', async ({ page }) => {
// 改变 data 数据引用地址
await page.getByRole('button', { name: '改变 tableData 引用地址' }).click()
- await expect(page.locator('.tiny-grid-body__row')).toHaveCount(2)
+ await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(2)
})
diff --git a/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js b/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js
index 59a7a634aa..c8f8665a72 100644
--- a/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js
+++ b/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js
@@ -3,6 +3,11 @@ import { test, expect } from '@playwright/test'
test('多行编辑', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-editor#editor-custom-edit')
- await expect(page.getByRole('cell', { name: 'GFD 科技有限公司' }).getByRole('textbox')).toBeVisible()
- await expect(page.getByRole('cell', { name: 'WWWW 科技有限公司' }).getByRole('textbox')).toBeVisible()
+ const demo = page.locator('#editor-custom-edit')
+ await expect(
+ demo.locator('.tiny-grid-body__row').nth(0).locator('td').nth(1).locator('.tiny-input__inner')
+ ).toBeVisible()
+ await expect(
+ demo.locator('.tiny-grid-body__row').nth(1).locator('td').nth(1).locator('.tiny-input__inner')
+ ).toBeVisible()
})
diff --git a/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js b/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js
index 593345220b..7e02c7f5c0 100644
--- a/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js
+++ b/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js
@@ -8,5 +8,5 @@ test('固定居中', async ({ page }) => {
await expect(page.getByText('暂无数据').first()).toBeVisible()
// 判断是否居中
- await expect(page.locator('.empty-center-block')).toHaveCSS('justify-content', 'center')
+ await expect(page.locator('.tiny-grid__empty-block')).toHaveCSS('justify-content', 'center')
})
diff --git a/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js b/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js
index 5515a5188f..e465d89c04 100644
--- a/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js
+++ b/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js
@@ -15,5 +15,5 @@ test('工具栏点击事件', async ({ page }) => {
await page.getByRole('button', { name: '删除', exact: true }).click()
- await expect(page.locator('.tiny-grid-body__row')).toHaveCount(6)
+ await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(6)
})
diff --git a/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js b/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js
index 12bf540c95..39e91bd091 100644
--- a/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js
+++ b/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js
@@ -2,6 +2,10 @@ import { test, expect } from '@playwright/test'
test('设置指定展开行', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
+ await page.setViewportSize({
+ width: 1400,
+ height: 2500
+ })
await page.goto('grid-expand#expand-set-row-expansion')
await page.getByRole('button', { name: '展开指定行' }).click()
await expect(page.getByText('GFD 科技 YX 公司')).toHaveCount(2)
diff --git a/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js b/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js
index 79bc24af7c..0222300c47 100644
--- a/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js
+++ b/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js
@@ -7,5 +7,5 @@ test('输入过滤的默认选项', async ({ page }) => {
await page.getByRole('spinbutton').click()
await page.getByRole('spinbutton').fill('800')
await page.getByRole('button', { name: '确定' }).click()
- await expect(page.locator('.tiny-grid-body__row')).toHaveCount(2)
+ await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(2)
})
diff --git a/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js b/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js
index c2614135a3..a742baaad5 100644
--- a/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js
+++ b/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js
@@ -7,5 +7,5 @@ test('服务端过滤', async ({ page }) => {
await page.getByRole('cell', { name: '城市' }).getByRole('img').first().click()
await page.locator('li').filter({ hasText: '深圳' }).click()
await page.getByRole('button', { name: '确定' }).click()
- await expect(page.locator('.tiny-grid-body__row')).toHaveCount(2)
+ await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(2)
})
diff --git a/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js b/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js
index d4d48d8f24..ad3afa0919 100644
--- a/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js
+++ b/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js
@@ -3,7 +3,6 @@ import { test, expect } from '@playwright/test'
test('表尾统计(空数据)', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-footer#footer-footer-summation-empty')
- await expect(page.getByRole('row', { name: '平均 0' }).getByRole('cell', { name: '0' })).toBeVisible()
await page.getByRole('button', { name: '加载数据' }).click()
await expect(page.getByRole('cell', { name: '663' })).toBeVisible()
})
diff --git a/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js b/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js
index e4297a2db9..5cb20b20a6 100644
--- a/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js
+++ b/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js
@@ -3,6 +3,10 @@ import { test, expect } from '@playwright/test'
test('全量加载', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-large-data#large-data-full-data-loading')
+ await page.setViewportSize({
+ width: 1400,
+ height: 2500
+ })
await page.locator('.tiny-grid__body').hover()
// 先滚动 1000px
await page.mouse.wheel(0, 1000)
@@ -12,5 +16,5 @@ test('全量加载', async ({ page }) => {
// 先滚动 4000px
await page.mouse.wheel(0, 5000)
await page.waitForTimeout(200)
- await expect(page.getByRole('cell', { name: '153' })).toBeVisible()
+ await expect(page.getByRole('cell', { name: '129' })).toBeVisible()
})
diff --git a/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js b/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js
index 85bfea0d0b..211ba64b85 100644
--- a/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js
+++ b/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js
@@ -4,7 +4,10 @@ test('键盘导航测试', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
const demo = page.locator('#mouse-keyboard-keyboard-navigation')
await page.goto('grid-mouse-keyboard#mouse-keyboard-keyboard-navigation')
-
+ await page.setViewportSize({
+ width: 1400,
+ height: 2500
+ })
await page.getByText('GFD 科技 YX 公司').click()
await page.waitForTimeout(300)
await page.locator('body').press('ArrowDown')
diff --git a/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js b/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js
index 62660b89a3..26310aba2b 100644
--- a/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js
+++ b/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js
@@ -3,6 +3,10 @@ import { test, expect } from '@playwright/test'
test('内置渲染器', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-renderer#renderer-inner-renderer')
+ await page.setViewportSize({
+ width: 1400,
+ height: 2500
+ })
const cell = page.getByRole('cell', { name: '90.0%' }).locator('.tiny-grid__rate-chart')
await expect(cell).toHaveCSS('background-color', 'rgb(92, 179, 0)')
})
diff --git a/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js b/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js
index f13f13042c..34ec35b31e 100644
--- a/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js
+++ b/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js
@@ -3,6 +3,6 @@ import { test, expect } from '@playwright/test'
test('设置 maxHeight 最大高度', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-size#size-max-min-grid-height')
- await expect(page.locator('.tiny-grid__body-wrapper').nth(0)).toHaveCSS('max-height', '160px')
- await expect(page.locator('.tiny-grid__body-wrapper').nth(1)).toHaveCSS('min-height', '260px')
+ await expect(page.locator('.tiny-grid__body-wrapper').nth(0)).toHaveCSS('max-height', '200px')
+ await expect(page.locator('.tiny-grid__body-wrapper').nth(1)).toHaveCSS('min-height', '300px')
})
diff --git a/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js b/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js
index fa6f507020..4d59632d02 100644
--- a/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js
+++ b/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js
@@ -5,7 +5,7 @@ test('多字段组合排序', async ({ page }) => {
await page.goto('grid-sort#sort-combinations-sort')
await page.getByRole('cell', { name: '员工数(员工数和名称组合排序)' }).getByRole('img').click()
// 员工数第一优先级排序
- await expect(page.locator('.tiny-grid-body__row').first()).toContainText('1300')
+ await expect(page.locator('.tiny-grid-body__row').first()).toContainText('300')
// 公司名称第二优先级排序
- await expect(page.locator('.tiny-grid-body__row').nth(1)).toContainText('YHN 科技 YX 公司')
+ await expect(page.locator('.tiny-grid-body__row').nth(6)).toContainText('YHN 科技 YX 公司')
})
diff --git a/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js b/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js
index 112366def5..7407a2bc1e 100644
--- a/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js
+++ b/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js
@@ -27,7 +27,7 @@ export default {
name: { 'zh-CN': '固定居中', 'en-US': 'Fix Center' },
desc: {
'zh-CN':
- '
配置 is-center-empty
为 true
时,拖动横向滚动条可以保持空数据提示使终相对表格宽度居中显示。
\n',
+ '(从3.25.0版本开始默认固定居中)配置 is-center-empty
为 true
时,拖动横向滚动条可以保持空数据提示使终相对表格宽度居中显示。
\n',
'en-US':
'When is-center-empty
is set to true
, drag the horizontal scroll bar to keep the empty data prompt so that the final data is displayed in the center of the table width
\n'
},
diff --git a/examples/sites/demos/pc/app/icon/iconGroups.js b/examples/sites/demos/pc/app/icon/iconGroups.js
index 604bff844e..8fddd08ca0 100644
--- a/examples/sites/demos/pc/app/icon/iconGroups.js
+++ b/examples/sites/demos/pc/app/icon/iconGroups.js
@@ -291,6 +291,7 @@ export const iconGroups = {
'IconTabletView',
'IconUnlock',
'IconUser',
+ 'IconDelegatedProcessing',
'IconVersiontree',
'IconWebPlus',
'IconJs',
diff --git a/packages/renderless/src/grid/plugins/exportExcel.ts b/packages/renderless/src/grid/plugins/exportExcel.ts
index dfc6afa42f..10a57bd269 100644
--- a/packages/renderless/src/grid/plugins/exportExcel.ts
+++ b/packages/renderless/src/grid/plugins/exportExcel.ts
@@ -11,8 +11,7 @@
*
*/
-import { extend } from '@opentiny/utils'
-import { browserInfo } from '@opentiny/utils'
+import { extend, browserInfo } from '@opentiny/utils'
const isIE = browserInfo.name === 'ie'
const rgbRegExp = /^rgba?\((\d+),\s(\d+),\s(\d+)([\s\S]*)\)$/
diff --git a/packages/renderless/src/grid/utils/column.ts b/packages/renderless/src/grid/utils/column.ts
index 3fd4b447a0..40a657df5c 100644
--- a/packages/renderless/src/grid/utils/column.ts
+++ b/packages/renderless/src/grid/utils/column.ts
@@ -15,77 +15,126 @@ import { initFilter } from './common'
let columnUniqueId = 0
-export const setColumnFormat = (column, props) => (column.format = props.formatConfig)
-
-function setBasicProperty(column, context) {
- column.id = `col_${++columnUniqueId}`
- column.type = context.type
- column.prop = context.prop
- column.rules = context.rules
- column.required = context.required
- column.property = context.field || context.prop
- column.title = context.title
- column.label = context.label
- column.width = context.width
- column.minWidth = context.minWidth
- column.resizable = context.resizable
- column.fixed = context.fixed
- column.align = context.align
- column.headerAlign = context.headerAlign
- column.footerAlign = context.footerAlign
- column.showOverflow = context.showOverflow
- column.showHeaderOverflow = context.showHeaderOverflow
- column.showTip = context.showTip
- column.showHeaderTip = context.showHeaderTip
- column.className = context.class || context.className
- column.headerClassName = context.headerClassName
- column.footerClassName = context.footerClassName
- column.indexMethod = context.indexMethod
- column.formatText = context.formatText
- column.formatValue = context.formatValue
-
- setColumnFormat(column, context)
-
- column.sortable = context.sortable
- column.sortBy = context.sortBy
- column.sortMethod = context.sortMethod
- column.remoteSort = context.remoteSort
- column.filterMultiple = isBoolean(context.filterMultiple) ? context.filterMultiple : true
- column.filterMethod = context.filterMethod
- column.filterRender = context.filterRender
- column.filter = context.filter && initFilter(context.filter)
- column.treeNode = context.treeNode
- column.renderer = context.renderer
- column.editor = context.editor
- column.operationConfig = context.operationConfig
- column.equals = context.equals
+class FixedDetails {
+ isLeft: boolean
+ isLeftLast: boolean
+ isRight: boolean
+ isRightFirst: boolean
+ left: number
+ right: number
+
+ constructor(fixedType) {
+ this.isLeft = fixedType === 'left'
+ this.isLeftLast = false
+ this.isRight = fixedType === 'right'
+ this.isRightFirst = false
+ this.left = 0
+ this.right = 0
+ }
+
+ getStyle(rightExtra = 0) {
+ const { isLeft, left, isRight, right } = this
+
+ return {
+ left: isLeft ? `${left}px` : undefined,
+ right: isRight ? `${right + rightExtra}px` : undefined
+ }
+ }
+
+ getClass() {
+ const { isLeftLast, isRightFirst } = this
+
+ return {
+ 'fixed-left-last__column': isLeftLast,
+ 'fixed-right-first__column': isRightFirst
+ }
+ }
}
-function ColumnConfig(context, { renderHeader, renderCell, renderData } = {}, config = {}) {
- // 基本属性
- setBasicProperty(this, context)
- // 自定义参数
- this.params = context.params
- // 渲染属性
- this.visible = true
- this.level = 1
- this.rowSpan = 1
- this.colSpan = 1
- this.order = null
- this.renderWidth = 0 // 表格列最终的宽度,会将多种尺寸(number、%、auto)全部转化为固定的px尺寸
- this.renderHeight = 0
- this.resizeWidth = 0
- this.renderLeft = 0
- this.model = {}
- this.renderHeader = renderHeader || context.renderHeader
- this.renderCell = renderCell || context.renderCell
- this.renderData = renderData
- this.showIcon = isBoolean(context.showIcon) ? context.showIcon : true
- this.loading = false
- // 单元格插槽,只对 grid 有效
- this.slots = context.slots
- this.own = context
- this.asyncPrefix = config.constant.asyncPrefix
+class ColumnConfig {
+ constructor(context, { renderHeader, renderCell, renderData } = {}, config = {}) {
+ // 基本属性
+ this.id = `col_${++columnUniqueId}`
+ this.type = context.type
+ this.prop = context.prop
+ this.rules = context.rules
+ this.required = context.required
+ this.property = context.field || context.prop
+ this.title = context.title
+ this.label = context.label
+ this.width = context.width
+ this.minWidth = context.minWidth
+ this.resizable = context.resizable
+
+ this._fixed = context.fixed
+ this._fixedDetails = context.fixed ? new FixedDetails(context.fixed) : undefined
+
+ this.align = context.align
+ this.headerAlign = context.headerAlign
+ this.footerAlign = context.footerAlign
+ this.showOverflow = context.showOverflow
+ this.showHeaderOverflow = context.showHeaderOverflow
+ this.showTip = context.showTip
+ this.showHeaderTip = context.showHeaderTip
+ this.className = context.class || context.className
+ this.headerClassName = context.headerClassName
+ this.footerClassName = context.footerClassName
+ this.indexMethod = context.indexMethod
+ this.formatText = context.formatText
+ this.formatValue = context.formatValue
+ this.format = context.formatConfig
+ this.sortable = context.sortable
+ this.sortBy = context.sortBy
+ this.sortMethod = context.sortMethod
+ this.remoteSort = context.remoteSort
+ this.filterMultiple = isBoolean(context.filterMultiple) ? context.filterMultiple : true
+ this.filterMethod = context.filterMethod
+ this.filterRender = context.filterRender
+ this.filter = context.filter && initFilter(context.filter)
+ this.treeNode = context.treeNode
+ this.renderer = context.renderer
+ this.editor = context.editor
+ this.operationConfig = context.operationConfig
+ this.equals = context.equals
+
+ // 自定义参数
+ this.params = context.params
+
+ // 渲染属性
+ this.visible = true
+ this.level = 1
+ this.rowSpan = 1
+ this.colSpan = 1
+ this.order = null
+ this.renderWidth = 0
+ this.renderHeight = 0
+ this.resizeWidth = 0
+ this.renderLeft = 0
+ this.model = {}
+ this.renderHeader = renderHeader || context.renderHeader
+ this.renderCell = renderCell || context.renderCell
+ this.renderData = renderData
+ this.showIcon = isBoolean(context.showIcon) ? context.showIcon : true
+ this.loading = false
+
+ // 单元格插槽,只对 grid 有效
+ this.slots = context.slots
+ this.own = context
+ this.asyncPrefix = config.constant.asyncPrefix
+ }
+
+ set fixed(val) {
+ this._fixed = val
+ this._fixedDetails = val ? new FixedDetails(val) : undefined
+ }
+
+ get fixed() {
+ return this._fixed
+ }
+
+ get fixedDetails() {
+ return this._fixedDetails
+ }
}
export const getColumnConfig = (context, options, config) =>
diff --git a/packages/renderless/src/grid/utils/common.ts b/packages/renderless/src/grid/utils/common.ts
index 42c7829d9f..23264b2edb 100644
--- a/packages/renderless/src/grid/utils/common.ts
+++ b/packages/renderless/src/grid/utils/common.ts
@@ -23,9 +23,8 @@
*
*/
-import { isNull } from '@opentiny/utils'
-import { find } from '@opentiny/utils'
-import { get, isFunction, set } from '../static'
+import { isNull, find, isFunction } from '@opentiny/utils'
+import { get, set } from '../static'
export const gridSize = ['medium', 'small', 'mini']
@@ -43,20 +42,58 @@ export const getRowid = ($table, row) => {
}
// 获取所有的列,排除分组
-export const getColumnList = (columns) => {
+export const getColumnList = (columns, options = {}, level = 0) => {
const result = []
- columns.forEach((column) => {
- if (column.children && column.children.length) {
- result.push(...getColumnList(column.children))
- } else {
- result.push(column)
+ columns.forEach((column, index) => {
+ const hasChildren = column.children?.length
+
+ // 所有层级中,存在固定列配置,就认为存在固定列
+ if (!options.hasFixed && column.fixed) {
+ options.hasFixed = true
}
+
+ // 是否存在 type selection 列
+ if (!options.isCheckable && column.type === 'selection') {
+ options.isCheckable = true
+ }
+
+ // 第一层级存在子级,就认为是多级表头
+ if (level === 0 && !options.isGroup && hasChildren) {
+ options.isGroup = true
+ }
+
+ options.columnCaches.push({ colid: column.id, column, index })
+
+ result.push.apply(result, hasChildren ? getColumnList(column.children, options, level + 1) : [column])
})
return result
}
+export const repairFixed = (root) => {
+ const subtree = []
+ let fixed
+
+ const recursive = (col) => {
+ subtree.push(col)
+
+ if (!fixed && col.fixed) {
+ fixed = col.fixed
+ }
+
+ if (Array.isArray(col.children) && col.children.length > 0) {
+ col.children.forEach((col) => recursive(col))
+ }
+ }
+
+ recursive(root)
+
+ if (fixed) {
+ subtree.forEach((c) => (c.fixed = fixed))
+ }
+}
+
export const getClass = (property, params) => (property ? (isFunction(property) ? property(params) : property) : '')
export const getFilters = (filters) =>
@@ -69,19 +106,28 @@ export const getFilters = (filters) =>
}))
export const initFilter = (filter) => {
- // 改成这种方式可以让用户配置一些筛选的默认行为,如果用户不配置就采用默认的
- return {
- condition: {
- input: '',
- relation: 'equals',
- empty: null,
- type: null,
- value: []
- },
- hasFilter: false,
+ const {
+ values,
+ value: valueKey = 'value',
+ checked: checkedKey = 'checked',
+ condition,
+ enumable,
+ multi,
+ inputFilter
+ } = filter
+
+ const value: any[] = values?.filter?.((i) => i[checkedKey]).map((i) => i[valueKey]) || []
+
+ const hasChecked = values?.some?.((i) => i[checkedKey]) ?? false
+
+ const filterOptions = {
+ condition: { input: '', relation: 'equals', empty: null, type: null, value },
+ hasFilter: (inputFilter && !!condition?.input) || (enumable && multi && hasChecked) || false,
custom: null,
- ...filter
+ showClear: true
}
+
+ return { ...filterOptions, ...filter }
}
export const formatText = (value) => `${isNull(value) ? '' : value}`
@@ -182,3 +228,38 @@ export const getListeners = ($attrs, $listeners) => {
return listeners
}
+
+/** DFS深度优先遍历树形结构,并生成备份 */
+export function dfsCopy(tree, callback, parent = undefined, isTree = false, childrenKey = 'children') {
+ let copy
+
+ if (Array.isArray(tree)) {
+ copy = []
+
+ tree.forEach((node, index) => {
+ const copyItem = callback(node, index, parent)
+
+ if (copyItem) {
+ copy.push(copyItem)
+ }
+
+ if (isTree) {
+ const children = node[childrenKey]
+
+ if (children) {
+ const childrenCopy = dfsCopy(children, callback, node, isTree, childrenKey)
+
+ if (copyItem) {
+ copyItem[childrenKey] = childrenCopy
+ }
+ }
+ }
+ })
+ }
+
+ return copy
+}
+
+let rowUniqueId = 0
+
+export const getRowUniqueId = () => `row_${++rowUniqueId}`
diff --git a/packages/renderless/src/grid/utils/dom.ts b/packages/renderless/src/grid/utils/dom.ts
index f8cd4fb266..b0a61f8d56 100644
--- a/packages/renderless/src/grid/utils/dom.ts
+++ b/packages/renderless/src/grid/utils/dom.ts
@@ -36,8 +36,13 @@ 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: Event, td: HTMLElement) => {
+ const cellEl = td
+ ? td.querySelector('.tiny-grid-cell-text') || td.querySelector(CELL_CLS)
+ : (event.currentTarget as HTMLElement)?.querySelector(CELL_CLS)
+ if (!cellEl) {
+ return
+ }
const content = cellEl.innerText
if (cellEl.getAttribute('title') !== content) {
@@ -47,142 +52,89 @@ export const updateCellTitle = (event) => {
export const rowToVisible = ($table, row) => {
$table.$nextTick(() => {
- const tableBodyVnode = $table.$refs.tableBody
-
- if (tableBodyVnode) {
- const gridbodyEl = tableBodyVnode.$el
- const trEl = gridbodyEl.querySelector(`[${ATTR_NAME}="${getRowid($table, row)}"]`)
-
- // 处理虚拟滚动
- if ($table.scrollYLoad) {
- // 对应行是否在表格视图外
- const isOutOfBody = () => {
- const bodyRect = $table.$el.getBoundingClientRect()
- const trRect = trEl.getBoundingClientRect()
- return trRect.top + trRect.height / 2 > bodyRect.top + bodyRect.height
- }
-
- if (!trEl || isOutOfBody()) {
- gridbodyEl.scrollTop = ($table.afterFullData.indexOf(row) - 1) * $table.scrollYStore.rowHeight
- }
- } else if (trEl) {
- // 非虚拟滚动且有trEl元素
- const bodyHeight = gridbodyEl.clientHeight
- const bodySrcollTop = gridbodyEl.scrollTop
- const trOffsetTop = trEl.offsetTop + (trEl.offsetParent ? trEl.offsetParent.offsetTop : 0)
- const trHeight = trEl.clientHeight
-
- if (trOffsetTop < bodySrcollTop || trOffsetTop > bodySrcollTop + bodyHeight) {
- // 如果跨行滚动
- gridbodyEl.scrollTop = trOffsetTop
- } else if (trOffsetTop + trHeight >= bodyHeight + bodySrcollTop) {
- gridbodyEl.scrollTop = bodySrcollTop + trHeight
- }
- }
+ const { $refs, scrollYLoad, rowHeight, headerHeight, footerHeight, _tileInfo, _graphInfo } = $table
+ const { tableBody: bodyVm } = $refs
+ const { $el } = bodyVm
+ const { map } = _tileInfo
+ const { graphed } = _graphInfo
+ const trEl = $el.querySelector(`[${ATTR_NAME}="${getRowid($table, row)}"]`)
+ const visibleStart = headerHeight
+ const visibleEnd = $el.clientHeight - footerHeight
+ const scrollTop = $el.scrollTop
+
+ let position, trHeight
+ let flag = false
+
+ if (scrollYLoad) {
+ // 如果是虚拟渲染跨行滚动
+ position = headerHeight + rowHeight * graphed.indexOf(map.get(row)) - scrollTop
+ trHeight = rowHeight
+ flag = true
+ } else if (trEl) {
+ position = trEl.offsetTop - scrollTop
+ trHeight = trEl.clientHeight
+ flag = true
}
- })
-}
-
-function getFixedLeft($table, from, column, body, offset) {
- let scrollLeft = $table.elemStore['main-body-wrapper'].scrollLeft + offset
- if (!column.fixed) {
- from.fixed === 'left' && (scrollLeft = 0)
- from.fixed === 'right' && (scrollLeft = body.scrollWidth)
- }
+ if (flag) {
+ if (position < visibleStart) {
+ $el.scrollTop = scrollTop - (visibleStart - position)
+ return
+ }
- return scrollLeft
-}
+ position += trHeight
-// 计算水平滚动位置(考虑存在冻结表的情况)
-function computeScrollLeft($table, td) {
- const { tableBody } = $table.$refs
- const { visibleColumn } = $table
- const { scrollLeft: bodyLeft, clientWidth: bodyWidth } = tableBody.$el
- // Tiny表格冻结列采用sticky,需遍历计算整体宽度
- let leftWidth = 0
- let rightWidth = 0
- visibleColumn.forEach((column) => {
- if (column.fixed === 'left') {
- leftWidth += column.renderWidth
- } else if (column.fixed === 'right') {
- rightWidth += column.renderWidth
+ if (position > visibleEnd) {
+ $el.scrollTop = scrollTop + (position - visibleEnd)
+ }
}
})
- const tdLeft = td._accumulateRenderWidth || td.offsetLeft + (td.offsetParent ? td.offsetParent.offsetLeft : 0)
- const tdWidth = td._renderWidth || td.clientWidth
-
- let scrollLeft
-
- // 列元素在主表体可视区左侧(包括被左冻结表部分遮挡的情况)
- if (tdLeft < bodyLeft + leftWidth) {
- scrollLeft = tdLeft - leftWidth
- } else if (tdLeft + tdWidth > bodyLeft + bodyWidth - rightWidth) {
- // 列元素在主表体可视区右侧(包括被右冻结表部分遮挡的情况)
- scrollLeft = tdLeft + tdWidth - bodyWidth + rightWidth
- } else {
- // 列元素在主表体可视区内
- scrollLeft = bodyLeft
- }
-
- return scrollLeft
}
-function setBodyLeft(body, td, $table, column, move) {
- const { isLeftArrow, isRightArrow, from } = move || {}
-
- const bodyScollLeft = computeScrollLeft($table, td)
- $table.scrollTo(bodyScollLeft)
- $table.lastScrollLeft = bodyScollLeft
- if (from) {
- const direction = isLeftArrow ? 'left' : isRightArrow ? 'right' : null
- const fixedDom = $table.elemStore[`${direction}-body-list`]
- const mainBody = $table.elemStore['main-body-wrapper']
- const { left, right } = td.getBoundingClientRect()
- let offset = 0
-
- if (isLeftArrow && fixedDom) {
- const div = fixedDom.querySelector('td.fixed__column')
- const division = div ? div.getBoundingClientRect().left : fixedDom.getBoundingClientRect().right
-
- division > left && (offset = left - division)
- }
-
- if (isRightArrow && fixedDom) {
- const div = fixedDom.querySelector('td:not(.fixed__column)') || fixedDom
- const division = div.getBoundingClientRect().left
-
- division < right && (offset = right - division)
- }
-
- mainBody.scrollLeft = getFixedLeft($table, from, column, body, offset)
+export const colToVisible = ($table, column) => {
+ // 固定列始终可见,无需继续处理
+ if (column.fixed) {
+ return
}
-}
-export const colToVisible = ($table, column, move) => {
$table.$nextTick(() => {
- const gridbodyEl = $table.$refs.tableBody.$el
- const tdElem = gridbodyEl.querySelector(`.${column.id}`)
+ const { $refs, scrollXLoad, visibleColumn, columnStore } = $table
+ const { tableBody: bodyVm } = $refs
+ const { $el } = bodyVm
+ const { leftList, rightList } = columnStore
+ const tdEl = $el.querySelector(`.${column.id}`)
+ const visibleStart = leftList.reduce((p, c) => (p += c.renderWidth), 0)
+ const visibleEnd = $el.clientWidth - rightList.reduce((p, c) => (p += c.renderWidth), 0)
+ const scrollLeft = $el.scrollLeft
+ const colWidth = column.renderWidth
+
+ let position
+ let flag = false
+
+ if (scrollXLoad) {
+ flag = true
+ position = -scrollLeft
+
+ for (const col of visibleColumn) {
+ if (col === column) break
+ position += col.renderWidth
+ }
+ } else if (tdEl) {
+ flag = true
+ position = tdEl.offsetLeft - scrollLeft
+ }
- if (tdElem) {
- setBodyLeft(gridbodyEl, tdElem, $table, column, move)
- } else if ($table.scrollXLoad) {
- // 如果是虚拟渲染跨行滚动
- const visibleColumn = $table.visibleColumn
- let scrollLeft = 0
+ if (flag) {
+ if (position < visibleStart) {
+ $el.scrollLeft = scrollLeft - (visibleStart - position)
+ return
+ }
- for (let index = 0; index < visibleColumn.length; index++) {
- if (visibleColumn[index] === column) {
- break
- }
+ position += colWidth
- scrollLeft += visibleColumn[index].renderWidth
+ if (position > visibleEnd) {
+ $el.scrollLeft = scrollLeft + (position - visibleEnd)
}
-
- gridbodyEl.scrollLeft = computeScrollLeft($table, {
- _accumulateRenderWidth: scrollLeft,
- _renderWidth: column.renderWidth
- })
}
})
}
diff --git a/packages/theme-saas/src/grid/body.less b/packages/theme-saas/src/grid/body.less
index b14ed9fdd7..8d0036bbce 100644
--- a/packages/theme-saas/src/grid/body.less
+++ b/packages/theme-saas/src/grid/body.less
@@ -10,6 +10,16 @@
@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}-body__y-space {
+ @apply hidden;
+ }
+ }
}
.@{grid-prefix-cls}__borders {
diff --git a/packages/theme-saas/src/grid/header.less b/packages/theme-saas/src/grid/header.less
index 11c4876262..64565c86c0 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;
@@ -168,57 +166,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 17bcedd741..1fa33d9dda 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} {
@@ -267,23 +269,6 @@
}
}
- // tiny新增滚动条放置表格内
- tbody tr:last-child {
- @apply relative;
-
- &::after {
- @apply content-[''];
- @apply absolute;
- @apply bottom-0;
- @apply left-0;
- @apply right-0;
- @apply bottom-0;
- @apply h-px;
- @apply bg-color-bg-1;
- @apply ~'z-[3]';
- }
- }
-
&&__border,
&&__border-saas {
// 启用 border 只有表头生效,默认不建议启用 border 属性,另外如果内嵌列,则表头会自动启用 border 属性
@@ -385,7 +370,7 @@
}
&&__group-saas {
- .@{grid-prefix-cls}__header {
+ .@{grid-prefix-cls}__body thead {
@apply relative;
&::before {
@@ -431,7 +416,7 @@
}
&&__border-vertical {
- .@{grid-prefix-cls}__body {
+ .@{grid-prefix-cls}__body tbody {
@apply relative;
&::before {
@@ -687,8 +672,7 @@
& &-body__x-space {
@apply w-full;
- @apply h-px;
- @apply -mb-px;
+ @apply h-0;
}
& &-body__y-space {
@@ -878,37 +862,16 @@
}
& &__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 flex-auto;
+ @apply items-center;
@apply justify-center;
- @apply text-center;
- @apply absolute;
- @apply w-full;
-
- .@{grid-prefix-cls}__empty-text {
- @apply w-full;
- }
+ @apply sticky;
+ @apply left-0;
}
& &__empty-img {
@@ -921,8 +884,8 @@
& &__empty-text {
@apply block;
- @apply mt-2;
- @apply ~'w-1/2';
+ @apply w-full;
+ @apply text-center;
}
& &-body__column {
@@ -1029,14 +992,15 @@
& &__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;
+ @apply z-20;
.@{grid-prefix-cls}-body__y-scrollbar {
@apply w-px;
@@ -1392,4 +1356,89 @@
}
}
}
+
+ .@{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;
+ }
+ }
+ }
+ }
+
+ .sticky-wrapper {
+ @apply sticky;
+ @apply top-0;
+ @apply left-0;
+ @apply overflow-hidden;
+ @apply h-full;
+ }
}
diff --git a/packages/theme-saas/src/svgs/delegated-processing.svg b/packages/theme-saas/src/svgs/delegated-processing.svg
new file mode 100644
index 0000000000..5cb83e86d5
--- /dev/null
+++ b/packages/theme-saas/src/svgs/delegated-processing.svg
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/packages/theme-saas/src/svgs/pushpin-solid.svg b/packages/theme-saas/src/svgs/pushpin-solid.svg
index 83d23971b0..480712ad86 100644
--- a/packages/theme-saas/src/svgs/pushpin-solid.svg
+++ b/packages/theme-saas/src/svgs/pushpin-solid.svg
@@ -1,10 +1,14 @@
-
+
- Created with Pixso.
+ Created with Pixso.
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/theme-saas/src/svgs/pushpin.svg b/packages/theme-saas/src/svgs/pushpin.svg
index 64e38c2eba..1652550f0d 100644
--- a/packages/theme-saas/src/svgs/pushpin.svg
+++ b/packages/theme-saas/src/svgs/pushpin.svg
@@ -1,7 +1,14 @@
-
-
-
-
-
\ No newline at end of file
+
+
+ Created with Pixso.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/theme/src/grid/body.less b/packages/theme/src/grid/body.less
index 895293937f..ecf2cc17d1 100644
--- a/packages/theme/src/grid/body.less
+++ b/packages/theme/src/grid/body.less
@@ -21,6 +21,16 @@
.@{grid-prefix-cls}__fixed-right-body-wrapper {
overflow-y: auto;
overflow-x: auto;
+
+ &.no-data {
+ overflow-y: hidden;
+ display: flex;
+ flex-direction: column;
+
+ > .@{grid-prefix-cls}-body__y-space {
+ display: none;
+ }
+ }
}
// 鼠标配置项开启后,选中单元格的边框样式(position:absolute)
diff --git a/packages/theme/src/grid/header.less b/packages/theme/src/grid/header.less
index f28c187ba3..0c97568908 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 64f55a6c43..2ee12af0a3 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,11 +313,8 @@
.@{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:
@@ -340,7 +338,7 @@
top: 0;
width: 0;
height: 100%;
- z-index: 1;
+ z-index: 10;
}
&:before {
@@ -604,8 +602,7 @@
& &-body__x-space {
width: 100%;
- height: 1px;
- margin-bottom: -1px;
+ height: 0;
}
& &-body__y-space {
@@ -722,38 +719,16 @@
// 暂无数据
& &__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;
- flex-direction: column;
+ align-items: center;
justify-content: center;
- text-align: center;
- position: absolute;
- width: 100%;
- height: calc(100% - 60px);
-
- .@{grid-prefix-cls}__empty-text {
- width: 100%;
- }
+ position: sticky;
+ left: 0;
+ flex: auto;
+ flex-direction: column;
}
// 表格无数据背景图
@@ -766,7 +741,8 @@
& &__empty-text {
display: block;
margin-top: 8px;
- width: 50%;
+ text-align: center;
+ width: 100%;
}
// 校验不通过
@@ -879,15 +855,16 @@
& &__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;
+ z-index: 20;
.@{grid-prefix-cls}-body__y-scrollbar {
width: 1px;
@@ -1197,7 +1174,8 @@
transform: translateY(-50%);
width: 8px;
height: 14px;
- background-image: linear-gradient(
+ background-image:
+ linear-gradient(
180deg,
var(--row-drop-handle-bgcolor) 0 2px,
transparent 2px 6px,
@@ -1227,6 +1205,59 @@
}
}
}
+
+ .@{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;
+ }
+ }
+ }
+
+ .sticky-wrapper {
+ position: sticky;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ height: 100%;
+ }
}
// 表格全屏样式
diff --git a/packages/theme/src/svgs/delegated-processing.svg b/packages/theme/src/svgs/delegated-processing.svg
new file mode 100644
index 0000000000..5cb83e86d5
--- /dev/null
+++ b/packages/theme/src/svgs/delegated-processing.svg
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/packages/vue-icon-saas/index.ts b/packages/vue-icon-saas/index.ts
index dc7b2cca99..f0677fd0ec 100644
--- a/packages/vue-icon-saas/index.ts
+++ b/packages/vue-icon-saas/index.ts
@@ -90,6 +90,7 @@ import IconDeltaLeftO from './src/delta-left-o'
import IconDeltaLeft from './src/delta-left'
import IconDeltaRightO from './src/delta-right-o'
import IconDeltaRight from './src/delta-right'
+import IconDelegatedProcessing from './src/delegated-processing'
import IconDeltaUpO from './src/delta-up-o'
import IconDeltaUp from './src/delta-up'
import IconDerive from './src/derive'
@@ -857,6 +858,8 @@ export {
IconDeltaRightO as iconDeltaRightO,
IconDeltaRight,
IconDeltaRight as iconDeltaRight,
+ IconDelegatedProcessing,
+ IconDelegatedProcessing as iconDelegatedProcessing,
IconDeltaUpO,
IconDeltaUpO as iconDeltaUpO,
IconDeltaUp,
@@ -1740,6 +1743,7 @@ export default {
IconDeltaLeft,
IconDeltaRightO,
IconDeltaRight,
+ IconDelegatedProcessing,
IconDeltaUpO,
IconDeltaUp,
IconDerive,
diff --git a/packages/vue-icon/index.ts b/packages/vue-icon/index.ts
index dc7b2cca99..f0677fd0ec 100644
--- a/packages/vue-icon/index.ts
+++ b/packages/vue-icon/index.ts
@@ -90,6 +90,7 @@ import IconDeltaLeftO from './src/delta-left-o'
import IconDeltaLeft from './src/delta-left'
import IconDeltaRightO from './src/delta-right-o'
import IconDeltaRight from './src/delta-right'
+import IconDelegatedProcessing from './src/delegated-processing'
import IconDeltaUpO from './src/delta-up-o'
import IconDeltaUp from './src/delta-up'
import IconDerive from './src/derive'
@@ -857,6 +858,8 @@ export {
IconDeltaRightO as iconDeltaRightO,
IconDeltaRight,
IconDeltaRight as iconDeltaRight,
+ IconDelegatedProcessing,
+ IconDelegatedProcessing as iconDelegatedProcessing,
IconDeltaUpO,
IconDeltaUpO as iconDeltaUpO,
IconDeltaUp,
@@ -1740,6 +1743,7 @@ export default {
IconDeltaLeft,
IconDeltaRightO,
IconDeltaRight,
+ IconDelegatedProcessing,
IconDeltaUpO,
IconDeltaUp,
IconDerive,
diff --git a/packages/vue-icon/src/delegated-processing/index.ts b/packages/vue-icon/src/delegated-processing/index.ts
new file mode 100644
index 0000000000..a1143fd542
--- /dev/null
+++ b/packages/vue-icon/src/delegated-processing/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
+ *
+ * Use of this source code is governed by an MIT-style license.
+ *
+ * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
+ * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
+ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
+ *
+ */
+import { svg } from '@opentiny/vue-common'
+import DelegatedProcessing from '@opentiny/vue-theme/svgs/delegated-processing.svg'
+
+export default () => svg({ name: 'IconDelegatedProcessing', component: DelegatedProcessing })()
diff --git a/packages/vue/src/grid-toolbar/src/index.ts b/packages/vue/src/grid-toolbar/src/index.ts
index 3e43b1b197..96fb3e4987 100644
--- a/packages/vue/src/grid-toolbar/src/index.ts
+++ b/packages/vue/src/grid-toolbar/src/index.ts
@@ -555,7 +555,7 @@ export default defineComponent({
if (comp) {
const colWidth = this.loadColWidth()
- comp.reloadCustoms(customs, sort, colWidth).then((fullColumn) => {
+ comp.reloadCustoms(customs, sort, colWidth)?.then((fullColumn) => {
this.tableFullColumn = fullColumn
})
}
@@ -670,13 +670,7 @@ export default defineComponent({
if (this.$grid) {
if (columns && columns.length) {
const colWidth = this.loadColWidth()
- this.$grid.reloadCustoms(columns, sort, colWidth).then(() => {
- // 处理表格数据,否则列排序不生效
- this.$grid.handleTableData(true).then(() => {
- // 重新计算内部元素的位置
- this.$grid.recalculate()
- })
- })
+ this.$grid.reloadCustoms(columns, sort, colWidth)
}
if (isNumber(pageSize) && this.$grid.pagerConfig && this.$grid.pagerConfig.pageSize !== pageSize) {
diff --git a/packages/vue/src/grid/index.ts b/packages/vue/src/grid/index.ts
index d0799e53cc..3084806403 100644
--- a/packages/vue/src/grid/index.ts
+++ b/packages/vue/src/grid/index.ts
@@ -86,7 +86,7 @@ const getWrapFunc = (name) =>
function (...args) {
const tinyTable = this.$refs.tinyTable
if (tinyTable) {
- return this.$refs.tinyTable[name].apply(tinyTable, args)
+ return tinyTable[name]?.apply(tinyTable, args)
}
}
diff --git a/packages/vue/src/grid/src/adapter/src/renderer.ts b/packages/vue/src/grid/src/adapter/src/renderer.ts
index 776a7ea3bc..9d82d35881 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'
function getAttrs({ name, attrs }, params) {
@@ -68,15 +68,18 @@ function getEvents(renderOpts, params, context) {
[type](event) {
let cellValue = native ? event.target.value : event
- if (!renderOpts.isValidAlways && isSyncCell(renderOpts, params, context)) {
- setCellValue(row, column, cellValue)
- } else {
- 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))
change && change.apply(null, [params].concat.apply(params, arguments))
@@ -195,7 +198,10 @@ function defaultFilterMethod({ option, row, column }) {
}
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)
}
@@ -221,8 +227,12 @@ function defaultEditRender(h, renderOpts, params, context) {
let editorModel = component.model || {}
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}` : '',
attrs: {
diff --git a/packages/vue/src/grid/src/body/src/body.tsx b/packages/vue/src/grid/src/body/src/body.tsx
index 98bf54c949..8eff55dc31 100644
--- a/packages/vue/src/grid/src/body/src/body.tsx
+++ b/packages/vue/src/grid/src/body/src/body.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable unused-imports/no-unused-vars */
/**
* MIT License
*
@@ -24,494 +23,515 @@
*
*/
-import { isFunction, find } from '@opentiny/vue-renderless/grid/static/'
-import { isNull } from '@opentiny/utils'
+import { isFunction, find, isBoolean } from '@opentiny/vue-renderless/grid/static/'
+import { removeClass, addClass, isObject, throttle } from '@opentiny/utils'
import {
- updateCellTitle,
emitEvent,
getClass,
getFuncText,
getRowid,
- formatText
+ formatText,
+ getOffsetPos
} from '@opentiny/vue-renderless/grid/utils'
import { getCellLabel } from '../../tools'
import GlobalConfig from '../../config'
-import { iconChevronRight, iconChevronDown, iconGridNoData } from '@opentiny/vue-icon'
+import { iconGridNoData, iconChevronRight, iconChevronDown } from '@opentiny/vue-icon'
import { h, hooks, $prefix, defineComponent } from '@opentiny/vue-common'
-import { getTreeChildrenKey, getTreeShowKey, handleRowGroupFold, isVirtualRow } from '../../table/src/strategy'
-import { generateFixedClassName } from '../../table/src/utils/handleFixedColumn'
+import { handleRowGroupFold, isVirtualRow, getFixedStyle, getFixedClass } from '../../table/src/strategy'
+import { usePool } from './usePool'
+import { getConfigOverflow, useCellEvent, useCellSpan, useHeader } from '../../composable'
+
+const ChevronRight = iconChevronRight()
+const ChevronDown = iconChevronDown()
const GridNoData = iconGridNoData()
-// 滚动、拖动过程中不需要触发鼠标移入移出事件
-const isOperateMouse = ($table) =>
- $table._isResize || ($table.lastScrollTime && Date.now() < $table.lastScrollTime + $table.optimizeOpts.delayHover)
-
-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'
-}
-const renderBorder = (h, type) => {
- let vnTop = h('span', {
- class: 'tiny-grid-border-top',
- ref: `${type}Top`
- })
- let vnRight = h('span', {
- class: 'tiny-grid-border-right',
- ref: `${type}Right`
- })
- let vnBottom = h('span', {
- class: 'tiny-grid-border-bottom',
- ref: `${type}Bottom`
- })
- let vnLeft = h('span', {
- class: 'tiny-grid-border-left',
- ref: `${type}Left`
- })
+let renderRowFlag = true
- return h(
- 'div',
- {
- class: `tiny-grid-${type}ed-borders`,
- ref: `${type}Borders`
- },
- [vnTop, vnRight, vnBottom, vnLeft]
+const renderBorder = (type) => {
+ return (
+
+
+
+
+
+
)
}
-function buildColumnProps(args) {
- const { attrs, cellAlign, cellClassName, className, column, columnActived, columnIndex, columnKey, editor } = args
- const { fixedHiddenColumn, hasEllipsis, isDirty, params, tdOns, validError, validated, columnStore } = args
-
- const { leftList, rightList } = columnStore
-
- return {
- class: [
- 'tiny-grid-body__column',
- 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__valid-error': validError && validated,
- 'col__valid-success': columnActived ? !validError && !validated : isDirty && !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
- },
- getClass(className, params),
- getClass(cellClassName, params)
- ],
- style: fixedHiddenColumn
- ? {
- left: `${column.style?.left}px`,
- right: `${column.style?.right}px`
- }
- : null,
- key: columnKey ? column.id : columnIndex,
- attrs,
- on: tdOns
- }
-}
+/** 渲染列 */
+function renderColumn({ $columnIndex, $table, _vm, column, id, row, rowid, seq, used }) {
+ const { align: allAlign, cellClassName, dropConfig = {}, editConfig, editRules, editStore } = $table
+ const { height, rowId, scrollXLoad, scrollYLoad, validOpts, validStore, validatedMap } = $table
+ const { normalRows } = _vm
+ const { attrs = { rowspan: 1, colspan: 1, visible: true }, params = { $table, row, column } } =
+ normalRows[rowid]?.[column.id] || {}
+ const { isMessageDefault, isMessageInline } = validOpts
+ const { actived } = editStore
+ const validated = validatedMap[column.id + '-' + row[rowId]]
+ const validError = validStore.row === row && validStore.column === column
+ const hasDefaultTip = editRules && (isMessageDefault ? height || !_vm.isNoData : isMessageInline)
+ const { align, className, editor } = column
-function buildColumnChildren(args) {
- let { h, hasDefaultTip, params, row, validError, column, $table } = args
- let { showEllipsis, showTip, showTitle, showTooltip, validStore } = args
- const dropConfig = args.dropConfig || {}
- const { validOpts } = $table
- let cellNode: any[] = []
- let validNode: any = null
- if (hasDefaultTip) {
- validNode = [null]
- if (validError) {
- validNode = h(
- 'div',
- {
- class: 'tiny-grid-cell__valid',
- style: validStore.rule && validStore.rule.width ? { width: `${validStore.rule.width}px` } : null
- },
- [
- validOpts?.icon ? h(validOpts.icon, { class: 'tiny-grid-cell__valid-icon' }) : null,
- h('span', { class: 'tiny-grid-cell__valid-msg', attrs: { title: validStore.content } }, validStore.content)
- ]
- )
- }
- }
- cellNode = [
- dropConfig.rowHandle === 'index' && column.type === 'index' ? h('div', { class: 'row__drop-handle' }) : null,
- h(
- 'div',
- {
- class: [
- 'tiny-grid-cell',
- {
- 'tiny-grid-cell__title': showTitle,
- 'tiny-grid-cell__tooltip': showTooltip || showTip,
- 'tiny-grid-cell__ellipsis': showEllipsis
- }
- ],
- attrs: { title: showTitle ? getCellLabel(row, column, params) : null }
- },
- // 调用column组件的renderCell渲染单元格内部的内容
- // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容
- $table.isShapeTable ? column.renderCell(h, params) : null
- ),
- validNode
- ]
- return cellNode
-}
+ let cellAlign = align || allAlign
-function modifyCellAlign({ cellAlign, column }) {
- if (~['radio', 'selection', 'index'].indexOf(column.type)) {
+ // 索引列、选择列如果不配置对齐方式则默认为居中对齐
+ if (['radio', 'selection', 'index'].includes(column.type)) {
cellAlign = cellAlign || 'center'
}
- return cellAlign
-}
+ let { cellTip, cellOverflowTitle, cellOverflowTooltip, cellOverflowEllipsis, cellOverflowHint } = getConfigOverflow(
+ column,
+ $table
+ )
-function modifyShowEllipsis({ hasEllipsis, scrollXLoad, scrollYLoad, showEllipsis }) {
- if ((scrollXLoad || scrollYLoad) && !hasEllipsis) {
- showEllipsis = true
+ // 滚动的渲染不支持动态行高
+ if ((scrollXLoad || scrollYLoad) && !cellOverflowHint) {
+ cellOverflowEllipsis = true
}
- return showEllipsis
-}
-
-function addListenerMouseenter({ $table, evntParams, showTip, showTitle, showTooltip, tableListeners, tdOns }) {
- if (showTip || showTitle || showTooltip || tableListeners['cell-mouseenter']) {
- tdOns.mouseenter = (event) => {
- if (isOperateMouse($table)) {
- return
- }
-
- evntParams.cell = event.currentTarget
-
- if (showTitle) {
- updateCellTitle(event)
- } else if (showTip || showTooltip) {
- // 如果配置了显示 tooltip
- $table.triggerTooltipEvent(event, evntParams)
- }
+ const columnActived =
+ editConfig && editor && actived.row === row && (actived.column === column || editConfig.mode === 'row')
- emitEvent($table, 'cell-mouseenter', [evntParams, event])
- }
- }
+ // 如果显示状态
+ const isDirty = $table.getCellStatus(row, column).isDirty
+
+ params.$columnIndex = $columnIndex
+
+ return (
+ 1 ? attrs.rowspan : undefined}
+ colspan={attrs.colspan > 1 ? attrs.colspan : undefined}
+ class={[
+ 'tiny-grid-body__column',
+ column.id,
+ {
+ [`col__${cellAlign}`]: cellAlign,
+ 'col__edit': editor,
+ 'col__index': column.type === 'index',
+ 'col__radio': column.type === 'radio',
+ 'col__selection': column.type === 'selection',
+ 'col__ellipsis': cellOverflowHint,
+ 'edit__visible': editor && editor.type === 'visible',
+ 'col__dirty': isDirty,
+ 'col__actived': columnActived,
+ 'col__valid-error': validError && validated,
+ 'col__valid-success': columnActived ? !validError && !validated : isDirty && !validated,
+ 'col__treenode': column.treeNode
+ },
+ getClass(className, params),
+ getClass(cellClassName, params),
+ getFixedClass(column, $table),
+ attrs._stickyClass || ''
+ ]}>
+ {[
+ // 行拖拽手柄
+ dropConfig.rowHandle === 'index' && column.type === 'index' ?
: null,
+ // 单元格主内容
+ // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容
+
+ {$table.isShapeTable ? column.renderCell(h, params) : null}
+
,
+ // 行内校验
+ hasDefaultTip && validError ? (
+
+
+ {validStore.content}
+
+
+ ) : null
+ ]}
+
+ )
}
-function addListenerMouseleave({ $table, evntParams, showTip, showTooltip, tableListeners, tdOns }) {
- if (showTip || showTooltip || tableListeners['cell-mouseleave']) {
- tdOns.mouseleave = (event) => {
- if (isOperateMouse($table)) {
- return
- }
-
- if (showTip || showTooltip) {
- $table.clostTooltip()
- }
+function renderHeaderRows(_vm: any): any {
+ const { $parent: $table, headerTable } = _vm
+ const { headerCellClassName, headerRowClassName, headerSuffixIconAbsolute } = $table
+ const { align: allAlign, border, headerAlign: allHeaderAlign, resizable } = $table
+ const { editConfig, operationColumnResizable, mouseConfig = {}, dropConfig = {} } = $table
+ return headerTable.map((cols, $rowIndex) => {
+ return (
+
+ {cols.map(({ id, column, colspan, rowspan, height, top }, $columnIndex) => {
+ const isColGroup = column.children?.length
+ const { headerAlign, align, headerClassName } = column
+ const { headerTip, headerOverflowTitle, headerOverflowTooltip, headerOverflowEllipsis, headerOverflowHint } =
+ getConfigOverflow(column, $table)
+ const columnIndex = $table.getColumnIndex(column)
+ const params = { $table, $rowIndex, column, columnIndex, $columnIndex, isHidden: false }
+ const isColResize = ['index', 'radio', 'selection'].includes(column.type) ? operationColumnResizable : true
+
+ let headAlign = headerAlign || align || allHeaderAlign || allAlign
+
+ if (['radio', 'selection', 'index'].includes(column.type)) {
+ headAlign = headAlign || 'center'
+ }
- evntParams.cell = event.currentTarget
+ return (
+ 1 ? colspan : undefined}
+ rowspan={rowspan > 1 ? rowspan : undefined}
+ style={[
+ getFixedStyle(column, $table),
+ { height: rowspan > 1 ? `${height}px` : undefined, top: `${top}px`, zIndex: column.fixed ? 20 : 10 }
+ ]}
+ class={[
+ 'tiny-grid-header__column',
+ column.id,
+ {
+ [`col__${headAlign}`]: headAlign,
+ 'col__fixed': column.fixed,
+ 'col__index': column.type === 'index',
+ 'col__radio': column.type === 'radio',
+ 'col__selection': column.type === 'selection',
+ 'col__group': isColGroup,
+ 'col__ellipsis': headerOverflowHint,
+ 'is__sortable': !['index', 'radio', 'selection'].includes(column.type) && column.sortable,
+ 'is__editable': column.editor,
+ 'is__filter': isObject(column.filter),
+ 'filter__active': column.filter && column.filter.hasFilter,
+ 'is__multilevel': rowspan > 1
+ },
+ getClass(headerClassName, params),
+ getClass(headerCellClassName, params),
+ getFixedClass(column, $table)
+ ]}>
+ {[
+ !isColGroup &&
+ !(isBoolean(column.resizable) ? column.resizable : resizable) &&
+ column.type !== 'index' ? (
+
+ ) : null,
+ // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容
+
+ {$table.isShapeTable ? column.renderHeader(h, params) : null}
+
,
+ mouseConfig.checked && dropConfig.column && !column.type && !column.fixed ? (
+ event.stopPropagation()} />
+ ) : null,
+ !isColGroup && isColResize && (isBoolean(column.resizable) ? column.resizable : resizable) ? (
+
_vm.resizeMousedown(event, params)}
+ />
+ ) : null
+ ]}
+
+ )
+ })}
+
+ )
+ })
+}
- emitEvent($table, 'cell-mouseleave', [evntParams, event])
- }
- }
+function renderFooterRows(_vm: any): any {
+ const { $parent: $table, footerData, columnPool, rowHeight, footerRows } = _vm
+ const { align: allAlign, footerAlign: allFooterAlign, footerCellClassName, footerRowClassName } = $table
+ const footerDataLength = footerData.length
+
+ return footerData.map((list, $rowIndex) => {
+ const trBottom = (footerDataLength - 1 - $rowIndex) * rowHeight
+
+ return (
+
+ {columnPool.map(({ id, item: column, used }, $columnIndex) => {
+ const { footerAlign, align, footerClassName } = column
+ const ftAlign = footerAlign || align || allFooterAlign || allAlign
+ const { cellOverflowHint } = getConfigOverflow(column, $table)
+ const { attrs = { rowspan: 1, colspan: 1, visible: true }, params = { $table, column } } =
+ footerRows[$rowIndex]?.[column.id] || {}
+ const rowspan = attrs?.rowspan || 1
+ const tdBottom = trBottom - (rowspan > 1 ? (rowspan - 1) * rowHeight : 0)
+
+ params.$columnIndex = $columnIndex
+
+ return (
+ 1 ? attrs.rowspan : undefined}
+ colspan={attrs.colspan > 1 ? attrs.colspan : undefined}
+ style={[
+ getFixedStyle(column, $table),
+ attrs._stickyStyle,
+ {
+ bottom: `${tdBottom}px`,
+ zIndex: column.fixed ? 20 : 10,
+ display: used && attrs.visible ? undefined : 'none'
+ }
+ ]}
+ class={[
+ 'tiny-grid-footer__column',
+ column.id,
+ {
+ [`col__${ftAlign}`]: ftAlign,
+ 'col__ellipsis': cellOverflowHint,
+ 'filter__active': column.filter && column.filter.hasFilter
+ },
+ getClass(footerClassName, params),
+ getClass(footerCellClassName, params),
+ getFixedClass(column, $table),
+ attrs._stickyClass || ''
+ ]}>
+
+ {$table.isShapeTable ? formatText(list[$columnIndex]) : null}
+
+
+ )
+ })}
+
+ )
+ })
}
-function addListenerMousedown({ $table, evntParams, mouseConfig, tdOns }) {
- if (mouseConfig.checked || mouseConfig.selected) {
- tdOns.mousedown = (event) => {
- evntParams.cell = event.currentTarget
- $table.triggerCellMousedownEvent(event, evntParams)
+function renderRows(_vm) {
+ const { $parent: $table, tableColumn, rowPool } = _vm
+ const { afterFullData, editConfig, editStore, expandConfig = {}, expandeds, hasVirtualRow } = $table
+ const { rowClassName, rowGroup, scrollYLoad, scrollYStore, selection, treeConfig, treeOrdered } = $table
+ const expandMethod = expandConfig.activeMethod
+ const startIndex = scrollYStore.startIndex
+ const isOrdered = treeConfig ? !!treeOrdered : false
+ const { hideMethod } = treeConfig || {}
+ const { actived } = editStore
+ const rows = []
+ const seqCount = { value: 0 }
+ const $seq = ''
+
+ rowPool.forEach(({ id, item: { payload: row, level: rowLevel }, used }, $rowIndex) => {
+ const rowActived = editConfig && actived.row === row
+ const virtualRow = isVirtualRow(row)
+ const isSkipRowRender = (hideMethod && hideMethod(row, rowLevel)) || virtualRow
+ const rowid = getRowid($table, row)
+ const rowIndex = $table.getRowIndex(row)
+
+ if (!isSkipRowRender) {
+ seqCount.value = seqCount.value + 1
}
- }
-}
-function addListenerClick(args) {
- let { $table, column, editConfig, editor, evntParams, expandConfig, highlightCurrentRow } = args
- let { mouseConfig, radioConfig, selectConfig, tableListeners, tdOns, treeConfig } = args
- let satisfy = (equal, trigger) => trigger === 'row' || (equal(column) && trigger === 'cell')
+ let seq = isOrdered ? seqCount.value : $rowIndex + 1
- if (
- highlightCurrentRow ||
- tableListeners['cell-click'] ||
- mouseConfig.checked ||
- (editor && editConfig) ||
- satisfy(() => true, expandConfig.trigger) ||
- satisfy(({ type }) => type === 'radio', radioConfig.trigger) ||
- satisfy(({ type }) => type === 'selection', selectConfig.trigger) ||
- satisfy(({ treeNode }) => treeNode, treeConfig.trigger)
- ) {
- tdOns.click = (event) => {
- evntParams.cell = event.currentTarget
- $table.triggerCellClickEvent(event, evntParams)
+ if (scrollYLoad) {
+ seq += startIndex
}
- }
-}
-function getRowSpanMethod(rowSpan) {
- return ({ row, $rowIndex, column, data }) => {
- let fields = []
-
- if (rowSpan) {
- rowSpan.forEach((item) => {
- column.visible && fields.push(item.field)
- })
+ // 分组表场景正常数据行的序号由在afterFullData中的位置提供
+ if (hasVirtualRow && !virtualRow) {
+ seq = afterFullData.indexOf(row) + 1
}
- let cellVal = row[column.property]
+ renderRowGroupData({ $table, _vm, id, row, rowGroup, rowid, rows, used, virtualRow })
- if (cellVal && ~fields.indexOf(column.property)) {
- let prevSiblingRow = data[$rowIndex - 1]
- let nextSiblingRow = data[$rowIndex + 1]
+ let args = { $rowIndex, $seq, $table, _vm, editStore, id, isSkipRowRender, row, rowActived, rowClassName }
- if (prevSiblingRow && prevSiblingRow[column.property] === cellVal) {
- return { rowspan: 0, colspan: 0 }
- } else {
- let rowspanCount = 1
+ Object.assign(args, { rowIndex, rowLevel, rowid, rows, selection, seq, treeConfig, used })
- while (nextSiblingRow && nextSiblingRow[column.property] === cellVal) {
- nextSiblingRow = data[++rowspanCount + $rowIndex]
- }
+ renderRow(args)
- if (rowspanCount > 1) {
- return { rowspan: rowspanCount, colspan: 1 }
- }
- }
- }
- }
-}
+ renderRowAfter({ $table, _vm, id, row, rowIndex, rows, used })
-function addListenerDblclick({ $table, evntParams, tableListeners, tdOns, triggerDblclick }) {
- if (triggerDblclick || tableListeners['cell-dblclick']) {
- tdOns.dblclick = (event) => {
- evntParams.cell = event.currentTarget
- $table.triggerCellDBLClickEvent(event, evntParams)
- }
- }
+ args = { $table, expandMethod, expandeds, id, row, rowIndex, rowLevel, rows, seq, tableColumn, treeConfig, used }
+
+ // 如果行被展开了
+ renderRowExpanded(args)
+ })
+ renderRowFlag = !renderRowFlag
+ return rows
}
-function doSpan({ attrs, params, rowSpan, spanMethod }) {
- const rowSpanMethod = getRowSpanMethod(rowSpan)
+function renderRowExpanded(args) {
+ const { $table, expandMethod, expandeds, id, row, rowIndex } = args
+ const { rowLevel, rows, seq, tableColumn, treeConfig, used } = args
- if (spanMethod || rowSpan) {
- let { rowspan = 1, colspan = 1 } = (spanMethod ? spanMethod(params) : rowSpanMethod(params)) || {}
+ if (
+ expandeds.length &&
+ expandeds.includes(row) &&
+ (typeof expandMethod === 'function' ? expandMethod(row, rowLevel) : true)
+ ) {
+ const column = find(tableColumn, (column) => column.type === 'expand')
+ const columnIndex = $table.getColumnIndex(column)
+ let cellStyle
- if (!rowspan || !colspan) {
- return false
+ if (treeConfig) {
+ cellStyle = { paddingLeft: `${rowLevel * (treeConfig.indent || 16) + 30}px` }
}
- attrs.rowspan = rowspan
- attrs.colspan = colspan
- }
-
- return true
-}
-
-function isCellDirty({ $table, column, editConfig, isDirty, row }) {
- const { showStatus = false, relationFields = true } = editConfig || {}
- // 关联字段配置为true,或者配置包含当前字段时,支持脏数据检查
- const canChange =
- relationFields === true || (Array.isArray(relationFields) && relationFields.includes(column.property))
+ if (column) {
+ const options = { $table, seq, row, rowIndex, column, columnIndex, level: rowLevel }
- if (editConfig && showStatus && column.property && (column.editor || (relationFields && canChange))) {
- isDirty = $table.hasRowChange(row, column.property)
+ rows.push(
+
+
+
+ {column.renderData(h, options)}
+
+
+
+ )
+ }
}
-
- return isDirty
}
-const setColumnEvents = (args1) => {
- let { $columnIndex, $rowIndex, $table, column, columnIndex } = args1
- let { row, rowIndex, rowLevel, seq } = args1
- let { editConfig, expandConfig = {} } = $table
- let { radioConfig = {}, showOverflow: allColumnOverflow } = $table
- let { highlightCurrentRow, mouseConfig = {} } = $table
- let { scrollXLoad, scrollYLoad, selectConfig = {} } = $table
- let { tableListeners, treeConfig = {} } = $table
- let tdOns = {}
- let fixedHiddenColumn = column.fixed
- let { editor, showOverflow, showTip } = column
- let cellOverflow = isNull(showOverflow) ? allColumnOverflow : showOverflow
- let showTitle = cellOverflow === 'title'
- let showTooltip = cellOverflow === true || cellOverflow === 'tooltip'
- let showEllipsis = cellOverflow === 'ellipsis'
- let hasEllipsis = showTitle || showTooltip || showEllipsis
- let triggerDblclick = editor && editConfig && editConfig.trigger === 'dblclick'
-
- let commonParams = { $columnIndex, $rowIndex, $table, column, columnIndex }
- Object.assign(commonParams, { isHidden: fixedHiddenColumn, level: rowLevel, row, rowIndex, seq })
-
- let evntParams = { showTip, ...commonParams }
- // 滚动的渲染不支持动态行高
- showEllipsis = modifyShowEllipsis({ hasEllipsis, scrollXLoad, scrollYLoad, showEllipsis })
- // 单元格hover 进入事件
- addListenerMouseenter({ $table, evntParams, showTip, showTitle, showTooltip, tableListeners, tdOns })
- // 单元格hover 退出事件
- addListenerMouseleave({ $table, evntParams, showTip, showTooltip, tableListeners, tdOns })
- // 按下事件处理
- addListenerMousedown({ $table, evntParams, mouseConfig, tdOns })
-
- let args = { $table, column, editConfig, editor, evntParams, expandConfig, highlightCurrentRow }
- Object.assign(args, { mouseConfig, radioConfig, selectConfig, tableListeners, tdOns, treeConfig })
- // 点击事件处理
- addListenerClick(args)
- // 双击事件处理
- addListenerDblclick({ $table, evntParams, tableListeners, tdOns, triggerDblclick })
-
- return {
- commonParams,
- args,
- cellOverflow,
- showTitle,
- showTooltip,
- showEllipsis,
- hasEllipsis,
- tdOns,
- fixedHiddenColumn
- }
+function renderRowAfter({ $table, _vm, row, rowIndex, rows, id, used }) {
+ typeof $table.renderRowAfter === 'function' &&
+ $table.renderRowAfter.call($table, { rows, row, data: _vm.tableData, rowIndex, renderColumn, id, used }, h)
}
-// 渲染列
-function renderColumn(args1) {
- let { $seq, $table, column, columnIndex } = args1
- let { h, row } = args1
- let { align: allAlign, cellClassName, columnKey, editConfig } = $table
- let { editRules, editStore, rowId, rowSpan, height } = $table
- let { tableData, validOpts, validStore, validatedMap, spanMethod, columnStore, dropConfig = {} } = $table
- let { isDirty, attrs = { 'data-colid': column.id } } = {}
- let { isMessageDefault, isMessageInline } = validOpts
- let { actived } = editStore
- let validated = validatedMap[`${column.id}-${row[rowId]}`]
- let validError = validStore.row === row && validStore.column === column
- let hasDefaultTip = editRules && (isMessageDefault ? height || tableData.length > 1 : isMessageInline)
- let { align, editor, showTip } = column
- const className = column.own.className
- let cellAlign = align || allAlign
- let columnActived =
- editConfig && editor && actived.row === row && (actived.column === column || editConfig.mode === 'row')
-
- let {
- commonParams,
- args,
- showTitle,
- showTooltip,
- showEllipsis,
- tdOns = {},
- hasEllipsis,
- fixedHiddenColumn
- } = setColumnEvents(args1)
- let params = { $seq, data: tableData, ...commonParams }
- // 索引列、选择列如果不配置对齐方式则默认为居中对齐
- cellAlign = modifyCellAlign({ cellAlign, column })
+function renderRow(args) {
+ const { $rowIndex, $seq, $table, _vm, editStore, id, isSkipRowRender, row, rowActived, rowClassName } = args
+ const { rowIndex, rowLevel, rowid, rows, selection, seq, treeConfig, used } = args
- // 合并行或列
- if (!doSpan({ attrs, params, rowSpan, spanMethod })) {
+ if (isSkipRowRender) {
return
}
- // 编辑后的显示状态(是否该单元格数据被更改)此处如果是树表大数据虚拟滚动+表格编辑器,会造成卡顿,这里需要递归树表数据
- isDirty = isCellDirty({ $table, column, editConfig, isDirty, row })
- args = {
- attrs,
- cellAlign,
- cellClassName,
- className,
- column,
- columnActived,
- columnIndex,
- columnKey,
- editor,
- columnStore
- }
- Object.assign(args, { fixedHiddenColumn, hasEllipsis, isDirty, params, tdOns, validError, validated })
- // 组装渲染单元格td所需要的props属性
- const colProps = buildColumnProps(args)
- args = { column, h, hasDefaultTip, params, row, $table }
- Object.assign(args, { showEllipsis, showTip, showTitle, showTooltip, validError, validStore, dropConfig })
+ let key = id
+ if (row._isDraging) {
+ // 防止数据多次刷新导致key回归rowid
+ _vm.$nextTick(() => {
+ delete row._isDraging
+ })
+ if (renderRowFlag) {
+ key = `drag_${key}`
+ }
+ }
- // 渲染td单元格中的div元素(自定义渲染和编辑器)
- const colChildren = buildColumnChildren(args)
+ const { columnPool } = _vm
- return h('td', colProps, colChildren)
+ rows.push(
+
+ {columnPool.map(({ id, item: column, used }, $columnIndex) =>
+ renderColumn({ $columnIndex, $table, _vm, column, id, row, rowid, seq, used })
+ )}
+
+ )
}
-function renderRowGroupTds(args) {
- const { $table, closeable, currentIcon, render, renderGroupCell } = args
- const { row, tableColumn, tds, title } = args
+function renderRowGroupTds({ $table, closeable, render, renderGroupCell, row, tds, title, _vm }) {
const targetColumn = $table._rowGroupTargetColumn
const value = row.value || ''
+ const { columnPool } = _vm
- for (let index in tableColumn) {
- if (Object.prototype.hasOwnProperty.call(tableColumn, index)) {
- const column = tableColumn[index]
- const columnIndex = $table.getColumnIndex(column)
- const header = title || formatText(getFuncText(column.title), 1) || value
- const params = { value, header, children: row.children, expand: !row.fold, row, column, columnIndex }
-
- // 不渲染colspan小于等于0的列
- if (column._rowGroupColspan <= 0) {
- continue
- }
- if (column === targetColumn) {
- let groupTitleVNode
-
- if (render) {
- groupTitleVNode = render(h, params)
- } else {
- groupTitleVNode = [
- {header} ,
- `:${value}`,
- {row.children.length}
- ]
- }
- tds.push(
-
- {[closeable ? currentIcon : null].concat(groupTitleVNode)}
-
- )
- } else {
- tds.push(
-
- {renderGroupCell ? renderGroupCell(h, params) : null}
-
- )
- }
- }
+ for (let { id, item: column, used } of columnPool) {
+ const columnIndex = $table.getColumnIndex(column)
+ const header = title || formatText(getFuncText(column.title), 1) || value
+ const params = { value, header, children: row.children, expand: !row.fold, row, column, columnIndex }
+ const isTarget = column === targetColumn
+
+ tds.push(
+ 0 ? undefined : 'none' }]}
+ class={[
+ 'tiny-grid-body__column',
+ isTarget ? 'td-group' : 'td-placeholder',
+ column.id,
+ getFixedClass(column, $table),
+ column._stickyClass || ''
+ ]}
+ colspan={column._rowGroupColspan}
+ data-colid={column.id}>
+
+ {isTarget
+ ? [
+ closeable ? (
+ row.fold ? (
+
+ ) : (
+
+ )
+ ) : null
+ ].concat(
+ render
+ ? render(h, params)
+ : [
+ {header} ,
+ ':' + value,
+ {row.children.length}
+ ]
+ )
+ : renderGroupCell
+ ? renderGroupCell(h, params)
+ : null}
+
+
+ )
}
}
-function renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, tableColumn }) {
- if (!virtualRow) {
- return
- }
+function renderRowGroupData({ $table, _vm, id, row, rowGroup, rowid, rows, used, virtualRow }) {
+ if (!virtualRow) return
const { title, closeable = true, render, renderGroupCell, className } = rowGroup
- const { tds = [], ChevronRight = iconChevronRight(), ChevronDown = iconChevronDown() } = {}
- const currentIcon = row.fold ? :
- const args = { $table, closeable, currentIcon, render, renderGroupCell }
- Object.assign(args, { row, tableColumn, tds, title })
- // 将分组行的td添加到tds数组中
- renderRowGroupTds(args)
+ const tds = []
+
+ renderRowGroupTds({ $table, closeable, render, renderGroupCell, row, tds, title, _vm })
const onClick = (event) => {
handleRowGroupFold(row, $table)
@@ -523,7 +543,9 @@ function renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, ta
rows.push(
(row.hover = false)}
onMouseover={() => (row.hover = true)}
@@ -533,449 +555,439 @@ function renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, ta
)
}
-function renderRow(args) {
- let { $rowIndex, $seq, $table, _vm, editStore } = args
- let { h, row, rowActived } = args
- let { rowClassName, rowIndex, rowKey, rowLevel, rowid, rows } = args
- let { seq, trOn, isNotRenderRow } = args
- const { selection, tableColumn, treeConfig, selectRow } = $table
+function renderTable({ $table, _vm }) {
+ const { tableLayout, scrollXLoad, scrollYLoad, bodyTableWidth, isColumnWidthAssigned } = $table
+ const { columnPool, isNoData } = _vm
- if (isNotRenderRow) {
+ if (!isColumnWidthAssigned) {
return
}
- let key = rowid
- if (row._isDraging) {
- // 防止数据多次刷新导致key回归rowid
- _vm.$nextTick(() => {
- delete row._isDraging
- })
- if (renderRowFlag) {
- key = `${rowid}${rowKey}`
- }
- }
-
- rows.push(
- h(
- 'tr',
- {
- class: [
- 'tiny-grid-body__row',
- {
- [`row__level-${rowLevel}`]: treeConfig,
- [classMap.rowNew]: editStore.insertList.includes(row),
- [classMap.rowSelected]: selection.includes(row),
- [classMap.rowRadio]: selectRow === row,
- [classMap.rowActived]: rowActived
- },
- rowClassName
- ? isFunction(rowClassName)
- ? rowClassName({ $table, $seq, seq, rowLevel, row, rowIndex, $rowIndex })
- : rowClassName
- : ''
- ],
- attrs: {
- 'data-rowid': rowid
- },
- key,
- on: trOn
- },
- tableColumn.map((column, $columnIndex) => {
- let columnIndex = $table.getColumnIndex(column)
- let args1 = { $columnIndex, $rowIndex, $seq, $table, _vm, column, columnIndex }
-
- Object.assign(args1, { h, row, rowIndex, rowLevel, seq })
+ const tableVnode = (
+
+ {[
+ // 列分组(用于指定列宽)
+
+ {columnPool.map(({ id, item: column, used }) => {
+ return (
+
+ )
+ })}
+ ,
+ // 表头
+ $table.showHeader ? {renderHeaderRows(_vm)} : null,
+ // 表体内容
+ {renderRows(_vm)} ,
+ // 表尾
+ $table.showFooter && !isNoData && typeof $table.renderFooter !== 'function' ? (
+ {renderFooterRows(_vm)}
+ ) : null
+ ]}
+
+ )
- return renderColumn(args1)
- })
- )
+ return scrollXLoad || scrollYLoad ? (
+
+ {tableVnode}
+
+ ) : (
+ tableVnode
)
}
-function renderRowAfter({ $table, h, row, rowIndex, rows, tableData }) {
- typeof $table.renderRowAfter === 'function' &&
- $table.renderRowAfter({ rows, row, data: tableData, rowIndex, renderColumn }, h)
-}
+const calcScrollLeft = ($table, wrapperScrollLeft) => {
+ const { visibleColumn, tableColumn } = $table
+ let start, end, total, offset, column
-function renderRowExpanded(args) {
- const { $table, expandMethod, expandeds, h, row, rowIndex } = args
- const { rowLevel, rowid, rows, seq, tableColumn, trOn, treeConfig } = args
+ start = end = total = offset = 0
- if (
- expandeds.length &&
- expandeds.includes(row) &&
- (typeof expandMethod === 'function' ? expandMethod(row, rowLevel) : true)
- ) {
- const column = find(tableColumn, (column) => column.type === 'expand')
- const columnIndex = $table.getColumnIndex(column)
- let cellStyle
+ for (const col of visibleColumn) {
+ start = end
+ end = start + col.renderWidth
- if (treeConfig) {
- cellStyle = { paddingLeft: `${rowLevel * (treeConfig.indent || 16) + 30}px` }
+ if (wrapperScrollLeft >= start && wrapperScrollLeft < end) {
+ offset = wrapperScrollLeft - total
+ column = col
+ break
}
- if (column) {
- const renderData = { $table, seq, row, rowIndex, column, columnIndex, level: rowLevel }
- rows.push(
- h(
- 'tr',
- {
- class: 'tiny-grid-body__expanded-row',
- key: `expand_${rowid}`,
- on: trOn
- },
- [
- h(
- 'td',
- {
- class: 'tiny-grid-body__expanded-column',
- attrs: { colspan: tableColumn.length }
- },
- [
- h(
- 'div',
- {
- class: 'tiny-grid-body__expanded-cell',
- style: cellStyle
- },
- [column.renderData(h, renderData)]
- )
- ]
- )
- ]
- )
- )
- }
+ total += col.renderWidth
}
-}
-
-function renderRowTree(args, renderRows) {
- let { $seq, $table, _vm, h, row, rowLevel } = args
- let { rows, seq, seqCount, tableColumn, treeConfig, treeExpandeds } = args
- let { scrollYLoad } = $table
- // 如果没有树表配置或者树表展开行数为零,则直接跳过
- if (!treeConfig || !treeExpandeds.length) {
- return
- }
+ total = 0
- let childrenKey = getTreeChildrenKey({ scrollYLoad, treeConfig })
- let rowChildren = row[childrenKey]
-
- // 若果当前行不是展开行或者子节点个数为零,则跳过
- if (!rowChildren || !rowChildren.length || !~treeExpandeds.indexOf(row)) {
- return
- }
+ for (const col of tableColumn) {
+ if (col === column) {
+ total += offset
+ break
+ }
- const args1 = {
- h,
- _vm,
- $table,
- // $seq 树表特有序号:1 --> 1.1
- $seq: $seq ? `${$seq}.${seq}` : `${seq}`,
- rowLevel: rowLevel + 1,
- tableData: rowChildren,
- tableColumn,
- seqCount
+ total += col.renderWidth
}
- rows.push(...renderRows(args1))
+ return total
}
-function renderRows({ h, _vm, $table, $seq, rowLevel, tableData, tableColumn, seqCount }) {
- let { rowKey, rowClassName, treeConfig, treeExpandeds } = $table
- let { groupData, scrollYLoad, scrollYStore, editConfig, editStore, expandConfig = {} } = $table
- let { expandeds, selection, rowGroup, hasVirtualRow, afterFullData, treeOrdered } = $table
- let rows = []
- let expandMethod = expandConfig.activeMethod
- let startIndex = scrollYStore.startIndex
- // 子级索引是否按数字递增显示:true(子级索引按数字递增显示,父级1,子级2);false(子级索引在父级索引基础上增加,父级1,子级1.1)
- let isOrdered = treeConfig ? Boolean(treeOrdered) : false
- seqCount = seqCount || { value: 0 }
- let treeShowKey = getTreeShowKey({ scrollYLoad, treeConfig })
- let { hideMethod } = treeConfig || {}
-
- // 循环表格数据,生成表格主体内容VNode,此处也是性能优化的整改点
- tableData.forEach((row, $rowIndex) => {
- let trOn = {}
- let rowIndex = $rowIndex
- let { actived } = editStore
- let rowActived = editConfig && actived.row === row
- let virtualRow = isVirtualRow(row)
- const isNotRenderRow = (treeShowKey && !row[treeShowKey]) || (hideMethod && hideMethod(row, rowLevel)) || virtualRow
-
- // 树表虚拟滚动,如果当前行被剪切不需要渲染,则无需自增序号
- if (!isNotRenderRow) {
- seqCount.value = seqCount.value + 1
- }
+const calcScrollTop = ($table, wrapperScrollTop) => {
+ const { _graphInfo, tableData, headerHeight, rowHeight } = $table
+ const graphed = _graphInfo.graphed
+ const rows = graphed.map((node) => node.payload)
+ let start, end, total, offset, row
- let seq = isOrdered ? seqCount.value : rowIndex + 1
- if (scrollYLoad) {
- seq += startIndex
- }
- // 分组表场景正常数据行的序号由在afterFullData中的位置提供
- if (hasVirtualRow && !virtualRow) {
- seq = afterFullData.indexOf(row) + 1
- }
- // 确保任何情况下 rowIndex 都精准指向真实 data 索引
- rowIndex = $table.getRowIndex(row)
+ start = end = total = offset = 0
- let rowid = getRowid($table, row)
+ if (wrapperScrollTop < headerHeight) {
+ total = wrapperScrollTop
+ } else {
+ start = end = total = offset = headerHeight
- // 如果有表格分组信息,则执行分组逻辑
- renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, tableColumn })
- let args = { $rowIndex, $seq, $table, _vm, editStore, h, row, rowActived }
- Object.assign(args, { rowClassName, rowIndex, rowKey, rowLevel, rowid, rows, selection, seq })
+ for (const r of rows) {
+ start = end
+ end = start + rowHeight
- Object.assign(args, { tableColumn, trOn, treeConfig, isNotRenderRow })
+ if (wrapperScrollTop >= start && wrapperScrollTop < end) {
+ offset = wrapperScrollTop - total
+ row = r
+ break
+ }
- // 输出表格行列的vnode节点列表
- renderRow(args)
+ total += rowHeight
+ }
- // 允许用户自定义表格行渲染后的逻辑
- renderRowAfter({ $table, h, row, rowIndex, rows, tableData })
- args = { $table, expandMethod, expandeds, h, row, rowIndex, rowLevel }
- Object.assign(args, { rowid, rows, seq, tableColumn, trOn, treeConfig })
+ total = headerHeight
- // 如果行被展开了,这里渲染展开行的vnode节点
- renderRowExpanded(args)
- args = { $seq, $table, _vm, h, row, rowLevel, rows }
- Object.assign(args, { seq, seqCount, tableColumn, treeConfig, treeExpandeds })
+ for (const r of tableData) {
+ if (hooks.toRaw(r) === hooks.toRaw(row)) {
+ total += offset
+ break
+ }
- // 如果是树形表格,则会递归渲染已展开行的子节点
- renderRowTree(args, renderRows)
- })
- renderRowFlag = !renderRowFlag
+ total += rowHeight
+ }
+ }
- return rows
+ return total
}
+export default defineComponent({
+ name: `${$prefix}GridBody`,
+ props: {
+ collectColumn: Array,
+ tableColumn: Array,
+ tableNode: Array,
+ tableData: Array,
+ footerData: Array
+ },
+ setup(props, { slots }) {
+ const vm = hooks.getCurrentInstance()?.proxy
+ const $table = vm?.$parent
+ const rowHeight = hooks.computed(() => $table.rowHeight)
+ const headerRowHeight = hooks.computed(() => $table.headerRowHeight)
+ const { headerTable } = useHeader(props, vm, headerRowHeight)
+ const { columnPool, rowPool, isNoData } = usePool(props)
+ const wrapperScrollLeft = hooks.ref(0)
+ const wrapperScrollTop = hooks.ref(0)
+ const stickyWrapper = hooks.ref()
+ const table = hooks.ref()
+ const body = hooks.ref()
+ const customFooter = hooks.ref()
+ const colgroup = hooks.ref()
+ const thead = hooks.ref()
+ const tbody = hooks.ref()
+ const ySpace = hooks.ref()
+
+ hooks.watch(wrapperScrollLeft, (wrapperScrollLeft) => {
+ const el = stickyWrapper.value
+ if (!el) return
+ hooks.nextTick(() => (el.scrollLeft = calcScrollLeft($table, wrapperScrollLeft)))
+ })
-function renderDefEmpty(h) {
- return [
- h(GridNoData, {
- class: 'tiny-grid__empty-img'
- }),
- h(
- 'span',
- {
- class: 'tiny-grid__empty-text'
- },
- GlobalConfig.i18n('ui.grid.emptyText')
- )
- ]
-}
+ hooks.watch(wrapperScrollTop, (wrapperScrollTop) => {
+ const el = stickyWrapper.value
+ if (!el) return
+ hooks.nextTick(() => (el.scrollTop = calcScrollTop($table, wrapperScrollTop)))
+ })
-const syncHeaderAndFooterScroll = ({ bodyElem, footerElem, headerElem, isX }) => {
- const scrollLeft = bodyElem.scrollLeft
- if (isX && headerElem) {
- headerElem.scrollLeft = scrollLeft
- }
- if (isX && footerElem) {
- footerElem.scrollLeft = scrollLeft
- }
-}
+ useCellEvent({ table, $table })
+ const { normalRows, footerRows } = useCellSpan(vm, props)
-function doScrollLoad({ $table, _vm, bodyElem, event, headerElem, isX, isY, scrollLeft, scrollXLoad, scrollYLoad }) {
- let isScrollX = scrollXLoad && isX
+ hooks.watch([body, table, thead, tbody, ySpace], () => {
+ const { elemStore } = $table
- // 如果是水平虚拟滚动,并且正在进行水平滚动,就触发水平虚滚事件
- if (isScrollX) {
- // 处理x轴方法虚拟滚动加载数据逻辑
- $table.triggerScrollXEvent(event)
- }
+ elemStore['main-body-wrapper'] = body.value
+ elemStore['main-body-table'] = table.value
+ elemStore['main-body-headerList'] = thead.value
+ elemStore['main-body-list'] = tbody.value
+ elemStore['main-body-ySpace'] = ySpace.value
+ })
- // 同上,并且主表头存在时,修复极端场景(拖动滚动条到最右侧)表头表体水平滚动位置不同步问题
- if (isScrollX && headerElem && scrollLeft + bodyElem.clientWidth >= bodyElem.scrollWidth) {
- // 修复拖动滚动条时可能存在不同步问题
- _vm.$nextTick(() => {
- if (bodyElem.scrollLeft !== headerElem.scrollLeft) {
- headerElem.scrollLeft = bodyElem.scrollLeft
+ const bodyClientWidth = hooks.ref(0)
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ for (let entry of entries) {
+ const target = entry.target as HTMLElement
+
+ switch (target) {
+ case body.value:
+ bodyClientWidth.value = target.clientWidth
+ break
+ case customFooter.value:
+ $table.footerHeight = target.offsetHeight
+ }
}
})
- }
- // 如果是垂直虚拟滚动,并且正在进行垂直滚动,就触发垂直虚滚事件
- if (scrollYLoad && isY) {
- // 处理y轴方法虚拟滚动加载数据逻辑
- $table.triggerScrollYEvent(event)
- }
-}
+ hooks.watch(body, (body) => body && resizeObserver.observe(body))
-function renderEmptyBlock({ $slots, $table, _vm, isCenterCls, renderEmpty, tableData }) {
- return h(
- 'div',
- {
- class: `tiny-grid__empty-block${tableData.length ? '' : ' is__visible'} ${isCenterCls}`,
- ref: 'emptyBlock'
- },
- $slots.empty ? $slots.empty.call(_vm, { $table }, h) : renderEmpty ? [renderEmpty(h, $table)] : renderDefEmpty(h)
- )
-}
+ hooks.watch(customFooter, (customFooter) => customFooter && resizeObserver.observe(customFooter))
-function renderBorders({ keyboardConfig, mouseConfig }) {
- let res: any = null
+ hooks.watchEffect(() => {
+ if ($table.isColumnWidthAssigned) {
+ const columns = $table.scrollXLoad ? $table.visibleColumn : $table.tableColumn
+ const scrollWidth = columns.reduce((total, col) => (total += col.renderWidth), 0)
+ const clientWidth = bodyClientWidth.value
- // 如果用户配置了鼠标和键盘配置项
- if (mouseConfig.checked || keyboardConfig.isCut) {
- res = h('div', { class: 'tiny-grid__borders' }, [
- mouseConfig.checked ? renderBorder(h, 'check') : null,
- keyboardConfig.isCut ? renderBorder(h, 'copy') : null
- ])
- }
+ $table.horizonScroll.max = scrollWidth > clientWidth ? scrollWidth - clientWidth : 0
+ }
+ })
- return res
-}
+ hooks.onMounted(() => {
+ // 节流滚动降低滚动事件处理次数
+ vm._throttleScrollHandler = throttle($table.optimizeOpts.scrollDelay, vm.handleScroll)
-function renderTable({ $table, _vm, tableColumn, tableData, tableLayout }) {
- return h(
- 'table',
- {
- class: 'tiny-grid__body',
- style: { tableLayout },
- attrs: { cellspacing: 0, cellpadding: 0, border: 0 },
- ref: 'table'
- },
- [
- // 渲染colgroup标签,设置表格列宽度,保证表头的表格和表体的表格每列宽相同
- h(
- 'colgroup',
- { ref: 'colgroup' },
- tableColumn.map((column, columnIndex) => h('col', { attrs: { name: column.id }, key: columnIndex }))
- ),
- // 表格每次数据改变都会触发renderRow重新执行,会造成性能损失,此处待优化
- h('tbody', { ref: 'tbody' }, renderRows({ h, _vm, $table, $seq: '', rowLevel: 0, tableData, tableColumn }))
- ]
- )
-}
+ body.value?.addEventListener('scroll', vm._throttleScrollHandler)
-// 如果scrollLoad存在,标识开启了滚动分页功能
-function renderYSpace({ scrollLoad }) {
- return h('div', { class: 'tiny-grid-body__y-space visual', ref: 'ySpace' }, [
- scrollLoad ? h('div', { class: 'tiny-grid-body__y-scrollbar' }) : [null]
- ])
-}
+ // 初始化行列拖拽
+ setTimeout(() => {
+ const { dropConfig } = $table
+
+ if (dropConfig) {
+ const { plugin, row = true, column = true, scheme } = dropConfig
+
+ plugin && row && (vm.rowSortable = $table.rowDrop(body.value))
+
+ if (scheme !== 'v2') {
+ plugin && column && (vm.columnSortable = $table.columnDrop(body.value))
+ }
+ }
+ }, 50)
+ })
-export default defineComponent({
- name: `${$prefix}GridBody`,
- props: {
- collectColumn: Array,
- fixedColumn: Array,
- isGroup: Boolean,
- size: String,
- tableColumn: Array,
- tableData: Array,
- visibleColumn: Array
- },
- mounted() {
- const { $el, $parent: $table, $refs } = this as any
- const { elemStore, dropConfig } = $table
- const keyPrefix = 'main-body-'
-
- // 表体第一层div,出现滚动条的dom元素
- elemStore[`${keyPrefix}wrapper`] = $el
- // 表体table元素
- elemStore[`${keyPrefix}table`] = $refs.table
- // colgroup元素,保持表头和表体宽度保持一致
- elemStore[`${keyPrefix}colgroup`] = $refs.colgroup
- // tbody元素
- elemStore[`${keyPrefix}list`] = $refs.tbody
- // x轴滚动条占位元素
- elemStore[`${keyPrefix}xSpace`] = $refs.xSpace
- // y轴滚动条占位元素
- elemStore[`${keyPrefix}ySpace`] = $refs.ySpace
- // 空数据元素
- elemStore[`${keyPrefix}emptyBlock`] = $refs.emptyBlock
-
- if (dropConfig) {
- const { plugin, row = true } = dropConfig
- plugin && row && (this.rowSortable = $table.rowDrop(this.$el))
- }
- },
- 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
+ const { rowSortable, columnSortable } = vm
- table.$el._onscroll = null
- table.$el.onscroll = null
+ body.value?.removeEventListener('scroll', vm._throttleScrollHandler)
+ vm._throttleScrollHandler = null
+ rowSortable && rowSortable.destroy()
+ columnSortable && columnSortable.destroy()
+ resizeObserver.disconnect()
})
- return { slots }
+ return {
+ slots,
+ rowHeight,
+ headerTable,
+ columnPool,
+ rowPool,
+ isNoData,
+ wrapperScrollLeft,
+ wrapperScrollTop,
+ stickyWrapper,
+ table,
+ body,
+ customFooter,
+ colgroup,
+ thead,
+ tbody,
+ ySpace,
+ normalRows,
+ footerRows
+ }
},
render() {
- let { $parent: $table } = this as any
- let { $grid, isCenterEmpty, keyboardConfig = {}, mouseConfig = {}, renderEmpty } = $table
- let { scrollLoad, tableColumn, tableData, tableLayout } = $table
- let $slots = $grid.slots
- let isCenterCls = isCenterEmpty ? 'is__center' : ''
-
- return h(
- 'div',
- {
- ref: 'body',
- class: ['tiny-grid__body-wrapper', 'body__wrapper', { [classMap.isScrollload]: scrollLoad }],
- on: {
- scroll: this.scrollEvent
- }
- },
- [
- // 表格主体内容x轴方向虚拟滚动条占位元素
- h('div', { class: 'tiny-grid-body__x-space', ref: 'xSpace' }),
- renderYSpace({ scrollLoad }),
- renderTable({ $table, _vm: this, tableColumn, tableData, tableLayout }),
- // 开启鼠标或者配置项选中边框线
- renderBorders({ keyboardConfig, mouseConfig }),
- // 空数据
- renderEmptyBlock({ $slots, $table, _vm: this, isCenterCls, renderEmpty, tableData })
- ]
+ const { $parent: $table, isNoData, tableColumn, footerData } = this
+ const { $grid, keyboardConfig, mouseConfig, scrollLoad, showFooter, renderFooter } = $table
+ const { containerScrollWidth, containerScrollHeight, scrollLoadScrollHeight } = $table
+ const { bodyWrapperHeight, bodyWrapperMinHeight, bodyWrapperMaxHeight } = $table
+ const $slots = $grid.slots
+ const _vm = this
+
+ return (
+
+ {[
+
,
+
+ {scrollLoad ? (
+
+ ) : null}
+
,
+ // 内容表格
+ renderTable({ $table, _vm }),
+ // 渲染自定义表尾
+ showFooter && !isNoData && typeof renderFooter === 'function' ? (
+
+ ) : null,
+ // 选中边框线
+ mouseConfig.checked || keyboardConfig.isCut ? (
+
+ {[mouseConfig.checked ? renderBorder('check') : null, keyboardConfig.isCut ? renderBorder('copy') : null]}
+
+ ) : null,
+ // 空数据
+ isNoData ? (
+
+ {$slots.empty
+ ? $slots.empty.call(_vm, { $table }, h)
+ : $table.renderEmpty
+ ? [$table.renderEmpty(h, $table)]
+ : [
+ ,
+ {GlobalConfig.i18n('ui.grid.emptyText')}
+ ]}
+
+ ) : null
+ ]}
+
)
},
methods: {
- // 滚动处理,如果存在列固定右侧,同步更新滚动状态
- scrollEvent(event) {
- let { $parent: $table } = this as any
- let { $refs, lastScrollLeft, lastScrollTop, scrollXLoad, scrollYLoad, columnStore } = $table
- let { leftList, rightList } = columnStore
- let { tableBody, tableFooter, tableHeader } = $refs
-
- // 获取主表头,主表体,主表尾,左表体,右表体
- let headerElem = tableHeader ? tableHeader.$el : null
- let bodyElem = tableBody.$el
- let footerElem = tableFooter ? tableFooter.$el : null
-
- // 获取主表体元素的滚动位置
- let scrollLeft = bodyElem.scrollLeft
- let scrollTop = bodyElem.scrollTop
-
- // 对比当前滚动位置和最后一次滚动位置,来得到当前滚动的是哪个方向上的滚动条
- let isY = scrollTop !== lastScrollTop
- let isX = scrollLeft !== lastScrollLeft
-
- // 记录新的滚动位置和时间
+ handleScroll(event) {
+ const { $parent: $table, $el }: any = this
+ const { lastScrollLeft, lastScrollTop, scrollXLoad, scrollYLoad, horizonScroll } = $table
+ const { max, threshold } = horizonScroll
+ const { scrollLeft, scrollTop } = $el
+ const isX = scrollLeft !== lastScrollLeft
+ const isY = scrollTop !== lastScrollTop
+
+ this.wrapperScrollLeft = scrollLeft
+ this.wrapperScrollTop = scrollTop
+
$table.lastScrollTime = Date.now()
$table.lastScrollLeft = scrollLeft
$table.lastScrollTop = scrollTop
$table.scrollDirection = isX ? 'X' : 'Y'
+ $table.horizonScroll.isLeft = scrollLeft < threshold
+ $table.horizonScroll.isRight = scrollLeft > max - threshold
- // 同步滚动条状态,只同步表头(表尾)滚动条状态,冻结列已优化为sticky方式
- syncHeaderAndFooterScroll({ bodyElem, footerElem, headerElem, isX })
-
- // 处理关于冻结列最外层div类名
- if (leftList.length || rightList.length) {
- generateFixedClassName({ $table, bodyElem, leftList, rightList })
+ if (isX && scrollXLoad) {
+ $table.triggerScrollXEvent(event)
}
- // 处理x和y轴方法虚拟滚动数据加载逻辑
- doScrollLoad({ $table, _vm: this, bodyElem, event, headerElem, isX, isY, scrollLeft, scrollXLoad, scrollYLoad })
+ if (isY && scrollYLoad) {
+ $table.triggerScrollYEvent(event)
+ }
- // 触发用户监听的表格滚动事件
emitEvent($table, 'scroll', [{ type: 'body', scrollTop, scrollLeft, isX, isY, $table }, event])
+ },
+ resizeMousedown(event, params) {
+ let { $el, $parent: $table }: any = this
+ let { clientX: dragClientX, target: dragBtnElem } = event
+ let { column } = params
+ let { dragLeft = 0, minInterval = 40 } = {}
+ let { resizeBar: resizeBarElem, tableBody } = $table.$refs
+ let { cell = dragBtnElem.parentNode, dragBtnWidth = dragBtnElem.clientWidth } = {}
+ let { pos = getOffsetPos(dragBtnElem, $el), tableBodyElem = tableBody.$el } = {}
+ let dragMinLeft = pos.left - cell.clientWidth + dragBtnWidth + minInterval
+ let dragPosLeft = pos.left + Math.floor(dragBtnWidth)
+ 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
+
+ dragLeft = Math.max(left, dragMinLeft)
+ if ($table.resizableConfig?.limit) {
+ const limitWidth = $table.resizableConfig?.limit({
+ field: column.own.field,
+ width: column.renderWidth + (dragLeft - dragPosLeft)
+ })
+ dragLeft = dragMinLeft - minInterval + limitWidth
+ }
+
+ resizeBarElem.style.left = dragLeft - scrollLeft + 'px'
+ }
+
+ resizeBarElem.style.display = 'block'
+ addClass($table.$el, 'tiny-grid-cell__resize')
+ $table._isResize = true
+ document.onmousemove = handleMousemoveEvent
+
+ document.onmouseup = function () {
+ 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()
+
+ const toolbarVm = $table.getVm('toolbar')
+
+ if (toolbarVm) {
+ toolbarVm.updateResizable()
+ }
+
+ emitEvent($table, 'resizable-change', [params])
+
+ // 拖拽列宽后更新水平滚动最大位置
+ if ($table.horizonScroll.fixed) {
+ setTimeout(() => {
+ let { scrollWidth, clientWidth, scrollLeft } = $table.$refs.tableBody.$el
+
+ $table.horizonScroll.max = scrollWidth > clientWidth ? scrollWidth - clientWidth : 0
+ $table.horizonScroll.isRight = scrollLeft > $table.horizonScroll.max - $table.horizonScroll.threshold
+ }, 50)
+ }
+ }
+
+ handleMousemoveEvent(event)
+ },
+ handleScrollLoad(e) {
+ const { $parent: $table } = this
+ if ($table.scrollLoad) {
+ $table.debounceScrollLoad(e)
+ }
}
}
})
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..93e07b2fd4
--- /dev/null
+++ b/packages/vue/src/grid/src/body/src/usePool.ts
@@ -0,0 +1,108 @@
+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) => {
+ if (!Array.isArray(array)) {
+ return
+ }
+
+ 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) => {
+ if (!Array.isArray(array)) {
+ return
+ }
+
+ 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([])
+ const rowPool = hooks.ref([])
+ const isNoData = hooks.ref(true)
+
+ let columnContext
+
+ hooks.watch(
+ () => props.tableColumn,
+ () => {
+ if (columnContext) {
+ updatePool(props.tableColumn, columnContext)
+ } else {
+ columnContext = createPool(props.tableColumn)
+ }
+
+ columnPool.value = columnContext.pool
+ }
+ )
+
+ let rowContext
+
+ hooks.watch(
+ () => props.tableNode,
+ () => {
+ if (rowContext) {
+ updatePool(props.tableNode, rowContext)
+ } else {
+ rowContext = createPool(props.tableNode)
+ }
+
+ rowPool.value = rowContext?.pool || rowPool.value
+ isNoData.value = !(props.tableNode?.length > 0)
+ }
+ )
+
+ return { columnPool, rowPool, isNoData }
+}
diff --git a/packages/vue/src/grid/src/cell/src/cell.ts b/packages/vue/src/grid/src/cell/src/cell.ts
index ccf1809d7d..ab10749d61 100644
--- a/packages/vue/src/grid/src/cell/src/cell.ts
+++ b/packages/vue/src/grid/src/cell/src/cell.ts
@@ -394,16 +394,16 @@ export const Cell = {
return Cell.renderTreeIcon(h, params).concat(Cell.renderIndexCell(h, params))
},
renderIndexCell(h, params) {
- const { $table, column, row, seq, $seq, level } = params
+ const { $table, column, row, seq, level } = params
// startIndex:序号列的起始值
- const { startIndex, treeConfig, scrollYLoad, treeOrdered } = $table
+ const { startIndex, treeConfig, treeOrdered } = $table
const { indexMethod, slots } = column
const { temporaryIndex = '_$index_' } = treeConfig || {}
const isTreeOrderedFalse = treeConfig && !treeOrdered
let indexValue = startIndex + seq
// tree-config为false的情况下,序号为1.1这种形式
if (isTreeOrderedFalse && level) {
- indexValue = scrollYLoad ? row[temporaryIndex] : `${$seq}.${seq}`
+ indexValue = row[temporaryIndex]
}
if (slots && slots.default) {
diff --git a/packages/vue/src/grid/src/checkbox/src/methods.ts b/packages/vue/src/grid/src/checkbox/src/methods.ts
index c00058f7a2..4387cd5737 100644
--- a/packages/vue/src/grid/src/checkbox/src/methods.ts
+++ b/packages/vue/src/grid/src/checkbox/src/methods.ts
@@ -1,7 +1,6 @@
import { hasCheckField, hasNoCheckField } from './handleSelectRow'
import { hasCheckFieldNoStrictly, hasNoCheckFieldNoStrictly, setSelectionNoStrictly } from './setAllSelection'
-import { getTableRowKey } from '../../table/src/strategy'
-import { emitEvent } from '@opentiny/vue-renderless/grid/utils'
+import { emitEvent, getRowkey } from '@opentiny/vue-renderless/grid/utils'
import { isArray, set, get, eachTree, find, toStringJSON, toArray } from '@opentiny/vue-renderless/grid/static/'
export default {
@@ -129,7 +128,7 @@ export default {
reserveCheckSelection() {
let { fullDataRowIdData, selection } = this
let { reserve } = this.selectConfig || {}
- let rowkey = getTableRowKey(this)
+ let rowkey = getRowkey(this)
if (reserve && selection.length) {
this.selection = selection.map((row) => {
let rowCache = fullDataRowIdData[`${get(row, rowkey)}`]
@@ -215,14 +214,15 @@ 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
+ const rowHeight = GlobalConfig.rowHeight[tinyTheme]?.[vSize || 'default'] || 40
+ 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/column-anchor/src/methods.ts b/packages/vue/src/grid/src/column-anchor/src/methods.ts
index 8d833f486f..d6bcfd3518 100644
--- a/packages/vue/src/grid/src/column-anchor/src/methods.ts
+++ b/packages/vue/src/grid/src/column-anchor/src/methods.ts
@@ -91,7 +91,11 @@ export default {
activeAnchor,
action: (field, e) => this.anchorAction({ field, anchors, _vm: this, e })
}
- this.emitter.once('active-anchor', () => this.anchorAction({ field: activeAnchor.field, anchors, _vm: this }))
+
+ this._delayActivateAnchor = () => {
+ this._delayActivateAnchor = undefined
+ setTimeout(() => this.anchorAction({ field: activeAnchor.field, anchors, _vm: this }), activeAnchor.delay)
+ }
},
anchorAction({ field, anchors, _vm }) {
const fromAnchor = anchors.find((anchor) => anchor.active)
diff --git a/packages/vue/src/grid/src/column/src/column.ts b/packages/vue/src/grid/src/column/src/column.ts
index 775313249e..279bcadeb0 100644
--- a/packages/vue/src/grid/src/column/src/column.ts
+++ b/packages/vue/src/grid/src/column/src/column.ts
@@ -23,7 +23,6 @@
*
*/
import { findTree } from '@opentiny/vue-renderless/grid/static'
-import { setColumnFormat } from '@opentiny/vue-renderless/grid/utils'
import { h, hooks, $props, defineComponent, useRelation, useInstanceSlots } from '@opentiny/vue-common'
import Cell from '../../cell'
import { warn } from '../../tools'
@@ -162,7 +161,7 @@ export default defineComponent({
watch(
() => props.formatConfig,
- () => setColumnFormat(state.columnConfig, props)
+ () => (state.columnConfig.format = props.formatConfig)
)
onUpdated(() => {
diff --git a/packages/vue/src/grid/src/composable/index.ts b/packages/vue/src/grid/src/composable/index.ts
index bd25c3d097..b46e748753 100644
--- a/packages/vue/src/grid/src/composable/index.ts
+++ b/packages/vue/src/grid/src/composable/index.ts
@@ -1,2 +1,7 @@
export * from './useDrag'
export * from './useRowGroup'
+export * from './useCellStatus'
+export * from './useData'
+export * from './useHeader'
+export * from './useCellEvent'
+export * from './useCellSpan'
diff --git a/packages/vue/src/grid/src/composable/useCellEvent.ts b/packages/vue/src/grid/src/composable/useCellEvent.ts
new file mode 100644
index 0000000000..683c6b97a8
--- /dev/null
+++ b/packages/vue/src/grid/src/composable/useCellEvent.ts
@@ -0,0 +1,365 @@
+/* eslint-disable no-cond-assign */
+import { hooks } from '@opentiny/vue-common'
+import { off, on, isNull } from '@opentiny/utils'
+import { isUndefined } from '@opentiny/vue-renderless/grid/static/'
+import { updateCellTitle, emitEvent } from '@opentiny/vue-renderless/grid/utils'
+
+const getEventSource = (e, $table) => {
+ const target = e.target
+ const tableEl = target.closest('.tiny-grid__body')
+
+ if (tableEl.dataset?.tableid !== String($table.id)) return
+
+ let cellEl = target.closest('.tiny-grid-header__column')
+ let rowEl, part, rowType, row, column
+
+ if (cellEl) {
+ rowEl = cellEl.parentNode
+ part = 'header'
+ } else if ((cellEl = target.closest('.tiny-grid-body__column'))) {
+ rowEl = cellEl.parentNode
+ part = 'body'
+ } else if ((cellEl = target.closest('.tiny-grid-footer__column'))) {
+ rowEl = cellEl.parentNode
+ part = 'footer'
+ }
+
+ if (!part || !cellEl || !rowEl) return
+
+ column = $table.getColumnNode(cellEl)?.item
+
+ if (rowEl.dataset?.rowid?.startsWith('row_g_')) {
+ rowType = 'virtual'
+ } else if (part === 'body') {
+ rowType = 'normal'
+ row = $table.getRowNode(rowEl)?.item
+ }
+
+ return { part, rowType, row, column, cell: cellEl, tr: rowEl }
+}
+
+const parseNumber = (numStr) => (isUndefined(numStr) ? numStr : parseInt(numStr, 10))
+
+const normalOverflow = (overflow, _overflow) => {
+ overflow = isNull(overflow) ? _overflow : overflow
+
+ const overflowTitle = overflow === 'title'
+ const overflowTooltip = overflow === true || overflow === 'tooltip'
+ const overflowEllipsis = overflow === 'ellipsis'
+ const overflowHint = overflowTitle || overflowTooltip || overflowEllipsis
+
+ return { overflow, overflowTitle, overflowTooltip, overflowEllipsis, overflowHint }
+}
+
+export const getConfigOverflow = (column, $table) => {
+ const { showOverflow: _cellOverflow, showHeaderOverflow: _headerOverflow } = $table
+ const { showTip, showOverflow, showHeaderTip, showHeaderOverflow } = column || {}
+
+ const {
+ overflowTitle: cellOverflowTitle,
+ overflowTooltip: cellOverflowTooltip,
+ overflowEllipsis: cellOverflowEllipsis,
+ overflowHint: cellOverflowHint
+ } = normalOverflow(showOverflow, _cellOverflow)
+
+ const {
+ overflowTitle: headerOverflowTitle,
+ overflowTooltip: headerOverflowTooltip,
+ overflowEllipsis: headerOverflowEllipsis,
+ overflowHint: headerOverflowHint
+ } = normalOverflow(showHeaderOverflow, _headerOverflow)
+
+ return {
+ cellTip: showTip,
+ cellOverflowTitle,
+ cellOverflowTooltip,
+ cellOverflowEllipsis,
+ cellOverflowHint,
+ headerTip: showHeaderTip,
+ headerOverflowTitle,
+ headerOverflowTooltip,
+ headerOverflowEllipsis,
+ headerOverflowHint
+ }
+}
+
+const getEventParams = ({ row, column, cell, tr }, $table) => {
+ const { showTip, showHeaderTip } = column || {}
+ const rowIndex = $table.getRowIndex(row)
+ const columnIndex = $table.getColumnIndex(column)
+ const { seq, colindex } = cell?.dataset || {}
+ const { rowindex, rowlevel } = tr?.dataset || {}
+
+ return {
+ $table,
+ row,
+ column,
+ cell,
+ rowIndex,
+ columnIndex,
+ showTip,
+ showHeaderTip,
+ seq: parseNumber(seq),
+ $rowIndex: parseNumber(rowindex),
+ $columnIndex: parseNumber(colindex),
+ level: parseNumber(rowlevel)
+ }
+}
+
+// 滚动、拖动过程中不需要触发
+const isOperateMouse = ($table) => {
+ return (
+ $table._isResize || ($table.lastScrollTime && Date.now() < $table.lastScrollTime + $table.optimizeOpts.delayHover)
+ )
+}
+
+export const useCellEvent = ({ table, $table }) => {
+ let isBound = false
+ const hoverCell = hooks.shallowRef()
+ const hoverCellContext = hooks.shallowRef()
+ const hoverRow = hooks.shallowRef()
+ const hoverText = hooks.shallowRef()
+ let textContent
+
+ const handleMouseEnter = (e) => {
+ if (isOperateMouse($table)) return
+ const source = getEventSource(e, $table)
+
+ if (source) {
+ const params = getEventParams(source, $table)
+
+ if (hoverCell.value !== source.cell) {
+ hoverCell.value = source.cell
+ hoverCellContext.value = { params, source, e }
+ textContent =
+ hoverCell.value.querySelector('.tiny-grid-cell-text') || hoverCell.value.querySelector('.tiny-grid-cell')
+ }
+
+ if (e.target === textContent) {
+ hoverText.value = textContent
+ }
+
+ if (source.part !== 'body') {
+ hoverRow.value = null
+ } else if (hoverRow.value !== source.row) {
+ hoverRow.value = source.row
+ }
+ }
+ }
+
+ const handleMouseLeave = (e) => {
+ if (isOperateMouse($table)) return
+
+ const target = e.target
+
+ if (target === hoverText.value) {
+ hoverText.value = null
+ }
+
+ if (target.localName === 'table' && target.dataset?.tableid === String($table.id)) {
+ hoverRow.value = hoverCell.value = hoverCellContext.value = null
+ }
+ }
+
+ const handleMouseDown = (e) => {
+ const source = getEventSource(e, $table)
+
+ if (source) {
+ const params = getEventParams(source, $table)
+ const { mouseConfig = {} } = $table
+
+ if (source.part === 'header' && mouseConfig.checked) {
+ $table.triggerHeaderCellMousedownEvent(e, params)
+ }
+
+ if (source.part === 'body' && (mouseConfig.checked || mouseConfig.selected)) {
+ $table.triggerCellMousedownEvent(e, params)
+ }
+ }
+ }
+
+ const handleClick = (e) => {
+ const source = getEventSource(e, $table)
+
+ if (source) {
+ const params = getEventParams(source, $table)
+ const column = source.column || {}
+ const { editor } = column
+ const satisfy = (equal, trigger) => trigger === 'row' || (equal(column) && trigger === 'cell')
+ const {
+ editConfig,
+ expandConfig = {},
+ highlightCurrentColumn,
+ highlightCurrentRow,
+ mouseConfig = {},
+ radioConfig = {},
+ selectConfig = {},
+ sortOpts,
+ tableListeners,
+ treeConfig = {}
+ } = $table
+
+ if (
+ source.part === 'header' &&
+ (highlightCurrentColumn ||
+ tableListeners['header-cell-click'] ||
+ mouseConfig.checked ||
+ sortOpts.trigger === 'cell')
+ ) {
+ $table.triggerHeaderCellClickEvent(e, params)
+ }
+
+ if (
+ source.part === 'body' &&
+ (highlightCurrentRow ||
+ tableListeners['cell-click'] ||
+ mouseConfig.checked ||
+ (editor && editConfig) ||
+ satisfy(() => true, expandConfig.trigger) ||
+ satisfy(({ type }) => type === 'radio', radioConfig.trigger) ||
+ satisfy(({ type }) => type === 'selection', selectConfig.trigger) ||
+ satisfy(({ treeNode }) => treeNode, treeConfig.trigger))
+ ) {
+ $table.triggerCellClickEvent(e, params)
+ }
+
+ if (source.part === 'footer' && tableListeners['footer-cell-click']) {
+ emitEvent($table, 'footer-cell-click', [params, e])
+ }
+ }
+ }
+
+ const handleDoubleClick = (e) => {
+ const source = getEventSource(e, $table)
+
+ if (source) {
+ const params = getEventParams(source, $table)
+ const column = source.column || {}
+ const { editor } = column
+ const { editConfig, tableListeners } = $table
+ const triggerDblclick = editor && editConfig && editConfig.trigger === 'dblclick'
+
+ if (source.part === 'header' && tableListeners['header-cell-dblclick']) {
+ emitEvent($table, 'header-cell-dblclick', [params, e])
+ }
+
+ if (source.part === 'body' && (triggerDblclick || tableListeners['cell-dblclick'])) {
+ $table.triggerCellDBLClickEvent(e, params)
+ }
+
+ if (source.part === 'footer' && tableListeners['footer-cell-dblclick']) {
+ emitEvent($table, 'footer-cell-dblclick', [params, e])
+ }
+ }
+ }
+
+ const bindMouseEvents = (target) => {
+ on(target, 'mouseenter', handleMouseEnter, true)
+ on(target, 'mouseleave', handleMouseLeave, true)
+ on(target, 'mousedown', handleMouseDown, true)
+ on(target, 'click', handleClick, true)
+ on(target, 'dblclick', handleDoubleClick, true)
+ }
+
+ const unbindMouseEvents = (target) => {
+ off(target, 'mouseenter', handleMouseEnter, true)
+ off(target, 'mouseleave', handleMouseLeave, true)
+ off(target, 'mousedown', handleMouseDown, true)
+ off(target, 'click', handleClick, true)
+ off(target, 'dblclick', handleDoubleClick, true)
+ }
+
+ hooks.onBeforeUnmount(() => {
+ if (isBound && table.value) {
+ unbindMouseEvents(table.value)
+ isBound = false
+ hoverRow.value = hoverCell.value = hoverCellContext.value = null
+ $table.hoverCell = null
+ }
+ })
+
+ hooks.watch(table, (table, old) => {
+ if (isBound && old) {
+ unbindMouseEvents(old)
+ isBound = false
+ }
+
+ if (!isBound && table) {
+ bindMouseEvents(table)
+ isBound = true
+ }
+ })
+
+ hooks.watch(hoverText, (curText, preText) => {
+ const { params, source, e } = hoverCellContext.value
+ const column = source.column || {}
+ const { headerOverflowTitle, headerOverflowTooltip, headerTip, cellOverflowTitle, cellOverflowTooltip, cellTip } =
+ getConfigOverflow(column, $table)
+
+ $table.hoverText = curText
+
+ if (preText) {
+ if (source.part === 'header') {
+ if (headerTip || headerOverflowTooltip) {
+ $table.clostTooltip()
+ }
+ } else if (source.part === 'body') {
+ if (cellTip || cellOverflowTooltip) {
+ $table.clostTooltip()
+ }
+ } else if (source.part === 'footer') {
+ if (cellOverflowTooltip) {
+ $table.clostTooltip()
+ }
+ }
+ }
+
+ if (curText) {
+ if (source.part === 'header') {
+ if (headerOverflowTitle) {
+ updateCellTitle(e, source.cell)
+ } else if (headerTip || headerOverflowTooltip) {
+ $table.triggerHeaderTooltipEvent(e, params)
+ }
+ } else if (source.part === 'body') {
+ if (cellOverflowTitle) {
+ updateCellTitle(e, source.cell)
+ } else if (cellTip || cellOverflowTooltip) {
+ $table.triggerTooltipEvent(e, params)
+ }
+ } else if (source.part === 'footer') {
+ if (cellOverflowTitle) {
+ updateCellTitle(e, source.cell)
+ } else if (cellOverflowTooltip) {
+ $table.triggerFooterTooltipEvent(e, params)
+ }
+ }
+ }
+ })
+
+ hooks.watch([hoverCell, hoverCellContext], ([curCell, curCtx], [preCell, preCtx]) => {
+ const { tableListeners } = $table
+
+ if (preCell) {
+ const { params, source, e } = preCtx
+
+ if (source.part === 'body') {
+ if (tableListeners['cell-mouseleave']) {
+ emitEvent($table, 'cell-mouseleave', [params, e])
+ }
+ }
+ }
+
+ if (curCell) {
+ const { params, source, e } = curCtx
+
+ $table.hoverCell = curCell
+
+ if (source.part === 'body') {
+ if (tableListeners['cell-mouseenter']) {
+ emitEvent($table, 'cell-mouseenter', [params, e])
+ }
+ }
+ }
+ })
+}
diff --git a/packages/vue/src/grid/src/composable/useCellSpan.ts b/packages/vue/src/grid/src/composable/useCellSpan.ts
new file mode 100644
index 0000000000..344d3071fb
--- /dev/null
+++ b/packages/vue/src/grid/src/composable/useCellSpan.ts
@@ -0,0 +1,245 @@
+import { hooks } from '@opentiny/vue-common'
+import { isVirtualRow } from '../table/src/strategy'
+import { getRowid } from '@opentiny/vue-renderless/grid/utils'
+
+export const useCellSpan = (bodyVm, bodyProps) => {
+ const $table = bodyVm.$parent
+ const normalRows = hooks.shallowRef({})
+ const footerRows = hooks.shallowRef({})
+
+ hooks.watch(
+ [() => $table.visibleColumn, () => bodyProps.tableData, () => $table.isColumnWidthAssigned],
+ ([visibleColumn, tableData, isColumnWidthAssigned]) => {
+ if (!Array.isArray(tableData) || !isColumnWidthAssigned) {
+ return
+ }
+
+ const { tableNode } = bodyProps
+ const { hasVirtualRow, treeConfig, treeOrdered, scrollYLoad, scrollYStore, afterFullData, columnStore } = $table
+ const { hideMethod } = treeConfig || {}
+ const isOrdered = treeConfig ? !!treeOrdered : false
+ const seqCount = { value: 0 }
+ const startIndex = scrollYStore.startIndex
+ const { leftList, rightList } = columnStore
+
+ const normalState = {}
+ const normalTable = []
+
+ for (let $rowIndex = 0; $rowIndex < tableData.length; $rowIndex++) {
+ const row = tableData[$rowIndex]
+ const rowLevel = tableNode[$rowIndex].level
+ const rowIndex = $table.getRowIndex(row)
+ let virtualRow = false
+
+ if (hasVirtualRow) {
+ virtualRow = isVirtualRow(row)
+ }
+
+ const isSkipRowRender = (hideMethod && hideMethod(row, rowLevel)) || virtualRow
+
+ if (!isSkipRowRender) {
+ seqCount.value = seqCount.value + 1
+ }
+
+ let seq = isOrdered ? seqCount.value : $rowIndex + 1
+
+ if (scrollYLoad) {
+ seq += startIndex
+ }
+
+ if (hasVirtualRow) {
+ if (virtualRow) {
+ // 分组行列合并的详细计算逻辑在useRowGroup.ts
+ continue
+ } else {
+ seq = afterFullData.indexOf(row) + 1
+ }
+ }
+
+ const params = { $table, data: tableData, row, $rowIndex, rowIndex, level: rowLevel, $seq: '', seq }
+
+ stateNormalCell(normalState, normalTable, params, $table)
+ }
+
+ if (leftList.length > 0) {
+ adjustColspan(normalTable, leftList.length - 1, true)
+ }
+
+ if (rightList.length > 0) {
+ adjustColspan(normalTable, visibleColumn.indexOf(rightList[0]) - 1, false)
+ }
+
+ normalRows.value = normalState
+ }
+ )
+
+ hooks.watch(
+ [() => $table.visibleColumn, () => bodyProps.footerData, () => $table.isColumnWidthAssigned],
+ ([visibleColumn, footerData, isColumnWidthAssigned]) => {
+ if (!Array.isArray(footerData) || !isColumnWidthAssigned) {
+ return
+ }
+
+ const { columnStore } = $table
+ const { leftList, rightList } = columnStore
+
+ const footerState = {}
+ const footerTable = []
+
+ for (let $rowIndex = 0; $rowIndex < footerData.length; $rowIndex++) {
+ const params = { $table, $rowIndex, data: footerData }
+ stateFooterCell(footerState, footerTable, params, $table)
+ }
+
+ if (leftList.length > 0) {
+ adjustColspan(footerTable, leftList.length - 1, true)
+ }
+
+ if (rightList.length > 0) {
+ adjustColspan(footerTable, visibleColumn.indexOf(rightList[0]) - 1, false)
+ }
+
+ footerRows.value = footerState
+ }
+ )
+
+ return { normalRows, footerRows }
+}
+
+const stateNormalCell = (state, table, params, $table) => {
+ const { rowSpan, spanMethod, visibleColumn } = $table
+ const rowId = getRowid($table, params.row)
+ const rowState = {}
+ const rowAttrs = []
+
+ for (let $columnIndex = 0; $columnIndex < visibleColumn.length; $columnIndex++) {
+ const column = visibleColumn[$columnIndex]
+ const columnIndex = $table.getColumnIndex(column)
+ const attrs = { rowspan: 1, colspan: 1, visible: true, _stickyClass: '', _stickyStyle: null }
+
+ params = { ...params, column, $columnIndex, columnIndex }
+
+ if (spanMethod || rowSpan) {
+ doSpan({ attrs, params, rowSpan, spanMethod })
+ }
+
+ rowState[column.id] = { attrs, params }
+ rowAttrs.push({ attrs, params })
+ }
+
+ state[rowId] = rowState
+ table.push(rowAttrs)
+}
+
+const stateFooterCell = (state, table, params, $table) => {
+ const { footerSpanMethod, visibleColumn } = $table
+ const rowState = {}
+ const rowAttrs = []
+
+ for (let $columnIndex = 0; $columnIndex < visibleColumn.length; $columnIndex++) {
+ const column = visibleColumn[$columnIndex]
+ const columnIndex = $table.getColumnIndex(column)
+ const attrs = { rowspan: 1, colspan: 1, visible: true }
+
+ params = { ...params, column, $columnIndex, columnIndex }
+
+ if (footerSpanMethod) {
+ const { rowspan = 1, colspan = 1 } = footerSpanMethod(params) || {}
+
+ attrs.rowspan = rowspan
+ attrs.colspan = colspan
+ attrs.visible = rowspan > 0 && colspan > 0
+ }
+
+ rowState[column.id] = { attrs, params }
+ rowAttrs.push({ attrs, params })
+ }
+
+ state[params.$rowIndex] = rowState
+ table.push(rowAttrs)
+}
+
+const doSpan = ({ attrs, params, rowSpan, spanMethod }) => {
+ const { rowspan = 1, colspan = 1 } = (spanMethod ? spanMethod(params) : rowSpanMethod(rowSpan, params)) || {}
+
+ attrs.rowspan = rowspan
+ attrs.colspan = colspan
+ attrs.visible = rowspan > 0 && colspan > 0
+}
+
+const rowSpanMethod = (rowSpan, { row, $rowIndex, column, data }) => {
+ const fields = []
+
+ if (column.visible && rowSpan) {
+ rowSpan.forEach((item) => fields.push(item.field))
+
+ const cellVal = row[column.property]
+
+ if (cellVal && fields.includes(column.property)) {
+ const prevSiblingRow = data[$rowIndex - 1]
+ let nextSiblingRow = data[$rowIndex + 1]
+
+ if (prevSiblingRow?.[column.property] === cellVal) {
+ return { rowspan: 0, colspan: 0 }
+ } else {
+ let rowspanCount = 1
+
+ while (nextSiblingRow?.[column.property] === cellVal) {
+ nextSiblingRow = data[++rowspanCount + $rowIndex]
+ }
+
+ if (rowspanCount > 1) {
+ return { rowspan: rowspanCount, colspan: 1 }
+ }
+ }
+ }
+ }
+}
+
+const adjustColspan = (table, pos, isLeft) => {
+ if (pos < 0) {
+ return
+ }
+
+ for (let i = 0; i < table.length; i++) {
+ const row = table[i]
+
+ for (let j = 0; j <= pos && j < row.length; j++) {
+ const { attrs } = row[j]
+ const oldColspan = attrs.colspan
+ let k, posCol
+
+ if (oldColspan > 1 && (k = j + oldColspan - 1) > pos) {
+ attrs.colspan = pos - j + 1
+
+ if (isLeft) {
+ attrs._stickyClass = 'fixed-left-last__column'
+ }
+
+ if ((posCol = row[pos + 1])) {
+ posCol.attrs.colspan = k - pos
+
+ if (posCol.attrs.rowspan < 1) {
+ posCol.attrs.rowspan = attrs.rowspan
+ }
+
+ posCol.attrs.visible = true
+ }
+ }
+ }
+
+ if (!isLeft) {
+ const rightCols = row.slice(pos + 1).reverse()
+ let right = 0
+
+ for (let j = 0; j < rightCols.length; j++) {
+ const { attrs, params } = rightCols[j]
+
+ if (attrs.visible) {
+ attrs._stickyStyle = { right: `${right}px` }
+ right += params.column.renderWidth
+ }
+ }
+ }
+ }
+}
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..da6ce2a63d
--- /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
+}
+
+export 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/useData.ts b/packages/vue/src/grid/src/composable/useData.ts
new file mode 100644
index 0000000000..cbf29c6e66
--- /dev/null
+++ b/packages/vue/src/grid/src/composable/useData.ts
@@ -0,0 +1,126 @@
+import { getRowid } from '@opentiny/vue-renderless/grid/utils'
+
+const isContented = (array) => Array.isArray(array) && array.length > 0
+
+let nid = 0
+
+const structure = ({ array, stack, tiled, map, customMappings, getID, childrenKey, sizeKey }) => {
+ if (!Array.isArray(array)) {
+ return
+ }
+
+ const level = stack.length
+ const nodes = []
+
+ for (let i = 0; i < array.length; i++) {
+ const item = array[i]
+
+ const node = {
+ id: getID(item) || ++nid,
+ payload: item,
+ path: [...stack, item],
+ level,
+ parentNode: level > 0 ? map.get(stack[stack.length - 1]) : undefined,
+ childNodes: undefined,
+ space: { originDistance: 0, size: item[sizeKey] || 36 },
+ mappings: customMappings ? Object.assign({}, customMappings({ payload: item, viewIndex: tiled.length })) : {}
+ }
+
+ tiled.push(node)
+ map.set(item, node)
+ nodes.push(node)
+
+ if (childrenKey) {
+ stack.push(item)
+ node.childNodes = structure({
+ array: item[childrenKey],
+ stack,
+ tiled,
+ map,
+ customMappings,
+ getID,
+ childrenKey,
+ sizeKey
+ })
+ stack.pop()
+ }
+ }
+
+ return nodes
+}
+
+const makeTile = ({ list, getID, childrenKey, sizeKey, customMappings }) => {
+ const tiled = []
+ const map = new WeakMap()
+
+ if (isContented(list)) {
+ structure({
+ array: list,
+ stack: [],
+ tiled,
+ map,
+ customMappings,
+ getID,
+ childrenKey,
+ sizeKey
+ })
+ }
+
+ return { tiled, map }
+}
+
+const getAncestors = (node, map) =>
+ node.parentNode ? node.path.slice(0, node.path.length - 1).map((row) => map.get(row)) : []
+
+const isExpand = (node, isRowExpand) => isRowExpand(node.payload)
+
+const isParentExpand = (node, isRowExpand, map) => getAncestors(node, map).every((p) => isExpand(p, isRowExpand))
+
+const makeGraph = ({ isRowExpand, tileInfo }) => {
+ const { tiled, map } = tileInfo
+ const graphed = []
+ let scrollSize = 0
+
+ for (let i = 0; i < tiled.length; i++) {
+ const node = tiled[i]
+
+ if (!node.parentNode || isParentExpand(node, isRowExpand, map)) {
+ node.space.originDistance = scrollSize
+ scrollSize += node.space.size
+ graphed.push(node)
+ }
+ }
+
+ return { graphed, scrollSize }
+}
+
+export const buildRenderGraph = ($table) => {
+ tileFullData($table)
+ graphFullData($table)
+}
+
+export const tileFullData = ($table) => {
+ const { treeConfig, rowGroup, groupFullData, afterFullData } = $table
+
+ // @ts-expect-error
+ const tileInfo = makeTile({
+ list: !treeConfig && rowGroup?.field ? groupFullData : afterFullData,
+ getID: (row) => getRowid($table, row),
+ childrenKey: treeConfig ? treeConfig.children : undefined
+ })
+
+ $table._tileInfo = tileInfo
+}
+
+export const graphFullData = ($table) => {
+ const { treeConfig, treeExpandeds, _tileInfo } = $table
+
+ if (_tileInfo) {
+ const graphInfo = makeGraph({
+ isRowExpand: (row) => (treeConfig ? treeExpandeds.includes(row) : true),
+ tileInfo: $table._tileInfo
+ })
+
+ $table._graphInfo = graphInfo
+ }
+}
diff --git a/packages/vue/src/grid/src/composable/useDrag/index.ts b/packages/vue/src/grid/src/composable/useDrag/index.ts
index 25b1c8022f..d4ba114834 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,9 @@ const getColidMap = (treeArray) => {
}
const createDragHander = (state, $table) => {
+ const dropConfig = state.dropConfig
+ const dropable = dropConfig?.column && dropConfig?.scheme === 'v2'
+
// 开始拖拽处理
const dragStart = (dragTarget) => {
const dragColid = dragTarget.dataset.colid
@@ -84,6 +89,7 @@ const createDragHander = (state, $table) => {
const dragIndex = dragParentChildren.indexOf(dragColumn)
$table.$emit('column-drag-start', { dragParentChildren, dragColumn, dragIndex })
+ clearTimeout(timer)
}
// 放置结束处理
@@ -103,7 +109,7 @@ const createDragHander = (state, $table) => {
// 拖拽信息参数
const args = { dragParentChildren, dragColumn, dragIndex, dropParentChildren, dropColumn, dropIndex }
// 放置前处理
- callInterceptor(state.dropConfig.columnBeforeDrop, {
+ callInterceptor(state.dropConfig?.columnBeforeDrop, {
args: [args],
done: () => {
// 移除被拖拽列,并插入到被放置的位置
@@ -123,6 +129,13 @@ const createDragHander = (state, $table) => {
scrollYLoad && $table.triggerScrollYEvent({ target: { scrollTop: lastScrollTop } })
}
})
+
+ // 2s后用户没有再次拖动排序再保存数据,防止事件频繁触发消耗性能
+ if ($table.getVm('toolbar') && dropable) {
+ timer = setTimeout(() => {
+ $table.getVm('toolbar').$refs.custom.saveSettings('drag')
+ }, time)
+ }
}
})
}
@@ -132,78 +145,67 @@ const createDragHander = (state, $table) => {
const createTableColumnWatch = ($table, state, isColumnGroupLevel, stopHandlerMap) =>
debounce(100, () => {
- const headers = ['table', 'left', 'right']
+ const dndProxy = $table.$el.querySelector('thead')
- headers.forEach((key) => {
- const headerVm = $table.$refs[`${key}Header`]
+ if (dndProxy) {
+ let dndThs = dndProxy.querySelectorAll(headerTh)
- if (headerVm) {
- const dndProxy = headerVm.$el
-
- if (dndProxy) {
- const dndThs = Array.from(dndProxy.querySelectorAll(headerTh))
+ if (dndThs.length > 0) {
+ dndThs = Array.from(dndThs)
+ }
- // 表头th渲染时已添加data-colid属性,这里额外增加draggable、data-p-colid和data-dnd-group属性
- setDndAttribute(dndThs, state.colidMap, isColumnGroupLevel)
+ // 表头th渲染时已添加data-colid属性,这里额外增加draggable、data-p-colid和data-dnd-group属性
+ setDndAttribute(dndThs, state.colidMap, isColumnGroupLevel)
- if (stopHandlerMap.has(dndProxy)) {
- stopHandlerMap.get(dndProxy).destroy()
- stopHandlerMap.delete(dndProxy)
- }
+ if (stopHandlerMap.has(dndProxy)) {
+ stopHandlerMap.get(dndProxy).destroy()
+ stopHandlerMap.delete(dndProxy)
+ }
- const { dragStart, drop } = createDragHander(state, $table)
- const dropClass = state.dropConfig.columnDropClass || ''
+ const { dragStart, drop } = createDragHander(state, $table)
+ const dropClass = state.dropConfig?.columnDropClass || ''
- stopHandlerMap.set(
- dndProxy,
- initDrag(dndProxy, dndThs, { dragStart, drop, dropClass, groupKey, idKey, pidKey })
- )
- }
- }
- })
+ stopHandlerMap.set(dndProxy, initDrag(dndProxy, dndThs, { dragStart, drop, dropClass, groupKey, idKey, pidKey }))
+ }
})
-const createUseDrag =
- ({ reactive, watch, getCurrentInstance, onBeforeUnmount }) =>
- ({ dropConfig, collectColumn, tableColumn }) => {
- const state = reactive({
- dropConfig,
- collectColumn,
- tableColumn,
- colidMap: null
- })
-
- // 在设置 scheme 标志位 v2 时,列拖拽使用新方案
- if (!state.dropConfig || (state.dropConfig && state.dropConfig.scheme !== 'v2')) return
+export const useDrag = ({ props, collectColumn, tableColumn }) => {
+ const state = hooks.reactive({
+ dropConfig: hooks.toRef(props, 'dropConfig'),
+ collectColumn,
+ tableColumn,
+ colidMap: null
+ })
- const $table = getCurrentInstance().proxy
+ // 在设置 scheme 标志位 v2 时,列拖拽使用新方案
+ if (!state.dropConfig || state.dropConfig?.scheme !== 'v2') return
- // 列拖拽处理
- if (state.dropConfig.column) {
- // 是否只允许同层级拖拽
- const isColumnGroupLevel = !state.dropConfig.columnGroup || state.dropConfig.columnGroup === 'level'
- const stopHandlerMap = new Map()
- const tableColumnWatch = createTableColumnWatch($table, state, isColumnGroupLevel, stopHandlerMap)
+ const $table = hooks.getCurrentInstance()?.proxy
- watch(collectColumn, () => {
- state.colidMap = getColidMap(state.collectColumn)
- })
+ // 列拖拽处理
+ if (state.dropConfig?.column) {
+ // 是否只允许同层级拖拽
+ const isColumnGroupLevel = !state.dropConfig?.columnGroup || state.dropConfig?.columnGroup === 'level'
+ const stopHandlerMap = new Map()
+ const tableColumnWatch = createTableColumnWatch($table, state, isColumnGroupLevel, stopHandlerMap)
- watch(tableColumn, () => tableColumnWatch())
+ hooks.watch(collectColumn, () => {
+ state.colidMap = getColidMap(state.collectColumn)
+ })
- onBeforeUnmount(() => {
- if (stopHandlerMap.size > 0) {
- const dndProxyList = []
+ hooks.watch(tableColumn, () => tableColumnWatch())
- for (const [dndProxy, stopHander] of stopHandlerMap) {
- dndProxyList.push(dndProxy)
- stopHander.destroy()
- }
+ hooks.onBeforeUnmount(() => {
+ if (stopHandlerMap.size > 0) {
+ const dndProxyList = []
- dndProxyList.forEach((dndProxy) => stopHandlerMap.delete(dndProxy))
+ for (const [dndProxy, stopHander] of stopHandlerMap) {
+ dndProxyList.push(dndProxy)
+ stopHander.destroy()
}
- })
- }
- }
-export const useDrag = createUseDrag(hooks)
+ dndProxyList.forEach((dndProxy) => stopHandlerMap.delete(dndProxy))
+ }
+ })
+ }
+}
diff --git a/packages/vue/src/grid/src/composable/useHeader.ts b/packages/vue/src/grid/src/composable/useHeader.ts
new file mode 100644
index 0000000000..9a0af49b21
--- /dev/null
+++ b/packages/vue/src/grid/src/composable/useHeader.ts
@@ -0,0 +1,116 @@
+import { hooks } from '@opentiny/vue-common'
+
+export 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, maxLevel }
+}
+
+const calcSpan = (tableColumn, header, headerRowHeight) => {
+ 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 * headerRowHeight,
+ top: i * headerRowHeight
+ }
+ })
+ })
+
+ return subTable
+}
+
+export const useHeader = (props, bodyVm, headerRowHeight) => {
+ const headerTable = hooks.ref([])
+ const $table = bodyVm.$parent
+ const { showHeader } = $table
+ const header = hooks.ref()
+
+ hooks.watch(
+ () => props.collectColumn,
+ () => {
+ const head = (header.value = calcHeader(props.collectColumn))
+ if (showHeader) {
+ $table.headerHeight = (head.maxLevel + 1) * headerRowHeight.value
+ }
+ }
+ )
+
+ hooks.watch([() => props.tableColumn, header], () => {
+ const head = header.value
+
+ if (showHeader && head) {
+ headerTable.value = calcSpan(props.tableColumn, head, headerRowHeight.value)
+ }
+ })
+
+ return { headerTable }
+}
diff --git a/packages/vue/src/grid/src/composable/useRowGroup.ts b/packages/vue/src/grid/src/composable/useRowGroup.ts
index 140db08dca..5fb141539e 100644
--- a/packages/vue/src/grid/src/composable/useRowGroup.ts
+++ b/packages/vue/src/grid/src/composable/useRowGroup.ts
@@ -1,61 +1,82 @@
import { hooks } from '@opentiny/vue-common'
import { find } from '@opentiny/vue-renderless/grid/static'
-const createUseRowGroup =
- ({ reactive, watch, getCurrentInstance, onBeforeUnmount }) =>
- ({ rowGroup, visibleColumn, tableFullColumn, tableColumn }) => {
- const state = reactive({
- rowGroup,
- visibleColumn,
- tableFullColumn,
- tableColumn
- })
+export const useRowGroup = ({ props, visibleColumn, tableFullColumn, tableColumn, columnStore }) => {
+ const state = hooks.reactive({
+ rowGroup: hooks.toRef(props, 'rowGroup'),
+ visibleColumn,
+ tableFullColumn,
+ tableColumn,
+ columnStore
+ })
- if (!state.rowGroup) return
+ if (!state.rowGroup) return
- const $table = getCurrentInstance().proxy
+ const $table = hooks.getCurrentInstance()?.proxy
- watch([visibleColumn, tableColumn], () => {
- // 取可见列中第一个rowGroup.field作为分组列
- let targetColumn = find(state.visibleColumn, (col) => col.property === state.rowGroup.field)
+ hooks.watch([visibleColumn, tableColumn], () => {
+ // 取可见列中第一个rowGroup.field作为分组列
+ let targetColumn = find(state.visibleColumn, (col) => col.property === state.rowGroup.field)
- // 如果rowGroup.field指定的列不存在或不可见,就取第一个field配置存在的列
- if (!targetColumn) {
- targetColumn = find(state.visibleColumn, (col) => !!col.property)
- }
+ // 如果rowGroup.field指定的列不存在或不可见,就取第一个field配置存在的列
+ if (!targetColumn) {
+ targetColumn = find(state.visibleColumn, (col) => !!col.property)
+ }
+
+ if (targetColumn) {
+ $table._rowGroupTargetColumn = targetColumn
+
+ const index = state.tableColumn.indexOf(targetColumn)
- if (targetColumn) {
- $table._rowGroupTargetColumn = targetColumn
+ const length = state.tableColumn.length
+ let targetColumnColspan = state.rowGroup.colspan || 1
- const index = state.tableColumn.indexOf(targetColumn)
- const length = state.tableColumn.length
- let targetColumnColspan = state.rowGroup.colspan || 1
+ targetColumnColspan = Math.max(targetColumnColspan, 1)
- targetColumnColspan = Math.max(targetColumnColspan, 1)
+ if (targetColumnColspan > 1) {
+ let leftIndex = -1
+ let rightIndex = -1
- if (targetColumnColspan > 1) {
- targetColumnColspan = Math.min(targetColumnColspan, length - index)
+ if ((leftIndex = state.columnStore.leftList.length) > 0) {
+ leftIndex = leftIndex - 1
}
- for (let i = 0; i < length; i++) {
- const vCol = state.tableColumn[i]
+ if (state.columnStore.rightList.length > 0) {
+ rightIndex = state.tableColumn.indexOf(state.columnStore.rightList[0]) - 1
+ }
+
+ let max
- if (vCol === targetColumn) {
- vCol._rowGroupColspan = targetColumnColspan
- } else {
- vCol._rowGroupColspan = i > index && i < index + targetColumnColspan ? 0 : 1
- }
+ if (leftIndex > -1 && index <= leftIndex) {
+ max = leftIndex - index + 1
+ } else if (rightIndex > -1 && index <= rightIndex) {
+ max = rightIndex - index + 1
+ } else {
+ max = length - index
}
+
+ targetColumnColspan = Math.min(targetColumnColspan, max)
}
- })
- onBeforeUnmount(() => {
- delete $table._rowGroupTargetColumn
+ for (let i = 0; i < length; i++) {
+ const vCol = state.tableColumn[i]
- state.tableFullColumn.forEach((column) => {
- delete column._rowGroupColspan
- })
- })
- }
+ if (vCol === targetColumn) {
+ vCol._rowGroupColspan = targetColumnColspan
+ vCol._stickyClass = 'fixed-left-last__column'
+ } else {
+ vCol._rowGroupColspan = i > index && i < index + targetColumnColspan ? 0 : 1
+ }
+ }
+ }
+ })
+
+ hooks.onBeforeUnmount(() => {
+ delete $table._rowGroupTargetColumn
-export const useRowGroup = createUseRowGroup(hooks)
+ state.tableFullColumn.forEach((column) => {
+ delete column._rowGroupColspan
+ delete column._stickyClass
+ })
+ })
+}
diff --git a/packages/vue/src/grid/src/config.ts b/packages/vue/src/grid/src/config.ts
index 8885059514..148dae54f5 100644
--- a/packages/vue/src/grid/src/config.ts
+++ b/packages/vue/src/grid/src/config.ts
@@ -41,6 +41,7 @@ const GlobalConfig = {
message: 'tooltip',
icon: iconError()
},
+ editConfig: { trigger: 'click', mode: 'cell', showStatus: true },
// 默认开启点击头部单元格触发排序
sortConfig: { multipleColumnSort: false },
// 默认不开启隔行换色和行高亮,不暴露此配置
@@ -65,6 +66,7 @@ const GlobalConfig = {
optimization: {
animat: true,
delayHover: 250,
+ scrollDelay: 60,
scrollX: {
gt: 100
},
@@ -166,6 +168,16 @@ const GlobalConfig = {
TINY: 'tiny',
SAAS: 'saas'
},
+ rowHeight: {
+ tiny: { mini: 32, small: 40, default: 48, medium: 52 },
+ saas: { mini: 32, small: 36, default: 36, medium: 40 }
+ },
+ headerRowHeight: {
+ tiny: { mini: 28, small: 32, default: 40, medium: 40 },
+ saas: { mini: 32, small: 36, default: 36, medium: 40 }
+ },
+ // 空数据最小表格高度
+ emptyMinHeight: 200,
columnLevelKey: 'ColumnLevelProvideKey',
defaultColumnName: $prefix + 'GridColumn'
}
diff --git a/packages/vue/src/grid/src/dragger/src/methods.ts b/packages/vue/src/grid/src/dragger/src/methods.ts
index 04ee3d874f..92d703a055 100644
--- a/packages/vue/src/grid/src/dragger/src/methods.ts
+++ b/packages/vue/src/grid/src/dragger/src/methods.ts
@@ -5,7 +5,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 = {
...this.dropConfig,
diff --git a/packages/vue/src/grid/src/edit/src/methods.ts b/packages/vue/src/grid/src/edit/src/methods.ts
index b89d59fd63..936703f28d 100644
--- a/packages/vue/src/grid/src/edit/src/methods.ts
+++ b/packages/vue/src/grid/src/edit/src/methods.ts
@@ -156,7 +156,7 @@ export default {
operArrs({ _vm: this, editStore, newRecords, newRecordsCopy, nowData, row, tableFullData, tableSourceData })
- this.updateCache(true)
+ this.updateCache()
this.handleTableData(true)
this.checkSelectionStatus()
this.updateFooter()
@@ -234,7 +234,7 @@ export default {
remove(insertList, (row) => inArr(row, rows))
// 修改缓存
- this.updateCache(true)
+ this.updateCache()
this.handleTableData(true)
this.checkSelectionStatus()
@@ -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(actived.row)
this.updateFooter()
// 处理数字输入框返回string类型数据,导致还原初始数字还是编辑状态的问题
@@ -558,7 +561,7 @@ export default {
/**
* 处理选中源
*/
- handleSelected(params, event) {
+ handleSelected(params, event, noDebounce) {
let { editConfig, editStore, elemStore, mouseConfig = {} } = this
let { actived, selected } = editStore
let { cell, column, row } = params || {}
@@ -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,13 +600,13 @@ 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()
}
- selectMethod = debounce(20, selectMethod)
+ selectMethod = noDebounce ? selectMethod : debounce(20, selectMethod)
return selectMethod()
}
diff --git a/packages/vue/src/grid/src/fetch-data/src/methods.ts b/packages/vue/src/grid/src/fetch-data/src/methods.ts
index db9c8a1f6a..4084c50573 100644
--- a/packages/vue/src/grid/src/fetch-data/src/methods.ts
+++ b/packages/vue/src/grid/src/fetch-data/src/methods.ts
@@ -25,6 +25,8 @@ export default {
},
handleFetch(code, sortArg) {
let { pager, sortData, filterData, pagerConfig, fetchOption, fetchData, dataset } = this as any
+ let { reloadConfig = {} } = fetchData
+ let { scroll = false } = reloadConfig
if (this.isInitialLoading) {
this.isInitialLoading = false
@@ -34,7 +36,7 @@ export default {
if (code !== 'prefetch') {
this.clearRadioRow()
- this.resetScrollTop()
+ !scroll && this.resetScrollTop()
}
if (!fetchOption) {
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/grid/grid.ts b/packages/vue/src/grid/src/grid/grid.ts
index f6047336c4..aa94cda7b2 100644
--- a/packages/vue/src/grid/src/grid/grid.ts
+++ b/packages/vue/src/grid/src/grid/grid.ts
@@ -81,7 +81,7 @@ function createRender(opt) {
}
},
[
- selectToolbar ? null : renderedToolbar,
+ selectToolbar ? null : renderedToolbar(),
columnAnchor ? _vm.renderColumnAnchor(columnAnchorParams, _vm) : null,
// 这里会渲染tiny-grid-column插槽内容,从而获取列配置
h(TinyGridTable, { props, on: tableOns, ref: 'tinyTable' }, slots.default && slots.default()),
@@ -159,7 +159,8 @@ export default defineComponent({
columnAnchorKey: '',
tasks: {},
fullScreenClass: '',
- isInitialLoading: true // 是否首次加载数据
+ isInitialLoading: true, // 是否首次加载数据
+ _delayActivateAnchor: undefined
}
},
computed: {
@@ -201,6 +202,19 @@ export default defineComponent({
},
isViewCustom() {
return this.viewType === V_CUSTOM
+ },
+ optimizOpt() {
+ return Object.assign(this.initOptimization, GlobalConfig.optimization, this.optimization)
+ },
+ editConfigOpt() {
+ return this.editConfig
+ ? Object.assign(this.initEditConfig, GlobalConfig.editConfig, this.editConfig, {
+ activeMethod: this.handleActiveMethod
+ })
+ : null
+ },
+ tooltipOpt() {
+ return Object.assign(this.initTooltipConfig, GlobalConfig.tooltip, this.designConfig?.tooltip, this.tooltipConfig)
}
},
watch: {
@@ -211,13 +225,10 @@ export default defineComponent({
tableCustoms() {
this.toolbar && this.$refs.toolbar && this.$refs.toolbar.loadStorage()
},
- columnAnchorParams() {
- setTimeout(() => this.emitter.emit('active-anchor'), this.columnAnchorParams.activeAnchor.delay)
- },
viewType(value) {
// 解决从卡片、列表视图切换至表格视图后,列宽未自动撑开问题
if (value === V_MF) {
- this.$nextTick(() => this.recalculate(true))
+ this.$nextTick(() => this.recalculate())
}
}
},
@@ -305,11 +316,12 @@ export default defineComponent({
if (this.isMultipleHistory) {
this.initMultipleHistory()
}
-
- this.addIntersectionObserver()
},
setup(props, context) {
const { listeners, attrs } = context
+ const initEditConfig = hooks.ref({})
+ const initOptimization = hooks.ref({})
+ const initTooltipConfig = hooks.ref({})
// 处理表格用户传递过来的事件监听
const tableListeners = getListeners(attrs, listeners)
const tinyTheme = hooks.ref(resolveTheme(props, context))
@@ -317,13 +329,20 @@ export default defineComponent({
const breakpoint = useBreakpoint()
const renderless = (props, hooks, { designConfig = null }) => {
- return { tableListeners, designConfig, tinyTheme, tinyMode, currentBreakpoint: breakpoint.current }
+ return {
+ tableListeners,
+ designConfig,
+ tinyTheme,
+ tinyMode,
+ currentBreakpoint: breakpoint.current,
+ initEditConfig,
+ initOptimization,
+ initTooltipConfig
+ }
}
hooks.onBeforeUnmount(() => {
const gridVm = hooks.getCurrentInstance().proxy
-
- gridVm.removeIntersectionObserver()
// 清空被缓存实例
gridVm.vmStore = null
})
@@ -332,16 +351,26 @@ export default defineComponent({
props,
context,
renderless,
- api: ['designConfig', 'tableListeners', 'tinyTheme', 'tinyMode', 'currentBreakpoint']
+ api: [
+ 'designConfig',
+ 'tableListeners',
+ 'tinyTheme',
+ 'tinyMode',
+ 'currentBreakpoint',
+ 'initEditConfig',
+ 'initOptimization',
+ 'initTooltipConfig'
+ ]
})
},
render() {
const {
- editConfig,
fetchOption,
listeners,
loading,
- optimization,
+ optimizOpt,
+ editConfigOpt,
+ tooltipOpt,
pager,
pagerConfig,
remoteFilter,
@@ -368,17 +397,12 @@ export default defineComponent({
Object.assign(GlobalConfig.icon, designConfig.icons)
}
- // 初始化虚拟滚动优化配置
- const optimizOpt = { ...GlobalConfig.optimization, ...optimization }
- const props = { ...tableProps, optimization: optimizOpt, startIndex: seqIndex }
-
- // 初始化 tooltip 配置
- props.tooltipConfig = Object.assign(
- {},
- GlobalConfig.tooltip || {},
- designConfig?.tooltip || {},
- props.tooltipConfig || {}
- )
+ const props = Object.assign(tableProps, {
+ optimization: optimizOpt,
+ startIndex: seqIndex,
+ editConfig: editConfigOpt,
+ tooltipConfig: tooltipOpt
+ })
// 在用户没有配置stripe时读取design配置
if (designConfig?.stripe !== undefined && !props.stripe) {
@@ -386,7 +410,7 @@ export default defineComponent({
props.stripe = designConfig?.stripe
}
- const tableOns = { ...listeners, ...tableListeners }
+ const tableOns = Object.assign(listeners, tableListeners)
const { handleRowClassName: rowClassName, sortChangeEvent, filterChangeEvent } = this
// fetchApi状态下初始化 loading、remoteSort、remoteFilter
@@ -402,19 +426,8 @@ export default defineComponent({
// 列就绪事件处理
tableOns['column-init-ready'] = this.handleColumnInitReady
- // 这里handleActiveMethod处理一些编辑器的声明周期的拦截,用户传递过来的activeMethod优先级最高
- if (editConfig) {
- props.editConfig = {
- trigger: 'click',
- mode: 'cell',
- showStatus: true,
- ...editConfig,
- activeMethod: this.handleActiveMethod
- }
- }
-
// 获取工具栏的渲染器
- const renderedToolbar = this.getRenderedToolbar({ $slots, _vm: this, loading, tableLoading, toolbar })
+ const renderedToolbar = () => this.getRenderedToolbar({ $slots, _vm: this, loading, tableLoading, toolbar })
// 创建表格最外层容器,并加载table组件
return createRender({
@@ -527,34 +540,6 @@ export default defineComponent({
viewCls(module) {
return GlobalConfig.viewConfig[module][this.viewType] || ''
},
- // 监听某个元素是否出现在视口中
- addIntersectionObserver() {
- if ((this.intersectionOption && this.intersectionOption.disabled) || typeof IntersectionObserver === 'undefined')
- return
-
- this.intersectionObserver = new IntersectionObserver((entries) => {
- let entry = entries[0]
-
- if (entries.length > 1) {
- const intersectingEntry = entries.find((entry) => entry.isIntersecting)
-
- if (intersectingEntry) {
- entry = intersectingEntry
- }
- }
-
- this.handleVisibilityChange(entry.isIntersecting, entry)
- }, this.intersectionOption)
-
- this.intersectionObserver.observe(this.$el)
- },
- removeIntersectionObserver() {
- if (this.intersectionObserver) {
- this.intersectionObserver.unobserve(this.$el)
- this.intersectionObserver.disconnect()
- this.intersectionObserver = null
- }
- },
filterChangeEvent(params) {
let eventParams = extend(false, { $grid: this }, params)
// 如果是服务端过滤
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..2630ffe6a5 100644
--- a/packages/vue/src/grid/src/keyboard/src/methods.ts
+++ b/packages/vue/src/grid/src/keyboard/src/methods.ts
@@ -35,7 +35,7 @@ import {
handleCellMousedownEvent
} from './utils/triggerCellMousedownEvent'
import { handleHeaderCellMousedownEvent } from './utils/triggerHeaderCellMousedownEvent'
-import { warn, Formatter } from '../../tools'
+import { warn } from '../../tools'
const removeCellClass = (bodyRef, clazz) =>
arrayEach(bodyRef.$el.querySelectorAll('.' + clazz), (elem) => removeClass(elem, clazz))
@@ -59,31 +59,11 @@ const getModify = ({ offsetTop, offsetLeft, cWidth, cHeight }) => {
}
}
-const writeClipboardText = ({ $table, columns, rows }) => {
- const { keyboardConfig = {}, isAsyncColumn } = $table
+const writeClipboardText = ({ $table, columns, rows, rowNodes }) => {
+ const { keyboardConfig = {} } = $table
const { clipboard = {} } = keyboardConfig
const { writeMethod, cellSplit = ',', rowSplit = ';' } = clipboard
- const getCellValue = (column, row) => {
- let cellValue = ''
-
- if (isAsyncColumn) {
- const format = column.format || {}
-
- if (format.async === true && format.type === 'enum') {
- cellValue = Formatter.enum.call(column, row[column.property])
- } else if (format.async && typeof format.async.fetch === 'function') {
- cellValue = row[$table.getAsyncColumnName(column.property)]
- } else {
- cellValue = row[column.property]
- }
- } else {
- cellValue = row[column.property]
- }
-
- return cellValue || ''
- }
-
if (!clipboard) return
let value
@@ -93,12 +73,11 @@ const writeClipboardText = ({ $table, columns, rows }) => {
} else {
const rowValues = []
- rows.forEach((row) => {
+ rowNodes.forEach((row) => {
const cellValues = []
- columns.forEach((column) => {
- const cellValue = getCellValue(column, row)
- cellValues.push(cellValue)
+ row.forEach((col) => {
+ cellValues.push(col && col.innerText)
})
rowValues.push(cellValues.join(cellSplit))
@@ -231,7 +210,7 @@ export default {
getCell(this, params).then((resCell) => {
params.cell = resCell
- this.handleSelected(params, event)
+ this.handleSelected(params, event, true)
this.scrollToRow(params.row, params.column, false, {
isLeftArrow,
isRightArrow,
@@ -242,7 +221,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
@@ -298,11 +277,15 @@ export default {
triggerCellMousedownEvent(event, params) {
let { $el, editConfig, editStore, elemStore, mouseConfig = {}, visibleColumn } = this
let { actived, checked } = editStore
+ let { excludes: excludeClo = [] } = mouseConfig
let { button } = event
let { cell, column, row } = params
let isLeftBtn = button === 0
let args
+ if (excludeClo.includes(column.type || column.property)) {
+ return
+ }
if (
editConfig &&
(actived.row !== row || !(editConfig.mode === 'cell' && actived.column === column)) &&
@@ -317,12 +300,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 +337,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 +460,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)
@@ -492,7 +475,7 @@ export default {
getCell(this, params).then((resCell) => {
params.cell = resCell
- this.handleSelected(params, event)
+ this.handleSelected(params, event, true)
this.handleHeaderChecked(
getRowNodes(
@@ -546,7 +529,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')
@@ -597,7 +580,7 @@ export default {
columns = tableColumn.slice(columnIndex, columnIndex + firstRowsLength)
rows = tableData.slice(rowIndex, rowIndex + rowNodes.length)
- writeClipboardText({ $table: this, columns, rows })
+ writeClipboardText({ $table: this, columns, rows, rowNodes })
}
arrayEach(rowNodes, (rowNode, rowIndex) => {
@@ -662,7 +645,7 @@ export default {
let cell = selected.args.cell
let bodyList = elemStore['main-body-list'].children
- let { rIndex, cIndex } = getCellIndex({ cell, elemStore, bodyList })
+ let { rIndex, cIndex } = getCellIndex({ cell, bodyList })
let maxIndex = bodyList.length - 1
let curIndex = rIndex + rows.length - 1
let targetTrElem = bodyList[curIndex > maxIndex ? maxIndex : curIndex]
diff --git a/packages/vue/src/grid/src/menu/src/methods.ts b/packages/vue/src/grid/src/menu/src/methods.ts
index 661b76266b..5c9571b969 100644
--- a/packages/vue/src/grid/src/menu/src/methods.ts
+++ b/packages/vue/src/grid/src/menu/src/methods.ts
@@ -100,14 +100,15 @@ export default {
},
// 快捷菜单事件处理
handleGlobalContextmenuEvent(event) {
- let { ctxMenuOpts, ctxMenuStore, isCtxMenu } = this
- let layoutList = ['header', 'body', 'footer']
+ const { ctxMenuOpts, ctxMenuStore, isCtxMenu } = this
if (!isCtxMenu) {
this.closeMenu()
this.closeFilter()
+
return
}
+
if (
ctxMenuStore.visible &&
this.$refs.ctxWrapper &&
@@ -117,42 +118,47 @@ export default {
return
}
- for (let i = 0; i < layoutList.length; i++) {
- let layout = layoutList[i]
- let eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid-${layout}__column`)
- let eventParams = { $table: this, columns: this.visibleColumn.slice(0), type: layout }
+ let eventTargetNode
+ const eventParams = { $table: this, columns: this.visibleColumn.slice(0) }
+
+ for (let layout of ['header', 'body', 'footer']) {
+ eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid-${layout}__column`)
+ eventParams.type = layout
if (eventTargetNode.flag) {
- let cell = eventTargetNode.targetElem
- let column = this.getColumnNode(cell)?.item
- if (!column) {
- return
- }
+ const cell = eventTargetNode.targetElem
+ const column = this.getColumnNode(cell).item
let typePrefix = `${layout}-`
+
Object.assign(eventParams, { cell, column, columnIndex: this.getColumnIndex(column) })
if (layout === 'body') {
- let row = this.getRowNode(cell.parentNode).item
+ const row = this.getRowNode(cell.parentNode).item
+
typePrefix = ''
Object.assign(eventParams, { row, rowIndex: this.getRowIndex(row) })
}
this.openContextMenu(event, layout, eventParams)
emitEvent(this, `${typePrefix}cell-context-menu`, [eventParams, event])
+
return
}
+ }
- eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid__${layout}-wrapper`)
+ eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid__body-wrapper`)
- if (eventTargetNode.flag) {
- if (ctxMenuOpts.trigger === 'cell') {
- event.preventDefault()
- } else {
- this.openContextMenu(event, layout, eventParams)
- }
- return
+ if (eventTargetNode.flag) {
+ if (ctxMenuOpts.trigger === 'cell') {
+ event.preventDefault()
+ } else {
+ eventParams.type = 'body'
+ this.openContextMenu(event, layout, eventParams)
}
+
+ return
}
+
this.closeMenu()
this.closeFilter()
},
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 @@