feat(web-admin): 列表 Excel 查看改为 Univer 只读在线表格

点击工具栏打开可筛选、复制的只读弹层,替换原行列点选复制模式,且不写回业务数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-14 23:25:18 +08:00
1 parent 43e240ad6a
commit aad74505c0
10 files changed
+1901 -374

No files matched your search

@@ -1,11 +1,13 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
buildExcelSnapshot,
buildExcelTsv,
excelCell,
isCopyableColumn,
isRowSelectorColumn,
resolveCopyAxes,
snapshotToUniverCellData,
toggleAxis
} from './list-excel-copy'
@@ -135,3 +137,45 @@ describe('buildExcelTsv', () => {
assert.equal(tsv, '联系人\t状态\r\n张三\t合作中\r\n李四\t跟进中')
})
})
describe('buildExcelSnapshot', () => {
it('keeps copyable columns, cleans cells, and drops 操作', () => {
const snapshot = buildExcelSnapshot({
columns: [
{ id: 'name', label: ' 联系人\n' },
{ id: 'act', label: '操作' },
{ id: 'status', label: '状态' }
],
rowValues: [
['张三\t甲', '编辑', '合作中'],
['李四', '删除', '跟进中']
]
})
assert.deepEqual(snapshot.headers, ['联系人', '状态'])
assert.deepEqual(snapshot.rows, [
['张三 甲', '合作中'],
['李四', '跟进中']
])
})
it('returns empty snapshot when no copyable columns', () => {
const snapshot = buildExcelSnapshot({
columns: [{ id: 'a', label: '操作' }],
rowValues: [['x']]
})
assert.deepEqual(snapshot, { headers: [], rows: [] })
})
})
describe('snapshotToUniverCellData', () => {
it('puts headers on row 0 and data from row 1', () => {
const cellData = snapshotToUniverCellData({
headers: ['A', 'B'],
rows: [['1', '2']]
})
assert.deepEqual(cellData[0][0], { v: 'A', t: 1 })
assert.deepEqual(cellData[0][1], { v: 'B', t: 1 })
assert.deepEqual(cellData[1][0], { v: '1', t: 1 })
assert.deepEqual(cellData[1][1], { v: '2', t: 1 })
})
})
+48
View File
@@ -68,6 +68,54 @@ export function buildExcelTsv(headers: string[], rows: string[][]): string {
return lines.join('\r\n')
}
/** 灌入只读在线表格的快照:表头 + 当前页文本矩阵 */
export type ListExcelSnapshot = {
headers: string[]
rows: string[][]
}
/**
* 从可复制列与行文本构建快照。headers/rows 均经 excelCell 清洗。
* rowValues 与 columns 按下标对齐;操作列等会被跳过。
* 无可复制列时返回 { headers: [], rows: [] }。
*/
export function buildExcelSnapshot(input: {
columns: ListExcelColumn[]
/** 与 columns 按下标对齐的行文本;通常来自表格 DOM textContent */
rowValues: string[][]
}): ListExcelSnapshot {
const indices: number[] = []
const headers: string[] = []
input.columns.forEach((col, index) => {
if (!col.id || !isCopyableColumn(col)) return
indices.push(index)
headers.push(excelCell(col.label))
})
if (!indices.length) return { headers: [], rows: [] }
const rows = input.rowValues.map((row) =>
indices.map((colIndex) => excelCell(row[colIndex] ?? ''))
)
return { headers, rows }
}
/** 将快照转为 Univer cellData(第 0 行为表头)。 */
export function snapshotToUniverCellData(snapshot: ListExcelSnapshot): Record<
number,
Record<number, { v: string; t: number }>
> {
const cellData: Record<number, Record<number, { v: string; t: number }>> = {}
const writeRow = (r: number, values: string[]) => {
const row: Record<number, { v: string; t: number }> = {}
values.forEach((value, c) => {
row[c] = { v: value, t: 1 }
})
cellData[r] = row
}
writeRow(0, snapshot.headers)
snapshot.rows.forEach((row, index) => writeRow(index + 1, row))
return cellData
}
function range(count: number): number[] {
return Array.from({ length: count }, (_, index) => index)
}