Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
node_modules
.pnpm-store/
dist/
dist2/
dist2.7/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<template>
<tiny-grid :row-span="rowSpanFields" :data="tableData">
<tiny-grid-column type="index" width="60"></tiny-grid-column>
<tiny-grid-column type="expand" width="60">
<template #default="{ row }">
<ul>
<li>
<span>公司名称:</span>
<span>{{ row.name }}</span>
</li>
<li>
<span>区域:</span>
<span>{{ row.area }}</span>
</li>
<li>
<span>城市:</span>
<span>{{ row.city }}</span>
</li>
</ul>
</template>
</tiny-grid-column>
<tiny-grid-column field="area" title="区域"></tiny-grid-column>
<tiny-grid-column field="province" title="省份"></tiny-grid-column>
<tiny-grid-column field="city" title="城市"></tiny-grid-column>
<tiny-grid-column field="name" title="公司名称"></tiny-grid-column>
</tiny-grid>
</template>

<script setup>
import { ref } from 'vue'
import { TinyGrid, TinyGridColumn } from '@opentiny/vue'

const rowSpanFields = ref([{ field: 'area' }, { field: 'province' }])
const tableData = ref([
{
id: '1',
name: 'WWW 科技 YX 公司',
area: '华南区',
province: '广东省',
city: '深圳'
},
{
id: '2',
name: 'RFV 有限责任公司',
area: '华南区',
province: '广东省',
city: '中山'
},
{
id: '3',
name: 'YHN 科技 YX 公司',
area: '华南区',
province: '广东省',
city: '韶关'
},
{
id: '4',
name: 'GFD 科技 YX 公司',
area: '华东区',
province: '福建省',
city: '福州'
},
{
id: '5',
name: 'TGB 科技 YX 公司',
area: '华东区',
province: '福建省',
city: '龙岩'
}
])
</script>
23 changes: 23 additions & 0 deletions examples/sites/demos/pc/app/grid/span/row-span-with-expand.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test, expect } from '@playwright/test'

test('行合并与展开行:展开为全宽且不重叠', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-span#span-row-span-with-expand')

const areaCell = page.getByRole('cell', { name: '华南区' }).first()
await expect(areaCell).toHaveAttribute('rowspan', '3')

await page.getByRole('row', { name: /中山/ }).locator('.tiny-grid__expanded').click()
const expandRow = page.locator('.tiny-grid-body__expanded-row')
await expect(expandRow).toBeVisible()
await expect(page.locator('.tiny-grid-body__expanded-cell')).toContainText('中山')
await expect(expandRow.locator('td')).toHaveAttribute('colspan', '6')
await expect(areaCell).toHaveAttribute('rowspan', '2')

const shaoguanRow = page.getByRole('row', { name: /韶关/ })
await expect(shaoguanRow.getByRole('cell', { name: '华南区' })).toBeVisible()

const expandBox = await expandRow.boundingBox()
const areaBox = await areaCell.boundingBox()
expect(expandBox.y).toBeGreaterThanOrEqual(areaBox.y + areaBox.height - 2)
})
80 changes: 80 additions & 0 deletions examples/sites/demos/pc/app/grid/span/row-span-with-expand.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<template>
<tiny-grid :row-span="rowSpanFields" :data="tableData">
<tiny-grid-column type="index" width="60"></tiny-grid-column>
<tiny-grid-column type="expand" width="60">
<template #default="{ row }">
<ul>
<li>
<span>公司名称:</span>
<span>{{ row.name }}</span>
</li>
<li>
<span>区域:</span>
<span>{{ row.area }}</span>
</li>
<li>
<span>城市:</span>
<span>{{ row.city }}</span>
</li>
</ul>
</template>
</tiny-grid-column>
<tiny-grid-column field="area" title="区域"></tiny-grid-column>
<tiny-grid-column field="province" title="省份"></tiny-grid-column>
<tiny-grid-column field="city" title="城市"></tiny-grid-column>
<tiny-grid-column field="name" title="公司名称"></tiny-grid-column>
</tiny-grid>
</template>

<script>
import { TinyGrid, TinyGridColumn } from '@opentiny/vue'

