using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Microsoft.Win32;
using Smart.CustomComponents;
namespace 成套报价软件
{
/// 主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。
public partial class MainForm
{
// ============================================================
// 公式实时重算(对应 Excel 的三级联动公式)
// 元件(底层,每行独立) → 箱号(聚合该箱号所有元件) → 图号(聚合该图号所有箱号)
// ============================================================
/// 把单元格值解析为 decimal;DBNull/空串/非数字返回 0。
private static decimal ParseDecimal(object v)
{
if (v == null || v == DBNull.Value) return 0m;
decimal d;
if (decimal.TryParse(v.ToString().Trim(), out d)) return d;
return 0m;
}
/// 把计算结果写回到行某列;为 0 写空串(保持界面整洁)。
/// ★ 虚拟模式:写内存 DataTable 行(按行号)+刷新该行,不再写 Cells.Value(虚拟模式下无效)。
private void SetCell(DataGridViewRow row, string colName, decimal value)
{
if (row == null || row.DataGridView == null) return;
DataGridViewColumn col = FindColumn(row.DataGridView, colName);
if (col == null) return;
// 写 double(ValueType=double,Format 才能格式化显示)
object v = value == 0m ? 0d : (double)value;
string realName = !string.IsNullOrEmpty(col.DataPropertyName) ? col.DataPropertyName : colName;
写单元格(row.DataGridView, row.Index, realName, v);
}
// ============================================================
// 价格显示格式化(用 DefaultCellStyle.Format,只改显示不改值)
// - 这是 DataGridView 原生的“只改显示”机制,不干扰用户输入
// - 仅作用于公式计算列(见 s_公式列集合);用户输入列(表价/数量/系数)不参与
// ============================================================
// ============================================================
// ★ 虚拟模式供数/回写:表格不存数据,由内存 DataTable 按需提供
// ============================================================
/// 虚拟模式:控件要显示某单元格时从内存表取值。
private void Dgv_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
var dgv = sender as DataGridView;
if (dgv == null) return;
var table = 取dgv数据表(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; }
// ★ 合并视图序号显示(2026-09-30 多选联动):
// 箱号合并(元件表):显示"箱号-N"(如 AH4-1),用户可分辨每行归属(仅显示层,不写库)
// 图号合并(箱号表):显示连续行号 1..N
if (colName == "序号" &&
string.IsNullOrEmpty(table.TableName) && table.Columns.Contains("_来源表名_"))
{
if (table.Columns.Contains("_归属箱号_") && table.Columns.Contains("_段内序号_"))
{
string xhName = Convert.ToString(table.Rows[e.RowIndex]["_归属箱号_"]);
int seqIn = 0;
try { seqIn = Convert.ToInt32(table.Rows[e.RowIndex]["_段内序号_"]); } catch { }
e.Value = (string.IsNullOrWhiteSpace(xhName) ? "" : xhName + "-") + seqIn.ToString();
}
else
{
e.Value = (e.RowIndex + 1).ToString();
}
return;
}
object v = table.Rows[e.RowIndex][colName];
// 公式列输出数值类型(DefaultCellStyle.Format 只对数值生效)
if (v != null && v != DBNull.Value && v is string &&
s_公式列集合.Contains(colName))
{
double d;
string s = v.ToString().Trim();
if (s.Length > 0 && double.TryParse(s,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out d))
{ e.Value = d; return; }
if (s.Length == 0) { e.Value = 0d; return; }
}
e.Value = (v == null || v == DBNull.Value) ? "" : v;
}
/// 虚拟模式:用户编辑单元格提交时写回内存表(读缓存),并排队直写数据库。
private void Dgv_CellValuePushed(object sender, DataGridViewCellValueEventArgs e)
{
var dgv = sender as DataGridView;
if (dgv == null) return;
var table = 取dgv数据表(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) || !table.Columns.Contains(colName)) return;
object 新值 = e.Value;
// ★ 图号/箱号重名自动加后缀:改成同表已有名时自动变"名(2)"/"名(3)"(不弹窗不打断;
// 元件名称不查重——元件名本来就常重复)。后缀版写回内存表并刷新该行显示。
if ((dgv == this.advancedDataGridView9 && colName == "图号") ||
(dgv == this.advancedDataGridView4 && colName == "箱号"))
{
新值 = 重名自动后缀(table, e.RowIndex, colName, 新值);
}
table.Rows[e.RowIndex][colName] = (新值 == null || 新值 == DBNull.Value) ? DBNull.Value : 新值;
// ★ 直接操作数据库:编辑即排队 UPDATE 库(防抖)
入队直写库(dgv, e.RowIndex, colName, 新值);
if (!Equals(新值, e.Value))
{
// 后缀生效:刷新该行显示(虚拟模式整表刷新一次供数)
try { dgv.InvalidateRow(e.RowIndex); } catch { }
Log("[重名] " + colName + " 重名自动改名: " + e.Value + " → " + 新值);
}
// ★★ 一劳永逸方案(2026-09-30 定稿):编辑提交后【只算本行元件公式】,
// 箱号行/图号行的聚合价格【一律不在编辑时更新内存显示】——
// 统一交给"防抖全库重算"(编辑停止 600ms 后):
// RecalcAllFromDatabase 按箱号逐个聚合 UPDATE 回库(库内计算,天然按归属) →
// 刷新箱号表显示从库(按内部ID逐行对齐) 一次跳到最终正确值。
// 单一更新路径 + 归属定位,结构上杜绝:①价格跳好几次中间值 ②显示到别的箱号。
// (旧的多条内存聚合/回填路径全部废弃——它们是跳动与错位的来源)
_isRecalculating = true;
_isReloading = true;
try
{
if (dgv == this.advancedDataGridView5)
{
// 本行元件公式(采购单价/采购价/报出单价/报出价)——单行数据自洽,即时显示
var yjRow = dgv.Rows[e.RowIndex];
if (yjRow != null && !yjRow.IsNewRow) RecalcYuanjianRow(yjRow);
// ★ B2(2026-10-01):记录本次编辑影响到的元件物理表(合并视图=行级来源表,
// 单箱号=整表表名),防抖到点后反查(图号,箱号)做增量重算——大项目编辑不再全库重算
try
{
bool mergedYj = string.IsNullOrEmpty(table.TableName) && table.Columns.Contains("_来源表名_");
string srcTbl = (mergedYj
? Convert.ToString(table.Rows[e.RowIndex]["_来源表名_"])
: table.TableName) ?? "";
srcTbl = srcTbl.Trim().Trim('"');
if (!string.IsNullOrWhiteSpace(srcTbl))
{
lock (_待重算元件表) { _待重算元件表.Add(srcTbl); }
}
}
catch { }
}
// 箱号表(dgv4)/图号表(dgv9)编辑:自身派生列依赖下级聚合,同样交给防抖重算
// (dgv4/dgv9 不收集范围→防抖到点走全量,安全优先)
// ★ 清缓存:切回单选/切图号时 Reload 读库新值
_yuanjianCache.Clear();
_xianghaoCache.Clear();
合并视图防抖重算();
}
finally
{
_isRecalculating = false;
_isReloading = false;
}
// ★ 项目页实时同步(800ms 防抖):图号汇总→项目列表库+项目页网格刷新
if (dgv == this.advancedDataGridView5 || dgv == this.advancedDataGridView4 || dgv == this.advancedDataGridView9)
启动项目页同步();
}
/// ★ 合并视图编辑后的防抖全库重算(2026-09-30):
/// 连续编辑合并触发一次(600ms 安静期),RecalcAllFromDatabase 纯库操作——
/// 按图号/箱号逐个聚合 UPDATE 回库,保证箱号行/图号行/项目汇总始终最新,
/// 且不会把"多箱号合计"写进单个箱号行。
/// ★ B2(2026-10-01):编辑收集过影响元件表时改为【增量重算】(只重算受影响箱号),
/// 未收集(dgv4/dgv9编辑等)走全量兜底。
private Timer _合并重算Timer;
/// B2:编辑影响到的元件物理表集合(防抖窗口内合并),Tick 时取走并反查(图号,箱号)。
private readonly System.Collections.Generic.HashSet _待重算元件表 =
new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase);
/// B2:元件物理表名集合 → 反查(图号\n箱号内部ID)范围集合。
private System.Collections.Generic.HashSet 反查重算范围(System.Collections.Generic.HashSet 表名s)
{
var scope = new System.Collections.Generic.HashSet();
if (表名s == null || 表名s.Count == 0 || string.IsNullOrWhiteSpace(_currentProjectId)) return scope;
string dbPath = GetQuoteDbPath(_currentProjectId);
if (!System.IO.File.Exists(dbPath)) return scope;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
foreach (string t in 表名s)
{
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 图号, 箱号 FROM 表索引 WHERE 类型='元件' AND 实际表名=@t;", conn))
{
sel.Parameters.AddWithValue("@t", t);
using (var rd = sel.ExecuteReader())
{
while (rd.Read())
{
string th = rd.IsDBNull(0) ? "" : rd.GetValue(0).ToString().Trim();
string xh = rd.IsDBNull(1) ? "" : rd.GetValue(1).ToString().Trim();
if (!string.IsNullOrWhiteSpace(th) && !string.IsNullOrWhiteSpace(xh))
scope.Add(th + "\n" + xh);
}
}
}
}
}
}
catch (Exception exQ) { Log("[重算·增量] 反查范围异常: " + exQ.Message); }
return scope;
}
private void 合并视图防抖重算()
{
if (_合并重算Timer == null)
{
_合并重算Timer = new Timer();
_合并重算Timer.Interval = 600;
_合并重算Timer.Tick += (s, e) =>
{
_合并重算Timer.Stop();
try
{
// ★ 修复(2026-10-01):重算读库前先把直写队列强制落库——
// 避免直写(300ms)与重算(600ms)时序竞争导致重算读到旧值
直写队列强制落库();
// ★ B2:先取走编辑收集的影响表 → 反查范围;非空走增量,空走全量
string[] tbls;
lock (_待重算元件表)
{
tbls = new string[_待重算元件表.Count];
_待重算元件表.CopyTo(tbls);
_待重算元件表.Clear();
}
var scope = 反查重算范围(
new System.Collections.Generic.HashSet(tbls));
RecalcAllFromDatabase(_currentProjectId,
scope.Count > 0 ? scope : null);
// ★ 显示保险(2026-09-30):库聚合完成后,按内部ID对齐从库刷新箱号表
// 价格列显示(不动行序/选中)——库是权威,彻底杜绝"改AH4显示到AH3"的错位
刷新箱号表显示从库();
// ★ 费用行实时联动(2026-10-01):百分比费用的表价已按最新基数在库里重写,
// 这里把元件表中费用行的显示值同步为新库值(普通行不动,不打断编辑)
刷新元件表费用行显示从库();
}
catch (Exception exR) { Log("[合并重算] 异常: " + exR.Message); }
};
}
_合并重算Timer.Stop();
_合并重算Timer.Start();
}
/// ★ 从库刷新箱号表/图号表价格列显示(2026-09-30):按"内部ID"对齐更新内存行,
/// 不动行序与选中。合并视图(TableName 空)时跳过——由切回单选时重载。
private void 刷新箱号表显示从库()
{
try
{
刷新表价格列从库(this.advancedDataGridView4, _xianghaoTable,
"单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率");
刷新表价格列从库(this.advancedDataGridView9, _tuhaoTable,
"箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率");
}
catch (Exception exF) { Log("[显示同步] 刷新异常: " + exF.Message); }
}
///
/// ★ 费用行实时联动的显示同步(2026-10-01):重算后库里的费用行表价/四价已按最新基数重写,
/// 这里按内部ID对齐把元件表(内存表)中 类型='费用' 的行同步为新库值(普通行不动,不打断编辑)。
/// 合并视图按各行 _来源表名_ 读对应物理表;单箱号视图读整表。
///
private void 刷新元件表费用行显示从库()
{
try
{
var tbl = _yuanjianTable;
var dgv = this.advancedDataGridView5;
if (dgv == null || tbl == null || string.IsNullOrWhiteSpace(_currentProjectId)) return;
if (!tbl.Columns.Contains("内部ID") || !tbl.Columns.Contains("类型")) return;
string dbPath = GetQuoteDbPath(_currentProjectId);
if (!System.IO.File.Exists(dbPath)) return;
// 内存表费用行(按内部ID)
var 费用行s = new System.Collections.Generic.List();
foreach (DataRow r in tbl.Rows)
if (Convert.ToString(r["类型"]).Trim() == "费用") 费用行s.Add(r);
if (费用行s.Count == 0) return;
bool merged = string.IsNullOrEmpty(tbl.TableName) && tbl.Columns.Contains("_来源表名_");
// 来源物理表集合(合并=行级来源;单箱号=整表)
var srcTables = new System.Collections.Generic.List();
if (merged)
{
foreach (DataRow r in tbl.Rows)
{
string s = Convert.ToString(r["_来源表名_"]).Trim();
if (!string.IsNullOrWhiteSpace(s) && !srcTables.Contains(s)) srcTables.Add(s);
}
}
else if (!string.IsNullOrWhiteSpace(tbl.TableName))
{
srcTables.Add(tbl.TableName);
}
if (srcTables.Count == 0) return;
// 库值:内部ID → [备注,规格,表价,采购单价,采购价,报出单价,报出价]
var 库值 = new System.Collections.Generic.Dictionary>();
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
foreach (string s in srcTables)
{
try
{
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID, 备注, 规格, 表价, 采购单价, 采购价, 报出单价, 报出价 FROM \"" + s.Replace("\"", "") +
"\" WHERE 类型='费用';", conn))
using (var r = sel.ExecuteReader())
{
while (r.Read())
{
string nid = r.IsDBNull(0) ? "" : r.GetValue(0).ToString().Trim();
if (string.IsNullOrEmpty(nid)) continue;
var vals = new System.Collections.Generic.List