Skip to content

refactor: optimize table performance and refactor the table #3514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(/员工数/)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Assertion no longer validates “fixed” behaviour – only text.

The previous test verified the header cell’s right style to ensure the column is actually fixed.
By switching to a pure text check we lose coverage of the sticky-column behaviour that this demo is supposed to showcase. At minimum, assert that the cell has the expected CSS class (col__fixed) or computed style (position: sticky, left/right offset), e.g.:

-await expect(demo.locator('.tiny-grid-header__row th').nth(3)).toHaveText(/员工数/)
+const target = demo.locator('.tiny-grid-header__row th').nth(3)
+await expect(target).toHaveText(/员工数/)
+await expect(target).toHaveClass(/col__fixed/)      // or style check
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(demo.locator('.tiny-grid-header__row th').nth(3)).toHaveText(//)
// replace the lone text assertion with these lines to also verify the fixed column
const target = demo.locator('.tiny-grid-header__row th').nth(3)
await expect(target).toHaveText(//)
await expect(target).toHaveClass(/col__fixed/)
🤖 Prompt for AI Agents
In examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js at line 12, the
test only checks the header cell text and does not verify the fixed column
behavior. Update the assertion to also check that the header cell has the CSS
class 'col__fixed' or verify its computed style includes 'position: sticky' with
the appropriate left or right offset to confirm the column is fixed as intended.

})
2 changes: 1 addition & 1 deletion examples/sites/demos/pc/app/grid/custom/page-size.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
9 changes: 7 additions & 2 deletions examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
})
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
})
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
Original file line number Diff line number Diff line change
Expand Up @@ -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 公司')
})
2 changes: 1 addition & 1 deletion examples/sites/demos/pc/app/grid/webdoc/grid-empty.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default {
name: { 'zh-CN': '固定居中', 'en-US': 'Fix Center' },
desc: {
'zh-CN':
'<p>配置 <code>is-center-empty</code> 为 <code>true</code> 时,拖动横向滚动条可以保持空数据提示使终相对表格宽度居中显示。</p>\n',
'<p>(从3.25.0版本开始默认固定居中)配置 <code>is-center-empty</code> 为 <code>true</code> 时,拖动横向滚动条可以保持空数据提示使终相对表格宽度居中显示。</p>\n',
'en-US':
'<p>When <code>is-center-empty</code> is set to <code>true</code>, 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</p>\n'
},
Expand Down
1 change: 1 addition & 0 deletions examples/sites/demos/pc/app/icon/iconGroups.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export const iconGroups = {
'IconTabletView',
'IconUnlock',
'IconUser',
'IconDelegatedProcessing',
'IconVersiontree',
'IconWebPlus',
'IconJs',
Expand Down
3 changes: 1 addition & 2 deletions packages/renderless/src/grid/plugins/exportExcel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]*)\)$/
Expand Down
187 changes: 118 additions & 69 deletions packages/renderless/src/grid/utils/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +120 to +124
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Null-safety for config.constant

this.asyncPrefix = config.constant.asyncPrefix will throw when
getColumnConfig(context, …) is called without passing the global config object.

-    this.asyncPrefix = config.constant.asyncPrefix
+    this.asyncPrefix = config?.constant?.asyncPrefix

Either add this guard or assert in the public factory that config.constant is provided.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 单元格插槽,只对 grid 有效
this.slots = context.slots
this.own = context
this.asyncPrefix = config.constant.asyncPrefix
}
// 单元格插槽,只对 grid 有效
this.slots = context.slots
this.own = context
this.asyncPrefix = config?.constant?.asyncPrefix
}
🤖 Prompt for AI Agents
In packages/renderless/src/grid/utils/column.ts around lines 120 to 124, the
assignment to this.asyncPrefix directly accesses config.constant.asyncPrefix
without checking if config.constant exists, which can cause a runtime error if
config.constant is undefined. To fix this, add a null check or guard to ensure
config.constant is defined before accessing asyncPrefix, or alternatively, add
an assertion in the public factory method to guarantee that config.constant is
always provided when calling getColumnConfig.


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) =>
Expand Down
Loading
Loading