export default {
components: {
TinyGrid,
TinyGridColumn
},
data() {
return {
rowSpanFields: [{ field: 'area' }, { field: 'province' }],
tableData: [
{
id: '1',
name: 'WWW 科技 YX 公司',
area: '华南区',
province: '广东省',
city: '深圳'
},
{
id: '2',
name: 'RFV 有限责任公司',
area: '华南区',
province: '广东省',
city: '中山'
},
{
id: '3',
name: 'YHN 科技 YX 公司',
area: '华南区',
province: '广东省',
city: '韶关'
},
{
id: '4',
name: 'GFD 科技 YX 公司',
area: '华东区',
province: '福建省',
city: '福州'
},
{
id: '5',
name: 'TGB 科技 YX 公司',
area: '华东区',
province: '福建省',
city: '龙岩'
}
]
}
}
}
</script>
14 changes: 14 additions & 0 deletions examples/sites/demos/pc/app/grid/webdoc/grid-span.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ export default {
},
codeFiles: ['span/row-span.vue']
},
{
demoId: 'span-row-span-with-expand',
name: {
'zh-CN': '行合并与展开行',
'en-US': 'Row Span with Expand'
},
desc: {
'zh-CN':
'<p>同时配置 <code>row-span</code> 与列 <code>type=&quot;expand&quot;</code>。展开行占满整行宽度;合并在展开处断开,避免与展开内容重叠。</p>\n',
'en-US':
'<p>Use <code>row-span</code> with column <code>type=&quot;expand&quot;</code>. The expand row is full width; merges split at the expand so content does not overlap. </p>\n'
},
codeFiles: ['span/row-span-with-expand.vue']
},
{
demoId: 'span-column-span',
name: { 'zh-CN': '列合并', 'en-US': 'Column Merge' },
Expand Down
100 changes: 100 additions & 0 deletions packages/vue/src/grid/__tests__/split-rowspan-at-expand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, expect, test } from 'vitest'
import { splitRowspanAtExpand } from '../src/composable/useCellSpan'

const col = (property) => ({ property, id: property })

const cell = (rowspan, column, row, extra = {}) => ({
attrs: { rowspan, colspan: rowspan > 0 ? 1 : 0, visible: rowspan > 0, _dataRowspan: rowspan, ...extra },
params: { column, row }
})

