using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Xml; using System.Windows.Forms; using System.Data.SQLite; namespace 成套报价软件 { /// /// 甲方报表 的交互逻辑。 /// 接收项目编号,从报价库读取数据,用 NPOI 填充 Excel 模板导出。 /// public partial class 甲方报表 { // ====== 字段 ====== private string _projectId; private string _quoteDbPath; // 模板目录(相对程序根目录) private const string TemplateDir = @"xiangmu\xlsx\按图号"; private const string TuhaoTableName = "advancedDataGridView9Table"; // ====== 构造函数 ====== // SharpDevelop 设计器需要无参数构造函数;运行时由主窗口调用带参数版本。 public 甲方报表() { InitializeComponent(); } public 甲方报表(string projectId) { InitializeComponent(); _projectId = projectId; _quoteDbPath = GetQuoteDbPath(projectId); this.Load += 甲方报表_Load; this.btn打开模板.Click += btn打开模板_Click; this.btn导入模板.Click += btn导入模板_Click; this.btn选择文件.Click += btn选择文件_Click; this.btn确定.Click += btn确定_Click; this.btn关闭.Click += btn关闭_Click; } // ====== 窗体事件 ====== private void 甲方报表_Load(object sender, EventArgs e) { LoadTemplateList(); // 默认导出文件名: 项目名称_甲方报价.xlsx (读取项目信息里的项目名称) var projInfo = LoadProjectInfo(); string projName = GetDict(projInfo, "项目名称"); if (string.IsNullOrWhiteSpace(projName)) projName = _projectId; // 项目名称为空时退回项目编号 string safeName = MakeSafeName(projName); string defaultName = safeName + "_甲方报价.xlsx"; // ★ 文本框只显示文件名,不显示完整路径 txt导出文件.Text = defaultName; cmb报表类型.Text = "按图号"; } /// 扫描模板目录,把所有 .xlsx 文件名填入下拉框。 private void LoadTemplateList() { cmb报表模板.Items.Clear(); string dir = GetTemplateDirFullPath(); if (!Directory.Exists(dir)) return; foreach (var file in Directory.GetFiles(dir, "*.xlsx")) { cmb报表模板.Items.Add(Path.GetFileNameWithoutExtension(file)); } if (cmb报表模板.Items.Count > 0) cmb报表模板.SelectedIndex = 0; } private void btn打开模板_Click(object sender, EventArgs e) { string path = GetSelectedTemplatePath(); if (path == null || !File.Exists(path)) { MessageBox.Show(this, "请先选择一个模板。", "甲方报表", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } System.Diagnostics.Process.Start(path); } private void btn导入模板_Click(object sender, EventArgs e) { using (var dlg = new OpenFileDialog()) { dlg.Filter = "Excel 文件|*.xlsx"; dlg.Title = "选择要导入的模板文件"; if (dlg.ShowDialog() != DialogResult.OK) return; string dir = GetTemplateDirFullPath(); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); string destPath = Path.Combine(dir, Path.GetFileName(dlg.FileName)); File.Copy(dlg.FileName, destPath, true); LoadTemplateList(); string name = Path.GetFileNameWithoutExtension(dlg.FileName); int idx = cmb报表模板.Items.IndexOf(name); if (idx >= 0) cmb报表模板.SelectedIndex = idx; MessageBox.Show(this, "模板导入成功。", "导入模板", MessageBoxButtons.OK, MessageBoxIcon.Information); } } private void btn选择文件_Click(object sender, EventArgs e) { using (var dlg = new SaveFileDialog()) { dlg.Filter = "Excel 文件|*.xlsx"; dlg.Title = "选择导出文件路径"; dlg.FileName = Path.GetFileName(txt导出文件.Text); if (dlg.ShowDialog() == DialogResult.OK) // ★ 写回完整路径:用户选的目录必须被尊重(ResolveOutputPath 对 rooted 路径直接使用) txt导出文件.Text = dlg.FileName; } } private void btn确定_Click(object sender, EventArgs e) { string templatePath = GetSelectedTemplatePath(); if (templatePath == null || !File.Exists(templatePath)) { MessageBox.Show(this, "请选择有效的报表模板。", "甲方报表", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // ★ 文本框只存文件名,导出时转成完整路径 string outputPath = ResolveOutputPath(); if (string.IsNullOrEmpty(outputPath)) { MessageBox.Show(this, "请指定导出文件名。", "甲方报表", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (!File.Exists(_quoteDbPath)) { MessageBox.Show(this, "当前项目的报价数据库不存在:\n" + _quoteDbPath, "甲方报表", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { var projInfo = LoadProjectInfo(); var companyInfo = 企业信息.LoadSettings(); var tuhaoList = LoadTuhaoData(); // 调试日志 var dbg = new System.Text.StringBuilder(); dbg.AppendLine("projInfo条数: " + projInfo.Count); foreach (var kv in projInfo) dbg.AppendLine(" " + kv.Key + "=" + kv.Value); dbg.AppendLine("companyInfo条数: " + companyInfo.Count); foreach (var kv in companyInfo) dbg.AppendLine(" " + kv.Key + "=" + kv.Value); dbg.AppendLine("tuhaoList条数: " + tuhaoList.Count); int cabTotal = 0; foreach (var t in tuhaoList) { dbg.AppendLine(" 图号=" + t.Tuhao + " 箱号数=" + t.Cabinets.Count); cabTotal += t.Cabinets.Count; } dbg.AppendLine("箱号总数: " + cabTotal); dbg.AppendLine("_quoteDbPath: " + _quoteDbPath); dbg.AppendLine("_projectId: " + _projectId); dbg.AppendLine("templatePath: " + templatePath); dbg.AppendLine("outputPath: " + outputPath); File.WriteAllText(Path.Combine(GetAppBaseDir(), "甲方报表_调试.log"), dbg.ToString()); if (tuhaoList.Count == 0 || TotalCabinetCount(tuhaoList) == 0) { MessageBox.Show(this, "当前项目没有箱号数据,无法导出。\n\n" + dbg.ToString(), "甲方报表", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } this.Cursor = Cursors.WaitCursor; ExportExcel(templatePath, outputPath, tuhaoList, projInfo, companyInfo, chk表格带Excel公式.Checked, chk箱柜链接.Checked); this.Cursor = Cursors.Default; MessageBox.Show(this, "导出成功!\n" + outputPath, "甲方报表", MessageBoxButtons.OK, MessageBoxIcon.Information); if (MessageBox.Show(this, "是否打开导出的文件?", "甲方报表", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { System.Diagnostics.Process.Start(outputPath); } } catch (Exception ex) { this.Cursor = Cursors.Default; MessageBox.Show(this, "导出失败:\n" + ex.Message + "\n\n" + ex.StackTrace, "甲方报表", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void btn关闭_Click(object sender, EventArgs e) { this.Close(); } // ====== 数据模型 ====== private class TuhaoData { public string ShowOrder; public string Tuhao; public string TuMing; public string TaiShu; public decimal XianguiCount; public decimal UnitPrice; public decimal TotalPrice; public decimal CnTotalPrice; public List Cabinets = new List(); } private class CabinetData { public string ShowOrder; public string CabinetNo; public string BoxName; // 箱号表."箱名"(进线柜、PT柜等名称) public string Model; public string SpecName; public string Unit; public string Count; public decimal OfferUnitPrice; public decimal OfferTotalPrice; public string Remark; public string CabinetName; // 图号表.图名 public string PaperModelShowOrder = null; // 图号序号 // ★ 查元件表用内部ID(改名不丢明细) public string InnerId; public List Elements = new List(); } private class ElementData { public string ShowOrder; public string Name; public string Spec; public string Unit; public string Count; public decimal OfferUnitPrice; public decimal OfferTotalPrice; public string DrawingSpec; public string FactName; public string Remark; public string ShowSpecName; // 图纸规格优先,否则用规格 } // ====== 数据读取 ====== private string GetAppBaseDir() { return AppDomain.CurrentDomain.BaseDirectory; } private string GetTemplateDirFullPath() { return Path.Combine(GetAppBaseDir(), TemplateDir); } private string GetSelectedTemplatePath() { string name = cmb报表模板.Text; if (string.IsNullOrEmpty(name)) return null; return Path.Combine(GetTemplateDirFullPath(), name + ".xlsx"); } /// 把字符串清理为合法文件名(非法字符替换为 _)。 private static string MakeSafeName(string s) { if (string.IsNullOrEmpty(s)) return "未命名"; string r = s; foreach (char c in Path.GetInvalidFileNameChars()) r = r.Replace(c, '_'); return r; } /// 把文本框里的文件名转为完整导出路径(默认存到 exe 所在目录)。 private string ResolveOutputPath() { string fn = (txt导出文件.Text ?? "").Trim(); if (string.IsNullOrEmpty(fn)) return ""; // 如果用户填的是完整路径就直接用,否则拼到 exe 目录下 if (Path.IsPathRooted(fn)) return fn; return Path.Combine(GetAppBaseDir(), fn); } private string GetQuoteDbPath(string projectId) { string safeName = projectId; foreach (char c in Path.GetInvalidFileNameChars()) safeName = safeName.Replace(c, '_'); return Path.Combine(GetAppBaseDir(), @"sjk\bjb\项目" + safeName + ".db"); } private string GetConnString(string dbPath) { // ★ A2(2026-10-01):原为裸串(无 WAL/超时,并发易锁死)——统一收敛到 数据库连接.串 return 数据库连接.串(dbPath); } private Dictionary LoadProjectInfo() { var result = new Dictionary(); if (!File.Exists(_quoteDbPath)) return result; try { using (var conn = new SQLiteConnection(GetConnString(_quoteDbPath))) { conn.Open(); string sql = "SELECT * FROM 项目信息 LIMIT 1;"; using (var cmd = new SQLiteCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { if (reader.Read()) { for (int i = 0; i < reader.FieldCount; i++) { string key = reader.GetName(i); object val = reader.GetValue(i); result[key] = val == DBNull.Value ? "" : val.ToString(); } } } } } catch { } return result; } private List LoadTuhaoData() { var result = new List(); if (!File.Exists(_quoteDbPath)) return result; using (var conn = new SQLiteConnection(GetConnString(_quoteDbPath))) { conn.Open(); string sql = "SELECT * FROM " + TuhaoTableName + ";"; using (var cmd = new SQLiteCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var t = new TuhaoData { ShowOrder = SafeStr(reader["序号"]), Tuhao = SafeStr(reader["图号"]), TuMing = SafeStr(reader["图名"]), TaiShu = SafeStr(reader["台数"]), XianguiCount = SafeDec(reader["箱柜数量"]), UnitPrice = SafeDec(reader["单价"]), TotalPrice = SafeDec(reader["总价"]), CnTotalPrice = SafeDec(reader["成本总价"]), }; LoadCabinetsForTuhao(conn, t); result.Add(t); } } } return result; } private void LoadCabinetsForTuhao(SQLiteConnection conn, TuhaoData tuhao) { string realTbl = LookupTableByIndex(conn, "箱号", tuhao.Tuhao, ""); if (string.IsNullOrEmpty(realTbl)) return; try { string sql = "SELECT * FROM \"" + realTbl + "\";"; using (var cmd = new SQLiteCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var c = new CabinetData { ShowOrder = SafeStr(reader["序号"]), CabinetNo = SafeStr(reader["箱号"]), BoxName = SafeStrCol(reader, "箱名"), // 老库没有此列时返回空串 Model = SafeStr(reader["型号"]), SpecName = SafeStr(reader["规格"]), Unit = SafeStr(reader["单位"]), Count = SafeStr(reader["数量"]), OfferUnitPrice = SafeDec(reader["单台报出"]), OfferTotalPrice = SafeDec(reader["报出价"]), Remark = SafeStr(reader["备注"]), CabinetName = tuhao.TuMing, // ★ 查元件表用内部ID(改名不丢明细) InnerId = SafeStrCol(reader, "内部ID"), }; LoadElementsForCabinet(conn, tuhao.Tuhao, c); tuhao.Cabinets.Add(c); } } } catch { } } private void LoadElementsForCabinet(SQLiteConnection conn, string tuhao, CabinetData cab) { // ★ 查元件表用【内部ID】(用户改箱号显示名后仍能查到;旧逻辑按显示名查会丢明细) string 查询键 = string.IsNullOrWhiteSpace(cab.InnerId) ? cab.CabinetNo : cab.InnerId; string realTbl = LookupTableByIndex(conn, "元件", tuhao, 查询键); if (string.IsNullOrEmpty(realTbl)) return; try { string sql = "SELECT * FROM \"" + realTbl + "\";"; using (var cmd = new SQLiteCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var e = new ElementData { ShowOrder = SafeStr(reader["序号"]), Name = SafeStr(reader["元件名称"]), Spec = SafeStr(reader["规格"]), Unit = SafeStr(reader["单位"]), Count = SafeStr(reader["数量"]), OfferUnitPrice = SafeDec(reader["报出单价"]), OfferTotalPrice = SafeDec(reader["报出价"]), DrawingSpec = SafeStr(reader["图纸规格"]), FactName = SafeStr(reader["品牌"]), Remark = SafeStr(reader["备注"]), }; e.ShowSpecName = string.IsNullOrEmpty(e.DrawingSpec) ? e.Spec : e.DrawingSpec; cab.Elements.Add(e); } } } catch { } } private string LookupTableByIndex(SQLiteConnection conn, string type, string tuhao, string xianghao) { try { string sql = "SELECT 实际表名 FROM 表索引 WHERE 类型=@t AND 图号=@th AND 箱号=@xh LIMIT 1;"; using (var cmd = new SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@t", type); cmd.Parameters.AddWithValue("@th", tuhao); cmd.Parameters.AddWithValue("@xh", xianghao ?? ""); object o = cmd.ExecuteScalar(); return (o == null || o == DBNull.Value) ? "" : o.ToString(); } } catch { return ""; } } // ====== Excel 导出(纯 C# zip+xml 操作,不依赖任何 Excel 库) ====== private void ExportExcel(string templatePath, string outputPath, List tuhaoList, Dictionary projInfo, Dictionary companyInfo, bool useFormula, bool useHyperlink) { // 1. 复制模板到输出路径 File.Copy(templatePath, outputPath, true); // 2. 构建占位符映射 var map = BuildPlaceholderMap(tuhaoList, projInfo, companyInfo); // 3. 读取 xlsx (zip) 中的 sharedStrings.xml 和所有 sheet // 先解析 sharedStrings,把占位符替换为实际值 // 同时处理 sheet 中的行数据填充 var entries = new Dictionary(); // entryName -> content using (var archive = System.IO.Compression.ZipFile.OpenRead(outputPath)) { foreach (var entry in archive.Entries) { using (var s = entry.Open()) using (var ms = new MemoryStream()) { s.CopyTo(ms); entries[entry.FullName] = ms.ToArray(); } } } // 4. 解析并替换 sharedStrings.xml(如果有) bool hasSharedStrings = false; foreach (var key in new List(entries.Keys)) { if (key == "xl/sharedStrings.xml") { hasSharedStrings = true; string xml = System.Text.Encoding.UTF8.GetString(entries[key]); xml = ReplacePlaceholdersInXml(xml, map); entries[key] = System.Text.Encoding.UTF8.GetBytes(xml); } } // 5. 如果没有 sharedStrings,字符串可能内联在 sheet 的 inlineStr 中 // 或者完全不存在(空模板)——遍历所有 sheet 做替换 if (!hasSharedStrings) { foreach (var key in new List(entries.Keys)) { if (key.StartsWith("xl/worksheets/sheet") && key.EndsWith(".xml")) { string xml = System.Text.Encoding.UTF8.GetString(entries[key]); xml = ReplacePlaceholdersInXml(xml, map); entries[key] = System.Text.Encoding.UTF8.GetBytes(xml); } } } // 5.5 处理 styles.xml: 根据项目设置的"项目小数位"调整数字格式的小数位数 // 模板中价格单元格用了 0.00 / #,##0.00 等格式 // 需根据报价软件中"项目小数位"设置(保留整数/保留N位小数)动态调整 int priceDecimals = 2; // 默认2位 if (projInfo != null && projInfo.ContainsKey("项目小数位")) { int n = 价格格式化.解析小数位(projInfo["项目小数位"]); if (n >= 0) priceDecimals = n; } // 新的小数部分: 0位→空(整数), N位→.后跟N个0 string decPart = priceDecimals > 0 ? "." + new string('0', priceDecimals) : ""; foreach (var key in new List(entries.Keys)) { if (key == "xl/styles.xml") { string xml = System.Text.Encoding.UTF8.GetString(entries[key]); var sdoc = new XmlDocument(); sdoc.LoadXml(xml); var snsmgr = new XmlNamespaceManager(sdoc.NameTable); snsmgr.AddNamespace("s", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); foreach (XmlNode nf in sdoc.SelectNodes("//s:numFmt", snsmgr)) { string code = ((XmlElement)nf).GetAttribute("formatCode"); if (string.IsNullOrEmpty(code)) continue; // 把所有 .0+ 替换为新的小数部分 string newCode = System.Text.RegularExpressions.Regex.Replace(code, @"\.0+", decPart); if (newCode != code) ((XmlElement)nf).SetAttribute("formatCode", newCode); } entries[key] = System.Text.Encoding.UTF8.GetBytes(sdoc.OuterXml); } } // 6. 填充汇总表和分项表数据行 var dbgLog = new System.Text.StringBuilder(); dbgLog.AppendLine("hasSharedStrings=" + hasSharedStrings); foreach (var key in entries.Keys) { if (key.StartsWith("xl/worksheets/sheet")) dbgLog.AppendLine(" sheet entry: " + key + " size=" + entries[key].Length); } FillSummaryAndDetail(entries, tuhaoList, map, dbgLog, useFormula, useHyperlink); File.WriteAllText(Path.Combine(GetAppBaseDir(), "甲方报表_调试.log"), dbgLog.ToString()); // ★ 公式单元格无缓存值:强制 Excel 打开时全量重算,否则公式列显示 0/空 string wbKey = "xl/workbook.xml"; if (entries.ContainsKey(wbKey)) { string wb = System.Text.Encoding.UTF8.GetString(entries[wbKey]); if (wb.Contains("]*)/>", m => ""); } else if (wb.Contains("")) { wb = wb.Replace("", ""); } entries[wbKey] = System.Text.Encoding.UTF8.GetBytes(wb); } // 7. 写回 xlsx File.Delete(outputPath); using (var fs = new FileStream(outputPath, FileMode.Create)) using (var archive = new System.IO.Compression.ZipArchive(fs, System.IO.Compression.ZipArchiveMode.Create)) { foreach (var kv in entries) { var entry = archive.CreateEntry(kv.Key); using (var s = entry.Open()) s.Write(kv.Value, 0, kv.Value.Length); } } } /// 构建占位符映射表 private Dictionary BuildPlaceholderMap( List tuhaoList, Dictionary projInfo, Dictionary companyInfo) { var map = new Dictionary(); map["[Project.ProjectNo]"] = GetDict(projInfo, "项目编号"); map["[Project.ProjectName]"] = GetDict(projInfo, "项目名称"); map["[Project.CreateTime]"] = GetDict(projInfo, "项目日期"); map["[Project.CustomerName]"] = GetDict(projInfo, "客户名称"); map["[Project.CustomerContact]"] = GetDict(projInfo, "联系人"); map["[Project.CustomerPhone]"] = GetDict(projInfo, "联系电话"); map["[Company.CompanyName]"] = GetDict(companyInfo, "企业名称"); map["[Company.Contact]"] = GetDict(companyInfo, "联系人"); map["[Company.Phone]"] = GetDict(companyInfo, "电话"); map["[PaperModel.PaperDesc]"] = "按图号"; // ★ 诊断日志:记录完整计算过程,便于排查"导出总价 vs 软件显示总价"不一致 var calcLog = new System.Text.StringBuilder(); calcLog.AppendLine("====== BuildPlaceholderMap 计算日志 ======"); calcLog.AppendLine("projInfo 中的小数位设置:"); if (projInfo != null) { foreach (var kv in projInfo) { if (kv.Key.Contains("小数位") || kv.Key.Contains("取整")) calcLog.AppendLine(" " + kv.Key + " = [" + kv.Value + "]"); } } else { calcLog.AppendLine(" projInfo == null!"); } decimal totalQty = 0, totalOffer = 0; foreach (var t in tuhaoList) { // 项目总台数=所有图号台数合计(与汇总表总计行G列SUM公式一致) totalQty += SafeDec(t.TaiShu); // 图号合计(1台设备价格)=SUM(所有箱号的(单台合计×箱号数量)) decimal tuhaoSubtotal = 0; calcLog.AppendLine("图号[" + t.Tuhao + "] 图名[" + t.TuMing + "] 台数[" + t.TaiShu + "] 箱号数[" + t.Cabinets.Count + "]"); foreach (var c in t.Cabinets) { decimal singleTotal = 0; foreach (var e in c.Elements) { singleTotal += SafeDec(e.Count) * e.OfferUnitPrice; calcLog.AppendLine(" 元件[" + e.Name + "] 数量[" + e.Count + "] 报出单价[" + e.OfferUnitPrice + "] 报出价[" + (SafeDec(e.Count) * e.OfferUnitPrice) + "]"); } // ★ 不提前四舍五入:与软件计算链一致(软件全程用小数累加,显示时才按小数位格式化) // 提前 ROUND 会让多个箱号的小数部分被截断,导致总价偏小(如 23249→23247) calcLog.AppendLine(" 箱号[" + c.CabinetNo + "] 数量[" + c.Count + "] 单台合计(未舍入)[" + singleTotal + "] 箱号总计[" + (singleTotal * SafeDec(c.Count)) + "]"); tuhaoSubtotal += singleTotal * SafeDec(c.Count); } // 图号总计=图号合计×台数, 项目总计=SUM(所有图号总计) calcLog.AppendLine(" 图号合计[" + tuhaoSubtotal + "] × 台数[" + t.TaiShu + "] = 图号总计[" + (tuhaoSubtotal * SafeDec(t.TaiShu)) + "]"); totalOffer += tuhaoSubtotal * SafeDec(t.TaiShu); } calcLog.AppendLine("项目总价(未舍入) = " + totalOffer); map["[Project.Qty]"] = totalQty.ToString("0.##"); // 总报价金额格式: 根据项目"项目小数位"设置决定显示精度 // (与 styles.xml 数字格式保持一致,确保C列显示与价格列一致) int priceDec = 2; // 默认2位 if (projInfo != null && projInfo.ContainsKey("项目小数位")) { int n = 价格格式化.解析小数位(projInfo["项目小数位"]); if (n >= 0) priceDec = n; } // ★ 同时检查"单台小数位"(软件图号表用这个显示,可能与"项目小数位"不同) int danTaiDec = -1; if (projInfo != null && projInfo.ContainsKey("单台小数位")) { danTaiDec = 价格格式化.解析小数位(projInfo["单台小数位"]); } calcLog.AppendLine("priceDec(项目小数位)=" + priceDec + " danTaiDec(单台小数位)=" + danTaiDec); string priceFmt = priceDec > 0 ? "0." + new string('0', priceDec) : "0"; // 按"项目小数位"设置统一四舍五入,确保显示数字与大写金额完全一致(与软件显示行为对齐) decimal roundedTotalOffer = Math.Round(totalOffer, priceDec, MidpointRounding.AwayFromZero); calcLog.AppendLine("roundedTotalOffer(按" + priceDec + "位舍入) = " + roundedTotalOffer); calcLog.AppendLine("priceFmt = [" + priceFmt + "]"); calcLog.AppendLine("[Project.ProjectOfferTotalAmt] = [" + roundedTotalOffer.ToString(priceFmt) + "]"); calcLog.AppendLine("[Project.CnAmt] = [" + ConvertToChineseAmount(roundedTotalOffer) + "]"); calcLog.AppendLine("====== 计算日志结束 ======"); try { System.IO.File.WriteAllText(System.IO.Path.Combine(GetAppBaseDir(), "甲方报表_计算日志.txt"), calcLog.ToString()); } catch { } map["[Project.ProjectOfferTotalAmt]"] = roundedTotalOffer.ToString(priceFmt); map["[Project.CnAmt]"] = ConvertToChineseAmount(roundedTotalOffer); map["[PaperModel.Qty]"] = totalQty.ToString("0.##"); map["[PaperModel.OfferTotalPrice]"] = roundedTotalOffer.ToString(priceFmt); return map; } /// /// 将金额转换为中文大写(符合财务规范)。 /// 例: 1234.56 → 壹仟贰佰叁拾肆元伍角陆分 /// 10000.00 → 壹万元整 /// private static string ConvertToChineseAmount(decimal amount) { string[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; string[] posUnit = { "", "拾", "佰", "仟" }; // 段内单位(个/十/百/千) string[] segUnit = { "", "万", "亿", "万亿" }; // 段间单位 bool negative = amount < 0; amount = Math.Abs(amount); // 四舍五入到分 long fenTotal = (long)Math.Round(amount * 100m); long intPart = fenTotal / 100; int jiao = (int)((fenTotal / 10) % 10); int fen = (int)(fenTotal % 10); if (intPart == 0 && jiao == 0 && fen == 0) return "零元整"; // 整数部分: 按4位一段分组转换 string intStr = ""; if (intPart > 0) { string numStr = intPart.ToString(); int segCount = (numStr.Length + 3) / 4; numStr = numStr.PadLeft(segCount * 4, '0'); var sb = new System.Text.StringBuilder(); bool lastSegHadNum = false; for (int s = 0; s < segCount; s++) { string seg = numStr.Substring(s * 4, 4); bool segAllZero = true; string segStr = ""; bool innerZero = false; for (int i = 0; i < 4; i++) { int d = seg[i] - '0'; if (d == 0) { innerZero = true; } else { segAllZero = false; if (innerZero && segStr.Length > 0) segStr += "零"; segStr += digit[d] + posUnit[3 - i]; innerZero = false; } } if (!segAllZero) { // 段间补零: 上一段有数字且当前段以零开头 if (lastSegHadNum && seg[0] == '0') sb.Append("零"); sb.Append(segStr); sb.Append(segUnit[segCount - 1 - s]); lastSegHadNum = true; } } intStr = sb.ToString() + "元"; } // 角分部分 string decStr = ""; if (jiao == 0 && fen == 0) { decStr = "整"; } else { if (intPart > 0 && jiao == 0) decStr += "零"; if (jiao > 0) decStr += digit[jiao] + "角"; if (fen > 0) decStr += digit[fen] + "分"; } string result = intStr + decStr; if (negative) result = "负" + result; return result; } /// 在 XML 文本中替换所有占位符(XML实体安全) private string ReplacePlaceholdersInXml(string xml, Dictionary map) { // 占位符在 XML 中可能以原始文本或数字实体形式出现 // 直接对整个 XML 字符串做 Replace 即可(占位符是 ASCII 字符,不会被实体编码) foreach (var kv in map) { xml = xml.Replace(kv.Key, XmlEscape(kv.Value)); } return xml; } /// XML 实体转义 private static string XmlEscape(string s) { if (string.IsNullOrEmpty(s)) return ""; return s.Replace("&", "&").Replace("<", "<").Replace(">", ">"); } // ====== 汇总表和分项表填充 ====== // 记录每个箱号在分项表中的关键行号(供汇总表公式引用) private class CabinetRowInfo { public int CabRow; // 柜号行 public int SingleTotalRow; // 单台合计行 public int GrandRow; // 总计行(单台×数量) } private void FillSummaryAndDetail(Dictionary entries, List tuhaoList, Dictionary map, System.Text.StringBuilder dbgLog, bool useFormula, bool useHyperlink) { if (tuhaoList.Count == 0) return; var sheetKeys = new List(); foreach (var key in entries.Keys) if (key.StartsWith("xl/worksheets/sheet") && key.EndsWith(".xml")) sheetKeys.Add(key); sheetKeys.Sort(); // 先填分项表,记录每个箱号的行号 var cabRowInfos = new List(); if (sheetKeys.Count >= 3) { dbgLog.AppendLine("调用 FillDetailSheet: " + sheetKeys[2]); FillDetailSheet(entries, sheetKeys[2], tuhaoList, cabRowInfos, dbgLog, useFormula); dbgLog.AppendLine("cabRowInfos.Count=" + cabRowInfos.Count); } // 再填汇总表,用公式引用分项表的行号 if (sheetKeys.Count >= 2) { dbgLog.AppendLine("调用 FillSummarySheet: " + sheetKeys[1]); FillSummarySheet(entries, sheetKeys[1], tuhaoList, cabRowInfos, dbgLog, useFormula, useHyperlink); } } // ====== 汇总表 ====== // 模板结构: 行11=图名行([PaperModel.PaperDesc]), 行12=表头, 行13=数据行, 行14=合计, 行15=总计 // 每个图号: 1行图名 + 表头 + N行箱号数据(公式引用分项表), 然后合计行, 最后总计行 private void FillSummarySheet(Dictionary entries, string sheetKey, List tuhaoList, List cabRowInfos, System.Text.StringBuilder dbgLog, bool useFormula, bool useHyperlink) { string xml = System.Text.Encoding.UTF8.GetString(entries[sheetKey]); var doc = new XmlDocument(); doc.LoadXml(xml); XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); nsmgr.AddNamespace("s", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); XmlNode sheetData = doc.SelectSingleNode("//s:sheetData", nsmgr); if (sheetData == null) { dbgLog.AppendLine(" [汇总] sheetData=null,返回"); return; } // 找模板行 XmlNode tplPaperRow = null; // 行11: 图名行 XmlNode tplHeaderRow = null; // 行12: 表头行 XmlNode tplDataRow = null; // 行13: 数据行 XmlNode tplTotalRow = null; // 行14: 合计行 XmlNode tplGrandRow = null; // 行15: 总计行 XmlNode tplExplainRow = null; // 行16: 报价说明行 XmlNode tplSignRow = null; // 行17: 报价人/审核人行 foreach (XmlNode row in sheetData.SelectNodes("s:row", nsmgr)) { string r = ((XmlElement)row).GetAttribute("r"); if (r == "11") tplPaperRow = row; else if (r == "12") tplHeaderRow = row; else if (r == "13") tplDataRow = row; else if (r == "14") tplTotalRow = row; else if (r == "15") tplGrandRow = row; else if (r == "16") tplExplainRow = row; else if (r == "17") tplSignRow = row; } dbgLog.AppendLine(" [汇总] tplPaperRow=" + (tplPaperRow != null) + " tplHeaderRow=" + (tplHeaderRow != null) + " tplDataRow=" + (tplDataRow != null) + " tplTotalRow=" + (tplTotalRow != null) + " tplGrandRow=" + (tplGrandRow != null) + " tplExplainRow=" + (tplExplainRow != null) + " tplSignRow=" + (tplSignRow != null)); if (tplDataRow == null) { dbgLog.AppendLine(" [汇总] tplDataRow=null,返回"); return; } // 克隆报价说明行、报价人行(稍后追加到末尾) XmlNode explainTplClone = tplExplainRow != null ? tplExplainRow.CloneNode(true) : null; XmlNode signTplClone = tplSignRow != null ? tplSignRow.CloneNode(true) : null; // 计算总箱号数 int totalCabinets = 0; foreach (var t in tuhaoList) totalCabinets += t.Cabinets.Count; // 记录柜号超链接: (汇总表行号, 分项表行号) 用于生成 var cabLinks = new List>(); // 在 styles.xml 中添加超链接样式(基于B列原始样式s=51,只把fontId改为14蓝色下划线) int hyperlinkStyleIdx = -1; if (useHyperlink) foreach (var stk in new List(entries.Keys)) { if (stk != "xl/styles.xml") continue; string sxml = System.Text.Encoding.UTF8.GetString(entries[stk]); var sdoc = new XmlDocument(); sdoc.LoadXml(sxml); var snsmgr = new XmlNamespaceManager(sdoc.NameTable); snsmgr.AddNamespace("s", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); XmlNode cellXfsNode = sdoc.SelectSingleNode("//s:cellXfs", snsmgr); if (cellXfsNode != null) { int curCount = 0; int.TryParse(((XmlElement)cellXfsNode).GetAttribute("count"), out curCount); // 基于B列原始样式(cellXfs[51]): 保留边框borderId=16/对齐/保护,只改fontId=14(蓝色下划线) XmlElement newXf = sdoc.CreateElement("xf", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); newXf.SetAttribute("numFmtId", "49"); newXf.SetAttribute("fontId", "14"); newXf.SetAttribute("fillId", "0"); newXf.SetAttribute("borderId", "16"); newXf.SetAttribute("xfId", "0"); newXf.SetAttribute("applyNumberFormat", "1"); newXf.SetAttribute("applyFont", "1"); newXf.SetAttribute("applyFill", "1"); newXf.SetAttribute("applyBorder", "1"); newXf.SetAttribute("applyAlignment", "1"); newXf.SetAttribute("applyProtection", "1"); XmlElement align = sdoc.CreateElement("alignment", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); align.SetAttribute("horizontal", "left"); align.SetAttribute("vertical", "center"); newXf.AppendChild(align); XmlElement prot = sdoc.CreateElement("protection", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); prot.SetAttribute("locked", "0"); newXf.AppendChild(prot); cellXfsNode.AppendChild(newXf); hyperlinkStyleIdx = curCount; ((XmlElement)cellXfsNode).SetAttribute("count", (curCount + 1).ToString()); } entries[stk] = System.Text.Encoding.UTF8.GetBytes(sdoc.OuterXml); break; } // 清理 mergeCells 中涉及行 >=11 的合并区域(这些行会被重建) XmlNode mergeCellsNode = doc.SelectSingleNode("//s:mergeCells", nsmgr); if (mergeCellsNode != null) { var toRemove = new List(); foreach (XmlNode mc in mergeCellsNode.SelectNodes("s:mergeCell", nsmgr)) { string range = ((XmlElement)mc).GetAttribute("ref"); if (MergeCellRangeOverlapsRow(range, 11)) toRemove.Add(mc); } foreach (XmlNode mc in toRemove) mergeCellsNode.RemoveChild(mc); // 更新 count 属性 int remain = mergeCellsNode.SelectNodes("s:mergeCell", nsmgr).Count; if (remain == 0) { doc.DocumentElement.RemoveChild(mergeCellsNode); mergeCellsNode = null; } else { ((XmlElement)mergeCellsNode).SetAttribute("count", remain.ToString()); } } // 删除原模板行 11~17 if (tplSignRow != null) sheetData.RemoveChild(tplSignRow); if (tplExplainRow != null) sheetData.RemoveChild(tplExplainRow); if (tplGrandRow != null) sheetData.RemoveChild(tplGrandRow); if (tplTotalRow != null) sheetData.RemoveChild(tplTotalRow); if (tplDataRow != null) sheetData.RemoveChild(tplDataRow); if (tplHeaderRow != null) sheetData.RemoveChild(tplHeaderRow); if (tplPaperRow != null) sheetData.RemoveChild(tplPaperRow); // 找到插入点 (行10 之后) XmlNode insertAfter = null; foreach (XmlNode row in sheetData.SelectNodes("s:row", nsmgr)) { if (((XmlElement)row).GetAttribute("r") == "10") { insertAfter = row; break; } } // 逐图号生成行,记录每个图号区域的合计行号(供总计行SUM引用) int rowNum = 11; var subtotalRows = new List(); // 每个图号的合计行行号 var tuhaoTotalRows = new List(); // 每个图号的总计行行号(供总计行SUM引用) int cabInfoIndex = 0; // 全局箱号索引(对应 cabRowInfos) int tuhaoIdx = 0; // 图号序号(0基) // 为图号总计行创建带橙色背景(fillId=5)的样式(基于汇总表合计行各列自身样式) var totalStyleMap = new Dictionary(); // 原样式索引 -> 新样式索引 if (tplTotalRow != null) { string sxml = System.Text.Encoding.UTF8.GetString(entries["xl/styles.xml"]); var sdoc = new XmlDocument(); sdoc.LoadXml(sxml); var snsmgr = new XmlNamespaceManager(sdoc.NameTable); snsmgr.AddNamespace("s", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); XmlNode cellXfsNode = sdoc.SelectSingleNode("//s:cellXfs", snsmgr); if (cellXfsNode != null) { int curCount = 0; int.TryParse(((XmlElement)cellXfsNode).GetAttribute("count"), out curCount); // 收集合计行各列的样式索引(去重) var baseStyles = new HashSet(); foreach (XmlNode cell in tplTotalRow.ChildNodes) { if (!(cell is XmlElement)) continue; string s = ((XmlElement)cell).GetAttribute("s"); if (!string.IsNullOrEmpty(s)) baseStyles.Add(s); } // 为每个样式创建带橙色背景的副本 foreach (string baseStyle in baseStyles) { int baseIdx; if (!int.TryParse(baseStyle, out baseIdx)) continue; if (baseIdx >= cellXfsNode.ChildNodes.Count) continue; XmlNode origXf = cellXfsNode.ChildNodes[baseIdx]; XmlElement newXf = (XmlElement)origXf.CloneNode(true); newXf.SetAttribute("fillId", "4"); // 灰色背景(与图号表头一致) newXf.SetAttribute("applyFill", "1"); cellXfsNode.AppendChild(newXf); totalStyleMap[baseStyle] = curCount.ToString(); curCount++; } ((XmlElement)cellXfsNode).SetAttribute("count", curCount.ToString()); entries["xl/styles.xml"] = System.Text.Encoding.UTF8.GetBytes(sdoc.OuterXml); } } foreach (var tuhao in tuhaoList) { string tuhaoName = !string.IsNullOrEmpty(tuhao.TuMing) ? tuhao.TuMing : tuhao.Tuhao; int tuhaoCabCount = tuhao.Cabinets.Count; // 1) 图名行 if (tplPaperRow != null) { XmlNode paperRow = tplPaperRow.CloneNode(true); ((XmlElement)paperRow).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(paperRow, rowNum); SetCellInRow(paperRow, "A", tuhaoName, nsmgr); InsertRowAfter(sheetData, paperRow, ref insertAfter); AddMergeCell(doc, ref mergeCellsNode, "A" + rowNum + ":J" + rowNum); rowNum++; } // 2) 表头行 if (tplHeaderRow != null) { XmlNode headerRow = tplHeaderRow.CloneNode(true); ((XmlElement)headerRow).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(headerRow, rowNum); InsertRowAfter(sheetData, headerRow, ref insertAfter); rowNum++; } // 3) 数据行(用公式引用分项表) int dataStartRow = rowNum; int cabIdxInTuhao = 0; // 图号内箱号序号(0基) foreach (var cab in tuhao.Cabinets) { XmlNode dataRow = tplDataRow.CloneNode(true); ((XmlElement)dataRow).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(dataRow, rowNum); // 用公式引用分项表 if (cabInfoIndex < cabRowInfos.Count) { var info = cabRowInfos[cabInfoIndex]; // A列序号: 图号序号-箱号序号 (如 1-1, 1-2, 2-1) SetCellInRow(dataRow, "A", (tuhaoIdx + 1) + "-" + (cabIdxInTuhao + 1), nsmgr); if (useFormula) { SetCellFormulaInRow(dataRow, "B", "屏柜分项表!B" + info.CabRow, nsmgr); SetCellFormulaInRow(dataRow, "C", "屏柜分项表!G" + info.CabRow, nsmgr); SetCellFormulaInRow(dataRow, "D", "屏柜分项表!D" + info.CabRow, nsmgr); SetCellFormulaInRow(dataRow, "E", "屏柜分项表!I" + info.CabRow, nsmgr); SetCellFormulaInRow(dataRow, "G", "屏柜分项表!E" + info.GrandRow, nsmgr); SetCellFormulaInRow(dataRow, "H", "屏柜分项表!G" + info.SingleTotalRow, nsmgr); SetCellFormulaInRow(dataRow, "I", "屏柜分项表!G" + info.GrandRow, nsmgr); } else { // C#计算: 单台合计=ROUND(SUM(元件数量×单价),0), 箱号总计=单台合计×数量 decimal elemSum = 0; foreach (var elem in cab.Elements) elemSum += SafeDec(elem.Count) * elem.OfferUnitPrice; // 不提前四舍五入,与软件计算链一致(小数累加,由Excel数字格式控制显示) decimal singleTotal = elemSum; decimal cabTotal = singleTotal * SafeDec(cab.Count); SetCellInRow(dataRow, "B", cab.CabinetNo, nsmgr); // ★ 与公式分支的模板列义对齐:B柜号/C名称(箱名)/D型号/E规格 // 旧代码 C写型号/D写规格/E写图名 是错位占位 string 名称值 = !string.IsNullOrEmpty(cab.BoxName) ? cab.BoxName : cab.CabinetName; SetCellInRow(dataRow, "C", 名称值, nsmgr); SetCellInRow(dataRow, "D", cab.Model, nsmgr); SetCellInRow(dataRow, "E", cab.SpecName, nsmgr); SetCellNumericInRow(dataRow, "G", SafeDec(cab.Count), nsmgr); SetCellNumericInRow(dataRow, "H", singleTotal, nsmgr); SetCellNumericInRow(dataRow, "I", cabTotal, nsmgr); } SetCellInRow(dataRow, "F", "台", nsmgr); // B列柜号设置超链接样式(蓝色下划线) if (hyperlinkStyleIdx >= 0) SetCellStyle(dataRow, "B", hyperlinkStyleIdx, nsmgr); } SetCellInRow(dataRow, "J", cab.Remark, nsmgr); // 记录柜号超链接: 汇总表B{rowNum} → 分项表B{info.CabRow} if (cabInfoIndex < cabRowInfos.Count) cabLinks.Add(new KeyValuePair(rowNum, cabRowInfos[cabInfoIndex].CabRow)); InsertRowAfter(sheetData, dataRow, ref insertAfter); rowNum++; cabInfoIndex++; cabIdxInTuhao++; } // 4) 合计行(用SUM公式) if (tplTotalRow != null) { XmlNode totalRow = tplTotalRow.CloneNode(true); ((XmlElement)totalRow).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(totalRow, rowNum); SetCellInRow(totalRow, "B", "合计", nsmgr); SetCellInRow(totalRow, "F", "台", nsmgr); if (useFormula) { SetCellFormulaInRow(totalRow, "G", "SUM(G" + dataStartRow + ":G" + (rowNum - 1) + ")", nsmgr); SetCellFormulaInRow(totalRow, "I", "SUM(I" + dataStartRow + ":I" + (rowNum - 1) + ")", nsmgr); } else { decimal sumQty = 0, sumPrice = 0; foreach (var cab in tuhao.Cabinets) { sumQty += SafeDec(cab.Count); decimal elemSum = 0; foreach (var elem in cab.Elements) elemSum += SafeDec(elem.Count) * elem.OfferUnitPrice; sumPrice += elemSum * SafeDec(cab.Count); } SetCellNumericInRow(totalRow, "G", sumQty, nsmgr); SetCellNumericInRow(totalRow, "I", sumPrice, nsmgr); } InsertRowAfter(sheetData, totalRow, ref insertAfter); subtotalRows.Add(rowNum); rowNum++; } // 图号总计行(放在合计行下面,台数×合计行总价=图号总价) if (tplTotalRow != null) { XmlNode taiShuRow = tplTotalRow.CloneNode(true); ((XmlElement)taiShuRow).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(taiShuRow, rowNum); SetCellInRow(taiShuRow, "B", "总计", nsmgr); SetCellInRow(taiShuRow, "F", "套", nsmgr); SetCellNumericInRow(taiShuRow, "G", SafeDec(tuhao.TaiShu), nsmgr); // I列=合计行总价(rowNum-1) × 台数(当前行G列) if (useFormula) SetCellFormulaInRow(taiShuRow, "I", "I" + (rowNum - 1) + "*G" + rowNum, nsmgr); else { decimal sumPrice = 0; foreach (var cab in tuhao.Cabinets) { decimal elemSum = 0; foreach (var elem in cab.Elements) elemSum += SafeDec(elem.Count) * elem.OfferUnitPrice; sumPrice += elemSum * SafeDec(cab.Count); } SetCellNumericInRow(taiShuRow, "I", sumPrice * SafeDec(tuhao.TaiShu), nsmgr); } InsertRowAfter(sheetData, taiShuRow, ref insertAfter); // 设置图号总计行各列样式(带橙色背景,与分项表总计行一致) foreach (XmlNode cell in taiShuRow.ChildNodes) { if (!(cell is XmlElement)) continue; string origStyle = ((XmlElement)cell).GetAttribute("s"); if (totalStyleMap.ContainsKey(origStyle)) ((XmlElement)cell).SetAttribute("s", totalStyleMap[origStyle]); } tuhaoTotalRows.Add(rowNum); rowNum++; } tuhaoIdx++; } // 5) 总计行(C列总报价金额、G列总台数用SUM公式, H列大写金额由占位符替换) if (tplGrandRow != null) { XmlNode grandRow = tplGrandRow.CloneNode(true); ((XmlElement)grandRow).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(grandRow, rowNum); SetCellInRow(grandRow, "B", "总计", nsmgr); SetCellInRow(grandRow, "F", "套", nsmgr); if (useFormula) { // C列=ROUND(SUM(所有图号总计行I列),0) 总报价金额 // G列=SUM(所有图号总计行G列) 总台数 if (tuhaoTotalRows.Count > 0) { var iRefs = new List(); var gRefs = new List(); foreach (int tr in tuhaoTotalRows) { iRefs.Add("I" + tr); gRefs.Add("G" + tr); } SetCellFormulaInRow(grandRow, "C", "ROUND(SUM(" + string.Join(",", iRefs.ToArray()) + "),0)", nsmgr); SetCellFormulaInRow(grandRow, "G", "SUM(" + string.Join(",", gRefs.ToArray()) + ")", nsmgr); } // H列大写金额=TEXT(C列,"[DBNum2]")&"元整" (跟着C列总报价金额变化) SetCellFormulaInRow(grandRow, "H", "TEXT(C" + rowNum + ",\"[DBNum2]\")&\"元整\"", nsmgr); } // useFormula=false 时保留模板占位符,由 sharedStrings.xml 替换填充 InsertRowAfter(sheetData, grandRow, ref insertAfter); // 总计行合并区域(模板原行15有 C15:D15 和 H15:J15) AddMergeCell(doc, ref mergeCellsNode, "C" + rowNum + ":D" + rowNum); AddMergeCell(doc, ref mergeCellsNode, "H" + rowNum + ":J" + rowNum); rowNum++; } // 6) 追加报价说明行(原模板行16) if (explainTplClone != null) { ((XmlElement)explainTplClone).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(explainTplClone, rowNum); InsertRowAfter(sheetData, explainTplClone, ref insertAfter); AddMergeCell(doc, ref mergeCellsNode, "A" + rowNum + ":J" + rowNum); rowNum++; } // 7) 追加报价人/审核人行(原模板行17) if (signTplClone != null) { ((XmlElement)signTplClone).SetAttribute("r", rowNum.ToString()); UpdateRowCellReferences(signTplClone, rowNum); InsertRowAfter(sheetData, signTplClone, ref insertAfter); AddMergeCell(doc, ref mergeCellsNode, "B" + rowNum + ":C" + rowNum); AddMergeCell(doc, ref mergeCellsNode, "D" + rowNum + ":G" + rowNum); AddMergeCell(doc, ref mergeCellsNode, "H" + rowNum + ":I" + rowNum); rowNum++; } // 8) 添加柜号超链接(点击B列柜号跳转到分项表对应区域) if (useHyperlink && cabLinks.Count > 0) { XmlNode hyperlinksNode = doc.CreateElement("hyperlinks", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); foreach (var link in cabLinks) { XmlElement hl = doc.CreateElement("hyperlink", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); hl.SetAttribute("ref", "B" + link.Key); hl.SetAttribute("location", "屏柜分项表!B" + link.Value); hl.SetAttribute("display", "=屏柜分项表!B" + link.Value); hyperlinksNode.AppendChild(hl); } // 按OOXML规范,hyperlinks在mergeCells之后;若没有mergeCells则放sheetData之后 XmlNode refNode = mergeCellsNode ?? sheetData; refNode.ParentNode.InsertAfter(hyperlinksNode, refNode); } entries[sheetKey] = System.Text.Encoding.UTF8.GetBytes(doc.OuterXml); } /// 在 insertAfter 之后插入行,并更新 insertAfter private void InsertRowAfter(XmlNode sheetData, XmlNode newRow, ref XmlNode insertAfter) { if (insertAfter == null) sheetData.AppendChild(newRow); else sheetData.InsertAfter(newRow, insertAfter); insertAfter = newRow; } /// 确保 mergeCells 节点存在并返回它(按 OOXML 顺序插入到 sheetData 之后) private XmlNode EnsureMergeCells(XmlDocument doc, ref XmlNode mergeCellsNode) { if (mergeCellsNode != null) return mergeCellsNode; XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); nsmgr.AddNamespace("s", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); mergeCellsNode = doc.CreateElement("mergeCells", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); ((XmlElement)mergeCellsNode).SetAttribute("count", "0"); // mergeCells 必须位于 sheetData 之后(OOXML schema 顺序) XmlNode sheetData = doc.SelectSingleNode("//s:sheetData", nsmgr); if (sheetData != null && sheetData.NextSibling != null) doc.DocumentElement.InsertAfter(mergeCellsNode, sheetData); else if (sheetData != null) doc.DocumentElement.AppendChild(mergeCellsNode); else doc.DocumentElement.AppendChild(mergeCellsNode); return mergeCellsNode; } /// 添加一个合并区域到 mergeCells,并自动更新 count private void AddMergeCell(XmlDocument doc, ref XmlNode mergeCellsNode, string range) { XmlNode mc = EnsureMergeCells(doc, ref mergeCellsNode); XmlElement me = doc.CreateElement("mergeCell", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); me.SetAttribute("ref", range); mc.AppendChild(me); int cnt = 0; int.TryParse(((XmlElement)mc).GetAttribute("count"), out cnt); ((XmlElement)mc).SetAttribute("count", (cnt + 1).ToString()); } /// 判断合并区域 "A11:J17" 是否与 >= startRow 的行重叠 private static bool MergeCellRangeOverlapsRow(string range, int startRow) { if (string.IsNullOrEmpty(range)) return false; string[] parts = range.Split(':'); if (parts.Length != 2) return false; int endRowNum = ParseRowNumber(parts[1]); int startRowNum = ParseRowNumber(parts[0]); if (endRowNum == 0) endRowNum = startRowNum; return endRowNum >= startRow || startRowNum >= startRow; } /// 从单元格引用(如 "K13")解析行号 private static int ParseRowNumber(string cellRef) { if (string.IsNullOrEmpty(cellRef)) return 0; string digits = ""; for (int i = 0; i < cellRef.Length; i++) { if (char.IsDigit(cellRef[i])) digits += cellRef[i]; } int n; return int.TryParse(digits, out n) ? n : 0; } // ====== 分项表 ====== // ============ 明细表:字符串模板替换法(零 DOM——上万像素级 DOM+OuterXml 双份字符串会 OOM) ============ /// 行字符串:更新行号与全部单元格引用(r="列行号")。 /// ★ 用 MatchEvaluator 而非 "$1" 替换串:"$1"+数字会被 .NET 贪婪解析成不存在的分组($1120),原样输出毁掉引用。 private static string 行换号(string rowXml, int rowNum) { rowXml = System.Text.RegularExpressions.Regex.Replace(rowXml, " " "r=\"" + m.Groups[1].Value + rowNum + "\""); } /// 行字符串:设置某列单元格内容(innerXml=单元格内部 XML;tAttr=类型属性,null=移除 t)。 private static string 行设单元格(string rowXml, string col, int rowNum, string innerXml, string tAttr) { string r = col + rowNum; var m = System.Text.RegularExpressions.Regex.Match(rowXml, "]*?)(?:/>|>.*?)"); if (!m.Success) return rowXml; string attrs = m.Groups[1].Value; attrs = System.Text.RegularExpressions.Regex.Replace(attrs, "\\s*t=\"[^\"]*\"", ""); if (!string.IsNullOrEmpty(tAttr)) attrs += " t=\"" + tAttr + "\""; return rowXml.Substring(0, m.Index) + "" + innerXml + "" + rowXml.Substring(m.Index + m.Length); } private static string 行设文本(string rowXml, string col, int rowNum, string text) { return 行设单元格(rowXml, col, rowNum, "" + XmlEscape(text) + "", "str"); } private static string 行设数值(string rowXml, string col, int rowNum, decimal v) { return 行设单元格(rowXml, col, rowNum, "" + v.ToString(System.Globalization.CultureInfo.InvariantCulture) + "", null); } private static string 行设公式(string rowXml, string col, int rowNum, string formula) { return 行设单元格(rowXml, col, rowNum, "" + XmlEscape(formula) + "", null); } private void FillDetailSheet(Dictionary entries, string sheetKey, List tuhaoList, List cabRowInfos, System.Text.StringBuilder dbgLog, bool useFormula) { string xml = System.Text.Encoding.UTF8.GetString(entries[sheetKey]); // 提取全部行块,定位模板行 11~16 与头/尾(零 DOM) var 行匹配 = System.Text.RegularExpressions.Regex.Matches( xml, "]*?(?:/>|>.*?)"); if (行匹配.Count == 0) return; string[] tpl = new string[6]; int bodyStart = -1, bodyEnd = -1; foreach (System.Text.RegularExpressions.Match m in 行匹配) { int rn = int.Parse(m.Groups[1].Value); if (rn >= 11 && rn <= 16) { tpl[rn - 11] = m.Value; if (bodyStart < 0) bodyStart = m.Index; bodyEnd = m.Index + m.Length; } } string tplPaperRow = tpl[0], tplCabRow = tpl[1], tplHeaderRow = tpl[2], tplElemRow = tpl[3], tplSingleRow = tpl[4], tplGrandRow = tpl[5]; if (tplCabRow == null || tplElemRow == null) return; string 头 = xml.Substring(0, bodyStart); // 行1~10 string 尾 = xml.Substring(bodyEnd); // 行17(签名)及之后 var sb = new System.Text.StringBuilder(); int rowNum = 11; decimal grandQty = 0, grandTotal = 0; int tuhaoIdx = 0; foreach (var tuhao in tuhaoList) { string tuhaoName = !string.IsNullOrEmpty(tuhao.TuMing) ? tuhao.TuMing : tuhao.Tuhao; // 图名行 if (tplPaperRow != null) { string row = 行换号(tplPaperRow, rowNum); row = 行设文本(row, "A", rowNum, tuhaoName); sb.Append(row); rowNum++; } // 每个箱号: 柜号行 + 表头 + 元件行*N + 单台合计 + 总计 int cabOrder = 0; foreach (var cab in tuhao.Cabinets) { cabOrder++; int cabRowNum = rowNum; // 柜号行 { string row = 行换号(tplCabRow, rowNum); row = 行设文本(row, "A", rowNum, (tuhaoIdx + 1) + "-" + cabOrder); row = 行设文本(row, "B", rowNum, "柜号:" + cab.CabinetNo); row = 行设文本(row, "D", rowNum, cab.Model); // ★ G列"名称:"= 箱名(进线柜/PT柜等);库里没有时退回图名(与旧版行为一致) string 名称值 = !string.IsNullOrEmpty(cab.BoxName) ? cab.BoxName : tuhaoName; row = 行设文本(row, "G", rowNum, 名称值); row = 行设文本(row, "I", rowNum, cab.SpecName); sb.Append(row); rowNum++; } // 表头行 if (tplHeaderRow != null) { sb.Append(行换号(tplHeaderRow, rowNum)); rowNum++; } // 元件行(G列用 =E*F 公式) int elemStartRow = rowNum; if (cab.Elements.Count == 0) { sb.Append(行换号(tplElemRow, rowNum)); rowNum++; } else { int elemOrder = 0; foreach (var elem in cab.Elements) { elemOrder++; string row = 行换号(tplElemRow, rowNum); row = 行设文本(row, "A", rowNum, elemOrder.ToString()); row = 行设文本(row, "B", rowNum, elem.Name); row = 行设文本(row, "C", rowNum, elem.ShowSpecName); row = 行设文本(row, "D", rowNum, elem.Unit); row = 行设数值(row, "E", rowNum, SafeDec(elem.Count)); row = 行设数值(row, "F", rowNum, elem.OfferUnitPrice); // G列: 公式 =E{行}*F{行} 或直接值 if (useFormula) row = 行设公式(row, "G", rowNum, "E" + rowNum + "*F" + rowNum); else row = 行设数值(row, "G", rowNum, SafeDec(elem.Count) * elem.OfferUnitPrice); row = 行设文本(row, "H", rowNum, elem.FactName); row = 行设文本(row, "I", rowNum, elem.Remark); sb.Append(row); rowNum++; } } int elemEndRow = rowNum - 1; // 单台合计行(G列用 =ROUND(SUM(G{起}:G{止}),0) 公式) int singleRowNum = rowNum; if (tplSingleRow != null) { string row = 行换号(tplSingleRow, rowNum); row = 行设文本(row, "B", rowNum, "单台合计"); if (useFormula) row = 行设公式(row, "G", rowNum, "ROUND(SUM(G" + elemStartRow + ":G" + elemEndRow + "),0)"); else { decimal elemSum = 0; foreach (var elem in cab.Elements) elemSum += SafeDec(elem.Count) * elem.OfferUnitPrice; row = 行设数值(row, "G", rowNum, elemSum); } sb.Append(row); rowNum++; } // 总计行(G列用 =G{单台合计}*E{总计} 公式) int grandRowNum = rowNum; if (tplGrandRow != null) { string row = 行换号(tplGrandRow, rowNum); row = 行设文本(row, "B", rowNum, "总计"); row = 行设文本(row, "D", rowNum, "台"); row = 行设数值(row, "E", rowNum, SafeDec(cab.Count)); if (useFormula) row = 行设公式(row, "G", rowNum, "G" + singleRowNum + "*E" + rowNum); else { decimal elemSum = 0; foreach (var elem in cab.Elements) elemSum += SafeDec(elem.Count) * elem.OfferUnitPrice; row = 行设数值(row, "G", rowNum, elemSum * SafeDec(cab.Count)); } sb.Append(row); rowNum++; } // 记录行号供汇总表引用 cabRowInfos.Add(new CabinetRowInfo { CabRow = cabRowNum, SingleTotalRow = singleRowNum, GrandRow = grandRowNum }); grandQty += SafeDec(cab.Count); grandTotal += cab.OfferTotalPrice; } tuhaoIdx++; } entries[sheetKey] = System.Text.Encoding.UTF8.GetBytes(头 + sb.ToString() + 尾); } // ====== XML 行/单元格操作辅助 ====== private class RowSpec { public string Type = null; public CabinetData Cab = null; public ElementData Elem = null; public int CabOrder = 0; } /// 更新行内所有单元格的行号引用 (如 r="A13" → r="A15") private void UpdateRowCellReferences(XmlNode rowNode, int newRowNum) { foreach (XmlNode cell in rowNode.ChildNodes) { if (cell is XmlElement) { string r = ((XmlElement)cell).GetAttribute("r"); if (!string.IsNullOrEmpty(r)) { // r 格式如 "K3" → 替换数字部分 string colPart = ""; for (int i = 0; i < r.Length; i++) { if (char.IsLetter(r[i])) colPart += r[i]; else break; } ((XmlElement)cell).SetAttribute("r", colPart + newRowNum.ToString()); } } } } /// 按行号查找行节点 private XmlNode FindRowByNumber(XmlNode sheetData, int rowNum, XmlNamespaceManager nsmgr) { foreach (XmlNode row in sheetData.SelectNodes("s:row", nsmgr)) { if (((XmlElement)row).GetAttribute("r") == rowNum.ToString()) return row; } return null; } /// 设置行中指定列的字符串值 private void SetCellInRow(XmlNode rowNode, string colLetter, string value, XmlNamespaceManager nsmgr) { string targetRef = colLetter + ((XmlElement)rowNode).GetAttribute("r"); foreach (XmlNode cell in rowNode.ChildNodes) { if (!(cell is XmlElement)) continue; string r = ((XmlElement)cell).GetAttribute("r"); if (r == targetRef) { // 清除现有内容,设置新值 cell.InnerXml = "" + XmlEscape(value) + ""; ((XmlElement)cell).SetAttribute("t", "str"); return; } } } /// 设置行中指定列的数值 private void SetCellNumericInRow(XmlNode rowNode, string colLetter, decimal value, XmlNamespaceManager nsmgr) { string targetRef = colLetter + ((XmlElement)rowNode).GetAttribute("r"); foreach (XmlNode cell in rowNode.ChildNodes) { if (!(cell is XmlElement)) continue; string r = ((XmlElement)cell).GetAttribute("r"); if (r == targetRef) { cell.InnerXml = "" + value.ToString(System.Globalization.CultureInfo.InvariantCulture) + ""; ((XmlElement)cell).RemoveAttribute("t"); // 数值类型不需要 t 属性 return; } } } /// 设置行中指定列的公式 private void SetCellFormulaInRow(XmlNode rowNode, string colLetter, string formula, XmlNamespaceManager nsmgr) { string targetRef = colLetter + ((XmlElement)rowNode).GetAttribute("r"); foreach (XmlNode cell in rowNode.ChildNodes) { if (!(cell is XmlElement)) continue; string r = ((XmlElement)cell).GetAttribute("r"); if (r == targetRef) { // 公式格式: 公式 cell.InnerXml = "" + XmlEscape(formula) + ""; ((XmlElement)cell).RemoveAttribute("t"); // 公式单元格不需要 t 属性 return; } } } /// 设置行中指定列单元格的样式索引(s属性) private void SetCellStyle(XmlNode rowNode, string colLetter, int styleIdx, XmlNamespaceManager nsmgr) { string targetRef = colLetter + ((XmlElement)rowNode).GetAttribute("r"); foreach (XmlNode cell in rowNode.ChildNodes) { if (!(cell is XmlElement)) continue; string r = ((XmlElement)cell).GetAttribute("r"); if (r == targetRef) { ((XmlElement)cell).SetAttribute("s", styleIdx.ToString()); return; } } } // ====== 清理不需要的方法 ====== private static string FindPythonExe() { return "python.exe"; } private static string JsonEscape(string s) { return s ?? ""; } // ====== 通用辅助 ====== private static string SafeStr(object o) { if (o == null || o == DBNull.Value) return ""; return o.ToString(); } /// 防御性读取字符串列:老项目库的箱号表可能还没有"箱名"列,缺列时返回空串而不是抛异常。 private static string SafeStrCol(System.Data.SQLite.SQLiteDataReader reader, string col) { try { int i = reader.GetOrdinal(col); if (i < 0) return ""; if (reader.IsDBNull(i)) return ""; return SafeStr(reader.GetValue(i)); } catch { return ""; } } private static decimal SafeDec(object o) { if (o == null || o == DBNull.Value) return 0m; decimal d; return decimal.TryParse(o.ToString(), out d) ? d : 0m; } private static string GetDict(Dictionary dict, string key) { string v; return dict.TryGetValue(key, out v) ? (v ?? "") : ""; } private static int TotalCabinetCount(List list) { int n = 0; foreach (var t in list) n += t.Cabinets.Count; return n; } void Chk显示图纸规格选中状态更改(object sender, EventArgs e) { } void Btn打开模板单击(object sender, EventArgs e) { } void Chk表格带Excel公式选中状态更改(object sender, EventArgs e) { } } }