using System; using System.Collections.Generic; using System.Configuration; using System.Drawing; using System.IO; using System.Linq; using Aspose.Words; using Aspose.Words.Tables; namespace BLL { /// /// 安全巡检——导出监督记录表 Word /// public static class SafetyPatrolWordService { /// /// 生成附件9《(危大工程类别)监督记录表》Word 文档(.docx) /// /// 巡检记录主键 /// docx 字节流;记录不存在返回 null public static byte[] Generate(string patrolId) { Model.HSSE_SafetyPatrol info = SafetyPatrolService.GetById(patrolId); if (info == null) { return null; } Document doc = new Document(); DocumentBuilder b = new DocumentBuilder(doc); AppendRecord(b, info); ApplyFonts(doc); ApplyLineSpacing(doc); MemoryStream ms = new MemoryStream(); doc.Save(ms, SaveFormat.Docx); return ms.ToArray(); } /// /// 写入单独一条(标题 + 记录表) /// /// 文档构建器 /// 巡检记录 private static void AppendRecord(DocumentBuilder b, Model.HSSE_SafetyPatrol info) { ////全局默认字体 b.Font.Name = "微软雅黑"; b.Font.NameFarEast = "微软雅黑"; b.Font.Size = 10.5; ////固定页面为 A4,保证表格宽度不会被压缩(A4 内容宽 = 595 - 40 - 40 = 515 磅) PageSetup ps = b.Document.FirstSection.PageSetup; ps.PageWidth = 595; ps.PageHeight = 842; ps.LeftMargin = 40; ps.RightMargin = 40; ps.TopMargin = 40; ps.BottomMargin = 40; ////标题(附件9) b.ParagraphFormat.Alignment = ParagraphAlignment.Center; b.Font.Size = 16; b.Font.Bold = true; b.Write("附件9 (危大工程类别)监督记录表"); b.ParagraphFormat.SpaceAfter = 30; // 标题与下方「监督巡视部位」之间留 40px ≈ 30pt b.Writeln(); b.Font.Bold = false; b.Font.Size = 10.5; b.ParagraphFormat.SpaceAfter = 0; // 还原,避免影响后续段落 b.ParagraphFormat.Alignment = ParagraphAlignment.Left; ////监督巡视部位 / 巡视日期:拆成左右两列、各占一行 50%(无边框), ////与下方记录表同宽(412 磅、居中,两列各 206 磅),保证与表格两列精确对齐。 Table head = b.StartTable(); b.CellFormat.VerticalAlignment = CellVerticalAlignment.Center; b.CellFormat.Width = 206; b.InsertCell(); b.Write("监督巡视部位:" + (info.HazardProjectName ?? "")); b.CellFormat.Width = 206; b.InsertCell(); b.Write("巡视日期:" + (info.PatrolDate.HasValue ? info.PatrolDate.Value.ToString("yyyy-MM-dd") : "")); b.EndRow(); b.EndTable(); ////无边框 + 定宽居中,使两列与下方记录表的起止完全对齐 head.AllowAutoFit = false; head.PreferredWidth = PreferredWidth.FromPoints(412); head.Alignment = TableAlignment.Center; head.Rows[0].Cells[0].CellFormat.Width = 206; head.Rows[0].Cells[1].CellFormat.Width = 206; ////默认表格无边框;个别环境下若带边框则显式清空 foreach (Cell c in head.Rows[0].Cells) { foreach (Border bd in c.CellFormat.Borders) { bd.LineStyle = LineStyle.None; } } ////记录表主体:统一两列(左=标签/内容区,右=发现问题/签收人/空),每行都是独立单元格。 ////重要:不要用 VerticalMerge 或全宽 gridSpan —— DocumentBuilder 无法可靠地只合并右列, ////会把左列下部的「处理要求/整改后复查/备注」吞掉或把表格网格弄乱。统一两列最稳定。 Table table = b.StartTable(); b.CellFormat.VerticalAlignment = CellVerticalAlignment.Center; ////行1:表头 监督巡视内容 | 发现问题 b.CellFormat.Width = 340; b.InsertCell(); b.Write("监督巡视内容"); b.CellFormat.Width = 175; b.InsertCell(); b.Write("发现问题"); b.EndRow(); ////行3:勾选项(固定4项+其它) | 发现问题内容 b.CellFormat.Width = 340; b.InsertCell(); WriteContentLines(b, info.Content); b.CellFormat.Width = 175; b.InsertCell(); b.Write(info.Problems ?? ""); b.EndRow(); ////行4:处理要求: | 签收人: b.CellFormat.Width = 340; b.InsertCell(); b.Write("处理要求:"); b.CellFormat.Width = 175; b.InsertCell(); b.Write("签收人:" + (string.IsNullOrEmpty(info.Receiver) ? "____________" : info.Receiver)); b.EndRow(); ////行5:处理要求内容区(占3行),整格横向合并(横跨两列) b.CellFormat.Width = 340; b.InsertCell(); WriteArea(b, info.HandleRequire, 3); b.CellFormat.Width = 175; b.InsertCell(); b.EndRow(); ////行6-7:整改后复查情况 + 内容区,合并为一个整宽纵向单元格。 ////重要:纵并组里只有 restart 格(上半行)的正文会显示,continue 格必须留空, ////因此把「标签:」与内容都写入 restart 格。 b.CellFormat.Width = 340; b.InsertCell(); WriteLabelArea(b, "整改后复查情况:", info.RectifyReview, 5); b.CellFormat.Width = 175; b.InsertCell(); b.EndRow(); ////行7:纵并 continue 格(留空) b.CellFormat.Width = 340; b.InsertCell(); b.Writeln(); b.CellFormat.Width = 175; b.InsertCell(); b.Writeln(); b.EndRow(); ////行8-9:备注 + 内容区,合并为一个整宽纵向单元格 b.CellFormat.Width = 340; b.InsertCell(); WriteLabelArea(b, "备注:", info.Remarks, 2); b.CellFormat.Width = 175; b.InsertCell(); b.EndRow(); ////行9:纵并 continue 格(留空) b.CellFormat.Width = 340; b.InsertCell(); b.Writeln(); b.CellFormat.Width = 175; b.InsertCell(); b.Writeln(); b.EndRow(); b.EndTable(); ////表格宽度:占内容区 80%(515 × 0.8 = 412 磅),并水平居中 table.AllowAutoFit = false; table.PreferredWidth = PreferredWidth.FromPoints(412); table.Alignment = TableAlignment.Center; double cellWidth = 412 / 2.0; // 两列等宽 = 206 磅 foreach (Row row in table.Rows) { row.Cells[0].CellFormat.Width = cellWidth; // 左列 row.Cells[1].CellFormat.Width = cellWidth; // 右列(与左列等宽) } ////发现问题内容(行1右列):左上显示,不垂直居中。 ////DocumentBuilder 中途设置 CellFormat.VerticalAlignment 对当前单元格不生效, ////必须在 post-build 阶段用 Cell 对象设置(与合并同一方式)。 table.Rows[1].Cells[1].CellFormat.VerticalAlignment = CellVerticalAlignment.Top; ////整宽 / 跨行合并:post-build 阶段用 Cell 对象合并,gridCol 仍保持两列。 ////表头之下共 8 行:索引0=表头,1=发现问题,2=处理要求/签收人, ////索引3=处理要求内容区(横并),索引4-5=整改后复查(纵并+整宽),索引6-7=备注(纵并+整宽)。 HorizontalMergeRow(table.Rows[3]); HorizontalMergeRow(table.Rows[4]); VerticalMergeTop(table.Rows[4]); HorizontalMergeRow(table.Rows[5]); VerticalMergeBottom(table.Rows[5]); HorizontalMergeRow(table.Rows[6]); VerticalMergeTop(table.Rows[6]); HorizontalMergeRow(table.Rows[7]); VerticalMergeBottom(table.Rows[7]); ////表格样式:细黑色网格线(与图片一致) table.StyleIdentifier = StyleIdentifier.TableGrid; ////底部:监督巡视人员(恢复正文大小,签收人已入表格第4行右列) ////右对齐到表格右缘:右缩进 51.5 磅(= 表格居中后右侧留白) b.Font.Size = 10.5; b.Font.Bold = false; b.ParagraphFormat.Alignment = ParagraphAlignment.Right; b.ParagraphFormat.LeftIndent = 51.5; b.ParagraphFormat.RightIndent = 51.5; b.Write("监督巡视人员:" + (info.PatrolMan ?? "")); b.Writeln(); ////注:左对齐到表格左缘(左缩进 51.5 磅),还原右对齐/右缩进 b.ParagraphFormat.Alignment = ParagraphAlignment.Left; b.ParagraphFormat.RightIndent = 0; b.Write("注:此表可优化调整,但管理要素不得少于上表。"); b.Writeln(); ////巡检附件 / 整改附件:另起一页显示(第二页起) b.InsertBreak(BreakType.PageBreak); AppendAttachments(b, info.PatrolId); } /// /// 在第二页写入巡检附件与整改附件图片(按记录遍历,逐张缩放嵌入)。 /// 附件图片均为磁盘文件,通过 localRoot + AttachUrl 定位;无附件则给出「无」。 /// /// 文档构建器 /// 巡检记录主键 private static void AppendAttachments(DocumentBuilder b, string patrolId) { if (string.IsNullOrEmpty(patrolId)) { return; } string localRoot = ConfigurationManager.AppSettings["localRoot"] ?? string.Empty; b.Font.Bold = false; b.Font.Size = 10.5; WriteAttachSection(b, "一、巡检附件", patrolId, Const.SafetyPatrolMenuId, localRoot); WriteAttachSection(b, "二、整改附件", patrolId, Const.SafetyPatrolRectifyMenuId, localRoot); } /// /// 写入一个附件分组:标题行 + 该分组下的所有附件图片(缩放至最大宽 412 磅保持原比例)。 /// 非图片文件仅列出文件名;分组无附件则写「无」。 /// /// 文档构建器 /// 分组标题(如「一、巡检附件」) /// 巡检记录主键 /// 附件菜单ID /// 附件磁盘根目录 private static void WriteAttachSection(DocumentBuilder b, string title, string patrolId, string menuId, string localRoot) { b.ParagraphFormat.Alignment = ParagraphAlignment.Left; b.ParagraphFormat.SpaceBefore = 6; b.Write(title); b.Writeln(); b.ParagraphFormat.SpaceBefore = 0; List files = Funs.DB.AttachFile .Where(p => p.ToKeyId == patrolId && p.MenuId == menuId) .ToList(); if (files == null || files.Count == 0) { b.Write("无"); b.Writeln(); return; } foreach (Model.AttachFile f in files) { if (string.IsNullOrEmpty(f.AttachUrl)) { continue; } string filePath = Path.Combine(localRoot, f.AttachUrl.Replace('/', '\\')); if (!File.Exists(filePath)) { continue; } string ext = Path.GetExtension(filePath).ToLowerInvariant(); if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".bmp") { b.ParagraphFormat.SpaceBefore = 4; InsertImageScaled(b, filePath, 400, 260); b.Writeln(); // 每张图独占一行,避免后续标题/图片内联到图右侧 b.ParagraphFormat.SpaceBefore = 0; } else { b.Write(Path.GetFileName(filePath)); b.Writeln(); } } } /// /// 插入图片:缩放至不超过指定宽高(磅),保持原始宽高比。 /// /// 文档构建器 /// 图片物理路径 /// 最大宽(磅) /// 最大高(磅) private static void InsertImageScaled(DocumentBuilder b, string filePath, double maxWidthPt, double maxHeightPt) { try { using (Image img = Image.FromFile(filePath)) { int w = img.Width; int h = img.Height; if (w <= 0 || h <= 0) { return; } double scale = Math.Min(maxWidthPt / w, maxHeightPt / h); b.InsertImage(filePath, w * scale, h * scale); } } catch { // 非图片或读取失败:忽略 } } /// /// 写入内容区单元格:先写内容,再用空段落撑高到指定行数。 /// 无内容时也保证最小行高,便于手写填写。 /// /// 文档构建器 /// 内容 /// 最小行数(空段落数) private static void WriteArea(DocumentBuilder b, string text, int minLines) { if (!string.IsNullOrEmpty(text)) { b.Write(text); } for (int i = 0; i < minLines; i++) { b.Writeln(); } } /// /// 监督巡视内容固定项(与编辑页复选框 Text 一致) /// private static readonly string[] PatrolContentItems = new string[] { "1、施工条件保持情况", "2、按方案施工情况,实体与方案参数的对比", "3、现场安全防护状况", "4、作业人员持证上岗情况" }; /// /// 将监督巡视内容写入单元格:始终保持 4 个固定项(写死默认值)+ 一行「其它」。 /// 其它:Content 字段以后只存「其它」内容,直接显示;若含「其它:」前缀则取后半段(兼容旧格式);无内容则下划线占位。 /// /// 文档构建器 /// Content 文本(以后仅存其它内容,如:基坑围挡损坏;旧数据可能形如:1、…;4、…;其它:xxx) private static void WriteContentLines(DocumentBuilder b, string content) { bool first = true; ////固定 4 项始终输出(写死默认值) foreach (string item in PatrolContentItems) { if (!first) { b.Writeln(); } b.Write(item); first = false; } ////其它行:优先取 Content 中「其它:」后半段(兼容旧格式);否则 Content 即其它内容 b.Writeln(); string other = ExtractOther(content); if (other.Length == 0) { other = content ?? string.Empty; } b.Write("其它:" + (other.Length > 0 ? other : "____________")); } /// /// 从 Content 文本中提取「其它」说明内容(无则返回空串) /// /// Content 文本 /// 其它说明;未包含返回空串 private static string ExtractOther(string content) { if (string.IsNullOrEmpty(content)) { return string.Empty; } foreach (string seg in content.Split(';')) { string s = seg.Trim(); if (s.Length == 0) { continue; } if (s.StartsWith("其它") || s.StartsWith("其他")) { int idx = s.IndexOf(':'); return idx >= 0 ? s.Substring(idx + 1).Trim() : string.Empty; } } return string.Empty; } /// /// 将一行的左右两格横向合并为整宽单元格(gridSpan=2)。 /// 必须在 post-build 阶段用 Cell 对象合并,避免 DocumentBuilder 把表格网格弄乱。 /// /// 待合并的行 private static void HorizontalMergeRow(Row row) { row.Cells[0].CellFormat.HorizontalMerge = CellMerge.First; row.Cells[1].CellFormat.HorizontalMerge = CellMerge.Previous; } /// /// 纵向合并的起始行(restart)。该行正文会被保留,并与其下方 continue 行合并为一个单元格。 /// /// 起始行 private static void VerticalMergeTop(Row row) { row.Cells[0].CellFormat.VerticalMerge = CellMerge.First; row.Cells[1].CellFormat.VerticalMerge = CellMerge.First; } /// /// 纵向合并的延续行(continue)。该行正文不会显示,必须留空。 /// /// 延续行 private static void VerticalMergeBottom(Row row) { row.Cells[0].CellFormat.VerticalMerge = CellMerge.Previous; row.Cells[1].CellFormat.VerticalMerge = CellMerge.Previous; } /// /// 写入「标签:」+ 内容区,标签独占一行,内容随后以内容/空段落撑高。 /// 用于纵向合并单元格(标签与内容需写入 restart 格)。 /// /// 文档构建器 /// 标签(如「整改后复查情况:」) /// 内容 /// 最小行数(空段落数) private static void WriteLabelArea(DocumentBuilder b, string label, string text, int minLines) { b.Write(label); b.Writeln(); WriteArea(b, text, minLines); } /// /// 统一全文正文字体:仅标题「附件9 …监督记录表」加粗(16 磅),其余一律 10.5 磅、不加粗。 /// DocumentBuilder 在跨表格书写时会丢失字符格式状态(标题的加粗/16 磅曾泄漏到记录表与落款), /// 故在 post-build 阶段对所有 Run 兜底逐一遍历设置,保证只有标题加粗。 /// /// 文档 private static void ApplyFonts(Document doc) { foreach (Run run in doc.GetChildNodes(NodeType.Run, true)) { bool isTitle = run.Text == "附件9 (危大工程类别)监督记录表"; run.Font.Bold = isTitle; run.Font.Size = isTitle ? 16 : 10.5; } } /// /// 为文档中所有段落(含表格单元格内)设置 1.5 倍行距。 /// Aspose 的 Multiple 行距以磅为单位:单倍 = 12 磅,1.5 倍 = 18 磅, /// 生成 w:line="360" w:lineRule="auto"(360/240 = 1.5 倍)。 /// /// 文档 private static void ApplyLineSpacing(Document doc) { foreach (Paragraph p in doc.GetChildNodes(NodeType.Paragraph, true)) { p.ParagraphFormat.LineSpacingRule = LineSpacingRule.Multiple; p.ParagraphFormat.LineSpacing = 14; // 1.5 倍行距(18 磅 → w:line="360",360/240=1.5) } } } }