Files

1025 lines
44 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Data;
using System.IO;
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;
namespace 成套报价软件
{
/// <summary>
/// 停靠窗口:方案(储存报价方案)。
/// 项目页 = 方案列表(sjk\方案库\ 下每个 .db 文件 = 一个方案,即一份完整项目库副本);
/// 双击/右键打开 → 报价页三表从该方案文件加载(只读浏览)。
/// 与主窗口项目模型一致:一个方案一个库文件,天然隔离。
/// </summary>
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 += 方案项目页_双击;
}
/// <summary>方案库目录(exe 旁 sjk\方案库\)。</summary>
private string 方案目录
{
get
{
string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sjk", "方案库");
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
return dir;
}
}
/// <summary>主窗体引用(停靠面板的宿主窗体)。</summary>
private MainForm 主窗体
{
get { return DockPanel == null ? null : DockPanel.FindForm() as MainForm; }
}
/// <summary>绑定项目页网格(方案列表;由主窗体在方案数据变化时调用)。</summary>
public void 绑定项目页(System.Data.DataTable list)
{
advancedDataGridView6.AutoGenerateColumns = false;
advancedDataGridView6.DataSource = list;
advancedDataGridView6.Refresh();
}
/// <summary>重新加载当前打开的方案(粘贴/加入等操作完成后刷新显示;未打开则只刷方案列表)。</summary>
public void 重新加载当前方案()
{
if (!string.IsNullOrEmpty(_当前方案文件) && File.Exists(_当前方案文件))
打开方案(_当前方案文件);
刷新项目页();
}
/// <summary>主窗体"存为方案"后调用:切换并打开新存的方案文件,报价页立即显示其内容。</summary>
public void 打开方案文件(string 方案文件路径)
{
if (string.IsNullOrEmpty(方案文件路径) || !File.Exists(方案文件路径))
{
刷新项目页();
return;
}
_当前方案文件 = 方案文件路径;
打开方案(方案文件路径);
刷新项目页();
}
/// <summary>当前打开的方案文件路径(报价页三表的数据源;空=未打开)。</summary>
public string 当前方案文件
{
get { return _当前方案文件; }
}
/// <summary>方案报价页当前选中的图号名(图号表当前行"图号"列;未选中返回空)。
/// 供主窗口"加入方案库"定位插入目标:用户在方案里选中哪个图号,就插到哪个图号下。</summary>
public string 方案选中图号
{
get
{
if (advancedDataGridView图号 == null || advancedDataGridView图号.CurrentRow == null) return "";
return Cell文本(advancedDataGridView图号.CurrentRow, "图号");
}
}
/// <summary>方案报价页当前选中的箱号内部ID(箱号表当前行"内部ID"列,空则退回"箱号"显示名;未选中返回空)。
/// 供主窗口"加入方案库"定位插入目标:用户在方案里选中哪个箱号,元件就插到它的元件表。</summary>
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;
}
}
// ============ 项目页:方案列表 ============
/// <summary>方案窗口诊断日志(logs\方案窗口诊断.log)。</summary>
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 { }
}
/// <summary>扫描方案库目录,列出全部方案(每文件读项目信息+图号表汇总)。</summary>
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));
}
/// <summary>项目页双击:打开该方案。</summary>
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);
}
}
// ============ 方案内容删除(作用于当前打开的方案文件) ============
/// <summary>右键删除当前方案中的选中图号(连其箱号/元件子树)。</summary>
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();
}
}
打开方案(_当前方案文件); // 重载刷新
}
/// <summary>右键删除当前方案中的选中箱号(连其元件表);并刷新图号行聚合。</summary>
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();
}
}
打开方案(_当前方案文件);
}
/// <summary>右键删除当前方案中选中的元件行(删除后刷新箱号/图号聚合)。</summary>
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();
}
}
刷新元件和聚合();
}
/// <summary>简单参数封装(WHERE 参数)。</summary>
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();
}
}
/// <summary>查方案文件的表索引:该图号(可选箱号)关联的全部实际表名。</summary>
private System.Collections.Generic.List<string> 方案表清单(
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx,
string 图号, string 箱号)
{
var list = new System.Collections.Generic.List<string>();
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;
}
/// <summary>删除元件后重刷元件表与箱号/图号聚合(简单重开方案)。</summary>
private void 刷新元件和聚合()
{
string f = _当前方案文件;
if (string.IsNullOrEmpty(f)) return;
打开方案(f);
}
// ============ 报价页:从方案文件加载三表(虚拟模式,与主窗口 Reload 相同顺序) ============
/// <summary>当前三表的数据源字段(虚拟模式供数用;TableName=库中真实表名)。</summary>
private DataTable _方案图号表, _方案箱号表, _方案元件表;
private DataTable 取方案数据表(DataGridView dgv)
{
if (dgv == advancedDataGridView图号) return _方案图号表;
if (dgv == advancedDataGridView箱号) return _方案箱号表;
if (dgv == advancedDataGridView元件) return _方案元件表;
return null;
}
/// <summary>安全设置行数:ADGV 列头排序/过滤后会激活内部数据绑定,
/// 此时直接设 RowCount 抛"不能在数据绑定的 DataGridView 控件上设置 RowCount 属性"
/// (加入方案库成功但方案窗口刷新失败,界面看不到新数据)。先解绑再设。</summary>
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);
}
/// <summary>加载一张表到虚拟模式(★先换字段表引用,再设 RowCount,最后 Invalidate——主窗口同序)。</summary>
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);
}
/// <summary>打开方案:整份加载到报价页三表。</summary>
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元件);
}
// ============ 工具 ============
/// <summary>方案联动专用日志(logs\方案联动.log)。</summary>
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();
}
}
}
/// <summary>取行值:报价三表(虚拟模式)按 row.Index 从字段表取;项目页(绑定模式)从 DataBoundItem 取。</summary>
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 防抖队列 / 后台单事务写库+聚合)
// ============================================================
/// <summary>虚拟模式供数(主窗口 Dgv_CellValueNeeded 同款)。</summary>
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;
}
/// <summary>编辑立即提交(主窗口 Grid_DirtyStateChanged 同款)。</summary>
private void 方案表_DirtyStateChanged(object sender, EventArgs e)
{
var dgv = sender as DataGridView;
if (dgv != null && dgv.IsCurrentCellDirty)
dgv.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
/// <summary>虚拟模式写回:写内存表 + 内存重算链(乐观显示) + 入队直写方案文件。</summary>
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 的方案版) ----
/// <summary>派生列集合(由计算维护;用户编辑这些列不触发直写)。</summary>
private static readonly System.Collections.Generic.HashSet<string> _方案派生列 =
new System.Collections.Generic.HashSet<string>(
new[] { "采购单价", "采购价", "报出单价", "报出价", "单台成本", "成本价", "单台报出",
"利润", "利润率", "毛利率", "箱柜数量", "单价", "总价", "成本总价", "毛利" });
/// <summary>待写库项:表名|rowid|列 → 值(后写覆盖先写)。主窗口 _待写项 同款。</summary>
private readonly System.Collections.Generic.Dictionary<string, string[]> _方案待写项 =
new System.Collections.Generic.Dictionary<string, string[]>();
/// <summary>防抖定时器(300ms)。主窗口 _直写Timer 同款。</summary>
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();
}
/// <summary>防抖到期:批量直写方案文件(后台单事务)+按公式刷聚合。</summary>
private void 方案直写到期(object sender, EventArgs e)
{
_方案直写Timer.Stop();
if (_方案待写项.Count == 0 || string.IsNullOrEmpty(_当前方案文件)) return;
System.Collections.Generic.List<string[]> 批;
lock (_方案待写项)
{
批 = new System.Collections.Generic.List<string[]>(_方案待写项.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);
}
});
}
/// <summary>简单 SQL 执行。</summary>
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();
}
}
/// <summary>方案窗口自己的 fak.db 连接(公式表来源;已打开)。</summary>
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;
}
}
}