using System; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.Drawing; using System.IO; using System.Windows.Forms; using Microsoft.Win32; using Smart.CustomComponents; namespace 成套报价软件 { /// 主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。 public partial class MainForm { // ============================================================ // 右键菜单:创建并挂载 // ============================================================ // ----- 复制/粘贴剪贴板(每个层级独立缓存)----- // 图号:存图号行 + 其所有箱号表内容 + 各箱号下元件表内容 private class ClipTuhao { public string ProjectId; public string SrcTuhaoName; // 源图号显示名(供参考) public System.Collections.Generic.List> TuhaoRow; public System.Collections.Generic.List> XianghaoRows; // 每个箱号对应的元件表内容(按下标和 XianghaoRows 对应) public System.Collections.Generic.List>> YuanjianRowsPerXianghao; } private System.Collections.Generic.List _clipTuhaos; // 箱柜:存箱号行 + 其下所有元件行 private class ClipXianghao { public string ProjectId; public string SrcXianghaoName; public System.Collections.Generic.Dictionary XianghaoRow; public System.Collections.Generic.List> YuanjianRows; } private System.Collections.Generic.List _clipXianghaos; // 元件:存多行(支持多选复制) private System.Collections.Generic.List> _clipYuanjianRows; /// 把 DataGridViewRow(绑定到 DataTable)的字段拷贝为字典(排除序号和内部ID)。 private System.Collections.Generic.Dictionary CopyRowFields(DataGridViewRow row, System.Collections.Generic.HashSet excludeCols) { var dict = new System.Collections.Generic.Dictionary(); if (row == null) return dict; // ★ 虚拟模式:按行号从内存表读(无 DataBoundItem) var vtable = 取dgv数据表(row.DataGridView); if (vtable != null && row.Index >= 0 && row.Index < vtable.Rows.Count) { foreach (System.Data.DataColumn dc in vtable.Columns) { if (dc.ColumnName == "_rowid_") continue; // 行定位列,不参与复制 if (excludeCols != null && excludeCols.Contains(dc.ColumnName)) continue; object v = vtable.Rows[row.Index][dc.ColumnName]; dict[dc.ColumnName] = (v == DBNull.Value) ? null : v; } return dict; } var drv = row.DataBoundItem as DataRowView; if (drv == null || drv.Row == null) return dict; DataTable dt = drv.Row.Table; if (dt == null) return dict; foreach (DataColumn dc in dt.Columns) { if (excludeCols != null && excludeCols.Contains(dc.ColumnName)) continue; dict[dc.ColumnName] = drv.Row[dc]; } return dict; } /// 从 SQLite 读取整个表所有行的字段字典列表(用于复制时备份下级表)。 private static System.Collections.Generic.List> ReadAllRowsFromTable(string dbPath, string tableName) { var list = new System.Collections.Generic.List>(); if (string.IsNullOrEmpty(dbPath) || string.IsNullOrEmpty(tableName)) return list; if (!System.IO.File.Exists(dbPath)) return list; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); string sql = "SELECT * FROM \"" + tableName + "\";"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var dict = new System.Collections.Generic.Dictionary(); for (int i = 0; i < reader.FieldCount; i++) { dict[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i); } list.Add(dict); } } } } catch { } return list; } /// GetConnString 的静态包装,供静态方法调用。 private static string GetConnStringStatic(string dbPath) { // ★ B4(2026-10-01):与实例版同款加 BusyTimeout(防并发锁直报错) return "Data Source=\"" + dbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=5000;"; } /// /// 生成唯一的内部ID:先试 baseName+"(副本)",有重复就 +"(副本1)"、+"(副本2)"…递增。 /// checkAction 返回 true 表示该名称在目标表里已存在。 /// 会先清除原名里旧的 "_副本XXXX"(GUID残留)和旧的 "(副本N)" 后缀,只保留干净原名。 /// private static string MakeUniqueName(string baseName, System.Func exists) { if (string.IsNullOrEmpty(baseName)) baseName = "未命名"; // 清除原名里旧的命名残留,只留干净的原始名 baseName = CleanItemName(baseName); string candidate = baseName + "(副本)"; if (exists == null || !exists(candidate)) return candidate; int i = 1; while (true) { candidate = baseName + "(副本" + i.ToString() + ")"; if (!exists(candidate)) return candidate; i++; if (i > 9999) // 防止死循环 { return baseName + "(副本" + System.Guid.NewGuid().ToString("N").Substring(0, 6) + ")"; } } } /// /// 清除名称里的旧命名残留: /// "_副本XXXX"(8位十六进制 GUID 残留)、"(副本)"、"(副本N)" 全部去掉。 /// 例:"新图号1_副本966a(副本)" → "新图号1" /// "箱号A_副本a3b2" → "箱号A" /// "原名(副本2)" → "原名" /// private static string CleanItemName(string name) { if (string.IsNullOrEmpty(name)) return name; // 反复清理,直到没有可清理的模式(一次可能清不干净,如"(副本)(副本)") string s = name; for (int loop = 0; loop < 5; loop++) { string before = s; // 去掉末尾的 _副本 + 4~8位十六进制(GUID残留) s = System.Text.RegularExpressions.Regex.Replace(s, @"_副本[0-9a-fA-F]{4,8}$", ""); // 去掉末尾的 (副本) 或 (副本N) s = System.Text.RegularExpressions.Regex.Replace(s, @"\(副本\d*\)$", ""); if (s == before) break; // 没变化就停止 } return s.Trim(); } /// 检查图号表里是否已存在某个内部ID。 private bool TuhaoIdExists(string projectId, string id) { if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrEmpty(id)) return false; string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return false; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); string sql = "SELECT COUNT(*) FROM " + TuhaoTableName + " WHERE 内部ID=@id;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@id", id); return Convert.ToInt32(cmd.ExecuteScalar()) > 0; } } } catch { return false; } } /// 检查某图号下箱号表里是否已存在某个内部ID。 private bool XianghaoIdExists(string projectId, string tuhao, string id) { if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrEmpty(id)) return false; string tbl = GetXianghaoTableByIndex(projectId, tuhao); if (string.IsNullOrEmpty(tbl)) return false; string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return false; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); string sql = "SELECT COUNT(*) FROM \"" + tbl + "\" WHERE 内部ID=@id;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@id", id); return Convert.ToInt32(cmd.ExecuteScalar()) > 0; } } } catch { return false; } } // ===== 图号:复制/粘贴 ===== private volatile bool _复制图号中; private void MenuTuhao_Copy_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao)) { MessageBox.Show(this, "请先选择一个箱柜。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var rows = GetAllSelectedRows(this.advancedDataGridView9); if (rows.Count == 0) { MessageBox.Show(this, "请先选择要复制的图号。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } rows.Sort((a, b) => a.Index.CompareTo(b.Index)); if (_复制图号中) { MessageBox.Show(this, "上一个复制仍在后台进行,请稍候。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" }; string pid = _currentProjectId; // UI 线程:先备好每行的字段字典(避免后台线程碰表格对象) var 待收集 = new System.Collections.Generic.List>>(); foreach (var row in rows) { string tuhaoId = GetBoundValueAsString(row, "内部ID"); if (string.IsNullOrWhiteSpace(tuhaoId)) { Log("[复制图号] 跳过无内部ID的行(行" + (row.Index + 1) + ")"); continue; } 待收集.Add(System.Tuple.Create(tuhaoId, CopyRowFields(row, exclude))); } if (待收集.Count == 0) { MessageBox.Show(this, "选中的行都没有图号内部ID,无法复制。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // ★ 复制前先把未保存的编辑落库:子树从库读,不保存会复制到旧数据 SaveAllQuoteTablesIfDirty(false); // ★ 子树读取(全部箱号+元件)放后台线程,界面不卡 _复制图号中 = true; Cursor = Cursors.WaitCursor; Log("[复制图号] 开始收集 " + 待收集.Count + " 个图号的子树..."); var 复制线程 = new System.Threading.Thread(() => { System.Collections.Generic.List 结果 = null; Exception 错 = null; try { 结果 = new System.Collections.Generic.List(); string dbPath = GetQuoteDbPath(pid); foreach (var item in 待收集) { var clip = new ClipTuhao { ProjectId = pid, SrcTuhaoName = item.Item1, TuhaoRow = new System.Collections.Generic.List> { item.Item2 }, XianghaoRows = new System.Collections.Generic.List>(), YuanjianRowsPerXianghao = new System.Collections.Generic.List>>() }; string xhTbl = LookupTableByIndex(pid, "箱号", item.Item1, ""); if (!string.IsNullOrEmpty(xhTbl)) { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); try { string sql = "SELECT * FROM \"" + xhTbl + "\";"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var dict = new System.Collections.Generic.Dictionary(); for (int i = 0; i < reader.FieldCount; i++) dict[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i); clip.XianghaoRows.Add(dict); } } } catch { } foreach (var xhRow in clip.XianghaoRows) { string xhId = (xhRow.ContainsKey("内部ID") ? xhRow["内部ID"] : null) as string; if (string.IsNullOrWhiteSpace(xhId)) xhId = (xhRow.ContainsKey("箱号") ? xhRow["箱号"] : null) as string; var yjRows = new System.Collections.Generic.List>(); string yjTbl = GetYuanjianTableByIndex(item.Item1, xhId, conn); if (!string.IsNullOrEmpty(yjTbl)) { try { string sql = "SELECT * FROM \"" + yjTbl + "\";"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var dict = new System.Collections.Generic.Dictionary(); for (int i = 0; i < reader.FieldCount; i++) dict[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i); yjRows.Add(dict); } } } catch { } } clip.YuanjianRowsPerXianghao.Add(yjRows); } } } 结果.Add(clip); } } catch (Exception ex) { 错 = ex; } BeginInvoke((MethodInvoker)delegate { _复制图号中 = false; Cursor = Cursors.Default; if (错 != null) { Log("[复制图号] 异常: " + 错.Message); MessageBox.Show(this, "复制图号失败:\n" + 错.Message, "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } _clipTuhaos = 结果; int 箱号总数 = 0; foreach (var c in 结果) 箱号总数 += c.XianghaoRows.Count; Log("[复制图号] 已缓存图号 " + 结果.Count + " 个,含箱号 " + 箱号总数 + " 个"); SetStatus("已复制 " + 结果.Count + " 个图号(含 " + 箱号总数 + " 个箱号), 请选择位置后粘贴"); }); }); 复制线程.IsBackground = true; 复制线程.Start(); } private void MenuTuhao_Paste_Click(object sender, EventArgs e) { if (_复制图号中) { MessageBox.Show(this, "复制仍在后台进行,请稍候再粘贴。", "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (_clipTuhaos == null || _clipTuhaos.Count == 0) { MessageBox.Show(this, "剪贴板里没有图号,请先复制。", "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先打开一个项目。", "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView9); var clips = _clipTuhaos; string pid = _currentProjectId; var sw = System.Diagnostics.Stopwatch.StartNew(); Cursor = Cursors.WaitCursor; Log("[粘贴图号] 开始,共 " + clips.Count + " 个图号..."); // ★ 粘贴前先把未保存的编辑落库:粘贴回调会清缓存并从库 Reload, // 不保存的话用户修改只存在于内存表,Reload 后被旧数据覆盖丢失 SaveAllQuoteTablesIfDirty(false); // ★ 内存优先管线:表索引/现有图号/序号一次载入内存,SQL 只剩批量落库 var 粘贴线程 = new System.Threading.Thread(() => { string lastTuhaoId = ""; int pasted = 0; Exception 错 = null; try { string dbPath = GetQuoteDbPath(pid); EnsureQuoteDbExists(pid); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); // ★ 诊断:确认连接的库文件与 WAL 状态(排查"提交后数据丢失") using (var dcmd = new System.Data.SQLite.SQLiteCommand( "PRAGMA journal_mode; SELECT @p AS p;", conn)) { dcmd.Parameters.AddWithValue("@p", dbPath); Log("[粘贴图号] 库=" + dbPath + " journal_mode=" + dcmd.ExecuteScalar()); // ★ 公式加载(Excel 式公式表):触发器生成与管线内联计算共用 using (var fakConn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic( GetDbFullPath(@"sjk\fak.db")))) { fakConn.Open(); 计算引擎.EnsureFormulaTable(fakConn); _公式_元件 = 计算引擎.加载公式(fakConn, "元件"); _公式_箱号 = 计算引擎.加载公式(fakConn, "箱号"); _公式_图号 = 计算引擎.加载公式(fakConn, "图号"); } } using (var tx = conn.BeginTransaction()) { // 内存快照:箱号/元件表最大序号、现有图号内部ID int xhMax = 0, yjMax = 0, thSeqMax = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 类型, 实际表名 FROM 表索引;", conn, tx)) using (var r = cmd.ExecuteReader()) { while (r.Read()) { string ty = r.IsDBNull(0) ? "" : r.GetString(0); string real = r.IsDBNull(1) ? "" : r.GetString(1); if (ty == "箱号" && real.StartsWith("xh_")) { int n; if (int.TryParse(real.Substring(3), out n) && n > xhMax) xhMax = n; } else if (ty == "元件" && real.StartsWith("yj_")) { int n; if (int.TryParse(real.Substring(3), out n) && n > yjMax) yjMax = n; } } } using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COALESCE(MAX(CAST(序号 AS INTEGER)),0) FROM " + TuhaoTableName + ";", conn, tx)) { object o = cmd.ExecuteScalar(); if (o != null && o != DBNull.Value) thSeqMax = Convert.ToInt32(o); } int 快照图号数; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM " + TuhaoTableName + ";", conn, tx)) { object o = cmd.ExecuteScalar(); 快照图号数 = (o == null || o == DBNull.Value) ? 0 : Convert.ToInt32(o); } Log("[粘贴图号] 快照: 事务内图号数=" + 快照图号数 + " xhMax=" + xhMax + " yjMax=" + yjMax); var 现有图号 = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 内部ID FROM " + TuhaoTableName + ";", conn, tx)) using (var r = cmd.ExecuteReader()) { while (r.Read()) { if (!r.IsDBNull(0)) { string v = r.GetString(0); if (v.Length > 0) 现有图号.Add(v); } } } foreach (var clip in clips) { string baseName = !string.IsNullOrWhiteSpace(clip.SrcTuhaoName) ? clip.SrcTuhaoName : "图号"; string nt = 唯一化名称(baseName, 现有图号); 现有图号.Add(nt); thSeqMax++; // ★ 清理同名图号的残留索引和动态表: // 此前删除图号行时索引/表可能残留,导致点击该图号时 // LIMIT 1 命中旧残缺表、显示错数据 using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 类型, 实际表名 FROM 表索引 WHERE 图号=@th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); using (var r = cmd.ExecuteReader()) { while (r.Read()) { string tbl = r.IsDBNull(1) ? "" : r.GetString(1); if (string.IsNullOrWhiteSpace(tbl)) continue; using (var dcmd = new System.Data.SQLite.SQLiteCommand( "DROP TABLE IF EXISTS \"" + tbl + "\";", conn, tx)) dcmd.ExecuteNonQuery(); } } } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 图号=@th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); int n = cmd.ExecuteNonQuery(); if (n > 0) Log("[粘贴图号] 清理[" + nt + "]的残留索引 " + n + " 行"); } using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO " + TuhaoTableName + " (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " + "VALUES (@s, @th, @ts, @sl, @dj, @zj, @cbzj, @ml, @mll, @iid);", conn, tx)) { // ★ 副本=源:聚合值直接抄源图号行(箱号/元件表是完整副本,聚合天然相等) var srcTh = clip.TuhaoRow.Count > 0 ? clip.TuhaoRow[0] : null; cmd.Parameters.AddWithValue("@s", thSeqMax.ToString()); cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@ts", GetSrcField(srcTh, "台数", "1")); cmd.Parameters.AddWithValue("@sl", GetSrcFieldDecimal(srcTh, "箱柜数量", 0)); cmd.Parameters.AddWithValue("@dj", GetSrcFieldDecimal(srcTh, "单价", 0)); cmd.Parameters.AddWithValue("@zj", GetSrcFieldDecimal(srcTh, "总价", 0)); cmd.Parameters.AddWithValue("@cbzj", GetSrcFieldDecimal(srcTh, "成本总价", 0)); cmd.Parameters.AddWithValue("@ml", GetSrcFieldDecimal(srcTh, "毛利", 0)); cmd.Parameters.AddWithValue("@mll", GetSrcFieldDecimal(srcTh, "毛利率", 0)); cmd.Parameters.AddWithValue("@iid", nt); cmd.ExecuteNonQuery(); } var 已用箱号名 = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase); // ★ 本图号的箱号表只建一张(每图号一张表,表内多行=多个箱号),全部箱号行进同一张表 xhMax++; string xhTbl = "xh_" + xhMax; using (var cmd = new System.Data.SQLite.SQLiteCommand( "CREATE TABLE \"" + xhTbl + "\" (" + " 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," + " 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," + " 内部ID TEXT);", conn, tx)) cmd.ExecuteNonQuery(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('箱号', @th, '', @real);", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@real", xhTbl); cmd.ExecuteNonQuery(); } // 箱号表触发器管尾统一建(行级依赖各行的元件表名) if (!_待建xh触发器.Exists(x => x[0] == xhTbl)) _待建xh触发器.Add(new[] { xhTbl, nt }); int xhSeq = 0; for (int i = 0; i < clip.XianghaoRows.Count; i++) { var xhRow = clip.XianghaoRows[i]; string srcXhId = (xhRow.ContainsKey("内部ID") ? xhRow["内部ID"] : null) as string; if (string.IsNullOrWhiteSpace(srcXhId)) srcXhId = (xhRow.ContainsKey("箱号") ? xhRow["箱号"] : null) as string; string nx = 唯一化名称(!string.IsNullOrWhiteSpace(srcXhId) ? srcXhId : "箱号", 已用箱号名); 已用箱号名.Add(nx); // ★ 清理该箱号名下的旧元件表残留(防 LIMIT 1 命中旧表) using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 类型='元件' AND 图号=@th AND 箱号=@xh;", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@xh", nx); using (var r = cmd.ExecuteReader()) { var oldTbls = new System.Collections.Generic.List(); while (r.Read()) oldTbls.Add(r.IsDBNull(0) ? "" : r.GetString(0)); foreach (var ot in oldTbls) { if (string.IsNullOrWhiteSpace(ot)) continue; using (var dcmd = new System.Data.SQLite.SQLiteCommand( "DROP TABLE IF EXISTS \"" + ot + "\";", conn, tx)) dcmd.ExecuteNonQuery(); } } } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 类型='元件' AND 图号=@th AND 箱号=@xh;", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@xh", nx); cmd.ExecuteNonQuery(); } xhSeq++; using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO \"" + xhTbl + "\" VALUES (@s, @xh, @xm, @model, @spec, @unit, @count," + " @dtcb, @cbj, @dtbc, @bcj, @lr, @lrl, @mgl, @type, @remark, @drawing, @iid);", conn, tx)) { // ★ 副本=源:箱号行聚合值直接抄源行(单台成本/成本价/单台报出/报出价/利润/利润率/毛利率) cmd.Parameters.AddWithValue("@s", xhSeq.ToString()); cmd.Parameters.AddWithValue("@xh", nx); cmd.Parameters.AddWithValue("@xm", GetSrcField(xhRow, "箱名", "")); cmd.Parameters.AddWithValue("@model", GetSrcField(xhRow, "型号", "")); cmd.Parameters.AddWithValue("@spec", GetSrcField(xhRow, "规格", "")); cmd.Parameters.AddWithValue("@unit", GetSrcField(xhRow, "单位", "台")); cmd.Parameters.AddWithValue("@count", GetSrcField(xhRow, "数量", "1")); cmd.Parameters.AddWithValue("@dtcb", GetSrcFieldDecimal(xhRow, "单台成本", 0)); cmd.Parameters.AddWithValue("@cbj", GetSrcFieldDecimal(xhRow, "成本价", 0)); cmd.Parameters.AddWithValue("@dtbc", GetSrcFieldDecimal(xhRow, "单台报出", 0)); cmd.Parameters.AddWithValue("@bcj", GetSrcFieldDecimal(xhRow, "报出价", 0)); cmd.Parameters.AddWithValue("@lr", GetSrcFieldDecimal(xhRow, "利润", 0)); cmd.Parameters.AddWithValue("@lrl", GetSrcFieldDecimal(xhRow, "利润率", 0)); cmd.Parameters.AddWithValue("@mgl", GetSrcFieldDecimal(xhRow, "毛利率", 0)); cmd.Parameters.AddWithValue("@type", GetSrcField(xhRow, "箱柜类型", "")); cmd.Parameters.AddWithValue("@remark", GetSrcField(xhRow, "备注", "")); cmd.Parameters.AddWithValue("@drawing", GetSrcField(xhRow, "图纸", "")); cmd.Parameters.AddWithValue("@iid", nx); cmd.ExecuteNonQuery(); } var yjRows = (i < clip.YuanjianRowsPerXianghao.Count) ? clip.YuanjianRowsPerXianghao[i] : null; if (yjRows != null && yjRows.Count > 0) { yjMax++; string yjTbl = "yj_" + yjMax; using (var cmd = new System.Data.SQLite.SQLiteCommand( "CREATE TABLE \"" + yjTbl + "\" (" + " 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," + " 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," + " 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT);", conn, tx)) cmd.ExecuteNonQuery(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@xh", nx); cmd.Parameters.AddWithValue("@real", yjTbl); cmd.ExecuteNonQuery(); } int ys = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO \"" + yjTbl + "\" VALUES (@s, @name, @spec, @unit, @count, @biaojia," + " @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," + " @drawingSpec, @brand, @series, @type, @remark);", conn, tx)) { // ★ 预添加参数(此前漏加,循环内按名访问直接抛索引越界) foreach (string pn in new[] { "@s", "@name", "@spec", "@unit", "@count", "@biaojia", "@caigouDanjia", "@caigouXishu", "@caigouJia", "@baochuDanjia", "@baochuXishu", "@baochuJia", "@drawingSpec", "@brand", "@series", "@type", "@remark" }) cmd.Parameters.AddWithValue(pn, ""); foreach (var yjRow in yjRows) { ys++; cmd.Parameters["@s"].Value = ys.ToString(); cmd.Parameters["@name"].Value = GetSrcField(yjRow, "元件名称", ""); cmd.Parameters["@spec"].Value = GetSrcField(yjRow, "规格", ""); cmd.Parameters["@unit"].Value = GetSrcField(yjRow, "单位", "只"); cmd.Parameters["@count"].Value = GetSrcField(yjRow, "数量", "1"); cmd.Parameters["@biaojia"].Value = GetSrcFieldDecimal(yjRow, "表价", 0); cmd.Parameters["@caigouDanjia"].Value = GetSrcFieldDecimal(yjRow, "采购单价", 0); cmd.Parameters["@caigouXishu"].Value = GetSrcField(yjRow, "采购系数", "1"); cmd.Parameters["@caigouJia"].Value = GetSrcFieldDecimal(yjRow, "采购价", 0); cmd.Parameters["@baochuDanjia"].Value = GetSrcFieldDecimal(yjRow, "报出单价", 0); cmd.Parameters["@baochuXishu"].Value = GetSrcField(yjRow, "报出系数", "1"); cmd.Parameters["@baochuJia"].Value = GetSrcFieldDecimal(yjRow, "报出价", 0); cmd.Parameters["@drawingSpec"].Value = GetSrcField(yjRow, "图纸规格", ""); cmd.Parameters["@brand"].Value = GetSrcField(yjRow, "品牌", ""); cmd.Parameters["@series"].Value = GetSrcField(yjRow, "系列", ""); cmd.Parameters["@type"].Value = GetSrcField(yjRow, "类型", "元件"); cmd.Parameters["@remark"].Value = GetSrcField(yjRow, "备注", ""); cmd.ExecuteNonQuery(); } } } } pasted++; lastTuhaoId = nt; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM " + TuhaoTableName + ";", conn, tx)) Log("[粘贴图号] 图号[" + nt + "] 子树完成: 事务内图号数=" + cmd.ExecuteScalar() + " 本图号箱号=" + clip.XianghaoRows.Count); } // ★ 恢复触发器(编辑自动计算) using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM " + TuhaoTableName + ";", conn, tx)) Log("[粘贴图号] Commit前(事务内): 图号数=" + cmd.ExecuteScalar()); tx.Commit(); } // ★ 触发器已在 INSERT 时自动算好派生列与箱号/图号聚合;只补跨库项目汇总 计算引擎.刷新项目汇总(dbPath, pid, TuhaoTableName); // ★ 自验:提交后立即重连查询(独立连接,验证持久化) using (var vconn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { vconn.Open(); using (var vcmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM " + TuhaoTableName + ";", vconn)) Log("[粘贴图号] 提交后自验: 库中图号数=" + vcmd.ExecuteScalar()); } } } catch (Exception ex) { 错 = ex; } BeginInvoke((MethodInvoker)delegate { Cursor = Cursors.Default; if (错 != null) { Log("[粘贴图号] 后台异常,完整堆栈: " + 错.ToString()); MessageBox.Show(this, "粘贴图号失败:\n" + 错.Message, "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } try { _isReloading = true; this.advancedDataGridView9.SuspendLayout(); _xianghaoCache.Clear(); _yuanjianCache.Clear(); ReloadTuhaoByProject(pid); MoveLastRowsBelowSelected(this.advancedDataGridView9, _tuhaoTable, selectedIndex, pasted, TuhaoTableName, pid); RecalcAllAfterReload(); this.advancedDataGridView9.ResumeLayout(); _currentTuhao = lastTuhaoId; Log("[粘贴图号] 完成,共 " + pasted + " 个图号,耗时 " + sw.ElapsedMilliseconds + "ms,最后=[" + lastTuhaoId + "]"); SetStatus("粘贴完成 " + pasted + " 个图号, 耗时 " + sw.ElapsedMilliseconds + "ms"); } finally { _isReloading = false; SelectRowByValue(this.advancedDataGridView9, "内部ID", _currentTuhao); ReloadXianghaoByTuhao(pid, _currentTuhao); } }); }); 粘贴线程.IsBackground = true; 粘贴线程.Start(); } /// 集合内唯一化名称(内存判定,替代逐次查库)。 private static string 唯一化名称(string baseName, System.Collections.Generic.HashSet used) { // ★ 原名优先:粘贴的箱号在新图号的箱号表里,原名不冲突就直接用(不加"(副本)"字样); // 只有真的重名时才依次加"(副本)"/"(副本1)"后缀区分。 string b = CleanItemName(string.IsNullOrEmpty(baseName) ? "未命名" : baseName); if (!used.Contains(b)) { used.Add(b); return b; } string c = b + "(副本)"; if (!used.Contains(c)) { used.Add(c); return c; } int i = 1; while (true) { c = b + "(副本" + i.ToString() + ")"; if (!used.Contains(c)) { used.Add(c); return c; } i++; } } /// 事务内判断图号内部ID是否已存在。 private static bool NameExistsInConn(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string tableName, string col, string value) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM " + tableName + " WHERE " + col + "=@v;", conn, tx)) { cmd.Parameters.AddWithValue("@v", value); return Convert.ToInt32(cmd.ExecuteScalar()) > 0; } } /// 事务内判断箱号内部ID是否已存在(同图号)。 private static bool NameExistsXhInConn(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string tuhaoId, string value) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM 表索引 WHERE 类型='箱号' AND 图号=@th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", tuhaoId); return Convert.ToInt32(cmd.ExecuteScalar()) > 0; } } /// 事务内解析下一个可用动态表名(prefix_N)。 private static string GetNextTableIdTx(string projectId, string prefix, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { if (string.IsNullOrWhiteSpace(projectId)) return prefix + "_1"; int maxN = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 实际表名 LIKE @p;", conn, tx)) { cmd.Parameters.AddWithValue("@p", prefix + "_%"); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { string t = reader.IsDBNull(0) ? "" : reader.GetString(0); string numPart = t.StartsWith(prefix + "_") ? t.Substring(prefix.Length + 1) : ""; int n; if (int.TryParse(numPart, out n) && n > maxN) maxN = n; } } } return prefix + "_" + (maxN + 1).ToString(); } /// 事务内按真实ID表名建一张空箱号表(含内部ID)。 private static void CreateXianghaoTableTx(string projectId, string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "CREATE TABLE \"" + realTableName + "\" (" + " 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," + " 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," + " 内部ID TEXT);", conn, tx)) cmd.ExecuteNonQuery(); } /// 事务内登记表索引。 private static void AddTableIndexTx(string projectId, string type, string tuhao, string xianghao, string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES (@t, @th, @xh, @real);", conn, tx)) { cmd.Parameters.AddWithValue("@t", type); cmd.Parameters.AddWithValue("@th", tuhao ?? ""); cmd.Parameters.AddWithValue("@xh", xianghao ?? ""); cmd.Parameters.AddWithValue("@real", realTableName); cmd.ExecuteNonQuery(); } } /// 事务内取元件表下一序号。 private static int GetYuanjianCountTx(string projectId, string tuhao, string xhId, string tblName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM \"" + tblName + "\";", conn, tx)) { object o = cmd.ExecuteScalar(); int n = (o != null && o != DBNull.Value) ? Convert.ToInt32(o) : 0; return n + 1; } } /// 指定连接+事务内取下一序号(批量粘贴用,避免每条开关连接)。 private static string GetNextSeqNumberTx(string tableName, string keyCol, string projectId, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { int max = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COALESCE(MAX(CAST(" + keyCol + " AS INTEGER)),0) FROM " + tableName + ";", conn, tx)) { object o = cmd.ExecuteScalar(); if (o != null && o != DBNull.Value) max = Convert.ToInt32(o); } return (max + 1).ToString(); } /// 把缓存的图号字段写入数据库(新内部ID,公式列清零)。★ 带事务重载:批量粘贴共用一个事务。 private void PasteTuhaoRowToDb(string projectId, string newTuhaoId, System.Collections.Generic.Dictionary srcRow, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string seq = GetNextSeqNumberTx(TuhaoTableName, "图号", projectId, conn, tx); string sql = "INSERT INTO " + TuhaoTableName + " (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " + "VALUES (@seq, @th, @taishu, 0, 0, 0, 0, 0, 0, @iid);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@th", newTuhaoId); cmd.Parameters.AddWithValue("@taishu", GetSrcField(srcRow, "台数", "1")); cmd.Parameters.AddWithValue("@iid", newTuhaoId); cmd.ExecuteNonQuery(); } } /// 把缓存的图号字段写入数据库(新内部ID,公式列清零)。 private void PasteTuhaoRowToDb(string projectId, string newTuhaoId, System.Collections.Generic.Dictionary srcRow) { EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); string seq = GetNextSeqNumber(TuhaoTableName, "图号", projectId); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); string sql = "INSERT INTO " + TuhaoTableName + " (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " + "VALUES (@seq, @th, @taishu, 0, 0, 0, 0, 0, 0, @iid);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@th", newTuhaoId); // 图号显示名先用内部ID,用户可改 cmd.Parameters.AddWithValue("@taishu", GetSrcField(srcRow, "台数", "1")); cmd.Parameters.AddWithValue("@iid", newTuhaoId); cmd.ExecuteNonQuery(); } } } // ===== 箱柜:复制/粘贴 ===== private void MenuXianghao_Copy_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao)) { MessageBox.Show(this, "请先选择一个图号。", "复制箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 支持多选:收集全部选中箱柜行(按表格显示顺序) var rows = GetAllSelectedRows(this.advancedDataGridView4); if (rows.Count == 0) { MessageBox.Show(this, "请先选择要复制的箱柜。", "复制箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } rows.Sort((a, b) => a.Index.CompareTo(b.Index)); var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" }; // ★ 复制前先把未保存的编辑落库:元件子树从库读,不保存会复制到旧数据 SaveAllQuoteTablesIfDirty(false); _clipXianghaos = new System.Collections.Generic.List(); foreach (var row in rows) { string xhId = GetBoundValueAsString(row, "内部ID"); if (string.IsNullOrWhiteSpace(xhId)) { Log("[复制箱柜] 跳过无内部ID的行(行" + (row.Index + 1) + ")"); continue; } var clip = new ClipXianghao { ProjectId = _currentProjectId, SrcXianghaoName = xhId, XianghaoRow = CopyRowFields(row, exclude), YuanjianRows = new System.Collections.Generic.List>() }; // 读该箱号下所有元件 string yjTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, xhId); if (!string.IsNullOrEmpty(yjTbl)) clip.YuanjianRows = ReadAllRowsFromTable(GetQuoteDbPath(_currentProjectId), yjTbl); _clipXianghaos.Add(clip); } if (_clipXianghaos.Count == 0) { MessageBox.Show(this, "选中的行都没有箱号内部ID,无法复制。", "复制箱柜", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } int 元件总数 = 0; foreach (var c in _clipXianghaos) 元件总数 += c.YuanjianRows.Count; Log("[复制箱柜] 已缓存箱柜 " + _clipXianghaos.Count + " 个,含元件 " + 元件总数 + " 个"); } private void MenuXianghao_Paste_Click(object sender, EventArgs e) { if (_clipXianghaos == null || _clipXianghaos.Count == 0) { MessageBox.Show(this, "剪贴板里没有箱柜,请先复制。", "粘贴箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao)) { MessageBox.Show(this, "请先选择一个图号再粘贴箱柜。", "粘贴箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView4); string rememberXh = _currentXianghao; var clips = _clipXianghaos; string pid = _currentProjectId, th = _currentTuhao; var sw = System.Diagnostics.Stopwatch.StartNew(); Cursor = Cursors.WaitCursor; Log("[粘贴箱柜] 开始,共 " + clips.Count + " 个箱柜..."); // ★ 粘贴前先把未保存的编辑落库:粘贴回调会从库 Reload, // 不保存的话用户修改只存在于内存表,Reload 后被旧数据覆盖丢失 SaveAllQuoteTablesIfDirty(false); // ★ 整条管线放后台线程,单连接+单事务;完成后回 UI 刷新 System.Threading.Tasks.Task.Run(() => { int pastedCount = 0; string lastXhId = ""; try { string dbPath = GetQuoteDbPath(pid); EnsureQuoteDbExists(pid); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { foreach (var clip in clips) { string baseName = !string.IsNullOrWhiteSpace(clip.SrcXianghaoName) ? clip.SrcXianghaoName : "箱号"; string newXhId = MakeUniqueName(baseName, candidate => NameExistsXhInConn2(conn, tx, th, candidate)); PasteXianghaoRowToDb(pid, th, newXhId, clip.XianghaoRow, conn, tx); PasteYuanjianRowsToDb(pid, th, newXhId, clip.YuanjianRows, conn, tx); pastedCount++; lastXhId = newXhId; } // 箱号行聚合抄源行已正确;刷一次图号行聚合 计算引擎.刷新图号行聚合(conn, tx, dbPath, th, TuhaoTableName); tx.Commit(); } // 只补跨库项目汇总 计算引擎.刷新项目汇总(dbPath, pid, TuhaoTableName); } sw.Stop(); BeginInvoke((MethodInvoker)delegate { try { _isReloading = true; this.advancedDataGridView4.SuspendLayout(); if (!string.IsNullOrEmpty(th) && _xianghaoCache.ContainsKey(th)) _xianghaoCache.Remove(th); ReloadXianghaoByTuhao(pid, th); MoveLastRowsBelowSelected(this.advancedDataGridView4, _xianghaoTable, selectedIndex, pastedCount, "\"" + _xianghaoTable.TableName + "\"", pid); RecalcAllAfterReload(); _currentXianghao = lastXhId; this.advancedDataGridView4.ResumeLayout(); Log("[粘贴箱柜] 完成,共 " + pastedCount + " 个箱柜,耗时 " + sw.ElapsedMilliseconds + "ms,最后=[" + lastXhId + "]"); SetStatus("粘贴完成 " + pastedCount + " 个箱柜, 耗时 " + sw.ElapsedMilliseconds + "ms"); } finally { Cursor = Cursors.Default; // ★ 恢复光标(成功/异常路径都恢复,防沙漏卡死) _isReloading = false; if (!string.IsNullOrEmpty(lastXhId)) SelectRowByValue(this.advancedDataGridView4, "内部ID", lastXhId); ReloadYuanjianByXianghao(pid, th, _currentXianghao); } }); } catch (Exception ex) { Log("[粘贴箱柜] 后台异常: " + ex.Message); BeginInvoke((MethodInvoker)delegate { Cursor = Cursors.Default; MessageBox.Show(this, "粘贴箱柜失败:\n" + ex.Message, "粘贴箱柜", MessageBoxButtons.OK, MessageBoxIcon.Error); }); } }); } /// 事务内判断箱号内部ID是否已存在(同图号)。 private static bool NameExistsXhInConn2(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string tuhaoId, string value) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM 表索引 WHERE 类型='箱号' AND 图号=@th AND 箱号=@v;", conn, tx)) { cmd.Parameters.AddWithValue("@th", tuhaoId); cmd.Parameters.AddWithValue("@v", value); return Convert.ToInt32(cmd.ExecuteScalar()) > 0; } } /// 把缓存的箱号字段写入数据库(新内部ID,公式列清零)。 /// ★ 带事务重载:批量粘贴共用一个事务。 private void PasteXianghaoRowToDb(string projectId, string tuhao, string newXhId, System.Collections.Generic.Dictionary srcRow, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { // ★ 复用事务内连接查表索引(自开新连接会撞外层写锁,是粘贴卡顿的根因) string tblName = LookupTableByIndex("箱号", tuhao, "", conn); if (string.IsNullOrEmpty(tblName)) { tblName = GetNextTableIdTx(projectId, "xh", conn, tx); CreateXianghaoTableTx(projectId, tblName, conn, tx); AddTableIndexTx(projectId, "箱号", tuhao, "", tblName, conn, tx); } int seq; using (var cnt = new System.Data.SQLite.SQLiteCommand( "SELECT COALESCE(MAX(CAST(序号 AS INTEGER)),0)+1 FROM \"" + tblName + "\";", conn, tx)) seq = Convert.ToInt32(cnt.ExecuteScalar()); // 老库结构兼容:目标箱号表可能没有"箱名"列,写入前幂等补列 EnsureColumns(conn, "'" + tblName + "'", new[] { "箱名" }); string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 箱号, 箱名, 型号, 规格, 单位, 数量," + " 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 箱柜类型, 备注, 图纸, 内部ID) " + "VALUES (@seq, @xh, @xm, @model, @spec, @unit, @count," + " @dtcb, @cbj, @dtbc, @bcj, @lr, @lrl, @mgl, @type, @remark, @drawing, @iid);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { // ★ 副本=源:聚合值直接抄源行(批量模式下触发器被跳过,值必须随行写对) cmd.Parameters.AddWithValue("@seq", seq.ToString()); cmd.Parameters.AddWithValue("@xh", newXhId); cmd.Parameters.AddWithValue("@xm", GetSrcField(srcRow, "箱名", "")); cmd.Parameters.AddWithValue("@model", GetSrcField(srcRow, "型号", "")); cmd.Parameters.AddWithValue("@spec", GetSrcField(srcRow, "规格", "")); cmd.Parameters.AddWithValue("@unit", GetSrcField(srcRow, "单位", "台")); cmd.Parameters.AddWithValue("@count", GetSrcField(srcRow, "数量", "1")); cmd.Parameters.AddWithValue("@dtcb", GetSrcFieldDecimal(srcRow, "单台成本", 0)); cmd.Parameters.AddWithValue("@cbj", GetSrcFieldDecimal(srcRow, "成本价", 0)); cmd.Parameters.AddWithValue("@dtbc", GetSrcFieldDecimal(srcRow, "单台报出", 0)); cmd.Parameters.AddWithValue("@bcj", GetSrcFieldDecimal(srcRow, "报出价", 0)); cmd.Parameters.AddWithValue("@lr", GetSrcFieldDecimal(srcRow, "利润", 0)); cmd.Parameters.AddWithValue("@lrl", GetSrcFieldDecimal(srcRow, "利润率", 0)); cmd.Parameters.AddWithValue("@mgl", GetSrcFieldDecimal(srcRow, "毛利率", 0)); cmd.Parameters.AddWithValue("@type", GetSrcField(srcRow, "箱柜类型", "")); cmd.Parameters.AddWithValue("@remark", GetSrcField(srcRow, "备注", "")); cmd.Parameters.AddWithValue("@drawing", GetSrcField(srcRow, "图纸", "")); cmd.Parameters.AddWithValue("@iid", newXhId); cmd.ExecuteNonQuery(); } } private void PasteXianghaoRowToDb(string projectId, string tuhao, string newXhId, System.Collections.Generic.Dictionary srcRow) { EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); string tblName = GetXianghaoTableByIndex(projectId, tuhao); if (string.IsNullOrEmpty(tblName)) { tblName = GetNextTableId(projectId, "xh"); CreateXianghaoTable(projectId, tblName); AddTableIndex(projectId, "箱号", tuhao, "", tblName); } string seq = GetXianghaoCount(projectId, tuhao); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); // 老库结构兼容:目标箱号表可能没有"箱名"列,写入前幂等补列 EnsureColumns(conn, "\"" + tblName + "\"", new[] { "箱名" }); string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 箱号, 箱名, 型号, 规格, 单位, 数量," + " 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 箱柜类型, 备注, 图纸, 内部ID) " + "VALUES (@seq, @xh, @xm, @model, @spec, @unit, @count," + " 0, 0, 0, 0, 0, 0, 0, @type, @remark, @drawing, @iid);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@xh", newXhId); // 箱号显示名先用内部ID cmd.Parameters.AddWithValue("@xm", GetSrcField(srcRow, "箱名", "")); cmd.Parameters.AddWithValue("@model", GetSrcField(srcRow, "型号", "")); cmd.Parameters.AddWithValue("@spec", GetSrcField(srcRow, "规格", "")); cmd.Parameters.AddWithValue("@unit", GetSrcField(srcRow, "单位", "台")); cmd.Parameters.AddWithValue("@count", GetSrcField(srcRow, "数量", "1")); cmd.Parameters.AddWithValue("@type", GetSrcField(srcRow, "箱柜类型", "")); cmd.Parameters.AddWithValue("@remark", GetSrcField(srcRow, "备注", "")); cmd.Parameters.AddWithValue("@drawing", GetSrcField(srcRow, "图纸", "")); cmd.Parameters.AddWithValue("@iid", newXhId); cmd.ExecuteNonQuery(); } } } // ===== 元件:复制/粘贴 ===== private void MenuYuanjian_Copy_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao)) { MessageBox.Show(this, "请先选择一个箱柜。", "复制元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 支持多选:收集全部选中行(按表格显示顺序),一次性复制整批元件 var rows = GetAllSelectedRows(this.advancedDataGridView5); if (rows.Count == 0) { MessageBox.Show(this, "请先选择要复制的元件。", "复制元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } rows.Sort((a, b) => a.Index.CompareTo(b.Index)); // 按行号排序,保持表格顺序 var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "序号" }; _clipYuanjianRows = new System.Collections.Generic.List>(); foreach (var row in rows) _clipYuanjianRows.Add(CopyRowFields(row, exclude)); Log("[复制元件] 已缓存元件 " + _clipYuanjianRows.Count + " 个"); } private void MenuYuanjian_Paste_Click(object sender, EventArgs e) { if (_clipYuanjianRows == null || _clipYuanjianRows.Count == 0) { MessageBox.Show(this, "剪贴板里没有元件,请先复制。", "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao)) { MessageBox.Show(this, "请先选择一个箱柜再粘贴元件。", "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5); int 条数 = _clipYuanjianRows.Count; string pid = _currentProjectId, th = _currentTuhao, xh = _currentXianghao; var rowsSnapshot = _clipYuanjianRows; var sw = System.Diagnostics.Stopwatch.StartNew(); try { _isReloading = true; Cursor = Cursors.WaitCursor; Log("[粘贴元件] 开始,共 " + 条数 + " 条..."); // UI 线程:提交当前编辑并保存脏表(快,脏才存) SaveAllQuoteTablesIfDirty(false); // ★ 批量插入放后台线程(纯 SQLite 操作,不碰任何 UI 对象),界面不阻塞 System.Threading.Tasks.Task.Run(() => { try { PasteYuanjianRowsToDb(pid, th, xh, rowsSnapshot); BeginInvoke((MethodInvoker)delegate { try { // 以下必须 UI 线程(操作绑定的 DataTable 和表格) string cacheKey = th + "\n" + xh; if (_yuanjianCache.ContainsKey(cacheKey)) _yuanjianCache.Remove(cacheKey); ReloadYuanjianByXianghao(pid, th, xh); MoveLastRowsBelowSelected(this.advancedDataGridView5, _yuanjianTable, selectedIndex, 条数, "\"" + _yuanjianTable.TableName + "\"", pid); RecalcAllAfterReload(); // ★ 选中刚粘贴的新元件行(在选中行下面,或在末尾) int newYjIdx = selectedIndex >= 0 ? selectedIndex + 1 : this.advancedDataGridView5.Rows.Count - 1; if (newYjIdx < 0) newYjIdx = this.advancedDataGridView5.Rows.Count - 1; if (newYjIdx >= 0 && newYjIdx < this.advancedDataGridView5.Rows.Count) { try { this.advancedDataGridView5.ClearSelection(); this.advancedDataGridView5.Rows[newYjIdx].Selected = true; this.advancedDataGridView5.CurrentCell = this.advancedDataGridView5.Rows[newYjIdx].Cells[0]; this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = newYjIdx; } catch { } } sw.Stop(); Log("[粘贴元件] 完成,共 " + 条数 + " 条,耗时 " + sw.ElapsedMilliseconds + "ms,选中行=" + newYjIdx); SetStatus("粘贴完成 " + 条数 + " 条元件, 耗时 " + sw.ElapsedMilliseconds + "ms"); } catch (Exception exUi) { Log("[粘贴元件] 刷新异常: " + exUi.Message); MessageBox.Show(this, "粘贴后刷新失败(已记录日志):\n" + exUi.Message, "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { Cursor = Cursors.Default; _isReloading = false; } }); } catch (Exception exDb) { Log("[粘贴元件] 批量插入异常: " + exDb.Message); BeginInvoke((MethodInvoker)delegate { Cursor = Cursors.Default; _isReloading = false; Log("[粘贴元件] 批量插入异常: " + exDb.Message); MessageBox.Show(this, "粘贴元件失败:\n" + exDb.Message, "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Error); }); } }); // 保留 _clipYuanjianRows,支持多次粘贴相同内容 } catch (Exception ex) { Cursor = Cursors.Default; _isReloading = false; Log("[粘贴元件] 异常: " + ex.Message); MessageBox.Show(this, "粘贴元件失败:\n" + ex.Message, "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// 把缓存的元件字段写入数据库(公式列清零,等重算)。 /// /// ★ 批量粘贴元件到目标箱号:单连接+单事务,一次性插入整批。 /// 旧的单条循环每条要开关 3~4 次连接、查 5 条 SQL,粘 N 条卡 N 倍; /// 这里一次解析目标表、一次取起始序号、预编译命令循环插、一次提交。 /// /// ★ 带事务重载:图号级联粘贴共用外层事务。 private void PasteYuanjianRowsToDb(string projectId, string tuhao, string xhId, System.Collections.Generic.List> srcRows, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { if (srcRows == null || srcRows.Count == 0) return; // ★ 复用事务内连接查表索引(自开新连接会撞外层写锁) string tblName = LookupTableByIndex("元件", tuhao, xhId, conn); if (string.IsNullOrEmpty(tblName)) { tblName = GetNextTableIdTx(projectId, "yj", conn, tx); using (var cmd = new System.Data.SQLite.SQLiteCommand( "CREATE TABLE \"" + tblName + "\" (" + " 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," + " 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," + " 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT);", conn, tx)) cmd.ExecuteNonQuery(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", conn, tx)) { cmd.Parameters.AddWithValue("@th", tuhao); cmd.Parameters.AddWithValue("@xh", xhId); cmd.Parameters.AddWithValue("@real", tblName); cmd.ExecuteNonQuery(); } } int seq = GetYuanjianCountTx(projectId, tuhao, xhId, tblName, conn, tx); string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 元件名称, 规格, 单位, 数量, 表价," + " 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," + " 图纸规格, 品牌, 系列, 类型, 备注) " + "VALUES (@seq, @name, @spec, @unit, @count, @biaojia," + " @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," + " @drawingSpec, @brand, @series, @type, @remark);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { foreach (string pn in new[] { "@seq", "@name", "@spec", "@unit", "@count", "@biaojia", "@caigouDanjia", "@caigouXishu", "@caigouJia", "@baochuDanjia", "@baochuXishu", "@baochuJia", "@drawingSpec", "@brand", "@series", "@type", "@remark" }) cmd.Parameters.AddWithValue(pn, ""); foreach (var srcRow in srcRows) { cmd.Parameters["@seq"].Value = seq.ToString(); cmd.Parameters["@name"].Value = GetSrcField(srcRow, "元件名称", ""); cmd.Parameters["@spec"].Value = GetSrcField(srcRow, "规格", ""); cmd.Parameters["@unit"].Value = GetSrcField(srcRow, "单位", ""); cmd.Parameters["@count"].Value = GetSrcField(srcRow, "数量", "1"); cmd.Parameters["@biaojia"].Value = GetSrcFieldDecimal(srcRow, "表价", 0); cmd.Parameters["@caigouDanjia"].Value = GetSrcFieldDecimal(srcRow, "采购单价", 0); cmd.Parameters["@caigouXishu"].Value = GetSrcField(srcRow, "采购系数", "1"); cmd.Parameters["@caigouJia"].Value = GetSrcFieldDecimal(srcRow, "采购价", 0); cmd.Parameters["@baochuDanjia"].Value = GetSrcFieldDecimal(srcRow, "报出单价", 0); cmd.Parameters["@baochuXishu"].Value = GetSrcField(srcRow, "报出系数", "1"); cmd.Parameters["@baochuJia"].Value = GetSrcFieldDecimal(srcRow, "报出价", 0); cmd.Parameters["@drawingSpec"].Value = GetSrcField(srcRow, "图纸规格", ""); cmd.Parameters["@brand"].Value = GetSrcField(srcRow, "品牌", ""); cmd.Parameters["@series"].Value = GetSrcField(srcRow, "系列", ""); cmd.Parameters["@type"].Value = GetSrcField(srcRow, "类型", ""); cmd.Parameters["@remark"].Value = GetSrcField(srcRow, "备注", ""); cmd.ExecuteNonQuery(); seq++; } } } private void PasteYuanjianRowsToDb(string projectId, string tuhao, string xhId, System.Collections.Generic.List> srcRows) { if (srcRows == null || srcRows.Count == 0) return; EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); string tblName = GetYuanjianTableByIndex(projectId, tuhao, xhId); if (string.IsNullOrEmpty(tblName)) { tblName = GetNextTableId(projectId, "yj"); CreateYuanjianTable(projectId, tblName); AddTableIndex(projectId, "元件", tuhao, xhId, tblName); } using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { int seq; using (var cnt = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM \"" + tblName + "\";", conn, tx)) seq = Convert.ToInt32(cnt.ExecuteScalar()); string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 元件名称, 规格, 单位, 数量, 表价," + " 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," + " 图纸规格, 品牌, 系列, 类型, 备注) " + "VALUES (@seq, @name, @spec, @unit, @count, @biaojia," + " @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," + " @drawingSpec, @brand, @series, @type, @remark);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { foreach (string pn in new[] { "@seq", "@name", "@spec", "@unit", "@count", "@biaojia", "@caigouDanjia", "@caigouXishu", "@caigouJia", "@baochuDanjia", "@baochuXishu", "@baochuJia", "@drawingSpec", "@brand", "@series", "@type", "@remark" }) cmd.Parameters.AddWithValue(pn, ""); foreach (var srcRow in srcRows) { seq++; cmd.Parameters["@seq"].Value = seq.ToString(); cmd.Parameters["@name"].Value = GetSrcField(srcRow, "元件名称", ""); cmd.Parameters["@spec"].Value = GetSrcField(srcRow, "规格", ""); cmd.Parameters["@unit"].Value = GetSrcField(srcRow, "单位", ""); cmd.Parameters["@count"].Value = GetSrcField(srcRow, "数量", "1"); cmd.Parameters["@biaojia"].Value = GetSrcFieldDecimal(srcRow, "表价", 0); cmd.Parameters["@caigouDanjia"].Value = GetSrcFieldDecimal(srcRow, "采购单价", 0); cmd.Parameters["@caigouXishu"].Value = GetSrcField(srcRow, "采购系数", "1"); cmd.Parameters["@caigouJia"].Value = GetSrcFieldDecimal(srcRow, "采购价", 0); cmd.Parameters["@baochuDanjia"].Value = GetSrcFieldDecimal(srcRow, "报出单价", 0); cmd.Parameters["@baochuXishu"].Value = GetSrcField(srcRow, "报出系数", "1"); cmd.Parameters["@baochuJia"].Value = GetSrcFieldDecimal(srcRow, "报出价", 0); cmd.Parameters["@drawingSpec"].Value = GetSrcField(srcRow, "图纸规格", ""); cmd.Parameters["@brand"].Value = GetSrcField(srcRow, "品牌", ""); cmd.Parameters["@series"].Value = GetSrcField(srcRow, "系列", ""); cmd.Parameters["@type"].Value = GetSrcField(srcRow, "类型", ""); cmd.Parameters["@remark"].Value = GetSrcField(srcRow, "备注", ""); cmd.ExecuteNonQuery(); } } tx.Commit(); } } } private void PasteYuanjianRowToDb(string projectId, string tuhao, string xhId, System.Collections.Generic.Dictionary srcRow) { EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); string tblName = GetYuanjianTableByIndex(projectId, tuhao, xhId); if (string.IsNullOrEmpty(tblName)) { tblName = GetNextTableId(projectId, "yj"); CreateYuanjianTable(projectId, tblName); AddTableIndex(projectId, "元件", tuhao, xhId, tblName); } string seq = GetYuanjianCount(projectId, tuhao, xhId); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 元件名称, 规格, 单位, 数量, 表价," + " 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," + " 图纸规格, 品牌, 系列, 类型, 备注) " + "VALUES (@seq, @name, @spec, @unit, @count, @biaojia," + " @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," + " @drawingSpec, @brand, @series, @type, @remark);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@name", GetSrcField(srcRow, "元件名称", "")); cmd.Parameters.AddWithValue("@spec", GetSrcField(srcRow, "规格", "")); cmd.Parameters.AddWithValue("@unit", GetSrcField(srcRow, "单位", "")); cmd.Parameters.AddWithValue("@count", GetSrcField(srcRow, "数量", "1")); cmd.Parameters.AddWithValue("@biaojia", GetSrcFieldDecimal(srcRow, "表价", 0)); cmd.Parameters.AddWithValue("@caigouDanjia", GetSrcFieldDecimal(srcRow, "采购单价", 0)); cmd.Parameters.AddWithValue("@caigouXishu", GetSrcField(srcRow, "采购系数", "1")); cmd.Parameters.AddWithValue("@caigouJia", GetSrcFieldDecimal(srcRow, "采购价", 0)); cmd.Parameters.AddWithValue("@baochuDanjia", GetSrcFieldDecimal(srcRow, "报出单价", 0)); cmd.Parameters.AddWithValue("@baochuXishu", GetSrcField(srcRow, "报出系数", "1")); cmd.Parameters.AddWithValue("@baochuJia", GetSrcFieldDecimal(srcRow, "报出价", 0)); cmd.Parameters.AddWithValue("@drawingSpec", GetSrcField(srcRow, "图纸规格", "")); cmd.Parameters.AddWithValue("@brand", GetSrcField(srcRow, "品牌", "")); cmd.Parameters.AddWithValue("@series", GetSrcField(srcRow, "系列", "")); cmd.Parameters.AddWithValue("@type", GetSrcField(srcRow, "类型", "")); cmd.Parameters.AddWithValue("@remark", GetSrcField(srcRow, "备注", "")); cmd.ExecuteNonQuery(); } } } /// 从字段字典读字符串(找不到返回默认值)。 private static string GetSrcField(System.Collections.Generic.Dictionary src, string key, string defVal) { if (src == null || string.IsNullOrEmpty(key)) return defVal; foreach (var kv in src) { if (string.Equals(kv.Key, key, System.StringComparison.OrdinalIgnoreCase)) { if (kv.Value == null) return defVal; string s = kv.Value.ToString().Trim(); return string.IsNullOrEmpty(s) ? defVal : s; } } return defVal; } /// 从字段字典读 decimal(找不到/非数字返回默认值)。 private static decimal GetSrcFieldDecimal(System.Collections.Generic.Dictionary src, string key, decimal defVal) { string s = GetSrcField(src, key, ""); decimal d; if (decimal.TryParse(s, out d)) return d; return defVal; } // ============================================================ // 撤销删除(快照式):删除操作前把整库备份到 undo 缓存, // 右键「撤销删除」把库还原到删除前状态。只支持撤销最近一次删除。 // ============================================================ /// 撤销缓存目录(exe 下 undo_cache\),null=当前无可撤销操作。 private string _undoSnapshotDir = null; /// 撤销快照对应的目标库路径(报价库 或 fak.db)。 private string _undoTargetDbPath = null; /// 撤销快照对应的项目编号(用于还原后重载界面)。 private string _undoProjectId = null; /// 撤销快照是否来自方案库(fak.db)。 private bool _undoIsFak = false; /// /// 删除前调用:把目标库整体快照到 undo 缓存(用 SQLite Backup API 避锁)。 /// 同一次会话只保留最近一次快照(新删除覆盖旧快照)。 /// private void PushUndoSnapshot(string targetDbPath, string projectId, bool isFak) { try { if (string.IsNullOrEmpty(targetDbPath) || !System.IO.File.Exists(targetDbPath)) return; string baseDir = AppDomain.CurrentDomain.BaseDirectory; string undoDir = System.IO.Path.Combine(baseDir, "undo_cache"); if (!System.IO.Directory.Exists(undoDir)) System.IO.Directory.CreateDirectory(undoDir); // 清掉旧快照(只保留一份) try { System.IO.Directory.Delete(undoDir, true); } catch { } System.IO.Directory.CreateDirectory(undoDir); string snapPath = System.IO.Path.Combine(undoDir, "snapshot.db"); BackupSQLiteDb(targetDbPath, snapPath); _undoSnapshotDir = undoDir; _undoTargetDbPath = targetDbPath; _undoProjectId = projectId; _undoIsFak = isFak; UpdateUndoMenuEnabled(); Log("[撤销] 已快照: " + targetDbPath); } catch (Exception ex) { // 快照失败不阻断删除,只是无法撤销 _undoSnapshotDir = null; Log("[撤销] 快照失败(本次删除将无法撤销): " + ex.Message); } } /// 右键菜单「撤销删除」:把库还原到最近一次删除前的快照。 private void MenuUndoDelete_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_undoSnapshotDir) || string.IsNullOrEmpty(_undoTargetDbPath)) { MessageBox.Show(this, "当前没有可撤销的删除操作。", "撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string snapPath = System.IO.Path.Combine(_undoSnapshotDir, "snapshot.db"); if (!System.IO.File.Exists(snapPath)) { MessageBox.Show(this, "撤销快照已丢失,无法撤销。", "撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Warning); _undoSnapshotDir = null; UpdateUndoMenuEnabled(); return; } if (MessageBox.Show(this, "将把数据还原到最近一次删除之前的状态,\n当前删除之后做的修改会丢失。\n\n确定撤销吗?", "撤销删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return; try { _isReloading = true; // 先把内存里未保存的编辑丢弃(以快照为准),清缓存 _xianghaoCache.Clear(); _yuanjianCache.Clear(); // 快照 → 目标库 BackupSQLiteDb(snapPath, _undoTargetDbPath); bool wasFak = _undoIsFak; string pid = _undoProjectId; string tuhao = _currentTuhao; string xianghao = _currentXianghao; // ★ 快照库的触发器可能是旧公式版本,还原后按当前公式表重建 if (!wasFak) EnsureFormulaTriggersQuiet(_undoProjectId); if (wasFak) { ReloadFakData(); MessageBox.Show(this, "已撤销:方案库已还原到删除前状态。", "撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { if (!string.IsNullOrEmpty(pid)) { ReloadTuhaoByProject(pid); RecalcAllAfterReload(); // 尽量恢复选中 if (!string.IsNullOrEmpty(tuhao)) SelectRowByValue(this.advancedDataGridView9, "内部ID", tuhao); if (!string.IsNullOrEmpty(xianghao)) SelectRowByValue(this.advancedDataGridView4, "内部ID", xianghao); } MessageBox.Show(this, "已撤销:数据已还原到删除前状态。", "撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Information); } Log("[撤销] 已还原: " + _undoTargetDbPath); // 撤销只允许一次(避免在还原后的状态上再次"撤销"造成混乱) _undoSnapshotDir = null; _undoTargetDbPath = null; UpdateUndoMenuEnabled(); } catch (Exception ex) { MessageBox.Show(this, "撤销失败:\n" + ex.Message, "撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Error); Log("[撤销] 失败: " + ex.Message); } finally { _isReloading = false; } } /// 根据是否存在可撤销快照,刷新所有右键菜单里「撤销删除」项的可用状态。 private void UpdateUndoMenuEnabled() { bool can = (!string.IsNullOrEmpty(_undoSnapshotDir) && System.IO.File.Exists(System.IO.Path.Combine(_undoSnapshotDir, "snapshot.db"))); SetMenuItemEnabled(_menuTuhao, "撤销删除", can); SetMenuItemEnabled(_menuXianghao, "撤销删除", can); SetMenuItemEnabled(_menuYuanjian, "撤销删除", can); SetMenuItemEnabled(_menuFakTuhao, "撤销删除", can); SetMenuItemEnabled(_menuFakXianghao, "撤销删除", can); SetMenuItemEnabled(_menuFakYuanjian, "撤销删除", can); } /// 在右键菜单里按文本找菜单项并设置 Enabled(找不到忽略)。 private static void SetMenuItemEnabled(ContextMenuStrip menu, string text, bool enabled) { if (menu == null) return; foreach (ToolStripItem item in menu.Items) { if (item is ToolStripMenuItem && item.Text == text) { item.Enabled = enabled; break; } } } /// 创建「撤销删除」菜单项:初始不可用,有待撤销快照时才亮起。 private ToolStripMenuItem MakeUndoMenuItem() { var mi = new ToolStripMenuItem("撤销删除", null, MenuUndoDelete_Click); mi.Enabled = false; // 无快照时灰色;PushUndoSnapshot 里 UpdateUndoMenuEnabled 会点亮 return mi; } private void BuildContextMenus() { Log("开始创建右键菜单..."); // ---- 图号菜单(advancedDataGridView9)---- _menuTuhao = new ContextMenuStrip(); _menuTuhao.Items.Add("新建图号", null, MenuTuhao_New_Click); _menuTuhao.Items.Add("删除图号", null, MenuTuhao_Delete_Click); _menuTuhao.Items.Add(MakeUndoMenuItem()); _menuTuhao.Items.Add(new ToolStripSeparator()); _menuTuhao.Items.Add("复制图号", null, MenuTuhao_Copy_Click); _menuTuhao.Items.Add("粘贴图号", null, MenuTuhao_Paste_Click); _menuTuhao.Items.Add(new ToolStripSeparator()); _menuTuhao.Items.Add("加入方案库", null, MenuTuhao_AddToFak_Click); _menuTuhao.Items.Add(new ToolStripSeparator()); _menuTuhao.Items.Add("置顶", null, MenuTuhao_MoveTop_Click); _menuTuhao.Items.Add("向上移动", null, MenuTuhao_MoveUp_Click); _menuTuhao.Items.Add("向下移动", null, MenuTuhao_MoveDown_Click); _menuTuhao.Items.Add("置底", null, MenuTuhao_MoveBottom_Click); _menuTuhao.Items.Add("移动到...", null, MenuTuhao_MoveTo_Click); this.advancedDataGridView9.ContextMenuStrip = _menuTuhao; Log(" 图号菜单已挂到 advancedDataGridView9(含「加入方案库」+ 行移动)"); // ---- 箱号菜单(advancedDataGridView4)---- _menuXianghao = new ContextMenuStrip(); _menuXianghao.Items.Add("新建箱号", null, MenuXianghao_New_Click); _menuXianghao.Items.Add("删除箱号", null, MenuXianghao_Delete_Click); _menuXianghao.Items.Add(MakeUndoMenuItem()); _menuXianghao.Items.Add(new ToolStripSeparator()); _menuXianghao.Items.Add("复制箱柜", null, MenuXianghao_Copy_Click); _menuXianghao.Items.Add("粘贴箱柜", null, MenuXianghao_Paste_Click); _menuXianghao.Items.Add(new ToolStripSeparator()); _menuXianghao.Items.Add("加入方案库", null, MenuXianghao_AddToFak_Click); _menuXianghao.Items.Add(new ToolStripSeparator()); _menuXianghao.Items.Add("置顶", null, MenuXianghao_MoveTop_Click); _menuXianghao.Items.Add("向上移动", null, MenuXianghao_MoveUp_Click); _menuXianghao.Items.Add("向下移动", null, MenuXianghao_MoveDown_Click); _menuXianghao.Items.Add("置底", null, MenuXianghao_MoveBottom_Click); _menuXianghao.Items.Add("移动到...", null, MenuXianghao_MoveTo_Click); this.advancedDataGridView4.ContextMenuStrip = _menuXianghao; Log(" 箱号菜单已挂到 advancedDataGridView4(含行移动)"); // ---- 元件菜单(advancedDataGridView5)---- _menuYuanjian = new ContextMenuStrip(); _menuYuanjian.Items.Add("新建元件", null, MenuYuanjian_New_Click); _menuYuanjian.Items.Add("删除元件", null, MenuYuanjian_Delete_Click); _menuYuanjian.Items.Add(MakeUndoMenuItem()); _menuYuanjian.Items.Add(new ToolStripSeparator()); _menuYuanjian.Items.Add("复制元件", null, MenuYuanjian_Copy_Click); _menuYuanjian.Items.Add("粘贴元件", null, MenuYuanjian_Paste_Click); _menuYuanjian.Items.Add(new ToolStripSeparator()); _menuYuanjian.Items.Add("加入用户库", null, MenuYuanjian_AddToUserLib_Click); _menuYuanjian.Items.Add("加入柜体库", null, MenuYuanjian_AddToGuiTi_Click); _menuYuanjian.Items.Add("加入方案库", null, MenuYuanjian_AddToFak_Click); _menuYuanjian.Items.Add(new ToolStripSeparator()); _menuYuanjian.Items.Add("置顶", null, MenuYuanjian_MoveTop_Click); _menuYuanjian.Items.Add("向上移动", null, MenuYuanjian_MoveUp_Click); _menuYuanjian.Items.Add("向下移动", null, MenuYuanjian_MoveDown_Click); _menuYuanjian.Items.Add("置底", null, MenuYuanjian_MoveBottom_Click); _menuYuanjian.Items.Add("移动到...", null, MenuYuanjian_MoveTo_Click); this.advancedDataGridView5.ContextMenuStrip = _menuYuanjian; Log(" 元件菜单已挂到 advancedDataGridView5(含「加入用户库」「加入柜体库」+ 行移动)"); // ---- 方案窗口 三表右键菜单(操作当前打开的方案文件)---- _menuFakTuhao = new ContextMenuStrip(); _menuFakTuhao.Items.Add("删除图号", null, FakTuhao_Delete_Click); _menuFakTuhao.Items.Add(MakeUndoMenuItem()); _menuFakTuhao.Items.Add(new ToolStripSeparator()); _menuFakTuhao.Items.Add("插入图号到当前项目", null, FakTuhao_Insert_Click); this.advancedDataGridView图号.ContextMenuStrip = _menuFakTuhao; Log(" 方案图号菜单已挂到 advancedDataGridView图号(删除/插入到当前项目)"); _menuFakXianghao = new ContextMenuStrip(); _menuFakXianghao.Items.Add("删除箱号", null, FakXianghao_Delete_Click); _menuFakXianghao.Items.Add(MakeUndoMenuItem()); _menuFakXianghao.Items.Add(new ToolStripSeparator()); _menuFakXianghao.Items.Add("插入箱号到当前项目", null, FakXianghao_Insert_Click); this.advancedDataGridView箱号.ContextMenuStrip = _menuFakXianghao; Log(" 方案箱号菜单已挂到 advancedDataGridView箱号(删除/插入到当前项目)"); _menuFakYuanjian = new ContextMenuStrip(); _menuFakYuanjian.Items.Add("删除元件", null, FakYuanjian_Delete_Click); _menuFakYuanjian.Items.Add(MakeUndoMenuItem()); _menuFakYuanjian.Items.Add(new ToolStripSeparator()); _menuFakYuanjian.Items.Add("插入元件到当前项目", null, FakYuanjian_Insert_Click); this.advancedDataGridView元件.ContextMenuStrip = _menuFakYuanjian; Log(" 方案元件菜单已挂到 advancedDataGridView元件(删除/插入到当前项目)"); // ---- 方案窗口(advancedDataGridView图号/箱号/元件)右键菜单已由 DockWindow8 自治挂接 // (操作对象为当前打开的方案文件,不再是 fak.db)---- // 清理上次运行残留的撤销快照(撤销只在本次会话内有效,重启即失效) try { string undoDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "undo_cache"); if (System.IO.Directory.Exists(undoDir)) System.IO.Directory.Delete(undoDir, true); } catch { } } // ============================================================ // 右键菜单:行移动(置顶/向上/向下/置底/移动到) // 适用于三个报价表:图号(advancedDataGridView9)、箱号(advancedDataGridView4)、元件(advancedDataGridView5) // 原理:在内存 DataTable 中移动 DataRow 位置 → 重排序号列 → 保存到库(DELETE+INSERT) → 重新加载 // ============================================================ // ============================================================ // ★ 行头选择模式(2026-09-30):递归扫描控件树,把所有"有行头 + CellSelect" // 的表格改为 RowHeaderSelect(Excel 风格:点格=选格,点行头=选整行, // Ctrl/Shift+点行头=多选整行)。有选中单元格时先清空(改模式的前提)。 // ============================================================ internal static void ApplyRowHeaderSelectAll(System.Windows.Forms.Control root) { if (root == null) return; var dgv = root as System.Windows.Forms.DataGridView; if (dgv != null) { try { if (dgv.RowHeadersVisible && dgv.SelectionMode == System.Windows.Forms.DataGridViewSelectionMode.CellSelect) { if (dgv.SelectedCells.Count > 0) dgv.ClearSelection(); dgv.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.RowHeaderSelect; } } catch { /* 个别表格改不动(设计器锁/特殊情况)跳过 */ } } foreach (System.Windows.Forms.Control c in root.Controls) ApplyRowHeaderSelectAll(c); } private enum RowMoveAction { Top, Up, Down, Bottom, To } // ============================================================ // ★ 右键多选保持钩子(2026-09-30,抄扒图窗口同款): // 没有它,多选若干行后右键点"移动"菜单的瞬间,ADGV 默认行为会把 // 选中重置为右键所在行 → 多选被清空 → 移动只动一行(用户报障根因)。 // ============================================================ private System.Collections.Generic.List _keepMultiHooks; private void HookKeepMulti(System.Windows.Forms.DataGridView dgv) { if (dgv == null) return; if (_keepMultiHooks == null) _keepMultiHooks = new System.Collections.Generic.List(); var hook = new AdgvMsgHook(dgv, msg => { try { Log(msg); } catch { } }); _keepMultiHooks.Add(hook); // 存字段防 GC try { Log("[右键多选保持] 已挂钩 dgv=" + dgv.Name + " MultiSelect=" + dgv.MultiSelect); } catch { } } /// /// ADGV 右键多选保持钩子(NativeWindow):吞掉"右键点已选中行"的 WM_RBUTTONDOWN, /// 多选集合不被清空;右键菜单由 WM_CONTEXTMENU 正常弹出(与扒图窗口同款语义)。 /// private class AdgvMsgHook : System.Windows.Forms.NativeWindow { private const int WM_RBUTTONDOWN = 0x0204; private readonly System.Windows.Forms.DataGridView _dgv; private readonly System.Action _log; public AdgvMsgHook(System.Windows.Forms.DataGridView dgv, System.Action log) { _dgv = dgv; _log = log; dgv.HandleCreated += (s, e) => AssignHandle(dgv.Handle); dgv.HandleDestroyed += (s, e) => ReleaseHandle(); if (dgv.IsHandleCreated) AssignHandle(dgv.Handle); } protected override void WndProc(ref System.Windows.Forms.Message m) { if (m.Msg == WM_RBUTTONDOWN && _dgv != null && _dgv.MultiSelect) { try { int lp = m.LParam.ToInt32(); var pos = new System.Drawing.Point(lp & 0xFFFF, (lp >> 16) & 0xFFFF); var hit = _dgv.HitTest(pos.X, pos.Y); if (hit.RowIndex >= 0 && _dgv.SelectedCells != null) { // ★ 关键修复(2026-09-30):CellSelect 模式下 SelectedRows 恒空, // 用 SelectedCells 判断"右键点在已选区域内"(两种选择模式通吃) foreach (System.Windows.Forms.DataGridViewCell c in _dgv.SelectedCells) { if (c != null && c.RowIndex == hit.RowIndex) { if (_log != null) try { _log("[右键多选保持] 吞掉 dgv=" + _dgv.Name + " row=" + hit.RowIndex + " 已选格数=" + _dgv.SelectedCells.Count); } catch { } return; // ★ 吞掉:不清多选 } } if (_log != null) try { _log("[右键多选保持] 未拦截(点在未选中区域) dgv=" + _dgv.Name + " hit=" + hit.RowIndex + " 已选格数=" + _dgv.SelectedCells.Count); } catch { } } } catch { /* 异常时退回默认行为 */ } } base.WndProc(ref m); } } /// /// 通用行移动(★ 2026-09-30 多选适配):把 dgv 中选中的【连续或离散】多行整块移动到 /// 指定位置(保持相对顺序),重排序号,保存,恢复多选。单选行同样走块逻辑(块=1行)。 /// dgv 决定操作哪个表(图号/箱号/元件)。 /// private void MoveSelectedRowInGrid(DataGridView dgv, RowMoveAction action) { if (dgv == null || string.IsNullOrWhiteSpace(_currentProjectId)) return; // 确定操作的 DataTable 和重载方法 DataTable table = null; if (dgv == this.advancedDataGridView5) { table = _yuanjianTable; } else if (dgv == this.advancedDataGridView4) { table = _xianghaoTable; } else if (dgv == this.advancedDataGridView9) { table = _tuhaoTable; } if (table == null || table.Rows.Count == 0) { MessageBox.Show(this, "没有数据可移动。", "移动行", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 多选:取全部选中行索引(升序,排除 NewRow) // ★ 关键修复(2026-09-30):表格是默认 CellSelect 模式,SelectedRows 永远为空 // (诊断日志"已选行数=0"实锤)——改用 SelectedCells 的行索引去重, // CellSelect/FullRowSelect 两种模式都工作 var selSet = new System.Collections.Generic.SortedSet(); foreach (DataGridViewCell c in dgv.SelectedCells) { if (c == null || c.RowIndex < 0 || c.RowIndex >= table.Rows.Count) continue; selSet.Add(c.RowIndex); } var selIndices = new System.Collections.Generic.List(selSet); if (selIndices.Count == 0) { // 兜底 CurrentRow(某些选中态下 SelectedCells 不含当前行) var cur = dgv.CurrentRow; if (cur != null && !cur.IsNewRow && cur.Index >= 0 && cur.Index < table.Rows.Count) selIndices.Add(cur.Index); } if (selIndices.Count == 0) { MessageBox.Show(this, "请先选择要移动的行。", "移动行", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } selIndices.Sort(); int count = selIndices.Count; // ★ 费用行置底保护(2026-10-01):元件表的费用行固定在底部,不参与移动 if (table.Columns.Contains("类型")) { foreach (int idx in selIndices) { if (Convert.ToString(table.Rows[idx]["类型"]).Trim() == "费用") { MessageBox.Show(this, "费用行固定显示在最底部,不能移动。\n(如需调整费用请用功能区「模板费用」)", "移动行", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } } } // 数据行数(排除末尾 NewRow) int dataCount = dgv.Rows.Count; if (dgv.AllowUserToAddRows) dataCount--; if (dataCount <= 0) return; int blockStart = selIndices[0]; int blockEnd = selIndices[count - 1]; // ★ 费用行置底保护:普通行块可移动的边界=费用行区首行(不得移进费用区) int 可移边界 = 费用区首行(table); // 无类型列时=Rows.Count(不限制) // ★ 尽早设 guard,防止"移动到"对话框期间或行移动期间触发 SelectionChanged 联动重载, // 导致 table 引用的 DataTable 和 _yuanjianTable 不一致(之前的 bug 根因) string rememberTuhao = _currentTuhao; string rememberXianghao = _currentXianghao; int newBlockStart = -1; // 提到 try 外,finally 中要用(<0 表示未移动) _isReloading = true; try { // 提交当前编辑到 DataTable dgv.EndEdit(); // 计算目标块首位置(整块移动,保持相对顺序) switch (action) { case RowMoveAction.Top: newBlockStart = 0; break; case RowMoveAction.Up: newBlockStart = blockStart > 0 ? blockStart - 1 : blockStart; break; case RowMoveAction.Down: // ★ 费用行置底保护:下移一步若块尾进入费用行区则不动 if (blockEnd + 1 >= 可移边界) newBlockStart = blockStart; else newBlockStart = blockEnd < dataCount - 1 ? blockStart + 1 : blockStart; break; case RowMoveAction.Bottom: // ★ 费用行置底保护:"置底"=移到费用行区之前(费用行永远在最后) newBlockStart = Math.Max(0, Math.Min(dataCount - count, 可移边界 - count)); if (blockEnd == 可移边界 - 1) newBlockStart = blockStart; // 本就在费用行前,不动 break; case RowMoveAction.To: string input = ShowInputDialog("移动到", "请输入目标行号(1-" + (dataCount - count + 1) + ")\n(选中 " + count + " 行整块移动,此为块首行号):", (blockStart + 1).ToString()); if (string.IsNullOrEmpty(input)) { newBlockStart = -1; return; } int target; if (!int.TryParse(input.Trim(), out target)) { MessageBox.Show(this, "请输入有效数字。", "移动到", MessageBoxButtons.OK, MessageBoxIcon.Warning); newBlockStart = -1; return; } newBlockStart = Math.Max(0, Math.Min(dataCount - count, target - 1)); // ★ 费用行置底保护:目标块尾不得超过费用行区首行 newBlockStart = Math.Max(0, Math.Min(newBlockStart, 可移边界 - count)); break; } if (newBlockStart == blockStart) { newBlockStart = -1; return; } // 位置没变 Log("[移动行] dgv=" + dgv.Name + " 多选" + count + "行 块[" + blockStart + ".." + blockEnd + "] → 块首" + newBlockStart + " table.Rows.Count=" + table.Rows.Count + " table.TableName=" + table.TableName); // ★ 在 DataTable 中整块移动行(ItemArray 技术防 Detached 丢值,与单行版一致) var savedRows = new System.Collections.Generic.List(count); foreach (int idx in selIndices) savedRows.Add(table.Rows[idx].ItemArray); // 降序 Remove for (int i = count - 1; i >= 0; i--) table.Rows.RemoveAt(selIndices[i]); // 按目标位置依次插回(保持原相对顺序) for (int i = 0; i < savedRows.Count; i++) { DataRow newRow = table.NewRow(); newRow.ItemArray = savedRows[i]; table.Rows.InsertAt(newRow, newBlockStart + i); } Log("[移动行] 块移动完成, table.Rows.Count=" + table.Rows.Count); // 重新编号序号列(1, 2, 3...) if (table.Columns.Contains("序号")) { for (int i = 0; i < table.Rows.Count; i++) { table.Rows[i]["序号"] = (i + 1).ToString(); } } // ★ A3(2026-09-30):移动改【逐行 UPDATE 序号】——不再整表 DELETE+INSERT 重建。 // 重建会重排全部 rowid(定位漂移/闪错的根源);UPDATE 后库 rowid 永远稳定, // 内存 _rowid_ 与库保持一致,后续编辑直写/回填定位天然可靠。 string upTbl = (dgv == this.advancedDataGridView9) ? TuhaoTableName : table.TableName; if (string.IsNullOrWhiteSpace(upTbl)) { throw new Exception("合并视图下不支持移动行,请先切回单选图号/箱号。"); } bool canRowUpdate = table.Columns.Contains("_rowid_") && table.Columns.Contains("序号"); long rid0 = canRowUpdate && table.Rows.Count > 0 ? (table.Rows[0]["_rowid_"] is long ? Convert.ToInt64(table.Rows[0]["_rowid_"]) : 0) : 0; if (canRowUpdate && rid0 > 0) { using (var mConn = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId)))) { mConn.Open(); using (var mTx = mConn.BeginTransaction()) { using (var uCmd = new System.Data.SQLite.SQLiteCommand( "UPDATE \"" + upTbl.Replace("\"", "") + "\" SET 序号=@s WHERE rowid=@r;", mConn, mTx)) { uCmd.Parameters.Add("@s", System.Data.DbType.String); uCmd.Parameters.Add("@r", System.Data.DbType.Int64); uCmd.Prepare(); for (int i = 0; i < table.Rows.Count; i++) { long rid = table.Rows[i]["_rowid_"] is long ? Convert.ToInt64(table.Rows[i]["_rowid_"]) : 0; if (rid <= 0) continue; // 未落库的新行:无 rowid 可定位,跳过(下次 Reload 自然带出) uCmd.Parameters["@s"].Value = (i + 1).ToString(); uCmd.Parameters["@r"].Value = rid; uCmd.ExecuteNonQuery(); } } mTx.Commit(); } } Log("[移动行] 序号UPDATE完成(rowid稳定,未重建表): 表=" + upTbl + " 行数=" + table.Rows.Count); } else { // 兜底:无 _rowid_ 列的旧结构 → 退回全表保存 if (dgv == this.advancedDataGridView9) SaveTableToDatabase(table, _currentProjectId, TuhaoTableName, null, null, true); else SaveTableToDatabase(table, _currentProjectId, "\"" + table.TableName + "\"", null, null, true); Log("[移动行] 全表保存完成(兜底路径), TableName=" + table.TableName); } } catch (Exception ex) { Log("[移动行] 异常: " + ex); MessageBox.Show(this, "移动行失败:\n" + ex.Message, "移动行", MessageBoxButtons.OK, MessageBoxIcon.Error); newBlockStart = -1; // 异常时不恢复选中 } finally { _isReloading = false; // 只在移动成功时恢复选中(块范围选中) if (newBlockStart >= 0) { Log("[移动行] finally: 恢复多选, dgv=" + dgv.Name + " 块首=" + newBlockStart + " 行数=" + count + " rememberTuhao=[" + rememberTuhao + "]"); // ★ 关键修复(2026-09-30):移动保存是全表 DELETE+INSERT 重建,库 rowid 全部重排, // 内存表的 _rowid_ 还是旧值——不重载同步的话,后续编辑的直写 UPDATE 会打到错误的库行。 // 显式重载被移动的表(重读 _rowid_);重载会重置选中,其后再做下面的选中恢复。 try { if (dgv == this.advancedDataGridView9) ReloadTuhaoByProject(_currentProjectId); else if (dgv == this.advancedDataGridView4) ReloadXianghaoByTuhao(_currentProjectId, rememberTuhao); else if (dgv == this.advancedDataGridView5) ReloadYuanjianByXianghao(_currentProjectId, rememberTuhao, rememberXianghao); } catch (Exception exRl) { Log("[移动行] 移动后重载异常: " + exRl.Message); } // ① 图号/箱号表:先用内部ID 选中当前行触发联动重载(保持原逻辑), // 再把整块行全部选中(恢复多选状态) if (dgv == this.advancedDataGridView9) SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao); else if (dgv == this.advancedDataGridView4) SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao); // ② 整块恢复多选(含元件表:元件表无联动,直接块选中) // ★ CellSelect 模式下用单元格选中(Row.Selected 会抛异常) try { dgv.ClearSelection(); for (int i = newBlockStart; i < newBlockStart + count && i < dgv.Rows.Count; i++) { var row = dgv.Rows[i]; foreach (System.Windows.Forms.DataGridViewCell c in row.Cells) { if (c != null && c.Visible) c.Selected = true; } } if (newBlockStart < dgv.Rows.Count) { // 设 CurrentCell 到块首(触发选中态一致;图号/箱号表联动已由上面完成) var firstCell = dgv.Rows[newBlockStart].Cells[0]; if (firstCell != null && firstCell.Visible) dgv.CurrentCell = firstCell; } } catch (Exception exSel) { Log("[移动行] 恢复多选异常: " + exSel.Message); } // ★ 诊断:记录移动后各 DataGridView 的行数和选中行 int dgv9Rows = (this.advancedDataGridView9 != null) ? this.advancedDataGridView9.Rows.Count : -1; int dgv4Rows = (this.advancedDataGridView4 != null) ? this.advancedDataGridView4.Rows.Count : -1; int dgv5Rows = (this.advancedDataGridView5 != null) ? this.advancedDataGridView5.Rows.Count : -1; Log("[移动行] 移动后状态: dgv9行数=" + dgv9Rows + " dgv4行数=" + dgv4Rows + "] dgv5行数=" + dgv5Rows + " _currentTuhao=[" + _currentTuhao + "] _currentXianghao=[" + _currentXianghao + "]"); } } } // ============================================================ // 通用:把刚插入表末尾的新行移动到选中行的下面 // ============================================================ /// /// 获取 DataGridView 当前选中行的索引(无选中或新建行返回-1)。 /// 在 InsertXxx 之前调用,记住选中位置。 /// private static int GetSelectedRowIndex(DataGridView dgv) { if (dgv == null) return -1; if (dgv.CurrentRow == null || dgv.CurrentRow.IsNewRow) return -1; return dgv.CurrentRow.Index; } /// /// 把 DataTable 末尾的行(刚插入的新行)移动到指定索引的后面。 /// 用于"在选中行下面插入":InsertXxx+ReloadXxx 之后调用。 /// 会重新编号序号列并保存到库。 /// /// 目标 DataGridView(用于滚动到新位置) /// 目标 DataTable /// 新行要插在这个索引之后(-1=不移动,保持在末尾) /// 数据库表名(用于保存,如图号表用 TuhaoTableName,箱号/元件表用 "\""+table.TableName+"\"") /// 项目ID /// /// ★ 费用行置底保护(2026-10-01 用户要求):返回表尾"连续费用行区"的起始索引 /// (无费用行=Rows.Count)。普通行的插入/移动目标一律不得落进该区。 private static int 费用区首行(DataTable table) { if (table == null || !table.Columns.Contains("类型")) return table == null ? 0 : table.Rows.Count; int 首 = table.Rows.Count; for (int i = table.Rows.Count - 1; i >= 0; i--) { string tp = Convert.ToString(table.Rows[i]["类型"]) == null ? "" : Convert.ToString(table.Rows[i]["类型"]).Trim(); if (tp == "费用") 首 = i; else break; // 尾部连续费用区到此为止 } return 首; } /// 费用行置底保护:钳制插入目标索引到费用行区之前(表无类型列原样返回)。 private static int 钳制到费用行前(DataTable table, int targetIndex) { if (table == null || !table.Columns.Contains("类型")) return targetIndex; int 首 = 费用区首行(table); return targetIndex > 首 ? 首 : targetIndex; } private void MoveLastRowBelowSelected(DataGridView dgv, DataTable table, int afterIndex, string dbTableName, string projectId) { if (dgv == null || table == null || afterIndex < 0 || table.Rows.Count < 2) return; // ★ 费用行置底修复(2026-10-01):新元件经"库插+重载"后位于【普通区末尾】(费用行之前), // 不能盲取表末行——有费用行时末行是费用行,搬它会破坏置底。这里取普通区末行作为新行。 int lastIndex = 费用区首行(table) - 1; if (lastIndex < 0 || lastIndex >= table.Rows.Count) lastIndex = table.Rows.Count - 1; // 无费用行回退表末 // ★ 费用行置底保护:目标不得落进表尾费用行区(普通元件永远插在费用行之前) int targetIndex = 钳制到费用行前(table, afterIndex + 1); if (targetIndex >= lastIndex) return; // 新行已经在选中行下面(或选中行就是最后一行) // 用 ItemArray 技术移动行(避免 DataRow.Remove 清空列值) object[] savedValues = table.Rows[lastIndex].ItemArray; table.Rows.RemoveAt(lastIndex); DataRow newRow = table.NewRow(); newRow.ItemArray = savedValues; table.Rows.InsertAt(newRow, targetIndex); // 重新编号序号列(1, 2, 3...) if (table.Columns.Contains("序号")) { for (int i = 0; i < table.Rows.Count; i++) { table.Rows[i]["序号"] = (i + 1).ToString(); } } // 保存到库 SaveTableToDatabase(table, projectId, dbTableName, null, null, true); // 滚动到新行位置 try { if (targetIndex < dgv.Rows.Count) dgv.FirstDisplayedScrollingRowIndex = targetIndex; } catch { } Log("[插入定位] 新行已移动到行" + (targetIndex + 1) + "(选中行" + (afterIndex + 1) + "下面)"); } /// 把内存表末尾的 count 行整体移动到指定行下面(批量粘贴定位用),重编号并保存。 private void MoveLastRowsBelowSelected(DataGridView dgv, DataTable table, int afterIndex, int count, string dbTableName, string projectId) { if (dgv == null || table == null || count <= 0 || afterIndex < 0) return; if (count >= table.Rows.Count) return; // 整表都是新行,无需移动 // ★ 费用行置底修复(2026-10-01):Reload 型场景新块在【普通区末尾】(费用行之前), // "末尾 count 行"按费用区计算——无类型列的表(图号/箱号)费用区首=行数,行为与原来一致 int lastStart = 费用区首行(table) - count; if (lastStart < 0) lastStart = table.Rows.Count - count; // ★ 费用行置底保护:批量新行(普通元件)不得插进表尾费用行区 int targetIndex = 钳制到费用行前(table, afterIndex + 1); if (targetIndex >= lastStart) return; // 新行已在选中行下面 // 保存末尾 count 行 → 删除 → 整体插到目标位置 var saved = new System.Collections.Generic.List(count); for (int i = lastStart; i < table.Rows.Count; i++) saved.Add(table.Rows[i].ItemArray); for (int i = table.Rows.Count - 1; i >= lastStart; i--) table.Rows.RemoveAt(i); for (int k = 0; k < saved.Count; k++) { var nr = table.NewRow(); nr.ItemArray = saved[k]; table.Rows.InsertAt(nr, targetIndex + k); } // 重新编号序号列(1, 2, 3...) if (table.Columns.Contains("序号")) { for (int i = 0; i < table.Rows.Count; i++) table.Rows[i]["序号"] = (i + 1).ToString(); } SaveTableToDatabase(table, projectId, dbTableName, null, null, true); // 滚动到新行位置 try { if (targetIndex < dgv.Rows.Count) dgv.FirstDisplayedScrollingRowIndex = targetIndex; } catch { } Log("[插入定位] 新行 " + count + " 条已移动到行" + (targetIndex + 1) + "(选中行" + (afterIndex + 1) + "下面)"); } /// /// 把新行插入到 DataTable 的指定位置(选中行下面),而不是末尾。 /// 用于直接操作内存表的插入(如用户库/柜体库/母排插入)。 /// 会自动重新编号序号列。 /// /// 目标 DataTable /// 要插入的新行(已填好数据) /// 插在这个索引之后(-1=插在末尾) private void InsertRowBelowSelected(DataTable table, DataRow newRow, int afterIndex) { if (table == null || newRow == null) return; if (afterIndex < 0 || afterIndex >= table.Rows.Count) { // 无选中行:插在末尾——费用行置底保护:普通新行插在费用行区之前 table.Rows.InsertAt(newRow, 钳制到费用行前(table, table.Rows.Count)); return; } // ★ 费用行置底保护:目标不得落进表尾费用行区 int targetIndex = 钳制到费用行前(table, afterIndex + 1); table.Rows.InsertAt(newRow, targetIndex); // 重新编号序号列(1, 2, 3...) if (table.Columns.Contains("序号")) { for (int i = 0; i < table.Rows.Count; i++) { table.Rows[i]["序号"] = (i + 1).ToString(); } } } /// /// 把 DataTable 末尾的 N 行(刚插入的多条新行)移动到指定索引的后面。 /// 用于"在选中行下面插入多条":方案库批量插入图号/箱号/元件 + Reload 之后调用。 /// 会重新编号序号列并保存到库。 /// /// 目标 DataGridView(用于滚动到新位置) /// 目标 DataTable /// 新行要插在这个索引之后(-1=不移动,保持在末尾) /// 末尾要移动的新行数量 /// 数据库表名 /// 项目ID private void MoveLastNRowsBelowSelected(DataGridView dgv, DataTable table, int afterIndex, int newRowCount, string dbTableName, string projectId) { if (dgv == null || table == null || afterIndex < 0 || newRowCount <= 0) return; int totalRows = table.Rows.Count; // 表里至少要有 选中行 + newRowCount 行才有意义移动 if (totalRows < newRowCount + 1) return; // ★ 费用行置底修复(2026-10-01):Reload 型新块在【普通区末尾】(费用行之前)—— // 按费用区计算块首;无类型列的表费用区首=行数,与原行为一致 int firstNewRowIndex = 费用区首行(table) - newRowCount; // 新行块起始索引 if (firstNewRowIndex < 0) firstNewRowIndex = totalRows - newRowCount; // ★ 费用行置底保护:新行块不得插进表尾费用行区 int targetIndex = 钳制到费用行前(table, afterIndex + 1); // 新行已经在选中行下面(或选中行就在末尾区域),不需要移动 if (targetIndex >= firstNewRowIndex) return; // 用 ItemArray 技术移动行(避免 DataRow.Remove 清空列值) var savedRows = new System.Collections.Generic.List(); for (int i = firstNewRowIndex; i < totalRows; i++) { savedRows.Add(table.Rows[i].ItemArray); } // 从末尾开始删除 N 行 for (int i = 0; i < newRowCount; i++) { table.Rows.RemoveAt(totalRows - 1 - i); } // 在 targetIndex 位置依次插入 N 行 for (int i = 0; i < savedRows.Count; i++) { DataRow newRow = table.NewRow(); newRow.ItemArray = savedRows[i]; table.Rows.InsertAt(newRow, targetIndex + i); } // 重新编号序号列(1, 2, 3...) if (table.Columns.Contains("序号")) { for (int i = 0; i < table.Rows.Count; i++) { table.Rows[i]["序号"] = (i + 1).ToString(); } } // 保存到库 SaveTableToDatabase(table, projectId, dbTableName, null, null, true); // 滚动到新行位置 try { if (targetIndex < dgv.Rows.Count) dgv.FirstDisplayedScrollingRowIndex = targetIndex; } catch { } Log("[插入定位] " + newRowCount + "行已移动到行" + (targetIndex + 1) + "(选中行" + (afterIndex + 1) + "下面)"); } /// /// 简单输入对话框(不依赖 Microsoft.VisualBasic),返回用户输入或空字符串(取消)。 /// private string ShowInputDialog(string title, string prompt, string defaultValue) { using (var dlg = new Form()) { dlg.Text = title; dlg.StartPosition = FormStartPosition.CenterParent; dlg.FormBorderStyle = FormBorderStyle.FixedDialog; dlg.MinimizeBox = false; dlg.MaximizeBox = false; dlg.Width = 340; dlg.Height = 170; var lbl = new Label(); lbl.Text = prompt; lbl.Left = 12; lbl.Top = 15; lbl.Width = 300; lbl.Height = 30; dlg.Controls.Add(lbl); var txt = new TextBox(); txt.Text = defaultValue; txt.Left = 12; txt.Top = 55; txt.Width = 300; dlg.Controls.Add(txt); var btnOk = new Button(); btnOk.Text = "确定"; btnOk.Left = 210; btnOk.Top = 92; btnOk.Width = 90; btnOk.DialogResult = DialogResult.OK; dlg.Controls.Add(btnOk); var btnCancel = new Button(); btnCancel.Text = "取消"; btnCancel.Left = 108; btnCancel.Top = 92; btnCancel.Width = 90; btnCancel.DialogResult = DialogResult.Cancel; dlg.Controls.Add(btnCancel); dlg.AcceptButton = btnOk; dlg.CancelButton = btnCancel; if (dlg.ShowDialog(this) == DialogResult.OK) return txt.Text; return ""; } } // ---- 图号表 行移动菜单 ---- private void MenuTuhao_MoveTop_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Top); } private void MenuTuhao_MoveUp_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Up); } private void MenuTuhao_MoveDown_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Down); } private void MenuTuhao_MoveBottom_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Bottom); } private void MenuTuhao_MoveTo_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.To); } // ---- 箱号表 行移动菜单 ---- private void MenuXianghao_MoveTop_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Top); } private void MenuXianghao_MoveUp_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Up); } private void MenuXianghao_MoveDown_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Down); } private void MenuXianghao_MoveBottom_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Bottom); } private void MenuXianghao_MoveTo_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.To); } // ---- 元件表 行移动菜单 ---- private void MenuYuanjian_MoveTop_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Top); } private void MenuYuanjian_MoveUp_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Up); } private void MenuYuanjian_MoveDown_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Down); } private void MenuYuanjian_MoveBottom_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Bottom); } private void MenuYuanjian_MoveTo_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.To); } /// /// 右键菜单弹出前:强制结束当前正在编辑的单元格,并立即保存到数据库。 /// 这样新建/删除操作执行时,用户之前的编辑(如刚填的表价)已经持久化, /// 不会因为编辑态未提交而丢失。 /// // 右键菜单弹出前不再做任何操作(不再取消编辑/保存/动选中), // 完全用系统默认的右键行为,选中单元格由用户自己控制。 /// /// 把表格的选中/光标设到指定行指定列(完整三步:清空选中+选中该行+设光标到该单元格)。 /// 三步都执行才能保证 SelectedRows 和 CurrentCell 状态一致,避免横跳。 /// 调用方应已持有 _isReloading guard。 /// private static void SetCurrentCell(DataGridView dgv, int rowIndex, int colIndex = 0) { if (dgv == null || dgv.Rows == null || rowIndex < 0 || rowIndex >= dgv.Rows.Count) return; try { if (dgv.Rows[rowIndex].IsNewRow) return; if (colIndex < 0 || colIndex >= dgv.Columns.Count) colIndex = 0; dgv.ClearSelection(); dgv.Rows[rowIndex].Selected = true; dgv.CurrentCell = dgv.Rows[rowIndex].Cells[colIndex]; } catch { } } /// /// 把表格的选中/光标设到指定行,用列名定位(完整三步)。 /// 调用方应已持有 _isReloading guard。 /// private static void SetCurrentCellByCol(DataGridView dgv, int rowIndex, string colName) { if (dgv == null) { SetCurrentCell(dgv, rowIndex); return; } DataGridViewColumn col = FindColumn(dgv, colName); SetCurrentCell(dgv, rowIndex, col == null ? 0 : col.Index); } /// 按列值选中对应行(防止右键导致选中跳回第一行)。不触发联动。 private void SelectRowByValue(DataGridView dgv, string colName, string value) { if (dgv == null || dgv.Rows == null || string.IsNullOrEmpty(value)) return; try { _isReloading = true; foreach (DataGridViewRow r in dgv.Rows) { if (r == null || r.IsNewRow) continue; // ★ 先从表格列读,读不到再从底层数据源读(支持"内部ID"等隐藏列) string v = GetCellValueAsString(r, colName); if (string.IsNullOrEmpty(v)) v = GetBoundValueAsString(r, colName); if (v == value) { dgv.ClearSelection(); r.Selected = true; dgv.CurrentCell = r.Cells[0]; return; } } } catch { } finally { _isReloading = false; } } // ---------- 图号菜单事件 ---------- private void MenuTuhao_New_Click(object sender, EventArgs e) { Log("点击[新建图号], _currentProjectId=[" + _currentProjectId + "]"); if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在项目表中双击选择一个项目,再新建图号。", "新建图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 不弹输入框,直接用默认名(新图号N)创建 string newTuhao = "新图号" + GetNextSeqNumber(TuhaoTableName, "图号", _currentProjectId); Log(" 直接创建图号=[" + newTuhao + "]"); // 记住当前图号,新建完成后恢复(防止 ADGV 选中抖动) string rememberTuhao = _currentTuhao; // ★ 记住选中行索引,用于"在选中行下面插入" int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView9); try { // 整个新建流程用 _isReloading 包裹,挡住所有 SelectionChanged 联动 _isReloading = true; // 新建前先保存当前三个表的所有编辑,避免用户编辑丢失 // 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。 // 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。 SaveAllQuoteTablesIfDirty(false); InsertTuhao(_currentProjectId, newTuhao); Log(" InsertTuhao 成功"); // ★ 清空箱号/元件缓存(图号表变了,所有下级缓存可能失效) _xianghaoCache.Clear(); _yuanjianCache.Clear(); ReloadTuhaoByProject(_currentProjectId); // ★ 把新行(在表末尾)移动到选中行下面 MoveLastRowBelowSelected(this.advancedDataGridView9, _tuhaoTable, selectedIndex, TuhaoTableName, _currentProjectId); // 新建后重算当前层并刷新显示 RecalcAllAfterReload(); } catch (Exception ex) { Log(" 新建图号异常: " + ex.Message); MessageBox.Show(this, "新建图号失败:\n" + ex.Message, "新建图号", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 恢复图号选中(新建的图号会在表末尾,优先选中新图号) _currentTuhao = newTuhao; SelectRowByValue(this.advancedDataGridView9, "内部ID", newTuhao); _isReloading = false; // ★ 新建图号后,箱号表必须跳到这个新图号(显示它里面的箱号,新建的图号默认空) // ReloadTuhaoByProject 默认选第一个图号,这里手动刷新成新图号。 ReloadXianghaoByTuhao(_currentProjectId, newTuhao); } } private void MenuTuhao_Delete_Click(object sender, EventArgs e) { Log("点击[删除图号], _currentProjectId=[" + _currentProjectId + "]"); // 取所有选中行(支持批量多选) var rows = GetAllSelectedRows(this.advancedDataGridView9); if (rows == null || rows.Count == 0) { MessageBox.Show(this, "请先选择要删除的图号。", "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 收集所有选中行的图号内部ID(去重) ★ 用内部ID,用户改名不影响删除 var tuhaos = new System.Collections.Generic.List(); // ★ 同时记显示名:扒图 Form3 曾按显示名登记 表索引/建表,删除时需要按 // 显示名兜底清理孤儿(2026-09-21 残留根治),否则 AI 清单会读到已删数据 var tuhaoNames = new System.Collections.Generic.List(); foreach (var r in rows) { string th = GetBoundValueAsString(r, "内部ID"); string nm = GetBoundValueAsString(r, "图号"); if (!string.IsNullOrWhiteSpace(th) && !tuhaos.Contains(th)) { tuhaos.Add(th); tuhaoNames.Add(nm ?? ""); } } if (tuhaos.Count == 0) { MessageBox.Show(this, "选中行没有图号,无法删除。", "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 不弹确认框,直接删除 try { // 整个删除流程用 _isReloading 包裹,挡住选中抖动 _isReloading = true; // 删除前先保存当前三个表的所有编辑 // 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。 // 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。 SaveAllQuoteTablesIfDirty(false); // ★ 删除前快照整库,支持右键「撤销删除」还原 PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false); // ★ 删除管线放后台线程:单连接+单事务(所有 DROP/DELETE 一次提交,不再逐条 fsync) string pid = _currentProjectId; var sw = System.Diagnostics.Stopwatch.StartNew(); Cursor = Cursors.WaitCursor; SetStatus("正在删除 " + tuhaos.Count + " 个图号..."); Log("[删除图号] 开始,共 " + tuhaos.Count + " 个图号..."); System.Threading.Tasks.Task.Run(() => { Exception 错 = null; try { string dbPath = GetQuoteDbPath(pid); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { for (int ti = 0; ti < tuhaos.Count; ti++) { string tuhao = tuhaos[ti]; string tuhaoNm = ti < tuhaoNames.Count ? tuhaoNames[ti] : ""; if (tuhao == (_currentTuhao ?? "")) { /* 当前图号也一并删除 */ } DeleteTuhaoTx(pid, tuhao, tuhaoNm, conn, tx); } tx.Commit(); } } } catch (Exception exBg) { 错 = exBg; } BeginInvoke((MethodInvoker)delegate { try { _isReloading = true; if (错 == null) { RenumberTable(pid, TuhaoTableName); _xianghaoCache.Clear(); _yuanjianCache.Clear(); } ReloadTuhaoByProject(pid); RecalcAllAfterReload(); sw.Stop(); Cursor = Cursors.Default; if (错 != null) { Log("[删除图号] 后台异常,完整堆栈: " + 错.ToString()); MessageBox.Show(this, "删除图号失败:\n" + 错.Message, "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } SetStatus("删除完成 " + tuhaos.Count + " 个图号, 耗时 " + sw.ElapsedMilliseconds + "ms"); Log("[删除图号] 完成,数量=" + tuhaos.Count + ",耗时 " + sw.ElapsedMilliseconds + "ms"); } finally { _isReloading = false; } }); }); } catch (Exception ex) { Cursor = Cursors.Default; _isReloading = false; MessageBox.Show(this, "删除图号失败:\n" + ex.Message, "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ============================================================ // 加入方案库:把当前选中图号(含其下所有箱号 + 每个箱号下所有元件) // 从项目报价库复制到方案库 sjk\fak.db // ============================================================ /// 方案库固定表名:图号表(不用索引,直接按图号查找)。 private const string FakTuhaoTable = "advancedDataGridView9Table"; /// /// 右键「加入方案库」:把当前图号(_currentTuhao)对应的全部数据 /// (图号行 + 该图号所有箱号行 + 每个箱号的所有元件行) /// 从项目报价库复制到方案库 fak.db。 /// 方案库数据结构与项目库完全一致:图号表固定 + xh_N/yj_M 动态建表 + 表索引映射。 /// 若 fak.db 里已存在相同图号,先删除其旧的 xh_N/yj_M 表和索引行,再重建,保证可重复加入。 /// private void MenuTuhao_AddToFak_Click(object sender, EventArgs e) { Log("点击[加入方案库], _currentProjectId=[" + _currentProjectId + "] _currentTuhao=[" + _currentTuhao + "]"); // 1) 校验当前选中状态 if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在项目表中双击选择一个项目,再加入方案库。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 多选:收集全部选中图号(内部ID,去重,保持选择顺序) var rows = GetAllSelectedRows(this.advancedDataGridView9); var 图号们 = new System.Collections.Generic.List(); var seenId = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string id = GetBoundValueAsString(r, "内部ID"); if (!string.IsNullOrEmpty(id) && seenId.Add(id)) 图号们.Add(id); } if (图号们.Count == 0) { MessageBox.Show(this, "请先在图号表中选择要加入方案库的图号(可按 Ctrl 多选)。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 2) 先保存当前三个表的未提交编辑,确保复制到方案库的是最新数据 try { SaveAllQuoteTablesIfDirty(false); } catch (Exception exSave) { Log("[加入方案库] 预保存失败(继续): " + exSave.Message); } string srcDbPath = GetQuoteDbPath(_currentProjectId); if (!System.IO.File.Exists(srcDbPath)) { MessageBox.Show(this, "项目报价库不存在:\n" + srcDbPath, "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } string fakDbPath = 方案数据源路径(); // ★ 方案窗口没打开方案时给出明确引导(旧逻辑空路径直接在 CreateDirectory 炸) if (string.IsNullOrWhiteSpace(fakDbPath) || !System.IO.File.Exists(fakDbPath)) { MessageBox.Show(this, "方案窗口当前没有打开方案,无法加入。\n\n请先在右侧方案窗口的项目页双击打开一个方案\n(或用项目表右键\"加入方案库\"把项目存为新方案),再加入。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 改用复制粘贴管线写方案库:收集选中图号的完整子树(clip 结构同复制图号), // 事务写入方案文件。同步执行(方案写入数据量通常不大)。 var clips = new System.Collections.Generic.List(); var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" }; foreach (var id in 图号们) { var clip = new ClipTuhao { ProjectId = _currentProjectId, SrcTuhaoName = id, TuhaoRow = new System.Collections.Generic.List>(), XianghaoRows = new System.Collections.Generic.List>(), YuanjianRowsPerXianghao = new System.Collections.Generic.List>>() }; // 图号行(从内存表取,含最新编辑) DataGridViewRow thRowFound = null; foreach (var r in GetAllSelectedRows(this.advancedDataGridView9)) if (GetBoundValueAsString(r, "内部ID") == id) { thRowFound = r; break; } if (thRowFound != null) foreach (DataGridViewColumn c in thRowFound.DataGridView.Columns) if (!exclude.Contains(c.Name)) clip.TuhaoRow.Add(new System.Collections.Generic.Dictionary { { c.Name, thRowFound.Cells[c.Name].Value ?? "" } }); // 箱号行+元件行(从项目库读,预保存已落最新) string xhTbl = GetXianghaoTableByIndex(_currentProjectId, id); if (!string.IsNullOrEmpty(xhTbl)) { clip.XianghaoRows = ReadAllRowsFromTable(srcDbPath, xhTbl); foreach (var xhRow in clip.XianghaoRows) { string xhId = (xhRow.ContainsKey("内部ID") ? xhRow["内部ID"] : null) as string; if (string.IsNullOrWhiteSpace(xhId)) xhId = (xhRow.ContainsKey("箱号") ? xhRow["箱号"] : null) as string; string yjTbl = GetYuanjianTableByIndex(_currentProjectId, id, xhId); var yjRows = string.IsNullOrEmpty(yjTbl) ? new System.Collections.Generic.List>() : ReadAllRowsFromTable(srcDbPath, yjTbl); clip.YuanjianRowsPerXianghao.Add(yjRows); } } clips.Add(clip); } int 收集箱号数 = 0; foreach (var c in clips) 收集箱号数 += c.XianghaoRows.Count; Log("[加入方案库] 收集完成: 图号=" + clips.Count + " 箱号=" + 收集箱号数 + " 目标=" + fakDbPath); Cursor = Cursors.WaitCursor; try { EnsureFakDbExists(fakDbPath); using (var conn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath))) { conn.Open(); // ★ 方案文件由整库复制而来,可能带旧架构遗留触发器(引用已退役的固定表"箱号")——写入前必清 计算引擎.清理遗留触发器(conn); using (var tx = conn.BeginTransaction()) { int xhMax = 0, yjMax = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 类型, 实际表名 FROM 表索引;", conn, tx)) using (var r = cmd.ExecuteReader()) { while (r.Read()) { string ty = r.IsDBNull(0) ? "" : r.GetString(0); string real = r.IsDBNull(1) ? "" : r.GetString(1); if (ty == "箱号" && real.StartsWith("xh_")) { int n; if (int.TryParse(real.Substring(3), out n) && n > xhMax) xhMax = n; } else if (ty == "元件" && real.StartsWith("yj_")) { int n; if (int.TryParse(real.Substring(3), out n) && n > yjMax) yjMax = n; } } } var 现有图号 = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 图号 FROM " + FakTuhaoTable + ";", conn, tx)) using (var r = cmd.ExecuteReader()) while (r.Read()) if (!r.IsDBNull(0) && r.GetString(0).Length > 0) 现有图号.Add(r.GetString(0)); foreach (var clip in clips) { string baseName = !string.IsNullOrWhiteSpace(clip.SrcTuhaoName) ? clip.SrcTuhaoName : "图号"; string nt = 唯一化名称(baseName, 现有图号); 现有图号.Add(nt); // 清理同名图号的残留索引和动态表(防 LIMIT 1 命中旧残缺表) using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 图号=@th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); using (var r = cmd.ExecuteReader()) { var olds = new System.Collections.Generic.List(); while (r.Read()) { string t = r.IsDBNull(0) ? "" : r.GetString(0); if (!string.IsNullOrWhiteSpace(t)) olds.Add(t); } foreach (var ot in olds) using (var dcmd = new System.Data.SQLite.SQLiteCommand( "DROP TABLE IF EXISTS \"" + ot + "\";", conn, tx)) dcmd.ExecuteNonQuery(); } } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 图号=@th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); cmd.ExecuteNonQuery(); } // 图号行:副本=源(聚合值抄源行),写入方案库图号表 var srcTh = clip.TuhaoRow.Count > 0 ? clip.TuhaoRow[0] : null; using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO " + FakTuhaoTable + " (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " + "VALUES (@s, @th, @ts, @sl, @dj, @zj, @cbzj, @ml, @mll, @iid);", conn, tx)) { int 序 = 现有图号.Count; cmd.Parameters.AddWithValue("@s", 序.ToString()); cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@ts", GetSrcField(srcTh, "台数", "1")); cmd.Parameters.AddWithValue("@sl", GetSrcFieldDecimal(srcTh, "箱柜数量", 0)); cmd.Parameters.AddWithValue("@dj", GetSrcFieldDecimal(srcTh, "单价", 0)); cmd.Parameters.AddWithValue("@zj", GetSrcFieldDecimal(srcTh, "总价", 0)); cmd.Parameters.AddWithValue("@cbzj", GetSrcFieldDecimal(srcTh, "成本总价", 0)); cmd.Parameters.AddWithValue("@ml", GetSrcFieldDecimal(srcTh, "毛利", 0)); cmd.Parameters.AddWithValue("@mll", GetSrcFieldDecimal(srcTh, "毛利率", 0)); cmd.Parameters.AddWithValue("@iid", nt); cmd.ExecuteNonQuery(); } // 箱号表(每图号一张)+全部箱号行;触发器不建(方案库不需要,且遗留触发器是祸根) xhMax++; string xhTbl = "xh_" + xhMax; using (var cmd = new System.Data.SQLite.SQLiteCommand( "CREATE TABLE \"" + xhTbl + "\" (" + " 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," + " 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," + " 内部ID TEXT);", conn, tx)) cmd.ExecuteNonQuery(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('箱号', @th, '', @real);", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@real", xhTbl); cmd.ExecuteNonQuery(); } int xhSeq = 0; var 已用箱号名 = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase); for (int i = 0; i < clip.XianghaoRows.Count; i++) { var xhRow = clip.XianghaoRows[i]; string srcXhId = (xhRow.ContainsKey("内部ID") ? xhRow["内部ID"] : null) as string; if (string.IsNullOrWhiteSpace(srcXhId)) srcXhId = (xhRow.ContainsKey("箱号") ? xhRow["箱号"] : null) as string; string nx = 唯一化名称(!string.IsNullOrWhiteSpace(srcXhId) ? srcXhId : "箱号", 已用箱号名); 已用箱号名.Add(nx); xhSeq++; using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO \"" + xhTbl + "\" VALUES (@s, @xh, @xm, @model, @spec, @unit, @count," + " @dtcb, @cbj, @dtbc, @bcj, @lr, @lrl, @mgl, @type, @remark, @drawing, @iid);", conn, tx)) { cmd.Parameters.AddWithValue("@s", xhSeq.ToString()); cmd.Parameters.AddWithValue("@xh", nx); cmd.Parameters.AddWithValue("@xm", GetSrcField(xhRow, "箱名", "")); cmd.Parameters.AddWithValue("@model", GetSrcField(xhRow, "型号", "")); cmd.Parameters.AddWithValue("@spec", GetSrcField(xhRow, "规格", "")); cmd.Parameters.AddWithValue("@unit", GetSrcField(xhRow, "单位", "台")); cmd.Parameters.AddWithValue("@count", GetSrcField(xhRow, "数量", "1")); cmd.Parameters.AddWithValue("@dtcb", GetSrcFieldDecimal(xhRow, "单台成本", 0)); cmd.Parameters.AddWithValue("@cbj", GetSrcFieldDecimal(xhRow, "成本价", 0)); cmd.Parameters.AddWithValue("@dtbc", GetSrcFieldDecimal(xhRow, "单台报出", 0)); cmd.Parameters.AddWithValue("@bcj", GetSrcFieldDecimal(xhRow, "报出价", 0)); cmd.Parameters.AddWithValue("@lr", GetSrcFieldDecimal(xhRow, "利润", 0)); cmd.Parameters.AddWithValue("@lrl", GetSrcFieldDecimal(xhRow, "利润率", 0)); cmd.Parameters.AddWithValue("@mgl", GetSrcFieldDecimal(xhRow, "毛利率", 0)); cmd.Parameters.AddWithValue("@type", GetSrcField(xhRow, "箱柜类型", "")); cmd.Parameters.AddWithValue("@remark", GetSrcField(xhRow, "备注", "")); cmd.Parameters.AddWithValue("@drawing", GetSrcField(xhRow, "图纸", "")); cmd.Parameters.AddWithValue("@iid", nx); cmd.ExecuteNonQuery(); } var yjRows = (i < clip.YuanjianRowsPerXianghao.Count) ? clip.YuanjianRowsPerXianghao[i] : null; if (yjRows != null && yjRows.Count > 0) { yjMax++; string yjTbl = "yj_" + yjMax; using (var cmd = new System.Data.SQLite.SQLiteCommand( "CREATE TABLE \"" + yjTbl + "\" (" + " 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," + " 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," + " 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT);", conn, tx)) cmd.ExecuteNonQuery(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", conn, tx)) { cmd.Parameters.AddWithValue("@th", nt); cmd.Parameters.AddWithValue("@xh", nx); cmd.Parameters.AddWithValue("@real", yjTbl); cmd.ExecuteNonQuery(); } int ys = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO \"" + yjTbl + "\" VALUES (@s, @name, @spec, @unit, @count, @biaojia," + " @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," + " @drawingSpec, @brand, @series, @type, @remark);", conn, tx)) { foreach (string pn in new[] { "@s", "@name", "@spec", "@unit", "@count", "@biaojia", "@caigouDanjia", "@caigouXishu", "@caigouJia", "@baochuDanjia", "@baochuXishu", "@baochuJia", "@drawingSpec", "@brand", "@series", "@type", "@remark" }) cmd.Parameters.AddWithValue(pn, ""); foreach (var yjRow in yjRows) { ys++; cmd.Parameters["@s"].Value = ys.ToString(); cmd.Parameters["@name"].Value = GetSrcField(yjRow, "元件名称", ""); cmd.Parameters["@spec"].Value = GetSrcField(yjRow, "规格", ""); cmd.Parameters["@unit"].Value = GetSrcField(yjRow, "单位", "只"); cmd.Parameters["@count"].Value = GetSrcField(yjRow, "数量", "1"); cmd.Parameters["@biaojia"].Value = GetSrcFieldDecimal(yjRow, "表价", 0); cmd.Parameters["@caigouDanjia"].Value = GetSrcFieldDecimal(yjRow, "采购单价", 0); cmd.Parameters["@caigouXishu"].Value = GetSrcField(yjRow, "采购系数", "1"); cmd.Parameters["@caigouJia"].Value = GetSrcFieldDecimal(yjRow, "采购价", 0); cmd.Parameters["@baochuDanjia"].Value = GetSrcFieldDecimal(yjRow, "报出单价", 0); cmd.Parameters["@baochuXishu"].Value = GetSrcField(yjRow, "报出系数", "1"); cmd.Parameters["@baochuJia"].Value = GetSrcFieldDecimal(yjRow, "报出价", 0); cmd.Parameters["@drawingSpec"].Value = GetSrcField(yjRow, "图纸规格", ""); cmd.Parameters["@brand"].Value = GetSrcField(yjRow, "品牌", ""); cmd.Parameters["@series"].Value = GetSrcField(yjRow, "系列", ""); cmd.Parameters["@type"].Value = GetSrcField(yjRow, "类型", "元件"); cmd.Parameters["@remark"].Value = GetSrcField(yjRow, "备注", ""); cmd.ExecuteNonQuery(); } } } } } tx.Commit(); } } Log("[加入方案库] 成功,共 " + clips.Count + " 个图号(含箱号 " + 收集箱号数 + " 个)"); ReloadFakData(); MessageBox.Show(this, "已把 " + clips.Count + " 个图号加入方案库。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception exAdd) { Log("[加入方案库] 失败: " + exAdd); MessageBox.Show(this, "加入方案库失败:\n" + exAdd.Message, "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// 项目表右键「加入方案库」:把整个项目(全部图号+箱号+元件+项目信息)复制到方案库。 private void MenuProject_AddToFak_Click(object sender, EventArgs e) { var row = GetFirstSelectedRow(this.advancedDataGridView6); if (row == null) { MessageBox.Show(this, "请先在项目列表中选择要存为方案的项目。", "存为方案", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string projectId = GetBoundValueAsString(row, "项目编号"); string projName = GetBoundValueAsString(row, "项目名称"); if (string.IsNullOrWhiteSpace(projectId)) { MessageBox.Show(this, "选中行没有项目编号,无法存为方案。", "存为方案", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } string srcDbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(srcDbPath)) { MessageBox.Show(this, "项目报价库不存在:\n" + srcDbPath, "存为方案", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 方案名默认 = 项目名称(空则项目编号);重名自动加序号 string 方案名 = !string.IsNullOrWhiteSpace(projName) ? projName : projectId; string dstDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sjk", "方案库"); System.IO.Directory.CreateDirectory(dstDir); string dstPath = System.IO.Path.Combine(dstDir, 方案名 + ".db"); int seq = 1; while (System.IO.File.Exists(dstPath)) { seq++; dstPath = System.IO.Path.Combine(dstDir, 方案名 + "(" + seq + ").db"); } var sw = System.Diagnostics.Stopwatch.StartNew(); Cursor = Cursors.WaitCursor; try { // ★ 一个方案 = 一份独立项目库副本(含全部图号/箱号/元件/项目信息) BackupSQLiteDb(srcDbPath, dstPath); sw.Stop(); Log("[存为方案] 项目=" + projectId + " → " + dstPath + " 耗时 " + sw.ElapsedMilliseconds + "ms"); // ★ 存完立即切换打开新方案(方案窗口报价页直接显示新方案内容) if (this.w8 != null) this.w8.打开方案文件(dstPath); else ReloadFakData(); MessageBox.Show(this, "已把项目 [" + projectId + "] 存为方案。\n方案文件: " + dstPath + "\n耗时 " + sw.ElapsedMilliseconds + "ms。", "存为方案", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception exSave) { MessageBox.Show(this, "存为方案失败:\n" + exSave.Message, "存为方案", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { Cursor = Cursors.Default; } } /// Exec 简写(删除表数据用)。 private static void Exec(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string sql) { using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) cmd.ExecuteNonQuery(); } /// 复制一个图号(含箱号/元件子树)从项目库到方案库。供单图号加入与整项目加入共用。 private void 复制图号到方案库(string projectId, string 图号内部ID, string srcDbPath, string fakDbPath) { try { int xhCount = 0; // 复制的箱号行数 int yjCount = 0; // 复制的元件行数 // ---- 阶段一:从项目报价库读出当前图号的全部数据 ---- // 图号行(只取第一条,一个图号在图号表里通常只有一行) DataTable tuhaoRow = new DataTable(); // 该图号的箱号表真实表名 + 全部箱号行 string xhRealTbl = ""; DataTable xhRows = new DataTable(); // 每个箱号:元件表真实表名 + 该箱号的箱号名称 + 元件行 var yjBuckets = new System.Collections.Generic.List< System.Collections.Generic.KeyValuePair>(); // 真实表名 → 元件行 var xhNamesInOrder = new System.Collections.Generic.List(); // 箱号顺序(用于元件行关联箱号名) using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath))) { srcConn.Open(); // (a) 图号行 // ★ 用"内部ID"列查(不是"图号"显示名列),用户改图号名不影响查找 string selTh = "SELECT * FROM " + TuhaoTableName + " WHERE 内部ID = @th LIMIT 1;"; using (var cmdTh = new System.Data.SQLite.SQLiteCommand(selTh, srcConn)) { cmdTh.Parameters.AddWithValue("@th", 图号内部ID); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdTh)) { ad.Fill(tuhaoRow); } } if (tuhaoRow.Rows.Count == 0) { MessageBox.Show(this, "图号 [" + 图号内部ID + "] 在项目报价库里没有数据,无法加入方案库。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // (b) 该图号的箱号表真实表名 + 全部箱号行 xhRealTbl = GetXianghaoTableByIndex(图号内部ID, srcConn); if (!string.IsNullOrEmpty(xhRealTbl)) { string selXh = "SELECT * FROM \"" + xhRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);"; using (var cmdXh = new System.Data.SQLite.SQLiteCommand(selXh, srcConn)) { using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdXh)) { ad.Fill(xhRows); } } } // (c) 遍历每个箱号,读其元件表 foreach (DataRow xhRow in xhRows.Rows) { // ★ 用"内部ID"列查元件表索引(不是"箱号"显示名列),用户改箱号名不影响 string xhName = FakRead内部ID(xhRow); xhNamesInOrder.Add(xhName); string yjRealTbl = GetYuanjianTableByIndex(图号内部ID, xhName, srcConn); if (string.IsNullOrEmpty(yjRealTbl)) { yjBuckets.Add(new System.Collections.Generic.KeyValuePair("", null)); continue; } DataTable yjRows = new DataTable(); string selYj = "SELECT * FROM \"" + yjRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);"; using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn)) { using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj)) { ad.Fill(yjRows); } } yjBuckets.Add(new System.Collections.Generic.KeyValuePair(yjRealTbl, yjRows)); } } xhCount = xhRows.Rows.Count; foreach (var kv in yjBuckets) { if (kv.Value != null) yjCount += kv.Value.Rows.Count; } Log("[加入方案库] 读取完成: 图号行=1 箱号行=" + xhCount + " 元件行=" + yjCount + " 目标库=" + fakDbPath); // ---- 阶段二:确保 fak.db 存在(图号表 + 表索引 + 项目信息三张固定表)---- EnsureFakDbExists(fakDbPath); // ---- 阶段三:写入 fak.db ---- // 与项目库结构一致:图号表直接写;箱号/元件按 xh_N/yj_M 动态建表,通过表索引映射。 using (var dstConn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath))) { dstConn.Open(); using (var tx = dstConn.BeginTransaction()) { try { // (1) 图号表:DELETE 旧 + INSERT 新(固定表名,直接按图号查找) string delTh = "DELETE FROM " + FakTuhaoTable + " WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delTh, dstConn, tx)) { cmd.Parameters.AddWithValue("@th", 图号内部ID); cmd.ExecuteNonQuery(); } if (tuhaoRow.Rows.Count > 0) { // 图号表的固定列(与项目库完全一致,无额外列) // fak.db 的图号表没有"内部ID"列,不带它 string[] thCols = { "序号", "图号", "图名", "台数", "箱柜数量", "单价", "总价", "成本总价", "毛利", "毛利率" }; InsertRowByColumns(tuhaoRow.Rows[0], FakTuhaoTable, thCols, dstConn, tx); } // (2) 删除该图号在 fak.db 里的旧数据: // 先从表索引查出该图号所有旧的 xh_N/yj_M 真实表名,逐个 DROP, // 再删除该图号在表索引里的所有记录。 var oldTables = new System.Collections.Generic.List(); string selOldIdx = "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(selOldIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@th", 图号内部ID); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { string t = reader.IsDBNull(0) ? "" : reader.GetString(0); if (!string.IsNullOrEmpty(t)) oldTables.Add(t); } } } foreach (string oldTbl in oldTables) { string dropSql = "DROP TABLE IF EXISTS \"" + oldTbl + "\";"; using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropSql, dstConn, tx)) { dropCmd.ExecuteNonQuery(); } } string delOldIdx = "DELETE FROM 表索引 WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delOldIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@th", 图号内部ID); cmd.ExecuteNonQuery(); } // (3) 计算新的表名序号起点:查表索引里 xh_/yj_ 的最大序号 + 1 int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh"); int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj"); // (4) 为该图号创建一张新的箱号表 xh_N,插入所有箱号行,登记表索引 string xhTblName = "xh_" + (xhSeq + 1).ToString(); CreateFakXianghaoTable(xhTblName, dstConn, tx); foreach (DataRow r in xhRows.Rows) { InsertFakXianghaoRow(r, xhTblName, dstConn, tx); } // 表索引:(类型='箱号', 图号=@th, 箱号='', 实际表名='xh_N') string insXhIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " + "VALUES (@t, @th, @xh, @real);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(insXhIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@t", "箱号"); cmd.Parameters.AddWithValue("@th", 图号内部ID); cmd.Parameters.AddWithValue("@xh", ""); cmd.Parameters.AddWithValue("@real", xhTblName); cmd.ExecuteNonQuery(); } Log("[加入方案库] 建箱号表 " + xhTblName + ",插入 " + xhRows.Rows.Count + " 行"); // (5) 对每个箱号:创建元件表 yj_M,插入元件行,登记表索引 for (int i = 0; i < yjBuckets.Count; i++) { var kv = yjBuckets[i]; if (kv.Value == null) continue; string xhName = (i < xhNamesInOrder.Count) ? xhNamesInOrder[i] : ""; yjSeq++; string yjTblName = "yj_" + yjSeq.ToString(); CreateFakYuanjianTable(yjTblName, dstConn, tx); foreach (DataRow r in kv.Value.Rows) { InsertFakYuanjianRow(r, yjTblName, dstConn, tx); } // 表索引:(类型='元件', 图号=@th, 箱号=箱号名, 实际表名='yj_M') string insYjIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " + "VALUES (@t, @th, @xh, @real);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(insYjIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@t", "元件"); cmd.Parameters.AddWithValue("@th", 图号内部ID); cmd.Parameters.AddWithValue("@xh", xhName); cmd.Parameters.AddWithValue("@real", yjTblName); cmd.ExecuteNonQuery(); } Log("[加入方案库] 建元件表 " + yjTblName + "(箱号=" + xhName + "),插入 " + kv.Value.Rows.Count + " 行"); } tx.Commit(); } catch { try { tx.Rollback(); } catch { } throw; } } } } catch (Exception ex) { Log("[加入方案库] 失败: " + ex.Message); MessageBox.Show(this, "加入方案库失败:\n" + ex.Message, "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 旧方案刷新入口(fak.db 平铺方式已退役)。 /// 现架构:方案 = sjk\方案库\ 下的独立 .db 文件,方案窗口(DockWindow8)自治加载与刷新。 /// public void ReloadFakData() { try { if (this.w8 != null) this.w8.重新加载当前方案(); Log("[方案刷新] 已通知方案窗口重载当前方案"); } catch (Exception ex) { Log("[方案刷新] 失败: " + ex.Message); } } /// 方案窗口当前打开的方案文件路径(未打开返回 null)。 private string 方案数据源路径() { return (this.w8 != null) ? this.w8.当前方案文件 : null; } /// 写方案库文件的连接串:与方案窗口(DockWindow8.连接串)同款,带 BusyTimeout=3000—— /// 主窗口 GetConnString 无 BusyTimeout(默认0),方案窗口正持 WAL 连接时写方遇锁会立刻炸 database is locked。 private string 方案库写连接串(string fakDbPath) { return "Data Source=\"" + fakDbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=3000;"; } /// 读方案库"项目信息"表第一行为字典(无表/无行返回空字典)。 private System.Collections.Generic.Dictionary LoadFak项目信息(string fakDbPath) { var d = new System.Collections.Generic.Dictionary(); try { if (!System.IO.File.Exists(fakDbPath)) return d; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT * FROM 项目信息 LIMIT 1;", conn)) using (var r = cmd.ExecuteReader()) { if (r.Read()) for (int i = 0; i < r.FieldCount; i++) d[r.GetName(i)] = r.IsDBNull(i) ? "" : r.GetValue(i).ToString(); } } } catch { } return d; } // ===== 方案页面(tabPage13)三层联动:图号 → 箱号 → 元件 ===== private bool _fakReloading = false; /// 方案页面图号表选中变化 → 联动刷新箱号表 private void FakTuhao_SelectionChanged(object sender, EventArgs e) { if (_fakReloading) return; DataGridViewRow row = GetFirstSelectedRow(this.advancedDataGridView图号); if (row == null) return; string tuhao = GetCellValueAsString(row, "图号"); if (string.IsNullOrEmpty(tuhao)) return; ReloadFakXianghao(tuhao); } /// 方案页面箱号表选中变化 → 联动刷新元件表 private void FakXianghao_SelectionChanged(object sender, EventArgs e) { if (_fakReloading) return; DataGridViewRow rowTuhao = GetFirstSelectedRow(this.advancedDataGridView图号); if (rowTuhao == null) return; string tuhao = GetCellValueAsString(rowTuhao, "图号"); DataGridViewRow rowXianghao = GetFirstSelectedRow(this.advancedDataGridView箱号); if (rowXianghao == null) return; string xianghao = GetCellValueAsString(rowXianghao, "箱号"); if (string.IsNullOrEmpty(xianghao)) return; ReloadFakYuanjian(tuhao, xianghao); } /// 从 fak.db 加载指定图号的箱号数据 private void ReloadFakXianghao(string tuhao) { _fakReloading = true; try { string fakDbPath = 方案数据源路径(); if (!System.IO.File.Exists(fakDbPath)) return; // 从表索引查出箱号真实表名 string xhRealTbl = ""; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); xhRealTbl = GetXianghaoTableByIndex(tuhao, conn); if (string.IsNullOrEmpty(xhRealTbl)) { this.advancedDataGridView箱号.DataSource = null; this.advancedDataGridView元件.DataSource = null; return; } var table = new DataTable(); // 老fak库结构兼容:箱号表可能没有"箱名"列,读取前幂等补列(表格绑定依赖该列) EnsureColumns(conn, "\"" + xhRealTbl + "\"", new[] { "箱名" }); string sql = "SELECT * FROM \"" + xhRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) { ad.Fill(table); } // 关闭自动生成列,保留 .ui.cs 里定义的列宽(序号=35px 等) this.advancedDataGridView箱号.AutoGenerateColumns = false; this.advancedDataGridView箱号.DataSource = table; this.advancedDataGridView元件.DataSource = null; } } catch (Exception ex) { Log("[方案刷新] 箱号加载失败: " + ex.Message); } finally { _fakReloading = false; } } /// 从 fak.db 加载指定图号+箱号的元件数据 private void ReloadFakYuanjian(string tuhao, string xianghao) { _fakReloading = true; try { string fakDbPath = 方案数据源路径(); if (!System.IO.File.Exists(fakDbPath)) return; string yjRealTbl = ""; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); yjRealTbl = GetYuanjianTableByIndex(tuhao, xianghao, conn); if (string.IsNullOrEmpty(yjRealTbl)) { this.advancedDataGridView元件.DataSource = null; return; } var table = new DataTable(); string sql = "SELECT * FROM \"" + yjRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) { ad.Fill(table); } // 关闭自动生成列,保留 .ui.cs 里定义的列宽(序号=35px 等) this.advancedDataGridView元件.AutoGenerateColumns = false; this.advancedDataGridView元件.DataSource = table; } } catch (Exception ex) { Log("[方案刷新] 元件加载失败: " + ex.Message); } finally { _fakReloading = false; } } // ============================================================ // 方案页面 右键菜单事件(图号/箱号/元件 的 删除/插入) // - 删除:从 fak.db 删除选中数据,刷新方案页面 // - 插入:把 fak.db 选中数据复制到当前项目报价库(GetQuoteDbPath(_currentProjectId)) // ============================================================ // ---------- 图号菜单:删除 / 插入 ---------- /// /// 方案页面 图号右键菜单「删除图号」: /// 遍历 advancedDataGridView图号 选中的行,对每个图号: /// 1) 从 fak.db 的表索引查出该图号关联的 xh_N/yj_M 真实表名,逐个 DROP /// 2) 从 fak.db 图号表 DELETE WHERE 图号=@th /// 3) 从 fak.db 表索引 DELETE WHERE 图号=@th /// 4) 刷新 ReloadFakData() /// private void FakTuhao_Delete_Click(object sender, EventArgs e) { var rows = GetAllSelectedRows(this.advancedDataGridView图号); if (rows.Count == 0) { MessageBox.Show(this, "请先在图号表中选择要删除的图号。", "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 收集图号名(去重) var tuhaos = new System.Collections.Generic.List(); var seen = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string th = GetCellValueAsString(r, "图号"); if (!string.IsNullOrEmpty(th) && seen.Add(th)) tuhaos.Add(th); } if (tuhaos.Count == 0) { MessageBox.Show(this, "未能从选中行读取图号。", "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string msg = "确认从方案库删除以下 " + tuhaos.Count + " 个图号?\n" + string.Join(", ", tuhaos) + "\n\n该图号下的所有箱号、元件数据也会一并删除。"; if (MessageBox.Show(this, msg, "删除图号", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; string fakDbPath = 方案数据源路径(); if (!System.IO.File.Exists(fakDbPath)) { MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案", "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { // ★ 删除前快照方案库,支持右键「撤销删除」还原 PushUndoSnapshot(fakDbPath, "", true); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { try { foreach (string th in tuhaos) { // 1) 查表索引拿该图号所有 xh_N/yj_M 真实表名,逐个 DROP var oldTables = new System.Collections.Generic.List(); string selOldIdx = "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(selOldIdx, conn, tx)) { cmd.Parameters.AddWithValue("@th", th); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { string t = reader.IsDBNull(0) ? "" : reader.GetString(0); if (!string.IsNullOrEmpty(t)) oldTables.Add(t); } } } foreach (string oldTbl in oldTables) { string dropSql = "DROP TABLE IF EXISTS \"" + oldTbl + "\";"; using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropSql, conn, tx)) { dropCmd.ExecuteNonQuery(); } } // 2) 从图号表 DELETE string delTh = "DELETE FROM " + FakTuhaoTable + " WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delTh, conn, tx)) { cmd.Parameters.AddWithValue("@th", th); cmd.ExecuteNonQuery(); } // 3) 从表索引 DELETE string delIdx = "DELETE FROM 表索引 WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delIdx, conn, tx)) { cmd.Parameters.AddWithValue("@th", th); cmd.ExecuteNonQuery(); } Log("[方案删除图号] 已删除图号=" + th + ", 关联表 " + oldTables.Count + " 张"); } tx.Commit(); } catch { try { tx.Rollback(); } catch { } throw; } } } ReloadFakData(); MessageBox.Show(this, "已从方案库删除 " + tuhaos.Count + " 个图号。", "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { Log("[方案删除图号] 失败: " + ex.Message); MessageBox.Show(this, "删除图号失败:\n" + ex.Message, "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// 按图号名删除方案库里的一个方案(图号行+箱号/元件表+表索引;带快照与确认)。供方案窗口项目页右键调用。 public void 删除方案图号(string 图号名) { if (string.IsNullOrWhiteSpace(图号名)) return; string fakDbPath = 方案数据源路径(); if (!System.IO.File.Exists(fakDbPath)) { MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案", "删除方案", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } string msg = "确认从方案库删除方案 [" + 图号名 + "]?\n\n该方案下的所有箱号、元件数据也会一并删除。"; if (MessageBox.Show(this, msg, "删除方案", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; try { PushUndoSnapshot(fakDbPath, "", true); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { try { var oldTables = new System.Collections.Generic.List(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", 图号名); using (var reader = cmd.ExecuteReader()) while (reader.Read()) { string t = reader.IsDBNull(0) ? "" : reader.GetString(0); if (!string.IsNullOrEmpty(t)) oldTables.Add(t); } } foreach (string oldTbl in oldTables) Exec(conn, tx, "DROP TABLE IF EXISTS \"" + oldTbl + "\";"); using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + FakTuhaoTable + " WHERE 图号 = @th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", 图号名); cmd.ExecuteNonQuery(); } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 图号 = @th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", 图号名); cmd.ExecuteNonQuery(); } tx.Commit(); Log("[方案删除] 已删除方案=" + 图号名 + ", 关联表 " + oldTables.Count + " 张"); } catch { try { tx.Rollback(); } catch { } throw; } } } ReloadFakData(); } catch (Exception ex) { Log("[方案删除] 失败: " + ex.Message); MessageBox.Show(this, "删除方案失败:\n" + ex.Message, "删除方案", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 方案页面 图号右键菜单「插入图号」: /// 把选中的图号(含其下所有箱号+元件)从 fak.db 复制到当前项目报价库。 /// 逻辑与 MenuTuhao_AddToFak_Click 相反方向(fak.db → 项目库)。 /// private void FakTuhao_Insert_Click(object sender, EventArgs e) { Log("点击[方案插入图号到报价], _currentProjectId=[" + _currentProjectId + "]"); // 1) 校验当前项目 if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在报价页面双击选择一个项目,再从方案库插入图号。", "插入图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var rows = GetAllSelectedRows(this.advancedDataGridView图号); if (rows.Count == 0) { MessageBox.Show(this, "请先在方案图号表中选择要插入的图号。", "插入图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 收集图号名(去重) var tuhaos = new System.Collections.Generic.List(); var seen = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string th = GetCellValueAsString(r, "图号"); if (!string.IsNullOrEmpty(th) && seen.Add(th)) tuhaos.Add(th); } if (tuhaos.Count == 0) { MessageBox.Show(this, "未能从选中行读取图号。", "插入图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string srcDbPath = 方案数据源路径(); if (!System.IO.File.Exists(srcDbPath)) { MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案", "插入图号", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 确保目标项目库存在 EnsureQuoteDbExists(_currentProjectId); string dstDbPath = GetQuoteDbPath(_currentProjectId); try { int totalXh = 0, totalYj = 0; foreach (string th in tuhaos) { int xhCount = 0, yjCount = 0; // ★ 真隔离(2026-09-29):每个箱号实例生成独立 GUID 作内部ID, // 元件表索引按 GUID 登记,跨实例绝不共用元件表(根治串数据) var newXhIds = new System.Collections.Generic.List(); // ---- 阶段一:从 fak.db 读出该图号的全部数据 ---- DataTable tuhaoRow = new DataTable(); string xhRealTblSrc = ""; DataTable xhRows = new DataTable(); var yjBuckets = new System.Collections.Generic.List< System.Collections.Generic.KeyValuePair>(); var xhNamesInOrder = new System.Collections.Generic.List(); using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath))) { srcConn.Open(); // (a) 图号行 string selTh = "SELECT * FROM " + FakTuhaoTable + " WHERE 图号 = @th LIMIT 1;"; using (var cmdTh = new System.Data.SQLite.SQLiteCommand(selTh, srcConn)) { cmdTh.Parameters.AddWithValue("@th", th); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdTh)) { ad.Fill(tuhaoRow); } } if (tuhaoRow.Rows.Count == 0) { Log("[方案插入图号] fak.db 里找不到图号=" + th + ",跳过"); continue; } // (b) 该图号的箱号表真实表名 + 全部箱号行 xhRealTblSrc = GetXianghaoTableByIndex(th, srcConn); if (!string.IsNullOrEmpty(xhRealTblSrc)) { string selXh = "SELECT * FROM \"" + xhRealTblSrc + "\" ORDER BY CAST(序号 AS INTEGER);"; using (var cmdXh = new System.Data.SQLite.SQLiteCommand(selXh, srcConn)) using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdXh)) { ad.Fill(xhRows); } } // (c) 遍历每个箱号,读其元件表 foreach (DataRow xhRow in xhRows.Rows) { // ★ 用"内部ID"列查元件表索引(不是"箱号"显示名列),用户改箱号名不影响 string xhName = FakRead内部ID(xhRow); xhNamesInOrder.Add(xhName); string yjRealTblSrc = GetYuanjianTableByIndex(th, xhName, srcConn); if (string.IsNullOrEmpty(yjRealTblSrc)) { yjBuckets.Add(new System.Collections.Generic.KeyValuePair("", null)); continue; } DataTable yjRows = new DataTable(); string selYj = "SELECT * FROM \"" + yjRealTblSrc + "\" ORDER BY CAST(序号 AS INTEGER);"; using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn)) using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj)) { ad.Fill(yjRows); } yjBuckets.Add(new System.Collections.Generic.KeyValuePair(yjRealTblSrc, yjRows)); } } xhCount = xhRows.Rows.Count; foreach (var kv in yjBuckets) { if (kv.Value != null) yjCount += kv.Value.Rows.Count; } Log("[方案插入图号] 读取图号=" + th + " 完成: 图号行=1 箱号行=" + xhCount + " 元件行=" + yjCount); // ---- 阶段二:写入项目报价库 ---- // 如果项目库已有相同图号,自动重命名(新图号1 → 新图号1_2, 新图号1_3...) string actualTh = th; using (var chkConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath))) { chkConn.Open(); string chkTh = "SELECT COUNT(*) FROM " + TuhaoTableName + " WHERE 图号 = @th;"; using (var chkCmd = new System.Data.SQLite.SQLiteCommand(chkTh, chkConn)) { chkCmd.Parameters.AddWithValue("@th", th); int dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar()); if (dupCnt > 0) { int suffix = 2; while (dupCnt > 0) { actualTh = th + "_" + suffix.ToString(); suffix++; chkCmd.Parameters["@th"].Value = actualTh; dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar()); } Log("[方案插入图号] 图号=" + th + " 已存在,重命名为 " + actualTh); } } } // 修改图号名 tuhaoRow.Rows[0]["图号"] = actualTh; if (!tuhaoRow.Columns.Contains("内部ID")) tuhaoRow.Columns.Add("内部ID", typeof(string)); // ★ 真隔离修正(2026-09-30):图号内部ID = 显示名(与箱号表/元件表索引的"图号"键一致)。 // 不能用 GUID——主窗口选中图号后 _currentTuhao 从"内部ID"列读, // 若为 GUID 会查不到按显示名登记的箱号表/元件表索引(图号唯一性靠重命名机制保证, // 真隔离只需要箱号层用 GUID)。 tuhaoRow.Rows[0]["内部ID"] = actualTh; using (var dstConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath))) { dstConn.Open(); using (var tx = dstConn.BeginTransaction()) { try { // (1) 图号表:INSERT(用重命名后的图号名) string[] thCols = { "序号", "图号", "图名", "台数", "箱柜数量", "单价", "总价", "成本总价", "毛利", "毛利率" }; InsertRowByColumns(tuhaoRow.Rows[0], TuhaoTableName, thCols, dstConn, tx); // (2) 计算新的表名序号起点 int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh"); int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj"); // (3) 创建箱号表 xh_N,插入箱号行,登记表索引(用 actualTh) string newXhTbl = "xh_" + (xhSeq + 1).ToString(); CreateFakXianghaoTable(newXhTbl, dstConn, tx); if (!xhRows.Columns.Contains("内部ID")) xhRows.Columns.Add("内部ID", typeof(string)); foreach (DataRow r in xhRows.Rows) { r.BeginEdit(); // ★ 真隔离(2026-09-29):每条箱号生成独立 GUID 作内部ID, // 不复用方案库原内部ID,杜绝跨实例串数据 string xhId = System.Guid.NewGuid().ToString("N"); r["内部ID"] = xhId; newXhIds.Add(xhId); r.EndEdit(); InsertFakXianghaoRow(r, newXhTbl, dstConn, tx); } string insXhIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " + "VALUES (@t, @th, @xh, @real);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(insXhIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@t", "箱号"); cmd.Parameters.AddWithValue("@th", actualTh); cmd.Parameters.AddWithValue("@xh", ""); cmd.Parameters.AddWithValue("@real", newXhTbl); cmd.ExecuteNonQuery(); } // (4) 为每个箱号创建元件表 yj_M,插入元件行,登记表索引 for (int i = 0; i < yjBuckets.Count; i++) { var kv = yjBuckets[i]; if (kv.Value == null) continue; // ★ 真隔离(2026-09-29):元件表索引"箱号"字段用箱号行的新 GUID, // 不再用方案库原内部ID/显示名,跨实例绝不共用元件表 string newXhId = (i < newXhIds.Count) ? newXhIds[i] : ""; yjSeq++; string newYjTbl = "yj_" + yjSeq.ToString(); CreateFakYuanjianTable(newYjTbl, dstConn, tx); foreach (DataRow r in kv.Value.Rows) { InsertFakYuanjianRow(r, newYjTbl, dstConn, tx); } // ★ 插入后按公式重算元件派生价 重算元件表派生价(newYjTbl, dstConn, tx); string insYjIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " + "VALUES (@t, @th, @xh, @real);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(insYjIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@t", "元件"); cmd.Parameters.AddWithValue("@th", actualTh); cmd.Parameters.AddWithValue("@xh", newXhId); // ★ 用箱号新 GUID 作索引键 cmd.Parameters.AddWithValue("@real", newYjTbl); cmd.ExecuteNonQuery(); } } tx.Commit(); } catch { try { tx.Rollback(); } catch { } throw; } } } totalXh += xhCount; totalYj += yjCount; Log("[方案插入图号] 图号=" + th + " 写入项目库完成"); } // ★ 记住报价图号表选中行索引,用于"在选中行下面插入" int selectedTuhaoIndex = GetSelectedRowIndex(this.advancedDataGridView9); // 刷新报价页面图号表 // 关键:清空所有缓存(_xianghaoCache / _yuanjianCache),防止 // SaveAllToDatabaseSilent 遍历缓存时用旧数据覆盖刚插入的数据库行。 _isReloading = true; try { _xianghaoCache.Clear(); _yuanjianCache.Clear(); if (_tuhaoTable != null) { _tuhaoTable.Clear(); _tuhaoTable.AcceptChanges(); } if (_xianghaoTable != null) { _xianghaoTable.Clear(); _xianghaoTable.AcceptChanges(); } if (_yuanjianTable != null) { _yuanjianTable.Clear(); _yuanjianTable.AcceptChanges(); } 计算引擎.刷新项目汇总(GetQuoteDbPath(_currentProjectId), _currentProjectId, TuhaoTableName); // 触发器已算明细,补跨库项目汇总 ReloadTuhaoByProject(_currentProjectId); // ★ 把末尾新插入的图号移动到选中行下面 if (_tuhaoTable != null) MoveLastNRowsBelowSelected(this.advancedDataGridView9, _tuhaoTable, selectedTuhaoIndex, tuhaos.Count, TuhaoTableName, _currentProjectId); } catch (Exception exReload) { Log("[方案插入图号] 刷新异常: " + exReload.Message); } finally { _isReloading = false; } // ★ 与"重新打开项目"完全相同的完整刷新路径:重算汇总+全表重载+选中联动。 // 插入流程自己的局部刷新覆盖不了全部状态(消耗列/元件表要重开才出现)。 string 记住图号 = _currentTuhao, 记住箱号 = _currentXianghao; try { EnterProjectCore(_currentProjectId); } catch (Exception exEnter) { Log("[方案插入] 完整刷新异常: " + exEnter.Message); } // ★ 完整刷新会把选中重置回第一个图号/箱号。这里不靠"选中联动"(它被 // _isReloading 屏蔽,不触发加载),而是直接按记住的图号/箱号强制重载, // 保证界面立即显示刚插入的数据。 try { if (!string.IsNullOrEmpty(记住图号)) { _currentTuhao = 记住图号; ReloadXianghaoByTuhao(_currentProjectId, 记住图号); if (!string.IsNullOrEmpty(记住箱号)) { ReloadYuanjianByXianghao(_currentProjectId, 记住图号, 记住箱号); } // ★ 关键:重载完成后最后校正当前箱号变量并高亮对应行, // 保证 箱号表高亮 与 元件表显示 完全一致(之前变量停在旧箱号,误导显示) _currentXianghao = 记住箱号; SelectRowByValue(this.advancedDataGridView9, "内部ID", 记住图号); if (!string.IsNullOrEmpty(记住箱号)) SelectRowByValue(this.advancedDataGridView4, "内部ID", 记住箱号); } } catch (Exception exRestore) { Log("[方案插入] 恢复选中视图异常: " + exRestore.Message); } string doneMsg = "已把 " + tuhaos.Count + " 个图号从方案库插入到当前项目报价库。\n" + "箱号行 " + totalXh + " 条,元件行 " + totalYj + " 条。"; Log("[方案插入图号] 成功: " + doneMsg); MessageBox.Show(this, doneMsg, "插入图号", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { Log("[方案插入图号] 失败: " + ex.Message); MessageBox.Show(this, "插入图号失败:\n" + ex.Message, "插入图号", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 箱号菜单:删除 / 插入 ---------- /// /// 方案页面 箱号右键菜单「删除箱号」: /// 对每个选中的箱号(在当前选中的图号下): /// 1) 从 fak.db 的 xh_N 表 DELETE 该箱号行 /// 2) 查表索引拿该箱号的 yj_M 表名,DROP 它 /// 3) 从表索引 DELETE WHERE 类型='元件' AND 图号=@th AND 箱号=@xh /// 4) 刷新当前图号的箱号表(ReloadFakXianghao) /// private void FakXianghao_Delete_Click(object sender, EventArgs e) { // 必须先有图号选中 DataGridViewRow rowTuhao = GetFirstSelectedRow(this.advancedDataGridView图号); if (rowTuhao == null) { MessageBox.Show(this, "请先在方案图号表中选择图号,再删除箱号。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string tuhao = GetCellValueAsString(rowTuhao, "图号"); if (string.IsNullOrEmpty(tuhao)) { MessageBox.Show(this, "当前图号为空,无法删除箱号。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var rows = GetAllSelectedRows(this.advancedDataGridView箱号); if (rows.Count == 0) { MessageBox.Show(this, "请先在箱号表中选择要删除的箱号。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 收集箱号名(去重) var xhs = new System.Collections.Generic.List(); var seen = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string xh = GetCellValueAsString(r, "箱号"); if (!string.IsNullOrEmpty(xh) && seen.Add(xh)) xhs.Add(xh); } if (xhs.Count == 0) { MessageBox.Show(this, "未能从选中行读取箱号。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string msg = "确认从方案库图号 [" + tuhao + "] 下删除以下 " + xhs.Count + " 个箱号?\n" + string.Join(", ", xhs) + "\n\n该箱号下的所有元件数据也会一并删除。"; if (MessageBox.Show(this, msg, "删除箱号", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; string fakDbPath = 方案数据源路径(); if (!System.IO.File.Exists(fakDbPath)) { MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { // ★ 删除前快照方案库,支持右键「撤销删除」还原 PushUndoSnapshot(fakDbPath, "", true); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); string xhRealTbl = GetXianghaoTableByIndex(tuhao, conn); if (string.IsNullOrEmpty(xhRealTbl)) { MessageBox.Show(this, "图号 [" + tuhao + "] 没有箱号表。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using (var tx = conn.BeginTransaction()) { try { foreach (string xh in xhs) { // 1) 从 xh_N 表删除该箱号行 string delXh = "DELETE FROM \"" + xhRealTbl + "\" WHERE 箱号 = @xh;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delXh, conn, tx)) { cmd.Parameters.AddWithValue("@xh", xh); cmd.ExecuteNonQuery(); } // 2) 查表索引拿该箱号的元件表名,DROP string yjTbl = LookupTableByIndex("元件", tuhao, xh, conn); if (!string.IsNullOrEmpty(yjTbl)) { string dropYj = "DROP TABLE IF EXISTS \"" + yjTbl + "\";"; using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropYj, conn, tx)) { dropCmd.ExecuteNonQuery(); } } // 3) 从表索引删除该箱号的元件记录 string delIdx = "DELETE FROM 表索引 WHERE 类型=@t AND 图号=@th AND 箱号=@xh;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delIdx, conn, tx)) { cmd.Parameters.AddWithValue("@t", "元件"); cmd.Parameters.AddWithValue("@th", tuhao); cmd.Parameters.AddWithValue("@xh", xh); cmd.ExecuteNonQuery(); } Log("[方案删除箱号] 图号=" + tuhao + " 箱号=" + xh + " 已删除"); } tx.Commit(); } catch { try { tx.Rollback(); } catch { } throw; } } } // 刷新当前图号的箱号表 ReloadFakXianghao(tuhao); MessageBox.Show(this, "已删除 " + xhs.Count + " 个箱号。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { Log("[方案删除箱号] 失败: " + ex.Message); MessageBox.Show(this, "删除箱号失败:\n" + ex.Message, "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 方案页面 箱号右键菜单「插入箱号」: /// 把选中的箱号(含其下所有元件)从 fak.db 插入到报价页面当前图号(_currentTuhao)下。 /// private void FakXianghao_Insert_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在报价页面双击选择一个项目,再插入箱号。", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 目标图号 = 报价页面当前选中的图号(用户需要先在报价页面选中一个图号) if (string.IsNullOrWhiteSpace(_currentTuhao)) { MessageBox.Show(this, "请先在报价页面的图号表中选中一个图号,\n再回到方案页面插入箱号。", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string targetTuhao = _currentTuhao; // 方案页面当前选中的图号(箱号数据来源) DataGridViewRow rowFakTuhao = GetFirstSelectedRow(this.advancedDataGridView图号); if (rowFakTuhao == null) { MessageBox.Show(this, "请先在方案图号表中选择图号,再插入箱号。", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string fakTuhao = GetCellValueAsString(rowFakTuhao, "图号"); if (string.IsNullOrEmpty(fakTuhao)) { MessageBox.Show(this, "方案图号为空,无法读取箱号数据。", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } Log("点击[方案插入箱号到报价], 项目=[" + _currentProjectId + "] 报价图号=[" + targetTuhao + "] 方案图号=[" + fakTuhao + "]"); var rows = GetAllSelectedRows(this.advancedDataGridView箱号); if (rows.Count == 0) { MessageBox.Show(this, "请先在方案箱号表中选择要插入的箱号。", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 顺序修复(2026-09-29):SelectedRows 集合按选中操作逆序返回, // 按 Grid 行索引(=方案库显示顺序)排序,保证插入顺序与方案库一致 rows.Sort((a, b) => a.Index.CompareTo(b.Index)); // 收集箱号名(去重) var xhs = new System.Collections.Generic.List(); var seen = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string xh = GetCellValueAsString(r, "箱号"); if (!string.IsNullOrEmpty(xh) && seen.Add(xh)) xhs.Add(xh); } if (xhs.Count == 0) { MessageBox.Show(this, "未能从选中行读取箱号。", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string srcDbPath = 方案数据源路径(); if (!System.IO.File.Exists(srcDbPath)) { MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } EnsureQuoteDbExists(_currentProjectId); string dstDbPath = GetQuoteDbPath(_currentProjectId); // ★ 定位插入(2026-09-29):新箱号插到报价页面当前选中箱号的下方—— // 选中行序号=基准,序号更大的行整体+n 让位,新行依次用让出的序号。 // (旧逻辑沿用方案库源序号,与已有行序号重复导致排序交错、事后移动错位) long baseSeq = 0; { using (var seqConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath))) { seqConn.Open(); string dstXhTbl0 = LookupTableByIndex("箱号", targetTuhao, "", seqConn); if (!string.IsNullOrEmpty(dstXhTbl0)) { EnsureColumns(seqConn, "\"" + dstXhTbl0 + "\"", new[] { "内部ID" }); // 选中行序号(双键:内部ID GUID 优先,老数据显示名兜底) if (!string.IsNullOrWhiteSpace(_currentXianghao)) { string selSeqSql = "SELECT CAST(序号 AS INTEGER) FROM \"" + dstXhTbl0 + "\"" + " WHERE 内部ID=@k OR 箱号=@k LIMIT 1;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(selSeqSql, seqConn)) { cmd.Parameters.AddWithValue("@k", _currentXianghao); object o = cmd.ExecuteScalar(); if (o != null && o != DBNull.Value) baseSeq = Convert.ToInt64(o); } } if (baseSeq <= 0) { // 无选中行(或未命中):追加到末尾 = 当前最大序号 using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT MAX(CAST(序号 AS INTEGER)) FROM \"" + dstXhTbl0 + "\";", seqConn)) { object o = cmd.ExecuteScalar(); if (o != null && o != DBNull.Value) baseSeq = Convert.ToInt64(o); } } // 让位:序号 > baseSeq 的行整体 +n,空出的位置给新行 string shiftSql = "UPDATE \"" + dstXhTbl0 + "\" SET 序号 = CAST(序号 AS INTEGER) + @n" + " WHERE CAST(序号 AS INTEGER) > @base;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(shiftSql, seqConn)) { cmd.Parameters.AddWithValue("@n", xhs.Count); cmd.Parameters.AddWithValue("@base", baseSeq); int moved = cmd.ExecuteNonQuery(); Log("[方案插入箱号] 定位插入: 选中序号=" + baseSeq + " 让位行数=" + moved + " 新行序号=" + (baseSeq + 1) + ".." + (baseSeq + xhs.Count)); } } } } int xhInsertCounter = 0; try { int totalYj = 0; using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath))) { srcConn.Open(); string xhRealTblSrc = GetXianghaoTableByIndex(fakTuhao, srcConn); if (string.IsNullOrEmpty(xhRealTblSrc)) { MessageBox.Show(this, "方案图号 [" + fakTuhao + "] 没有箱号表。", "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } foreach (string xh in xhs) { // 读箱号行 DataTable xhRow = new DataTable(); string selXh = "SELECT * FROM \"" + xhRealTblSrc + "\" WHERE 箱号 = @xh LIMIT 1;"; using (var cmdXh = new System.Data.SQLite.SQLiteCommand(selXh, srcConn)) { cmdXh.Parameters.AddWithValue("@xh", xh); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdXh)) { ad.Fill(xhRow); } } if (xhRow.Rows.Count == 0) { Log("[方案插入箱号] fak.db 里找不到箱号=" + xh + ",跳过"); continue; } // 读该箱号的元件数据 DataTable yjRows = new DataTable(); string yjRealTblSrc = GetYuanjianTableByIndex(fakTuhao, xh, srcConn); if (!string.IsNullOrEmpty(yjRealTblSrc)) { string selYj = "SELECT * FROM \"" + yjRealTblSrc + "\" ORDER BY CAST(序号 AS INTEGER);"; using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn)) using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj)) { ad.Fill(yjRows); } } // ---- 写入项目库目标图号(targetTuhao)---- using (var dstConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath))) { dstConn.Open(); using (var tx = dstConn.BeginTransaction()) { try { // (1) 确保目标图号有箱号表,没有就建 string dstXhTbl = LookupTableByIndex("箱号", targetTuhao, "", dstConn); if (string.IsNullOrEmpty(dstXhTbl)) { int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh"); dstXhTbl = "xh_" + (xhSeq + 1).ToString(); CreateFakXianghaoTable(dstXhTbl, dstConn, tx); string insXhIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " + "VALUES (@t, @th, @xh, @real);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(insXhIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@t", "箱号"); cmd.Parameters.AddWithValue("@th", targetTuhao); cmd.Parameters.AddWithValue("@xh", ""); cmd.Parameters.AddWithValue("@real", dstXhTbl); cmd.ExecuteNonQuery(); } } // ★ 老库结构兼容:目标图号的箱号表若已存在且是老结构(无"箱名"列), // 幂等补列,否则下面的全列 INSERT 会报 no column named 箱名 // ★ 真隔离(2026-09-29):同时补"内部ID"列,新插入写入 GUID EnsureColumns(dstConn, "\"" + dstXhTbl + "\"", new[] { "箱名", "内部ID" }); // (2) 插入箱号行(如果箱号已存在,自动重命名) string actualXhName = xh; // ★ 真隔离(2026-09-29):每个箱号实例生成独立 GUID 作内部ID, // 元件表索引按 GUID 登记,跨实例绝不共用元件表(根治串数据) string newXhId = System.Guid.NewGuid().ToString("N"); { string chkDup = "SELECT COUNT(*) FROM \"" + dstXhTbl + "\" WHERE 箱号 = @xh;"; using (var chkCmd = new System.Data.SQLite.SQLiteCommand(chkDup, dstConn, tx)) { chkCmd.Parameters.AddWithValue("@xh", xh); int dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar()); if (dupCnt > 0) { // 自动重命名:新箱号2 → 新箱号2_2, 新箱号2_3... int suffix = 2; while (dupCnt > 0) { actualXhName = xh + "_" + suffix.ToString(); suffix++; chkCmd.Parameters["@xh"].Value = actualXhName; dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar()); } Log("[方案插入箱号] 箱号=" + xh + " 已存在,重命名为 " + actualXhName); } } // 修改箱号名后插入 DataRow renamedRow = xhRow.Rows[0].Table.NewRow(); foreach (DataColumn col in xhRow.Rows[0].Table.Columns) { renamedRow[col.ColumnName] = xhRow.Rows[0][col.ColumnName]; } renamedRow["箱号"] = actualXhName; if (!renamedRow.Table.Columns.Contains("内部ID")) renamedRow.Table.Columns.Add("内部ID", typeof(string)); // ★ 真隔离(2026-09-29):内部ID 用 GUID,不再等于显示名, // 杜绝跨实例共用同一元件表索引导致串数据 newXhId = System.Guid.NewGuid().ToString("N"); renamedRow["内部ID"] = newXhId; // ★ 定位插入:序号用让位后的连续序号(不用方案库源序号, // 避免与已有行序号重复导致排序交错) xhInsertCounter++; if (renamedRow.Table.Columns.Contains("序号")) renamedRow["序号"] = (baseSeq + xhInsertCounter).ToString(); Log("[方案插入箱号] 准备插入: 表=" + dstXhTbl + " 箱号=" + actualXhName + " 内部ID=" + newXhId + " 目标图号=" + targetTuhao); InsertFakXianghaoRow(renamedRow, dstXhTbl, dstConn, tx); Log("[方案插入箱号] 插入完成: 表=" + dstXhTbl + " 箱号=" + actualXhName + " 内部ID=" + newXhId); } // (3) ★ 真隔离:永远创建新元件表(不复用旧表), // 元件表索引"箱号"字段用新 GUID 登记,跨实例绝不共用元件表 int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj"); string dstYjTbl = "yj_" + (yjSeq + 1).ToString(); CreateFakYuanjianTable(dstYjTbl, dstConn, tx); string insYjIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " + "VALUES (@t, @th, @xh, @real);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(insYjIdx, dstConn, tx)) { cmd.Parameters.AddWithValue("@t", "元件"); cmd.Parameters.AddWithValue("@th", targetTuhao); cmd.Parameters.AddWithValue("@xh", newXhId); // ★ 用新 GUID 作索引键 cmd.Parameters.AddWithValue("@real", dstYjTbl); cmd.ExecuteNonQuery(); } // (4) 插入元件行 foreach (DataRow yjR in yjRows.Rows) { InsertFakYuanjianRow(yjR, dstYjTbl, dstConn, tx); } // ★ 插入后按公式重算元件派生价 重算元件表派生价(dstYjTbl, dstConn, tx); tx.Commit(); totalYj += yjRows.Rows.Count; Log("[方案插入箱号] 箱号=" + xh + " 写入图号[" + targetTuhao + "], 元件行=" + yjRows.Rows.Count); } catch { try { tx.Rollback(); } catch { } throw; } } } } } // ★ 定位插入(2026-09-29):插入前已按选中行序号让位,新行天然在选中行下方, // 不再需要事后移动(且 ReloadXianghaoByTuhao 会把选中重置回第一行, // 事后移动的"选中行索引"已失真,会造成误移) // 刷新报价页面(切到目标图号的箱号表) // 关键:清空缓存,防止 SaveAllToDatabaseSilent 遍历缓存时覆盖刚插入的数据 _currentTuhao = targetTuhao; _isReloading = true; try { _xianghaoCache.Clear(); _yuanjianCache.Clear(); if (_xianghaoTable != null) { _xianghaoTable.Clear(); _xianghaoTable.AcceptChanges(); } if (_yuanjianTable != null) { _yuanjianTable.Clear(); _yuanjianTable.AcceptChanges(); } if (_tuhaoTable != null) _tuhaoTable.AcceptChanges(); RecalcAllFromDatabase(_currentProjectId); // ★ 重算箱号/图号/项目汇总价 ReloadXianghaoByTuhao(_currentProjectId, targetTuhao); } catch (Exception exReload) { Log("[方案插入箱号] 刷新报价页面异常: " + exReload.Message); } finally { _isReloading = false; } // ★ 与"重新打开项目"完全相同的完整刷新路径:重算汇总+全表重载+选中联动。 // 插入流程自己的局部刷新覆盖不了全部状态(消耗列/元件表要重开才出现)。 string 记住图号 = _currentTuhao, 记住箱号 = _currentXianghao; try { EnterProjectCore(_currentProjectId); } catch (Exception exEnter) { Log("[方案插入] 完整刷新异常: " + exEnter.Message); } // ★ 完整刷新会把选中重置回第一个图号/箱号。这里不靠"选中联动"(它被 // _isReloading 屏蔽,不触发加载),而是直接按记住的图号/箱号强制重载, // 保证界面立即显示刚插入的数据。 try { if (!string.IsNullOrEmpty(记住图号)) { _currentTuhao = 记住图号; ReloadXianghaoByTuhao(_currentProjectId, 记住图号); if (!string.IsNullOrEmpty(记住箱号)) { ReloadYuanjianByXianghao(_currentProjectId, 记住图号, 记住箱号); } // ★ 关键:重载完成后最后校正当前箱号变量并高亮对应行, // 保证 箱号表高亮 与 元件表显示 完全一致(之前变量停在旧箱号,误导显示) _currentXianghao = 记住箱号; SelectRowByValue(this.advancedDataGridView9, "内部ID", 记住图号); if (!string.IsNullOrEmpty(记住箱号)) SelectRowByValue(this.advancedDataGridView4, "内部ID", 记住箱号); } } catch (Exception exRestore) { Log("[方案插入] 恢复选中视图异常: " + exRestore.Message); } string doneMsg = "已把 " + xhs.Count + " 个箱号插入到报价图号 [" + targetTuhao + "] 下。\n" + "元件行 " + totalYj + " 条。"; Log("[方案插入箱号] 成功: " + doneMsg); MessageBox.Show(this, doneMsg, "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { Log("[方案插入箱号] 失败: " + ex.Message); MessageBox.Show(this, "插入箱号失败:\n" + ex.Message, "插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 元件菜单:删除 / 插入 ---------- /// /// 方案页面 元件右键菜单「删除元件」: /// 从 fak.db 的当前 yj_M 表删除选中的行(可多选),刷新当前箱号的元件表。 /// private void FakYuanjian_Delete_Click(object sender, EventArgs e) { // 必须先选中图号+箱号(用于定位 yj_M 表) DataGridViewRow rowTuhao = GetFirstSelectedRow(this.advancedDataGridView图号); DataGridViewRow rowXianghao = GetFirstSelectedRow(this.advancedDataGridView箱号); if (rowTuhao == null || rowXianghao == null) { MessageBox.Show(this, "请先在方案图号/箱号表中选择图号和箱号,再删除元件。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string tuhao = GetCellValueAsString(rowTuhao, "图号"); string xianghao = GetCellValueAsString(rowXianghao, "箱号"); if (string.IsNullOrEmpty(tuhao) || string.IsNullOrEmpty(xianghao)) { MessageBox.Show(this, "图号或箱号为空,无法定位元件表。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var rows = GetAllSelectedRows(this.advancedDataGridView元件); if (rows.Count == 0) { MessageBox.Show(this, "请先在元件表中选择要删除的元件。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 收集要删除的序号(元件表通过 序号 定位行) var seqs = new System.Collections.Generic.List(); var seen = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string seq = GetCellValueAsString(r, "序号"); if (seen.Add(seq)) seqs.Add(seq); } if (MessageBox.Show(this, "确认删除选中的 " + seqs.Count + " 条元件?", "删除元件", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; string fakDbPath = 方案数据源路径(); if (!System.IO.File.Exists(fakDbPath)) { MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { // ★ 删除前快照方案库,支持右键「撤销删除」还原 PushUndoSnapshot(fakDbPath, "", true); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); string yjRealTbl = GetYuanjianTableByIndex(tuhao, xianghao, conn); if (string.IsNullOrEmpty(yjRealTbl)) { MessageBox.Show(this, "未找到图号 [" + tuhao + "] 箱号 [" + xianghao + "] 的元件表。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 按序号删除(序号唯一标识行) int deleted = 0; foreach (string seq in seqs) { string del = "DELETE FROM \"" + yjRealTbl + "\" WHERE 序号 = @seq;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(del, conn)) { cmd.Parameters.AddWithValue("@seq", seq); deleted += cmd.ExecuteNonQuery(); } } Log("[方案删除元件] 图号=" + tuhao + " 箱号=" + xianghao + " 删除 " + deleted + " 条(选中 " + seqs.Count + ")"); } ReloadFakYuanjian(tuhao, xianghao); MessageBox.Show(this, "已删除 " + seqs.Count + " 条元件。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { Log("[方案删除元件] 失败: " + ex.Message); MessageBox.Show(this, "删除元件失败:\n" + ex.Message, "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 方案页面 元件右键菜单「插入元件」: /// 把选中的元件行从 fak.db 插入到报价页面当前箱号(_currentTuhao + _currentXianghao)的 yj_M 表。 /// private void FakYuanjian_Insert_Click(object sender, EventArgs e) { Log("点击[方案插入元件到报价], _currentProjectId=[" + _currentProjectId + "] _currentTuhao=[" + _currentTuhao + "] _currentXianghao=[" + _currentXianghao + "]"); if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在报价页面双击选择一个项目,再插入元件。", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao)) { MessageBox.Show(this, "请先在报价页面选择目标图号和箱号,再插入元件。", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 先保存当前所有编辑(包括内存里新建的图号/箱号等),确保插入目标的箱号行已在数据库中存在, // 否则方案库插入元件时读不到这个箱号,元件表建不了、汇总也算不了。 SaveAllToDatabaseSilent(); // 方案页面当前图号+箱号(读取元件数据时要用) DataGridViewRow rowFakTuhao = GetFirstSelectedRow(this.advancedDataGridView图号); DataGridViewRow rowFakXianghao = GetFirstSelectedRow(this.advancedDataGridView箱号); if (rowFakTuhao == null || rowFakXianghao == null) { MessageBox.Show(this, "请先在方案图号/箱号表中选择图号和箱号,再插入元件。", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string fakTuhao = GetCellValueAsString(rowFakTuhao, "图号"); string fakXianghao = GetCellValueAsString(rowFakXianghao, "箱号"); if (string.IsNullOrEmpty(fakTuhao) || string.IsNullOrEmpty(fakXianghao)) { MessageBox.Show(this, "方案图号或箱号为空,无法读取元件数据。", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var rows = GetAllSelectedRows(this.advancedDataGridView元件); if (rows.Count == 0) { MessageBox.Show(this, "请先在方案元件表中选择要插入的元件。", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string srcDbPath = 方案数据源路径(); if (!System.IO.File.Exists(srcDbPath)) { MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 从 fak.db 读出选中的元件行(按序号) var seqs = new System.Collections.Generic.List(); var seenSeq = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string seq = GetCellValueAsString(r, "序号"); if (seenSeq.Add(seq)) seqs.Add(seq); } DataTable yjRows = new DataTable(); using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath))) { srcConn.Open(); string yjRealTblSrc = GetYuanjianTableByIndex(fakTuhao, fakXianghao, srcConn); if (string.IsNullOrEmpty(yjRealTblSrc)) { MessageBox.Show(this, "方案库里未找到图号 [" + fakTuhao + "] 箱号 [" + fakXianghao + "] 的元件表。", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 按序号读取选中的行 // SQLite 不方便做 IN(@p1,@p2,...) 动态参数,这里逐条读 foreach (string seq in seqs) { string selYj = "SELECT * FROM \"" + yjRealTblSrc + "\" WHERE 序号 = @seq LIMIT 1;"; using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn)) { cmdYj.Parameters.AddWithValue("@seq", seq); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj)) { DataTable tmp = new DataTable(); ad.Fill(tmp); // 合并到 yjRows(保证列结构一致) if (yjRows.Columns.Count == 0 && tmp.Columns.Count > 0) { foreach (DataColumn c in tmp.Columns) yjRows.Columns.Add(c.ColumnName, c.DataType); } foreach (DataRow r in tmp.Rows) yjRows.ImportRow(r); } } } } if (yjRows.Rows.Count == 0) { MessageBox.Show(this, "未能从方案库读取选中的元件数据。", "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } EnsureQuoteDbExists(_currentProjectId); // ============================================================ // ★ 参考 WebView元件库 / GCS母排 同款可行插入:直接写内存 _yuanjianTable, // 靠 DataGridView 绑定即时显示;保存走正常流程(切窗/关窗落库)。 // 之前"先写数据库再重载"在新建箱号/缓存未登记时会刷新不出来,现改为内存插入最可靠。 // ============================================================ string 记住图号 = _currentTuhao, 记住箱号 = _currentXianghao; // 元件表不存在(新建的箱号还没建表):走 InsertYuanjian 建表路径再插入 if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_yuanjianTable.TableName)) { _isReloading = true; try { SaveAllQuoteTablesIfDirty(false); if (string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao)) throw new Exception("请先选中图号和箱号。"); // ★ 建表不插占位行(InsertYuanjian 会插入"母排"垃圾行,源项目是直接建表) using (var yc = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId)))) { yc.Open(); string yTbl = LookupTableByIndex("元件", _currentTuhao, _currentXianghao, yc); if (string.IsNullOrEmpty(yTbl)) { using (var ytx = yc.BeginTransaction()) { int seq2 = GetFakMaxTableSeq(yc, ytx, "yj"); yTbl = "yj_" + (seq2 + 1).ToString(); CreateFakYuanjianTable(yTbl, yc, ytx); using (var ic = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件',@th,@xh,@t);", yc, ytx)) { ic.Parameters.AddWithValue("@th", _currentTuhao); ic.Parameters.AddWithValue("@xh", _currentXianghao); ic.Parameters.AddWithValue("@t", yTbl); ic.ExecuteNonQuery(); } ytx.Commit(); } } } string yjKey0 = _currentTuhao + "\n" + _currentXianghao; _yuanjianCache.Remove(yjKey0); ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao); } catch (Exception ex) { Log("[方案插入元件] 建表失败: " + ex.Message); MessageBox.Show(this, "插入元件失败:\n" + ex.Message, "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } finally { _currentTuhao = 记住图号; _currentXianghao = 记住箱号; _isReloading = false; } } if (_yuanjianTable == null) return; // 逐行把 fak 元件写进内存表(选中行下面插入),派生价按 Excel 公式现算 // ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入; // 单选=false 返回后走原逻辑 System.Action 插入动作 = delegate { this.advancedDataGridView5.EndEdit(); int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5); _isReloading = true; try { foreach (DataRow src in yjRows.Rows) { var row = _yuanjianTable.NewRow(); row["序号"] = "1"; // InsertRowBelowSelected 会自动重编号 row["元件名称"] = ColValue(src, "元件名称"); row["规格"] = ColValue(src, "规格"); row["单位"] = ColValue(src, "单位"); row["数量"] = ColValue(src, "数量"); row["表价"] = ColValue(src, "表价"); row["采购系数"] = ColValue(src, "采购系数"); row["报出系数"] = ColValue(src, "报出系数"); row["图纸规格"] = ColValue(src, "图纸规格"); row["品牌"] = ColValue(src, "品牌"); row["系列"] = ColValue(src, "系列"); row["类型"] = ColValue(src, "类型"); row["备注"] = ColValue(src, "备注"); // 派生价按公式现算(与 RecalcYuanjianRow 一致) decimal bj = TryParseDecimalOrZero(ColValue(src, "表价")); decimal xs = TryParseDecimalOrZero(ColValue(src, "采购系数")); if (xs == 0m) xs = 1m; decimal bcs = TryParseDecimalOrZero(ColValue(src, "报出系数")); if (bcs == 0m) bcs = 1m; decimal sl = TryParseDecimalOrZero(ColValue(src, "数量")); decimal cgdj = bj * xs; row["采购单价"] = (double)cgdj; row["采购价"] = (double)(cgdj * sl); row["报出单价"] = (double)(cgdj * bcs); row["报出价"] = (double)(cgdj * bcs * sl); InsertRowBelowSelected(_yuanjianTable, row, selectedIndex); Log("[方案插入元件] 已插入: " + ColValue(src, "元件名称") + " 表价=" + bj); } } catch (Exception ex) { Log("[方案插入元件] 内存插入异常: " + ex.Message); throw; } finally { _isReloading = false; } // ★ 虚拟模式收尾:同步网格行数+全表保存(循环里插的行才会显示并持久化) 元件表插行收尾(); // 滚动到最后插入的行 try { if (_yuanjianTable.Rows.Count > 0) this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = _yuanjianTable.Rows.Count - 1; } catch { } // 重算箱号/图号/项目聚合并刷新显示(与 WebView/GCS 插入一致);保存走正常流程 重算聚合(); ApplyPriceFormat(this.advancedDataGridView5, _单台小数位文本); }; if (工具窗口批量插入(插入动作)) { _currentTuhao = 记住图号; _currentXianghao = 记住箱号; return; } 插入动作(); _currentTuhao = 记住图号; _currentXianghao = 记住箱号; string doneMsg = "已把 " + yjRows.Rows.Count + " 条元件插入到报价图号 [" + 记住图号 + "] 箱号 [" + 记住箱号 + "] 下。"; Log("[方案插入元件] 成功: " + doneMsg); MessageBox.Show(this, doneMsg, "插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information); } /// /// 确保方案库 fak.db 存在,并创建图号表、表索引、项目信息三张固定表。 /// 数据结构与项目报价库完全一致: /// - 图号表 advancedDataGridView9Table(不用索引,直接按图号查找) /// - 表索引(类型/图号/箱号/实际表名),箱号表 xh_N、元件表 yj_N 通过它映射 /// - 项目信息表(沿用项目库结构) /// 箱号表/元件表不在固定建表之列,它们在「加入方案库」时按 xh_N/yj_M 动态创建。 /// private void EnsureFakDbExists(string fakDbPath) { // ★ 空路径防御:方案窗口没打开方案时 fakDbPath 为空,CreateDirectory("")/空连接串会裸抛 if (string.IsNullOrWhiteSpace(fakDbPath)) throw new Exception("方案库路径为空(方案窗口未打开方案)"); // 确保目录存在 string dir = System.IO.Path.GetDirectoryName(fakDbPath); if (!System.IO.Directory.Exists(dir)) { System.IO.Directory.CreateDirectory(dir); } using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath))) { conn.Open(); // 图号表(与项目库图号表结构一致,含内部ID列) string sqlTh = "CREATE TABLE IF NOT EXISTS " + FakTuhaoTable + " (" + " 序号 TEXT, 图号 TEXT, 图名 TEXT, 台数 TEXT, 箱柜数量 REAL," + " 单价 REAL, 总价 REAL, 成本总价 REAL, 毛利 REAL, 毛利率 REAL," + " 内部ID TEXT" + ");"; // ★ 老库兼容:CREATE TABLE IF NOT EXISTS 不加列,需 ALTER TABLE 补内部ID try { using (var checkCmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM pragma_table_info('" + FakTuhaoTable + "') WHERE name='内部ID';", conn)) { if (Convert.ToInt32(checkCmd.ExecuteScalar()) == 0) { using (var alterCmd = new System.Data.SQLite.SQLiteCommand( "ALTER TABLE " + FakTuhaoTable + " ADD COLUMN \"内部ID\" TEXT;", conn)) alterCmd.ExecuteNonQuery(); } } } catch { /* 表刚建好已有内部ID,或 ALTER 失败(列已存在),忽略 */ } // 表索引(与项目库结构完全一致) string sqlIndex = "CREATE TABLE IF NOT EXISTS 表索引 (" + " 类型 TEXT, 图号 TEXT, 箱号 TEXT, 实际表名 TEXT" + ");"; // 项目信息表(与项目库结构一致) string sqlProjectInfo = "CREATE TABLE IF NOT EXISTS " + ProjectInfoTableName + " (" + " 项目编号 TEXT, 项目名称 TEXT, 客户名称 TEXT, 客户地址 TEXT," + " 联系人 TEXT, 联系电话 TEXT, 项目日期 TEXT, 备注 TEXT," + " 成套厂名称 TEXT, 成套厂地址 TEXT, 成套厂联系人 TEXT, 成套厂联系电话 TEXT," + " 单台小数位 TEXT, 单台取整方式 TEXT, 项目小数位 TEXT, 项目取整方式 TEXT" + ");"; foreach (string sql in new[] { sqlTh, sqlIndex, sqlProjectInfo }) { using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.ExecuteNonQuery(); } } // ★ B1(2026-10-01):方案库表索引同样建复合索引(方案窗口查表索引高频,此前全表扫描) try { using (var idxCmd = new System.Data.SQLite.SQLiteCommand( "CREATE INDEX IF NOT EXISTS idx_表索引_类型图号箱号 ON 表索引(类型, 图号, 箱号);", conn)) { idxCmd.ExecuteNonQuery(); } } catch { } // ★ B3(2026-10-01):方案库数据表同样补"内部ID"索引(幂等) try { 确保数据表性能索引(conn); } catch { } } } /// /// 查 fak.db 表索引里指定前缀(xh 或 yj)的实际表名最大序号。 /// 用于生成下一个不冲突的 xh_N / yj_M 表名。 /// 返回最大序号;表索引为空或无匹配时返回 0。 /// private int GetFakMaxTableSeq(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string prefix) { if (conn == null || string.IsNullOrEmpty(prefix)) return 0; int maxN = 0; // 1) 从「表索引」表查最大序号 string sql1 = "SELECT 实际表名 FROM 表索引 WHERE 实际表名 LIKE @p;"; using (var cmd1 = new System.Data.SQLite.SQLiteCommand(sql1, conn, tx)) { cmd1.Parameters.AddWithValue("@p", prefix + "_%"); using (var reader = cmd1.ExecuteReader()) { while (reader.Read()) { string t = reader.IsDBNull(0) ? "" : reader.GetString(0); string numPart = t.StartsWith(prefix + "_") ? t.Substring(prefix.Length + 1) : ""; int n; if (int.TryParse(numPart, out n) && n > maxN) maxN = n; } } } // 2) ★ 同时从 sqlite_master 查实际存在的物理表的最大序号 // 防止"表索引被清空/部分清理,但物理表遗留"导致新表名与老表冲突 // (例如先 DROP 表索引里的记录但忘了 DROP 物理表,下次算序号会从 1 开始,冲突) string sql2 = "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE @p;"; using (var cmd2 = new System.Data.SQLite.SQLiteCommand(sql2, conn, tx)) { cmd2.Parameters.AddWithValue("@p", prefix + "_%"); using (var reader = cmd2.ExecuteReader()) { while (reader.Read()) { string t = reader.IsDBNull(0) ? "" : reader.GetString(0); string numPart = t.StartsWith(prefix + "_") ? t.Substring(prefix.Length + 1) : ""; int n; if (int.TryParse(numPart, out n) && n > maxN) maxN = n; } } } return maxN; } /// /// 在 fak.db 里建一张空箱号表(真实表名为 xh_N)。 /// 列结构与项目库箱号表完全一致(无「图号」列)。 /// private void CreateFakXianghaoTable(string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string sql = "CREATE TABLE \"" + realTableName + "\" (" + " 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," + " 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," + " 内部ID TEXT" + ");"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { cmd.ExecuteNonQuery(); } } /// /// 在 fak.db 里建一张空元件表(真实表名为 yj_M)。 /// 列结构与项目库元件表完全一致(无「图号」「箱号」列)。 /// private void CreateFakYuanjianTable(string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string sql = "CREATE TABLE \"" + realTableName + "\" (" + " 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," + " 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," + " 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT" + ");"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { cmd.ExecuteNonQuery(); } } /// /// 把一条箱号源行 INSERT 到 fak.db 的 xh_N 表。 /// 列名与项目库箱号表完全一致(无额外「图号」列)。 /// private void InsertFakXianghaoRow(DataRow src, string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string sql = "INSERT INTO \"" + realTableName + "\"" + " (序号, 箱号, 箱名, 型号, 规格, 单位, 数量," + " 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 箱柜类型, 备注, 图纸, 内部ID) " + "VALUES (@序号, @箱号, @箱名, @型号, @规格, @单位, @数量," + " @单台成本, @成本价, @单台报出, @报出价, @利润, @利润率, @毛利率, @箱柜类型, @备注, @图纸, @内部ID);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { cmd.Parameters.AddWithValue("@序号", ColValue(src, "序号")); cmd.Parameters.AddWithValue("@箱号", ColValue(src, "箱号")); cmd.Parameters.AddWithValue("@箱名", ColValue(src, "箱名")); cmd.Parameters.AddWithValue("@型号", ColValue(src, "型号")); cmd.Parameters.AddWithValue("@规格", ColValue(src, "规格")); cmd.Parameters.AddWithValue("@单位", ColValue(src, "单位")); cmd.Parameters.AddWithValue("@数量", ColValue(src, "数量")); AddNumericParam(cmd, "@单台成本", src, "单台成本"); AddNumericParam(cmd, "@成本价", src, "成本价"); AddNumericParam(cmd, "@单台报出", src, "单台报出"); AddNumericParam(cmd, "@报出价", src, "报出价"); AddNumericParam(cmd, "@利润", src, "利润"); AddNumericParam(cmd, "@利润率", src, "利润率"); AddNumericParam(cmd, "@毛利率", src, "毛利率"); cmd.Parameters.AddWithValue("@箱柜类型", ColValue(src, "箱柜类型")); cmd.Parameters.AddWithValue("@备注", ColValue(src, "备注")); cmd.Parameters.AddWithValue("@图纸", ColValue(src, "图纸")); cmd.Parameters.AddWithValue("@内部ID", ColValue(src, "内部ID")); cmd.ExecuteNonQuery(); } } /// /// 把一条元件源行 INSERT 到 fak.db 的 yj_M 表。 /// 列名与项目库元件表完全一致(无额外「图号」「箱号」列)。 /// /// 按元件表 Excel 公式重算整表派生价列(与 RecalcYuanjianRow 公式一致): /// 采购单价=表价×采购系数;采购价=采购单价×数量;报出单价=采购单价×报出系数;报出价=报出单价×数量。 /// 方案库插入的元件行带的是源表价格,插入后统一按公式重算,保证与手工编辑行为一致。 private void 重算元件表派生价(string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string sql = "UPDATE \"" + realTableName + "\" SET " + " 采购单价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL)," + " 采购价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL) * CAST(数量 AS REAL)," + " 报出单价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL) * CAST(报出系数 AS REAL)," + " 报出价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL) * CAST(报出系数 AS REAL) * CAST(数量 AS REAL)" + " WHERE 表价 IS NOT NULL AND 采购系数 IS NOT NULL AND 采购系数 != '' AND 数量 IS NOT NULL AND 数量 != '';"; try { using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { int rows = cmd.ExecuteNonQuery(); Log("[方案插入] 已重算元件派生价: 表=" + realTableName + " 行数=" + rows); } } catch (Exception ex) { Log("[方案插入] 重算元件派生价失败: " + ex.Message); } } private void InsertFakYuanjianRow(DataRow src, string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string sql = "INSERT INTO \"" + realTableName + "\"" + " (序号, 元件名称, 规格, 单位, 数量," + " 表价, 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," + " 图纸规格, 品牌, 系列, 类型, 备注) " + "VALUES (@序号, @元件名称, @规格, @单位, @数量," + " @表价, @采购单价, @采购系数, @采购价, @报出单价, @报出系数, @报出价," + " @图纸规格, @品牌, @系列, @类型, @备注);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { cmd.Parameters.AddWithValue("@序号", ColValue(src, "序号")); cmd.Parameters.AddWithValue("@元件名称", ColValue(src, "元件名称")); cmd.Parameters.AddWithValue("@规格", ColValue(src, "规格")); cmd.Parameters.AddWithValue("@单位", ColValue(src, "单位")); cmd.Parameters.AddWithValue("@数量", ColValue(src, "数量")); AddNumericParam(cmd, "@表价", src, "表价"); AddNumericParam(cmd, "@采购单价", src, "采购单价"); cmd.Parameters.AddWithValue("@采购系数", ColValue(src, "采购系数")); AddNumericParam(cmd, "@采购价", src, "采购价"); AddNumericParam(cmd, "@报出单价", src, "报出单价"); cmd.Parameters.AddWithValue("@报出系数", ColValue(src, "报出系数")); AddNumericParam(cmd, "@报出价", src, "报出价"); cmd.Parameters.AddWithValue("@图纸规格", ColValue(src, "图纸规格")); cmd.Parameters.AddWithValue("@品牌", ColValue(src, "品牌")); cmd.Parameters.AddWithValue("@系列", ColValue(src, "系列")); cmd.Parameters.AddWithValue("@类型", ColValue(src, "类型")); cmd.Parameters.AddWithValue("@备注", ColValue(src, "备注")); cmd.ExecuteNonQuery(); } } /// InsertFakYuanjianRow 的字典版(虚拟模式选中行用 CopyRowFields 取得)。 private void InsertFakYuanjianRowDict(System.Collections.Generic.Dictionary src, string realTableName, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string sql = "INSERT INTO \"" + realTableName + "\"" + " (序号, 元件名称, 规格, 单位, 数量," + " 表价, 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," + " 图纸规格, 品牌, 系列, 类型, 备注) " + "VALUES (@序号, @元件名称, @规格, @单位, @数量," + " @表价, @采购单价, @采购系数, @采购价, @报出单价, @报出系数, @报出价," + " @图纸规格, @品牌, @系列, @类型, @备注);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { cmd.Parameters.AddWithValue("@序号", DictValue(src, "序号")); cmd.Parameters.AddWithValue("@元件名称", DictValue(src, "元件名称")); cmd.Parameters.AddWithValue("@规格", DictValue(src, "规格")); cmd.Parameters.AddWithValue("@单位", DictValue(src, "单位")); cmd.Parameters.AddWithValue("@数量", DictValue(src, "数量")); AddNumericParamDict(cmd, "@表价", src, "表价"); AddNumericParamDict(cmd, "@采购单价", src, "采购单价"); cmd.Parameters.AddWithValue("@采购系数", DictValue(src, "采购系数")); AddNumericParamDict(cmd, "@采购价", src, "采购价"); AddNumericParamDict(cmd, "@报出单价", src, "报出单价"); cmd.Parameters.AddWithValue("@报出系数", DictValue(src, "报出系数")); AddNumericParamDict(cmd, "@报出价", src, "报出价"); cmd.Parameters.AddWithValue("@图纸规格", DictValue(src, "图纸规格")); cmd.Parameters.AddWithValue("@品牌", DictValue(src, "品牌")); cmd.Parameters.AddWithValue("@系列", DictValue(src, "系列")); cmd.Parameters.AddWithValue("@类型", DictValue(src, "类型")); cmd.Parameters.AddWithValue("@备注", DictValue(src, "备注")); cmd.ExecuteNonQuery(); } } /// 安全取字典某列的字符串值(键不存在或 null 返回空串)。 private static string DictValue(System.Collections.Generic.Dictionary src, string key) { if (src == null || string.IsNullOrEmpty(key)) return ""; object v; if (!src.TryGetValue(key, out v) || v == null || v == DBNull.Value) return ""; return v.ToString(); } /// 字典版数值参数(无法解析为 double 时写 0)。 private static void AddNumericParamDict(System.Data.SQLite.SQLiteCommand cmd, string paramName, System.Collections.Generic.Dictionary src, string key) { string s = DictValue(src, key); double d; if (!double.TryParse(s, out d)) d = 0; cmd.Parameters.AddWithValue(paramName, d); } /// 按指定列名列表,把源行的对应列 INSERT 到目标表(只插文本列,数值列也按文本写)。 private void InsertRowByColumns(DataRow src, string dstTableName, string[] cols, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { // 构造 INSERT INTO dstTableName (c1, c2, ...) VALUES (@c1, @c2, ...) string colList = string.Join(", ", cols); var paramNames = new System.Collections.Generic.List(cols.Length); for (int i = 0; i < cols.Length; i++) paramNames.Add("@" + cols[i]); string paramList = string.Join(", ", paramNames.ToArray()); string sql = "INSERT INTO " + dstTableName + " (" + colList + ") VALUES (" + paramList + ");"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { foreach (string c in cols) { cmd.Parameters.AddWithValue("@" + c, ColValue(src, c)); } cmd.ExecuteNonQuery(); } } /// 安全取 DataRow 某列的字符串值(列不存在或 DBNull 返回空串)。 /// 从箱号行读内部ID;表里没有该列或值为空时退回"箱号"显示名。 /// 方案库(fak.db)的箱号表没有"内部ID"列,内部ID就用箱号名。 /// 箱号表右键「加入方案库」:把选中的箱号(可多选,含各自元件表)复制到方案库。 /// 若方案库还没有所属图号行,自动从项目库补齐图号行。 private void MenuXianghao_AddToFak_Click(object sender, EventArgs e) { Log("点击[箱号加入方案库], 项目=[" + _currentProjectId + "] 图号=[" + _currentTuhao + "]"); if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao)) { MessageBox.Show(this, "请先打开项目并选择要加入方案库的箱柜。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 多选:收集全部选中箱号(内部ID,去重) var rows = GetAllSelectedRows(this.advancedDataGridView4); var 箱号们 = new System.Collections.Generic.List(); var seenId = new System.Collections.Generic.HashSet(); foreach (var r in rows) { string id = GetBoundValueAsString(r, "内部ID"); if (!string.IsNullOrEmpty(id) && seenId.Add(id)) 箱号们.Add(id); } if (箱号们.Count == 0) { MessageBox.Show(this, "请先在箱号表中选择要加入方案库的箱柜(可按 Ctrl 多选)。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { SaveAllQuoteTablesIfDirty(false); } catch (Exception exSave) { Log("[箱柜加入方案库] 预保存失败(继续): " + exSave.Message); } string srcDbPath = GetQuoteDbPath(_currentProjectId); string fakDbPath = 方案数据源路径(); // ★ 方案窗口没打开方案时给出明确引导(旧逻辑空路径直接炸) if (string.IsNullOrWhiteSpace(fakDbPath) || !System.IO.File.Exists(fakDbPath)) { MessageBox.Show(this, "方案窗口当前没有打开方案,无法加入。\n\n请先在右侧方案窗口的项目页双击打开一个方案\n(或用项目表右键\"加入方案库\"把项目存为新方案),再加入。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { int xhCount = 0, yjCount = 0; using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath))) using (var dstConn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath))) { srcConn.Open(); dstConn.Open(); EnsureFakDbExists(fakDbPath); // ★ 方案文件由整库复制而来,可能带旧架构遗留触发器(引用已退役的固定表"箱号")——写入前必清 计算引擎.清理遗留触发器(dstConn); foreach (string xhId in 箱号们) { using (var tx = dstConn.BeginTransaction()) { try { // (a) 确保方案库有该图号行(无则从项目库复制) int 图号行数 = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM " + FakTuhaoTable + " WHERE 内部ID = @th;", dstConn, tx)) { // ★ 用图号内部ID查图号表(之前误传箱号内部ID,恒为0 → 每次重复补插图号行) cmd.Parameters.AddWithValue("@th", _currentTuhao); 图号行数 = Convert.ToInt32(cmd.ExecuteScalar()); } // 图号行按"内部ID"判断:箱号加入时若方案库没有该图号,先复制图号行 // (图号行复制走 复制图号到方案库 会连带全部箱号——这里只补图号行本身) if (图号行数 == 0) { DataTable thRow = new DataTable(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT * FROM " + TuhaoTableName + " WHERE 内部ID = @th LIMIT 1;", srcConn)) { cmd.Parameters.AddWithValue("@th", _currentTuhao); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) ad.Fill(thRow); } var thCols = new List(); foreach (DataColumn c in thRow.Columns) thCols.Add(c.ColumnName); if (thRow.Rows.Count > 0) InsertRowByColumns(thRow.Rows[0], FakTuhaoTable, thCols.ToArray(), dstConn, tx); } // (b) 读项目库该箱号行 + 元件行 DataTable 箱号行表 = new DataTable(); string xhReal = GetXianghaoTableByIndex(_currentTuhao, srcConn); if (!string.IsNullOrEmpty(xhReal)) using (var cmd2 = new System.Data.SQLite.SQLiteCommand( "SELECT * FROM \"" + xhReal + "\" WHERE 内部ID = @xh LIMIT 1;", srcConn)) { cmd2.Parameters.AddWithValue("@xh", xhId); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd2)) ad.Fill(箱号行表); } if (箱号行表.Rows.Count == 0) { tx.Rollback(); continue; } DataRow 箱号行 = 箱号行表.Rows[0]; string 箱号显示名 = 箱号行表.Columns.Contains("箱号") && 箱号行["箱号"] != DBNull.Value ? 箱号行["箱号"].ToString() : xhId; string yjReal = GetYuanjianTableByIndex(_currentTuhao, xhId, srcConn); DataTable 元件行表 = new DataTable(); if (!string.IsNullOrEmpty(yjReal)) using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT * FROM \"" + yjReal + "\" ORDER BY CAST(序号 AS INTEGER);", srcConn)) using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) ad.Fill(元件行表); // (c) fak 箱号表:已有则直接插行,无则建表+登记索引 string fakXhTbl = GetXianghaoTableByIndex(_currentTuhao, dstConn); if (string.IsNullOrEmpty(fakXhTbl)) { int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh") + 1; fakXhTbl = "xh_" + xhSeq.ToString(); CreateFakXianghaoTable(fakXhTbl, dstConn, tx); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('箱号', @th, '', @real);", dstConn, tx)) { cmd.Parameters.AddWithValue("@th", _currentTuhao); cmd.Parameters.AddWithValue("@real", fakXhTbl); cmd.ExecuteNonQuery(); } } InsertFakXianghaoRow(箱号行, fakXhTbl, dstConn, tx); xhCount++; // (d) fak 元件表:每箱号新建一张 yj 表 + 登记索引 int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj") + 1; string fakYjTbl = "yj_" + yjSeq.ToString(); CreateFakYuanjianTable(fakYjTbl, dstConn, tx); foreach (DataRow r in 元件行表.Rows) InsertFakYuanjianRow(r, fakYjTbl, dstConn, tx); yjCount += 元件行表.Rows.Count; using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", dstConn, tx)) { cmd.Parameters.AddWithValue("@th", _currentTuhao); cmd.Parameters.AddWithValue("@xh", 箱号显示名); cmd.Parameters.AddWithValue("@real", fakYjTbl); cmd.ExecuteNonQuery(); } tx.Commit(); } catch { try { tx.Rollback(); } catch { } throw; } } } } Log("[箱柜加入方案库] 完成: 箱柜=" + xhCount + " 元件行=" + yjCount); ReloadFakData(); MessageBox.Show(this, "已把 " + xhCount + " 个箱柜(含元件 " + yjCount + " 行)加入方案库。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { Log("[箱柜加入方案库] 失败: " + ex.Message); MessageBox.Show(this, "加入方案库失败:\n" + ex.Message, "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// 元件表右键「加入方案库」:把选中的元件(可多选)追加到方案库同名方案的元件表。 /// 若方案库还没有该图号/箱号,自动补齐图号行与箱号行。 private void MenuYuanjian_AddToFak_Click(object sender, EventArgs e) { Log("点击[元件加入方案库], 项目=[" + _currentProjectId + "] 图号=[" + _currentTuhao + "] 箱号=[" + _currentXianghao + "]"); if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao)) { MessageBox.Show(this, "请先打开项目并选择要加入方案库的元件。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 多选:收集选中元件行(虚拟模式无 DataBoundItem,用 CopyRowFields 读内存表) var rows = GetAllSelectedRows(this.advancedDataGridView5); var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "_rowid_" }; var 元件DataRows = new System.Collections.Generic.List>(); foreach (var r in rows) { var dict = CopyRowFields(r, exclude); if (dict != null && dict.Count > 0) 元件DataRows.Add(dict); } if (元件DataRows.Count == 0) { MessageBox.Show(this, "请先在元件表中选择要加入方案库的元件(可按 Ctrl 多选)。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { SaveAllQuoteTablesIfDirty(false); } catch (Exception exSave) { Log("[元件加入方案库] 预保存失败(继续): " + exSave.Message); } string srcDbPath = GetQuoteDbPath(_currentProjectId); string fakDbPath = 方案数据源路径(); // ★ 方案窗口没打开方案时给出明确引导(旧逻辑空路径直接炸) if (string.IsNullOrWhiteSpace(fakDbPath) || !System.IO.File.Exists(fakDbPath)) { MessageBox.Show(this, "方案窗口当前没有打开方案,无法加入。\n\n请先在右侧方案窗口的项目页双击打开一个方案\n(或用项目表右键\"加入方案库\"把项目存为新方案),再加入。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 插入目标:优先方案窗口报价页当前选中的图号/箱号——用户在方案里选中哪个箱号, // 元件就插到那个箱号的元件表(fallback 项目库当前图号/箱号) string 目标图号 = (this.w8 != null) ? this.w8.方案选中图号 : ""; string 目标箱号 = (this.w8 != null) ? this.w8.方案选中箱号ID : ""; bool 用方案选中 = !string.IsNullOrEmpty(目标图号) && !string.IsNullOrEmpty(目标箱号); if (!用方案选中) { 目标图号 = _currentTuhao; 目标箱号 = _currentXianghao; } Log("[元件加入方案库] 插入目标: 图号=[" + 目标图号 + "] 箱号=[" + 目标箱号 + "]" + (用方案选中 ? "(方案窗口选中)" : "(项目当前)")); try { int yjCount = 0; using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath))) using (var dstConn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath))) { srcConn.Open(); dstConn.Open(); EnsureFakDbExists(fakDbPath); // ★ 方案文件由整库复制而来,可能带旧架构遗留触发器(引用已退役的固定表"箱号")——写入前必清 计算引擎.清理遗留触发器(dstConn); using (var tx = dstConn.BeginTransaction()) { try { // (a) 确保方案库有图号行 int 图号行数 = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM " + FakTuhaoTable + " WHERE 内部ID = @th OR 图号 = @th;", dstConn, tx)) { cmd.Parameters.AddWithValue("@th", 目标图号); 图号行数 = Convert.ToInt32(cmd.ExecuteScalar()); } if (图号行数 == 0) { DataTable thRow = new DataTable(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT * FROM " + TuhaoTableName + " WHERE 内部ID = @th LIMIT 1;", srcConn)) { cmd.Parameters.AddWithValue("@th", _currentTuhao); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) ad.Fill(thRow); } var thCols = new List(); foreach (DataColumn c in thRow.Columns) thCols.Add(c.ColumnName); if (thRow.Rows.Count > 0) InsertRowByColumns(thRow.Rows[0], FakTuhaoTable, thCols.ToArray(), dstConn, tx); } // (b) 确保方案库有箱号行(方案库没有该图号/箱号时,才从项目库补) string fakXhTbl = GetXianghaoTableByIndex(目标图号, dstConn); if (string.IsNullOrEmpty(fakXhTbl)) { int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh") + 1; fakXhTbl = "xh_" + xhSeq.ToString(); CreateFakXianghaoTable(fakXhTbl, dstConn, tx); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('箱号', @th, '', @real);", dstConn, tx)) { cmd.Parameters.AddWithValue("@th", 目标图号); cmd.Parameters.AddWithValue("@real", fakXhTbl); cmd.ExecuteNonQuery(); } } int 箱号行数 = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM \"" + fakXhTbl + "\" WHERE 内部ID = @xh OR 箱号 = @xh;", dstConn, tx)) { cmd.Parameters.AddWithValue("@xh", 目标箱号); 箱号行数 = Convert.ToInt32(cmd.ExecuteScalar()); } if (箱号行数 == 0) { string srcXhTbl = GetXianghaoTableByIndex(_currentTuhao, srcConn); if (!string.IsNullOrEmpty(srcXhTbl)) using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT * FROM \"" + srcXhTbl + "\" WHERE 内部ID = @xh LIMIT 1;", srcConn)) { cmd.Parameters.AddWithValue("@xh", 目标箱号); using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) { var t = new DataTable(); ad.Fill(t); foreach (DataRow r in t.Rows) InsertFakXianghaoRow(r, fakXhTbl, dstConn, tx); } } } // (c) 方案库元件表:有则追加,无则建表+登记 string fakYjTbl = GetYuanjianTableByIndex(目标图号, 目标箱号, dstConn); if (string.IsNullOrEmpty(fakYjTbl)) { int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj") + 1; fakYjTbl = "yj_" + yjSeq.ToString(); CreateFakYuanjianTable(fakYjTbl, dstConn, tx); using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", dstConn, tx)) { cmd.Parameters.AddWithValue("@th", 目标图号); cmd.Parameters.AddWithValue("@xh", 目标箱号); cmd.Parameters.AddWithValue("@real", fakYjTbl); cmd.ExecuteNonQuery(); } } foreach (var r in 元件DataRows) InsertFakYuanjianRowDict(r, fakYjTbl, dstConn, tx); yjCount = 元件DataRows.Count; tx.Commit(); } catch { try { tx.Rollback(); } catch { } throw; } } } Log("[元件加入方案库] 完成: 元件行=" + yjCount); ReloadFakData(); // ★ 通知方案窗口刷新 MessageBox.Show(this, "已把 " + yjCount + " 行元件加入方案库。", "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { Log("[元件加入方案库] 失败: " + ex.Message); MessageBox.Show(this, "加入方案库失败:\n" + ex.Message, "加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private static string FakRead内部ID(DataRow xhRow) { if (xhRow.Table.Columns.Contains("内部ID")) { object v = xhRow["内部ID"]; if (v != DBNull.Value && !string.IsNullOrWhiteSpace(v.ToString())) return v.ToString(); } if (xhRow.Table.Columns.Contains("箱号")) { object n2 = xhRow["箱号"]; if (n2 != DBNull.Value) return n2.ToString(); } return ""; } private static string ColValue(DataRow row, string colName) { if (row == null || string.IsNullOrEmpty(colName)) return ""; if (!row.Table.Columns.Contains(colName)) return ""; object v = row[colName]; if (v == null || v == DBNull.Value) return ""; return v.ToString(); } /// /// 把源行某列作为数值参数加入命令(REAL 列)。 /// 无法解析为 double 时写 0,避免类型不匹配。 /// private static void AddNumericParam(System.Data.SQLite.SQLiteCommand cmd, string paramName, DataRow row, string colName) { string s = ColValue(row, colName); double d; if (!double.TryParse(s, out d)) d = 0; cmd.Parameters.AddWithValue(paramName, d); } // ---------- 箱号菜单事件 ---------- private void MenuXianghao_New_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao)) { MessageBox.Show(this, "请先选择图号,再新建箱号。", "新建箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 不弹输入框,直接用默认名(新箱号N)创建 // 序号按当前图号的独立箱号表计数 // 记住当前图号,新建完成后恢复(防止 ADGV 选中抖动) string rememberTuhao = _currentTuhao; // ★ 记住选中行索引,用于"在选中行下面插入" int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView4); string newXh = "新箱号" + GetXianghaoCount(_currentProjectId, _currentTuhao); try { // 整个新建流程用 _isReloading 包裹,挡住所有 SelectionChanged 联动 _isReloading = true; // 新建前先保存当前三个表的所有编辑,避免用户编辑丢失 // 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。 // 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。 SaveAllQuoteTablesIfDirty(false); InsertXianghao(_currentProjectId, _currentTuhao, newXh); // ★ 清除该图号的箱号表缓存,强制下次 Reload 从库读(看到刚 INSERT 的新箱号) if (!string.IsNullOrEmpty(rememberTuhao)) _xianghaoCache.Remove(rememberTuhao); ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao); // ★ 把新行(在表末尾)移动到选中行下面 if (_xianghaoTable != null) MoveLastRowBelowSelected(this.advancedDataGridView4, _xianghaoTable, selectedIndex, "\"" + _xianghaoTable.TableName + "\"", _currentProjectId); // 新建后重算当前层并刷新显示 RecalcAllAfterReload(); } catch (Exception ex) { MessageBox.Show(this, "新建箱号失败:\n" + ex.Message, "新建箱号", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 恢复图号选中,并选中新箱号(新建的箱号会在表末尾) _currentTuhao = rememberTuhao; _currentXianghao = newXh; SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao); SelectRowByValue(this.advancedDataGridView4, "内部ID", newXh); _isReloading = false; // ★ 新建箱号后,元件表必须跳到这个新箱号(显示它里面的元件,新建的箱号默认空) // SelectRowByValue 在 guard 里没触发 SelectionChanged 联动,这里显式刷新。 ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, newXh); } } private void MenuXianghao_Delete_Click(object sender, EventArgs e) { // 取所有选中行(支持批量多选) var rows = GetAllSelectedRows(this.advancedDataGridView4); Log("删除箱号: GetAllSelectedRows 行数=" + (rows == null ? "null" : rows.Count.ToString()) + " SelectedCells=" + (this.advancedDataGridView4 == null ? "null" : this.advancedDataGridView4.SelectedCells.Count.ToString()) + " SelectedRows=" + (this.advancedDataGridView4 == null ? "null" : this.advancedDataGridView4.SelectedRows.Count.ToString())); if (rows == null || rows.Count == 0) { MessageBox.Show(this, "请先选择要删除的箱号。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 收集所有选中行的箱号内部ID(去重) ★ 用内部ID,用户改名不影响删除 var xhs = new System.Collections.Generic.List(); // ★ 同时记显示名(删除时按显示名兜底清扒图 Form3 登记的孤儿,2026-09-21 残留根治) var xhNames = new System.Collections.Generic.List(); foreach (var r in rows) { string xh = GetBoundValueAsString(r, "内部ID"); string nm = GetBoundValueAsString(r, "箱号"); if (!string.IsNullOrWhiteSpace(xh) && !xhs.Contains(xh)) { xhs.Add(xh); xhNames.Add(nm ?? ""); } } if (xhs.Count == 0) { MessageBox.Show(this, "选中行没有箱号,无法删除。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 记住当前图号,删除后恢复选中 string rememberTuhao = _currentTuhao; // 不弹确认框,直接删除 try { // 整个删除流程用 _isReloading 包裹,挡住选中抖动 _isReloading = true; // 删除前先保存当前三个表的所有编辑 // 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。 // 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。 SaveAllQuoteTablesIfDirty(false); // ★ 删除前快照整库,支持右键「撤销删除」还原 PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false); // ★ 删除管线放后台线程:单连接+单事务 string pid = _currentProjectId, th = _currentTuhao; // 当前图号显示名(UI线程解析,兜底清理用) string thNm = th; try { if (_tuhaoTable != null) { foreach (DataRow dr in _tuhaoTable.Rows) { if (Convert.ToString(dr["内部ID"]) == th) { thNm = Convert.ToString(dr["图号"]); break; } } } } catch { } var sw = System.Diagnostics.Stopwatch.StartNew(); Cursor = Cursors.WaitCursor; SetStatus("正在删除 " + xhs.Count + " 个箱号..."); Log("[删除箱号] 开始,共 " + xhs.Count + " 个箱号..."); System.Threading.Tasks.Task.Run(() => { Exception 错 = null; try { string dbPath = GetQuoteDbPath(pid); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { for (int xi = 0; xi < xhs.Count; xi++) { string xh = xhs[xi]; string xhNm = xi < xhNames.Count ? xhNames[xi] : ""; DeleteXianghaoTx(pid, th, thNm, xh, xhNm, conn, tx); _yuanjianCache.Remove(th + "\n" + xh); if (!string.IsNullOrEmpty(xhNm)) _yuanjianCache.Remove(th + "\n" + xhNm); } tx.Commit(); } } } catch (Exception exBg) { 错 = exBg; } BeginInvoke((MethodInvoker)delegate { try { _isReloading = true; if (错 == null) { string xhTbl = GetXianghaoTableByIndex(pid, th); if (!string.IsNullOrEmpty(xhTbl)) RenumberTable(pid, "\"" + xhTbl + "\""); _xianghaoCache.Remove(th); } ReloadXianghaoByTuhao(pid, th); RecalcAllAfterReload(); sw.Stop(); Cursor = Cursors.Default; if (错 != null) { Log("[删除箱号] 后台异常,完整堆栈: " + 错.ToString()); MessageBox.Show(this, "删除箱号失败:\n" + 错.Message, "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } SetStatus("删除完成 " + xhs.Count + " 个箱号, 耗时 " + sw.ElapsedMilliseconds + "ms"); Log("[删除箱号] 完成,数量=" + xhs.Count + ",耗时 " + sw.ElapsedMilliseconds + "ms"); } finally { _currentTuhao = rememberTuhao; SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao); _isReloading = false; } }); }); } catch (Exception ex) { Cursor = Cursors.Default; _isReloading = false; MessageBox.Show(this, "删除箱号失败:\n" + ex.Message, "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 元件菜单事件 ---------- private void MenuYuanjian_New_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao)) { MessageBox.Show(this, "请先选择箱号,再新建元件。", "新建元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 批量插入分发(2026-10-01,用户决策):多选箱号时弹确认—— // [全部插入]每个选中箱号各插一条(序号独立) / [取消]不插入 // null=单选→继续走下面原逻辑;非null=已批量完成(或取消)→到此为止 // ★ 防御(2026-10-01):分发在 try 外,异常曾被静默吞(表现为"点了没反应不弹窗")—— // 这里单独包住:异常记日志+提示,并降级走原单箱号逻辑,保证功能可用 try { var 批量结果 = 批量插入到选中箱号((th, xh) => InsertYuanjian(_currentProjectId, th, xh, "新元件" + GetYuanjianCount(_currentProjectId, th, xh))); if (批量结果 != null) return; } catch (Exception exBatch) { Log("[批量插入] 分发异常(降级为单箱号新建): " + exBatch); MessageBox.Show(this, "批量插入功能异常,已改为只插入当前箱号。\n异常信息:\n" + exBatch.Message, "批量插入", MessageBoxButtons.OK, MessageBoxIcon.Warning); } // 不弹输入框,直接用默认名(新元件N)创建 // 序号按当前箱号的独立元件表计数(不再跨箱号混计) // 记住当前图号/箱号,新建完成后恢复(防止 ADGV 选中抖动导致状态错乱) string rememberTuhao = _currentTuhao; string rememberXianghao = _currentXianghao; // ★ 记住选中行索引,用于"在选中行下面插入" int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5); string newName = "新元件" + GetYuanjianCount(_currentProjectId, _currentTuhao, _currentXianghao); try { // 整个新建流程用 _isReloading 包起来,挡住所有 SelectionChanged 联动 // (防止 ReloadYuanjianByXianghao 后 ADGV 选中抖动切到别的箱号/图号) _isReloading = true; // 新建前先保存当前三个表的所有编辑,避免用户编辑丢失 // 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。 // 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。 SaveAllQuoteTablesIfDirty(false); Log("新建元件: InsertYuanjian project=[" + _currentProjectId + "] 图号=[" + _currentTuhao + "] 箱号=[" + _currentXianghao + "] 名=[" + newName + "]"); InsertYuanjian(_currentProjectId, _currentTuhao, _currentXianghao, newName); // ★ 清除该箱号的元件表缓存,强制下次 Reload 从库读(看到刚 INSERT 的新元件) string yjKeyNew = _currentTuhao + "\n" + _currentXianghao; _yuanjianCache.Remove(yjKeyNew); ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao); Log("新建元件: Reload后元件表行数=" + (_yuanjianTable == null ? "null" : _yuanjianTable.Rows.Count.ToString())); // ★ 把新行(在表末尾)移动到选中行下面 if (_yuanjianTable != null) MoveLastRowBelowSelected(this.advancedDataGridView5, _yuanjianTable, selectedIndex, "\"" + _yuanjianTable.TableName + "\"", _currentProjectId); // 新建后重算当前层并刷新显示 RecalcAllAfterReload(); // ★ 重新绑定 DataSource 后 Format/ValueType 会丢失,需要重新应用 ApplyPriceFormat(this.advancedDataGridView5, _单台小数位文本); Log("新建元件: RecalcAfterReload完成"); } catch (Exception ex) { MessageBox.Show(this, "新建元件失败:\n" + ex.Message, "新建元件", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 恢复图号/箱号选中,再解除 guard _currentTuhao = rememberTuhao; _currentXianghao = rememberXianghao; SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao); SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao); _isReloading = false; } } private void MenuYuanjian_Delete_Click(object sender, EventArgs e) { // 取所有选中行(支持批量多选) var rows = GetAllSelectedRows(this.advancedDataGridView5); if (rows == null || rows.Count == 0) { MessageBox.Show(this, "请先选择要删除的元件。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 合并视图分支(2026-10-01):多选箱号联动时元件表是多箱号合集, // 序号列显示"箱号-N"且各行属于不同物理元件表——不能按 _currentXianghao+序号 删, // 按每行的"_来源表名_+rowid/内部ID"行级来源删(与编辑直写同一套定位) bool isMergedYj = _yuanjianTable != null && string.IsNullOrEmpty(_yuanjianTable.TableName) && _yuanjianTable.Columns.Contains("_来源表名_"); if (isMergedYj) { 删除合并视图选中元件(rows); return; } // 收集所有选中行的序号 var seqs = new System.Collections.Generic.List(); foreach (var r in rows) { string s = GetCellValueAsString(r, "序号"); if (!string.IsNullOrWhiteSpace(s) && !seqs.Contains(s)) seqs.Add(s); } if (seqs.Count == 0) { MessageBox.Show(this, "选中行没有序号,无法删除。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 记住当前图号/箱号,删除后恢复选中 string rememberTuhao = _currentTuhao; string rememberXianghao = _currentXianghao; // 不弹确认框,直接删除 try { // 整个删除流程用 _isReloading 包裹,挡住选中抖动 _isReloading = true; // 删除前先保存当前三个表的所有编辑 // 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。 // 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。 SaveAllQuoteTablesIfDirty(false); // ★ 删除前快照整库,支持右键「撤销删除」还原 PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false); // ★ 批量模式:开关=1 跳过触发器(删除行无需重算派生值),提交后统一刷聚合 using (var connSw = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId)))) { connSw.Open(); } // 批量删除所有选中元件 foreach (string seq in seqs) { DeleteYuanjian(_currentProjectId, _currentTuhao, _currentXianghao, seq); } Log("批量删除元件: 数量=" + seqs.Count); // 删除后重新编号该箱号的元件表(让序号连续 1、2、3...) string yjTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, _currentXianghao); if (!string.IsNullOrEmpty(yjTbl)) RenumberTable(_currentProjectId, "\"" + yjTbl + "\""); // ★ 恢复触发器前,补刷该箱号行与图号行聚合(删除后 SUM 变化) using (var connSw = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId)))) { connSw.Open(); using (var txSw = connSw.BeginTransaction()) { 计算引擎.刷新箱号行聚合(connSw, txSw, GetQuoteDbPath(_currentProjectId), _currentTuhao, _currentXianghao, TuhaoTableName); 计算引擎.刷新图号行聚合(connSw, txSw, GetQuoteDbPath(_currentProjectId), _currentTuhao, TuhaoTableName); txSw.Commit(); } } // ★ 清除该箱号的元件表缓存,强制下次 Reload 从库读(看到删除后的结果) string yjKeyDel = _currentTuhao + "\n" + _currentXianghao; _yuanjianCache.Remove(yjKeyDel); ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao); // 删除后重算,更新箱号行(成本价/利润)、图号行(总价/毛利)、项目表 RecalcAllAfterReload(); } catch (Exception ex) { MessageBox.Show(this, "删除元件失败:\n" + ex.Message, "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _currentTuhao = rememberTuhao; _currentXianghao = rememberXianghao; SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao); SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao); _isReloading = false; } } /// /// ★ 合并视图删除(2026-10-01):多选箱号时元件表是多箱号合集, /// 每个选中行按"_来源表名_ + rowid(内部ID优先)"删自己物理表里的行, /// 涉及的物理表逐个重编号,最后全库重算 + 按箱号表当前选中重建合并视图。 /// private void 删除合并视图选中元件(System.Collections.Generic.List rows) { try { // 删除流程用 _isReloading 包裹,挡住选中抖动 _isReloading = true; // 1) 合并视图的编辑全走直写队列,删除前强制落库(否则残留编辑随行一起丢) 直写队列强制落库(); // 2) 删除前快照整库,支持右键「撤销删除」还原 PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false); // 3) 收集选中行的行级来源(来源表名/rowid/内部ID),跳过来源不全的行 var targets = new System.Collections.Generic.List(); // {表名,rowid,内部ID} foreach (var r in rows) { if (r == null || r.IsNewRow) continue; string tbl = GetBoundValueAsString(r, "_来源表名_"); if (string.IsNullOrWhiteSpace(tbl)) continue; long rid = 0; long.TryParse(GetBoundValueAsString(r, "_rowid_"), out rid); string nid = GetBoundValueAsString(r, "内部ID"); if (rid <= 0 && string.IsNullOrWhiteSpace(nid)) continue; targets.Add(new[] { tbl, rid.ToString(), nid }); } if (targets.Count == 0) { MessageBox.Show(this, "选中行没有来源箱号信息,无法删除。\n请改在单个箱号下删除。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 4) 事务批量删除:内部ID优先定位,rowid 兜底(与直写同一套定位体系) string dbPath = GetQuoteDbPath(_currentProjectId); int deleted = 0; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { foreach (var t in targets) { string q = "\"" + t[0].Replace("\"", "") + "\""; int n; if (!string.IsNullOrWhiteSpace(t[2])) { using (var del = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + q + " WHERE 内部ID=@nid;", conn, tx)) { del.Parameters.AddWithValue("@nid", t[2]); n = del.ExecuteNonQuery(); } // 内部ID没删到(理论上不会)→rowid 兜底 if (n == 0 && long.Parse(t[1]) > 0) { using (var del = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + q + " WHERE rowid=@rid;", conn, tx)) { del.Parameters.AddWithValue("@rid", long.Parse(t[1])); n = del.ExecuteNonQuery(); } } } else { using (var del = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + q + " WHERE rowid=@rid;", conn, tx)) { del.Parameters.AddWithValue("@rid", long.Parse(t[1])); n = del.ExecuteNonQuery(); } } deleted += n; } tx.Commit(); } // 5) 涉及的物理表逐个重编号(让各箱号序号连续 1、2、3...) var doneTbls = new System.Collections.Generic.HashSet(); foreach (var t in targets) { if (!doneTbls.Add(t[0])) continue; try { RenumberTable(_currentProjectId, "\"" + t[0].Replace("\"", "") + "\""); } catch (Exception exRn) { Log("[合并删除] 重编号 " + t[0] + " 失败: " + exRn.Message); } } } Log("[合并删除] 选中=" + rows.Count + " 实删=" + deleted + " 涉及表=" + new System.Collections.Generic.HashSet( System.Linq.Enumerable.Select(targets, t => t[0])).Count); // 6) 涉及多个箱号,元件缓存全清 + 全库重算(刷全部箱号/图号/项目聚合) _yuanjianCache.Clear(); _xianghaoCache.Clear(); RecalcAllFromDatabase(_currentProjectId); // ★ 刷新修复(2026-10-01):库聚合完成后,按内部ID对齐从库刷新 // 箱号表/图号表价格列显示(合并视图也支持)——否则删除后界面价格仍是旧值 刷新箱号表显示从库(); // 7) 按箱号表当前选中重建合并视图(与 SelectionChanged 联动同一套收集逻辑) 重建元件合并视图(); } catch (Exception ex) { MessageBox.Show(this, "删除元件失败:\n" + ex.Message, "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _isReloading = false; } } /// /// ★ 收集箱号表(advancedDataGridView4)当前选中的箱号列表(2026-10-01): /// 返回 [来源图号, 箱号内部ID, 箱号显示名] 三元组列表(按行序,含 CurrentRow 保底)。 /// 合并视图按行级 _来源图号_ 取图号;普通视图统一用 _currentTuhao。 /// private System.Collections.Generic.List 收集选中箱号列表() { var pairs = new System.Collections.Generic.List(); var dgv4 = this.advancedDataGridView4; if (dgv4 == null) return pairs; bool mergedView = _xianghaoTable != null && string.IsNullOrEmpty(_xianghaoTable.TableName) && _xianghaoTable.Columns.Contains("_来源图号_"); var rowSet = new System.Collections.Generic.SortedSet(); foreach (DataGridViewCell c in dgv4.SelectedCells) { if (c != null && c.RowIndex >= 0) rowSet.Add(c.RowIndex); } if (dgv4.CurrentRow != null && dgv4.CurrentRow.Index >= 0) rowSet.Add(dgv4.CurrentRow.Index); foreach (int idx in rowSet) { if (_xianghaoTable == null || idx >= _xianghaoTable.Rows.Count) continue; string thSrc = mergedView ? Convert.ToString(_xianghaoTable.Rows[idx]["_来源图号_"]) : _currentTuhao; string xhId = Convert.ToString(_xianghaoTable.Rows[idx]["内部ID"]); string xhName = Convert.ToString(_xianghaoTable.Rows[idx]["箱号"]); if (!string.IsNullOrWhiteSpace(xhId)) pairs.Add(new[] { thSrc, xhId, xhName }); } return pairs; } /// 批量插入记忆设置(2026-10-01):不再询问=true 时不再弹窗,直接全部插入。 /// (用户已删"仅当前行"按钮,记忆只剩"不再询问"一个维度,旧配置里的选择字段被忽略) private static bool _批量插入设置已加载 = false; private static bool _批量插入不再询问 = false; /// 批量插入记忆配置路径(exe 目录,简单 XML,删除文件即重置)。 private static string 批量插入设置路径() { return System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "批量插入设置.xml"); } private static void 加载批量插入设置() { if (_批量插入设置已加载) return; _批量插入设置已加载 = true; try { string f = 批量插入设置路径(); if (!System.IO.File.Exists(f)) return; string xml = System.IO.File.ReadAllText(f, System.Text.Encoding.UTF8); // 简易标签值提取:<标签>值(配置只有本方法写入,格式固定) string 开 = "<不再询问>", 关 = ""; int i = xml.IndexOf(开, System.StringComparison.Ordinal); if (i < 0) return; int j = xml.IndexOf(关, i + 开.Length, System.StringComparison.Ordinal); if (j < 0) return; _批量插入不再询问 = xml.Substring(i + 开.Length, j - i - 开.Length).Trim() == "1"; } catch { } } private static void 保存批量插入设置() { try { var sb = new System.Text.StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("<批量插入>"); sb.AppendLine(" <不再询问>" + (_批量插入不再询问 ? "1" : "0") + ""); sb.AppendLine(""); System.IO.File.WriteAllText(批量插入设置路径(), sb.ToString(), System.Text.Encoding.UTF8); } catch { } } /// /// ★ 弹"批量插入元件"确认框(2026-10-01,"仅当前行"按钮已按用户要求删除): /// [全部插入]=插入所有选中箱号 / [取消]=不插入。 /// 复选框"以后不再询问"勾选且点了[全部插入]时记忆(持久化,删除 /// 安装目录"批量插入设置.xml"可恢复弹窗)。 /// ★ 窗体布局在 xiangmu\批量插入询问.Designer.cs——可用 SD 设计器打开微调。 /// 返回: 0=全部插入 -1=取消;out 不再询问=复选框勾选状态。 /// private int 弹批量插入询问(string 消息, out bool 不再询问) { using (var f = new 批量插入询问 { 消息 = 消息 }) { var r = f.ShowDialog(this); 不再询问 = f.不再询问; if (r == DialogResult.Yes) return 0; return -1; } } /// /// ★ 多选箱号批量插入分发(2026-10-01,用户决策:批量插入所有选中箱号; /// 同日按用户要求删除"仅当前行"按钮,只剩 [全部插入]/[取消])。 /// 箱号表多选(>1)时弹确认框;勾选"以后不再询问"会持久化,之后一直 /// 全部插入不再弹窗(删安装目录"批量插入设置.xml"可恢复)。 /// 单箱号插入动作收到 (图号, 箱号内部ID),自行完成库 INSERT(纯库操作,勿带 UI 副作用)。 /// 返回约定: /// null = 单选→调用方走原单箱号逻辑 /// 空列表 = 用户取消→调用方直接 return(什么都不插) /// 非空列表 = 已批量插入完成(缓存/重算/合并视图收口已做)→调用方 return /// private System.Collections.Generic.List 批量插入到选中箱号( System.Action 单箱号插入) { var pairs = 收集选中箱号列表(); // ★ 诊断日志(2026-10-01):排障"点了新建元件没反应不弹窗" Log("[批量插入] 分发入口: 选中箱号数=" + pairs.Count + " 不再询问=" + _批量插入不再询问); if (pairs.Count <= 1) return null; // 单选:调用方原逻辑 string names = ""; foreach (var p in pairs) { if (names.Length > 0) names += "、"; names += p[2]; if (names.Length > 200) { names += "…"; break; } } // ★ 记忆设置:不再询问时直接全部插入 加载批量插入设置(); if (!_批量插入不再询问) { bool 不再询问; Log("[批量插入] 即将弹出询问框: 箱号数=" + pairs.Count); int sel = 弹批量插入询问( "当前选中 " + pairs.Count + " 个箱号:\n" + names + "\n\n" + "[全部插入] 插入到全部 " + pairs.Count + " 个箱号\n" + "[取消] 不插入", out 不再询问); if (sel != 0) return new System.Collections.Generic.List(); // 取消 // 勾选"以后不再询问"→持久化([取消]不记忆) if (不再询问) { _批量插入不再询问 = true; 保存批量插入设置(); Log("[批量插入] 已记忆选择(不再询问,始终全部插入)"); } } else { Log("[批量插入] 沿用记忆设置: 全部选中箱号"); } // 批量插入全部选中箱号 try { _isReloading = true; 直写队列强制落库(); // 合并视图残留编辑先落库 int ok = 0; foreach (var p in pairs) { try { 单箱号插入(p[0], p[1]); ok++; } catch (Exception exIns) { Log("[批量插入] 箱号[" + p[2] + "] 插入失败: " + exIns.Message); } } // 收口:全清缓存+全库重算+从库刷价格显示+重建合并视图(与删除合并元件同款) _yuanjianCache.Clear(); _xianghaoCache.Clear(); RecalcAllFromDatabase(_currentProjectId); 刷新箱号表显示从库(); 重建元件合并视图(); Log("[批量插入] 已插入 " + ok + "/" + pairs.Count + " 个箱号: " + names); return pairs.ConvertAll(p => p[2]); } finally { _isReloading = false; } } /// /// ★ 工具窗口批量插入(2026-10-01):用户库/柜体库/母排/GCS铜排/箱体计算/方案库等 /// 内存表型插入入口的通用批量包装——多选箱号时弹确认,对每个选中箱号 /// 依次"切换当前箱号→装载该箱号内存表→执行原插入动作(NewRow+收尾)"。 /// 插入动作 = 原入口的 确保元件表存在→NewRow→元件表插行收尾 全流程, /// 动作内引用 _currentTuhao/_currentXianghao 的逻辑自动跟随切换。 /// 返回: false=单选→调用方走原逻辑;true=已批量完成(或用户取消)→调用方直接 return。 /// private bool 工具窗口批量插入(System.Action 插入动作) { var pairs = 收集选中箱号列表(); if (pairs.Count <= 1) return false; // 单选:调用方原逻辑 // 记忆设置:不再询问时直接全部插入 加载批量插入设置(); if (!_批量插入不再询问) { string names = ""; foreach (var p in pairs) { if (names.Length > 0) names += "、"; names += p[2]; if (names.Length > 200) { names += "…"; break; } } bool 不再询问; Log("[批量插入] 工具窗口即将弹询问框: 箱号数=" + pairs.Count); int sel = 弹批量插入询问( "当前选中 " + pairs.Count + " 个箱号:\n" + names + "\n\n" + "[全部插入] 插入到全部 " + pairs.Count + " 个箱号\n" + "[取消] 不插入", out 不再询问); if (sel != 0) return true; // 用户取消:调用方直接 return if (不再询问) { _批量插入不再询问 = true; 保存批量插入设置(); Log("[批量插入] 已记忆选择(不再询问,始终全部插入)"); } } else { Log("[批量插入] 沿用记忆设置: 全部选中箱号"); } // 批量执行:记住上下文,逐箱号切换→装载内存表→执行插入动作 string memTh = _currentTuhao, memXh = _currentXianghao; _isReloading = true; try { 直写队列强制落库(); // 合并视图残留编辑先落库 int ok = 0; foreach (var p in pairs) { try { _currentTuhao = p[0]; _currentXianghao = p[1]; // 强制装载该箱号的内存表(插入动作里的"确保元件表存在"见 TableName // 非空会跳过重载,所以这里必须先清缓存+Reload 再执行动作) _yuanjianCache.Remove(p[0] + "\n" + p[1]); ReloadYuanjianByXianghao(_currentProjectId, p[0], p[1]); if (_yuanjianTable == null || string.IsNullOrEmpty(_yuanjianTable.TableName)) throw new Exception("箱号[" + p[2] + "]元件表装载失败"); 插入动作(); ok++; } catch (Exception exIns) { Log("[批量插入] 箱号[" + p[2] + "] 插入失败: " + exIns.Message); } } Log("[批量插入] 工具窗口已插入 " + ok + "/" + pairs.Count + " 个箱号"); } finally { // 收口:恢复上下文+全清缓存+全库重算+刷价格显示+重建合并视图 _currentTuhao = memTh; _currentXianghao = memXh; _yuanjianCache.Clear(); _xianghaoCache.Clear(); try { RecalcAllFromDatabase(_currentProjectId); 刷新箱号表显示从库(); 重建元件合并视图(); } catch (Exception exEnd) { Log("[批量插入] 收口异常: " + exEnd.Message); } _isReloading = false; } return true; } /// /// ★ G1-1(2026-10-01):功能区「模板费用」入口——弹费用模板管理窗。 /// private void 打开费用模板窗口() { if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在项目列表双击打开一个项目,再使用模板费用。", "模板费用", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using (var f = new 费用模板(this)) { f.ShowDialog(this); } } /// /// ★ G1-1(2026-10-01):把启用的费用模板换算成费用行写入当前项目每个箱号的元件表。 /// 费用行=类型'费用'的普通元件行(表价=金额;计入'仅报出'时采购系数=0 → 公式天然算出采购价0), /// 参与箱号/图号/项目聚合且全库重算稳定。重复应用:先删同模板名旧行再插(幂等)。 /// internal string 应用费用模板到当前项目(System.Collections.Generic.List<费用模板.模板行> 模板s) { if (string.IsNullOrWhiteSpace(_currentProjectId)) throw new Exception("未打开项目"); string dbPath = GetQuoteDbPath(_currentProjectId); if (!System.IO.File.Exists(dbPath)) throw new Exception("项目库不存在: " + dbPath); int 写入箱号 = 0, 写入行 = 0; var 明细 = new System.Text.StringBuilder(); // 每箱号费用金额(应用结果报告) using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); // ★ 费用行置底配套(2026-10-01):老元件表可能没有"类型"列,幂等补列 // (INSERT 列清单与"删除旧费用行"都依赖该列) { var yj表s = new System.Collections.Generic.List(); using (var selT = new System.Data.SQLite.SQLiteCommand( "SELECT DISTINCT 实际表名 FROM 表索引 WHERE 类型='元件';", conn)) using (var rdT = selT.ExecuteReader()) while (rdT.Read()) { string t = rdT.IsDBNull(0) ? "" : rdT.GetString(0).Trim(); if (!string.IsNullOrWhiteSpace(t)) yj表s.Add(t); } foreach (string t in yj表s) EnsureColumns(conn, "\"" + t + "\"", new[] { "类型", "费用公式" }); } using (var tx = conn.BeginTransaction()) { // 全部图号(内部ID) var tuhaos = new System.Collections.Generic.List(); using (var selTh = new System.Data.SQLite.SQLiteCommand( "SELECT 内部ID FROM " + TuhaoTableName + " WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx)) using (var rdTh = selTh.ExecuteReader()) while (rdTh.Read()) tuhaos.Add(rdTh.GetString(0)); foreach (string th in tuhaos) { // 该图号全部箱号(内部ID) string xhTbl = GetXianghaoTableByIndex(th, conn); if (string.IsNullOrEmpty(xhTbl)) continue; var xhs = new System.Collections.Generic.List(); using (var selXh = new System.Data.SQLite.SQLiteCommand( "SELECT 内部ID FROM \"" + xhTbl + "\" WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx)) using (var rdXh = selXh.ExecuteReader()) while (rdXh.Read()) xhs.Add(rdXh.GetString(0)); foreach (string xh in xhs) { try { string yjTbl = GetYuanjianTableByIndex(th, xh, conn); if (string.IsNullOrEmpty(yjTbl)) continue; string q = "\"" + yjTbl + "\""; // 1) 该箱号非费用元件的采购价/报出价合计(百分比基数;IFNULL 兼容类型列为 NULL 的老行) decimal sumCg = 0m, sumBc = 0m; using (var selYj2 = new System.Data.SQLite.SQLiteCommand( "SELECT 采购价, 报出价 FROM " + q + " WHERE IFNULL(类型,'')<>'费用';", conn, tx)) using (var rdYj2 = selYj2.ExecuteReader()) { while (rdYj2.Read()) { sumCg += ParseDecimal(rdYj2.IsDBNull(0) ? "" : rdYj2.GetValue(0)); sumBc += ParseDecimal(rdYj2.IsDBNull(1) ? "" : rdYj2.GetValue(1)); } } // ★ 该箱号数量(台数)——费用行数量跟随台数,价格=表价×系数×数量 全走公式 // (改箱号台数后全库重算会自动联动费用行,不再写死) decimal 台数 = 1m; using (var selSl = new System.Data.SQLite.SQLiteCommand( "SELECT 数量 FROM \"" + xhTbl + "\" WHERE 内部ID=@id;", conn, tx)) { selSl.Parameters.AddWithValue("@id", xh); object vSl = selSl.ExecuteScalar(); decimal sl = ParseDecimal(vSl == null || vSl == DBNull.Value ? "" : vSl.ToString()); if (sl > 0m) 台数 = sl; } // 2) 删同模板名旧费用行(幂等重应用) foreach (var t in 模板s) { using (var del = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + q + " WHERE 类型='费用' AND 元件名称=@n;", conn, tx)) { del.Parameters.AddWithValue("@n", t.名称); del.ExecuteNonQuery(); } } // 3) 逐模板换算【单台金额】并 INSERT 费用行 // ★ 修复(2026-10-01 二次):完全公式化——INSERT 只写输入列(表价/系数/数量), // 采购单价/采购价/报出单价/报出价 一律不写,交给收口的 RecalcAllFromDatabase // 按统一公式(表价×系数×数量)算出——用户在元件表改费用行的表价/系数/数量也会联动 string seqBase = "SELECT IFNULL(MAX(CAST(序号 AS INTEGER)),0)+1 FROM " + q + ";"; var 本箱号费用明细 = new System.Text.StringBuilder(); decimal 本箱号费用合计 = 0m; foreach (var t in 模板s) { // 单台金额:固定=数值;百分比=该箱号元件单台合计×% decimal 单台金额; if (t.方式 == "元件采购价×%") 单台金额 = decimal.Round(sumCg * t.数值 / 100m, 2); else if (t.方式 == "元件报出价×%") 单台金额 = decimal.Round(sumBc * t.数值 / 100m, 2); else 单台金额 = t.数值; int seq; using (var c = new System.Data.SQLite.SQLiteCommand(seqBase, conn, tx)) seq = Convert.ToInt32(c.ExecuteScalar()); using (var ins = new System.Data.SQLite.SQLiteCommand( "INSERT INTO " + q + " (序号, 元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数," + " 品牌, 系列, 类型, 费用公式, 内部ID) VALUES " + "(@seq,@n,@gg,'项',@sl,@bj,'1','1','','','费用',@gf,@nid);", conn, tx)) { ins.Parameters.AddWithValue("@seq", seq.ToString()); ins.Parameters.AddWithValue("@n", t.名称); // ★ 实时联动(2026-10-01):完整公式存专用隐藏列"费用公式"(表格不绑定该列,永不显示; // 联动解析用,如 元件采购价×13%),"规格"列只显示纯数值(13%),备注保持空; // 固定金额:规格空,费用公式=固定金额(表价不联动,数量×台数仍走公式) string 数值文本 = t.数值.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture); if (t.方式 == "固定金额(每柜)") { ins.Parameters.AddWithValue("@gg", ""); ins.Parameters.AddWithValue("@gf", "固定金额"); } else { ins.Parameters.AddWithValue("@gg", 数值文本 + "%"); ins.Parameters.AddWithValue("@gf", t.方式.Substring(0, t.方式.Length - 1) + 数值文本 + "%"); } ins.Parameters.AddWithValue("@sl", 台数.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)); ins.Parameters.AddWithValue("@bj", (double)单台金额); ins.Parameters.AddWithValue("@nid", System.Guid.NewGuid().ToString("N")); ins.ExecuteNonQuery(); 写入行++; } decimal 本行合计 = decimal.Round(单台金额 * 台数, 2); 本箱号费用明细.Append(t.名称 + "=" + 本行合计.ToString("0.##") + "(单台" + 单台金额.ToString("0.##") + "×" + 台数.ToString("0.##") + "台) "); 本箱号费用合计 += 本行合计; } 写入箱号++; 明细.AppendLine("箱号[" + 取箱号显示名(conn, tx, xhTbl, xh) + "] 费用合计 " + 本箱号费用合计.ToString("0.##") + ": " + 本箱号费用明细); } catch (Exception exXh) { Log("[模板费用] 箱号[" + xh + "] 应用失败: " + exXh.Message); } } } tx.Commit(); } } // 收口:清缓存+全库重算+刷价格+重建视图(费用行参与全部聚合) _yuanjianCache.Clear(); _xianghaoCache.Clear(); RecalcAllFromDatabase(_currentProjectId); 刷新箱号表显示从库(); 重建元件合并视图(); Log("[模板费用] 已应用: 箱号" + 写入箱号 + "个 新费用行" + 写入行 + "条"); return "已应用到 " + 写入箱号 + " 个箱号,共写入 " + 写入行 + " 条费用行。\n" + "各箱号成本/报出、图号总价、项目汇总已重算。\n\n各箱号费用明细:\n" + 明细; } /// 费用模板应用报告用:按箱号内部ID取箱号显示名。 private static string 取箱号显示名(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string xhTbl, string xhId) { try { using (var sel = new System.Data.SQLite.SQLiteCommand( "SELECT 箱号 FROM \"" + xhTbl + "\" WHERE 内部ID=@id;", conn, tx)) { sel.Parameters.AddWithValue("@id", xhId); object v = sel.ExecuteScalar(); string s = v == null || v == DBNull.Value ? "" : v.ToString().Trim(); return s == "" ? xhId : s; } } catch { return xhId; } } /// ★ G1-1(2026-10-01):删除当前项目所有箱号元件表里 类型='费用' 的行并重算。 internal string 清空项目费用行() { if (string.IsNullOrWhiteSpace(_currentProjectId)) throw new Exception("未打开项目"); string dbPath = GetQuoteDbPath(_currentProjectId); int 删除 = 0; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { var tuhaos = new System.Collections.Generic.List(); using (var selTh = new System.Data.SQLite.SQLiteCommand( "SELECT 内部ID FROM " + TuhaoTableName + " WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx)) using (var rdTh = selTh.ExecuteReader()) while (rdTh.Read()) tuhaos.Add(rdTh.GetString(0)); foreach (string th in tuhaos) { string xhTbl = GetXianghaoTableByIndex(th, conn); if (string.IsNullOrEmpty(xhTbl)) continue; var xhs = new System.Collections.Generic.List(); using (var selXh = new System.Data.SQLite.SQLiteCommand( "SELECT 内部ID FROM \"" + xhTbl + "\" WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx)) using (var rdXh = selXh.ExecuteReader()) while (rdXh.Read()) xhs.Add(rdXh.GetString(0)); foreach (string xh in xhs) { string yjTbl = GetYuanjianTableByIndex(th, xh, conn); if (string.IsNullOrEmpty(yjTbl)) continue; try { using (var del = new System.Data.SQLite.SQLiteCommand( "DELETE FROM \"" + yjTbl + "\" WHERE 类型='费用';", conn, tx)) 删除 += del.ExecuteNonQuery(); } catch (Exception exD) { Log("[模板费用] 清空箱号[" + xh + "]失败: " + exD.Message); } } } tx.Commit(); } } _yuanjianCache.Clear(); _xianghaoCache.Clear(); RecalcAllFromDatabase(_currentProjectId); 刷新箱号表显示从库(); 重建元件合并视图(); Log("[模板费用] 已清空费用行 " + 删除 + " 条"); return "已删除 " + 删除 + " 条费用行,价格已重算。"; } /// /// ★ 按箱号表(advancedDataGridView4)当前选中重建元件表视图: /// 多选→ReloadYuanjianByXianghaos 合并视图;单选→ReloadYuanjianByXianghao 原路径。 /// (删除/刷新后调用,保持与 SelectionChanged 联动一致的显示规则) /// private void 重建元件合并视图() { if (string.IsNullOrWhiteSpace(_currentProjectId)) return; var dgv4 = this.advancedDataGridView4; if (dgv4 == null) return; // ★ 兜底:CurrentRow 丢失(理论上不会)时按 _currentXianghao 单箱号重载,保证元件表必刷新 if (dgv4.CurrentRow == null || dgv4.CurrentRow.Index < 0) { if (!string.IsNullOrWhiteSpace(_currentXianghao)) ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao); return; } var pairs = 收集选中箱号列表(); if (pairs.Count > 1) { ReloadYuanjianByXianghaos(_currentProjectId, pairs); } else { int idx = dgv4.CurrentRow.Index; string xhId = (_xianghaoTable != null && idx < _xianghaoTable.Rows.Count) ? Convert.ToString(_xianghaoTable.Rows[idx]["内部ID"]) : _currentXianghao; bool mergedView = _xianghaoTable != null && string.IsNullOrEmpty(_xianghaoTable.TableName) && _xianghaoTable.Columns.Contains("_来源图号_"); string thSrc = (mergedView && _xianghaoTable != null && idx < _xianghaoTable.Rows.Count) ? Convert.ToString(_xianghaoTable.Rows[idx]["_来源图号_"]) : _currentTuhao; if (!string.IsNullOrWhiteSpace(xhId)) ReloadYuanjianByXianghao(_currentProjectId, thSrc, xhId); } } // ============================================================ // 元件排序(button33):弹出「元件排序」窗体(xiangmu\元件排序.cs,设计器制作)。 // 窗体内:左侧箱号列表 + 右侧元件列表,上移/下移/置顶/置底 手动排序, // 点「确定」由窗体把新顺序写回数据库。这里只负责: // 1) 校验已打开项目,先把内存编辑落盘 // 2) 传上下文(项目编号/当前图号/当前箱号)给窗体 // 3) 窗体关闭后清缓存刷新报价页,恢复选中 // ============================================================ /// button33 点击:打开元件排序窗体。 private void button33_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在项目列表双击打开一个项目,再对元件排序。", "元件排序", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 记住当前选中,排序窗体关闭后恢复 string rememberTuhao = _currentTuhao; string rememberXianghao = _currentXianghao; try { // 排序前把内存未保存的编辑落盘,保证窗体读到最新数据 SaveAllQuoteTablesIfDirty(false); using (var dlg = new 元件排序(_currentProjectId, rememberTuhao, rememberXianghao)) { dlg.ShowDialog(this); } } catch (Exception ex) { Log("[元件排序] 失败: " + ex.Message); MessageBox.Show(this, "打开元件排序失败:\n" + ex.Message, "元件排序", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 窗体可能改了元件顺序 → 清缓存,刷新报价页,恢复原选中 try { _isReloading = true; _yuanjianCache.Clear(); if (!string.IsNullOrEmpty(rememberTuhao)) { ReloadTuhaoByProject(_currentProjectId); RecalcAllAfterReload(); } } finally { _currentTuhao = rememberTuhao; _currentXianghao = rememberXianghao; SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao); SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao); _isReloading = false; } } } // ============================================================ // 加入用户库(右键菜单:把元件表选中行加入 advancedDataGridView7 用户库表) // ============================================================ /// /// 用户库数据库路径(共享库,所有项目通用,存所有可用元件)。 /// 与 ui.cs 的 LoadSqliteData_advancedDataGridView7 一致。 /// private string UserLibDbPath { get { return GetDbFullPath(@"sjk\advancedDataGridView7Table.db"); } } private const string UserLibTableName = "advancedDataGridView7Table"; /// /// 右键"加入用户库":把元件表(advancedDataGridView5)中选中的行插入到用户库。 /// private void MenuYuanjian_AddToUserLib_Click(object sender, EventArgs e) { AddRowsToLib("用户库", UserLibDbPath, UserLibTableName, () => ReloadUserLibFromDatabase()); } /// /// 右键"加入柜体库":把元件表(advancedDataGridView5)中选中的行插入到柜体库。 /// 只插入"类型"为"壳体"的行,非壳体行跳过并提示。 /// private void MenuYuanjian_AddToGuiTi_Click(object sender, EventArgs e) { // 先取选中行,过滤出类型="壳体"的 var allRows = GetAllSelectedRows(this.advancedDataGridView5); if (allRows == null || allRows.Count == 0) { MessageBox.Show(this, "请先在元件表中选择要加入柜体库的行。", "加入柜体库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } int skipped = 0; var shellRows = new System.Collections.Generic.List(); foreach (var r in allRows) { string 类型 = GetCellValueAsString(r, "类型"); if (string.Equals(类型, "壳体", System.StringComparison.Ordinal)) { shellRows.Add(r); } else { skipped++; } } if (shellRows.Count == 0) { MessageBox.Show(this, "选中的 " + allRows.Count + " 行中,没有类型为「壳体」的元件。\n" + "柜体库只接收类型为「壳体」的元件。", "加入柜体库", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 把过滤后的行传给通用插入方法 AddRowsToLibFiltered("柜体库", GuiTiDbPath, GuiTiTableName, () => ReloadGuiTiFromDatabase(), shellRows); if (skipped > 0) { Log("[加入柜体库] 跳过 " + skipped + " 行(非壳体类型)"); } } /// /// 通用:把指定的行列表插入到目标库(不做选中行检查,由调用方过滤后传入)。 /// 用于"加入柜体库"(只插入壳体类型的行)。 /// private void AddRowsToLibFiltered(string libName, string dbPath, string tableName, System.Action reloadAction, System.Collections.Generic.IList rows) { if (!System.IO.File.Exists(dbPath)) { MessageBox.Show(this, "找不到" + libName + "数据库文件:\n" + dbPath, "加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { this.advancedDataGridView5.EndEdit(); } catch { } int successCount = 0; int failCount = 0; string lastError = ""; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); foreach (var row in rows) { try { string 元件名称 = GetCellValueAsString(row, "元件名称"); string 规格 = GetCellValueAsString(row, "规格"); string 单位 = GetCellValueAsString(row, "单位"); string 数量 = GetCellValueAsString(row, "数量"); string 表价 = GetCellValueAsString(row, "表价"); string 采购系数 = GetCellValueAsString(row, "采购系数"); string 报出系数 = GetCellValueAsString(row, "报出系数"); string 品牌 = GetCellValueAsString(row, "品牌"); string 系列 = GetCellValueAsString(row, "系列"); string 元件类型 = GetCellValueAsString(row, "类型"); if (string.IsNullOrWhiteSpace(元件名称) && string.IsNullOrWhiteSpace(规格)) { failCount++; lastError = "元件名称和规格均为空,跳过"; continue; } string sql = "INSERT INTO " + tableName + " (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " + "VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@元件名称", 元件名称 ?? ""); cmd.Parameters.AddWithValue("@规格", 规格 ?? ""); cmd.Parameters.AddWithValue("@单位", 单位 ?? ""); cmd.Parameters.AddWithValue("@数量", 数量 ?? ""); cmd.Parameters.AddWithValue("@表价", 表价 ?? ""); cmd.Parameters.AddWithValue("@采购系数", 采购系数 ?? ""); cmd.Parameters.AddWithValue("@报出系数", 报出系数 ?? ""); cmd.Parameters.AddWithValue("@品牌", 品牌 ?? ""); cmd.Parameters.AddWithValue("@系列", 系列 ?? ""); cmd.Parameters.AddWithValue("@元件类型", 元件类型 ?? ""); cmd.ExecuteNonQuery(); } successCount++; } catch (Exception exRow) { failCount++; lastError = exRow.Message; Log("[加入" + libName + "] 单行插入失败: " + exRow.Message); } } } reloadAction(); string msg = "已加入 " + successCount + " 条到" + libName + "。"; if (failCount > 0) { msg += "\n失败 " + failCount + " 条"; if (!string.IsNullOrEmpty(lastError)) msg += "(最后错误: " + lastError + ")"; } Log("[加入" + libName + "] " + msg); MessageBox.Show(this, msg, "加入" + libName, MessageBoxButtons.OK, failCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); } catch (Exception ex) { Log("[加入" + libName + "] 整体失败: " + ex.Message); MessageBox.Show(this, "加入" + libName + "失败:\n" + ex.Message, "加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 通用:把元件表选中行插入到指定库。被"加入用户库"和"加入柜体库"共用。 /// /// 库名(用于提示文字,如"用户库"/"柜体库") /// 目标数据库路径 /// 目标表名 /// 插入成功后的刷新回调 private void AddRowsToLib(string libName, string dbPath, string tableName, System.Action reloadAction) { // 1) 取所有选中行 var rows = GetAllSelectedRows(this.advancedDataGridView5); if (rows == null || rows.Count == 0) { MessageBox.Show(this, "请先在元件表中选择要加入" + libName + "的行。", "加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 2) 校验数据库文件存在 if (!System.IO.File.Exists(dbPath)) { MessageBox.Show(this, "找不到" + libName + "数据库文件:\n" + dbPath, "加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 3) 先结束元件表编辑态 try { this.advancedDataGridView5.EndEdit(); } catch { } // 4) 逐行插入 int successCount = 0; int failCount = 0; string lastError = ""; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); foreach (var row in rows) { try { string 元件名称 = GetCellValueAsString(row, "元件名称"); string 规格 = GetCellValueAsString(row, "规格"); string 单位 = GetCellValueAsString(row, "单位"); string 数量 = GetCellValueAsString(row, "数量"); string 表价 = GetCellValueAsString(row, "表价"); string 采购系数 = GetCellValueAsString(row, "采购系数"); string 报出系数 = GetCellValueAsString(row, "报出系数"); string 品牌 = GetCellValueAsString(row, "品牌"); string 系列 = GetCellValueAsString(row, "系列"); string 元件类型 = GetCellValueAsString(row, "类型"); if (string.IsNullOrWhiteSpace(元件名称) && string.IsNullOrWhiteSpace(规格)) { failCount++; lastError = "元件名称和规格均为空,跳过"; continue; } string sql = "INSERT INTO " + tableName + " (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " + "VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@元件名称", 元件名称 ?? ""); cmd.Parameters.AddWithValue("@规格", 规格 ?? ""); cmd.Parameters.AddWithValue("@单位", 单位 ?? ""); cmd.Parameters.AddWithValue("@数量", 数量 ?? ""); cmd.Parameters.AddWithValue("@表价", 表价 ?? ""); cmd.Parameters.AddWithValue("@采购系数", 采购系数 ?? ""); cmd.Parameters.AddWithValue("@报出系数", 报出系数 ?? ""); cmd.Parameters.AddWithValue("@品牌", 品牌 ?? ""); cmd.Parameters.AddWithValue("@系列", 系列 ?? ""); cmd.Parameters.AddWithValue("@元件类型", 元件类型 ?? ""); cmd.ExecuteNonQuery(); } successCount++; } catch (Exception exRow) { failCount++; lastError = exRow.Message; Log("[加入" + libName + "] 单行插入失败: " + exRow.Message); } } } // 5) 刷新目标库表格 reloadAction(); // 6) 反馈 string msg = "已加入 " + successCount + " 条到" + libName + "。"; if (failCount > 0) { msg += "\n失败 " + failCount + " 条"; if (!string.IsNullOrEmpty(lastError)) msg += "(最后错误: " + lastError + ")"; } Log("[加入" + libName + "] " + msg); MessageBox.Show(this, msg, "加入" + libName, MessageBoxButtons.OK, failCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); } catch (Exception ex) { Log("[加入" + libName + "] 整体失败: " + ex.Message); MessageBox.Show(this, "加入" + libName + "失败:\n" + ex.Message, "加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 从用户库数据库重新查询并绑定到 advancedDataGridView7。 /// 与 ui.cs 的 LoadSqliteData_advancedDataGridView7 同款逻辑, /// 但放在主窗口.cs 中以便在加入用户库后主动刷新。 /// private void ReloadUserLibFromDatabase() { string dbPath = UserLibDbPath; if (!System.IO.File.Exists(dbPath)) return; try { string connStr = GetConnString(dbPath); var table = new DataTable(); using (var conn = new System.Data.SQLite.SQLiteConnection(connStr)) { conn.Open(); string sql = "SELECT * FROM " + UserLibTableName + ";"; using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(sql, conn)) { adapter.Fill(table); } } this.advancedDataGridView7.AutoGenerateColumns = false; this.advancedDataGridView7.DataSource = table; // 同步刷新 comboBox3 的不重复元件名称下拉项 LoadUserLibDistinctNames(); } catch (Exception ex) { Log("[加入用户库] 刷新用户库表格失败: " + ex.Message); } } /// /// 扫描用户库(advancedDataGridView7)的"元件名称"列,把所有不重复的名称 /// 填入 comboBox3 作为下拉项(首项固定为"全部")。保留当前选中值。 /// private void LoadUserLibDistinctNames() { // 取当前选中,稍后恢复(避免刷新时选中被重置) string prev = this.comboBox3.Text; var names = new System.Collections.Generic.List(); names.Add("全部"); var table = this.advancedDataGridView7.DataSource as DataTable; if (table != null && table.Columns.Contains("元件名称")) { var set = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase); foreach (DataRow r in table.Rows) { if (r == null) continue; object v = r["元件名称"]; if (v == null || v == DBNull.Value) continue; string s = v.ToString().Trim(); if (s.Length == 0) continue; // 用原大小写保留首次出现的写法 if (set.Add(s)) names.Add(s); } } // 临时解绑事件,避免填充项时反复触发筛选 this.comboBox3.SelectedIndexChanged -= UserLib_NameFilter_Changed; try { this.comboBox3.Items.Clear(); this.comboBox3.Items.AddRange(names.ToArray()); // 恢复选中项(C# 5 的 List.Contains 只有 1 参数,用 OrdinalIgnoreCase 集合兜底判断) bool found = false; if (!string.IsNullOrEmpty(prev)) { foreach (var n in names) { if (string.Equals(n, prev, System.StringComparison.OrdinalIgnoreCase)) { this.comboBox3.Text = prev; found = true; break; } } } if (!found) this.comboBox3.Text = "全部"; } finally { this.comboBox3.SelectedIndexChanged += UserLib_NameFilter_Changed; } } /// comboBox3 选择改变:按元件名称筛选用户库。 private void UserLib_NameFilter_Changed(object sender, EventArgs e) { var table = this.advancedDataGridView7.DataSource as DataTable; if (table == null) return; string sel = this.comboBox3.Text == null ? "" : this.comboBox3.Text.Trim(); try { if (sel.Length == 0 || sel == "全部") { if (table.DefaultView.RowFilter.Length > 0) table.DefaultView.RowFilter = ""; } else { string escaped = EscapeLikeValue(sel); // 转换为 LIKE 模式;不区分大小写由 DataView 默认不敏感处理 table.DefaultView.RowFilter = "[元件名称] LIKE '%" + escaped + "%'"; } } catch (Exception ex) { Log("[用户库筛选] 异常: " + ex.Message); } } /// /// button30 查找:在用户库按"元件名称"和"规格"两列不区分大小写模糊匹配。 /// 关键词为空则清除筛选。参考项目表 button28_Click 的实现。 /// private void UserLib_Search_Click(object sender, EventArgs e) { var table = this.advancedDataGridView7.DataSource as DataTable; if (table == null) { MessageBox.Show(this, "用户库暂无数据,无法查找。", "查找", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string keyword = this.textBox2.Text == null ? "" : this.textBox2.Text.Trim(); // 关键词为空:清除筛选,恢复显示全部(同时把名称筛选也复位) if (keyword.Length == 0) { if (table.DefaultView.RowFilter.Length > 0) table.DefaultView.RowFilter = ""; return; } // 转义 LIKE 特殊字符 string escaped = EscapeLikeValue(keyword); try { // 不区分大小写 + 同时匹配元件名称和规格两列(任一命中即显示) table.DefaultView.RowFilter = "[元件名称] LIKE '%" + escaped + "%' OR [规格] LIKE '%" + escaped + "%'"; } catch (System.Data.SyntaxErrorException ex) { MessageBox.Show(this, "查找条件解析失败,请检查输入的关键词。\n详情:" + ex.Message, "查找", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (table.DefaultView.Count == 0) { MessageBox.Show(this, "没有找到元件名称或规格包含 “" + keyword + "” 的元件。", "查找", MessageBoxButtons.OK, MessageBoxIcon.Information); } } // ============================================================ // 用户库工具栏(toolStrip1)事件 // · 插入(toolStripButton1):用户库选中行 → 元件表新增一行 // · 替换(toolStripButton2):用户库选中行 → 元件表选中行字段覆盖 // · 新增(toolStripButton3):用户库表格末尾加一空行(进入编辑) // · 修改(toolStripButton4):用户库选中行进入编辑 // · 删除(toolStripButton5):删除用户库选中行 // · 导入(toolStripButton6):从 CSV/Excel(xlsx) 导入到用户库 // · 导出(toolStripButton7):用户库全部数据导出为 CSV // · 打开模板(toolStripButton8):打开/下载用户库模板(CSV 表头) // ============================================================ /// 从用户库 DataGridViewRow 安全取出一个字段值(字符串)。 private static string UserLibGetField(DataGridViewRow row, string colName) { if (row == null || row.Cells == null) return ""; // 用户库列可能没 Name(只有 HeaderText),用 HeaderText 兜底查找 DataGridViewColumn col = null; foreach (DataGridViewColumn c in row.DataGridView.Columns) { if (c.Name == colName || c.HeaderText == colName || (c.DataPropertyName ?? "") == colName) { col = c; break; } } if (col == null) return ""; object v = row.Cells[col.Index].Value; return v == null ? "" : v.ToString(); } /// /// 把用户库选中行解析成 元件表插入所需的参数包。 /// 返回 null 表示行无效或空。 /// private struct UserLibRowData { public string 元件名称; public string 规格; public string 单位; public string 数量; public string 表价; public string 采购系数; public string 报出系数; public string 品牌; public string 系列; public string 元件类型; } private static UserLibRowData? UserLibExtractRow(DataGridViewRow row) { if (row == null) return null; var d = new UserLibRowData { 元件名称 = UserLibGetField(row, "元件名称"), 规格 = UserLibGetField(row, "规格"), 单位 = UserLibGetField(row, "单位"), 数量 = UserLibGetField(row, "数量"), 表价 = UserLibGetField(row, "表价"), 采购系数 = UserLibGetField(row, "采购系数"), 报出系数 = UserLibGetField(row, "报出系数"), 品牌 = UserLibGetField(row, "品牌"), 系列 = UserLibGetField(row, "系列"), 元件类型 = UserLibGetField(row, "元件类型"), }; // 元件名称和规格都为空 → 无效行 if (string.IsNullOrWhiteSpace(d.元件名称) && string.IsNullOrWhiteSpace(d.规格)) return null; return d; } // ---------- 插入 ---------- /// 插入:把用户库选中行数据插入到元件表(advancedDataGridView5)末尾。 private void UserLib_Insert_Click(object sender, EventArgs e) { // 必须先打开项目和图号/箱号,否则元件表无数据源 if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从用户库插入。", "用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 该箱号还没建过元件表时自动建表(否则 NewRow 后写"序号"列会抛异常) if (!确保元件表存在()) { MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从用户库插入。", "用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var row = GetFirstSelectedRow(this.advancedDataGridView7); if (row == null) { MessageBox.Show(this, "请先在用户库表格中选择一行再插入。", "用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var data = UserLibExtractRow(row); if (!data.HasValue) { MessageBox.Show(this, "选中的用户库行为空(元件名称和规格都为空),无法插入。", "用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var d = data.Value; // 结束用户库编辑态(避免选中行的值还没提交) try { this.advancedDataGridView7.EndEdit(); } catch { } // 复用现有的"插入元件到报价表"方法(它负责派生列计算 + 重算聚合 + 日志) // 参数:规格/表价/品牌/采购系数/系列 decimal price = 0m; decimal.TryParse(d.表价, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out price); // 用元件名称作为元件名称(而不是规格);调用现有方法时会自动用系列名清理逻辑 // 这里把"元件名称"字段放到系列参数,让现有方法按系列名处理 → 元件名称 = 清理(系列名) // 但用户库已有完整的元件名称/系列,不需要再清理,所以直接调用并把数据传准 // 为了不改动现有方法,采用直接填充元件表新行的方式: decimal 系数 = 1m; decimal.TryParse(d.采购系数, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 系数); if (系数 == 0m) 系数 = 1m; decimal 报出系数Val = 1m; decimal.TryParse(d.报出系数, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 报出系数Val); if (报出系数Val == 0m) 报出系数Val = 1m; decimal qty = 1m; decimal.TryParse(d.数量, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out qty); if (qty == 0m) qty = 1m; // 派生列计算 decimal 采购单价 = price * 系数; decimal 采购价 = 采购单价 * qty; decimal 报出单价 = 采购单价 * 报出系数Val; decimal 报出价 = 报出单价 * qty; // ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入; // 单选=false 返回后走原逻辑 System.Action 插入动作 = delegate { this.advancedDataGridView5.EndEdit(); // ★ 记住选中行索引,用于"在选中行下面插入" int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5); var newRow = _yuanjianTable.NewRow(); newRow["序号"] = "1"; // InsertRowBelowSelected 会自动重编号 newRow["元件名称"] = d.元件名称 ?? ""; newRow["规格"] = d.规格 ?? ""; newRow["品牌"] = d.品牌 ?? ""; newRow["表价"] = (double)price; newRow["数量"] = d.数量 ?? "1"; newRow["采购系数"] = 系数.ToString(System.Globalization.CultureInfo.InvariantCulture); newRow["报出系数"] = 报出系数Val.ToString(System.Globalization.CultureInfo.InvariantCulture); newRow["单位"] = d.单位 ?? ""; newRow["图纸规格"] = ""; newRow["系列"] = d.系列 ?? ""; // 用户库插入的元件,类型也设为"元件"(与网页插入一致) newRow["类型"] = "元件"; newRow["备注"] = ""; newRow["采购单价"] = (double)采购单价; newRow["采购价"] = (double)采购价; newRow["报出单价"] = (double)报出单价; newRow["报出价"] = (double)报出价; // ★ 插入到选中行下面(而不是末尾) InsertRowBelowSelected(_yuanjianTable, newRow, selectedIndex); // ★ 虚拟模式收尾:同步网格行数+全表保存 元件表插行收尾(); // 滚动到新行 int idx = _yuanjianTable.Rows.IndexOf(newRow); if (idx >= 0) { try { this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = idx; } catch { } } 重算聚合(); }; try { if (工具窗口批量插入(插入动作)) return; 插入动作(); Log("[用户库] 已插入到元件表: " + d.元件名称 + " / " + d.规格); } catch (Exception ex) { MessageBox.Show(this, "插入失败:\n" + ex.Message, "用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 替换 ---------- /// 替换:用用户库选中行的数据覆盖元件表(advancedDataGridView5)当前选中行。 private void UserLib_Replace_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号。", "用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var libRow = GetFirstSelectedRow(this.advancedDataGridView7); if (libRow == null) { MessageBox.Show(this, "请先在用户库表格中选择一行。", "用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var data = UserLibExtractRow(libRow); if (!data.HasValue) { MessageBox.Show(this, "选中的用户库行为空,无法替换。", "用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // ★ 选型页分支:选型页有选中行→目标=选型页选中行(判定用"有选中行"防焦点漂移) if (替换目标在选型页()) { var d0 = data.Value; 库替换走选型页(d0.元件名称 ?? "", d0.规格 ?? "", d0.系列 ?? "", d0.品牌 ?? "", d0.表价, d0.采购系数, "用户库"); return; } if (_yuanjianTable == null) { MessageBox.Show(this, "请先在报价软件中选择图号/箱号。", "用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var dgvRow = GetFirstSelectedRow(this.advancedDataGridView5); if (dgvRow == null || dgvRow.IsNewRow) { MessageBox.Show(this, "请先在元件表中选中要被替换的行。", "用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var d = data.Value; try { this.advancedDataGridView7.EndEdit(); } catch { } try { this.advancedDataGridView5.EndEdit(); } catch { } decimal price = 0m; decimal.TryParse(d.表价, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out price); decimal 系数 = 1m; decimal.TryParse(d.采购系数, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 系数); if (系数 == 0m) 系数 = 1m; // ★ 虚拟模式:写内存表+刷新行(数量/报出系数保留原值,由重算派生列时用) 写单元格(this.advancedDataGridView5, dgvRow.Index, "元件名称", d.元件名称 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "规格", d.规格 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "单位", d.单位 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "品牌", d.品牌 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "表价", (double)price); 写单元格(this.advancedDataGridView5, dgvRow.Index, "采购系数", 系数.ToString(System.Globalization.CultureInfo.InvariantCulture)); 写单元格(this.advancedDataGridView5, dgvRow.Index, "系列", d.系列 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "类型", "元件"); // 重算派生列 RecalcYuanjianRow(dgvRow); 重算聚合(); Log("[用户库] 已替换元件表选中行: " + d.元件名称 + " / " + d.规格); } // ---------- 新增 ---------- /// 新增:弹出编辑窗体,填好后添加到用户库表格末尾。 private void UserLib_New_Click(object sender, EventArgs e) { try { this.advancedDataGridView7.EndEdit(); var table = this.advancedDataGridView7.DataSource as DataTable; if (table == null) { MessageBox.Show(this, "用户库表格未绑定数据,无法新增。", "用户库新增", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 弹出编辑窗体(默认值) using (var dlg = new 用户库元件编辑()) { dlg.Text = "新增用户库元件"; dlg.单位 = "只"; dlg.数量 = "1"; dlg.表价 = "0"; dlg.采购系数 = "1"; dlg.报出系数 = "1"; dlg.元件类型 = "元件"; if (dlg.ShowDialog(this) != DialogResult.OK) return; // 把窗体数据写入新行 var newRow = table.NewRow(); newRow["元件名称"] = dlg.元件名称 ?? ""; newRow["规格"] = dlg.规格 ?? ""; newRow["单位"] = dlg.单位 ?? ""; newRow["数量"] = dlg.数量 ?? "1"; newRow["表价"] = dlg.表价 ?? "0"; newRow["采购系数"] = dlg.采购系数 ?? "1"; newRow["报出系数"] = dlg.报出系数 ?? "1"; newRow["品牌"] = dlg.品牌 ?? ""; newRow["系列"] = dlg.系列 ?? ""; newRow["元件类型"] = dlg.元件类型 ?? "元件"; table.Rows.Add(newRow); table.AcceptChanges(); // 滚动并选中最后一行 int lastIdx = this.advancedDataGridView7.Rows.Count - 1; if (lastIdx >= 0) { this.advancedDataGridView7.FirstDisplayedScrollingRowIndex = lastIdx; this.advancedDataGridView7.ClearSelection(); this.advancedDataGridView7.Rows[lastIdx].Selected = true; } // 保存到数据库 SaveUserLibToDatabase(); Log("[用户库] 新增元件: " + dlg.元件名称 + " / " + dlg.规格); } } catch (Exception ex) { MessageBox.Show(this, "新增失败:\n" + ex.Message, "用户库新增", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 修改 ---------- /// 修改:弹出编辑窗体,把用户库选中行的数据加载进去供用户修改。 private void UserLib_Modify_Click(object sender, EventArgs e) { var row = GetFirstSelectedRow(this.advancedDataGridView7); if (row == null) { MessageBox.Show(this, "请先在用户库中选择要修改的行。", "用户库修改", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { this.advancedDataGridView7.EndEdit(); using (var dlg = new 用户库元件编辑()) { dlg.Text = "修改用户库元件"; // 把当前行的数据加载到窗体 dlg.元件名称 = UserLibGetField(row, "元件名称"); dlg.规格 = UserLibGetField(row, "规格"); dlg.单位 = UserLibGetField(row, "单位"); dlg.数量 = UserLibGetField(row, "数量"); dlg.表价 = UserLibGetField(row, "表价"); dlg.采购系数 = UserLibGetField(row, "采购系数"); dlg.报出系数 = UserLibGetField(row, "报出系数"); dlg.品牌 = UserLibGetField(row, "品牌"); dlg.系列 = UserLibGetField(row, "系列"); dlg.元件类型 = UserLibGetField(row, "元件类型"); if (dlg.ShowDialog(this) != DialogResult.OK) return; // 把窗体数据写回选中行 SetUserLibField(row, "元件名称", dlg.元件名称); SetUserLibField(row, "规格", dlg.规格); SetUserLibField(row, "单位", dlg.单位); SetUserLibField(row, "数量", dlg.数量); SetUserLibField(row, "表价", dlg.表价); SetUserLibField(row, "采购系数", dlg.采购系数); SetUserLibField(row, "报出系数", dlg.报出系数); SetUserLibField(row, "品牌", dlg.品牌); SetUserLibField(row, "系列", dlg.系列); SetUserLibField(row, "元件类型", dlg.元件类型); // 保存到数据库 var table = this.advancedDataGridView7.DataSource as DataTable; if (table != null) table.AcceptChanges(); SaveUserLibToDatabase(); Log("[用户库] 修改元件: " + dlg.元件名称 + " / " + dlg.规格); } } catch (Exception ex) { MessageBox.Show(this, "修改失败:\n" + ex.Message, "用户库修改", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// 设置用户库行的某个字段值(按 DataPropertyName/HeaderText 匹配列)。 private static void SetUserLibField(DataGridViewRow row, string colName, string value) { if (row == null || row.Cells == null) return; foreach (DataGridViewColumn c in row.DataGridView.Columns) { if (c.Name == colName || c.HeaderText == colName || (c.DataPropertyName ?? "") == colName) { row.Cells[c.Index].Value = value ?? ""; return; } } } // ---------- 删除 ---------- /// 删除:删除用户库中所有选中行,并保存到数据库。 private void UserLib_Delete_Click(object sender, EventArgs e) { var rows = GetAllSelectedRows(this.advancedDataGridView7); if (rows == null || rows.Count == 0) { MessageBox.Show(this, "请先在用户库中选择要删除的行。", "用户库删除", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (MessageBox.Show(this, "确认删除选中的 " + rows.Count + " 行吗?", "用户库删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; try { this.advancedDataGridView7.EndEdit(); // 按索引倒序删除(避免索引错位) var sortedRows = new System.Collections.Generic.List(rows); sortedRows.Sort((a, b) => b.Index.CompareTo(a.Index)); foreach (var r in sortedRows) { if (r == null || r.IsNewRow) continue; this.advancedDataGridView7.Rows.RemoveAt(r.Index); } SaveUserLibToDatabase(); Log("[用户库] 已删除 " + rows.Count + " 行"); } catch (Exception ex) { MessageBox.Show(this, "删除失败:\n" + ex.Message, "用户库删除", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// 清除:清空用户库表所有数据(不可恢复,二次确认)。 private void UserLib_Clear_Click(object sender, EventArgs e) { var table = this.advancedDataGridView7.DataSource as DataTable; if (table == null || table.Rows.Count == 0) { MessageBox.Show(this, "用户库已经是空的。", "用户库清除", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 二次确认(高危操作) if (MessageBox.Show(this, "确认清空用户库的全部数据吗?\n当前共 " + table.Rows.Count + " 行,清除后不可恢复!", "用户库清除", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; try { this.advancedDataGridView7.EndEdit(); int 已清行数 = table.Rows.Count; table.Clear(); // 清内存 DataTable SaveUserLibToDatabase(); // 写库(库会跟着清空) Log("[用户库] 已清除全部数据: " + 已清行数 + " 行"); MessageBox.Show(this, "已清空用户库 " + 已清行数 + " 行。", "用户库清除", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show(this, "清空失败:\n" + ex.Message, "用户库清除", MessageBoxButtons.OK, MessageBoxIcon.Error); Log("[用户库] 清空异常: " + ex.Message); } } // ---------- 导入 ---------- /// 导入:从 CSV 文件导入数据到用户库(追加,不覆盖现有数据)。 private void UserLib_Import_Click(object sender, EventArgs e) { // 确保数据库存在 string dbPath = UserLibDbPath; if (!System.IO.File.Exists(dbPath)) { MessageBox.Show(this, "用户库数据库不存在,请先在 advancedDataGridView7 上右键「根据列标题创建表」。", "用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } using (var ofd = new OpenFileDialog { Title = "选择要导入的 CSV 文件", Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*", RestoreDirectory = true, }) { if (ofd.ShowDialog(this) != DialogResult.OK) return; try { string[] allLines = AutoDetectReadAllLines(ofd.FileName); if (allLines.Length == 0) { MessageBox.Show(this, "文件为空。", "用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 第一行是表头,解析列名 → 列索引 string[] headers = ParseCsvLine(allLines[0]); var colMap = new System.Collections.Generic.Dictionary(System.StringComparer.OrdinalIgnoreCase); for (int i = 0; i < headers.Length; i++) { string h = (headers[i] ?? "").Trim().Trim('"'); if (h.Length > 0) colMap[h] = i; } // 用户库的标准列 string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价", "采购系数", "报出系数", "品牌", "系列", "元件类型" }; // 检查至少有一个匹配的列 bool anyMatch = false; foreach (string c in userLibCols) if (colMap.ContainsKey(c)) { anyMatch = true; break; } if (!anyMatch) { MessageBox.Show(this, "CSV 表头与用户库列名不匹配。\n需要的列: " + string.Join(", ", userLibCols), "用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } int imported = 0; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); // 逐行解析并 INSERT for (int li = 1; li < allLines.Length; li++) { string line = allLines[li]; if (string.IsNullOrWhiteSpace(line)) continue; string[] fields = ParseCsvLine(line); // 取字段值(列名不存在时用空字符串) System.Collections.Generic.Dictionary vals = new System.Collections.Generic.Dictionary(); foreach (string c in userLibCols) { int idx; string v = (colMap.TryGetValue(c, out idx) && idx < fields.Length) ? (fields[idx] ?? "").Trim().Trim('"') : ""; vals[c] = v; } // 元件名称和规格都为空的行跳过 if (string.IsNullOrWhiteSpace(vals["元件名称"]) && string.IsNullOrWhiteSpace(vals["规格"])) continue; string sql = "INSERT INTO " + UserLibTableName + " (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " + "VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { foreach (string c in userLibCols) cmd.Parameters.AddWithValue("@" + c, vals[c]); cmd.ExecuteNonQuery(); } imported++; } } // 刷新表格 ReloadUserLibFromDatabase(); MessageBox.Show(this, "导入完成,共 " + imported + " 行。", "用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Information); Log("[用户库] 导入 " + imported + " 行"); } catch (Exception ex) { MessageBox.Show(this, "导入失败:\n" + ex.Message, "用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } /// /// 自动检测编码读取文本文件全部行(UTF-8 优先,GB2312 兜底)。 /// 用途:CSV 导入 — 用户用 Excel 编辑保存的 CSV 默认是 GBK 编码(尤其中文 Windows), /// 而程序导出的是 UTF-8。这里两种都能正确读取。 /// 检测规则: /// 1) 文件有 UTF-8 BOM (EF BB BF) → 直接按 UTF-8 读 /// 2) 文件有 GBK BOM (FF FE / FE FF) → 按对应 Unicode 读 /// 3) 无 BOM → 先按 UTF-8 严格解码,失败(出现乱码/异常)则改用 GB2312 /// private static string[] AutoDetectReadAllLines(string path) { byte[] bytes = System.IO.File.ReadAllBytes(path); if (bytes.Length == 0) return new string[0]; // 1) BOM 检测 if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) { return SplitLines(System.Text.Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3)); } if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) { return SplitLines(System.Text.Encoding.Unicode.GetString(bytes, 2, bytes.Length - 2)); } if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) { return SplitLines(System.Text.Encoding.BigEndianUnicode.GetString(bytes, 2, bytes.Length - 2)); } // 2) 无 BOM:先试 UTF-8 严格解码 // UTF-8 解码器遇到非法字节会替换成 \uFFFD,检测到该字符说明不是 UTF-8 try { var utf8 = new System.Text.UTF8Encoding(false, true); // throwOnInvalidBytes=true string s = utf8.GetString(bytes); if (s.IndexOf('\uFFFD') < 0) return SplitLines(s); } catch { /* 不是合法 UTF-8,继续尝试 GBK */ } // 3) GBK(GB2312)兜底 — 中文 Windows 上 Excel 默认保存的编码 return SplitLines(System.Text.Encoding.GetEncoding("GB2312").GetString(bytes)); } /// 把文本按 \r\n / \n / \r 拆成行(保留空行)。 private static string[] SplitLines(string text) { if (text == null) return new string[0]; // 用 Universal-line-break 拆分,兼容 Windows/Unix/Mac var lines = new System.Collections.Generic.List(); var sb = new System.Text.StringBuilder(); for (int i = 0; i < text.Length; i++) { char c = text[i]; if (c == '\r') { lines.Add(sb.ToString()); sb.Length = 0; if (i + 1 < text.Length && text[i + 1] == '\n') i++; } else if (c == '\n') { lines.Add(sb.ToString()); sb.Length = 0; } else { sb.Append(c); } } lines.Add(sb.ToString()); return lines.ToArray(); } /// 简单 CSV 行解析(支持双引号包裹、逗号分隔)。 private static string[] ParseCsvLine(string line) { if (string.IsNullOrEmpty(line)) return new string[0]; var list = new System.Collections.Generic.List(); bool inQuote = false; var sb = new System.Text.StringBuilder(); for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '"') { if (inQuote && i + 1 < line.Length && line[i + 1] == '"') { sb.Append('"'); i++; // 转义的引号 } else { inQuote = !inQuote; } } else if (c == ',' && !inQuote) { list.Add(sb.ToString()); sb.Length = 0; } else { sb.Append(c); } } list.Add(sb.ToString()); return list.ToArray(); } // ---------- 导出 ---------- /// 导出:把用户库全部数据导出为 CSV 文件。 private void UserLib_Export_Click(object sender, EventArgs e) { var table = this.advancedDataGridView7.DataSource as DataTable; if (table == null || table.Rows.Count == 0) { MessageBox.Show(this, "用户库没有数据可导出。", "用户库导出", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using (var sfd = new SaveFileDialog { Title = "导出用户库数据", Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*", FileName = "用户库_" + System.DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv", RestoreDirectory = true, }) { if (sfd.ShowDialog(this) != DialogResult.OK) return; try { var sb = new System.Text.StringBuilder(); // 表头(用用户库标准列名) string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价", "采购系数", "报出系数", "品牌", "系列", "元件类型" }; // 第一行:列名 for (int i = 0; i < userLibCols.Length; i++) { if (i > 0) sb.Append(","); sb.Append(CsvEscape(userLibCols[i])); } sb.Append("\r\n"); // 数据行 foreach (DataRow r in table.Rows) { for (int i = 0; i < userLibCols.Length; i++) { if (i > 0) sb.Append(","); // 如果表中没有这一列,直接写空 if (table.Columns.Contains(userLibCols[i])) sb.Append(CsvEscape(r[userLibCols[i]] == null ? "" : r[userLibCols[i]].ToString())); else sb.Append(""); } sb.Append("\r\n"); } System.IO.File.WriteAllText(sfd.FileName, sb.ToString(), System.Text.Encoding.UTF8); MessageBox.Show(this, "导出完成,共 " + table.Rows.Count + " 行。\n" + sfd.FileName, "用户库导出", MessageBoxButtons.OK, MessageBoxIcon.Information); Log("[用户库] 导出 " + table.Rows.Count + " 行到 " + sfd.FileName); } catch (Exception ex) { MessageBox.Show(this, "导出失败:\n" + ex.Message, "用户库导出", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } /// CSV 字段转义:含逗号/引号/换行时用双引号包裹。 private static string CsvEscape(string s) { if (s == null) return ""; if (s.IndexOfAny(new char[] { ',', '"', '\r', '\n' }) < 0) return s; return "\"" + s.Replace("\"", "\"\"") + "\""; } // ---------- 打开模板 ---------- /// 打开模板:生成一份只含表头的 CSV 模板文件,并打开所在目录。 private void UserLib_OpenTemplate_Click(object sender, EventArgs e) { using (var sfd = new SaveFileDialog { Title = "保存用户库模板", Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*", FileName = "用户库模板.csv", RestoreDirectory = true, }) { if (sfd.ShowDialog(this) != DialogResult.OK) return; try { string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价", "采购系数", "报出系数", "品牌", "系列", "元件类型" }; // 用 UTF-8 BOM,Excel 打开才不会乱码 var sb = new System.Text.StringBuilder(); sb.Append(string.Join(",", userLibCols)); sb.Append("\r\n"); // 加一行示例数据 sb.Append(string.Join(",", new string[] { "示例元件", "示例规格", "只", "1", "10.5", "1", "1.2", "XX品牌", "XX系列", "元件" })); sb.Append("\r\n"); System.IO.File.WriteAllText(sfd.FileName, sb.ToString(), new System.Text.UTF8Encoding(true)); MessageBox.Show(this, "模板已保存:\n" + sfd.FileName + "\n\n填好数据后用「导入」功能批量导入到用户库。", "用户库模板", MessageBoxButtons.OK, MessageBoxIcon.Information); Log("[用户库] 模板已保存: " + sfd.FileName); } catch (Exception ex) { MessageBox.Show(this, "保存模板失败:\n" + ex.Message, "用户库模板", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } /// /// 把用户库 DataTable 的所有数据整体写回数据库(DELETE + INSERT 全量)。 /// 用户库没有 PRIMARY KEY,无法 UPDATE 单行,采用全量重建方式。 /// private void SaveUserLibToDatabase() { string dbPath = UserLibDbPath; if (!System.IO.File.Exists(dbPath)) return; try { var table = this.advancedDataGridView7.DataSource as DataTable; if (table == null) return; string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价", "采购系数", "报出系数", "品牌", "系列", "元件类型" }; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); // 先清空 using (var cmdDel = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + UserLibTableName + ";", conn)) { cmdDel.ExecuteNonQuery(); } // 重新插入所有行 foreach (DataRow r in table.Rows) { if (r.RowState == DataRowState.Deleted) continue; string sql = "INSERT INTO " + UserLibTableName + " (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " + "VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { foreach (string c in userLibCols) cmd.Parameters.AddWithValue("@" + c, table.Columns.Contains(c) && r[c] != null ? r[c].ToString() : ""); cmd.ExecuteNonQuery(); } } } table.AcceptChanges(); Log("[用户库] 已保存到数据库"); // 保存后同步刷新 comboBox3 的下拉项(让新加进来的元件名称立即出现) LoadUserLibDistinctNames(); } catch (Exception ex) { Log("[用户库] 保存失败: " + ex.Message); } } // ============================================================ // 柜体库(advancedDataGridView10) — 功能与用户库完全一致 // · 插入(toolStripButton12):柜体库选中行 → 元件表新增一行 // · 替换(toolStripButton13):柜体库选中行 → 元件表选中行字段覆盖 // · 新增(toolStripButton14):弹出窗体新增柜体库元件 // · 修改(toolStripButton15):弹出窗体修改柜体库选中行 // · 删除(toolStripButton16):删除柜体库选中行 // · 筛选(comboBox4):按元件名称筛选 // · 查找(button31 + textBox5):按元件名称/规格模糊匹配 // ============================================================ /// /// 柜体库数据库路径(独立库,与用户库分开存储)。 /// 与 ui.cs 的 LoadSqliteData_advancedDataGridView10 一致。 /// private string GuiTiDbPath { get { return GetDbFullPath(@"sjk\advancedDataGridView10Table.db"); } } private const string GuiTiTableName = "advancedDataGridView10Table"; /// 从柜体库 DataGridViewRow 安全取出一个字段值(字符串)。 private static string GuiTiGetField(DataGridViewRow row, string colName) { if (row == null || row.Cells == null) return ""; // 柜体库列可能没 Name(只有 HeaderText),用 HeaderText 兜底查找 DataGridViewColumn col = null; foreach (DataGridViewColumn c in row.DataGridView.Columns) { if (c.Name == colName || c.HeaderText == colName || (c.DataPropertyName ?? "") == colName) { col = c; break; } } if (col == null) return ""; object v = row.Cells[col.Index].Value; return v == null ? "" : v.ToString(); } /// 设置柜体库行的某个字段值(按 DataPropertyName/HeaderText 匹配列)。 private static void SetGuiTiField(DataGridViewRow row, string colName, string value) { if (row == null || row.Cells == null) return; foreach (DataGridViewColumn c in row.DataGridView.Columns) { if (c.Name == colName || c.HeaderText == colName || (c.DataPropertyName ?? "") == colName) { row.Cells[c.Index].Value = value ?? ""; return; } } } /// /// 把柜体库选中行解析成 元件表插入所需的参数包。 /// 返回 null 表示行无效或空。 /// private struct GuiTiRowData { public string 元件名称; public string 规格; public string 单位; public string 数量; public string 表价; public string 采购系数; public string 报出系数; public string 品牌; public string 系列; public string 元件类型; } private static GuiTiRowData? GuiTiExtractRow(DataGridViewRow row) { if (row == null) return null; var d = new GuiTiRowData { 元件名称 = GuiTiGetField(row, "元件名称"), 规格 = GuiTiGetField(row, "规格"), 单位 = GuiTiGetField(row, "单位"), 数量 = GuiTiGetField(row, "数量"), 表价 = GuiTiGetField(row, "表价"), 采购系数 = GuiTiGetField(row, "采购系数"), 报出系数 = GuiTiGetField(row, "报出系数"), 品牌 = GuiTiGetField(row, "品牌"), 系列 = GuiTiGetField(row, "系列"), 元件类型 = GuiTiGetField(row, "元件类型"), }; // 元件名称和规格都为空 → 无效行 if (string.IsNullOrWhiteSpace(d.元件名称) && string.IsNullOrWhiteSpace(d.规格)) return null; return d; } // ---------- 插入 ---------- /// 插入:把柜体库选中行数据插入到元件表(advancedDataGridView5)末尾。 private void GuiTi_Insert_Click(object sender, EventArgs e) { // 必须先打开项目和图号/箱号,否则元件表无数据源 if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从柜体库插入。", "柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // ★ 该箱号还没建过元件表时自动建表(否则 NewRow 后写"序号"列会抛异常) if (!确保元件表存在()) { MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从柜体库插入。", "柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var row = GetFirstSelectedRow(this.advancedDataGridView10); if (row == null) { MessageBox.Show(this, "请先在柜体库表格中选择一行再插入。", "柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var data = GuiTiExtractRow(row); if (!data.HasValue) { MessageBox.Show(this, "选中的柜体库行为空(元件名称和规格都为空),无法插入。", "柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var d = data.Value; // 结束柜体库编辑态(避免选中行的值还没提交) try { this.advancedDataGridView10.EndEdit(); } catch { } decimal price = 0m; decimal.TryParse(d.表价, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out price); decimal 系数 = 1m; decimal.TryParse(d.采购系数, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 系数); if (系数 == 0m) 系数 = 1m; decimal 报出系数Val = 1m; decimal.TryParse(d.报出系数, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 报出系数Val); if (报出系数Val == 0m) 报出系数Val = 1m; decimal qty = 1m; decimal.TryParse(d.数量, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out qty); if (qty == 0m) qty = 1m; // 派生列计算 decimal 采购单价 = price * 系数; decimal 采购价 = 采购单价 * qty; decimal 报出单价 = 采购单价 * 报出系数Val; decimal 报出价 = 报出单价 * qty; // ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入; // 单选=false 返回后走原逻辑 System.Action 插入动作 = delegate { this.advancedDataGridView5.EndEdit(); // ★ 记住选中行索引,用于"在选中行下面插入" int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5); var newRow = _yuanjianTable.NewRow(); newRow["序号"] = "1"; // InsertRowBelowSelected 会自动重编号 newRow["元件名称"] = d.元件名称 ?? ""; newRow["规格"] = d.规格 ?? ""; newRow["品牌"] = d.品牌 ?? ""; newRow["表价"] = (double)price; newRow["数量"] = d.数量 ?? "1"; newRow["采购系数"] = 系数.ToString(System.Globalization.CultureInfo.InvariantCulture); newRow["报出系数"] = 报出系数Val.ToString(System.Globalization.CultureInfo.InvariantCulture); newRow["单位"] = d.单位 ?? ""; newRow["图纸规格"] = ""; newRow["系列"] = d.系列 ?? ""; // 柜体库插入的元件,类型也设为"元件"(与用户库一致) newRow["类型"] = "元件"; newRow["备注"] = ""; newRow["采购单价"] = (double)采购单价; newRow["采购价"] = (double)采购价; newRow["报出单价"] = (double)报出单价; newRow["报出价"] = (double)报出价; // ★ 插入到选中行下面(而不是末尾) InsertRowBelowSelected(_yuanjianTable, newRow, selectedIndex); // ★ 虚拟模式收尾:同步网格行数+全表保存 元件表插行收尾(); // 滚动到新行 int idx = _yuanjianTable.Rows.IndexOf(newRow); if (idx >= 0) { try { this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = idx; } catch { } } 重算聚合(); }; try { if (工具窗口批量插入(插入动作)) return; 插入动作(); Log("[柜体库] 已插入到元件表: " + d.元件名称 + " / " + d.规格); } catch (Exception ex) { MessageBox.Show(this, "插入失败:\n" + ex.Message, "柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 替换 ---------- /// 替换:用柜体库选中行的数据覆盖元件表(advancedDataGridView5)当前选中行。 private void GuiTi_Replace_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_currentProjectId)) { MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号。", "柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var libRow = GetFirstSelectedRow(this.advancedDataGridView10); if (libRow == null) { MessageBox.Show(this, "请先在柜体库表格中选择一行。", "柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var data = GuiTiExtractRow(libRow); if (!data.HasValue) { MessageBox.Show(this, "选中的柜体库行为空,无法替换。", "柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // ★ 选型页分支:选型页有选中行→目标=选型页选中行(判定用"有选中行"防焦点漂移) if (替换目标在选型页()) { var d0 = data.Value; 库替换走选型页(d0.元件名称 ?? "", d0.规格 ?? "", d0.系列 ?? "", d0.品牌 ?? "", d0.表价, d0.采购系数, "柜体库"); return; } if (_yuanjianTable == null) { MessageBox.Show(this, "请先在报价软件中选择图号/箱号。", "柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var dgvRow = GetFirstSelectedRow(this.advancedDataGridView5); if (dgvRow == null || dgvRow.IsNewRow) { MessageBox.Show(this, "请先在元件表中选中要被替换的行。", "柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var d = data.Value; try { this.advancedDataGridView10.EndEdit(); } catch { } try { this.advancedDataGridView5.EndEdit(); } catch { } decimal price = 0m; decimal.TryParse(d.表价, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out price); decimal 系数 = 1m; decimal.TryParse(d.采购系数, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 系数); if (系数 == 0m) 系数 = 1m; // ★ 虚拟模式:写内存表+刷新行(数量/报出系数保留原值) 写单元格(this.advancedDataGridView5, dgvRow.Index, "元件名称", d.元件名称 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "规格", d.规格 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "单位", d.单位 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "品牌", d.品牌 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "表价", (double)price); 写单元格(this.advancedDataGridView5, dgvRow.Index, "采购系数", 系数.ToString(System.Globalization.CultureInfo.InvariantCulture)); 写单元格(this.advancedDataGridView5, dgvRow.Index, "系列", d.系列 ?? ""); 写单元格(this.advancedDataGridView5, dgvRow.Index, "类型", "元件"); // 重算派生列 RecalcYuanjianRow(dgvRow); 重算聚合(); Log("[柜体库] 已替换元件表选中行: " + d.元件名称 + " / " + d.规格); } // ---------- 新增 ---------- /// 新增:弹出编辑窗体,填好后添加到柜体库表格末尾。 private void GuiTi_New_Click(object sender, EventArgs e) { try { this.advancedDataGridView10.EndEdit(); var table = this.advancedDataGridView10.DataSource as DataTable; if (table == null) { MessageBox.Show(this, "柜体库表格未绑定数据,无法新增。", "柜体库新增", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 弹出编辑窗体(默认值) using (var dlg = new 用户库元件编辑()) { dlg.Text = "新增柜体库元件"; dlg.单位 = "只"; dlg.数量 = "1"; dlg.表价 = "0"; dlg.采购系数 = "1"; dlg.报出系数 = "1"; dlg.元件类型 = "元件"; if (dlg.ShowDialog(this) != DialogResult.OK) return; // 柜体库表格 ReadOnly=true,写入 DataTable 不受影响 // 把窗体数据写入新行 var newRow = table.NewRow(); newRow["元件名称"] = dlg.元件名称 ?? ""; newRow["规格"] = dlg.规格 ?? ""; newRow["单位"] = dlg.单位 ?? ""; newRow["数量"] = dlg.数量 ?? "1"; newRow["表价"] = dlg.表价 ?? "0"; newRow["采购系数"] = dlg.采购系数 ?? "1"; newRow["报出系数"] = dlg.报出系数 ?? "1"; newRow["品牌"] = dlg.品牌 ?? ""; newRow["系列"] = dlg.系列 ?? ""; newRow["元件类型"] = dlg.元件类型 ?? "元件"; table.Rows.Add(newRow); table.AcceptChanges(); // 滚动并选中最后一行 int lastIdx = this.advancedDataGridView10.Rows.Count - 1; if (lastIdx >= 0) { this.advancedDataGridView10.FirstDisplayedScrollingRowIndex = lastIdx; this.advancedDataGridView10.ClearSelection(); this.advancedDataGridView10.Rows[lastIdx].Selected = true; } // 保存到数据库 SaveGuiTiToDatabase(); Log("[柜体库] 新增元件: " + dlg.元件名称 + " / " + dlg.规格); } } catch (Exception ex) { MessageBox.Show(this, "新增失败:\n" + ex.Message, "柜体库新增", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 修改 ---------- /// 修改:弹出编辑窗体,把柜体库选中行的数据加载进去供用户修改。 private void GuiTi_Modify_Click(object sender, EventArgs e) { var row = GetFirstSelectedRow(this.advancedDataGridView10); if (row == null) { MessageBox.Show(this, "请先在柜体库中选择要修改的行。", "柜体库修改", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { this.advancedDataGridView10.EndEdit(); using (var dlg = new 用户库元件编辑()) { dlg.Text = "修改柜体库元件"; // 把当前行的数据加载到窗体 dlg.元件名称 = GuiTiGetField(row, "元件名称"); dlg.规格 = GuiTiGetField(row, "规格"); dlg.单位 = GuiTiGetField(row, "单位"); dlg.数量 = GuiTiGetField(row, "数量"); dlg.表价 = GuiTiGetField(row, "表价"); dlg.采购系数 = GuiTiGetField(row, "采购系数"); dlg.报出系数 = GuiTiGetField(row, "报出系数"); dlg.品牌 = GuiTiGetField(row, "品牌"); dlg.系列 = GuiTiGetField(row, "系列"); dlg.元件类型 = GuiTiGetField(row, "元件类型"); if (dlg.ShowDialog(this) != DialogResult.OK) return; // 把窗体数据写回选中行(柜体库表格 ReadOnly=true, // 写入 DataGridViewRow.Cells 需要临时关闭 ReadOnly) bool prevRO = this.advancedDataGridView10.ReadOnly; try { this.advancedDataGridView10.ReadOnly = false; SetGuiTiField(row, "元件名称", dlg.元件名称); SetGuiTiField(row, "规格", dlg.规格); SetGuiTiField(row, "单位", dlg.单位); SetGuiTiField(row, "数量", dlg.数量); SetGuiTiField(row, "表价", dlg.表价); SetGuiTiField(row, "采购系数", dlg.采购系数); SetGuiTiField(row, "报出系数", dlg.报出系数); SetGuiTiField(row, "品牌", dlg.品牌); SetGuiTiField(row, "系列", dlg.系列); SetGuiTiField(row, "元件类型", dlg.元件类型); } finally { this.advancedDataGridView10.ReadOnly = prevRO; } // 保存到数据库 var table = this.advancedDataGridView10.DataSource as DataTable; if (table != null) table.AcceptChanges(); SaveGuiTiToDatabase(); Log("[柜体库] 修改元件: " + dlg.元件名称 + " / " + dlg.规格); } } catch (Exception ex) { MessageBox.Show(this, "修改失败:\n" + ex.Message, "柜体库修改", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ---------- 删除 ---------- /// 删除:删除柜体库中所有选中行,并保存到数据库。 private void GuiTi_Delete_Click(object sender, EventArgs e) { var rows = GetAllSelectedRows(this.advancedDataGridView10); if (rows == null || rows.Count == 0) { MessageBox.Show(this, "请先在柜体库中选择要删除的行。", "柜体库删除", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (MessageBox.Show(this, "确认删除选中的 " + rows.Count + " 行吗?", "柜体库删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; try { this.advancedDataGridView10.EndEdit(); // 按索引倒序删除(避免索引错位) var sortedRows = new System.Collections.Generic.List(rows); sortedRows.Sort((a, b) => b.Index.CompareTo(a.Index)); foreach (var r in sortedRows) { if (r == null || r.IsNewRow) continue; this.advancedDataGridView10.Rows.RemoveAt(r.Index); } SaveGuiTiToDatabase(); Log("[柜体库] 已删除 " + rows.Count + " 行"); } catch (Exception ex) { MessageBox.Show(this, "删除失败:\n" + ex.Message, "柜体库删除", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 从柜体库数据库重新查询并绑定到 advancedDataGridView10。 /// 与 ui.cs 的 LoadSqliteData_advancedDataGridView10 同款逻辑, /// 但放在主窗口.cs 中以便在编辑后主动刷新。 /// private void ReloadGuiTiFromDatabase() { string dbPath = GuiTiDbPath; if (!System.IO.File.Exists(dbPath)) return; try { string connStr = GetConnString(dbPath); var table = new DataTable(); using (var conn = new System.Data.SQLite.SQLiteConnection(connStr)) { conn.Open(); string sql = "SELECT * FROM " + GuiTiTableName + ";"; using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(sql, conn)) { adapter.Fill(table); } } this.advancedDataGridView10.AutoGenerateColumns = false; this.advancedDataGridView10.DataSource = table; // 同步刷新 comboBox4 的不重复元件名称下拉项 LoadGuiTiDistinctNames(); } catch (Exception ex) { Log("[柜体库] 刷新柜体库表格失败: " + ex.Message); } } /// /// 扫描柜体库(advancedDataGridView10)的"元件名称"列,把所有不重复的名称 /// 填入 comboBox4 作为下拉项(首项固定为"全部")。保留当前选中值。 /// private void LoadGuiTiDistinctNames() { // 取当前选中,稍后恢复(避免刷新时选中被重置) string prev = this.comboBox4.Text; var names = new System.Collections.Generic.List(); names.Add("全部"); var table = this.advancedDataGridView10.DataSource as DataTable; if (table != null && table.Columns.Contains("元件名称")) { var set = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase); foreach (DataRow r in table.Rows) { if (r == null) continue; object v = r["元件名称"]; if (v == null || v == DBNull.Value) continue; string s = v.ToString().Trim(); if (s.Length == 0) continue; // 用原大小写保留首次出现的写法 if (set.Add(s)) names.Add(s); } } // 临时解绑事件,避免填充项时反复触发筛选 this.comboBox4.SelectedIndexChanged -= GuiTi_NameFilter_Changed; try { this.comboBox4.Items.Clear(); this.comboBox4.Items.AddRange(names.ToArray()); // 恢复选中项 bool found = false; if (!string.IsNullOrEmpty(prev)) { foreach (var n in names) { if (string.Equals(n, prev, System.StringComparison.OrdinalIgnoreCase)) { this.comboBox4.Text = prev; found = true; break; } } } if (!found) this.comboBox4.Text = "全部"; } finally { this.comboBox4.SelectedIndexChanged += GuiTi_NameFilter_Changed; } } /// comboBox4 选择改变:按元件名称筛选柜体库。 private void GuiTi_NameFilter_Changed(object sender, EventArgs e) { var table = this.advancedDataGridView10.DataSource as DataTable; if (table == null) return; string sel = this.comboBox4.Text == null ? "" : this.comboBox4.Text.Trim(); try { if (sel.Length == 0 || sel == "全部") { if (table.DefaultView.RowFilter.Length > 0) table.DefaultView.RowFilter = ""; } else { string escaped = EscapeLikeValue(sel); // 转换为 LIKE 模式;不区分大小写由 DataView 默认不敏感处理 table.DefaultView.RowFilter = "[元件名称] LIKE '%" + escaped + "%'"; } } catch (Exception ex) { Log("[柜体库筛选] 异常: " + ex.Message); } } /// /// button31 查找:在柜体库按"元件名称"和"规格"两列不区分大小写模糊匹配。 /// 关键词为空则清除筛选。 /// private void GuiTi_Search_Click(object sender, EventArgs e) { var table = this.advancedDataGridView10.DataSource as DataTable; if (table == null) { MessageBox.Show(this, "柜体库暂无数据,无法查找。", "查找", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string keyword = this.textBox5.Text == null ? "" : this.textBox5.Text.Trim(); // 关键词为空:清除筛选,恢复显示全部(同时把名称筛选也复位) if (keyword.Length == 0) { if (table.DefaultView.RowFilter.Length > 0) table.DefaultView.RowFilter = ""; return; } // 转义 LIKE 特殊字符 string escaped = EscapeLikeValue(keyword); try { // 不区分大小写 + 同时匹配元件名称和规格两列(任一命中即显示) table.DefaultView.RowFilter = "[元件名称] LIKE '%" + escaped + "%' OR [规格] LIKE '%" + escaped + "%'"; } catch (System.Data.SyntaxErrorException ex) { MessageBox.Show(this, "查找条件解析失败,请检查输入的关键词。\n详情:" + ex.Message, "查找", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (table.DefaultView.Count == 0) { MessageBox.Show(this, "没有找到元件名称或规格包含 [" + keyword + "] 的元件。", "查找", MessageBoxButtons.OK, MessageBoxIcon.Information); } } /// /// 把柜体库 DataTable 的所有数据整体写回数据库(DELETE + INSERT 全量)。 /// 柜体库没有 PRIMARY KEY,无法 UPDATE 单行,采用全量重建方式。 /// private void SaveGuiTiToDatabase() { string dbPath = GuiTiDbPath; if (!System.IO.File.Exists(dbPath)) return; try { var table = this.advancedDataGridView10.DataSource as DataTable; if (table == null) return; string[] guiTiCols = { "元件名称", "规格", "单位", "数量", "表价", "采购系数", "报出系数", "品牌", "系列", "元件类型" }; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); // 先清空 using (var cmdDel = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + GuiTiTableName + ";", conn)) { cmdDel.ExecuteNonQuery(); } // 重新插入所有行 foreach (DataRow r in table.Rows) { if (r.RowState == DataRowState.Deleted) continue; string sql = "INSERT INTO " + GuiTiTableName + " (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " + "VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { foreach (string c in guiTiCols) cmd.Parameters.AddWithValue("@" + c, table.Columns.Contains(c) && r[c] != null ? r[c].ToString() : ""); cmd.ExecuteNonQuery(); } } } table.AcceptChanges(); Log("[柜体库] 已保存到数据库"); // 保存后同步刷新 comboBox4 的下拉项(让新加进来的元件名称立即出现) LoadGuiTiDistinctNames(); } catch (Exception ex) { Log("[柜体库] 保存失败: " + ex.Message); } } // ============================================================ // 数据库 CRUD:图号/箱号/元件(全部带关联字段) // ============================================================ /// 计算同级别下一个序号字符串(用于默认命名提示)。在当前项目的独立库内计数。 private string GetNextSeqNumber(string tableName, string nameCol, string projectId, string tuhao = null) { if (string.IsNullOrWhiteSpace(projectId)) return "1"; string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return "1"; string sql; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); if (tuhao == null) { sql = "SELECT COUNT(*) FROM " + tableName + ";"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { int n = 0; object o = cmd.ExecuteScalar(); if (o != null && o != DBNull.Value) n = Convert.ToInt32(o); return (n + 1).ToString(); } } else { sql = "SELECT COUNT(*) FROM " + tableName + " WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@th", tuhao); int n = 0; object o = cmd.ExecuteScalar(); if (o != null && o != DBNull.Value) n = Convert.ToInt32(o); return (n + 1).ToString(); } } } } catch { return "1"; } } // ---------- 图号 CRUD ---------- private void InsertTuhao(string projectId, string tuhao) { if (string.IsNullOrWhiteSpace(projectId)) return; EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); // 台数默认为 1(总价/成本总价/毛利公式依赖台数) // 公式列给默认值 0,避免空值导致 ValueType=double 时报 DataError // ★ 内部ID = 创建时的图号名,永远不变;表索引和 _currentTuhao 用它做关联 string sql = "INSERT INTO " + TuhaoTableName + " (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " + "VALUES (@seq, @th, @taishu, 0, 0, 0, 0, 0, 0, @iid);"; string seq = GetNextSeqNumber(TuhaoTableName, "图号", projectId); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@th", tuhao); cmd.Parameters.AddWithValue("@taishu", "1"); cmd.Parameters.AddWithValue("@iid", tuhao); // 内部ID = 创建时的图号名 cmd.ExecuteNonQuery(); } } } private void DeleteTuhao(string projectId, string tuhao) { if (string.IsNullOrWhiteSpace(projectId)) return; string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); // 1) 先查出该图号的所有箱号表/元件表真实表名(收集到列表,避免 reader 嵌套执行) var tablesToDrop = new System.Collections.Generic.List(); string selIdx = "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;"; using (var sel = new System.Data.SQLite.SQLiteCommand(selIdx, conn)) { sel.Parameters.AddWithValue("@th", tuhao); using (var reader = sel.ExecuteReader()) { while (reader.Read()) { string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0); if (!string.IsNullOrWhiteSpace(realTbl)) tablesToDrop.Add(realTbl); } } } // 2) reader 关闭后,逐个 DROP foreach (string realTbl in tablesToDrop) { string dropSql = "DROP TABLE IF EXISTS \"" + realTbl + "\";"; using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropSql, conn)) { dropCmd.ExecuteNonQuery(); } } // 3) 删图号本身(★ 按内部ID删,用户改图号名不影响删除) string delTh = "DELETE FROM " + TuhaoTableName + " WHERE 内部ID = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delTh, conn)) { cmd.Parameters.AddWithValue("@th", tuhao); cmd.ExecuteNonQuery(); } // 4) 删该图号的所有索引行(箱号+元件) string delIdx = "DELETE FROM 表索引 WHERE 图号 = @th;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delIdx, conn)) { cmd.Parameters.AddWithValue("@th", tuhao); cmd.ExecuteNonQuery(); } } } // ---------- 箱号 CRUD ---------- private void InsertXianghao(string projectId, string tuhao, string xianghao) { if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return; EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); // 查索引表:该图号是否已有箱号表?没有就生成ID建表+登记索引 string tblName = GetXianghaoTableByIndex(projectId, tuhao); if (string.IsNullOrEmpty(tblName)) { tblName = GetNextTableId(projectId, "xh"); CreateXianghaoTable(projectId, tblName); AddTableIndex(projectId, "箱号", tuhao, "", tblName); } string seq = GetXianghaoCount(projectId, tuhao); // 数量默认为 1(成本价/报出价公式依赖数量) // 单位默认为"台"(对应 Excel 箱号表 F2 的默认值) // 公式列给默认值 0,避免空值导致 ValueType=double 时报 DataError // ★ 内部ID = 创建时的箱号名,永远不变;表索引和 _currentXianghao 用它做关联 string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 箱号, 箱名, 单位, 数量," + " 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 内部ID) " + "VALUES (@seq, @xh, @xm, @unit, @shuliang," + " 0, 0, 0, 0, 0, 0, 0, @iid);"; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@xh", xianghao); cmd.Parameters.AddWithValue("@xm", ""); // 箱名默认空,由用户在表格里填写 cmd.Parameters.AddWithValue("@unit", "台"); cmd.Parameters.AddWithValue("@shuliang", "1"); cmd.Parameters.AddWithValue("@iid", xianghao); // 内部ID = 创建时的箱号名 cmd.ExecuteNonQuery(); } } } /// 按真实ID表名建一张空箱号表。 private void CreateXianghaoTable(string projectId, string realTableName) { string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); string sql = "CREATE TABLE \"" + realTableName + "\" (" + " 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," + " 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," + " 内部ID TEXT" + ");"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.ExecuteNonQuery(); } } // ★ 公式触发器兜底:重建项目库全部触发器(幂等,新表即刻具备自动计算能力) EnsureFormulaTriggersQuiet(projectId); } catch { } } /// 按真实ID表名建一张空元件表。 private void CreateYuanjianTable(string projectId, string realTableName) { string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return; try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); string sql = "CREATE TABLE \"" + realTableName + "\" (" + " 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," + " 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," + " 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," + " 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT" + ");"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.ExecuteNonQuery(); } } // ★ 公式触发器兜底:重建项目库全部触发器(幂等) EnsureFormulaTriggersQuiet(projectId); } catch { } } private void DeleteXianghao(string projectId, string tuhao, string xianghao) { if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return; string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return; // 查索引表拿该箱号的元件表真实表名,DROP 它,并删索引行 string yjTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); if (!string.IsNullOrEmpty(yjTbl)) { string dropYj = "DROP TABLE IF EXISTS \"" + yjTbl + "\";"; using (var cmd = new System.Data.SQLite.SQLiteCommand(dropYj, conn)) { cmd.ExecuteNonQuery(); } } // 从该图号的箱号表里删该箱号行(★ 按内部ID删,用户改箱号名不影响删除) string xhTbl = GetXianghaoTableByIndex(projectId, tuhao); if (!string.IsNullOrEmpty(xhTbl)) { string delXh = "DELETE FROM \"" + xhTbl + "\" WHERE 内部ID = @xh;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(delXh, conn)) { cmd.Parameters.AddWithValue("@xh", xianghao); cmd.ExecuteNonQuery(); } } } // 删索引行(元件) RemoveTableIndex(projectId, "元件", tuhao, xianghao); } // ---------- 元件 CRUD ---------- /// 确保指定箱号的元件表存在,不存在则创建空表+登记索引 private void EnsureYuanjianTableExists(string projectId, string tuhao, string xianghao) { string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao); if (string.IsNullOrEmpty(tblName)) { EnsureQuoteDbExists(projectId); tblName = GetNextTableId(projectId, "yj"); CreateYuanjianTable(projectId, tblName); AddTableIndex(projectId, "元件", tuhao, xianghao, tblName); } } /// 在指定箱号的元件表 INSERT 一行默认元件,返回新行 rowid(-1=失败)。 /// ★ 序号用 MAX(CAST(序号))+1 而不是 COUNT+1:删除行后序号有洞(如 1,2,5)时, /// COUNT+1 会排不到最后,覆盖"末行"的调用方就会盖错行(网页插入规格/表价为空的根因)。 private long InsertYuanjian(string projectId, string tuhao, string xianghao, string name) { if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(xianghao)) return -1; EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); // 查索引表:该箱号是否已有元件表?没有就生成ID建表+登记索引 EnsureYuanjianTableExists(projectId, tuhao, xianghao); string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao); // 元件序号:取最大序号+1(序号有洞时也保证排在最后) // ★ 费用行置底(2026-10-01):取【非费用行】的最大序号+1——新普通元件排普通区末尾, // 费用行序号保持更大,永远在最底;顺带幂等补"类型"列(老表可能没有) string seq; using (var conn0 = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn0.Open(); EnsureColumns(conn0, "\"" + tblName + "\"", new[] { "类型" }); string cntSql = "SELECT IFNULL(MAX(CAST(序号 AS INTEGER)),0)+1 FROM \"" + tblName + "\" WHERE IFNULL(类型,'')<>'费用';"; using (var c = new System.Data.SQLite.SQLiteCommand(cntSql, conn0)) { object o = c.ExecuteScalar(); seq = o == null || o == DBNull.Value ? "1" : Convert.ToString(o); } } // 公式列给默认值 0,避免空值导致 ValueType=double 时报 DataError // 类型默认填"元件"(右键新建的就是元件,与插入元件按钮一致) string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 元件名称, 单位, 数量, 采购系数, 报出系数," + " 表价, 采购单价, 采购价, 报出单价, 报出价, 类型) " + "VALUES (@seq, @name, @unit, @shuliang, @caigou, @baochu," + " 0, 0, 0, 0, 0, '元件');"; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@name", name); cmd.Parameters.AddWithValue("@unit", "只"); cmd.Parameters.AddWithValue("@shuliang", "1"); cmd.Parameters.AddWithValue("@caigou", "1"); cmd.Parameters.AddWithValue("@baochu", "1"); cmd.ExecuteNonQuery(); return conn.LastInsertRowId; // 新行 rowid,调用方按它定位覆盖 } } } /// 网页插入专用:一次 INSERT 完整字段的新行(规格/表价/品牌/系数/系列 全带上), /// 不再"插默认空行再覆盖"——彻底杜绝空行中间态。返回新行 rowid。 /// 数量默认1、单位只、报出系数默认1、类型元件;采购价/报出价按公式算。 private long InsertYuanjianFull(string projectId, string tuhao, string xianghao, string 名称, string 规格, decimal 表价, string 品牌, string 采购系数, string 系列名, decimal 数量 = 1m) { if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(xianghao)) return -1; EnsureQuoteDbExists(projectId); string dbPath = GetQuoteDbPath(projectId); EnsureYuanjianTableExists(projectId, tuhao, xianghao); string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao); // 序号=最大序号+1(删除行后有洞也排最后) // ★ 费用行置底(2026-10-01):取【非费用行】的最大序号+1,新元件排普通区末尾(费用行永远最底); // 顺带幂等补"类型"列(老表可能没有) string seq; using (var conn0 = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn0.Open(); EnsureColumns(conn0, "\"" + tblName + "\"", new[] { "类型" }); string cntSql = "SELECT IFNULL(MAX(CAST(序号 AS INTEGER)),0)+1 FROM \"" + tblName + "\" WHERE IFNULL(类型,'')<>'费用';"; using (var c = new System.Data.SQLite.SQLiteCommand(cntSql, conn0)) { object o = c.ExecuteScalar(); seq = o == null || o == DBNull.Value ? "1" : Convert.ToString(o); } } decimal 系数 = 1m; decimal.TryParse(采购系数, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 系数); if (系数 == 0m) 系数 = 1m; if (数量 < 1m) 数量 = 1m; decimal 采购单价 = 表价 * 系数; decimal 采购价 = 采购单价 * 数量; decimal 报出单价 = 采购单价 * 1m; decimal 报出价 = 报出单价 * 数量; string sql = "INSERT INTO \"" + tblName + "\"" + " (序号, 元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数," + " 采购单价, 采购价, 报出单价, 报出价, 品牌, 系列, 类型, 内部ID) " + "VALUES (@seq, @name, @spec, @unit, @shuliang, @biao, @caigou, @baochu," + " @cgdj, @cgj, @bcdj, @bcj, @brand, @xilie, '元件', @nid);"; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); // ★ A1(2026-09-30):新元件行写内部ID GUID(统一定位体系;表无该列时迁移已补) try { using (var ec = new System.Data.SQLite.SQLiteCommand( "ALTER TABLE \"" + tblName + "\" ADD COLUMN 内部ID TEXT;", conn)) { ec.ExecuteNonQuery(); } } catch { /* 列已存在 */ } using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@nid", System.Guid.NewGuid().ToString("N")); cmd.Parameters.AddWithValue("@seq", seq); cmd.Parameters.AddWithValue("@name", 名称 ?? ""); cmd.Parameters.AddWithValue("@spec", 规格 ?? ""); cmd.Parameters.AddWithValue("@unit", "只"); cmd.Parameters.AddWithValue("@shuliang", 数量.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)); cmd.Parameters.AddWithValue("@biao", (double)表价); cmd.Parameters.AddWithValue("@caigou", 系数.ToString(System.Globalization.CultureInfo.InvariantCulture)); cmd.Parameters.AddWithValue("@baochu", "1"); cmd.Parameters.AddWithValue("@cgdj", (double)采购单价); cmd.Parameters.AddWithValue("@cgj", (double)采购价); cmd.Parameters.AddWithValue("@bcdj", (double)报出单价); cmd.Parameters.AddWithValue("@bcj", (double)报出价); cmd.Parameters.AddWithValue("@brand", 品牌 ?? ""); cmd.Parameters.AddWithValue("@xilie", 系列名 ?? ""); cmd.ExecuteNonQuery(); return conn.LastInsertRowId; } } } /// ★ 事务版删除图号:DROP 其下箱号表/元件表 + 删图号行 + 清索引(批量删除共用事务)。 /// tuhaoNm=该行图号显示名:按显示名兜底清理扒图 Form3 按显示名登记的残留索引/表 /// (2026-09-21 残留根治——此前这类孤儿元件表被 AI 清单全量枚举读到)。 private void DeleteTuhaoTx(string projectId, string tuhao, string tuhaoNm, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { // 1) 收集该图号的所有动态表名 var tablesToDrop = new System.Collections.Generic.List(); using (var sel = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;", conn, tx)) { sel.Parameters.AddWithValue("@th", tuhao); using (var reader = sel.ExecuteReader()) { while (reader.Read()) { string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0); if (!string.IsNullOrWhiteSpace(realTbl)) tablesToDrop.Add(realTbl); } } } // 2) 逐个 DROP foreach (string realTbl in tablesToDrop) { using (var dropCmd = new System.Data.SQLite.SQLiteCommand( "DROP TABLE IF EXISTS \"" + realTbl + "\";", conn, tx)) dropCmd.ExecuteNonQuery(); } // 3) 删图号行(按内部ID;显示名兜底在步骤5) using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + TuhaoTableName + " WHERE 内部ID = @th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", tuhao); cmd.ExecuteNonQuery(); } // 4) 删索引行 using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 图号 = @th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", tuhao); cmd.ExecuteNonQuery(); } // 5) ★ 显示名兜底:扒图 Form3 按显示名登记 表索引/建表(无内部ID),上面按内部ID // 匹配不到 → 孤儿 xh_/yj_ 表+索引行残留。按显示名(项目内图号名唯一)再清一遍; // 同时删掉按显示名登记的图号行(老数据内部ID为空的行)。 if (!string.IsNullOrWhiteSpace(tuhaoNm)) { var more = new System.Collections.Generic.List(); using (var sel = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 图号 = @n;", conn, tx)) { sel.Parameters.AddWithValue("@n", tuhaoNm); using (var reader = sel.ExecuteReader()) { while (reader.Read()) { string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0); if (!string.IsNullOrWhiteSpace(realTbl)) more.Add(realTbl); } } } foreach (string realTbl in more) { using (var dropCmd = new System.Data.SQLite.SQLiteCommand( "DROP TABLE IF EXISTS \"" + realTbl + "\";", conn, tx)) dropCmd.ExecuteNonQuery(); } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM " + TuhaoTableName + " WHERE 图号 = @n;", conn, tx)) { cmd.Parameters.AddWithValue("@n", tuhaoNm); cmd.ExecuteNonQuery(); } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 图号 = @n;", conn, tx)) { cmd.Parameters.AddWithValue("@n", tuhaoNm); cmd.ExecuteNonQuery(); } } } /// ★ 事务版删除箱号:DROP 元件表 + 删箱号行 + 删元件索引行。 /// thNm/xhNm=图号/箱号显示名:按显示名组合兜底清理扒图 Form3 按显示名登记的 /// 孤儿元件表/索引行(2026-09-21 残留根治,AI 清单此前会读到这些已删数据)。 private void DeleteXianghaoTx(string projectId, string tuhao, string tuhaoNm, string xianghao, string xianghaoNm, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string yjTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao); if (!string.IsNullOrEmpty(yjTbl)) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "DROP TABLE IF EXISTS \"" + yjTbl + "\";", conn, tx)) cmd.ExecuteNonQuery(); } string xhTbl = GetXianghaoTableByIndex(projectId, tuhao); if (!string.IsNullOrEmpty(xhTbl)) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM \"" + xhTbl + "\" WHERE 内部ID = @xh;", conn, tx)) { cmd.Parameters.AddWithValue("@xh", xianghao); cmd.ExecuteNonQuery(); } } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 类型='元件' AND 图号=@th AND 箱号=@xh;", conn, tx)) { cmd.Parameters.AddWithValue("@th", tuhao); cmd.Parameters.AddWithValue("@xh", xianghao); cmd.ExecuteNonQuery(); } // ★ 显示名组合兜底:扒图 Form3 建的元件索引按(图号显示名,箱号显示名)登记, // 上面按内部ID组合匹配不到 → yj 表与索引行残留。同组合清一遍; // 箱号表里的显示名行(老数据内部ID为空)一并删除。 if (!string.IsNullOrWhiteSpace(tuhaoNm) && !string.IsNullOrWhiteSpace(xianghaoNm)) { var more = new System.Collections.Generic.List(); using (var sel = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 类型='元件' AND 图号=@tn AND 箱号=@xn;", conn, tx)) { sel.Parameters.AddWithValue("@tn", tuhaoNm); sel.Parameters.AddWithValue("@xn", xianghaoNm); using (var reader = sel.ExecuteReader()) { while (reader.Read()) { string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0); if (!string.IsNullOrWhiteSpace(realTbl)) more.Add(realTbl); } } } foreach (string realTbl in more) { using (var dropCmd = new System.Data.SQLite.SQLiteCommand( "DROP TABLE IF EXISTS \"" + realTbl + "\";", conn, tx)) dropCmd.ExecuteNonQuery(); } using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM 表索引 WHERE 类型='元件' AND 图号=@tn AND 箱号=@xn;", conn, tx)) { cmd.Parameters.AddWithValue("@tn", tuhaoNm); cmd.Parameters.AddWithValue("@xn", xianghaoNm); cmd.ExecuteNonQuery(); } if (!string.IsNullOrEmpty(xhTbl)) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM \"" + xhTbl + "\" WHERE 箱号 = @xn;", conn, tx)) { cmd.Parameters.AddWithValue("@xn", xianghaoNm); cmd.ExecuteNonQuery(); } } } } /// ★ 事务版删除元件行(按序号)。 private void DeleteYuanjianTx(string projectId, string tuhao, string xianghao, string seq, System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx) { string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao); if (string.IsNullOrEmpty(tblName)) return; using (var cmd = new System.Data.SQLite.SQLiteCommand( "DELETE FROM \"" + tblName + "\" WHERE 序号 = @seq;", conn, tx)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.ExecuteNonQuery(); } } private void DeleteYuanjian(string projectId, string tuhao, string xianghao, string seq) { if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(xianghao)) return; string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return; // 查索引表拿该箱号元件表的真实表名,按序号删 string realTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao); if (string.IsNullOrEmpty(realTbl)) return; string sql = "DELETE FROM \"" + realTbl + "\" WHERE 序号 = @seq;"; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath))) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@seq", seq); cmd.ExecuteNonQuery(); } } } // ---------- 项目级联删除(删除项目时直接删除该项目的独立报价库文件)---------- private void CascadeDeleteByProject(string projectId) { if (string.IsNullOrWhiteSpace(projectId)) return; 删除报价库文件必须成功(GetQuoteDbPath(projectId), projectId); } /// 彻底删除项目报价库文件(.db 及 -wal/-shm 伴随文件)——删不掉必须报错,绝不静默放弃。 /// ★ 池化连接(Pooling=True)会持有文件句柄,直接删必失败——先 ClearAllPools 再删 /// (方案库删除的已验证做法),按项目复制 File.Copy 兜底同款节奏重试 3 次 /// (ClearAllPools + GC + 等待 + 递增延时)。仍失败则抛异常:残留文件会被同编号的 /// 新建项目"复活"旧数据(实测:删了项目再建同编号,打开全是旧数据),必须让用户看到。 private static void 删除报价库文件必须成功(string dbPath, string projectId) { if (!System.IO.File.Exists(dbPath)) return; System.Exception lastErr = null; for (int attempt = 1; attempt <= 3; attempt++) { try { System.Data.SQLite.SQLiteConnection.ClearAllPools(); GC.Collect(); GC.WaitForPendingFinalizers(); System.Threading.Thread.Sleep(50 * attempt); System.IO.File.Delete(dbPath); // WAL 日志/共享内存伴随文件一并清掉(残留会让下次打开读到旧事务) foreach (string ext in new[] { "-wal", "-shm" }) { string p = dbPath + ext; if (System.IO.File.Exists(p)) { try { System.IO.File.Delete(p); } catch { } } } return; } catch (System.Exception ex) { lastErr = ex; 写日志("[删除项目] 删库文件第 " + attempt + " 次失败 [" + projectId + "]: " + ex.Message); } } throw new System.Exception("项目数据库文件删除失败(可能被其他程序占用,请关闭后重试):\n" + dbPath + "\n" + lastErr.Message); } // 提示:在设计器的属性窗口(事件页)双击事件名, // 会自动在此生成事件处理方法骨架。 } }