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)
{
return "Data Source=" + dbPath + ";Version=3;";
}
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)
{
}
}
}