using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Microsoft.Win32;
using Smart.CustomComponents;
namespace 成套报价软件
{
/// 主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。
public partial class MainForm
{
// ============================================================
// 右键菜单:创建并挂载
// ============================================================
// ----- 复制/粘贴剪贴板(每个层级独立缓存)-----
// 图号:存图号行 + 其所有箱号表内容 + 各箱号下元件表内容
private class ClipTuhao
{
public string ProjectId;
public string SrcTuhaoName; // 源图号显示名(供参考)
public System.Collections.Generic.List> TuhaoRow;
public System.Collections.Generic.List> XianghaoRows;
// 每个箱号对应的元件表内容(按下标和 XianghaoRows 对应)
public System.Collections.Generic.List>> YuanjianRowsPerXianghao;
}
private System.Collections.Generic.List _clipTuhaos;
// 箱柜:存箱号行 + 其下所有元件行
private class ClipXianghao
{
public string ProjectId;
public string SrcXianghaoName;
public System.Collections.Generic.Dictionary XianghaoRow;
public System.Collections.Generic.List> YuanjianRows;
}
private System.Collections.Generic.List _clipXianghaos;
// 元件:存多行(支持多选复制)
private System.Collections.Generic.List> _clipYuanjianRows;
/// 把 DataGridViewRow(绑定到 DataTable)的字段拷贝为字典(排除序号和内部ID)。
private System.Collections.Generic.Dictionary CopyRowFields(DataGridViewRow row, System.Collections.Generic.HashSet excludeCols)
{
var dict = new System.Collections.Generic.Dictionary();
if (row == null) return dict;
// ★ 虚拟模式:按行号从内存表读(无 DataBoundItem)
var vtable = 取dgv数据表(row.DataGridView);
if (vtable != null && row.Index >= 0 && row.Index < vtable.Rows.Count)
{
foreach (System.Data.DataColumn dc in vtable.Columns)
{
if (dc.ColumnName == "_rowid_") continue; // 行定位列,不参与复制
if (excludeCols != null && excludeCols.Contains(dc.ColumnName)) continue;
object v = vtable.Rows[row.Index][dc.ColumnName];
dict[dc.ColumnName] = (v == DBNull.Value) ? null : v;
}
return dict;
}
var drv = row.DataBoundItem as DataRowView;
if (drv == null || drv.Row == null) return dict;
DataTable dt = drv.Row.Table;
if (dt == null) return dict;
foreach (DataColumn dc in dt.Columns)
{
if (excludeCols != null && excludeCols.Contains(dc.ColumnName)) continue;
dict[dc.ColumnName] = drv.Row[dc];
}
return dict;
}
/// 从 SQLite 读取整个表所有行的字段字典列表(用于复制时备份下级表)。
private static System.Collections.Generic.List> ReadAllRowsFromTable(string dbPath, string tableName)
{
var list = new System.Collections.Generic.List>();
if (string.IsNullOrEmpty(dbPath) || string.IsNullOrEmpty(tableName)) return list;
if (!System.IO.File.Exists(dbPath)) return list;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
string sql = "SELECT * FROM \"" + tableName + "\";";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var dict = new System.Collections.Generic.Dictionary();
for (int i = 0; i < reader.FieldCount; i++)
{
dict[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
}
list.Add(dict);
}
}
}
}
catch { }
return list;
}
/// GetConnString 的静态包装,供静态方法调用。
/// ★ A2(2026-10-01):统一收敛到 数据库连接.串(一处管理),本方法保留签名做转发。
private static string GetConnStringStatic(string dbPath)
{
return 数据库连接.串(dbPath);
}
///
/// 生成唯一的内部ID:先试 baseName+"(副本)",有重复就 +"(副本1)"、+"(副本2)"…递增。
/// checkAction 返回 true 表示该名称在目标表里已存在。
/// 会先清除原名里旧的 "_副本XXXX"(GUID残留)和旧的 "(副本N)" 后缀,只保留干净原名。
///
private static string MakeUniqueName(string baseName, System.Func exists)
{
if (string.IsNullOrEmpty(baseName)) baseName = "未命名";
// 清除原名里旧的命名残留,只留干净的原始名
baseName = CleanItemName(baseName);
string candidate = baseName + "(副本)";
if (exists == null || !exists(candidate)) return candidate;
int i = 1;
while (true)
{
candidate = baseName + "(副本" + i.ToString() + ")";
if (!exists(candidate)) return candidate;
i++;
if (i > 9999) // 防止死循环
{
return baseName + "(副本" + System.Guid.NewGuid().ToString("N").Substring(0, 6) + ")";
}
}
}
///
/// 清除名称里的旧命名残留:
/// "_副本XXXX"(8位十六进制 GUID 残留)、"(副本)"、"(副本N)" 全部去掉。
/// 例:"新图号1_副本966a(副本)" → "新图号1"
/// "箱号A_副本a3b2" → "箱号A"
/// "原名(副本2)" → "原名"
///
private static string CleanItemName(string name)
{
if (string.IsNullOrEmpty(name)) return name;
// 反复清理,直到没有可清理的模式(一次可能清不干净,如"(副本)(副本)")
string s = name;
for (int loop = 0; loop < 5; loop++)
{
string before = s;
// 去掉末尾的 _副本 + 4~8位十六进制(GUID残留)
s = System.Text.RegularExpressions.Regex.Replace(s, @"_副本[0-9a-fA-F]{4,8}$", "");
// 去掉末尾的 (副本) 或 (副本N)
s = System.Text.RegularExpressions.Regex.Replace(s, @"\(副本\d*\)$", "");
if (s == before) break; // 没变化就停止
}
return s.Trim();
}
/// 检查图号表里是否已存在某个内部ID。
private bool TuhaoIdExists(string projectId, string id)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrEmpty(id)) return false;
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return false;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "SELECT COUNT(*) FROM " + TuhaoTableName + " WHERE 内部ID=@id;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@id", id);
return Convert.ToInt32(cmd.ExecuteScalar()) > 0;
}
}
}
catch { return false; }
}
/// 检查某图号下箱号表里是否已存在某个内部ID。
private bool XianghaoIdExists(string projectId, string tuhao, string id)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrEmpty(id)) return false;
string tbl = GetXianghaoTableByIndex(projectId, tuhao);
if (string.IsNullOrEmpty(tbl)) return false;
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return false;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "SELECT COUNT(*) FROM \"" + tbl + "\" WHERE 内部ID=@id;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@id", id);
return Convert.ToInt32(cmd.ExecuteScalar()) > 0;
}
}
}
catch { return false; }
}
// ===== 图号:复制/粘贴 =====
private volatile bool _复制图号中;
private void MenuTuhao_Copy_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先选择一个箱柜。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var rows = GetAllSelectedRows(this.advancedDataGridView9);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先选择要复制的图号。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
rows.Sort((a, b) => a.Index.CompareTo(b.Index));
if (_复制图号中)
{
MessageBox.Show(this, "上一个复制仍在后台进行,请稍候。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" };
string pid = _currentProjectId;
// UI 线程:先备好每行的字段字典(避免后台线程碰表格对象)
var 待收集 = new System.Collections.Generic.List>>();
foreach (var row in rows)
{
string tuhaoId = GetBoundValueAsString(row, "内部ID");
if (string.IsNullOrWhiteSpace(tuhaoId))
{
Log("[复制图号] 跳过无内部ID的行(行" + (row.Index + 1) + ")");
continue;
}
待收集.Add(System.Tuple.Create(tuhaoId, CopyRowFields(row, exclude)));
}
if (待收集.Count == 0)
{
MessageBox.Show(this, "选中的行都没有图号内部ID,无法复制。", "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// ★ 复制前先把未保存的编辑落库:子树从库读,不保存会复制到旧数据
SaveAllQuoteTablesIfDirty(false);
// ★ 子树读取(全部箱号+元件)放后台线程,界面不卡
_复制图号中 = true;
Cursor = Cursors.WaitCursor;
Log("[复制图号] 开始收集 " + 待收集.Count + " 个图号的子树...");
var 复制线程 = new System.Threading.Thread(() =>
{
System.Collections.Generic.List 结果 = null;
Exception 错 = null;
try
{
结果 = new System.Collections.Generic.List();
string dbPath = GetQuoteDbPath(pid);
foreach (var item in 待收集)
{
var clip = new ClipTuhao
{
ProjectId = pid,
SrcTuhaoName = item.Item1,
TuhaoRow = new System.Collections.Generic.List> { item.Item2 },
XianghaoRows = new System.Collections.Generic.List>(),
YuanjianRowsPerXianghao = new System.Collections.Generic.List>>()
};
string xhTbl = LookupTableByIndex(pid, "箱号", item.Item1, "");
if (!string.IsNullOrEmpty(xhTbl))
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
try
{
string sql = "SELECT * FROM \"" + xhTbl + "\";";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var dict = new System.Collections.Generic.Dictionary();
for (int i = 0; i < reader.FieldCount; i++)
dict[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
clip.XianghaoRows.Add(dict);
}
}
}
catch { }
foreach (var xhRow in clip.XianghaoRows)
{
string xhId = (xhRow.ContainsKey("内部ID") ? xhRow["内部ID"] : null) as string;
if (string.IsNullOrWhiteSpace(xhId)) xhId = (xhRow.ContainsKey("箱号") ? xhRow["箱号"] : null) as string;
var yjRows = new System.Collections.Generic.List>();
string yjTbl = GetYuanjianTableByIndex(item.Item1, xhId, conn);
if (!string.IsNullOrEmpty(yjTbl))
{
try
{
string sql = "SELECT * FROM \"" + yjTbl + "\";";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var dict = new System.Collections.Generic.Dictionary();
for (int i = 0; i < reader.FieldCount; i++)
dict[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
yjRows.Add(dict);
}
}
}
catch { }
}
clip.YuanjianRowsPerXianghao.Add(yjRows);
}
}
}
结果.Add(clip);
}
}
catch (Exception ex) { 错 = ex; }
BeginInvoke((MethodInvoker)delegate
{
_复制图号中 = false;
Cursor = Cursors.Default;
if (错 != null)
{
Log("[复制图号] 异常: " + 错.Message);
MessageBox.Show(this, "复制图号失败:\n" + 错.Message, "复制图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_clipTuhaos = 结果;
int 箱号总数 = 0;
foreach (var c in 结果) 箱号总数 += c.XianghaoRows.Count;
Log("[复制图号] 已缓存图号 " + 结果.Count + " 个,含箱号 " + 箱号总数 + " 个");
SetStatus("已复制 " + 结果.Count + " 个图号(含 " + 箱号总数 + " 个箱号), 请选择位置后粘贴");
});
});
复制线程.IsBackground = true;
复制线程.Start();
}
private void MenuTuhao_Paste_Click(object sender, EventArgs e)
{
if (_复制图号中)
{
MessageBox.Show(this, "复制仍在后台进行,请稍候再粘贴。", "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (_clipTuhaos == null || _clipTuhaos.Count == 0)
{
MessageBox.Show(this, "剪贴板里没有图号,请先复制。", "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先打开一个项目。", "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView9);
var clips = _clipTuhaos;
string pid = _currentProjectId;
var sw = System.Diagnostics.Stopwatch.StartNew();
Cursor = Cursors.WaitCursor;
Log("[粘贴图号] 开始,共 " + clips.Count + " 个图号...");
// ★ 粘贴前先把未保存的编辑落库:粘贴回调会清缓存并从库 Reload,
// 不保存的话用户修改只存在于内存表,Reload 后被旧数据覆盖丢失
SaveAllQuoteTablesIfDirty(false);
// ★ 内存优先管线:表索引/现有图号/序号一次载入内存,SQL 只剩批量落库
var 粘贴线程 = new System.Threading.Thread(() =>
{
string lastTuhaoId = "";
int pasted = 0;
Exception 错 = null;
try
{
string dbPath = GetQuoteDbPath(pid);
EnsureQuoteDbExists(pid);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
// ★ 诊断:确认连接的库文件与 WAL 状态(排查"提交后数据丢失")
using (var dcmd = new System.Data.SQLite.SQLiteCommand(
"PRAGMA journal_mode; SELECT @p AS p;", conn))
{
dcmd.Parameters.AddWithValue("@p", dbPath);
Log("[粘贴图号] 库=" + dbPath + " journal_mode=" + dcmd.ExecuteScalar());
// ★ 公式加载(Excel 式公式表):触发器生成与管线内联计算共用
using (var fakConn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(
GetDbFullPath(@"sjk\fak.db"))))
{
fakConn.Open();
计算引擎.EnsureFormulaTable(fakConn);
_公式_元件 = 计算引擎.加载公式(fakConn, "元件");
_公式_箱号 = 计算引擎.加载公式(fakConn, "箱号");
_公式_图号 = 计算引擎.加载公式(fakConn, "图号");
}
}
using (var tx = conn.BeginTransaction())
{
// 内存快照:箱号/元件表最大序号、现有图号内部ID
int xhMax = 0, yjMax = 0, thSeqMax = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 类型, 实际表名 FROM 表索引;", conn, tx))
using (var r = cmd.ExecuteReader())
{
while (r.Read())
{
string ty = r.IsDBNull(0) ? "" : r.GetString(0);
string real = r.IsDBNull(1) ? "" : r.GetString(1);
if (ty == "箱号" && real.StartsWith("xh_"))
{
int n;
if (int.TryParse(real.Substring(3), out n) && n > xhMax) xhMax = n;
}
else if (ty == "元件" && real.StartsWith("yj_"))
{
int n;
if (int.TryParse(real.Substring(3), out n) && n > yjMax) yjMax = n;
}
}
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COALESCE(MAX(CAST(序号 AS INTEGER)),0) FROM " + TuhaoTableName + ";", conn, tx))
{
object o = cmd.ExecuteScalar();
if (o != null && o != DBNull.Value) thSeqMax = Convert.ToInt32(o);
}
int 快照图号数;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM " + TuhaoTableName + ";", conn, tx))
{
object o = cmd.ExecuteScalar();
快照图号数 = (o == null || o == DBNull.Value) ? 0 : Convert.ToInt32(o);
}
Log("[粘贴图号] 快照: 事务内图号数=" + 快照图号数 + " xhMax=" + xhMax + " yjMax=" + yjMax);
var 现有图号 = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase);
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM " + TuhaoTableName + ";", conn, tx))
using (var r = cmd.ExecuteReader())
{
while (r.Read())
{
if (!r.IsDBNull(0))
{
string v = r.GetString(0);
if (v.Length > 0) 现有图号.Add(v);
}
}
}
foreach (var clip in clips)
{
string baseName = !string.IsNullOrWhiteSpace(clip.SrcTuhaoName) ? clip.SrcTuhaoName : "图号";
string nt = 唯一化名称(baseName, 现有图号);
现有图号.Add(nt);
thSeqMax++;
// ★ 清理同名图号的残留索引和动态表:
// 此前删除图号行时索引/表可能残留,导致点击该图号时
// LIMIT 1 命中旧残缺表、显示错数据
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 类型, 实际表名 FROM 表索引 WHERE 图号=@th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", nt);
using (var r = cmd.ExecuteReader())
{
while (r.Read())
{
string tbl = r.IsDBNull(1) ? "" : r.GetString(1);
if (string.IsNullOrWhiteSpace(tbl)) continue;
using (var dcmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + tbl + "\";", conn, tx))
dcmd.ExecuteNonQuery();
}
}
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 图号=@th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", nt);
int n = cmd.ExecuteNonQuery();
if (n > 0) Log("[粘贴图号] 清理[" + nt + "]的残留索引 " + n + " 行");
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO " + TuhaoTableName +
" (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " +
"VALUES (@s, @th, @ts, @sl, @dj, @zj, @cbzj, @ml, @mll, @iid);", conn, tx))
{
// ★ 副本=源:聚合值直接抄源图号行(箱号/元件表是完整副本,聚合天然相等)
var srcTh = clip.TuhaoRow.Count > 0 ? clip.TuhaoRow[0] : null;
cmd.Parameters.AddWithValue("@s", thSeqMax.ToString());
cmd.Parameters.AddWithValue("@th", nt);
cmd.Parameters.AddWithValue("@ts", GetSrcField(srcTh, "台数", "1"));
cmd.Parameters.AddWithValue("@sl", GetSrcFieldDecimal(srcTh, "箱柜数量", 0));
cmd.Parameters.AddWithValue("@dj", GetSrcFieldDecimal(srcTh, "单价", 0));
cmd.Parameters.AddWithValue("@zj", GetSrcFieldDecimal(srcTh, "总价", 0));
cmd.Parameters.AddWithValue("@cbzj", GetSrcFieldDecimal(srcTh, "成本总价", 0));
cmd.Parameters.AddWithValue("@ml", GetSrcFieldDecimal(srcTh, "毛利", 0));
cmd.Parameters.AddWithValue("@mll", GetSrcFieldDecimal(srcTh, "毛利率", 0));
cmd.Parameters.AddWithValue("@iid", nt);
cmd.ExecuteNonQuery();
}
var 已用箱号名 = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase);
// ★ 本图号的箱号表只建一张(每图号一张表,表内多行=多个箱号),全部箱号行进同一张表
xhMax++;
string xhTbl = "xh_" + xhMax;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"CREATE TABLE \"" + xhTbl + "\" (" +
" 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," +
" 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," +
" 内部ID TEXT);", conn, tx))
cmd.ExecuteNonQuery();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('箱号', @th, '', @real);", conn, tx))
{
cmd.Parameters.AddWithValue("@th", nt);
cmd.Parameters.AddWithValue("@real", xhTbl);
cmd.ExecuteNonQuery();
}
// 箱号表触发器管尾统一建(行级依赖各行的元件表名)
if (!_待建xh触发器.Exists(x => x[0] == xhTbl)) _待建xh触发器.Add(new[] { xhTbl, nt });
int xhSeq = 0;
for (int i = 0; i < clip.XianghaoRows.Count; i++)
{
var xhRow = clip.XianghaoRows[i];
string srcXhId = (xhRow.ContainsKey("内部ID") ? xhRow["内部ID"] : null) as string;
if (string.IsNullOrWhiteSpace(srcXhId)) srcXhId = (xhRow.ContainsKey("箱号") ? xhRow["箱号"] : null) as string;
string nx = 唯一化名称(!string.IsNullOrWhiteSpace(srcXhId) ? srcXhId : "箱号", 已用箱号名);
已用箱号名.Add(nx);
// ★ 清理该箱号名下的旧元件表残留(防 LIMIT 1 命中旧表)
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名 FROM 表索引 WHERE 类型='元件' AND 图号=@th AND 箱号=@xh;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", nt);
cmd.Parameters.AddWithValue("@xh", nx);
using (var r = cmd.ExecuteReader())
{
var oldTbls = new System.Collections.Generic.List();
while (r.Read())
oldTbls.Add(r.IsDBNull(0) ? "" : r.GetString(0));
foreach (var ot in oldTbls)
{
if (string.IsNullOrWhiteSpace(ot)) continue;
using (var dcmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + ot + "\";", conn, tx))
dcmd.ExecuteNonQuery();
}
}
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 类型='元件' AND 图号=@th AND 箱号=@xh;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", nt);
cmd.Parameters.AddWithValue("@xh", nx);
cmd.ExecuteNonQuery();
}
xhSeq++;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO \"" + xhTbl + "\" VALUES (@s, @xh, @xm, @model, @spec, @unit, @count," +
" @dtcb, @cbj, @dtbc, @bcj, @lr, @lrl, @mgl, @type, @remark, @drawing, @iid);", conn, tx))
{
// ★ 副本=源:箱号行聚合值直接抄源行(单台成本/成本价/单台报出/报出价/利润/利润率/毛利率)
cmd.Parameters.AddWithValue("@s", xhSeq.ToString());
cmd.Parameters.AddWithValue("@xh", nx);
cmd.Parameters.AddWithValue("@xm", GetSrcField(xhRow, "箱名", ""));
cmd.Parameters.AddWithValue("@model", GetSrcField(xhRow, "型号", ""));
cmd.Parameters.AddWithValue("@spec", GetSrcField(xhRow, "规格", ""));
cmd.Parameters.AddWithValue("@unit", GetSrcField(xhRow, "单位", "台"));
cmd.Parameters.AddWithValue("@count", GetSrcField(xhRow, "数量", "1"));
cmd.Parameters.AddWithValue("@dtcb", GetSrcFieldDecimal(xhRow, "单台成本", 0));
cmd.Parameters.AddWithValue("@cbj", GetSrcFieldDecimal(xhRow, "成本价", 0));
cmd.Parameters.AddWithValue("@dtbc", GetSrcFieldDecimal(xhRow, "单台报出", 0));
cmd.Parameters.AddWithValue("@bcj", GetSrcFieldDecimal(xhRow, "报出价", 0));
cmd.Parameters.AddWithValue("@lr", GetSrcFieldDecimal(xhRow, "利润", 0));
cmd.Parameters.AddWithValue("@lrl", GetSrcFieldDecimal(xhRow, "利润率", 0));
cmd.Parameters.AddWithValue("@mgl", GetSrcFieldDecimal(xhRow, "毛利率", 0));
cmd.Parameters.AddWithValue("@type", GetSrcField(xhRow, "箱柜类型", ""));
cmd.Parameters.AddWithValue("@remark", GetSrcField(xhRow, "备注", ""));
cmd.Parameters.AddWithValue("@drawing", GetSrcField(xhRow, "图纸", ""));
cmd.Parameters.AddWithValue("@iid", nx);
cmd.ExecuteNonQuery();
}
var yjRows = (i < clip.YuanjianRowsPerXianghao.Count) ? clip.YuanjianRowsPerXianghao[i] : null;
if (yjRows != null && yjRows.Count > 0)
{
yjMax++;
string yjTbl = "yj_" + yjMax;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"CREATE TABLE \"" + yjTbl + "\" (" +
" 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," +
" 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," +
" 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT);", conn, tx))
cmd.ExecuteNonQuery();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", conn, tx))
{
cmd.Parameters.AddWithValue("@th", nt);
cmd.Parameters.AddWithValue("@xh", nx);
cmd.Parameters.AddWithValue("@real", yjTbl);
cmd.ExecuteNonQuery();
}
int ys = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO \"" + yjTbl + "\" VALUES (@s, @name, @spec, @unit, @count, @biaojia," +
" @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," +
" @drawingSpec, @brand, @series, @type, @remark);", conn, tx))
{
// ★ 预添加参数(此前漏加,循环内按名访问直接抛索引越界)
foreach (string pn in new[] { "@s", "@name", "@spec", "@unit", "@count", "@biaojia",
"@caigouDanjia", "@caigouXishu", "@caigouJia", "@baochuDanjia", "@baochuXishu",
"@baochuJia", "@drawingSpec", "@brand", "@series", "@type", "@remark" })
cmd.Parameters.AddWithValue(pn, "");
foreach (var yjRow in yjRows)
{
ys++;
cmd.Parameters["@s"].Value = ys.ToString();
cmd.Parameters["@name"].Value = GetSrcField(yjRow, "元件名称", "");
cmd.Parameters["@spec"].Value = GetSrcField(yjRow, "规格", "");
cmd.Parameters["@unit"].Value = GetSrcField(yjRow, "单位", "只");
cmd.Parameters["@count"].Value = GetSrcField(yjRow, "数量", "1");
cmd.Parameters["@biaojia"].Value = GetSrcFieldDecimal(yjRow, "表价", 0);
cmd.Parameters["@caigouDanjia"].Value = GetSrcFieldDecimal(yjRow, "采购单价", 0);
cmd.Parameters["@caigouXishu"].Value = GetSrcField(yjRow, "采购系数", "1");
cmd.Parameters["@caigouJia"].Value = GetSrcFieldDecimal(yjRow, "采购价", 0);
cmd.Parameters["@baochuDanjia"].Value = GetSrcFieldDecimal(yjRow, "报出单价", 0);
cmd.Parameters["@baochuXishu"].Value = GetSrcField(yjRow, "报出系数", "1");
cmd.Parameters["@baochuJia"].Value = GetSrcFieldDecimal(yjRow, "报出价", 0);
cmd.Parameters["@drawingSpec"].Value = GetSrcField(yjRow, "图纸规格", "");
cmd.Parameters["@brand"].Value = GetSrcField(yjRow, "品牌", "");
cmd.Parameters["@series"].Value = GetSrcField(yjRow, "系列", "");
cmd.Parameters["@type"].Value = GetSrcField(yjRow, "类型", "元件");
cmd.Parameters["@remark"].Value = GetSrcField(yjRow, "备注", "");
cmd.ExecuteNonQuery();
}
}
}
}
pasted++;
lastTuhaoId = nt;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM " + TuhaoTableName + ";", conn, tx))
Log("[粘贴图号] 图号[" + nt + "] 子树完成: 事务内图号数=" + cmd.ExecuteScalar()
+ " 本图号箱号=" + clip.XianghaoRows.Count);
}
// ★ 恢复触发器(编辑自动计算)
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM " + TuhaoTableName + ";", conn, tx))
Log("[粘贴图号] Commit前(事务内): 图号数=" + cmd.ExecuteScalar());
tx.Commit();
}
// ★ 触发器已在 INSERT 时自动算好派生列与箱号/图号聚合;只补跨库项目汇总
计算引擎.刷新项目汇总(dbPath, pid, TuhaoTableName);
// ★ 自验:提交后立即重连查询(独立连接,验证持久化)
using (var vconn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
vconn.Open();
using (var vcmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM " + TuhaoTableName + ";", vconn))
Log("[粘贴图号] 提交后自验: 库中图号数=" + vcmd.ExecuteScalar());
}
}
}
catch (Exception ex) { 错 = ex; }
BeginInvoke((MethodInvoker)delegate
{
Cursor = Cursors.Default;
if (错 != null)
{
Log("[粘贴图号] 后台异常,完整堆栈: " + 错.ToString());
MessageBox.Show(this, "粘贴图号失败:\n" + 错.Message, "粘贴图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
try
{
_isReloading = true;
this.advancedDataGridView9.SuspendLayout();
_xianghaoCache.Clear();
_yuanjianCache.Clear();
ReloadTuhaoByProject(pid);
MoveLastRowsBelowSelected(this.advancedDataGridView9, _tuhaoTable, selectedIndex,
pasted, TuhaoTableName, pid);
RecalcAllAfterReload();
this.advancedDataGridView9.ResumeLayout();
_currentTuhao = lastTuhaoId;
Log("[粘贴图号] 完成,共 " + pasted + " 个图号,耗时 " + sw.ElapsedMilliseconds + "ms,最后=[" + lastTuhaoId + "]");
SetStatus("粘贴完成 " + pasted + " 个图号, 耗时 " + sw.ElapsedMilliseconds + "ms");
}
finally
{
_isReloading = false;
SelectRowByValue(this.advancedDataGridView9, "内部ID", _currentTuhao);
ReloadXianghaoByTuhao(pid, _currentTuhao);
}
});
});
粘贴线程.IsBackground = true;
粘贴线程.Start();
}
/// 集合内唯一化名称(内存判定,替代逐次查库)。
private static string 唯一化名称(string baseName, System.Collections.Generic.HashSet used)
{
// ★ 原名优先:粘贴的箱号在新图号的箱号表里,原名不冲突就直接用(不加"(副本)"字样);
// 只有真的重名时才依次加"(副本)"/"(副本1)"后缀区分。
string b = CleanItemName(string.IsNullOrEmpty(baseName) ? "未命名" : baseName);
if (!used.Contains(b)) { used.Add(b); return b; }
string c = b + "(副本)";
if (!used.Contains(c)) { used.Add(c); return c; }
int i = 1;
while (true)
{
c = b + "(副本" + i.ToString() + ")";
if (!used.Contains(c)) { used.Add(c); return c; }
i++;
}
}
/// 事务内判断图号内部ID是否已存在。
private static bool NameExistsInConn(System.Data.SQLite.SQLiteConnection conn,
System.Data.SQLite.SQLiteTransaction tx, string tableName, string col, string value)
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM " + tableName + " WHERE " + col + "=@v;", conn, tx))
{
cmd.Parameters.AddWithValue("@v", value);
return Convert.ToInt32(cmd.ExecuteScalar()) > 0;
}
}
/// 事务内判断箱号内部ID是否已存在(同图号)。
private static bool NameExistsXhInConn(System.Data.SQLite.SQLiteConnection conn,
System.Data.SQLite.SQLiteTransaction tx, string tuhaoId, string value)
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM 表索引 WHERE 类型='箱号' AND 图号=@th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", tuhaoId);
return Convert.ToInt32(cmd.ExecuteScalar()) > 0;
}
}
/// 事务内解析下一个可用动态表名(prefix_N)。
private static string GetNextTableIdTx(string projectId, string prefix,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
if (string.IsNullOrWhiteSpace(projectId)) return prefix + "_1";
int maxN = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名 FROM 表索引 WHERE 实际表名 LIKE @p;", conn, tx))
{
cmd.Parameters.AddWithValue("@p", prefix + "_%");
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
string t = reader.IsDBNull(0) ? "" : reader.GetString(0);
string numPart = t.StartsWith(prefix + "_") ? t.Substring(prefix.Length + 1) : "";
int n;
if (int.TryParse(numPart, out n) && n > maxN) maxN = n;
}
}
}
return prefix + "_" + (maxN + 1).ToString();
}
/// 事务内按真实ID表名建一张空箱号表(含内部ID)。
private static void CreateXianghaoTableTx(string projectId, string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"CREATE TABLE \"" + realTableName + "\" (" +
" 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," +
" 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," +
" 内部ID TEXT);", conn, tx))
cmd.ExecuteNonQuery();
}
/// 事务内登记表索引。
private static void AddTableIndexTx(string projectId, string type, string tuhao,
string xianghao, string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES (@t, @th, @xh, @real);", conn, tx))
{
cmd.Parameters.AddWithValue("@t", type);
cmd.Parameters.AddWithValue("@th", tuhao ?? "");
cmd.Parameters.AddWithValue("@xh", xianghao ?? "");
cmd.Parameters.AddWithValue("@real", realTableName);
cmd.ExecuteNonQuery();
}
}
/// 事务内取元件表下一序号。
private static int GetYuanjianCountTx(string projectId, string tuhao, string xhId, string tblName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM \"" + tblName + "\";", conn, tx))
{
object o = cmd.ExecuteScalar();
int n = (o != null && o != DBNull.Value) ? Convert.ToInt32(o) : 0;
return n + 1;
}
}
/// 指定连接+事务内取下一序号(批量粘贴用,避免每条开关连接)。
private static string GetNextSeqNumberTx(string tableName, string keyCol, string projectId,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
int max = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COALESCE(MAX(CAST(" + keyCol + " AS INTEGER)),0) FROM " + tableName + ";", conn, tx))
{
object o = cmd.ExecuteScalar();
if (o != null && o != DBNull.Value) max = Convert.ToInt32(o);
}
return (max + 1).ToString();
}
/// 把缓存的图号字段写入数据库(新内部ID,公式列清零)。★ 带事务重载:批量粘贴共用一个事务。
private void PasteTuhaoRowToDb(string projectId, string newTuhaoId, System.Collections.Generic.Dictionary srcRow,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string seq = GetNextSeqNumberTx(TuhaoTableName, "图号", projectId, conn, tx);
string sql = "INSERT INTO " + TuhaoTableName +
" (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " +
"VALUES (@seq, @th, @taishu, 0, 0, 0, 0, 0, 0, @iid);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@th", newTuhaoId);
cmd.Parameters.AddWithValue("@taishu", GetSrcField(srcRow, "台数", "1"));
cmd.Parameters.AddWithValue("@iid", newTuhaoId);
cmd.ExecuteNonQuery();
}
}
/// 把缓存的图号字段写入数据库(新内部ID,公式列清零)。
private void PasteTuhaoRowToDb(string projectId, string newTuhaoId, System.Collections.Generic.Dictionary srcRow)
{
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
string seq = GetNextSeqNumber(TuhaoTableName, "图号", projectId);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
string sql = "INSERT INTO " + TuhaoTableName +
" (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " +
"VALUES (@seq, @th, @taishu, 0, 0, 0, 0, 0, 0, @iid);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@th", newTuhaoId); // 图号显示名先用内部ID,用户可改
cmd.Parameters.AddWithValue("@taishu", GetSrcField(srcRow, "台数", "1"));
cmd.Parameters.AddWithValue("@iid", newTuhaoId);
cmd.ExecuteNonQuery();
}
}
}
// ===== 箱柜:复制/粘贴 =====
private void MenuXianghao_Copy_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先选择一个图号。", "复制箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 支持多选:收集全部选中箱柜行(按表格显示顺序)
var rows = GetAllSelectedRows(this.advancedDataGridView4);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先选择要复制的箱柜。", "复制箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
rows.Sort((a, b) => a.Index.CompareTo(b.Index));
var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" };
// ★ 复制前先把未保存的编辑落库:元件子树从库读,不保存会复制到旧数据
SaveAllQuoteTablesIfDirty(false);
_clipXianghaos = new System.Collections.Generic.List();
foreach (var row in rows)
{
string xhId = GetBoundValueAsString(row, "内部ID");
if (string.IsNullOrWhiteSpace(xhId))
{
Log("[复制箱柜] 跳过无内部ID的行(行" + (row.Index + 1) + ")");
continue;
}
var clip = new ClipXianghao
{
ProjectId = _currentProjectId,
SrcXianghaoName = xhId,
XianghaoRow = CopyRowFields(row, exclude),
YuanjianRows = new System.Collections.Generic.List>()
};
// 读该箱号下所有元件
string yjTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, xhId);
if (!string.IsNullOrEmpty(yjTbl))
clip.YuanjianRows = ReadAllRowsFromTable(GetQuoteDbPath(_currentProjectId), yjTbl);
_clipXianghaos.Add(clip);
}
if (_clipXianghaos.Count == 0)
{
MessageBox.Show(this, "选中的行都没有箱号内部ID,无法复制。", "复制箱柜", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
int 元件总数 = 0;
foreach (var c in _clipXianghaos) 元件总数 += c.YuanjianRows.Count;
Log("[复制箱柜] 已缓存箱柜 " + _clipXianghaos.Count + " 个,含元件 " + 元件总数 + " 个");
}
private void MenuXianghao_Paste_Click(object sender, EventArgs e)
{
if (_clipXianghaos == null || _clipXianghaos.Count == 0)
{
MessageBox.Show(this, "剪贴板里没有箱柜,请先复制。", "粘贴箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先选择一个图号再粘贴箱柜。", "粘贴箱柜", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView4);
string rememberXh = _currentXianghao;
var clips = _clipXianghaos;
string pid = _currentProjectId, th = _currentTuhao;
var sw = System.Diagnostics.Stopwatch.StartNew();
Cursor = Cursors.WaitCursor;
Log("[粘贴箱柜] 开始,共 " + clips.Count + " 个箱柜...");
// ★ 粘贴前先把未保存的编辑落库:粘贴回调会从库 Reload,
// 不保存的话用户修改只存在于内存表,Reload 后被旧数据覆盖丢失
SaveAllQuoteTablesIfDirty(false);
// ★ 整条管线放后台线程,单连接+单事务;完成后回 UI 刷新
System.Threading.Tasks.Task.Run(() =>
{
int pastedCount = 0;
string lastXhId = "";
try
{
string dbPath = GetQuoteDbPath(pid);
EnsureQuoteDbExists(pid);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
foreach (var clip in clips)
{
string baseName = !string.IsNullOrWhiteSpace(clip.SrcXianghaoName) ? clip.SrcXianghaoName : "箱号";
string newXhId = MakeUniqueName(baseName,
candidate => NameExistsXhInConn2(conn, tx, th, candidate));
PasteXianghaoRowToDb(pid, th, newXhId, clip.XianghaoRow, conn, tx);
PasteYuanjianRowsToDb(pid, th, newXhId, clip.YuanjianRows, conn, tx);
pastedCount++;
lastXhId = newXhId;
}
// 箱号行聚合抄源行已正确;刷一次图号行聚合
计算引擎.刷新图号行聚合(conn, tx, dbPath, th, TuhaoTableName);
tx.Commit();
}
// 只补跨库项目汇总
计算引擎.刷新项目汇总(dbPath, pid, TuhaoTableName);
}
sw.Stop();
BeginInvoke((MethodInvoker)delegate
{
try
{
_isReloading = true;
this.advancedDataGridView4.SuspendLayout();
if (!string.IsNullOrEmpty(th) && _xianghaoCache.ContainsKey(th))
_xianghaoCache.Remove(th);
ReloadXianghaoByTuhao(pid, th);
MoveLastRowsBelowSelected(this.advancedDataGridView4, _xianghaoTable, selectedIndex,
pastedCount, "\"" + _xianghaoTable.TableName + "\"", pid);
RecalcAllAfterReload();
_currentXianghao = lastXhId;
this.advancedDataGridView4.ResumeLayout();
Log("[粘贴箱柜] 完成,共 " + pastedCount + " 个箱柜,耗时 " + sw.ElapsedMilliseconds + "ms,最后=[" + lastXhId + "]");
SetStatus("粘贴完成 " + pastedCount + " 个箱柜, 耗时 " + sw.ElapsedMilliseconds + "ms");
}
finally
{
Cursor = Cursors.Default; // ★ 恢复光标(成功/异常路径都恢复,防沙漏卡死)
_isReloading = false;
if (!string.IsNullOrEmpty(lastXhId))
SelectRowByValue(this.advancedDataGridView4, "内部ID", lastXhId);
ReloadYuanjianByXianghao(pid, th, _currentXianghao);
}
});
}
catch (Exception ex)
{
Log("[粘贴箱柜] 后台异常: " + ex.Message);
BeginInvoke((MethodInvoker)delegate
{
Cursor = Cursors.Default;
MessageBox.Show(this, "粘贴箱柜失败:\n" + ex.Message, "粘贴箱柜", MessageBoxButtons.OK, MessageBoxIcon.Error);
});
}
});
}
/// 事务内判断箱号内部ID是否已存在(同图号)。
private static bool NameExistsXhInConn2(System.Data.SQLite.SQLiteConnection conn,
System.Data.SQLite.SQLiteTransaction tx, string tuhaoId, string value)
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM 表索引 WHERE 类型='箱号' AND 图号=@th AND 箱号=@v;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", tuhaoId);
cmd.Parameters.AddWithValue("@v", value);
return Convert.ToInt32(cmd.ExecuteScalar()) > 0;
}
}
/// 把缓存的箱号字段写入数据库(新内部ID,公式列清零)。
/// ★ 带事务重载:批量粘贴共用一个事务。
private void PasteXianghaoRowToDb(string projectId, string tuhao, string newXhId, System.Collections.Generic.Dictionary srcRow,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
// ★ 复用事务内连接查表索引(自开新连接会撞外层写锁,是粘贴卡顿的根因)
string tblName = LookupTableByIndex("箱号", tuhao, "", conn);
if (string.IsNullOrEmpty(tblName))
{
tblName = GetNextTableIdTx(projectId, "xh", conn, tx);
CreateXianghaoTableTx(projectId, tblName, conn, tx);
AddTableIndexTx(projectId, "箱号", tuhao, "", tblName, conn, tx);
}
int seq;
using (var cnt = new System.Data.SQLite.SQLiteCommand(
"SELECT COALESCE(MAX(CAST(序号 AS INTEGER)),0)+1 FROM \"" + tblName + "\";", conn, tx))
seq = Convert.ToInt32(cnt.ExecuteScalar());
// 老库结构兼容:目标箱号表可能没有"箱名"列,写入前幂等补列
EnsureColumns(conn, "'" + tblName + "'", new[] { "箱名" });
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 箱号, 箱名, 型号, 规格, 单位, 数量," +
" 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 箱柜类型, 备注, 图纸, 内部ID) " +
"VALUES (@seq, @xh, @xm, @model, @spec, @unit, @count," +
" @dtcb, @cbj, @dtbc, @bcj, @lr, @lrl, @mgl, @type, @remark, @drawing, @iid);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
// ★ 副本=源:聚合值直接抄源行(批量模式下触发器被跳过,值必须随行写对)
cmd.Parameters.AddWithValue("@seq", seq.ToString());
cmd.Parameters.AddWithValue("@xh", newXhId);
cmd.Parameters.AddWithValue("@xm", GetSrcField(srcRow, "箱名", ""));
cmd.Parameters.AddWithValue("@model", GetSrcField(srcRow, "型号", ""));
cmd.Parameters.AddWithValue("@spec", GetSrcField(srcRow, "规格", ""));
cmd.Parameters.AddWithValue("@unit", GetSrcField(srcRow, "单位", "台"));
cmd.Parameters.AddWithValue("@count", GetSrcField(srcRow, "数量", "1"));
cmd.Parameters.AddWithValue("@dtcb", GetSrcFieldDecimal(srcRow, "单台成本", 0));
cmd.Parameters.AddWithValue("@cbj", GetSrcFieldDecimal(srcRow, "成本价", 0));
cmd.Parameters.AddWithValue("@dtbc", GetSrcFieldDecimal(srcRow, "单台报出", 0));
cmd.Parameters.AddWithValue("@bcj", GetSrcFieldDecimal(srcRow, "报出价", 0));
cmd.Parameters.AddWithValue("@lr", GetSrcFieldDecimal(srcRow, "利润", 0));
cmd.Parameters.AddWithValue("@lrl", GetSrcFieldDecimal(srcRow, "利润率", 0));
cmd.Parameters.AddWithValue("@mgl", GetSrcFieldDecimal(srcRow, "毛利率", 0));
cmd.Parameters.AddWithValue("@type", GetSrcField(srcRow, "箱柜类型", ""));
cmd.Parameters.AddWithValue("@remark", GetSrcField(srcRow, "备注", ""));
cmd.Parameters.AddWithValue("@drawing", GetSrcField(srcRow, "图纸", ""));
cmd.Parameters.AddWithValue("@iid", newXhId);
cmd.ExecuteNonQuery();
}
}
private void PasteXianghaoRowToDb(string projectId, string tuhao, string newXhId, System.Collections.Generic.Dictionary srcRow)
{
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
string tblName = GetXianghaoTableByIndex(projectId, tuhao);
if (string.IsNullOrEmpty(tblName))
{
tblName = GetNextTableId(projectId, "xh");
CreateXianghaoTable(projectId, tblName);
AddTableIndex(projectId, "箱号", tuhao, "", tblName);
}
string seq = GetXianghaoCount(projectId, tuhao);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
// 老库结构兼容:目标箱号表可能没有"箱名"列,写入前幂等补列
EnsureColumns(conn, "\"" + tblName + "\"", new[] { "箱名" });
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 箱号, 箱名, 型号, 规格, 单位, 数量," +
" 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 箱柜类型, 备注, 图纸, 内部ID) " +
"VALUES (@seq, @xh, @xm, @model, @spec, @unit, @count," +
" 0, 0, 0, 0, 0, 0, 0, @type, @remark, @drawing, @iid);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@xh", newXhId); // 箱号显示名先用内部ID
cmd.Parameters.AddWithValue("@xm", GetSrcField(srcRow, "箱名", ""));
cmd.Parameters.AddWithValue("@model", GetSrcField(srcRow, "型号", ""));
cmd.Parameters.AddWithValue("@spec", GetSrcField(srcRow, "规格", ""));
cmd.Parameters.AddWithValue("@unit", GetSrcField(srcRow, "单位", "台"));
cmd.Parameters.AddWithValue("@count", GetSrcField(srcRow, "数量", "1"));
cmd.Parameters.AddWithValue("@type", GetSrcField(srcRow, "箱柜类型", ""));
cmd.Parameters.AddWithValue("@remark", GetSrcField(srcRow, "备注", ""));
cmd.Parameters.AddWithValue("@drawing", GetSrcField(srcRow, "图纸", ""));
cmd.Parameters.AddWithValue("@iid", newXhId);
cmd.ExecuteNonQuery();
}
}
}
// ===== 元件:复制/粘贴 =====
private void MenuYuanjian_Copy_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao))
{
MessageBox.Show(this, "请先选择一个箱柜。", "复制元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 支持多选:收集全部选中行(按表格显示顺序),一次性复制整批元件
var rows = GetAllSelectedRows(this.advancedDataGridView5);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先选择要复制的元件。", "复制元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
rows.Sort((a, b) => a.Index.CompareTo(b.Index)); // 按行号排序,保持表格顺序
var exclude = new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase) { "序号" };
_clipYuanjianRows = new System.Collections.Generic.List>();
foreach (var row in rows)
_clipYuanjianRows.Add(CopyRowFields(row, exclude));
Log("[复制元件] 已缓存元件 " + _clipYuanjianRows.Count + " 个");
}
private void MenuYuanjian_Paste_Click(object sender, EventArgs e)
{
if (_clipYuanjianRows == null || _clipYuanjianRows.Count == 0)
{
MessageBox.Show(this, "剪贴板里没有元件,请先复制。", "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao))
{
MessageBox.Show(this, "请先选择一个箱柜再粘贴元件。", "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
int 条数 = _clipYuanjianRows.Count;
string pid = _currentProjectId, th = _currentTuhao, xh = _currentXianghao;
var rowsSnapshot = _clipYuanjianRows;
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
_isReloading = true;
Cursor = Cursors.WaitCursor;
Log("[粘贴元件] 开始,共 " + 条数 + " 条...");
// UI 线程:提交当前编辑并保存脏表(快,脏才存)
SaveAllQuoteTablesIfDirty(false);
// ★ 批量插入放后台线程(纯 SQLite 操作,不碰任何 UI 对象),界面不阻塞
System.Threading.Tasks.Task.Run(() =>
{
try
{
PasteYuanjianRowsToDb(pid, th, xh, rowsSnapshot);
BeginInvoke((MethodInvoker)delegate
{
try
{
// 以下必须 UI 线程(操作绑定的 DataTable 和表格)
string cacheKey = th + "\n" + xh;
if (_yuanjianCache.ContainsKey(cacheKey)) _yuanjianCache.Remove(cacheKey);
ReloadYuanjianByXianghao(pid, th, xh);
MoveLastRowsBelowSelected(this.advancedDataGridView5, _yuanjianTable, selectedIndex,
条数, "\"" + _yuanjianTable.TableName + "\"", pid);
RecalcAllAfterReload();
// ★ 选中刚粘贴的新元件行(在选中行下面,或在末尾)
int newYjIdx = selectedIndex >= 0 ? selectedIndex + 1 : this.advancedDataGridView5.Rows.Count - 1;
if (newYjIdx < 0) newYjIdx = this.advancedDataGridView5.Rows.Count - 1;
if (newYjIdx >= 0 && newYjIdx < this.advancedDataGridView5.Rows.Count)
{
try
{
this.advancedDataGridView5.ClearSelection();
this.advancedDataGridView5.Rows[newYjIdx].Selected = true;
this.advancedDataGridView5.CurrentCell = this.advancedDataGridView5.Rows[newYjIdx].Cells[0];
this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = newYjIdx;
}
catch { }
}
sw.Stop();
Log("[粘贴元件] 完成,共 " + 条数 + " 条,耗时 " + sw.ElapsedMilliseconds + "ms,选中行=" + newYjIdx);
SetStatus("粘贴完成 " + 条数 + " 条元件, 耗时 " + sw.ElapsedMilliseconds + "ms");
}
catch (Exception exUi)
{
Log("[粘贴元件] 刷新异常: " + exUi.Message);
MessageBox.Show(this, "粘贴后刷新失败(已记录日志):\n" + exUi.Message,
"粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
Cursor = Cursors.Default;
_isReloading = false;
}
});
}
catch (Exception exDb)
{
Log("[粘贴元件] 批量插入异常: " + exDb.Message);
BeginInvoke((MethodInvoker)delegate
{
Cursor = Cursors.Default;
_isReloading = false;
Log("[粘贴元件] 批量插入异常: " + exDb.Message);
MessageBox.Show(this, "粘贴元件失败:\n" + exDb.Message,
"粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
});
}
});
// 保留 _clipYuanjianRows,支持多次粘贴相同内容
}
catch (Exception ex)
{
Cursor = Cursors.Default;
_isReloading = false;
Log("[粘贴元件] 异常: " + ex.Message);
MessageBox.Show(this, "粘贴元件失败:\n" + ex.Message, "粘贴元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// 把缓存的元件字段写入数据库(公式列清零,等重算)。
///
/// ★ 批量粘贴元件到目标箱号:单连接+单事务,一次性插入整批。
/// 旧的单条循环每条要开关 3~4 次连接、查 5 条 SQL,粘 N 条卡 N 倍;
/// 这里一次解析目标表、一次取起始序号、预编译命令循环插、一次提交。
///
/// ★ 带事务重载:图号级联粘贴共用外层事务。
private void PasteYuanjianRowsToDb(string projectId, string tuhao, string xhId,
System.Collections.Generic.List> srcRows,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
if (srcRows == null || srcRows.Count == 0) return;
// ★ 复用事务内连接查表索引(自开新连接会撞外层写锁)
string tblName = LookupTableByIndex("元件", tuhao, xhId, conn);
if (string.IsNullOrEmpty(tblName))
{
tblName = GetNextTableIdTx(projectId, "yj", conn, tx);
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"CREATE TABLE \"" + tblName + "\" (" +
" 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," +
" 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," +
" 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT);", conn, tx))
cmd.ExecuteNonQuery();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", conn, tx))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.Parameters.AddWithValue("@xh", xhId);
cmd.Parameters.AddWithValue("@real", tblName);
cmd.ExecuteNonQuery();
}
}
int seq = GetYuanjianCountTx(projectId, tuhao, xhId, tblName, conn, tx);
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 元件名称, 规格, 单位, 数量, 表价," +
" 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," +
" 图纸规格, 品牌, 系列, 类型, 备注) " +
"VALUES (@seq, @name, @spec, @unit, @count, @biaojia," +
" @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," +
" @drawingSpec, @brand, @series, @type, @remark);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
foreach (string pn in new[] { "@seq", "@name", "@spec", "@unit", "@count", "@biaojia",
"@caigouDanjia", "@caigouXishu", "@caigouJia", "@baochuDanjia", "@baochuXishu",
"@baochuJia", "@drawingSpec", "@brand", "@series", "@type", "@remark" })
cmd.Parameters.AddWithValue(pn, "");
foreach (var srcRow in srcRows)
{
cmd.Parameters["@seq"].Value = seq.ToString();
cmd.Parameters["@name"].Value = GetSrcField(srcRow, "元件名称", "");
cmd.Parameters["@spec"].Value = GetSrcField(srcRow, "规格", "");
cmd.Parameters["@unit"].Value = GetSrcField(srcRow, "单位", "");
cmd.Parameters["@count"].Value = GetSrcField(srcRow, "数量", "1");
cmd.Parameters["@biaojia"].Value = GetSrcFieldDecimal(srcRow, "表价", 0);
cmd.Parameters["@caigouDanjia"].Value = GetSrcFieldDecimal(srcRow, "采购单价", 0);
cmd.Parameters["@caigouXishu"].Value = GetSrcField(srcRow, "采购系数", "1");
cmd.Parameters["@caigouJia"].Value = GetSrcFieldDecimal(srcRow, "采购价", 0);
cmd.Parameters["@baochuDanjia"].Value = GetSrcFieldDecimal(srcRow, "报出单价", 0);
cmd.Parameters["@baochuXishu"].Value = GetSrcField(srcRow, "报出系数", "1");
cmd.Parameters["@baochuJia"].Value = GetSrcFieldDecimal(srcRow, "报出价", 0);
cmd.Parameters["@drawingSpec"].Value = GetSrcField(srcRow, "图纸规格", "");
cmd.Parameters["@brand"].Value = GetSrcField(srcRow, "品牌", "");
cmd.Parameters["@series"].Value = GetSrcField(srcRow, "系列", "");
cmd.Parameters["@type"].Value = GetSrcField(srcRow, "类型", "");
cmd.Parameters["@remark"].Value = GetSrcField(srcRow, "备注", "");
cmd.ExecuteNonQuery();
seq++;
}
}
}
private void PasteYuanjianRowsToDb(string projectId, string tuhao, string xhId,
System.Collections.Generic.List> srcRows)
{
if (srcRows == null || srcRows.Count == 0) return;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xhId);
if (string.IsNullOrEmpty(tblName))
{
tblName = GetNextTableId(projectId, "yj");
CreateYuanjianTable(projectId, tblName);
AddTableIndex(projectId, "元件", tuhao, xhId, tblName);
}
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
int seq;
using (var cnt = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM \"" + tblName + "\";", conn, tx))
seq = Convert.ToInt32(cnt.ExecuteScalar());
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 元件名称, 规格, 单位, 数量, 表价," +
" 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," +
" 图纸规格, 品牌, 系列, 类型, 备注) " +
"VALUES (@seq, @name, @spec, @unit, @count, @biaojia," +
" @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," +
" @drawingSpec, @brand, @series, @type, @remark);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
foreach (string pn in new[] { "@seq", "@name", "@spec", "@unit", "@count", "@biaojia",
"@caigouDanjia", "@caigouXishu", "@caigouJia", "@baochuDanjia", "@baochuXishu",
"@baochuJia", "@drawingSpec", "@brand", "@series", "@type", "@remark" })
cmd.Parameters.AddWithValue(pn, "");
foreach (var srcRow in srcRows)
{
seq++;
cmd.Parameters["@seq"].Value = seq.ToString();
cmd.Parameters["@name"].Value = GetSrcField(srcRow, "元件名称", "");
cmd.Parameters["@spec"].Value = GetSrcField(srcRow, "规格", "");
cmd.Parameters["@unit"].Value = GetSrcField(srcRow, "单位", "");
cmd.Parameters["@count"].Value = GetSrcField(srcRow, "数量", "1");
cmd.Parameters["@biaojia"].Value = GetSrcFieldDecimal(srcRow, "表价", 0);
cmd.Parameters["@caigouDanjia"].Value = GetSrcFieldDecimal(srcRow, "采购单价", 0);
cmd.Parameters["@caigouXishu"].Value = GetSrcField(srcRow, "采购系数", "1");
cmd.Parameters["@caigouJia"].Value = GetSrcFieldDecimal(srcRow, "采购价", 0);
cmd.Parameters["@baochuDanjia"].Value = GetSrcFieldDecimal(srcRow, "报出单价", 0);
cmd.Parameters["@baochuXishu"].Value = GetSrcField(srcRow, "报出系数", "1");
cmd.Parameters["@baochuJia"].Value = GetSrcFieldDecimal(srcRow, "报出价", 0);
cmd.Parameters["@drawingSpec"].Value = GetSrcField(srcRow, "图纸规格", "");
cmd.Parameters["@brand"].Value = GetSrcField(srcRow, "品牌", "");
cmd.Parameters["@series"].Value = GetSrcField(srcRow, "系列", "");
cmd.Parameters["@type"].Value = GetSrcField(srcRow, "类型", "");
cmd.Parameters["@remark"].Value = GetSrcField(srcRow, "备注", "");
cmd.ExecuteNonQuery();
}
}
tx.Commit();
}
}
}
private void PasteYuanjianRowToDb(string projectId, string tuhao, string xhId, System.Collections.Generic.Dictionary srcRow)
{
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xhId);
if (string.IsNullOrEmpty(tblName))
{
tblName = GetNextTableId(projectId, "yj");
CreateYuanjianTable(projectId, tblName);
AddTableIndex(projectId, "元件", tuhao, xhId, tblName);
}
string seq = GetYuanjianCount(projectId, tuhao, xhId);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 元件名称, 规格, 单位, 数量, 表价," +
" 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," +
" 图纸规格, 品牌, 系列, 类型, 备注) " +
"VALUES (@seq, @name, @spec, @unit, @count, @biaojia," +
" @caigouDanjia, @caigouXishu, @caigouJia, @baochuDanjia, @baochuXishu, @baochuJia," +
" @drawingSpec, @brand, @series, @type, @remark);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@name", GetSrcField(srcRow, "元件名称", ""));
cmd.Parameters.AddWithValue("@spec", GetSrcField(srcRow, "规格", ""));
cmd.Parameters.AddWithValue("@unit", GetSrcField(srcRow, "单位", ""));
cmd.Parameters.AddWithValue("@count", GetSrcField(srcRow, "数量", "1"));
cmd.Parameters.AddWithValue("@biaojia", GetSrcFieldDecimal(srcRow, "表价", 0));
cmd.Parameters.AddWithValue("@caigouDanjia", GetSrcFieldDecimal(srcRow, "采购单价", 0));
cmd.Parameters.AddWithValue("@caigouXishu", GetSrcField(srcRow, "采购系数", "1"));
cmd.Parameters.AddWithValue("@caigouJia", GetSrcFieldDecimal(srcRow, "采购价", 0));
cmd.Parameters.AddWithValue("@baochuDanjia", GetSrcFieldDecimal(srcRow, "报出单价", 0));
cmd.Parameters.AddWithValue("@baochuXishu", GetSrcField(srcRow, "报出系数", "1"));
cmd.Parameters.AddWithValue("@baochuJia", GetSrcFieldDecimal(srcRow, "报出价", 0));
cmd.Parameters.AddWithValue("@drawingSpec", GetSrcField(srcRow, "图纸规格", ""));
cmd.Parameters.AddWithValue("@brand", GetSrcField(srcRow, "品牌", ""));
cmd.Parameters.AddWithValue("@series", GetSrcField(srcRow, "系列", ""));
cmd.Parameters.AddWithValue("@type", GetSrcField(srcRow, "类型", ""));
cmd.Parameters.AddWithValue("@remark", GetSrcField(srcRow, "备注", ""));
cmd.ExecuteNonQuery();
}
}
}
/// 从字段字典读字符串(找不到返回默认值)。
private static string GetSrcField(System.Collections.Generic.Dictionary src, string key, string defVal)
{
if (src == null || string.IsNullOrEmpty(key)) return defVal;
foreach (var kv in src)
{
if (string.Equals(kv.Key, key, System.StringComparison.OrdinalIgnoreCase))
{
if (kv.Value == null) return defVal;
string s = kv.Value.ToString().Trim();
return string.IsNullOrEmpty(s) ? defVal : s;
}
}
return defVal;
}
/// 从字段字典读 decimal(找不到/非数字返回默认值)。
private static decimal GetSrcFieldDecimal(System.Collections.Generic.Dictionary src, string key, decimal defVal)
{
string s = GetSrcField(src, key, "");
decimal d;
if (decimal.TryParse(s, out d)) return d;
return defVal;
}
// ============================================================
// 撤销删除(快照式):删除操作前把整库备份到 undo 缓存,
// 右键「撤销删除」把库还原到删除前状态。只支持撤销最近一次删除。
// ============================================================
/// 撤销缓存目录(exe 下 undo_cache\),null=当前无可撤销操作。
private string _undoSnapshotDir = null;
/// 撤销快照对应的目标库路径(报价库 或 fak.db)。
private string _undoTargetDbPath = null;
/// 撤销快照对应的项目编号(用于还原后重载界面)。
private string _undoProjectId = null;
/// 撤销快照是否来自方案库(fak.db)。
private bool _undoIsFak = false;
///
/// 删除前调用:把目标库整体快照到 undo 缓存(用 SQLite Backup API 避锁)。
/// 同一次会话只保留最近一次快照(新删除覆盖旧快照)。
///
private void PushUndoSnapshot(string targetDbPath, string projectId, bool isFak)
{
try
{
if (string.IsNullOrEmpty(targetDbPath) || !System.IO.File.Exists(targetDbPath)) return;
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string undoDir = System.IO.Path.Combine(baseDir, "undo_cache");
if (!System.IO.Directory.Exists(undoDir))
System.IO.Directory.CreateDirectory(undoDir);
// 清掉旧快照(只保留一份)
try { System.IO.Directory.Delete(undoDir, true); } catch { }
System.IO.Directory.CreateDirectory(undoDir);
string snapPath = System.IO.Path.Combine(undoDir, "snapshot.db");
BackupSQLiteDb(targetDbPath, snapPath);
_undoSnapshotDir = undoDir;
_undoTargetDbPath = targetDbPath;
_undoProjectId = projectId;
_undoIsFak = isFak;
UpdateUndoMenuEnabled();
Log("[撤销] 已快照: " + targetDbPath);
}
catch (Exception ex)
{
// 快照失败不阻断删除,只是无法撤销
_undoSnapshotDir = null;
Log("[撤销] 快照失败(本次删除将无法撤销): " + ex.Message);
}
}
/// 右键菜单「撤销删除」:把库还原到最近一次删除前的快照。
private void MenuUndoDelete_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_undoSnapshotDir) || string.IsNullOrEmpty(_undoTargetDbPath))
{
MessageBox.Show(this, "当前没有可撤销的删除操作。",
"撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string snapPath = System.IO.Path.Combine(_undoSnapshotDir, "snapshot.db");
if (!System.IO.File.Exists(snapPath))
{
MessageBox.Show(this, "撤销快照已丢失,无法撤销。",
"撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_undoSnapshotDir = null;
UpdateUndoMenuEnabled();
return;
}
if (MessageBox.Show(this,
"将把数据还原到最近一次删除之前的状态,\n当前删除之后做的修改会丢失。\n\n确定撤销吗?",
"撤销删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2) != DialogResult.Yes)
return;
try
{
_isReloading = true;
// 先把内存里未保存的编辑丢弃(以快照为准),清缓存
_xianghaoCache.Clear();
_yuanjianCache.Clear();
// 快照 → 目标库
BackupSQLiteDb(snapPath, _undoTargetDbPath);
bool wasFak = _undoIsFak;
string pid = _undoProjectId;
string tuhao = _currentTuhao;
string xianghao = _currentXianghao;
// ★ 快照库的触发器可能是旧公式版本,还原后按当前公式表重建
if (!wasFak) EnsureFormulaTriggersQuiet(_undoProjectId);
if (wasFak)
{
ReloadFakData();
MessageBox.Show(this, "已撤销:方案库已还原到删除前状态。",
"撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
if (!string.IsNullOrEmpty(pid))
{
ReloadTuhaoByProject(pid);
RecalcAllAfterReload();
// 尽量恢复选中
if (!string.IsNullOrEmpty(tuhao)) SelectRowByValue(this.advancedDataGridView9, "内部ID", tuhao);
if (!string.IsNullOrEmpty(xianghao)) SelectRowByValue(this.advancedDataGridView4, "内部ID", xianghao);
}
MessageBox.Show(this, "已撤销:数据已还原到删除前状态。",
"撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
Log("[撤销] 已还原: " + _undoTargetDbPath);
// 撤销只允许一次(避免在还原后的状态上再次"撤销"造成混乱)
_undoSnapshotDir = null;
_undoTargetDbPath = null;
UpdateUndoMenuEnabled();
}
catch (Exception ex)
{
MessageBox.Show(this, "撤销失败:\n" + ex.Message,
"撤销删除", MessageBoxButtons.OK, MessageBoxIcon.Error);
Log("[撤销] 失败: " + ex.Message);
}
finally
{
_isReloading = false;
}
}
/// 根据是否存在可撤销快照,刷新所有右键菜单里「撤销删除」项的可用状态。
private void UpdateUndoMenuEnabled()
{
bool can = (!string.IsNullOrEmpty(_undoSnapshotDir) &&
System.IO.File.Exists(System.IO.Path.Combine(_undoSnapshotDir, "snapshot.db")));
SetMenuItemEnabled(_menuTuhao, "撤销删除", can);
SetMenuItemEnabled(_menuXianghao, "撤销删除", can);
SetMenuItemEnabled(_menuYuanjian, "撤销删除", can);
SetMenuItemEnabled(_menuFakTuhao, "撤销删除", can);
SetMenuItemEnabled(_menuFakXianghao, "撤销删除", can);
SetMenuItemEnabled(_menuFakYuanjian, "撤销删除", can);
}
/// 在右键菜单里按文本找菜单项并设置 Enabled(找不到忽略)。
private static void SetMenuItemEnabled(ContextMenuStrip menu, string text, bool enabled)
{
if (menu == null) return;
foreach (ToolStripItem item in menu.Items)
{
if (item is ToolStripMenuItem && item.Text == text)
{
item.Enabled = enabled;
break;
}
}
}
/// 创建「撤销删除」菜单项:初始不可用,有待撤销快照时才亮起。
private ToolStripMenuItem MakeUndoMenuItem()
{
var mi = new ToolStripMenuItem("撤销删除", null, MenuUndoDelete_Click);
mi.Enabled = false; // 无快照时灰色;PushUndoSnapshot 里 UpdateUndoMenuEnabled 会点亮
return mi;
}
private void BuildContextMenus()
{
Log("开始创建右键菜单...");
// ---- 图号菜单(advancedDataGridView9)----
_menuTuhao = new ContextMenuStrip();
_menuTuhao.Items.Add("新建图号", null, MenuTuhao_New_Click);
_menuTuhao.Items.Add("删除图号", null, MenuTuhao_Delete_Click);
_menuTuhao.Items.Add(MakeUndoMenuItem());
_menuTuhao.Items.Add(new ToolStripSeparator());
_menuTuhao.Items.Add("复制图号", null, MenuTuhao_Copy_Click);
_menuTuhao.Items.Add("粘贴图号", null, MenuTuhao_Paste_Click);
_menuTuhao.Items.Add(new ToolStripSeparator());
_menuTuhao.Items.Add("加入方案库", null, MenuTuhao_AddToFak_Click);
_menuTuhao.Items.Add(new ToolStripSeparator());
_menuTuhao.Items.Add("置顶", null, MenuTuhao_MoveTop_Click);
_menuTuhao.Items.Add("向上移动", null, MenuTuhao_MoveUp_Click);
_menuTuhao.Items.Add("向下移动", null, MenuTuhao_MoveDown_Click);
_menuTuhao.Items.Add("置底", null, MenuTuhao_MoveBottom_Click);
_menuTuhao.Items.Add("移动到...", null, MenuTuhao_MoveTo_Click);
this.advancedDataGridView9.ContextMenuStrip = _menuTuhao;
Log(" 图号菜单已挂到 advancedDataGridView9(含「加入方案库」+ 行移动)");
// ---- 箱号菜单(advancedDataGridView4)----
_menuXianghao = new ContextMenuStrip();
_menuXianghao.Items.Add("新建箱号", null, MenuXianghao_New_Click);
_menuXianghao.Items.Add("删除箱号", null, MenuXianghao_Delete_Click);
_menuXianghao.Items.Add(MakeUndoMenuItem());
_menuXianghao.Items.Add(new ToolStripSeparator());
_menuXianghao.Items.Add("复制箱柜", null, MenuXianghao_Copy_Click);
_menuXianghao.Items.Add("粘贴箱柜", null, MenuXianghao_Paste_Click);
_menuXianghao.Items.Add(new ToolStripSeparator());
_menuXianghao.Items.Add("加入方案库", null, MenuXianghao_AddToFak_Click);
_menuXianghao.Items.Add(new ToolStripSeparator());
_menuXianghao.Items.Add("置顶", null, MenuXianghao_MoveTop_Click);
_menuXianghao.Items.Add("向上移动", null, MenuXianghao_MoveUp_Click);
_menuXianghao.Items.Add("向下移动", null, MenuXianghao_MoveDown_Click);
_menuXianghao.Items.Add("置底", null, MenuXianghao_MoveBottom_Click);
_menuXianghao.Items.Add("移动到...", null, MenuXianghao_MoveTo_Click);
this.advancedDataGridView4.ContextMenuStrip = _menuXianghao;
Log(" 箱号菜单已挂到 advancedDataGridView4(含行移动)");
// ---- 元件菜单(advancedDataGridView5)----
_menuYuanjian = new ContextMenuStrip();
_menuYuanjian.Items.Add("新建元件", null, MenuYuanjian_New_Click);
_menuYuanjian.Items.Add("删除元件", null, MenuYuanjian_Delete_Click);
_menuYuanjian.Items.Add(MakeUndoMenuItem());
_menuYuanjian.Items.Add(new ToolStripSeparator());
_menuYuanjian.Items.Add("复制元件", null, MenuYuanjian_Copy_Click);
_menuYuanjian.Items.Add("粘贴元件", null, MenuYuanjian_Paste_Click);
_menuYuanjian.Items.Add(new ToolStripSeparator());
_menuYuanjian.Items.Add("加入用户库", null, MenuYuanjian_AddToUserLib_Click);
_menuYuanjian.Items.Add("加入柜体库", null, MenuYuanjian_AddToGuiTi_Click);
_menuYuanjian.Items.Add("加入方案库", null, MenuYuanjian_AddToFak_Click);
_menuYuanjian.Items.Add(new ToolStripSeparator());
_menuYuanjian.Items.Add("置顶", null, MenuYuanjian_MoveTop_Click);
_menuYuanjian.Items.Add("向上移动", null, MenuYuanjian_MoveUp_Click);
_menuYuanjian.Items.Add("向下移动", null, MenuYuanjian_MoveDown_Click);
_menuYuanjian.Items.Add("置底", null, MenuYuanjian_MoveBottom_Click);
_menuYuanjian.Items.Add("移动到...", null, MenuYuanjian_MoveTo_Click);
this.advancedDataGridView5.ContextMenuStrip = _menuYuanjian;
Log(" 元件菜单已挂到 advancedDataGridView5(含「加入用户库」「加入柜体库」+ 行移动)");
// ---- 方案窗口 三表右键菜单(操作当前打开的方案文件)----
_menuFakTuhao = new ContextMenuStrip();
_menuFakTuhao.Items.Add("删除图号", null, FakTuhao_Delete_Click);
_menuFakTuhao.Items.Add(MakeUndoMenuItem());
_menuFakTuhao.Items.Add(new ToolStripSeparator());
_menuFakTuhao.Items.Add("插入图号到当前项目", null, FakTuhao_Insert_Click);
this.advancedDataGridView图号.ContextMenuStrip = _menuFakTuhao;
Log(" 方案图号菜单已挂到 advancedDataGridView图号(删除/插入到当前项目)");
_menuFakXianghao = new ContextMenuStrip();
_menuFakXianghao.Items.Add("删除箱号", null, FakXianghao_Delete_Click);
_menuFakXianghao.Items.Add(MakeUndoMenuItem());
_menuFakXianghao.Items.Add(new ToolStripSeparator());
_menuFakXianghao.Items.Add("插入箱号到当前项目", null, FakXianghao_Insert_Click);
this.advancedDataGridView箱号.ContextMenuStrip = _menuFakXianghao;
Log(" 方案箱号菜单已挂到 advancedDataGridView箱号(删除/插入到当前项目)");
_menuFakYuanjian = new ContextMenuStrip();
_menuFakYuanjian.Items.Add("删除元件", null, FakYuanjian_Delete_Click);
_menuFakYuanjian.Items.Add(MakeUndoMenuItem());
_menuFakYuanjian.Items.Add(new ToolStripSeparator());
_menuFakYuanjian.Items.Add("插入元件到当前项目", null, FakYuanjian_Insert_Click);
this.advancedDataGridView元件.ContextMenuStrip = _menuFakYuanjian;
Log(" 方案元件菜单已挂到 advancedDataGridView元件(删除/插入到当前项目)");
// ---- 方案窗口(advancedDataGridView图号/箱号/元件)右键菜单已由 DockWindow8 自治挂接
// (操作对象为当前打开的方案文件,不再是 fak.db)----
// 清理上次运行残留的撤销快照(撤销只在本次会话内有效,重启即失效)
try
{
string undoDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "undo_cache");
if (System.IO.Directory.Exists(undoDir))
System.IO.Directory.Delete(undoDir, true);
}
catch { }
}
// ============================================================
// 右键菜单:行移动(置顶/向上/向下/置底/移动到)
// 适用于三个报价表:图号(advancedDataGridView9)、箱号(advancedDataGridView4)、元件(advancedDataGridView5)
// 原理:在内存 DataTable 中移动 DataRow 位置 → 重排序号列 → 保存到库(DELETE+INSERT) → 重新加载
// ============================================================
// ============================================================
// ★ 行头选择模式(2026-09-30):递归扫描控件树,把所有"有行头 + CellSelect"
// 的表格改为 RowHeaderSelect(Excel 风格:点格=选格,点行头=选整行,
// Ctrl/Shift+点行头=多选整行)。有选中单元格时先清空(改模式的前提)。
// ============================================================
internal static void ApplyRowHeaderSelectAll(System.Windows.Forms.Control root)
{
if (root == null) return;
var dgv = root as System.Windows.Forms.DataGridView;
if (dgv != null)
{
try
{
if (dgv.RowHeadersVisible &&
dgv.SelectionMode == System.Windows.Forms.DataGridViewSelectionMode.CellSelect)
{
if (dgv.SelectedCells.Count > 0) dgv.ClearSelection();
dgv.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.RowHeaderSelect;
}
}
catch { /* 个别表格改不动(设计器锁/特殊情况)跳过 */ }
}
foreach (System.Windows.Forms.Control c in root.Controls) ApplyRowHeaderSelectAll(c);
}
private enum RowMoveAction { Top, Up, Down, Bottom, To }
// ============================================================
// ★ 右键多选保持钩子(2026-09-30,抄扒图窗口同款):
// 没有它,多选若干行后右键点"移动"菜单的瞬间,ADGV 默认行为会把
// 选中重置为右键所在行 → 多选被清空 → 移动只动一行(用户报障根因)。
// ============================================================
private System.Collections.Generic.List _keepMultiHooks;
private void HookKeepMulti(System.Windows.Forms.DataGridView dgv)
{
if (dgv == null) return;
if (_keepMultiHooks == null) _keepMultiHooks = new System.Collections.Generic.List();
var hook = new AdgvMsgHook(dgv, msg => { try { Log(msg); } catch { } });
_keepMultiHooks.Add(hook); // 存字段防 GC
try { Log("[右键多选保持] 已挂钩 dgv=" + dgv.Name + " MultiSelect=" + dgv.MultiSelect); } catch { }
}
///
/// ADGV 右键多选保持钩子(NativeWindow):吞掉"右键点已选中行"的 WM_RBUTTONDOWN,
/// 多选集合不被清空;右键菜单由 WM_CONTEXTMENU 正常弹出(与扒图窗口同款语义)。
///
private class AdgvMsgHook : System.Windows.Forms.NativeWindow
{
private const int WM_RBUTTONDOWN = 0x0204;
private readonly System.Windows.Forms.DataGridView _dgv;
private readonly System.Action _log;
public AdgvMsgHook(System.Windows.Forms.DataGridView dgv, System.Action log)
{
_dgv = dgv;
_log = log;
dgv.HandleCreated += (s, e) => AssignHandle(dgv.Handle);
dgv.HandleDestroyed += (s, e) => ReleaseHandle();
if (dgv.IsHandleCreated) AssignHandle(dgv.Handle);
}
protected override void WndProc(ref System.Windows.Forms.Message m)
{
if (m.Msg == WM_RBUTTONDOWN && _dgv != null && _dgv.MultiSelect)
{
try
{
int lp = m.LParam.ToInt32();
var pos = new System.Drawing.Point(lp & 0xFFFF, (lp >> 16) & 0xFFFF);
var hit = _dgv.HitTest(pos.X, pos.Y);
if (hit.RowIndex >= 0 && _dgv.SelectedCells != null)
{
// ★ 关键修复(2026-09-30):CellSelect 模式下 SelectedRows 恒空,
// 用 SelectedCells 判断"右键点在已选区域内"(两种选择模式通吃)
foreach (System.Windows.Forms.DataGridViewCell c in _dgv.SelectedCells)
{
if (c != null && c.RowIndex == hit.RowIndex)
{
if (_log != null) try { _log("[右键多选保持] 吞掉 dgv=" + _dgv.Name + " row=" + hit.RowIndex + " 已选格数=" + _dgv.SelectedCells.Count); } catch { }
return; // ★ 吞掉:不清多选
}
}
if (_log != null) try { _log("[右键多选保持] 未拦截(点在未选中区域) dgv=" + _dgv.Name + " hit=" + hit.RowIndex + " 已选格数=" + _dgv.SelectedCells.Count); } catch { }
}
}
catch { /* 异常时退回默认行为 */ }
}
base.WndProc(ref m);
}
}
///
/// 通用行移动(★ 2026-09-30 多选适配):把 dgv 中选中的【连续或离散】多行整块移动到
/// 指定位置(保持相对顺序),重排序号,保存,恢复多选。单选行同样走块逻辑(块=1行)。
/// dgv 决定操作哪个表(图号/箱号/元件)。
///
private void MoveSelectedRowInGrid(DataGridView dgv, RowMoveAction action)
{
if (dgv == null || string.IsNullOrWhiteSpace(_currentProjectId)) return;
// 确定操作的 DataTable 和重载方法
DataTable table = null;
if (dgv == this.advancedDataGridView5)
{
table = _yuanjianTable;
}
else if (dgv == this.advancedDataGridView4)
{
table = _xianghaoTable;
}
else if (dgv == this.advancedDataGridView9)
{
table = _tuhaoTable;
}
if (table == null || table.Rows.Count == 0)
{
MessageBox.Show(this, "没有数据可移动。", "移动行",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 多选:取全部选中行索引(升序,排除 NewRow)
// ★ 关键修复(2026-09-30):表格是默认 CellSelect 模式,SelectedRows 永远为空
// (诊断日志"已选行数=0"实锤)——改用 SelectedCells 的行索引去重,
// CellSelect/FullRowSelect 两种模式都工作
var selSet = new System.Collections.Generic.SortedSet();
foreach (DataGridViewCell c in dgv.SelectedCells)
{
if (c == null || c.RowIndex < 0 || c.RowIndex >= table.Rows.Count) continue;
selSet.Add(c.RowIndex);
}
var selIndices = new System.Collections.Generic.List(selSet);
if (selIndices.Count == 0)
{
// 兜底 CurrentRow(某些选中态下 SelectedCells 不含当前行)
var cur = dgv.CurrentRow;
if (cur != null && !cur.IsNewRow && cur.Index >= 0 && cur.Index < table.Rows.Count)
selIndices.Add(cur.Index);
}
if (selIndices.Count == 0)
{
MessageBox.Show(this, "请先选择要移动的行。", "移动行",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
selIndices.Sort();
int count = selIndices.Count;
// ★ 费用行置底保护(2026-10-01):元件表的费用行固定在底部,不参与移动
if (table.Columns.Contains("类型"))
{
foreach (int idx in selIndices)
{
if (Convert.ToString(table.Rows[idx]["类型"]).Trim() == "费用")
{
MessageBox.Show(this, "费用行固定显示在最底部,不能移动。\n(如需调整费用请用功能区「模板费用」)",
"移动行", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
}
}
// 数据行数(排除末尾 NewRow)
int dataCount = dgv.Rows.Count;
if (dgv.AllowUserToAddRows) dataCount--;
if (dataCount <= 0) return;
int blockStart = selIndices[0];
int blockEnd = selIndices[count - 1];
// ★ 费用行置底保护:普通行块可移动的边界=费用行区首行(不得移进费用区)
int 可移边界 = 费用区首行(table); // 无类型列时=Rows.Count(不限制)
// ★ 尽早设 guard,防止"移动到"对话框期间或行移动期间触发 SelectionChanged 联动重载,
// 导致 table 引用的 DataTable 和 _yuanjianTable 不一致(之前的 bug 根因)
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
int newBlockStart = -1; // 提到 try 外,finally 中要用(<0 表示未移动)
_isReloading = true;
try
{
// 提交当前编辑到 DataTable
dgv.EndEdit();
// 计算目标块首位置(整块移动,保持相对顺序)
switch (action)
{
case RowMoveAction.Top:
newBlockStart = 0;
break;
case RowMoveAction.Up:
newBlockStart = blockStart > 0 ? blockStart - 1 : blockStart;
break;
case RowMoveAction.Down:
// ★ 费用行置底保护:下移一步若块尾进入费用行区则不动
if (blockEnd + 1 >= 可移边界) newBlockStart = blockStart;
else newBlockStart = blockEnd < dataCount - 1 ? blockStart + 1 : blockStart;
break;
case RowMoveAction.Bottom:
// ★ 费用行置底保护:"置底"=移到费用行区之前(费用行永远在最后)
newBlockStart = Math.Max(0, Math.Min(dataCount - count, 可移边界 - count));
if (blockEnd == 可移边界 - 1) newBlockStart = blockStart; // 本就在费用行前,不动
break;
case RowMoveAction.To:
string input = ShowInputDialog("移动到",
"请输入目标行号(1-" + (dataCount - count + 1) + ")\n(选中 " + count + " 行整块移动,此为块首行号):",
(blockStart + 1).ToString());
if (string.IsNullOrEmpty(input)) { newBlockStart = -1; return; }
int target;
if (!int.TryParse(input.Trim(), out target))
{
MessageBox.Show(this, "请输入有效数字。", "移动到",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
newBlockStart = -1;
return;
}
newBlockStart = Math.Max(0, Math.Min(dataCount - count, target - 1));
// ★ 费用行置底保护:目标块尾不得超过费用行区首行
newBlockStart = Math.Max(0, Math.Min(newBlockStart, 可移边界 - count));
break;
}
if (newBlockStart == blockStart) { newBlockStart = -1; return; } // 位置没变
Log("[移动行] dgv=" + dgv.Name + " 多选" + count + "行 块[" + blockStart + ".." + blockEnd +
"] → 块首" + newBlockStart + " table.Rows.Count=" + table.Rows.Count +
" table.TableName=" + table.TableName);
// ★ 在 DataTable 中整块移动行(ItemArray 技术防 Detached 丢值,与单行版一致)
var savedRows = new System.Collections.Generic.List