|
@@ -0,0 +1,310 @@
|
|
|
|
|
+import { saveAs } from 'file-saver';
|
|
|
|
|
+import { schemeChapterTree } from "@/api/planningScheme";
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 导出指定批次的靶标保障方案为Word文档(优化排版版)
|
|
|
|
|
+ * @param {string} batchId - 批次ID
|
|
|
|
|
+ * @param {string} [docName='靶标保障方案'] - 自定义文档名称(可选)
|
|
|
|
|
+ */
|
|
|
|
|
+export async function exportSolutionToWord(batchId, docName = '靶标保障方案') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取并处理数据
|
|
|
|
|
+ const treeData = await getSchemeTree(batchId);
|
|
|
|
|
+ const createDate = formatDate(new Date());
|
|
|
|
|
+ const version = "V1.0";
|
|
|
|
|
+
|
|
|
|
|
+ // 生成带优化排版的HTML
|
|
|
|
|
+ const htmlContent = generateOptimizedHtml(docName, version, createDate, treeData);
|
|
|
|
|
+
|
|
|
|
|
+ // 生成Word文档
|
|
|
|
|
+ const blob = new Blob(['\ufeff', htmlContent], {
|
|
|
|
|
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
|
|
|
+ });
|
|
|
|
|
+ saveAs(blob, `${docName}_${createDate}.docx`);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('导出Word失败:', error);
|
|
|
|
|
+ throw new Error('导出Word文档失败,请重试');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 获取方案树形结构数据
|
|
|
|
|
+ */
|
|
|
|
|
+async function getSchemeTree(batchId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await schemeChapterTree({ batchId });
|
|
|
|
|
+ const treeList = res.data || [];
|
|
|
|
|
+ processTreeData(treeList);
|
|
|
|
|
+ return treeList;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取方案数据失败:', error);
|
|
|
|
|
+ throw new Error('获取方案数据失败');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理树形结构数据
|
|
|
|
|
+ */
|
|
|
|
|
+function processTreeData(nodes) {
|
|
|
|
|
+ nodes.forEach(node => {
|
|
|
|
|
+ // 处理表格类型数据
|
|
|
|
|
+ if (node.chapterType === '0' && node.chapterValue) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = JSON.parse(node.chapterValue);
|
|
|
|
|
+ node.data = data.data || [];
|
|
|
|
|
+ node.columns = data.columns || [];
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('解析表格数据失败', e);
|
|
|
|
|
+ node.data = [];
|
|
|
|
|
+ node.columns = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 递归处理子节点
|
|
|
|
|
+ if (node.children && node.children.length) {
|
|
|
|
|
+ processTreeData(node.children);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 格式化日期为yyyy-mm-dd格式
|
|
|
|
|
+ */
|
|
|
|
|
+function formatDate(date) {
|
|
|
|
|
+ return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 生成优化排版的HTML内容
|
|
|
|
|
+ */
|
|
|
|
|
+function generateOptimizedHtml(title, version, createDate, treeList) {
|
|
|
|
|
+ // 文档样式定义 - 重点优化图片大小控制
|
|
|
|
|
+ const styles = `
|
|
|
|
|
+ <style>
|
|
|
|
|
+ /* 基础样式 */
|
|
|
|
|
+ body {
|
|
|
|
|
+ font-family: "SimSun", "宋体", serif;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 50px; /* 页边距 */
|
|
|
|
|
+ color: #333333;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 页面设置 */
|
|
|
|
|
+ @page {
|
|
|
|
|
+ size: A4;
|
|
|
|
|
+ margin: 2.5cm 2cm;
|
|
|
|
|
+ @top-center { content: "${title}"; font-size: 12px; color: #666; }
|
|
|
|
|
+ @bottom-center { content: "第 " counter(page) " 页"; font-size: 12px; color: #666; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 标题样式 */
|
|
|
|
|
+ .document-title { text-align: center; font-size: 22px; font-weight: bold; margin: 0 0 40px 0; padding-bottom: 10px; border-bottom: 2px solid #333; }
|
|
|
|
|
+ .document-meta { text-align: center; margin: 0 0 50px 0; font-size: 14px; line-height: 2; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 章节标题样式 */
|
|
|
|
|
+ .heading-1 { font-size: 18px; font-weight: bold; margin: 30px 0 15px 0; page-break-after: avoid; }
|
|
|
|
|
+ .heading-2 { font-size: 16px; font-weight: bold; margin: 25px 0 12px 0; padding-left: 20px; page-break-after: avoid; }
|
|
|
|
|
+ .heading-3 { font-size: 15px; font-weight: bold; margin: 20px 0 10px 0; padding-left: 40px; page-break-after: avoid; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 段落样式 */
|
|
|
|
|
+ p { margin: 0 0 16px 0; text-align: justify; text-justify: inter-ideograph; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 表格样式 */
|
|
|
|
|
+ .data-table { border-collapse: collapse; width: 100%; margin: 20px 0; page-break-inside: avoid; }
|
|
|
|
|
+ .data-table th, .data-table td { border: 1px solid #333; padding: 8px 12px; text-align: center; vertical-align: middle; }
|
|
|
|
|
+ .data-table th { background-color: #f2f2f2; font-weight: bold; }
|
|
|
|
|
+ .data-table tr:nth-child(even) td { background-color: #f9f9f9; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 图片样式 - 增强Word兼容性 */
|
|
|
|
|
+ .image-container {
|
|
|
|
|
+ height: 300px;
|
|
|
|
|
+ margin: 25px 0;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ page-break-inside: avoid;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 基础图片样式 - 使用width而非max-width提高兼容性 */
|
|
|
|
|
+ .document-image {
|
|
|
|
|
+ width: 50%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ border: 1px solid #ddd;
|
|
|
|
|
+ padding: 5px;
|
|
|
|
|
+ margin: 10px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 小型图片样式 */
|
|
|
|
|
+ .document-image.small {
|
|
|
|
|
+ width: 50%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 大型图片强制缩小 */
|
|
|
|
|
+ .document-image.large {
|
|
|
|
|
+ width: 60%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .image-caption {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 内容区块样式 */
|
|
|
|
|
+ .section { margin-bottom: 25px; }
|
|
|
|
|
+ .rich-content { margin-bottom: 20px; }
|
|
|
|
|
+ .rich-content ul, .rich-content ol { margin: 10px 0 10px 20px; padding-left: 20px; }
|
|
|
|
|
+ .rich-content li { margin-bottom: 8px; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ // 生成HTML主体内容
|
|
|
|
|
+ let html = `
|
|
|
|
|
+ <html xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
|
|
|
+ xmlns:w="urn:schemas-microsoft-com:office:word"
|
|
|
|
|
+ xmlns="http://www.w3.org/TR/REC-html40">
|
|
|
|
|
+ <head>
|
|
|
|
|
+ <meta charset="UTF-8">
|
|
|
|
|
+ <title>${title}</title>
|
|
|
|
|
+ <!-- Word特定设置 -->
|
|
|
|
|
+ <!--[if gte mso 9]><xml>
|
|
|
|
|
+ <w:WordDocument>
|
|
|
|
|
+ <w:View>Print</w:View>
|
|
|
|
|
+ <w:Zoom>100</w:Zoom>
|
|
|
|
|
+ <w:PunctuationKerning/>
|
|
|
|
|
+ <w:ValidateAgainstSchemas/>
|
|
|
|
|
+ <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>
|
|
|
|
|
+ <w:IgnoreMixedContent>false</w:IgnoreMixedContent>
|
|
|
|
|
+ <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>
|
|
|
|
|
+ <w:DoNotPromoteQF/>
|
|
|
|
|
+ <w:LidThemeOther>EN-US</w:LidThemeOther>
|
|
|
|
|
+ <w:LidThemeAsian>ZH-CN</w:LidThemeAsian>
|
|
|
|
|
+ <w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript>
|
|
|
|
|
+ </w:WordDocument>
|
|
|
|
|
+ </xml><![endif]-->
|
|
|
|
|
+ ${styles}
|
|
|
|
|
+ </head>
|
|
|
|
|
+ <body>
|
|
|
|
|
+ <h1 class="document-title">${title}</h1>
|
|
|
|
|
+ <div class="document-meta">
|
|
|
|
|
+ <p>版本:${version}</p>
|
|
|
|
|
+ <p>生成日期:${createDate}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ ${generateSectionsHtml(treeList, 1)}
|
|
|
|
|
+ </body>
|
|
|
|
|
+ </html>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ return html;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 生成章节HTML(带层级结构)
|
|
|
|
|
+ */
|
|
|
|
|
+function generateSectionsHtml(nodes, level) {
|
|
|
|
|
+ if (!nodes || nodes.length === 0) return '';
|
|
|
|
|
+
|
|
|
|
|
+ let html = '';
|
|
|
|
|
+ nodes.forEach((node, index) => {
|
|
|
|
|
+ // 生成章节标题(带层级)
|
|
|
|
|
+ const headingClass = `heading-${level}`;
|
|
|
|
|
+ const headingTag = `h${Math.min(level, 6)}`;
|
|
|
|
|
+
|
|
|
|
|
+ html += `<div class="section">`;
|
|
|
|
|
+ html += `<${headingTag} class="${headingClass}">${node.chapterName || '未命名章节'}</${headingTag}>`;
|
|
|
|
|
+
|
|
|
|
|
+ // 富文本内容
|
|
|
|
|
+ if (node.chapterType === '1' && node.chapterValue) {
|
|
|
|
|
+ html += `<div class="rich-content">${node.chapterValue}</div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 表格内容
|
|
|
|
|
+ if (node.chapterType === '0' && node.data && node.columns && node.columns.length) {
|
|
|
|
|
+ html += '<table class="data-table">';
|
|
|
|
|
+ html += '<thead><tr>';
|
|
|
|
|
+ node.columns.forEach(col => {
|
|
|
|
|
+ html += `<th>${col.label || ''}</th>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ html += '</tr></thead>';
|
|
|
|
|
+ html += '<tbody>';
|
|
|
|
|
+ node.data.forEach((row, rowIndex) => {
|
|
|
|
|
+ html += `<tr class="${rowIndex % 2 === 0 ? 'even-row' : 'odd-row'}">`;
|
|
|
|
|
+ node.columns.forEach(col => {
|
|
|
|
|
+ html += `<td>${row[col.prop] !== undefined && row[col.prop] !== null ? row[col.prop] : ''}</td>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ html += '</tr>';
|
|
|
|
|
+ });
|
|
|
|
|
+ html += '</tbody></table>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 图片内容 - 增强大小控制
|
|
|
|
|
+ if (node.chapterType === '2' && node.fileJson) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const files = JSON.parse(node.fileJson);
|
|
|
|
|
+ if (files && files.length) {
|
|
|
|
|
+ html += '<div class="images-section">';
|
|
|
|
|
+ files.forEach((file, imgIndex) => {
|
|
|
|
|
+ if (file.fileHttp) {
|
|
|
|
|
+ // 根据图片尺寸选择合适的样式类
|
|
|
|
|
+ let imageClass = 'document-image';
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有尺寸信息,使用更精确的控制
|
|
|
|
|
+ if (file.width && file.height) {
|
|
|
|
|
+ // 宽高比大于2的图片视为宽图,进一步缩小
|
|
|
|
|
+ const aspectRatio = file.width / file.height;
|
|
|
|
|
+ if (aspectRatio > 2) {
|
|
|
|
|
+ imageClass += ' small';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 没有尺寸信息则根据文件大小判断
|
|
|
|
|
+ else if (file.fileSize) {
|
|
|
|
|
+ // 大于3MB的图片强制缩小
|
|
|
|
|
+ if (file.fileSize > 3 * 1024 * 1024) {
|
|
|
|
|
+ imageClass += ' large';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 添加Word特定的尺寸控制标记
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="image-container">
|
|
|
|
|
+ <!--[if gte mso 9]><xml>
|
|
|
|
|
+ <o:OfficeDocumentSettings>
|
|
|
|
|
+ <o:AllowPNG/>
|
|
|
|
|
+ </o:OfficeDocumentSettings>
|
|
|
|
|
+ </xml><![endif]-->
|
|
|
|
|
+ <img src="${file.fileHttp}" class="${imageClass}" height="200"
|
|
|
|
|
+ alt="${file.fileName || `图片${imgIndex + 1}`}"
|
|
|
|
|
+ ${file.width ? `width="${Math.min(file.width, 600)}"` : ''}>
|
|
|
|
|
+ ${file.fileName ? `<div class="image-caption">图 ${getImageNumber(node, imgIndex)}:${file.fileName}</div>` : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ html += '</div>';
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('解析图片数据失败', e);
|
|
|
|
|
+ html += '<p class="error-message">图片数据解析失败</p>';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 递归处理子节点
|
|
|
|
|
+ if (node.children && node.children.length) {
|
|
|
|
|
+ html += generateSectionsHtml(node.children, level + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ html += `</div>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ return html;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 生成图片编号
|
|
|
|
|
+ */
|
|
|
|
|
+function getImageNumber(node, imgIndex) {
|
|
|
|
|
+ return `${imgIndex + 1}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|