Files
Agent-System/web/src/engine/xlsxRenderer.ts
T
WeiCHENandCursor fefb53ef30 feat(engine): 实现 DOCX 与 XLSX 客户端原生渲染导出引擎
- 引入 docx 与 xlsx 依赖,纯前端无损编译 Block AST 交付物
- buildDocxModel: 自动生成两行四列受控签署栏、多级编号标题、Markdown富文本、医疗标准三线表、Cambria Math公式与动态页眉页脚
- buildXlsxWorkbook: 自动提取文档概览与全部数据表格,生成带自适应列宽的 Excel 工作簿并支持直接下载

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:19 +00:00

157 lines
5.6 KiB
TypeScript

import * as XLSX from 'xlsx';
import { DocumentAST } from '../types/block';
/**
* sanitizeSheetName: 净化 Excel 工作表名称(限制在 31 字符以内,移除 \ / ? * [ ] : 等非法字符)
*/
function sanitizeSheetName(name: string, fallback: string): string {
const cleaned = name.replace(/[\\/?*\[\]:]/g, '_').trim();
if (!cleaned) return fallback;
return cleaned.substring(0, 30);
}
/**
* buildXlsxWorkbook: 将 DocumentAST 结构化提炼为正规的 Excel 工作簿模型
*/
export function buildXlsxWorkbook(ast: DocumentAST): XLSX.WorkBook {
const wb = XLSX.utils.book_new();
// 1. 创建 Sheet 1: “文档概览与受控签署”
const statusMap: Record<string, string> = {
drafting: '编制中 (草稿)',
reviewing: '审核中 (流程锁定)',
approving: '批准中 (流程锁定)',
approved: '已批准生效 (受控基线)',
rejected: '已驳回修改',
};
const overviewData: (string | number)[][] = [
['水木东方医疗器械受控体系交付物数据台账'],
[],
['交付物标题', ast.title],
['文档编号/ID', ast.docId],
['业务任务分类', ast.taskKey],
['受控版本', ast.version],
['受控状态', statusMap[ast.lifecycleState] || ast.lifecycleState],
['空间分类', ast.category === 'private' ? '个人私密生成' : ast.category === 'shared' ? '项目协作共享' : '流程受控交付物'],
['编制责任人', ast.author],
['审核责任人', ast.reviewer],
['批准责任人', ast.approver],
['最新归档时间', ast.updatedAt || '2026-09-08'],
['文档摘要', ast.summary || ''],
[],
['导出来源', '水木东方协同智能体工作台 (Browser XLSX Engine)'],
];
const wsOverview = XLSX.utils.aoa_to_sheet(overviewData);
// 设置概览页列宽
wsOverview['!cols'] = [{ wch: 18 }, { wch: 60 }];
XLSX.utils.book_append_sheet(wb, wsOverview, '文档概览与签署');
// 2. 遍历 AST 提取所有表格块 (Table Blocks)
const blocks = ast.rootBlock.children || [];
const tableBlocks = blocks.filter((b) => b.type === 'table' && b.properties?.tableData?.length);
if (tableBlocks.length > 0) {
const usedSheetNames = new Set<string>(['文档概览与签署']);
tableBlocks.forEach((tblBlock, idx) => {
const rawCaption = tblBlock.properties?.caption || `数据表_${idx + 1}`;
const prefix = tblBlock.numbering ? `${tblBlock.numbering} ` : '';
let baseSheetName = sanitizeSheetName(`${prefix}${rawCaption}`, `数据表_${idx + 1}`);
// 处理 Sheet 名称防重复
let finalSheetName = baseSheetName;
let counter = 1;
while (usedSheetNames.has(finalSheetName)) {
finalSheetName = `${baseSheetName.substring(0, 26)}_${counter++}`;
}
usedSheetNames.add(finalSheetName);
const tableData = tblBlock.properties?.tableData || [];
const sheetAoa: (string | number)[][] = [];
// 表头之前加入表格标题与归属说明
sheetAoa.push([`${prefix}${rawCaption}`]);
sheetAoa.push([]);
// 压入表格真实数据行
for (const row of tableData) {
// 清除单元格中的 Markdown 格式字符(如 **粗体**)使 Excel 数据整洁
const cleanedRow = row.map((cell) => cell.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\*(.*?)\*/g, '$1').trim());
sheetAoa.push(cleanedRow);
}
const ws = XLSX.utils.aoa_to_sheet(sheetAoa);
// 计算自适应列宽
if (tableData.length > 0) {
const colCount = Math.max(...tableData.map((r) => r.length));
const colWidths: { wch: number }[] = [];
for (let c = 0; c < colCount; c++) {
let maxLen = 10;
for (const row of tableData) {
const cellStr = row[c] || '';
const len = cellStr.length * 1.5; // 中文字符加权
if (len > maxLen) maxLen = len;
}
colWidths.push({ wch: Math.min(Math.max(Math.round(maxLen), 12), 50) });
}
ws['!cols'] = colWidths;
}
XLSX.utils.book_append_sheet(wb, ws, finalSheetName);
});
} else {
// 若文档无独立表格,提取列表及段落构建结构化清单 Sheet
const listData: string[][] = [['章节/类型', '序号', '条目内容']];
let curChapter = '正文';
for (const b of blocks) {
if (b.type === 'heading') {
curChapter = `${b.numbering || ''} ${b.content}`.trim();
} else if (b.type === 'list') {
const items =
b.properties?.listItems ||
b.content
.split('\n')
.map((line) => line.replace(/^\s*(\d+\.|[-*+])\s+/, '').trim())
.filter(Boolean);
items.forEach((item, i) => {
listData.push([curChapter, String(i + 1), item]);
});
}
}
if (listData.length > 1) {
const wsList = XLSX.utils.aoa_to_sheet(listData);
wsList['!cols'] = [{ wch: 25 }, { wch: 8 }, { wch: 60 }];
XLSX.utils.book_append_sheet(wb, wsList, '结构化条目清单');
}
}
return wb;
}
/**
* exportDocumentToXlsx: 编译并触发浏览器下载 XLSX 二进制文件
*/
export async function exportDocumentToXlsx(ast: DocumentAST): Promise<Blob> {
const wb = buildXlsxWorkbook(ast);
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([wbout], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${ast.title}_${ast.version}_数据清单.xlsx`;
a.click();
URL.revokeObjectURL(url);
}
return blob;
}