describe('splitRowspanAtExpand', () => {
test('no-op without expandeds', () => {
const area = col('area')
const data = [{ area: 'A' }, { area: 'A' }, { area: 'A' }]
const rows = [[cell(3, area, data[0])], [cell(0, area, data[1])], [cell(0, area, data[2])]]
splitRowspanAtExpand(rows, { expandeds: [], rowSpan: [{ field: 'area' }] })
expect(rows.map((r) => r[0].attrs.rowspan)).toEqual([3, 0, 0])
})

test('expand without merges is identity', () => {
const city = col('city')
const r0 = { city: '深圳' }
const r1 = { city: '中山' }
const rows = [[cell(1, city, r0)], [cell(1, city, r1)]]
splitRowspanAtExpand(rows, { expandeds: [r0], rowSpan: [] })
expect(rows.map((r) => r[0].attrs.rowspan)).toEqual([1, 1])
expect(rows[0][0].attrs.colspan).toBe(1)
})

test('splits merge when middle row expands (#4200)', () => {
const area = col('area')
const r0 = { area: '华南区' }
const r1 = { area: '华南区' }
const r2 = { area: '华南区' }
const rows = [[cell(3, area, r0)], [cell(0, area, r1)], [cell(0, area, r2)]]

splitRowspanAtExpand(rows, {
expandeds: [r1],
rowSpan: [{ field: 'area' }]
})

expect(rows[0][0].attrs).toMatchObject({ rowspan: 2, visible: true })
expect(rows[1][0].attrs).toMatchObject({ rowspan: 0, visible: false })
expect(rows[2][0].attrs).toMatchObject({ rowspan: 1, visible: true })
})

test('expand on first row restarts merge below', () => {
const area = col('area')
const r0 = { area: 'A' }
const r1 = { area: 'A' }
const r2 = { area: 'A' }
const rows = [[cell(3, area, r0)], [cell(0, area, r1)], [cell(0, area, r2)]]

splitRowspanAtExpand(rows, {
expandeds: [r0],
rowSpan: [{ field: 'area' }]
})

expect(rows[0][0].attrs.rowspan).toBe(1)
expect(rows[1][0].attrs).toMatchObject({ rowspan: 2, visible: true })
expect(rows[2][0].attrs).toMatchObject({ rowspan: 0, visible: false })
})

test('expand on last row of group keeps full merge above', () => {
const area = col('area')
const r0 = { area: 'A' }
const r1 = { area: 'A' }
const r2 = { area: 'A' }
const rows = [[cell(3, area, r0)], [cell(0, area, r1)], [cell(0, area, r2)]]

splitRowspanAtExpand(rows, {
expandeds: [r2],
rowSpan: [{ field: 'area' }]
})

expect(rows.map((r) => r[0].attrs.rowspan)).toEqual([3, 0, 0])
})

test('spanMethod covered cells stay hidden after expand', () => {
const a = col('a')
const b = col('b')
const r0 = { a: 1, b: 2 }
const r1 = { a: 1, b: 2 }
const covered = (column, row) => cell(0, column, row, { colspan: 0, visible: false, _dataRowspan: 0 })
const rows = [
[cell(2, a, r0, { colspan: 2, _dataRowspan: 2 }), covered(b, r0)],
[covered(a, r1), covered(b, r1)]
]

splitRowspanAtExpand(rows, {
expandeds: [r0],
spanMethod: () => ({})
})

expect(rows[0][0].attrs).toMatchObject({ rowspan: 1, colspan: 2, visible: true })
expect(rows[0][1].attrs).toMatchObject({ rowspan: 0, colspan: 0, visible: false })
expect(rows[1][0].attrs).toMatchObject({ rowspan: 0, colspan: 0, visible: false })
expect(rows[1][1].attrs).toMatchObject({ rowspan: 0, colspan: 0, visible: false })
})
})
64 changes: 63 additions & 1 deletion packages/vue/src/grid/src/composable/useCellSpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const useCellSpan = (bodyVm, bodyProps) => {
() => $table.visibleColumn,
() => bodyProps.tableData,
() => $table.isColumnWidthAssigned,
() => $table.columnStore.resizeList
() => $table.columnStore.resizeList,
() => $table.expandeds.map((r) => getRowid($table, r)).join()
],
([visibleColumn, tableData, isColumnWidthAssigned]) => {
if (!Array.isArray(tableData) || !isColumnWidthAssigned) {
Expand Down Expand Up @@ -76,6 +77,8 @@ export const useCellSpan = (bodyVm, bodyProps) => {
stateNormalCell(normalState, normalTable, params, $table)
}

splitRowspanAtExpand(normalTable, $table)

if (leftList.length > 0) {
adjustColspan(normalTable, leftList.length - 1, true)
}
Expand Down Expand Up @@ -184,6 +187,7 @@ const doSpan = ({ attrs, params, rowSpan, spanMethod }) => {

attrs.rowspan = rowspan
attrs.colspan = colspan
attrs._dataRowspan = rowspan
attrs.visible = rowspan > 0 && colspan > 0
}

Expand Down Expand Up @@ -216,6 +220,64 @@ const rowSpanMethod = (rowSpan, { row, $rowIndex, column, data }) => {
}
}

export const splitRowspanAtExpand = (normalTable, $table) => {
const expandeds = $table.expandeds
if (!expandeds?.length || !normalTable.length) return

const expandedSet = new Set(expandeds)
const colCount = normalTable[0].length
const hasSpanMethod = typeof $table.spanMethod === 'function'
const rowAt = (r) => normalTable[r][0].params.row

for (let c = 0; c < colCount; c++) {
let coverUntil = -1
for (let r = 0; r < normalTable.length; r++) {
const { attrs, params } = normalTable[r][c]
if (r <= coverUntil) {
attrs.rowspan = 0
attrs.colspan = 0
attrs.visible = false
continue
}

let intended = attrs._dataRowspan
if (!(intended > 0)) {
if (hasSpanMethod) continue
intended = remainingEqualSpan(normalTable, r, params.column, $table)
if (!(intended > 0)) continue
}

let span = 1
while (span < intended && r + span < normalTable.length) {
if (expandedSet.has(rowAt(r + span - 1))) break
span++
}

attrs.rowspan = span
if (!(attrs.colspan > 0)) attrs.colspan = 1
attrs.visible = true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
coverUntil = r + span - 1
}
}
}

const remainingEqualSpan = (normalTable, start, column, $table) => {
const { rowSpan, spanMethod } = $table
if (spanMethod || !rowSpan?.length || !column?.property) return 1

const fields = rowSpan.map((item) => item.field)
if (!fields.includes(column.property)) return 1

const cellVal = normalTable[start][0].params.row?.[column.property]
if (!cellVal) return 1

let count = 1
while (start + count < normalTable.length && normalTable[start + count][0].params.row[column.property] === cellVal) {
count++
}
return count
}

const adjustColspan = (table, pos, isLeft) => {
if (pos < 0) {
return
Expand Down
Loading