Files
shanxia2.0/web-admin/src/utils/list-excel-copy.ts
T
wuzilongandCursor aad74505c0 feat(web-admin): 列表 Excel 查看改为 Univer 只读在线表格
点击工具栏打开可筛选、复制的只读弹层,替换原行列点选复制模式,且不写回业务数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-14 23:25:18 +08:00

122 lines
3.8 KiB
TypeScript

export type ListExcelColumn = {
id: string
label: string
type?: string
className?: string
}
export function isCopyableColumn(col: ListExcelColumn): boolean {
if (col.type === 'selection' || col.type === 'index' || col.type === 'expand') return false
if (col.label === '操作') return false
const classes = (col.className || '').split(/\s+/).filter(Boolean)
if (classes.includes('col-actions')) return false
return true
}
/** 序号列是行选择头。没有 type 时,表头文案「序号」或「#」同样算行头。 */
export function isRowSelectorColumn(col: ListExcelColumn): boolean {
if (col.type === 'index') return true
const label = col.label.trim()
return label === '序号' || label === '#'
}
export function excelCell(value: string): string {
return String(value ?? '')
.replace(/\t/g, ' ')
.replace(/\r\n|\r|\n/g, ' ')
.trim()
}
export function toggleAxis(
selected: Set<number>,
index: number,
shift: boolean,
anchor: number | null
): { selected: Set<number>; anchor: number } {
const next = new Set(selected)
if (shift && anchor !== null) {
const from = Math.min(anchor, index)
const to = Math.max(anchor, index)
for (let i = from; i <= to; i += 1) next.add(i)
return { selected: next, anchor: index }
}
if (next.has(index)) next.delete(index)
else next.add(index)
return { selected: next, anchor: index }
}
export function resolveCopyAxes(input: {
selectedCols: number[]
selectedRows: number[]
columnCount: number
rowCount: number
}): { cols: number[]; rows: number[] } | null {
const { selectedCols, selectedRows, columnCount, rowCount } = input
if (!selectedCols.length && !selectedRows.length) return null
const cols = (selectedCols.length ? selectedCols : range(columnCount))
.slice()
.sort((a, b) => a - b)
const rows = (selectedRows.length ? selectedRows : range(rowCount)).slice().sort((a, b) => a - b)
return { cols, rows }
}
export function buildExcelTsv(headers: string[], rows: string[][]): string {
const lines = [headers.map(excelCell).join('\t')]
rows.forEach((row) => {
lines.push(row.map(excelCell).join('\t'))
})
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)
}