点击工具栏打开可筛选、复制的只读弹层,替换原行列点选复制模式,且不写回业务数据。 Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
3.8 KiB
TypeScript
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)
|
|
}
|