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;
}
}
}