using System; using System.Data; using System.IO; using System.Windows.Forms; using WeifenLuo.WinFormsUI.Docking; namespace 成套报价软件 { /// /// 停靠窗口:方案(储存报价方案)。 /// 项目页 = 方案列表(sjk\方案库\ 下每个 .db 文件 = 一个方案,即一份完整项目库副本); /// 双击/右键打开 → 报价页三表从该方案文件加载(只读浏览)。 /// 与主窗口项目模型一致:一个方案一个库文件,天然隔离。 /// public partial class DockWindow8 : DockContent { private const string 图号表名 = "advancedDataGridView9Table"; private string _当前方案文件 = ""; private int _联动层 = 0; // 联动加载深度(防嵌套重入) public DockWindow8() { InitializeComponent(); this.BackColor = System.Drawing.Color.White; // ===== 项目页(绑定模式,方案列表) ===== advancedDataGridView6.AutoGenerateColumns = false; advancedDataGridView6.Dock = DockStyle.Fill; // 项目页网格填满页签 // ===== 报价页三表:★ 与主窗口完全相同的虚拟模式(MainForm.cs 52~60 原样复制) ===== advancedDataGridView图号.AutoGenerateColumns = false; advancedDataGridView箱号.AutoGenerateColumns = false; advancedDataGridView元件.AutoGenerateColumns = false; advancedDataGridView图号.VirtualMode = true; advancedDataGridView箱号.VirtualMode = true; advancedDataGridView元件.VirtualMode = true; // 虚拟模式供数/写回(主窗口 Dgv_CellValueNeeded/Dgv_CellValuePushed 的方案版) advancedDataGridView图号.CellValueNeeded += 方案表_CellValueNeeded; advancedDataGridView箱号.CellValueNeeded += 方案表_CellValueNeeded; advancedDataGridView元件.CellValueNeeded += 方案表_CellValueNeeded; advancedDataGridView图号.CellValuePushed += 方案表_CellValuePushed; advancedDataGridView箱号.CellValuePushed += 方案表_CellValuePushed; advancedDataGridView元件.CellValuePushed += 方案表_CellValuePushed; // 编辑立即提交(主窗口 Grid_DirtyStateChanged 原样) advancedDataGridView图号.CurrentCellDirtyStateChanged += 方案表_DirtyStateChanged; advancedDataGridView箱号.CurrentCellDirtyStateChanged += 方案表_DirtyStateChanged; advancedDataGridView元件.CurrentCellDirtyStateChanged += 方案表_DirtyStateChanged; // 数据异常静默 advancedDataGridView图号.DataError += (s, e) => { e.ThrowException = false; }; advancedDataGridView箱号.DataError += (s, e) => { e.ThrowException = false; }; advancedDataGridView元件.DataError += (s, e) => { e.ThrowException = false; }; // ★ 报价页三表联动(点图号→箱号表,点箱号→元件表) advancedDataGridView图号.SelectionChanged += 图号表_选中变化; advancedDataGridView箱号.SelectionChanged += 箱号表_选中变化; Load += (s, e) => 刷新项目页(); // ★ 每次窗口显示时自动刷新方案列表(存为方案后切过来立即看到) VisibleChanged += (s, e) => { 写诊断("方案窗口 VisibleChanged, Visible=" + Visible); if (Visible) 刷新项目页(); }; // 报价页三表右键菜单(删除 = 从当前打开的方案文件中删除) var 图号菜单 = new ContextMenuStrip(); 图号菜单.Items.Add("从方案中删除该图号", null, 图号_删除_Click); advancedDataGridView图号.ContextMenuStrip = 图号菜单; var 箱号菜单 = new ContextMenuStrip(); 箱号菜单.Items.Add("从方案中删除该箱号", null, 箱号_删除_Click); advancedDataGridView箱号.ContextMenuStrip = 箱号菜单; var 元件菜单 = new ContextMenuStrip(); 元件菜单.Items.Add("从方案中删除选中元件", null, 元件_删除_Click); advancedDataGridView元件.ContextMenuStrip = 元件菜单; // 项目页右键菜单 var menu = new ContextMenuStrip(); menu.Items.Add("打开方案", null, 打开方案_Click); menu.Items.Add("删除方案", null, 删除方案_Click); advancedDataGridView6.ContextMenuStrip = menu; // 项目页双击 = 打开方案 advancedDataGridView6.CellDoubleClick += 方案项目页_双击; } /// 方案库目录(exe 旁 sjk\方案库\)。 private string 方案目录 { get { string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sjk", "方案库"); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); return dir; } } /// 主窗体引用(停靠面板的宿主窗体)。 private MainForm 主窗体 { get { return DockPanel == null ? null : DockPanel.FindForm() as MainForm; } } /// 绑定项目页网格(方案列表;由主窗体在方案数据变化时调用)。 public void 绑定项目页(System.Data.DataTable list) { advancedDataGridView6.AutoGenerateColumns = false; advancedDataGridView6.DataSource = list; advancedDataGridView6.Refresh(); } /// 重新加载当前打开的方案(粘贴/加入等操作完成后刷新显示;未打开则只刷方案列表)。 public void 重新加载当前方案() { if (!string.IsNullOrEmpty(_当前方案文件) && File.Exists(_当前方案文件)) 打开方案(_当前方案文件); 刷新项目页(); } /// 主窗体"存为方案"后调用:切换并打开新存的方案文件,报价页立即显示其内容。 public void 打开方案文件(string 方案文件路径) { if (string.IsNullOrEmpty(方案文件路径) || !File.Exists(方案文件路径)) { 刷新项目页(); return; } _当前方案文件 = 方案文件路径; 打开方案(方案文件路径); 刷新项目页(); } /// 当前打开的方案文件路径(报价页三表的数据源;空=未打开)。 public string 当前方案文件 { get { return _当前方案文件; } } /// 方案报价页当前选中的图号名(图号表当前行"图号"列;未选中返回空)。 /// 供主窗口"加入方案库"定位插入目标:用户在方案里选中哪个图号,就插到哪个图号下。 public string 方案选中图号 { get { if (advancedDataGridView图号 == null || advancedDataGridView图号.CurrentRow == null) return ""; return Cell文本(advancedDataGridView图号.CurrentRow, "图号"); } } /// 方案报价页当前选中的箱号内部ID(箱号表当前行"内部ID"列,空则退回"箱号"显示名;未选中返回空)。 /// 供主窗口"加入方案库"定位插入目标:用户在方案里选中哪个箱号,元件就插到它的元件表。 public string 方案选中箱号ID { get { if (advancedDataGridView箱号 == null || advancedDataGridView箱号.CurrentRow == null) return ""; string id = Cell文本(advancedDataGridView箱号.CurrentRow, "内部ID"); if (string.IsNullOrEmpty(id)) id = Cell文本(advancedDataGridView箱号.CurrentRow, "箱号"); return id; } } // ============ 项目页:方案列表 ============ /// 方案窗口诊断日志(logs\方案窗口诊断.log)。 private static void 写诊断(string msg) { try { string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); Directory.CreateDirectory(dir); File.AppendAllText(Path.Combine(dir, "方案窗口诊断.log"), DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + System.Environment.NewLine); } catch { } } /// 扫描方案库目录,列出全部方案(每文件读项目信息+图号表汇总)。 public void 刷新项目页() { 写诊断("刷新项目页 开始, 目录=" + 方案目录); var list = new DataTable(); foreach (var c in new[] { "项目编号", "项目名称", "客户名称", "客户地址", "联系人", "联系电话", "项目日期", "箱柜台数", "成本总价", "报出总价", "利润", "利润率", "文件名" }) list.Columns.Add(c); try { Directory.CreateDirectory(方案目录); // ★ 每文件最多尝试 2 次(短暂锁等待 300ms 后重试,防瞬时 BUSY 跳过) foreach (var file in Directory.GetFiles(方案目录, "*.db")) { bool added = false; for (int attempt = 0; attempt < 2 && !added; attempt++) { if (attempt > 0) System.Threading.Thread.Sleep(300); try { using (var conn = new System.Data.SQLite.SQLiteConnection(连接串(file))) { conn.Open(); string 编号 = "", 名称 = "", 客户 = "", 地址 = "", 联系人 = "", 电话 = "", 日期 = ""; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 项目编号,项目名称,客户名称,客户地址,联系人,联系电话,项目日期 FROM 项目信息 LIMIT 1;", conn)) using (var r = cmd.ExecuteReader()) { if (r.Read()) { 编号 = r.IsDBNull(0) ? "" : r.GetString(0); 名称 = r.IsDBNull(1) ? "" : r.GetString(1); 客户 = r.IsDBNull(2) ? "" : r.GetString(2); 地址 = r.IsDBNull(3) ? "" : r.GetString(3); 联系人 = r.IsDBNull(4) ? "" : r.GetString(4); 电话 = r.IsDBNull(5) ? "" : r.GetString(5); 日期 = r.IsDBNull(6) ? "" : r.GetString(6); } } decimal 台数 = 0, 箱柜 = 0, 总价 = 0, 成本 = 0, 毛利 = 0; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COALESCE(SUM(CAST(\"台数\" AS REAL)),0), COALESCE(SUM(CAST(\"箱柜数量\" AS REAL)),0), " + "COALESCE(SUM(CAST(\"总价\" AS REAL)),0), COALESCE(SUM(CAST(\"成本总价\" AS REAL)),0), " + "COALESCE(SUM(CAST(\"毛利\" AS REAL)),0) FROM \"" + 图号表名 + "\";", conn)) using (var r = cmd.ExecuteReader()) { if (r.Read()) { 台数 = Convert.ToDecimal(r.GetValue(0)); 箱柜 = Convert.ToDecimal(r.GetValue(1)); 总价 = Convert.ToDecimal(r.GetValue(2)); 成本 = Convert.ToDecimal(r.GetValue(3)); 毛利 = Convert.ToDecimal(r.GetValue(4)); } } list.Rows.Add(编号, 名称, 客户, 地址, 联系人, 电话, 日期, 台数.ToString("0.##"), 箱柜.ToString("0.##"), 总价.ToString("0.##"), 成本.ToString("0.##"), (成本 == 0 ? "" : (毛利 / 成本 * 100).ToString("0.##")), Path.GetFileName(file)); added = true; } } catch (Exception exFile) { 写诊断("文件读取失败(第" + (attempt + 1) + "次): " + Path.GetFileName(file) + " - " + exFile.Message); } } } } catch (Exception ex) { 写诊断("扫描异常: " + ex.Message); } 写诊断("扫描完成, 方案行数=" + list.Rows.Count); advancedDataGridView6.DataSource = list; } // ============ 项目页右键 ============ private DataGridViewRow 当前选中行() { if (advancedDataGridView6.CurrentRow != null && !advancedDataGridView6.CurrentRow.IsNewRow) return advancedDataGridView6.CurrentRow; foreach (DataGridViewRow r in advancedDataGridView6.SelectedRows) if (!r.IsNewRow) return r; return null; } private void 打开方案_Click(object sender, EventArgs e) { var row = 当前选中行(); if (row == null) { MessageBox.Show(this, "请先选择要打开的方案。", "打开方案", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string file = 取行值(row, "文件名"); if (string.IsNullOrEmpty(file)) return; 打开方案(Path.Combine(方案目录, file)); } /// 项目页双击:打开该方案。 private void 方案项目页_双击(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) return; var row = advancedDataGridView6.Rows[e.RowIndex]; string file = 取行值(row, "文件名"); if (string.IsNullOrEmpty(file)) return; 打开方案(Path.Combine(方案目录, file)); } private void 删除方案_Click(object sender, EventArgs e) { var row = 当前选中行(); if (row == null) { MessageBox.Show(this, "请先选择要删除的方案。", "删除方案", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } string file = 取行值(row, "文件名"); string 名称 = 取行值(row, "项目名称"); if (string.IsNullOrEmpty(file)) return; if (MessageBox.Show(this, "确认删除方案 [" + 名称 + "]?\n\n方案文件将被删除,不影响任何项目数据。", "删除方案", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return; try { string full = Path.Combine(方案目录, file); if (File.Exists(full)) { if (_当前方案文件 == full) { _当前方案文件 = ""; 清空报价页(); } // ★ 清空连接池:池化连接会持有文件句柄,导致删除报"正由另一进程使用" System.Data.SQLite.SQLiteConnection.ClearAllPools(); File.Delete(full); } 刷新项目页(); } catch (Exception ex) { MessageBox.Show(this, "删除失败:\n" + ex.Message, "删除方案", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ============ 方案内容删除(作用于当前打开的方案文件) ============ /// 右键删除当前方案中的选中图号(连其箱号/元件子树)。 private void 图号_删除_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_当前方案文件)) { MessageBox.Show(this, "请先打开一个方案。", "删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (advancedDataGridView图号.CurrentRow == null) return; string 图号 = Cell文本(advancedDataGridView图号.CurrentRow, "图号"); if (string.IsNullOrEmpty(图号)) return; if (MessageBox.Show(this, "确认从方案中删除图号 [" + 图号 + "]?\n其下所有箱号、元件也会一并删除。", "删除图号", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return; using (var conn = new System.Data.SQLite.SQLiteConnection(连接串(_当前方案文件))) { conn.Open(); using (var tx = conn.BeginTransaction()) { foreach (var t in 方案表清单(conn, tx, 图号, null)) Exec(conn, tx, "DROP TABLE IF EXISTS \"" + t + "\";"); Exec(conn, tx, "DELETE FROM 表索引 WHERE 图号 = @th;", new SQLiteP("@th", 图号)); Exec(conn, tx, "DELETE FROM \"" + 图号表名 + "\" WHERE 图号 = @th;", new SQLiteP("@th", 图号)); tx.Commit(); } } 打开方案(_当前方案文件); // 重载刷新 } /// 右键删除当前方案中的选中箱号(连其元件表);并刷新图号行聚合。 private void 箱号_删除_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_当前方案文件)) { MessageBox.Show(this, "请先打开一个方案。", "删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (advancedDataGridView箱号.CurrentRow == null) return; string 箱号 = Cell文本(advancedDataGridView箱号.CurrentRow, "内部ID"); if (string.IsNullOrEmpty(箱号)) 箱号 = Cell文本(advancedDataGridView箱号.CurrentRow, "箱号"); string 图号 = advancedDataGridView图号.CurrentRow == null ? "" : Cell文本(advancedDataGridView图号.CurrentRow, "图号"); if (string.IsNullOrEmpty(箱号) || string.IsNullOrEmpty(图号)) return; if (MessageBox.Show(this, "确认从方案中删除箱号 [" + 箱号 + "]?\n其元件数据也会一并删除。", "删除箱号", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return; using (var conn = new System.Data.SQLite.SQLiteConnection(连接串(_当前方案文件))) { conn.Open(); using (var tx = conn.BeginTransaction()) { foreach (var t in 方案表清单(conn, tx, 图号, 箱号)) Exec(conn, tx, "DROP TABLE IF EXISTS \"" + t + "\";"); Exec(conn, tx, "DELETE FROM 表索引 WHERE 图号 = @th AND 箱号 = @xh;", new SQLiteP("@th", 图号), new SQLiteP("@xh", 箱号)); Exec(conn, tx, "DELETE FROM \"" + 查方案表("箱号", 图号, null) + "\" WHERE \"内部ID\" = @xh;", new SQLiteP("@xh", 箱号)); tx.Commit(); } } 打开方案(_当前方案文件); } /// 右键删除当前方案中选中的元件行(删除后刷新箱号/图号聚合)。 private void 元件_删除_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_当前方案文件)) { MessageBox.Show(this, "请先打开一个方案。", "删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (advancedDataGridView元件.CurrentRow == null) return; string 序号 = Cell文本(advancedDataGridView元件.CurrentRow, "序号"); string 图号 = "", 箱号 = ""; if (advancedDataGridView图号.CurrentRow != null) 图号 = Cell文本(advancedDataGridView图号.CurrentRow, "图号"); if (advancedDataGridView箱号.CurrentRow != null) { 箱号 = Cell文本(advancedDataGridView箱号.CurrentRow, "内部ID"); if (string.IsNullOrEmpty(箱号)) 箱号 = Cell文本(advancedDataGridView箱号.CurrentRow, "箱号"); } if (string.IsNullOrEmpty(序号) || string.IsNullOrEmpty(图号) || string.IsNullOrEmpty(箱号)) return; if (MessageBox.Show(this, "确认从方案中删除选中的元件?", "删除元件", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return; string yjTbl = 查方案表("元件", 图号, 箱号); if (string.IsNullOrEmpty(yjTbl)) return; using (var conn = new System.Data.SQLite.SQLiteConnection(连接串(_当前方案文件))) { conn.Open(); using (var tx = conn.BeginTransaction()) { Exec(conn, tx, "DELETE FROM \"" + yjTbl + "\" WHERE 序号 = @s;", new SQLiteP("@s", 序号)); tx.Commit(); } } 刷新元件和聚合(); } /// 简单参数封装(WHERE 参数)。 private struct SQLiteP { public string Name; public string Value; public SQLiteP(string name, string value) { Name = name; Value = value; } } private static void Exec(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string sql, params SQLiteP[] ps) { using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) { foreach (var p in ps) cmd.Parameters.AddWithValue(p.Name, p.Value); cmd.ExecuteNonQuery(); } } /// 查方案文件的表索引:该图号(可选箱号)关联的全部实际表名。 private System.Collections.Generic.List 方案表清单( System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 图号, string 箱号) { var list = new System.Collections.Generic.List(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th" + (箱号 != null ? " AND 箱号 = @xh" : "") + ";", conn, tx)) { cmd.Parameters.AddWithValue("@th", 图号); if (箱号 != null) cmd.Parameters.AddWithValue("@xh", 箱号); using (var r = cmd.ExecuteReader()) while (r.Read()) { string t = r.IsDBNull(0) ? "" : r.GetString(0); if (!string.IsNullOrEmpty(t)) list.Add(t); } } return list; } /// 删除元件后重刷元件表与箱号/图号聚合(简单重开方案)。 private void 刷新元件和聚合() { string f = _当前方案文件; if (string.IsNullOrEmpty(f)) return; 打开方案(f); } // ============ 报价页:从方案文件加载三表(虚拟模式,与主窗口 Reload 相同顺序) ============ /// 当前三表的数据源字段(虚拟模式供数用;TableName=库中真实表名)。 private DataTable _方案图号表, _方案箱号表, _方案元件表; private DataTable 取方案数据表(DataGridView dgv) { if (dgv == advancedDataGridView图号) return _方案图号表; if (dgv == advancedDataGridView箱号) return _方案箱号表; if (dgv == advancedDataGridView元件) return _方案元件表; return null; } /// 安全设置行数:ADGV 列头排序/过滤后会激活内部数据绑定, /// 此时直接设 RowCount 抛"不能在数据绑定的 DataGridView 控件上设置 RowCount 属性" /// (加入方案库成功但方案窗口刷新失败,界面看不到新数据)。先解绑再设。 private static void 安全设行数(DataGridView dgv, int count) { if (dgv.DataSource != null) dgv.DataSource = null; dgv.RowCount = count; dgv.Invalidate(); } private void 清空方案表(DataGridView dgv) { if (dgv == advancedDataGridView图号) _方案图号表 = null; else if (dgv == advancedDataGridView箱号) _方案箱号表 = null; else if (dgv == advancedDataGridView元件) _方案元件表 = null; 安全设行数(dgv, 0); } /// 加载一张表到虚拟模式(★先换字段表引用,再设 RowCount,最后 Invalidate——主窗口同序)。 private void 加载方案表(DataGridView dgv, string dbPath, string sql) { var table = new DataTable(); using (var conn = new System.Data.SQLite.SQLiteConnection(连接串(dbPath))) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) ad.Fill(table); } // ★ TableName=库中真实表名(Fill 不会自动设置;直写库 UPDATE 靠它定位表) var mFrom = System.Text.RegularExpressions.Regex.Match(sql, @"FROM\s+""([^""]+)"""); if (mFrom.Success) table.TableName = mFrom.Groups[1].Value; if (dgv == advancedDataGridView图号) _方案图号表 = table; else if (dgv == advancedDataGridView箱号) _方案箱号表 = table; else if (dgv == advancedDataGridView元件) _方案元件表 = table; 安全设行数(dgv, table.Rows.Count); } /// 打开方案:整份加载到报价页三表。 private void 打开方案(string 方案文件) { 写联动日志("打开方案: " + 方案文件); if (!File.Exists(方案文件)) { MessageBox.Show(this, "方案文件不存在。", "打开方案", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } _联动层++; try { _当前方案文件 = 方案文件; tabControl1.SelectedTab = tabPage2; 加载方案表(advancedDataGridView图号, 方案文件, "SELECT rowid AS \"_rowid_\", * FROM \"" + 图号表名 + "\" ORDER BY CAST(序号 AS INTEGER);"); 清空方案表(advancedDataGridView箱号); 清空方案表(advancedDataGridView元件); // 默认选中第一个图号 → 联动箱号/元件 if (advancedDataGridView图号.Rows.Count > 0) { advancedDataGridView图号.ClearSelection(); advancedDataGridView图号.Rows[0].Selected = true; advancedDataGridView图号.CurrentCell = advancedDataGridView图号.Rows[0].Cells[0]; int old = _联动层; _联动层 = 0; // 程序化联动不受防重入 guard 挡 try { 图号表_选中变化(advancedDataGridView图号, EventArgs.Empty); } finally { _联动层 = old; } } } finally { _联动层 = 0; } } private void 图号表_选中变化(object sender, EventArgs e) { 写联动日志("图号联动进入: 层=" + _联动层 + " 方案文件=" + _当前方案文件); if (_联动层 > 0 || string.IsNullOrEmpty(_当前方案文件)) return; if (advancedDataGridView图号.CurrentRow == null) return; _联动层++; try { string 图号 = Cell文本(advancedDataGridView图号.CurrentRow, "图号"); if (string.IsNullOrEmpty(图号)) return; string xhTbl = 查方案表("箱号", 图号, null); if (string.IsNullOrEmpty(xhTbl)) { 清空方案表(advancedDataGridView箱号); 清空方案表(advancedDataGridView元件); return; } 加载方案表(advancedDataGridView箱号, _当前方案文件, "SELECT rowid AS \"_rowid_\", * FROM \"" + xhTbl + "\" ORDER BY CAST(序号 AS INTEGER);"); 清空方案表(advancedDataGridView元件); // 默认选中第一个箱号 → 元件 if (advancedDataGridView箱号.Rows.Count > 0) { advancedDataGridView箱号.ClearSelection(); advancedDataGridView箱号.Rows[0].Selected = true; advancedDataGridView箱号.CurrentCell = advancedDataGridView箱号.Rows[0].Cells[0]; int old = _联动层; _联动层 = 0; try { 箱号表_选中变化(advancedDataGridView箱号, EventArgs.Empty); } finally { _联动层 = old; } } } finally { _联动层 = 0; } } private void 箱号表_选中变化(object sender, EventArgs e) { 写联动日志("箱号联动进入: 层=" + _联动层 + " 方案文件=" + _当前方案文件); if (_联动层 > 0 || string.IsNullOrEmpty(_当前方案文件)) return; if (advancedDataGridView图号.CurrentRow == null || advancedDataGridView箱号.CurrentRow == null) return; _联动层++; try { string 图号 = Cell文本(advancedDataGridView图号.CurrentRow, "图号"); string 箱号 = Cell文本(advancedDataGridView箱号.CurrentRow, "内部ID"); if (string.IsNullOrEmpty(箱号)) 箱号 = Cell文本(advancedDataGridView箱号.CurrentRow, "箱号"); if (string.IsNullOrEmpty(图号) || string.IsNullOrEmpty(箱号)) return; string yjTbl = 查方案表("元件", 图号, 箱号); if (string.IsNullOrEmpty(yjTbl)) { 清空方案表(advancedDataGridView元件); return; } 加载方案表(advancedDataGridView元件, _当前方案文件, "SELECT rowid AS \"_rowid_\", * FROM \"" + yjTbl + "\" ORDER BY CAST(序号 AS INTEGER);"); } finally { _联动层 = 0; } } private void 清空报价页() { 清空方案表(advancedDataGridView图号); 清空方案表(advancedDataGridView箱号); 清空方案表(advancedDataGridView元件); } // ============ 工具 ============ /// 方案联动专用日志(logs\方案联动.log)。 private static void 写联动日志(string msg) { try { string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); Directory.CreateDirectory(dir); File.AppendAllText(Path.Combine(dir, "方案联动.log"), DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + System.Environment.NewLine); } catch { } } private string 连接串(string dbPath) { return "Data Source=\"" + dbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=3000;"; } private string 查方案表(string 类型, string 图号, string 箱号) { using (var conn = new System.Data.SQLite.SQLiteConnection(连接串(_当前方案文件))) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 类型=@t AND 图号=@th" + (箱号 != null ? " AND 箱号=@xh" : "") + " LIMIT 1;", conn)) { cmd.Parameters.AddWithValue("@t", 类型); cmd.Parameters.AddWithValue("@th", 图号); if (箱号 != null) cmd.Parameters.AddWithValue("@xh", 箱号); object o = cmd.ExecuteScalar(); return o == null || o == DBNull.Value ? null : o.ToString(); } } } /// 取行值:报价三表(虚拟模式)按 row.Index 从字段表取;项目页(绑定模式)从 DataBoundItem 取。 private string Cell文本(DataGridViewRow row, string dataPropertyName) { // 绑定模式(项目页方案列表):从数据源行取,不受网格列显示与否影响 var drv = row.DataBoundItem as System.Data.DataRowView; if (drv != null && drv.Row != null && drv.Row.Table.Columns.Contains(dataPropertyName)) { object v = drv.Row[dataPropertyName]; return v == null || v == DBNull.Value ? "" : v.ToString().Trim(); } // 虚拟模式(报价三表):按行号查字段表 var table = 取方案数据表(row.DataGridView); if (table != null && row.Index >= 0 && row.Index < table.Rows.Count && table.Columns.Contains(dataPropertyName)) { object v = table.Rows[row.Index][dataPropertyName]; return v == null || v == DBNull.Value ? "" : v.ToString().Trim(); } foreach (DataGridViewCell c in row.Cells) { if (c.OwningColumn != null && c.OwningColumn.DataPropertyName == dataPropertyName) return c.Value == null ? "" : c.Value.ToString(); } return ""; } private string 取行值(DataGridViewRow row, string dataPropertyName) { return Cell文本(row, dataPropertyName); } // ============================================================ // 方案三表实时保存 —— 主窗口报价页保存链的完整复制 // (虚拟模式:CellValueNeeded 供数 / CellValuePushed 写回 / // 内存重算链乐观显示 / 300ms 防抖队列 / 后台单事务写库+聚合) // ============================================================ /// 虚拟模式供数(主窗口 Dgv_CellValueNeeded 同款)。 private void 方案表_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { var dgv = sender as DataGridView; var table = 取方案数据表(dgv); if (table == null || e.RowIndex < 0 || e.RowIndex >= table.Rows.Count) { e.Value = ""; return; } if (e.ColumnIndex < 0 || e.ColumnIndex >= dgv.Columns.Count) { e.Value = ""; return; } string colName = dgv.Columns[e.ColumnIndex].DataPropertyName; if (string.IsNullOrEmpty(colName) || !table.Columns.Contains(colName)) { e.Value = ""; return; } object v = table.Rows[e.RowIndex][colName]; e.Value = (v == null || v == DBNull.Value) ? "" : v; } /// 编辑立即提交(主窗口 Grid_DirtyStateChanged 同款)。 private void 方案表_DirtyStateChanged(object sender, EventArgs e) { var dgv = sender as DataGridView; if (dgv != null && dgv.IsCurrentCellDirty) dgv.CommitEdit(DataGridViewDataErrorContexts.Commit); } /// 虚拟模式写回:写内存表 + 内存重算链(乐观显示) + 入队直写方案文件。 private void 方案表_CellValuePushed(object sender, DataGridViewCellValueEventArgs e) { 写联动日志("[Pushed] 触发: 表=" + ((DataGridView)sender).Name + " 行=" + e.RowIndex + " 值=" + (e.Value == null ? "" : e.Value.ToString())); var dgv = sender as DataGridView; var table = 取方案数据表(dgv); if (table == null || e.RowIndex < 0 || e.RowIndex >= table.Rows.Count) return; if (e.ColumnIndex < 0 || e.ColumnIndex >= dgv.Columns.Count) return; string colName = dgv.Columns[e.ColumnIndex].DataPropertyName; if (string.IsNullOrEmpty(colName) || colName == "_rowid_" || !table.Columns.Contains(colName)) return; table.Rows[e.RowIndex][colName] = (e.Value == null || e.Value == DBNull.Value) ? DBNull.Value : e.Value; // ★ 内存重算链(乐观显示):元件→箱号→图号 if (_联动层 == 0 && !_方案派生列.Contains(colName)) { _联动层++; try { if (dgv == advancedDataGridView元件) 方案重算元件行(e.RowIndex); else if (dgv == advancedDataGridView箱号) 方案重算箱号行(e.RowIndex); else if (dgv == advancedDataGridView图号) 方案重算图号行(e.RowIndex); } finally { _联动层--; } dgv.Invalidate(); if (dgv == advancedDataGridView元件) { advancedDataGridView箱号.Invalidate(); advancedDataGridView图号.Invalidate(); } else if (dgv == advancedDataGridView箱号) advancedDataGridView图号.Invalidate(); } // ★ 入队直写(主窗口 入队直写库 同款:300ms 防抖聚合) if (!_方案派生列.Contains(colName)) { 写联动日志("[Pushed] 入队: " + colName); 入队方案直写(dgv, e.RowIndex, colName, e.Value); } else 写联动日志("[Pushed] 派生列跳过: " + colName); } // ---- 内存重算链(主窗口 RecalcYuanjianRow/RecalcXianghaoRowFromYuanjian/RecalcTuhaoRowFromXianghao 的方案版) ---- private static decimal 方案数值(object v) { decimal d; return v == null || v == DBNull.Value ? 0 : (decimal.TryParse(Convert.ToString(v).Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out d) ? d : 0); } private void 方案重算元件行(int rowIndex) { var t = _方案元件表; if (t == null || rowIndex < 0 || rowIndex >= t.Rows.Count) return; var r = t.Rows[rowIndex]; decimal 表价 = 方案数值(r["表价"]), 采购系数 = 方案数值(r["采购系数"]), 报出系数 = 方案数值(r["报出系数"]), 数量 = 方案数值(r["数量"]); decimal 采购单价 = 表价 * 采购系数; if (t.Columns.Contains("采购单价")) r["采购单价"] = 采购单价; if (t.Columns.Contains("采购价")) r["采购价"] = 采购单价 * 数量; if (t.Columns.Contains("报出单价")) r["报出单价"] = 采购单价 * 报出系数; if (t.Columns.Contains("报出价")) r["报出价"] = 采购单价 * 报出系数 * 数量; 方案重算箱号行全部(); // 所属箱号聚合 → 图号聚合 } private void 方案重算箱号行(int rowIndex) { // 箱号行数量变:按内存元件表 SUM 重算本行 + 图号聚合 方案重算箱号行全部(); } private void 方案重算箱号行全部() { var xh = _方案箱号表; var yj = _方案元件表; if (xh == null) return; decimal 采购合计 = 0, 报出合计 = 0; if (yj != null) { foreach (DataRow r in yj.Rows) { 采购合计 += 方案数值(r["采购价"]); 报出合计 += 方案数值(r["报出价"]); } } // 写到当前选中箱号行(方案库每箱号一张元件表,当前元件表即当前箱号) if (advancedDataGridView箱号.CurrentRow != null) { int idx = advancedDataGridView箱号.CurrentRow.Index; if (idx >= 0 && idx < xh.Rows.Count) { var row = xh.Rows[idx]; decimal 数量 = 方案数值(row["数量"]); if (xh.Columns.Contains("单台成本")) row["单台成本"] = 采购合计; if (xh.Columns.Contains("成本价")) row["成本价"] = 采购合计 * 数量; if (xh.Columns.Contains("单台报出")) row["单台报出"] = 报出合计; if (xh.Columns.Contains("报出价")) row["报出价"] = 报出合计 * 数量; decimal 成本价 = 采购合计 * 数量, 报出价 = 报出合计 * 数量; if (xh.Columns.Contains("利润")) row["利润"] = 报出价 - 成本价; // ★ 口径(2026-09-29):利润率保持小数;毛利率存百分数值(×100,保留2位) if (xh.Columns.Contains("利润率")) row["利润率"] = 成本价 == 0 ? 0 : (报出价 - 成本价) / 成本价; if (xh.Columns.Contains("毛利率")) row["毛利率"] = 报出价 == 0 ? 0 : Math.Round((报出价 - 成本价) / 报出价 * 100m, 2); } } 方案重算图号行全部(); } private void 方案重算图号行(int rowIndex) { 方案重算图号行全部(); } private void 方案重算图号行全部() { var th = _方案图号表; var xh = _方案箱号表; if (th == null || xh == null) return; decimal 箱柜数量 = 0, 报出合计 = 0, 成本合计 = 0; foreach (DataRow r in xh.Rows) { 箱柜数量 += 方案数值(r["数量"]); 报出合计 += 方案数值(r["报出价"]); 成本合计 += 方案数值(r["成本价"]); } if (advancedDataGridView图号.CurrentRow != null) { int idx = advancedDataGridView图号.CurrentRow.Index; if (idx >= 0 && idx < th.Rows.Count) { var row = th.Rows[idx]; decimal 台数 = 方案数值(row["台数"]); decimal 总价 = 报出合计 * 台数, 成本总价 = 成本合计 * 台数; if (th.Columns.Contains("箱柜数量")) row["箱柜数量"] = 箱柜数量; if (th.Columns.Contains("单价")) row["单价"] = 报出合计; if (th.Columns.Contains("总价")) row["总价"] = 总价; if (th.Columns.Contains("成本总价")) row["成本总价"] = 成本总价; if (th.Columns.Contains("毛利")) row["毛利"] = 总价 - 成本总价; // ★ 口径统一(2026-09-29):毛利率存百分数值(×100,保留2位) if (th.Columns.Contains("毛利率")) row["毛利率"] = 总价 == 0 ? 0 : Math.Round((总价 - 成本总价) / 总价 * 100m, 2); } } } // ---- 防抖队列 + 后台事务(主窗口 编辑直写库.cs 的方案版) ---- /// 派生列集合(由计算维护;用户编辑这些列不触发直写)。 private static readonly System.Collections.Generic.HashSet _方案派生列 = new System.Collections.Generic.HashSet( new[] { "采购单价", "采购价", "报出单价", "报出价", "单台成本", "成本价", "单台报出", "利润", "利润率", "毛利率", "箱柜数量", "单价", "总价", "成本总价", "毛利" }); /// 待写库项:表名|rowid|列 → 值(后写覆盖先写)。主窗口 _待写项 同款。 private readonly System.Collections.Generic.Dictionary _方案待写项 = new System.Collections.Generic.Dictionary(); /// 防抖定时器(300ms)。主窗口 _直写Timer 同款。 private Timer _方案直写Timer; private void 入队方案直写(DataGridView dgv, int rowIndex, string colName, object value) { var table = 取方案数据表(dgv); if (table == null || !table.Columns.Contains("_rowid_")) return; if (rowIndex < 0 || rowIndex >= table.Rows.Count) return; long rowid = table.Rows[rowIndex]["_rowid_"] is long ? Convert.ToInt64(table.Rows[rowIndex]["_rowid_"]) : 0; if (rowid <= 0) return; lock (_方案待写项) { _方案待写项[table.TableName + "|" + rowid + "|" + colName] = new[] { table.TableName, rowid.ToString(), colName, value == null ? "" : value.ToString() }; } if (_方案直写Timer == null) { _方案直写Timer = new Timer(); _方案直写Timer.Interval = 300; _方案直写Timer.Tick += 方案直写到期; } _方案直写Timer.Stop(); _方案直写Timer.Start(); } /// 防抖到期:批量直写方案文件(后台单事务)+按公式刷聚合。 private void 方案直写到期(object sender, EventArgs e) { _方案直写Timer.Stop(); if (_方案待写项.Count == 0 || string.IsNullOrEmpty(_当前方案文件)) return; System.Collections.Generic.List 批; lock (_方案待写项) { 批 = new System.Collections.Generic.List(_方案待写项.Values); _方案待写项.Clear(); } string dbPath = _当前方案文件; System.Threading.Tasks.Task.Run(delegate { try { using (var conn = new System.Data.SQLite.SQLiteConnection(连接串(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { // ★ 图号改名同步:改"图号"列时同步内部ID与表索引(否则联动按新名查不到旧表,箱号/元件全空) foreach (var item in 批) { if (item[0] == 图号表名 && item[2] == "图号") { string 新名 = item[3].Trim(); if (新名.Length > 0) { string 旧名 = null; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 图号 FROM \"" + 图号表名 + "\" WHERE rowid=@r;", conn, tx)) { cmd.Parameters.AddWithValue("@r", long.Parse(item[1])); object o = cmd.ExecuteScalar(); 旧名 = o == null || o == DBNull.Value ? null : o.ToString(); } if (!string.IsNullOrEmpty(旧名) && 旧名 != 新名) { Exec(conn, tx, "UPDATE 表索引 SET 图号='" + 新名.Replace("'", "''") + "' WHERE 图号='" + 旧名.Replace("'", "''") + "';"); Exec(conn, tx, "UPDATE \"" + 图号表名 + "\" SET 内部ID='" + 新名.Replace("'", "''") + "' WHERE rowid=" + item[1] + ";"); 写联动日志("[改名] 图号 " + 旧名 + " → " + 新名 + " (索引+内部ID已同步)"); } } } } foreach (var item in 批) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "UPDATE \"" + item[0].Replace("\"", "") + "\" SET \"" + item[2].Replace("\"", "") + "\"=@v WHERE rowid=@r;", conn, tx)) { cmd.Parameters.AddWithValue("@v", item[3]); cmd.Parameters.AddWithValue("@r", long.Parse(item[1])); cmd.ExecuteNonQuery(); } // 元件行:重算本行派生价 if (item[0].StartsWith("yj_")) { string yjSet = 计算引擎.生成Set片段(计算引擎.加载公式(GetFak连接(), "元件"), "ROW", null); if (yjSet.Length > 0) Exec(conn, tx, "UPDATE \"" + item[0] + "\" SET " + yjSet + " WHERE rowid=" + item[1] + ";"); } } // 聚合链:反查 图号/箱号 → 箱号行聚合 + 图号行聚合 string 图号 = null, 箱号 = null; foreach (var item in 批) { if (item[0].StartsWith("yj_")) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 图号,箱号 FROM 表索引 WHERE 类型='元件' AND 实际表名=@t LIMIT 1;", conn, tx)) { cmd.Parameters.AddWithValue("@t", item[0]); using (var r = cmd.ExecuteReader()) if (r.Read()) { 图号 = r.IsDBNull(0) ? null : r.GetString(0); 箱号 = r.IsDBNull(1) ? null : r.GetString(1); } } } else if (item[0].StartsWith("xh_")) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 图号 FROM 表索引 WHERE 类型='箱号' AND 实际表名=@t LIMIT 1;", conn, tx)) { cmd.Parameters.AddWithValue("@t", item[0]); using (var r = cmd.ExecuteReader()) if (r.Read()) { 图号 = r.IsDBNull(0) ? null : r.GetString(0); 箱号 = null; } } } } if (!string.IsNullOrEmpty(图号)) { string xhTbl = 查方案表2(conn, tx, "箱号", 图号, null); if (!string.IsNullOrEmpty(xhTbl)) { if (!string.IsNullOrEmpty(箱号)) { string yjTbl = 查方案表2(conn, tx, "元件", 图号, 箱号); if (!string.IsNullOrEmpty(yjTbl)) { string xhSet = 计算引擎.生成Set片段(计算引擎.加载公式(GetFak连接(), "箱号"), "ROW", yjTbl); if (xhSet.Length > 0) Exec(conn, tx, "UPDATE \"" + xhTbl + "\" SET " + xhSet + " WHERE \"内部ID\"='" + 箱号.Replace("'", "''") + "';"); } } string thSet = 计算引擎.生成Set片段(计算引擎.加载公式(GetFak连接(), "图号"), "ROW", xhTbl); if (thSet.Length > 0) Exec(conn, tx, "UPDATE \"" + 图号表名 + "\" SET " + thSet + " WHERE \"内部ID\"='" + 图号.Replace("'", "''") + "';"); } } tx.Commit(); } } 写联动日志("方案直写完成: " + 批.Count + " 项"); } catch (Exception ex) { 写联动日志("方案直写异常: " + ex.Message); } }); } /// 简单 SQL 执行。 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 string 查方案表2(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 类型, string 图号, string 箱号) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 类型=@t AND 图号=@th" + (箱号 != null ? " AND 箱号=@xh" : "") + " LIMIT 1;", conn, tx)) { cmd.Parameters.AddWithValue("@t", 类型); cmd.Parameters.AddWithValue("@th", 图号); if (箱号 != null) cmd.Parameters.AddWithValue("@xh", 箱号); object o = cmd.ExecuteScalar(); return o == null || o == DBNull.Value ? null : o.ToString(); } } /// 方案窗口自己的 fak.db 连接(公式表来源;已打开)。 private System.Data.SQLite.SQLiteConnection GetFak连接() { var conn = new System.Data.SQLite.SQLiteConnection("Data Source=" + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sjk", "fak.db") + ";Version=3;"); conn.Open(); return conn; } } }