192824ea90
- 安全巡检表新增编码字段(Code,全局唯一 XJ-序号),WebAPI/桌面端新增时自动生成,不可重复 - 修复危大超危工程公示牌二维码不显示:urlName 含非法字符?导致 image.Save 抛异常,改用主键+内容哈希安全文件名 - 安全问题统计(mainProject/main3)排除作废数据(States=4),图表口径与顶部计数一致 - Word 导出文件名改为 监督巡视部位+编号 - Model.cs 由 SqlMetal 重新生成,补齐巡检人ID/签收人ID/发现问题/处理要求等字段;新增 HSSE_SafetyPatrol.Code.cs 补充 Code 列 - 新增安全巡检完整建表脚本(SGGLDB_V2026-08-25-001);删除已过时的接口文档 SafetyPatrol_API.md
501 lines
21 KiB
C#
501 lines
21 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 安全巡检——导出监督记录表 Word
|
||
/// </summary>
|
||
public static class SafetyPatrolWordService
|
||
{
|
||
/// <summary>
|
||
/// 生成附件9《(危大工程类别)监督记录表》Word 文档(.docx)
|
||
/// </summary>
|
||
/// <param name="patrolId">巡检记录主键</param>
|
||
/// <returns>docx 字节流;记录不存在返回 null</returns>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入单独一条(标题 + 记录表)
|
||
/// </summary>
|
||
/// <param name="b">文档构建器</param>
|
||
/// <param name="info">巡检记录</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在第二页写入巡检附件与整改附件图片(按记录遍历,逐张缩放嵌入)。
|
||
/// 附件图片均为磁盘文件,通过 localRoot + AttachUrl 定位;无附件则给出「无」。
|
||
/// </summary>
|
||
/// <param name="b">文档构建器</param>
|
||
/// <param name="patrolId">巡检记录主键</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入一个附件分组:标题行 + 该分组下的所有附件图片(缩放至最大宽 412 磅保持原比例)。
|
||
/// 非图片文件仅列出文件名;分组无附件则写「无」。
|
||
/// </summary>
|
||
/// <param name="b">文档构建器</param>
|
||
/// <param name="title">分组标题(如「一、巡检附件」)</param>
|
||
/// <param name="patrolId">巡检记录主键</param>
|
||
/// <param name="menuId">附件菜单ID</param>
|
||
/// <param name="localRoot">附件磁盘根目录</param>
|
||
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<Model.AttachFile> 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();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 插入图片:缩放至不超过指定宽高(磅),保持原始宽高比。
|
||
/// </summary>
|
||
/// <param name="b">文档构建器</param>
|
||
/// <param name="filePath">图片物理路径</param>
|
||
/// <param name="maxWidthPt">最大宽(磅)</param>
|
||
/// <param name="maxHeightPt">最大高(磅)</param>
|
||
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
|
||
{
|
||
// 非图片或读取失败:忽略
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入内容区单元格:先写内容,再用空段落撑高到指定行数。
|
||
/// 无内容时也保证最小行高,便于手写填写。
|
||
/// </summary>
|
||
/// <param name="b">文档构建器</param>
|
||
/// <param name="text">内容</param>
|
||
/// <param name="minLines">最小行数(空段落数)</param>
|
||
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();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 监督巡视内容固定项(与编辑页复选框 Text 一致)
|
||
/// </summary>
|
||
private static readonly string[] PatrolContentItems = new string[]
|
||
{
|
||
"1、施工条件保持情况",
|
||
"2、按方案施工情况,实体与方案参数的对比",
|
||
"3、现场安全防护状况",
|
||
"4、作业人员持证上岗情况"
|
||
};
|
||
|
||
/// <summary>
|
||
/// 将监督巡视内容写入单元格:始终保持 4 个固定项(写死默认值)+ 一行「其它」。
|
||
/// 其它:Content 字段以后只存「其它」内容,直接显示;若含「其它:」前缀则取后半段(兼容旧格式);无内容则下划线占位。
|
||
/// </summary>
|
||
/// <param name="b">文档构建器</param>
|
||
/// <param name="content">Content 文本(以后仅存其它内容,如:基坑围挡损坏;旧数据可能形如:1、…;4、…;其它:xxx)</param>
|
||
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 : "____________"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 Content 文本中提取「其它」说明内容(无则返回空串)
|
||
/// </summary>
|
||
/// <param name="content">Content 文本</param>
|
||
/// <returns>其它说明;未包含返回空串</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将一行的左右两格横向合并为整宽单元格(gridSpan=2)。
|
||
/// 必须在 post-build 阶段用 Cell 对象合并,避免 DocumentBuilder 把表格网格弄乱。
|
||
/// </summary>
|
||
/// <param name="row">待合并的行</param>
|
||
private static void HorizontalMergeRow(Row row)
|
||
{
|
||
row.Cells[0].CellFormat.HorizontalMerge = CellMerge.First;
|
||
row.Cells[1].CellFormat.HorizontalMerge = CellMerge.Previous;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 纵向合并的起始行(restart)。该行正文会被保留,并与其下方 continue 行合并为一个单元格。
|
||
/// </summary>
|
||
/// <param name="row">起始行</param>
|
||
private static void VerticalMergeTop(Row row)
|
||
{
|
||
row.Cells[0].CellFormat.VerticalMerge = CellMerge.First;
|
||
row.Cells[1].CellFormat.VerticalMerge = CellMerge.First;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 纵向合并的延续行(continue)。该行正文不会显示,必须留空。
|
||
/// </summary>
|
||
/// <param name="row">延续行</param>
|
||
private static void VerticalMergeBottom(Row row)
|
||
{
|
||
row.Cells[0].CellFormat.VerticalMerge = CellMerge.Previous;
|
||
row.Cells[1].CellFormat.VerticalMerge = CellMerge.Previous;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入「标签:」+ 内容区,标签独占一行,内容随后以内容/空段落撑高。
|
||
/// 用于纵向合并单元格(标签与内容需写入 restart 格)。
|
||
/// </summary>
|
||
/// <param name="b">文档构建器</param>
|
||
/// <param name="label">标签(如「整改后复查情况:」)</param>
|
||
/// <param name="text">内容</param>
|
||
/// <param name="minLines">最小行数(空段落数)</param>
|
||
private static void WriteLabelArea(DocumentBuilder b, string label, string text, int minLines)
|
||
{
|
||
b.Write(label);
|
||
b.Writeln();
|
||
WriteArea(b, text, minLines);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一全文正文字体:仅标题「附件9 …监督记录表」加粗(16 磅),其余一律 10.5 磅、不加粗。
|
||
/// DocumentBuilder 在跨表格书写时会丢失字符格式状态(标题的加粗/16 磅曾泄漏到记录表与落款),
|
||
/// 故在 post-build 阶段对所有 Run 兜底逐一遍历设置,保证只有标题加粗。
|
||
/// </summary>
|
||
/// <param name="doc">文档</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为文档中所有段落(含表格单元格内)设置 1.5 倍行距。
|
||
/// Aspose 的 Multiple 行距以磅为单位:单倍 = 12 磅,1.5 倍 = 18 磅,
|
||
/// 生成 w:line="360" w:lineRule="auto"(360/240 = 1.5 倍)。
|
||
/// </summary>
|
||
/// <param name="doc">文档</param>
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|