using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Windows.Forms; namespace 成套报价软件 { /// /// 费用模板 的交互逻辑(2026-10-01 G1-1): /// 维护"费用模板"列表(名称/计算方式/数值/计入),点「应用到当前项目」把启用模板 /// 换算成"费用行"写入当前项目每个箱号的元件表(类型='费用',参与箱号/图号/项目聚合, /// 与普通元件走同一套报价公式,重算稳定)。 /// 计算方式:固定金额(每柜)=数值;元件采购价×%=该箱号非费用元件采购价合计×数值%; /// 元件报出价×%=非费用元件报出价合计×数值%。 /// 计入:采购+报出(采购系数=1)/仅报出(采购系数=0,成本不含此费用)。 /// 模板存储:exe\sjk\费用模板.xml(全局,与项目无关)。 /// 布局在 费用模板.Designer.cs。 /// public partial class 费用模板 : Form { private readonly MainForm _主窗; /// 一条费用模板。 /// ★ 计入维度已删(2026-10-01 修复):费用固定"采购+报出"双向计入——公式链 /// 报出单价=采购单价×报出系数 决定了"仅报出"(采购系数=0)会把报出价也清零, /// 数学上不成立;辅料/人工/成套费本就应进成本,"只抬报价"请用「调系数」。 public class 模板行 { public bool 启用 = true; public string 名称 = ""; public string 方式 = "固定金额(每柜)"; // 固定金额(每柜)/元件采购价×%/元件报出价×% public decimal 数值 = 0m; } public 费用模板(MainForm 主窗) { InitializeComponent(); _主窗 = 主窗; Load模板(); 刷列表(); // ★ 事件挂接在 Designer.cs(SD 设计器可见可编辑,铁律:Designer.cs 纯布局+事件, // 逻辑与处理器方法在本文件) cbo方式.SelectedIndex = 0; } // ===== 模板存取(sjk\费用模板.xml) ===== private static string 模板路径() { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sjk", "费用模板.xml"); } private List<模板行> _模板s = new List<模板行>(); private void Load模板() { try { _模板s = new List<模板行>(); string f = 模板路径(); if (!File.Exists(f)) return; string xml = File.ReadAllText(f, Encoding.UTF8); int p = 0; while (true) { int s = xml.IndexOf("<模板>", p, StringComparison.Ordinal); if (s < 0) break; int e = xml.IndexOf("", s, StringComparison.Ordinal); if (e < 0) break; string seg = xml.Substring(s + 4, e - s - 4); _模板s.Add(new 模板行 { 启用 = 取段(seg, "启用") == "1", 名称 = 取段(seg, "名称"), 方式 = 取段(seg, "方式"), 数值 = TryParseDec(取段(seg, "数值")) // 旧 XML 的 <计入> 段忽略(维度已删) }); p = e; } } catch { } } private static string 取段(string xml, string 标签) { string 开 = "<" + 标签 + ">", 关 = ""; int i = xml.IndexOf(开, StringComparison.Ordinal); if (i < 0) return ""; int j = xml.IndexOf(关, i + 开.Length, StringComparison.Ordinal); if (j < 0) return ""; return xml.Substring(i + 开.Length, j - i - 开.Length).Trim(); } private void Save模板() { try { var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("<费用模板列表>"); foreach (var t in _模板s) { sb.AppendLine(" <模板>"); sb.AppendLine(" <启用>" + (t.启用 ? "1" : "0") + ""); sb.AppendLine(" <名称>" + (t.名称 ?? "") + ""); sb.AppendLine(" <方式>" + (t.方式 ?? "") + ""); sb.AppendLine(" <数值>" + t.数值.ToString("0.####", CultureInfo.InvariantCulture) + ""); sb.AppendLine(" "); } sb.AppendLine(""); string f = 模板路径(); string dir = Path.GetDirectoryName(f); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); File.WriteAllText(f, sb.ToString(), new UTF8Encoding(true)); } catch (Exception ex) { MessageBox.Show(this, "保存模板失败: " + ex.Message, "费用模板"); } } private static decimal TryParseDec(string s) { decimal d; if (decimal.TryParse((s ?? "").Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d; decimal.TryParse((s ?? "").Trim(), out d); return d; } // ===== 界面 ===== /// 合法的方式取值(与 Designer 列 Items、编辑区 ComboBox Items 保持一致)。 private static readonly string[] 合法方式 = { "固定金额(每柜)", "元件采购价×%", "元件报出价×%" }; private static string 归一化方式(string s) { foreach (string v in 合法方式) if (v == s) return v; return 合法方式[0]; } private void 刷列表() { dgv模板.Rows.Clear(); foreach (var t in _模板s) dgv模板.Rows.Add(t.启用, t.名称, 归一化方式(t.方式), t.数值.ToString("0.####")); } /// ★ 修复(2026-10-01):ComboBox 列值不在 Items 时吞掉 DataError 默认弹框 /// (写值已归一化,此为兜底;异常记日志便于排查)。 private void dgv模板_DataError(object sender, DataGridViewDataErrorEventArgs e) { e.ThrowException = false; try { System.Diagnostics.Debug.WriteLine("[费用模板] DataError 行" + e.RowIndex + " 列" + e.ColumnIndex + ": " + e.Exception.Message); } catch { } } private void dgv模板_SelectionChanged(object sender, EventArgs e) { if (dgv模板.CurrentRow == null) return; int i = dgv模板.CurrentRow.Index; if (i < 0 || i >= _模板s.Count) return; var t = _模板s[i]; txt名称.Text = t.名称; cbo方式.SelectedItem = 归一化方式(t.方式); txt数值.Text = t.数值.ToString("0.####"); } private 模板行 从编辑区读() { var t = new 模板行(); t.名称 = (txt名称.Text ?? "").Trim(); t.方式 = cbo方式.SelectedItem == null ? "固定金额(每柜)" : cbo方式.SelectedItem.ToString(); t.数值 = TryParseDec(txt数值.Text); return t; } private void btn新增_Click(object sender, EventArgs e) { var t = 从编辑区读(); if (t.名称.Length == 0) { MessageBox.Show(this, "请填写费用名称。", "费用模板"); return; } if (t.数值 <= 0m && t.方式 != "固定金额(每柜)") { MessageBox.Show(this, "百分比方式的数值必须大于 0。", "费用模板"); return; } _模板s.Add(t); Save模板(); 刷列表(); dgv模板.CurrentCell = dgv模板.Rows[dgv模板.Rows.Count - 1].Cells[1]; } private void btn修改_Click(object sender, EventArgs e) { if (dgv模板.CurrentRow == null) { MessageBox.Show(this, "请先在列表选中要修改的模板。", "费用模板"); return; } int i = dgv模板.CurrentRow.Index; if (i < 0 || i >= _模板s.Count) return; var t = 从编辑区读(); if (t.名称.Length == 0) { MessageBox.Show(this, "请填写费用名称。", "费用模板"); return; } _模板s[i] = t; Save模板(); 刷列表(); } private void btn删除_Click(object sender, EventArgs e) { if (dgv模板.CurrentRow == null) { MessageBox.Show(this, "请先在列表选中要删除的模板。", "费用模板"); return; } int i = dgv模板.CurrentRow.Index; if (i < 0 || i >= _模板s.Count) return; _模板s.RemoveAt(i); Save模板(); 刷列表(); } private void btn应用_Click(object sender, EventArgs e) { var 启用的 = _模板s.FindAll(t => t.启用); if (启用的.Count == 0) { MessageBox.Show(this, "没有勾选「启用」的模板,先勾选或新增。", "费用模板"); return; } if (MessageBox.Show(this, "将把 " + 启用的.Count + " 条启用模板换算成费用行,写入当前项目【每个箱号】的元件表:\n" + "· 之前应用过的同名费用行会被替换(可反复应用)\n" + "· 各箱号成本价/报出价、图号总价、项目汇总会自动重算\n\n确认应用?", "应用到当前项目", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; try { string 结果 = _主窗.应用费用模板到当前项目(启用的); MessageBox.Show(this, 结果, "应用完成", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show(this, "应用失败:\n" + ex.Message, "费用模板", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void btn清空费用_Click(object sender, EventArgs e) { if (MessageBox.Show(this, "删除当前项目所有箱号元件表里「类型=费用」的行?\n(普通元件行不受影响,价格自动重算)", "清空项目费用行", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; try { string 结果 = _主窗.清空项目费用行(); MessageBox.Show(this, 结果, "清空完成", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show(this, "清空失败:\n" + ex.Message, "费用模板", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } }