Files
ctbjrj/成套报价软件/主窗口.菜单与库.cs
T

9316 lines
500 KiB
C#
Raw 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.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 成套报价软件
{
/// <summary>主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。</summary>
public partial class MainForm
{
// ============================================================
// 右键菜单:创建并挂载
// ============================================================
// ----- 复制/粘贴剪贴板(每个层级独立缓存)-----
// 图号:存图号行 + 其所有箱号表内容 + 各箱号下元件表内容
private class ClipTuhao
{
public string ProjectId;
public string SrcTuhaoName; // 源图号显示名(供参考)
public System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> TuhaoRow;
public System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> XianghaoRows;
// 每个箱号对应的元件表内容(按下标和 XianghaoRows 对应)
public System.Collections.Generic.List<System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>> YuanjianRowsPerXianghao;
}
private System.Collections.Generic.List<ClipTuhao> _clipTuhaos;
// 箱柜:存箱号行 + 其下所有元件行
private class ClipXianghao
{
public string ProjectId;
public string SrcXianghaoName;
public System.Collections.Generic.Dictionary<string, object> XianghaoRow;
public System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> YuanjianRows;
}
private System.Collections.Generic.List<ClipXianghao> _clipXianghaos;
// 元件:存多行(支持多选复制)
private System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> _clipYuanjianRows;
/// <summary>把 DataGridViewRow(绑定到 DataTable)的字段拷贝为字典(排除序号和内部ID)。</summary>
private System.Collections.Generic.Dictionary<string, object> CopyRowFields(DataGridViewRow row, System.Collections.Generic.HashSet<string> excludeCols)
{
var dict = new System.Collections.Generic.Dictionary<string, object>();
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;
}
/// <summary>从 SQLite 读取整个表所有行的字段字典列表(用于复制时备份下级表)。</summary>
private static System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> ReadAllRowsFromTable(string dbPath, string tableName)
{
var list = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();
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<string, object>();
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;
}
/// <summary>GetConnString 的静态包装,供静态方法调用。</summary>
private static string GetConnStringStatic(string dbPath)
{
// ★ B4(2026-10-01):与实例版同款加 BusyTimeout(防并发锁直报错)
return "Data Source=\"" + dbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=5000;";
}
/// <summary>
/// 生成唯一的内部ID:先试 baseName+"(副本)",有重复就 +"(副本1)"、+"(副本2)"…递增。
/// checkAction 返回 true 表示该名称在目标表里已存在。
/// 会先清除原名里旧的 "_副本XXXX"(GUID残留)和旧的 "(副本N)" 后缀,只保留干净原名。
/// </summary>
private static string MakeUniqueName(string baseName, System.Func<string, bool> 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) + ")";
}
}
}
/// <summary>
/// 清除名称里的旧命名残留:
/// "_副本XXXX"(8位十六进制 GUID 残留)、"(副本)"、"(副本N)" 全部去掉。
/// 例:"新图号1_副本966a(副本)" → "新图号1"
/// "箱号A_副本a3b2" → "箱号A"
/// "原名(副本2)" → "原名"
/// </summary>
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();
}
/// <summary>检查图号表里是否已存在某个内部ID。</summary>
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; }
}
/// <summary>检查某图号下箱号表里是否已存在某个内部ID。</summary>
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<string>(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" };
string pid = _currentProjectId;
// UI 线程:先备好每行的字段字典(避免后台线程碰表格对象)
var 待收集 = new System.Collections.Generic.List<System.Tuple<string, System.Collections.Generic.Dictionary<string, object>>>();
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<ClipTuhao> 结果 = null;
Exception 错 = null;
try
{
结果 = new System.Collections.Generic.List<ClipTuhao>();
string dbPath = GetQuoteDbPath(pid);
foreach (var item in 待收集)
{
var clip = new ClipTuhao
{
ProjectId = pid,
SrcTuhaoName = item.Item1,
TuhaoRow = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> { item.Item2 },
XianghaoRows = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>(),
YuanjianRowsPerXianghao = new System.Collections.Generic.List<System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>>()
};
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<string, object>();
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<System.Collections.Generic.Dictionary<string, object>>();
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<string, object>();
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<string>(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<string>(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<string>();
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();
}
/// <summary>集合内唯一化名称(内存判定,替代逐次查库)。</summary>
private static string 唯一化名称(string baseName, System.Collections.Generic.HashSet<string> 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++;
}
}
/// <summary>事务内判断图号内部ID是否已存在。</summary>
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;
}
}
/// <summary>事务内判断箱号内部ID是否已存在(同图号)。</summary>
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;
}
}
/// <summary>事务内解析下一个可用动态表名(prefix_N)。</summary>
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();
}
/// <summary>事务内按真实ID表名建一张空箱号表(含内部ID)。</summary>
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();
}
/// <summary>事务内登记表索引。</summary>
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();
}
}
/// <summary>事务内取元件表下一序号。</summary>
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;
}
}
/// <summary>指定连接+事务内取下一序号(批量粘贴用,避免每条开关连接)。</summary>
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();
}
/// <summary>把缓存的图号字段写入数据库(新内部ID,公式列清零)。★ 带事务重载:批量粘贴共用一个事务。</summary>
private void PasteTuhaoRowToDb(string projectId, string newTuhaoId, System.Collections.Generic.Dictionary<string, object> 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();
}
}
/// <summary>把缓存的图号字段写入数据库(新内部ID,公式列清零)。</summary>
private void PasteTuhaoRowToDb(string projectId, string newTuhaoId, System.Collections.Generic.Dictionary<string, object> 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<string>(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" };
// ★ 复制前先把未保存的编辑落库:元件子树从库读,不保存会复制到旧数据
SaveAllQuoteTablesIfDirty(false);
_clipXianghaos = new System.Collections.Generic.List<ClipXianghao>();
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<System.Collections.Generic.Dictionary<string, object>>()
};
// 读该箱号下所有元件
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);
});
}
});
}
/// <summary>事务内判断箱号内部ID是否已存在(同图号)。</summary>
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;
}
}
/// <summary>把缓存的箱号字段写入数据库(新内部ID,公式列清零)。</summary>
/// <summary>★ 带事务重载:批量粘贴共用一个事务。</summary>
private void PasteXianghaoRowToDb(string projectId, string tuhao, string newXhId, System.Collections.Generic.Dictionary<string, object> 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<string, object> 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<string>(System.StringComparer.OrdinalIgnoreCase) { "序号" };
_clipYuanjianRows = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();
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);
}
}
/// <summary>把缓存的元件字段写入数据库(公式列清零,等重算)。</summary>
/// <summary>
/// ★ 批量粘贴元件到目标箱号:单连接+单事务,一次性插入整批。
/// 旧的单条循环每条要开关 3~4 次连接、查 5 条 SQL,粘 N 条卡 N 倍;
/// 这里一次解析目标表、一次取起始序号、预编译命令循环插、一次提交。
/// </summary>
/// <summary>★ 带事务重载:图号级联粘贴共用外层事务。</summary>
private void PasteYuanjianRowsToDb(string projectId, string tuhao, string xhId,
System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> 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<System.Collections.Generic.Dictionary<string, object>> 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<string, object> 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();
}
}
}
/// <summary>从字段字典读字符串(找不到返回默认值)。</summary>
private static string GetSrcField(System.Collections.Generic.Dictionary<string, object> 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;
}
/// <summary>从字段字典读 decimal(找不到/非数字返回默认值)。</summary>
private static decimal GetSrcFieldDecimal(System.Collections.Generic.Dictionary<string, object> src, string key, decimal defVal)
{
string s = GetSrcField(src, key, "");
decimal d;
if (decimal.TryParse(s, out d)) return d;
return defVal;
}
// ============================================================
// 撤销删除(快照式):删除操作前把整库备份到 undo 缓存,
// 右键「撤销删除」把库还原到删除前状态。只支持撤销最近一次删除。
// ============================================================
/// <summary>撤销缓存目录(exe 下 undo_cache\),null=当前无可撤销操作。</summary>
private string _undoSnapshotDir = null;
/// <summary>撤销快照对应的目标库路径(报价库 或 fak.db)。</summary>
private string _undoTargetDbPath = null;
/// <summary>撤销快照对应的项目编号(用于还原后重载界面)。</summary>
private string _undoProjectId = null;
/// <summary>撤销快照是否来自方案库(fak.db)。</summary>
private bool _undoIsFak = false;
/// <summary>
/// 删除前调用:把目标库整体快照到 undo 缓存(用 SQLite Backup API 避锁)。
/// 同一次会话只保留最近一次快照(新删除覆盖旧快照)。
/// </summary>
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);
}
}
/// <summary>右键菜单「撤销删除」:把库还原到最近一次删除前的快照。</summary>
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;
}
}
/// <summary>根据是否存在可撤销快照,刷新所有右键菜单里「撤销删除」项的可用状态。</summary>
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);
}
/// <summary>在右键菜单里按文本找菜单项并设置 Enabled(找不到忽略)。</summary>
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;
}
}
}
/// <summary>创建「撤销删除」菜单项:初始不可用,有待撤销快照时才亮起。</summary>
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<AdgvMsgHook> _keepMultiHooks;
private void HookKeepMulti(System.Windows.Forms.DataGridView dgv)
{
if (dgv == null) return;
if (_keepMultiHooks == null) _keepMultiHooks = new System.Collections.Generic.List<AdgvMsgHook>();
var hook = new AdgvMsgHook(dgv, msg => { try { Log(msg); } catch { } });
_keepMultiHooks.Add(hook); // 存字段防 GC
try { Log("[右键多选保持] 已挂钩 dgv=" + dgv.Name + " MultiSelect=" + dgv.MultiSelect); } catch { }
}
/// <summary>
/// ADGV 右键多选保持钩子(NativeWindow):吞掉"右键点已选中行"的 WM_RBUTTONDOWN,
/// 多选集合不被清空;右键菜单由 WM_CONTEXTMENU 正常弹出(与扒图窗口同款语义)。
/// </summary>
private class AdgvMsgHook : System.Windows.Forms.NativeWindow
{
private const int WM_RBUTTONDOWN = 0x0204;
private readonly System.Windows.Forms.DataGridView _dgv;
private readonly System.Action<string> _log;
public AdgvMsgHook(System.Windows.Forms.DataGridView dgv, System.Action<string> 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);
}
}
/// <summary>
/// 通用行移动(★ 2026-09-30 多选适配):把 dgv 中选中的【连续或离散】多行整块移动到
/// 指定位置(保持相对顺序),重排序号,保存,恢复多选。单选行同样走块逻辑(块=1行)。
/// dgv 决定操作哪个表(图号/箱号/元件)。
/// </summary>
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<int>();
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<int>(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<object[]>(count);
foreach (int idx in selIndices) savedRows.Add(table.Rows[idx].ItemArray);
// 降序 Remove
for (int i = count - 1; i >= 0; i--) table.Rows.RemoveAt(selIndices[i]);
// 按目标位置依次插回(保持原相对顺序)
for (int i = 0; i < savedRows.Count; i++)
{
DataRow newRow = table.NewRow();
newRow.ItemArray = savedRows[i];
table.Rows.InsertAt(newRow, newBlockStart + i);
}
Log("[移动行] 块移动完成, table.Rows.Count=" + table.Rows.Count);
// 重新编号序号列(1, 2, 3...)
if (table.Columns.Contains("序号"))
{
for (int i = 0; i < table.Rows.Count; i++)
{
table.Rows[i]["序号"] = (i + 1).ToString();
}
}
// ★ A3(2026-09-30):移动改【逐行 UPDATE 序号】——不再整表 DELETE+INSERT 重建。
// 重建会重排全部 rowid(定位漂移/闪错的根源);UPDATE 后库 rowid 永远稳定,
// 内存 _rowid_ 与库保持一致,后续编辑直写/回填定位天然可靠。
string upTbl = (dgv == this.advancedDataGridView9) ? TuhaoTableName : table.TableName;
if (string.IsNullOrWhiteSpace(upTbl))
{
throw new Exception("合并视图下不支持移动行,请先切回单选图号/箱号。");
}
bool canRowUpdate = table.Columns.Contains("_rowid_") && table.Columns.Contains("序号");
long rid0 = canRowUpdate && table.Rows.Count > 0
? (table.Rows[0]["_rowid_"] is long ? Convert.ToInt64(table.Rows[0]["_rowid_"]) : 0) : 0;
if (canRowUpdate && rid0 > 0)
{
using (var mConn = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId))))
{
mConn.Open();
using (var mTx = mConn.BeginTransaction())
{
using (var uCmd = new System.Data.SQLite.SQLiteCommand(
"UPDATE \"" + upTbl.Replace("\"", "") + "\" SET 序号=@s WHERE rowid=@r;", mConn, mTx))
{
uCmd.Parameters.Add("@s", System.Data.DbType.String);
uCmd.Parameters.Add("@r", System.Data.DbType.Int64);
uCmd.Prepare();
for (int i = 0; i < table.Rows.Count; i++)
{
long rid = table.Rows[i]["_rowid_"] is long ? Convert.ToInt64(table.Rows[i]["_rowid_"]) : 0;
if (rid <= 0) continue; // 未落库的新行:无 rowid 可定位,跳过(下次 Reload 自然带出)
uCmd.Parameters["@s"].Value = (i + 1).ToString();
uCmd.Parameters["@r"].Value = rid;
uCmd.ExecuteNonQuery();
}
}
mTx.Commit();
}
}
Log("[移动行] 序号UPDATE完成(rowid稳定,未重建表): 表=" + upTbl + " 行数=" + table.Rows.Count);
}
else
{
// 兜底:无 _rowid_ 列的旧结构 → 退回全表保存
if (dgv == this.advancedDataGridView9)
SaveTableToDatabase(table, _currentProjectId, TuhaoTableName, null, null, true);
else
SaveTableToDatabase(table, _currentProjectId, "\"" + table.TableName + "\"", null, null, true);
Log("[移动行] 全表保存完成(兜底路径), TableName=" + table.TableName);
}
}
catch (Exception ex)
{
Log("[移动行] 异常: " + ex);
MessageBox.Show(this, "移动行失败:\n" + ex.Message, "移动行",
MessageBoxButtons.OK, MessageBoxIcon.Error);
newBlockStart = -1; // 异常时不恢复选中
}
finally
{
_isReloading = false;
// 只在移动成功时恢复选中(块范围选中)
if (newBlockStart >= 0)
{
Log("[移动行] finally: 恢复多选, dgv=" + dgv.Name + " 块首=" + newBlockStart +
" 行数=" + count + " rememberTuhao=[" + rememberTuhao + "]");
// ★ 关键修复(2026-09-30):移动保存是全表 DELETE+INSERT 重建,库 rowid 全部重排,
// 内存表的 _rowid_ 还是旧值——不重载同步的话,后续编辑的直写 UPDATE 会打到错误的库行。
// 显式重载被移动的表(重读 _rowid_);重载会重置选中,其后再做下面的选中恢复。
try
{
if (dgv == this.advancedDataGridView9)
ReloadTuhaoByProject(_currentProjectId);
else if (dgv == this.advancedDataGridView4)
ReloadXianghaoByTuhao(_currentProjectId, rememberTuhao);
else if (dgv == this.advancedDataGridView5)
ReloadYuanjianByXianghao(_currentProjectId, rememberTuhao, rememberXianghao);
}
catch (Exception exRl) { Log("[移动行] 移动后重载异常: " + exRl.Message); }
// ① 图号/箱号表:先用内部ID 选中当前行触发联动重载(保持原逻辑),
// 再把整块行全部选中(恢复多选状态)
if (dgv == this.advancedDataGridView9)
SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao);
else if (dgv == this.advancedDataGridView4)
SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao);
// ② 整块恢复多选(含元件表:元件表无联动,直接块选中)
// ★ CellSelect 模式下用单元格选中(Row.Selected 会抛异常)
try
{
dgv.ClearSelection();
for (int i = newBlockStart; i < newBlockStart + count && i < dgv.Rows.Count; i++)
{
var row = dgv.Rows[i];
foreach (System.Windows.Forms.DataGridViewCell c in row.Cells)
{
if (c != null && c.Visible) c.Selected = true;
}
}
if (newBlockStart < dgv.Rows.Count)
{
// 设 CurrentCell 到块首(触发选中态一致;图号/箱号表联动已由上面完成)
var firstCell = dgv.Rows[newBlockStart].Cells[0];
if (firstCell != null && firstCell.Visible) dgv.CurrentCell = firstCell;
}
}
catch (Exception exSel) { Log("[移动行] 恢复多选异常: " + exSel.Message); }
// ★ 诊断:记录移动后各 DataGridView 的行数和选中行
int dgv9Rows = (this.advancedDataGridView9 != null) ? this.advancedDataGridView9.Rows.Count : -1;
int dgv4Rows = (this.advancedDataGridView4 != null) ? this.advancedDataGridView4.Rows.Count : -1;
int dgv5Rows = (this.advancedDataGridView5 != null) ? this.advancedDataGridView5.Rows.Count : -1;
Log("[移动行] 移动后状态: dgv9行数=" + dgv9Rows + " dgv4行数=" + dgv4Rows +
"] dgv5行数=" + dgv5Rows + " _currentTuhao=[" + _currentTuhao + "] _currentXianghao=[" + _currentXianghao + "]");
}
}
}
// ============================================================
// 通用:把刚插入表末尾的新行移动到选中行的下面
// ============================================================
/// <summary>
/// 获取 DataGridView 当前选中行的索引(无选中或新建行返回-1)。
/// 在 InsertXxx 之前调用,记住选中位置。
/// </summary>
private static int GetSelectedRowIndex(DataGridView dgv)
{
if (dgv == null) return -1;
if (dgv.CurrentRow == null || dgv.CurrentRow.IsNewRow) return -1;
return dgv.CurrentRow.Index;
}
/// <summary>
/// 把 DataTable 末尾的行(刚插入的新行)移动到指定索引的后面。
/// 用于"在选中行下面插入":InsertXxx+ReloadXxx 之后调用。
/// 会重新编号序号列并保存到库。
/// </summary>
/// <param name="dgv">目标 DataGridView(用于滚动到新位置)</param>
/// <param name="table">目标 DataTable</param>
/// <param name="afterIndex">新行要插在这个索引之后(-1=不移动,保持在末尾)</param>
/// <param name="dbTableName">数据库表名(用于保存,如图号表用 TuhaoTableName,箱号/元件表用 "\""+table.TableName+"\"")</param>
/// <param name="projectId">项目ID</param>
/// <summary>
/// ★ 费用行置底保护(2026-10-01 用户要求):返回表尾"连续费用行区"的起始索引
/// (无费用行=Rows.Count)。普通行的插入/移动目标一律不得落进该区。</summary>
private static int 费用区首行(DataTable table)
{
if (table == null || !table.Columns.Contains("类型")) return table == null ? 0 : table.Rows.Count;
int 首 = table.Rows.Count;
for (int i = table.Rows.Count - 1; i >= 0; i--)
{
string tp = Convert.ToString(table.Rows[i]["类型"]) == null ? "" : Convert.ToString(table.Rows[i]["类型"]).Trim();
if (tp == "费用") 首 = i;
else break; // 尾部连续费用区到此为止
}
return 首;
}
/// <summary>费用行置底保护:钳制插入目标索引到费用行区之前(表无类型列原样返回)。</summary>
private static int 钳制到费用行前(DataTable table, int targetIndex)
{
if (table == null || !table.Columns.Contains("类型")) return targetIndex;
int 首 = 费用区首行(table);
return targetIndex > 首 ? 首 : targetIndex;
}
private void MoveLastRowBelowSelected(DataGridView dgv, DataTable table,
int afterIndex, string dbTableName, string projectId)
{
if (dgv == null || table == null || afterIndex < 0 || table.Rows.Count < 2) return;
// ★ 费用行置底修复(2026-10-01):新元件经"库插+重载"后位于【普通区末尾】(费用行之前),
// 不能盲取表末行——有费用行时末行是费用行,搬它会破坏置底。这里取普通区末行作为新行。
int lastIndex = 费用区首行(table) - 1;
if (lastIndex < 0 || lastIndex >= table.Rows.Count) lastIndex = table.Rows.Count - 1; // 无费用行回退表末
// ★ 费用行置底保护:目标不得落进表尾费用行区(普通元件永远插在费用行之前)
int targetIndex = 钳制到费用行前(table, afterIndex + 1);
if (targetIndex >= lastIndex) return; // 新行已经在选中行下面(或选中行就是最后一行)
// 用 ItemArray 技术移动行(避免 DataRow.Remove 清空列值)
object[] savedValues = table.Rows[lastIndex].ItemArray;
table.Rows.RemoveAt(lastIndex);
DataRow newRow = table.NewRow();
newRow.ItemArray = savedValues;
table.Rows.InsertAt(newRow, targetIndex);
// 重新编号序号列(1, 2, 3...)
if (table.Columns.Contains("序号"))
{
for (int i = 0; i < table.Rows.Count; i++)
{
table.Rows[i]["序号"] = (i + 1).ToString();
}
}
// 保存到库
SaveTableToDatabase(table, projectId, dbTableName, null, null, true);
// 滚动到新行位置
try { if (targetIndex < dgv.Rows.Count) dgv.FirstDisplayedScrollingRowIndex = targetIndex; } catch { }
Log("[插入定位] 新行已移动到行" + (targetIndex + 1) + "(选中行" + (afterIndex + 1) + "下面)");
}
/// <summary>把内存表末尾的 count 行整体移动到指定行下面(批量粘贴定位用),重编号并保存。</summary>
private void MoveLastRowsBelowSelected(DataGridView dgv, DataTable table,
int afterIndex, int count, string dbTableName, string projectId)
{
if (dgv == null || table == null || count <= 0 || afterIndex < 0) return;
if (count >= table.Rows.Count) return; // 整表都是新行,无需移动
// ★ 费用行置底修复(2026-10-01):Reload 型场景新块在【普通区末尾】(费用行之前),
// "末尾 count 行"按费用区计算——无类型列的表(图号/箱号)费用区首=行数,行为与原来一致
int lastStart = 费用区首行(table) - count;
if (lastStart < 0) lastStart = table.Rows.Count - count;
// ★ 费用行置底保护:批量新行(普通元件)不得插进表尾费用行区
int targetIndex = 钳制到费用行前(table, afterIndex + 1);
if (targetIndex >= lastStart) return; // 新行已在选中行下面
// 保存末尾 count 行 → 删除 → 整体插到目标位置
var saved = new System.Collections.Generic.List<object[]>(count);
for (int i = lastStart; i < table.Rows.Count; i++)
saved.Add(table.Rows[i].ItemArray);
for (int i = table.Rows.Count - 1; i >= lastStart; i--)
table.Rows.RemoveAt(i);
for (int k = 0; k < saved.Count; k++)
{
var nr = table.NewRow();
nr.ItemArray = saved[k];
table.Rows.InsertAt(nr, targetIndex + k);
}
// 重新编号序号列(1, 2, 3...)
if (table.Columns.Contains("序号"))
{
for (int i = 0; i < table.Rows.Count; i++)
table.Rows[i]["序号"] = (i + 1).ToString();
}
SaveTableToDatabase(table, projectId, dbTableName, null, null, true);
// 滚动到新行位置
try { if (targetIndex < dgv.Rows.Count) dgv.FirstDisplayedScrollingRowIndex = targetIndex; } catch { }
Log("[插入定位] 新行 " + count + " 条已移动到行" + (targetIndex + 1) + "(选中行" + (afterIndex + 1) + "下面)");
}
/// <summary>
/// 把新行插入到 DataTable 的指定位置(选中行下面),而不是末尾。
/// 用于直接操作内存表的插入(如用户库/柜体库/母排插入)。
/// 会自动重新编号序号列。
/// </summary>
/// <param name="table">目标 DataTable</param>
/// <param name="newRow">要插入的新行(已填好数据)</param>
/// <param name="afterIndex">插在这个索引之后(-1=插在末尾)</param>
private void InsertRowBelowSelected(DataTable table, DataRow newRow, int afterIndex)
{
if (table == null || newRow == null) return;
if (afterIndex < 0 || afterIndex >= table.Rows.Count)
{
// 无选中行:插在末尾——费用行置底保护:普通新行插在费用行区之前
table.Rows.InsertAt(newRow, 钳制到费用行前(table, table.Rows.Count));
return;
}
// ★ 费用行置底保护:目标不得落进表尾费用行区
int targetIndex = 钳制到费用行前(table, afterIndex + 1);
table.Rows.InsertAt(newRow, targetIndex);
// 重新编号序号列(1, 2, 3...)
if (table.Columns.Contains("序号"))
{
for (int i = 0; i < table.Rows.Count; i++)
{
table.Rows[i]["序号"] = (i + 1).ToString();
}
}
}
/// <summary>
/// 把 DataTable 末尾的 N 行(刚插入的多条新行)移动到指定索引的后面。
/// 用于"在选中行下面插入多条":方案库批量插入图号/箱号/元件 + Reload 之后调用。
/// 会重新编号序号列并保存到库。
/// </summary>
/// <param name="dgv">目标 DataGridView(用于滚动到新位置)</param>
/// <param name="table">目标 DataTable</param>
/// <param name="afterIndex">新行要插在这个索引之后(-1=不移动,保持在末尾)</param>
/// <param name="newRowCount">末尾要移动的新行数量</param>
/// <param name="dbTableName">数据库表名</param>
/// <param name="projectId">项目ID</param>
private void MoveLastNRowsBelowSelected(DataGridView dgv, DataTable table,
int afterIndex, int newRowCount, string dbTableName, string projectId)
{
if (dgv == null || table == null || afterIndex < 0 || newRowCount <= 0) return;
int totalRows = table.Rows.Count;
// 表里至少要有 选中行 + newRowCount 行才有意义移动
if (totalRows < newRowCount + 1) return;
// ★ 费用行置底修复(2026-10-01):Reload 型新块在【普通区末尾】(费用行之前)——
// 按费用区计算块首;无类型列的表费用区首=行数,与原行为一致
int firstNewRowIndex = 费用区首行(table) - newRowCount; // 新行块起始索引
if (firstNewRowIndex < 0) firstNewRowIndex = totalRows - newRowCount;
// ★ 费用行置底保护:新行块不得插进表尾费用行区
int targetIndex = 钳制到费用行前(table, afterIndex + 1);
// 新行已经在选中行下面(或选中行就在末尾区域),不需要移动
if (targetIndex >= firstNewRowIndex) return;
// 用 ItemArray 技术移动行(避免 DataRow.Remove 清空列值)
var savedRows = new System.Collections.Generic.List<object[]>();
for (int i = firstNewRowIndex; i < totalRows; i++)
{
savedRows.Add(table.Rows[i].ItemArray);
}
// 从末尾开始删除 N 行
for (int i = 0; i < newRowCount; i++)
{
table.Rows.RemoveAt(totalRows - 1 - i);
}
// 在 targetIndex 位置依次插入 N 行
for (int i = 0; i < savedRows.Count; i++)
{
DataRow newRow = table.NewRow();
newRow.ItemArray = savedRows[i];
table.Rows.InsertAt(newRow, targetIndex + i);
}
// 重新编号序号列(1, 2, 3...)
if (table.Columns.Contains("序号"))
{
for (int i = 0; i < table.Rows.Count; i++)
{
table.Rows[i]["序号"] = (i + 1).ToString();
}
}
// 保存到库
SaveTableToDatabase(table, projectId, dbTableName, null, null, true);
// 滚动到新行位置
try { if (targetIndex < dgv.Rows.Count) dgv.FirstDisplayedScrollingRowIndex = targetIndex; } catch { }
Log("[插入定位] " + newRowCount + "行已移动到行" + (targetIndex + 1) + "(选中行" + (afterIndex + 1) + "下面)");
}
/// <summary>
/// 简单输入对话框(不依赖 Microsoft.VisualBasic),返回用户输入或空字符串(取消)。
/// </summary>
private string ShowInputDialog(string title, string prompt, string defaultValue)
{
using (var dlg = new Form())
{
dlg.Text = title;
dlg.StartPosition = FormStartPosition.CenterParent;
dlg.FormBorderStyle = FormBorderStyle.FixedDialog;
dlg.MinimizeBox = false;
dlg.MaximizeBox = false;
dlg.Width = 340;
dlg.Height = 170;
var lbl = new Label();
lbl.Text = prompt;
lbl.Left = 12; lbl.Top = 15;
lbl.Width = 300; lbl.Height = 30;
dlg.Controls.Add(lbl);
var txt = new TextBox();
txt.Text = defaultValue;
txt.Left = 12; txt.Top = 55;
txt.Width = 300;
dlg.Controls.Add(txt);
var btnOk = new Button();
btnOk.Text = "确定";
btnOk.Left = 210; btnOk.Top = 92;
btnOk.Width = 90;
btnOk.DialogResult = DialogResult.OK;
dlg.Controls.Add(btnOk);
var btnCancel = new Button();
btnCancel.Text = "取消";
btnCancel.Left = 108; btnCancel.Top = 92;
btnCancel.Width = 90;
btnCancel.DialogResult = DialogResult.Cancel;
dlg.Controls.Add(btnCancel);
dlg.AcceptButton = btnOk;
dlg.CancelButton = btnCancel;
if (dlg.ShowDialog(this) == DialogResult.OK)
return txt.Text;
return "";
}
}
// ---- 图号表 行移动菜单 ----
private void MenuTuhao_MoveTop_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Top); }
private void MenuTuhao_MoveUp_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Up); }
private void MenuTuhao_MoveDown_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Down); }
private void MenuTuhao_MoveBottom_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Bottom); }
private void MenuTuhao_MoveTo_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.To); }
// ---- 箱号表 行移动菜单 ----
private void MenuXianghao_MoveTop_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Top); }
private void MenuXianghao_MoveUp_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Up); }
private void MenuXianghao_MoveDown_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Down); }
private void MenuXianghao_MoveBottom_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Bottom); }
private void MenuXianghao_MoveTo_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.To); }
// ---- 元件表 行移动菜单 ----
private void MenuYuanjian_MoveTop_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Top); }
private void MenuYuanjian_MoveUp_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Up); }
private void MenuYuanjian_MoveDown_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Down); }
private void MenuYuanjian_MoveBottom_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Bottom); }
private void MenuYuanjian_MoveTo_Click(object sender, EventArgs e) { MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.To); }
/// <summary>
/// 右键菜单弹出前:强制结束当前正在编辑的单元格,并立即保存到数据库。
/// 这样新建/删除操作执行时,用户之前的编辑(如刚填的表价)已经持久化,
/// 不会因为编辑态未提交而丢失。
/// </summary>
// 右键菜单弹出前不再做任何操作(不再取消编辑/保存/动选中),
// 完全用系统默认的右键行为,选中单元格由用户自己控制。
/// <summary>
/// 把表格的选中/光标设到指定行指定列(完整三步:清空选中+选中该行+设光标到该单元格)。
/// 三步都执行才能保证 SelectedRows 和 CurrentCell 状态一致,避免横跳。
/// 调用方应已持有 _isReloading guard。
/// </summary>
private static void SetCurrentCell(DataGridView dgv, int rowIndex, int colIndex = 0)
{
if (dgv == null || dgv.Rows == null || rowIndex < 0 ||
rowIndex >= dgv.Rows.Count) return;
try
{
if (dgv.Rows[rowIndex].IsNewRow) return;
if (colIndex < 0 || colIndex >= dgv.Columns.Count) colIndex = 0;
dgv.ClearSelection();
dgv.Rows[rowIndex].Selected = true;
dgv.CurrentCell = dgv.Rows[rowIndex].Cells[colIndex];
}
catch { }
}
/// <summary>
/// 把表格的选中/光标设到指定行,用列名定位(完整三步)。
/// 调用方应已持有 _isReloading guard。
/// </summary>
private static void SetCurrentCellByCol(DataGridView dgv, int rowIndex, string colName)
{
if (dgv == null) { SetCurrentCell(dgv, rowIndex); return; }
DataGridViewColumn col = FindColumn(dgv, colName);
SetCurrentCell(dgv, rowIndex, col == null ? 0 : col.Index);
}
/// <summary>按列值选中对应行(防止右键导致选中跳回第一行)。不触发联动。</summary>
private void SelectRowByValue(DataGridView dgv, string colName, string value)
{
if (dgv == null || dgv.Rows == null || string.IsNullOrEmpty(value)) return;
try
{
_isReloading = true;
foreach (DataGridViewRow r in dgv.Rows)
{
if (r == null || r.IsNewRow) continue;
// ★ 先从表格列读,读不到再从底层数据源读(支持"内部ID"等隐藏列)
string v = GetCellValueAsString(r, colName);
if (string.IsNullOrEmpty(v)) v = GetBoundValueAsString(r, colName);
if (v == value)
{
dgv.ClearSelection();
r.Selected = true;
dgv.CurrentCell = r.Cells[0];
return;
}
}
}
catch { }
finally
{
_isReloading = false;
}
}
// ---------- 图号菜单事件 ----------
private void MenuTuhao_New_Click(object sender, EventArgs e)
{
Log("点击[新建图号], _currentProjectId=[" + _currentProjectId + "]");
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在项目表中双击选择一个项目,再新建图号。",
"新建图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 不弹输入框,直接用默认名(新图号N)创建
string newTuhao = "新图号" + GetNextSeqNumber(TuhaoTableName, "图号", _currentProjectId);
Log(" 直接创建图号=[" + newTuhao + "]");
// 记住当前图号,新建完成后恢复(防止 ADGV 选中抖动)
string rememberTuhao = _currentTuhao;
// ★ 记住选中行索引,用于"在选中行下面插入"
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView9);
try
{
// 整个新建流程用 _isReloading 包裹,挡住所有 SelectionChanged 联动
_isReloading = true;
// 新建前先保存当前三个表的所有编辑,避免用户编辑丢失
// 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。
// 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。
SaveAllQuoteTablesIfDirty(false);
InsertTuhao(_currentProjectId, newTuhao);
Log(" InsertTuhao 成功");
// ★ 清空箱号/元件缓存(图号表变了,所有下级缓存可能失效)
_xianghaoCache.Clear();
_yuanjianCache.Clear();
ReloadTuhaoByProject(_currentProjectId);
// ★ 把新行(在表末尾)移动到选中行下面
MoveLastRowBelowSelected(this.advancedDataGridView9, _tuhaoTable, selectedIndex, TuhaoTableName, _currentProjectId);
// 新建后重算当前层并刷新显示
RecalcAllAfterReload();
}
catch (Exception ex)
{
Log(" 新建图号异常: " + ex.Message);
MessageBox.Show(this, "新建图号失败:\n" + ex.Message,
"新建图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// 恢复图号选中(新建的图号会在表末尾,优先选中新图号)
_currentTuhao = newTuhao;
SelectRowByValue(this.advancedDataGridView9, "内部ID", newTuhao);
_isReloading = false;
// ★ 新建图号后,箱号表必须跳到这个新图号(显示它里面的箱号,新建的图号默认空)
// ReloadTuhaoByProject 默认选第一个图号,这里手动刷新成新图号。
ReloadXianghaoByTuhao(_currentProjectId, newTuhao);
}
}
private void MenuTuhao_Delete_Click(object sender, EventArgs e)
{
Log("点击[删除图号], _currentProjectId=[" + _currentProjectId + "]");
// 取所有选中行(支持批量多选)
var rows = GetAllSelectedRows(this.advancedDataGridView9);
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先选择要删除的图号。",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 收集所有选中行的图号内部ID(去重) ★ 用内部ID,用户改名不影响删除
var tuhaos = new System.Collections.Generic.List<string>();
// ★ 同时记显示名:扒图 Form3 曾按显示名登记 表索引/建表,删除时需要按
// 显示名兜底清理孤儿(2026-09-21 残留根治),否则 AI 清单会读到已删数据
var tuhaoNames = new System.Collections.Generic.List<string>();
foreach (var r in rows)
{
string th = GetBoundValueAsString(r, "内部ID");
string nm = GetBoundValueAsString(r, "图号");
if (!string.IsNullOrWhiteSpace(th) && !tuhaos.Contains(th))
{
tuhaos.Add(th);
tuhaoNames.Add(nm ?? "");
}
}
if (tuhaos.Count == 0)
{
MessageBox.Show(this, "选中行没有图号,无法删除。",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 不弹确认框,直接删除
try
{
// 整个删除流程用 _isReloading 包裹,挡住选中抖动
_isReloading = true;
// 删除前先保存当前三个表的所有编辑
// 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。
// 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。
SaveAllQuoteTablesIfDirty(false);
// ★ 删除前快照整库,支持右键「撤销删除」还原
PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false);
// ★ 删除管线放后台线程:单连接+单事务(所有 DROP/DELETE 一次提交,不再逐条 fsync)
string pid = _currentProjectId;
var sw = System.Diagnostics.Stopwatch.StartNew();
Cursor = Cursors.WaitCursor;
SetStatus("正在删除 " + tuhaos.Count + " 个图号...");
Log("[删除图号] 开始,共 " + tuhaos.Count + " 个图号...");
System.Threading.Tasks.Task.Run(() =>
{
Exception 错 = null;
try
{
string dbPath = GetQuoteDbPath(pid);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
for (int ti = 0; ti < tuhaos.Count; ti++)
{
string tuhao = tuhaos[ti];
string tuhaoNm = ti < tuhaoNames.Count ? tuhaoNames[ti] : "";
if (tuhao == (_currentTuhao ?? "")) { /* 当前图号也一并删除 */ }
DeleteTuhaoTx(pid, tuhao, tuhaoNm, conn, tx);
}
tx.Commit();
}
}
}
catch (Exception exBg) { 错 = exBg; }
BeginInvoke((MethodInvoker)delegate
{
try
{
_isReloading = true;
if (错 == null)
{
RenumberTable(pid, TuhaoTableName);
_xianghaoCache.Clear();
_yuanjianCache.Clear();
}
ReloadTuhaoByProject(pid);
RecalcAllAfterReload();
sw.Stop();
Cursor = Cursors.Default;
if (错 != null)
{
Log("[删除图号] 后台异常,完整堆栈: " + 错.ToString());
MessageBox.Show(this, "删除图号失败:\n" + 错.Message,
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SetStatus("删除完成 " + tuhaos.Count + " 个图号, 耗时 " + sw.ElapsedMilliseconds + "ms");
Log("[删除图号] 完成,数量=" + tuhaos.Count + ",耗时 " + sw.ElapsedMilliseconds + "ms");
}
finally
{
_isReloading = false;
}
});
});
}
catch (Exception ex)
{
Cursor = Cursors.Default;
_isReloading = false;
MessageBox.Show(this, "删除图号失败:\n" + ex.Message,
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ============================================================
// 加入方案库:把当前选中图号(含其下所有箱号 + 每个箱号下所有元件)
// 从项目报价库复制到方案库 sjk\fak.db
// ============================================================
/// <summary>方案库固定表名:图号表(不用索引,直接按图号查找)。</summary>
private const string FakTuhaoTable = "advancedDataGridView9Table";
/// <summary>
/// 右键「加入方案库」:把当前图号(_currentTuhao)对应的全部数据
/// (图号行 + 该图号所有箱号行 + 每个箱号的所有元件行)
/// 从项目报价库复制到方案库 fak.db。
/// 方案库数据结构与项目库完全一致:图号表固定 + xh_N/yj_M 动态建表 + 表索引映射。
/// 若 fak.db 里已存在相同图号,先删除其旧的 xh_N/yj_M 表和索引行,再重建,保证可重复加入。
/// </summary>
private void MenuTuhao_AddToFak_Click(object sender, EventArgs e)
{
Log("点击[加入方案库], _currentProjectId=[" + _currentProjectId +
"] _currentTuhao=[" + _currentTuhao + "]");
// 1) 校验当前选中状态
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在项目表中双击选择一个项目,再加入方案库。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 多选:收集全部选中图号(内部ID,去重,保持选择顺序)
var rows = GetAllSelectedRows(this.advancedDataGridView9);
var 图号们 = new System.Collections.Generic.List<string>();
var seenId = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string id = GetBoundValueAsString(r, "内部ID");
if (!string.IsNullOrEmpty(id) && seenId.Add(id)) 图号们.Add(id);
}
if (图号们.Count == 0)
{
MessageBox.Show(this, "请先在图号表中选择要加入方案库的图号(可按 Ctrl 多选)。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 2) 先保存当前三个表的未提交编辑,确保复制到方案库的是最新数据
try { SaveAllQuoteTablesIfDirty(false); }
catch (Exception exSave) { Log("[加入方案库] 预保存失败(继续): " + exSave.Message); }
string srcDbPath = GetQuoteDbPath(_currentProjectId);
if (!System.IO.File.Exists(srcDbPath))
{
MessageBox.Show(this, "项目报价库不存在:\n" + srcDbPath,
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string fakDbPath = 方案数据源路径();
// ★ 方案窗口没打开方案时给出明确引导(旧逻辑空路径直接在 CreateDirectory 炸)
if (string.IsNullOrWhiteSpace(fakDbPath) || !System.IO.File.Exists(fakDbPath))
{
MessageBox.Show(this,
"方案窗口当前没有打开方案,无法加入。\n\n请先在右侧方案窗口的项目页双击打开一个方案\n(或用项目表右键\"加入方案库\"把项目存为新方案),再加入。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 改用复制粘贴管线写方案库:收集选中图号的完整子树(clip 结构同复制图号),
// 事务写入方案文件。同步执行(方案写入数据量通常不大)。
var clips = new System.Collections.Generic.List<ClipTuhao>();
var exclude = new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase) { "序号", "内部ID" };
foreach (var id in 图号们)
{
var clip = new ClipTuhao
{
ProjectId = _currentProjectId,
SrcTuhaoName = id,
TuhaoRow = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>(),
XianghaoRows = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>(),
YuanjianRowsPerXianghao = new System.Collections.Generic.List<System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>>()
};
// 图号行(从内存表取,含最新编辑)
DataGridViewRow thRowFound = null;
foreach (var r in GetAllSelectedRows(this.advancedDataGridView9))
if (GetBoundValueAsString(r, "内部ID") == id) { thRowFound = r; break; }
if (thRowFound != null)
foreach (DataGridViewColumn c in thRowFound.DataGridView.Columns)
if (!exclude.Contains(c.Name))
clip.TuhaoRow.Add(new System.Collections.Generic.Dictionary<string, object>
{ { c.Name, thRowFound.Cells[c.Name].Value ?? "" } });
// 箱号行+元件行(从项目库读,预保存已落最新)
string xhTbl = GetXianghaoTableByIndex(_currentProjectId, id);
if (!string.IsNullOrEmpty(xhTbl))
{
clip.XianghaoRows = ReadAllRowsFromTable(srcDbPath, xhTbl);
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;
string yjTbl = GetYuanjianTableByIndex(_currentProjectId, id, xhId);
var yjRows = string.IsNullOrEmpty(yjTbl)
? new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>()
: ReadAllRowsFromTable(srcDbPath, yjTbl);
clip.YuanjianRowsPerXianghao.Add(yjRows);
}
}
clips.Add(clip);
}
int 收集箱号数 = 0;
foreach (var c in clips) 收集箱号数 += c.XianghaoRows.Count;
Log("[加入方案库] 收集完成: 图号=" + clips.Count + " 箱号=" + 收集箱号数 + " 目标=" + fakDbPath);
Cursor = Cursors.WaitCursor;
try
{
EnsureFakDbExists(fakDbPath);
using (var conn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath)))
{
conn.Open();
// ★ 方案文件由整库复制而来,可能带旧架构遗留触发器(引用已退役的固定表"箱号")——写入前必清
计算引擎.清理遗留触发器(conn);
using (var tx = conn.BeginTransaction())
{
int xhMax = 0, yjMax = 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;
}
}
}
var 现有图号 = new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 图号 FROM " + FakTuhaoTable + ";", conn, tx))
using (var r = cmd.ExecuteReader())
while (r.Read())
if (!r.IsDBNull(0) && r.GetString(0).Length > 0) 现有图号.Add(r.GetString(0));
foreach (var clip in clips)
{
string baseName = !string.IsNullOrWhiteSpace(clip.SrcTuhaoName) ? clip.SrcTuhaoName : "图号";
string nt = 唯一化名称(baseName, 现有图号);
现有图号.Add(nt);
// 清理同名图号的残留索引和动态表(防 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())
{
var olds = new System.Collections.Generic.List<string>();
while (r.Read())
{
string t = r.IsDBNull(0) ? "" : r.GetString(0);
if (!string.IsNullOrWhiteSpace(t)) olds.Add(t);
}
foreach (var ot in olds)
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 图号=@th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", nt);
cmd.ExecuteNonQuery();
}
// 图号行:副本=源(聚合值抄源行),写入方案库图号表
var srcTh = clip.TuhaoRow.Count > 0 ? clip.TuhaoRow[0] : null;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO " + FakTuhaoTable +
" (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " +
"VALUES (@s, @th, @ts, @sl, @dj, @zj, @cbzj, @ml, @mll, @iid);", conn, tx))
{
int 序 = 现有图号.Count;
cmd.Parameters.AddWithValue("@s", 序.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();
}
// 箱号表(每图号一张)+全部箱号行;触发器不建(方案库不需要,且遗留触发器是祸根)
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();
}
int xhSeq = 0;
var 已用箱号名 = new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
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);
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();
}
}
}
}
}
tx.Commit();
}
}
Log("[加入方案库] 成功,共 " + clips.Count + " 个图号(含箱号 " + 收集箱号数 + " 个)");
ReloadFakData();
MessageBox.Show(this, "已把 " + clips.Count + " 个图号加入方案库。", "加入方案库",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception exAdd)
{
Log("[加入方案库] 失败: " + exAdd);
MessageBox.Show(this, "加入方案库失败:\n" + exAdd.Message, "加入方案库",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>项目表右键「加入方案库」:把整个项目(全部图号+箱号+元件+项目信息)复制到方案库。</summary>
private void MenuProject_AddToFak_Click(object sender, EventArgs e)
{
var row = GetFirstSelectedRow(this.advancedDataGridView6);
if (row == null)
{
MessageBox.Show(this, "请先在项目列表中选择要存为方案的项目。",
"存为方案", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string projectId = GetBoundValueAsString(row, "项目编号");
string projName = GetBoundValueAsString(row, "项目名称");
if (string.IsNullOrWhiteSpace(projectId))
{
MessageBox.Show(this, "选中行没有项目编号,无法存为方案。",
"存为方案", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string srcDbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(srcDbPath))
{
MessageBox.Show(this, "项目报价库不存在:\n" + srcDbPath,
"存为方案", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 方案名默认 = 项目名称(空则项目编号);重名自动加序号
string 方案名 = !string.IsNullOrWhiteSpace(projName) ? projName : projectId;
string dstDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sjk", "方案库");
System.IO.Directory.CreateDirectory(dstDir);
string dstPath = System.IO.Path.Combine(dstDir, 方案名 + ".db");
int seq = 1;
while (System.IO.File.Exists(dstPath))
{
seq++;
dstPath = System.IO.Path.Combine(dstDir, 方案名 + "(" + seq + ").db");
}
var sw = System.Diagnostics.Stopwatch.StartNew();
Cursor = Cursors.WaitCursor;
try
{
// ★ 一个方案 = 一份独立项目库副本(含全部图号/箱号/元件/项目信息)
BackupSQLiteDb(srcDbPath, dstPath);
sw.Stop();
Log("[存为方案] 项目=" + projectId + " → " + dstPath + " 耗时 " + sw.ElapsedMilliseconds + "ms");
// ★ 存完立即切换打开新方案(方案窗口报价页直接显示新方案内容)
if (this.w8 != null) this.w8.打开方案文件(dstPath);
else ReloadFakData();
MessageBox.Show(this, "已把项目 [" + projectId + "] 存为方案。\n方案文件: " + dstPath +
"\n耗时 " + sw.ElapsedMilliseconds + "ms。", "存为方案",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception exSave)
{
MessageBox.Show(this, "存为方案失败:\n" + exSave.Message, "存为方案",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
Cursor = Cursors.Default;
}
}
/// <summary>Exec 简写(删除表数据用)。</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();
}
/// <summary>复制一个图号(含箱号/元件子树)从项目库到方案库。供单图号加入与整项目加入共用。</summary>
private void 复制图号到方案库(string projectId, string 图号内部ID, string srcDbPath, string fakDbPath)
{
try
{
int xhCount = 0; // 复制的箱号行数
int yjCount = 0; // 复制的元件行数
// ---- 阶段一:从项目报价库读出当前图号的全部数据 ----
// 图号行(只取第一条,一个图号在图号表里通常只有一行)
DataTable tuhaoRow = new DataTable();
// 该图号的箱号表真实表名 + 全部箱号行
string xhRealTbl = "";
DataTable xhRows = new DataTable();
// 每个箱号:元件表真实表名 + 该箱号的箱号名称 + 元件行
var yjBuckets = new System.Collections.Generic.List<
System.Collections.Generic.KeyValuePair<string, DataTable>>(); // 真实表名 → 元件行
var xhNamesInOrder = new System.Collections.Generic.List<string>(); // 箱号顺序(用于元件行关联箱号名)
using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath)))
{
srcConn.Open();
// (a) 图号行
// ★ 用"内部ID"列查(不是"图号"显示名列),用户改图号名不影响查找
string selTh = "SELECT * FROM " + TuhaoTableName + " WHERE 内部ID = @th LIMIT 1;";
using (var cmdTh = new System.Data.SQLite.SQLiteCommand(selTh, srcConn))
{
cmdTh.Parameters.AddWithValue("@th", 图号内部ID);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdTh))
{
ad.Fill(tuhaoRow);
}
}
if (tuhaoRow.Rows.Count == 0)
{
MessageBox.Show(this, "图号 [" + 图号内部ID + "] 在项目报价库里没有数据,无法加入方案库。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// (b) 该图号的箱号表真实表名 + 全部箱号行
xhRealTbl = GetXianghaoTableByIndex(图号内部ID, srcConn);
if (!string.IsNullOrEmpty(xhRealTbl))
{
string selXh = "SELECT * FROM \"" + xhRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var cmdXh = new System.Data.SQLite.SQLiteCommand(selXh, srcConn))
{
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdXh))
{
ad.Fill(xhRows);
}
}
}
// (c) 遍历每个箱号,读其元件表
foreach (DataRow xhRow in xhRows.Rows)
{
// ★ 用"内部ID"列查元件表索引(不是"箱号"显示名列),用户改箱号名不影响
string xhName = FakRead内部ID(xhRow);
xhNamesInOrder.Add(xhName);
string yjRealTbl = GetYuanjianTableByIndex(图号内部ID, xhName, srcConn);
if (string.IsNullOrEmpty(yjRealTbl)) { yjBuckets.Add(new System.Collections.Generic.KeyValuePair<string, DataTable>("", null)); continue; }
DataTable yjRows = new DataTable();
string selYj = "SELECT * FROM \"" + yjRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn))
{
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj))
{
ad.Fill(yjRows);
}
}
yjBuckets.Add(new System.Collections.Generic.KeyValuePair<string, DataTable>(yjRealTbl, yjRows));
}
}
xhCount = xhRows.Rows.Count;
foreach (var kv in yjBuckets)
{
if (kv.Value != null) yjCount += kv.Value.Rows.Count;
}
Log("[加入方案库] 读取完成: 图号行=1 箱号行=" + xhCount + " 元件行=" + yjCount +
" 目标库=" + fakDbPath);
// ---- 阶段二:确保 fak.db 存在(图号表 + 表索引 + 项目信息三张固定表)----
EnsureFakDbExists(fakDbPath);
// ---- 阶段三:写入 fak.db ----
// 与项目库结构一致:图号表直接写;箱号/元件按 xh_N/yj_M 动态建表,通过表索引映射。
using (var dstConn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath)))
{
dstConn.Open();
using (var tx = dstConn.BeginTransaction())
{
try
{
// (1) 图号表:DELETE 旧 + INSERT 新(固定表名,直接按图号查找)
string delTh = "DELETE FROM " + FakTuhaoTable + " WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delTh, dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", 图号内部ID);
cmd.ExecuteNonQuery();
}
if (tuhaoRow.Rows.Count > 0)
{
// 图号表的固定列(与项目库完全一致,无额外列)
// fak.db 的图号表没有"内部ID"列,不带它
string[] thCols = { "序号", "图号", "图名", "台数", "箱柜数量",
"单价", "总价", "成本总价", "毛利", "毛利率" };
InsertRowByColumns(tuhaoRow.Rows[0], FakTuhaoTable, thCols, dstConn, tx);
}
// (2) 删除该图号在 fak.db 里的旧数据:
// 先从表索引查出该图号所有旧的 xh_N/yj_M 真实表名,逐个 DROP,
// 再删除该图号在表索引里的所有记录。
var oldTables = new System.Collections.Generic.List<string>();
string selOldIdx = "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(selOldIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", 图号内部ID);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
string t = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrEmpty(t)) oldTables.Add(t);
}
}
}
foreach (string oldTbl in oldTables)
{
string dropSql = "DROP TABLE IF EXISTS \"" + oldTbl + "\";";
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropSql, dstConn, tx))
{
dropCmd.ExecuteNonQuery();
}
}
string delOldIdx = "DELETE FROM 表索引 WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delOldIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", 图号内部ID);
cmd.ExecuteNonQuery();
}
// (3) 计算新的表名序号起点:查表索引里 xh_/yj_ 的最大序号 + 1
int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh");
int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj");
// (4) 为该图号创建一张新的箱号表 xh_N,插入所有箱号行,登记表索引
string xhTblName = "xh_" + (xhSeq + 1).ToString();
CreateFakXianghaoTable(xhTblName, dstConn, tx);
foreach (DataRow r in xhRows.Rows)
{
InsertFakXianghaoRow(r, xhTblName, dstConn, tx);
}
// 表索引:(类型='箱号', 图号=@th, 箱号='', 实际表名='xh_N')
string insXhIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " +
"VALUES (@t, @th, @xh, @real);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(insXhIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@t", "箱号");
cmd.Parameters.AddWithValue("@th", 图号内部ID);
cmd.Parameters.AddWithValue("@xh", "");
cmd.Parameters.AddWithValue("@real", xhTblName);
cmd.ExecuteNonQuery();
}
Log("[加入方案库] 建箱号表 " + xhTblName + ",插入 " + xhRows.Rows.Count + " 行");
// (5) 对每个箱号:创建元件表 yj_M,插入元件行,登记表索引
for (int i = 0; i < yjBuckets.Count; i++)
{
var kv = yjBuckets[i];
if (kv.Value == null) continue;
string xhName = (i < xhNamesInOrder.Count) ? xhNamesInOrder[i] : "";
yjSeq++;
string yjTblName = "yj_" + yjSeq.ToString();
CreateFakYuanjianTable(yjTblName, dstConn, tx);
foreach (DataRow r in kv.Value.Rows)
{
InsertFakYuanjianRow(r, yjTblName, dstConn, tx);
}
// 表索引:(类型='元件', 图号=@th, 箱号=箱号名, 实际表名='yj_M')
string insYjIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " +
"VALUES (@t, @th, @xh, @real);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(insYjIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@t", "元件");
cmd.Parameters.AddWithValue("@th", 图号内部ID);
cmd.Parameters.AddWithValue("@xh", xhName);
cmd.Parameters.AddWithValue("@real", yjTblName);
cmd.ExecuteNonQuery();
}
Log("[加入方案库] 建元件表 " + yjTblName + "(箱号=" + xhName +
"),插入 " + kv.Value.Rows.Count + " 行");
}
tx.Commit();
}
catch
{
try { tx.Rollback(); } catch { }
throw;
}
}
}
}
catch (Exception ex)
{
Log("[加入方案库] 失败: " + ex.Message);
MessageBox.Show(this, "加入方案库失败:\n" + ex.Message,
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 旧方案刷新入口(fak.db 平铺方式已退役)。
/// 现架构:方案 = sjk\方案库\ 下的独立 .db 文件,方案窗口(DockWindow8)自治加载与刷新。
/// </summary>
public void ReloadFakData()
{
try
{
if (this.w8 != null) this.w8.重新加载当前方案();
Log("[方案刷新] 已通知方案窗口重载当前方案");
}
catch (Exception ex)
{
Log("[方案刷新] 失败: " + ex.Message);
}
}
/// <summary>方案窗口当前打开的方案文件路径(未打开返回 null)。</summary>
private string 方案数据源路径()
{
return (this.w8 != null) ? this.w8.当前方案文件 : null;
}
/// <summary>写方案库文件的连接串:与方案窗口(DockWindow8.连接串)同款,带 BusyTimeout=3000——
/// 主窗口 GetConnString 无 BusyTimeout(默认0),方案窗口正持 WAL 连接时写方遇锁会立刻炸 database is locked。</summary>
private string 方案库写连接串(string fakDbPath)
{
return "Data Source=\"" + fakDbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=3000;";
}
/// <summary>读方案库"项目信息"表第一行为字典(无表/无行返回空字典)。</summary>
private System.Collections.Generic.Dictionary<string, string> LoadFak项目信息(string fakDbPath)
{
var d = new System.Collections.Generic.Dictionary<string, string>();
try
{
if (!System.IO.File.Exists(fakDbPath)) return d;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM 项目信息 LIMIT 1;", conn))
using (var r = cmd.ExecuteReader())
{
if (r.Read())
for (int i = 0; i < r.FieldCount; i++)
d[r.GetName(i)] = r.IsDBNull(i) ? "" : r.GetValue(i).ToString();
}
}
}
catch { }
return d;
}
// ===== 方案页面(tabPage13)三层联动:图号 → 箱号 → 元件 =====
private bool _fakReloading = false;
/// <summary>方案页面图号表选中变化 → 联动刷新箱号表</summary>
private void FakTuhao_SelectionChanged(object sender, EventArgs e)
{
if (_fakReloading) return;
DataGridViewRow row = GetFirstSelectedRow(this.advancedDataGridView图号);
if (row == null) return;
string tuhao = GetCellValueAsString(row, "图号");
if (string.IsNullOrEmpty(tuhao)) return;
ReloadFakXianghao(tuhao);
}
/// <summary>方案页面箱号表选中变化 → 联动刷新元件表</summary>
private void FakXianghao_SelectionChanged(object sender, EventArgs e)
{
if (_fakReloading) return;
DataGridViewRow rowTuhao = GetFirstSelectedRow(this.advancedDataGridView图号);
if (rowTuhao == null) return;
string tuhao = GetCellValueAsString(rowTuhao, "图号");
DataGridViewRow rowXianghao = GetFirstSelectedRow(this.advancedDataGridView箱号);
if (rowXianghao == null) return;
string xianghao = GetCellValueAsString(rowXianghao, "箱号");
if (string.IsNullOrEmpty(xianghao)) return;
ReloadFakYuanjian(tuhao, xianghao);
}
/// <summary>从 fak.db 加载指定图号的箱号数据</summary>
private void ReloadFakXianghao(string tuhao)
{
_fakReloading = true;
try
{
string fakDbPath = 方案数据源路径();
if (!System.IO.File.Exists(fakDbPath)) return;
// 从表索引查出箱号真实表名
string xhRealTbl = "";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
xhRealTbl = GetXianghaoTableByIndex(tuhao, conn);
if (string.IsNullOrEmpty(xhRealTbl))
{
this.advancedDataGridView箱号.DataSource = null;
this.advancedDataGridView元件.DataSource = null;
return;
}
var table = new DataTable();
// 老fak库结构兼容:箱号表可能没有"箱名"列,读取前幂等补列(表格绑定依赖该列)
EnsureColumns(conn, "\"" + xhRealTbl + "\"", new[] { "箱名" });
string sql = "SELECT * FROM \"" + xhRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd))
{
ad.Fill(table);
}
// 关闭自动生成列,保留 .ui.cs 里定义的列宽(序号=35px 等)
this.advancedDataGridView箱号.AutoGenerateColumns = false;
this.advancedDataGridView箱号.DataSource = table;
this.advancedDataGridView元件.DataSource = null;
}
}
catch (Exception ex) { Log("[方案刷新] 箱号加载失败: " + ex.Message); }
finally { _fakReloading = false; }
}
/// <summary>从 fak.db 加载指定图号+箱号的元件数据</summary>
private void ReloadFakYuanjian(string tuhao, string xianghao)
{
_fakReloading = true;
try
{
string fakDbPath = 方案数据源路径();
if (!System.IO.File.Exists(fakDbPath)) return;
string yjRealTbl = "";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
yjRealTbl = GetYuanjianTableByIndex(tuhao, xianghao, conn);
if (string.IsNullOrEmpty(yjRealTbl))
{
this.advancedDataGridView元件.DataSource = null;
return;
}
var table = new DataTable();
string sql = "SELECT * FROM \"" + yjRealTbl + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd))
{
ad.Fill(table);
}
// 关闭自动生成列,保留 .ui.cs 里定义的列宽(序号=35px 等)
this.advancedDataGridView元件.AutoGenerateColumns = false;
this.advancedDataGridView元件.DataSource = table;
}
}
catch (Exception ex) { Log("[方案刷新] 元件加载失败: " + ex.Message); }
finally { _fakReloading = false; }
}
// ============================================================
// 方案页面 右键菜单事件(图号/箱号/元件 的 删除/插入)
// - 删除:从 fak.db 删除选中数据,刷新方案页面
// - 插入:把 fak.db 选中数据复制到当前项目报价库(GetQuoteDbPath(_currentProjectId))
// ============================================================
// ---------- 图号菜单:删除 / 插入 ----------
/// <summary>
/// 方案页面 图号右键菜单「删除图号」:
/// 遍历 advancedDataGridView图号 选中的行,对每个图号:
/// 1) 从 fak.db 的表索引查出该图号关联的 xh_N/yj_M 真实表名,逐个 DROP
/// 2) 从 fak.db 图号表 DELETE WHERE 图号=@th
/// 3) 从 fak.db 表索引 DELETE WHERE 图号=@th
/// 4) 刷新 ReloadFakData()
/// </summary>
private void FakTuhao_Delete_Click(object sender, EventArgs e)
{
var rows = GetAllSelectedRows(this.advancedDataGridView图号);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先在图号表中选择要删除的图号。",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 收集图号名(去重)
var tuhaos = new System.Collections.Generic.List<string>();
var seen = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string th = GetCellValueAsString(r, "图号");
if (!string.IsNullOrEmpty(th) && seen.Add(th)) tuhaos.Add(th);
}
if (tuhaos.Count == 0)
{
MessageBox.Show(this, "未能从选中行读取图号。",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string msg = "确认从方案库删除以下 " + tuhaos.Count + " 个图号?\n" +
string.Join(", ", tuhaos) +
"\n\n该图号下的所有箱号、元件数据也会一并删除。";
if (MessageBox.Show(this, msg, "删除图号",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
string fakDbPath = 方案数据源路径();
if (!System.IO.File.Exists(fakDbPath))
{
MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
// ★ 删除前快照方案库,支持右键「撤销删除」还原
PushUndoSnapshot(fakDbPath, "", true);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
try
{
foreach (string th in tuhaos)
{
// 1) 查表索引拿该图号所有 xh_N/yj_M 真实表名,逐个 DROP
var oldTables = new System.Collections.Generic.List<string>();
string selOldIdx = "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(selOldIdx, conn, tx))
{
cmd.Parameters.AddWithValue("@th", th);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
string t = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrEmpty(t)) oldTables.Add(t);
}
}
}
foreach (string oldTbl in oldTables)
{
string dropSql = "DROP TABLE IF EXISTS \"" + oldTbl + "\";";
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropSql, conn, tx))
{
dropCmd.ExecuteNonQuery();
}
}
// 2) 从图号表 DELETE
string delTh = "DELETE FROM " + FakTuhaoTable + " WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delTh, conn, tx))
{
cmd.Parameters.AddWithValue("@th", th);
cmd.ExecuteNonQuery();
}
// 3) 从表索引 DELETE
string delIdx = "DELETE FROM 表索引 WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delIdx, conn, tx))
{
cmd.Parameters.AddWithValue("@th", th);
cmd.ExecuteNonQuery();
}
Log("[方案删除图号] 已删除图号=" + th + ", 关联表 " + oldTables.Count + " 张");
}
tx.Commit();
}
catch
{
try { tx.Rollback(); } catch { }
throw;
}
}
}
ReloadFakData();
MessageBox.Show(this, "已从方案库删除 " + tuhaos.Count + " 个图号。",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[方案删除图号] 失败: " + ex.Message);
MessageBox.Show(this, "删除图号失败:\n" + ex.Message,
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>按图号名删除方案库里的一个方案(图号行+箱号/元件表+表索引;带快照与确认)。供方案窗口项目页右键调用。</summary>
public void 删除方案图号(string 图号名)
{
if (string.IsNullOrWhiteSpace(图号名)) return;
string fakDbPath = 方案数据源路径();
if (!System.IO.File.Exists(fakDbPath))
{
MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案",
"删除方案", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string msg = "确认从方案库删除方案 [" + 图号名 + "]?\n\n该方案下的所有箱号、元件数据也会一并删除。";
if (MessageBox.Show(this, msg, "删除方案",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
try
{
PushUndoSnapshot(fakDbPath, "", true);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
try
{
var oldTables = new System.Collections.Generic.List<string>();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", 图号名);
using (var reader = cmd.ExecuteReader())
while (reader.Read())
{
string t = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrEmpty(t)) oldTables.Add(t);
}
}
foreach (string oldTbl in oldTables)
Exec(conn, tx, "DROP TABLE IF EXISTS \"" + oldTbl + "\";");
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + FakTuhaoTable + " WHERE 图号 = @th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", 图号名);
cmd.ExecuteNonQuery();
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 图号 = @th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", 图号名);
cmd.ExecuteNonQuery();
}
tx.Commit();
Log("[方案删除] 已删除方案=" + 图号名 + ", 关联表 " + oldTables.Count + " 张");
}
catch
{
try { tx.Rollback(); } catch { }
throw;
}
}
}
ReloadFakData();
}
catch (Exception ex)
{
Log("[方案删除] 失败: " + ex.Message);
MessageBox.Show(this, "删除方案失败:\n" + ex.Message,
"删除方案", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 方案页面 图号右键菜单「插入图号」:
/// 把选中的图号(含其下所有箱号+元件)从 fak.db 复制到当前项目报价库。
/// 逻辑与 MenuTuhao_AddToFak_Click 相反方向(fak.db → 项目库)。
/// </summary>
private void FakTuhao_Insert_Click(object sender, EventArgs e)
{
Log("点击[方案插入图号到报价], _currentProjectId=[" + _currentProjectId + "]");
// 1) 校验当前项目
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在报价页面双击选择一个项目,再从方案库插入图号。",
"插入图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var rows = GetAllSelectedRows(this.advancedDataGridView图号);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先在方案图号表中选择要插入的图号。",
"插入图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 收集图号名(去重)
var tuhaos = new System.Collections.Generic.List<string>();
var seen = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string th = GetCellValueAsString(r, "图号");
if (!string.IsNullOrEmpty(th) && seen.Add(th)) tuhaos.Add(th);
}
if (tuhaos.Count == 0)
{
MessageBox.Show(this, "未能从选中行读取图号。",
"插入图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string srcDbPath = 方案数据源路径();
if (!System.IO.File.Exists(srcDbPath))
{
MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案",
"插入图号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 确保目标项目库存在
EnsureQuoteDbExists(_currentProjectId);
string dstDbPath = GetQuoteDbPath(_currentProjectId);
try
{
int totalXh = 0, totalYj = 0;
foreach (string th in tuhaos)
{
int xhCount = 0, yjCount = 0;
// ★ 真隔离(2026-09-29):每个箱号实例生成独立 GUID 作内部ID,
// 元件表索引按 GUID 登记,跨实例绝不共用元件表(根治串数据)
var newXhIds = new System.Collections.Generic.List<string>();
// ---- 阶段一:从 fak.db 读出该图号的全部数据 ----
DataTable tuhaoRow = new DataTable();
string xhRealTblSrc = "";
DataTable xhRows = new DataTable();
var yjBuckets = new System.Collections.Generic.List<
System.Collections.Generic.KeyValuePair<string, DataTable>>();
var xhNamesInOrder = new System.Collections.Generic.List<string>();
using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath)))
{
srcConn.Open();
// (a) 图号行
string selTh = "SELECT * FROM " + FakTuhaoTable + " WHERE 图号 = @th LIMIT 1;";
using (var cmdTh = new System.Data.SQLite.SQLiteCommand(selTh, srcConn))
{
cmdTh.Parameters.AddWithValue("@th", th);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdTh))
{
ad.Fill(tuhaoRow);
}
}
if (tuhaoRow.Rows.Count == 0)
{
Log("[方案插入图号] fak.db 里找不到图号=" + th + ",跳过");
continue;
}
// (b) 该图号的箱号表真实表名 + 全部箱号行
xhRealTblSrc = GetXianghaoTableByIndex(th, srcConn);
if (!string.IsNullOrEmpty(xhRealTblSrc))
{
string selXh = "SELECT * FROM \"" + xhRealTblSrc + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var cmdXh = new System.Data.SQLite.SQLiteCommand(selXh, srcConn))
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdXh))
{
ad.Fill(xhRows);
}
}
// (c) 遍历每个箱号,读其元件表
foreach (DataRow xhRow in xhRows.Rows)
{
// ★ 用"内部ID"列查元件表索引(不是"箱号"显示名列),用户改箱号名不影响
string xhName = FakRead内部ID(xhRow);
xhNamesInOrder.Add(xhName);
string yjRealTblSrc = GetYuanjianTableByIndex(th, xhName, srcConn);
if (string.IsNullOrEmpty(yjRealTblSrc))
{
yjBuckets.Add(new System.Collections.Generic.KeyValuePair<string, DataTable>("", null));
continue;
}
DataTable yjRows = new DataTable();
string selYj = "SELECT * FROM \"" + yjRealTblSrc + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn))
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj))
{
ad.Fill(yjRows);
}
yjBuckets.Add(new System.Collections.Generic.KeyValuePair<string, DataTable>(yjRealTblSrc, yjRows));
}
}
xhCount = xhRows.Rows.Count;
foreach (var kv in yjBuckets)
{
if (kv.Value != null) yjCount += kv.Value.Rows.Count;
}
Log("[方案插入图号] 读取图号=" + th + " 完成: 图号行=1 箱号行=" + xhCount + " 元件行=" + yjCount);
// ---- 阶段二:写入项目报价库 ----
// 如果项目库已有相同图号,自动重命名(新图号1 → 新图号1_2, 新图号1_3...)
string actualTh = th;
using (var chkConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath)))
{
chkConn.Open();
string chkTh = "SELECT COUNT(*) FROM " + TuhaoTableName + " WHERE 图号 = @th;";
using (var chkCmd = new System.Data.SQLite.SQLiteCommand(chkTh, chkConn))
{
chkCmd.Parameters.AddWithValue("@th", th);
int dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar());
if (dupCnt > 0)
{
int suffix = 2;
while (dupCnt > 0)
{
actualTh = th + "_" + suffix.ToString();
suffix++;
chkCmd.Parameters["@th"].Value = actualTh;
dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar());
}
Log("[方案插入图号] 图号=" + th + " 已存在,重命名为 " + actualTh);
}
}
}
// 修改图号名
tuhaoRow.Rows[0]["图号"] = actualTh;
if (!tuhaoRow.Columns.Contains("内部ID")) tuhaoRow.Columns.Add("内部ID", typeof(string));
// ★ 真隔离修正(2026-09-30):图号内部ID = 显示名(与箱号表/元件表索引的"图号"键一致)。
// 不能用 GUID——主窗口选中图号后 _currentTuhao 从"内部ID"列读,
// 若为 GUID 会查不到按显示名登记的箱号表/元件表索引(图号唯一性靠重命名机制保证,
// 真隔离只需要箱号层用 GUID)。
tuhaoRow.Rows[0]["内部ID"] = actualTh;
using (var dstConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath)))
{
dstConn.Open();
using (var tx = dstConn.BeginTransaction())
{
try
{
// (1) 图号表:INSERT(用重命名后的图号名)
string[] thCols = { "序号", "图号", "图名", "台数", "箱柜数量",
"单价", "总价", "成本总价", "毛利", "毛利率" };
InsertRowByColumns(tuhaoRow.Rows[0], TuhaoTableName, thCols, dstConn, tx);
// (2) 计算新的表名序号起点
int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh");
int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj");
// (3) 创建箱号表 xh_N,插入箱号行,登记表索引(用 actualTh)
string newXhTbl = "xh_" + (xhSeq + 1).ToString();
CreateFakXianghaoTable(newXhTbl, dstConn, tx);
if (!xhRows.Columns.Contains("内部ID")) xhRows.Columns.Add("内部ID", typeof(string));
foreach (DataRow r in xhRows.Rows)
{
r.BeginEdit();
// ★ 真隔离(2026-09-29):每条箱号生成独立 GUID 作内部ID,
// 不复用方案库原内部ID,杜绝跨实例串数据
string xhId = System.Guid.NewGuid().ToString("N");
r["内部ID"] = xhId;
newXhIds.Add(xhId);
r.EndEdit();
InsertFakXianghaoRow(r, newXhTbl, dstConn, tx);
}
string insXhIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " +
"VALUES (@t, @th, @xh, @real);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(insXhIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@t", "箱号");
cmd.Parameters.AddWithValue("@th", actualTh);
cmd.Parameters.AddWithValue("@xh", "");
cmd.Parameters.AddWithValue("@real", newXhTbl);
cmd.ExecuteNonQuery();
}
// (4) 为每个箱号创建元件表 yj_M,插入元件行,登记表索引
for (int i = 0; i < yjBuckets.Count; i++)
{
var kv = yjBuckets[i];
if (kv.Value == null) continue;
// ★ 真隔离(2026-09-29):元件表索引"箱号"字段用箱号行的新 GUID,
// 不再用方案库原内部ID/显示名,跨实例绝不共用元件表
string newXhId = (i < newXhIds.Count) ? newXhIds[i] : "";
yjSeq++;
string newYjTbl = "yj_" + yjSeq.ToString();
CreateFakYuanjianTable(newYjTbl, dstConn, tx);
foreach (DataRow r in kv.Value.Rows)
{
InsertFakYuanjianRow(r, newYjTbl, dstConn, tx);
}
// ★ 插入后按公式重算元件派生价
重算元件表派生价(newYjTbl, dstConn, tx);
string insYjIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " +
"VALUES (@t, @th, @xh, @real);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(insYjIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@t", "元件");
cmd.Parameters.AddWithValue("@th", actualTh);
cmd.Parameters.AddWithValue("@xh", newXhId); // ★ 用箱号新 GUID 作索引键
cmd.Parameters.AddWithValue("@real", newYjTbl);
cmd.ExecuteNonQuery();
}
}
tx.Commit();
}
catch
{
try { tx.Rollback(); } catch { }
throw;
}
}
}
totalXh += xhCount;
totalYj += yjCount;
Log("[方案插入图号] 图号=" + th + " 写入项目库完成");
}
// ★ 记住报价图号表选中行索引,用于"在选中行下面插入"
int selectedTuhaoIndex = GetSelectedRowIndex(this.advancedDataGridView9);
// 刷新报价页面图号表
// 关键:清空所有缓存(_xianghaoCache / _yuanjianCache),防止
// SaveAllToDatabaseSilent 遍历缓存时用旧数据覆盖刚插入的数据库行。
_isReloading = true;
try
{
_xianghaoCache.Clear();
_yuanjianCache.Clear();
if (_tuhaoTable != null) { _tuhaoTable.Clear(); _tuhaoTable.AcceptChanges(); }
if (_xianghaoTable != null) { _xianghaoTable.Clear(); _xianghaoTable.AcceptChanges(); }
if (_yuanjianTable != null) { _yuanjianTable.Clear(); _yuanjianTable.AcceptChanges(); }
计算引擎.刷新项目汇总(GetQuoteDbPath(_currentProjectId), _currentProjectId, TuhaoTableName); // 触发器已算明细,补跨库项目汇总
ReloadTuhaoByProject(_currentProjectId);
// ★ 把末尾新插入的图号移动到选中行下面
if (_tuhaoTable != null)
MoveLastNRowsBelowSelected(this.advancedDataGridView9, _tuhaoTable,
selectedTuhaoIndex, tuhaos.Count, TuhaoTableName, _currentProjectId);
}
catch (Exception exReload) { Log("[方案插入图号] 刷新异常: " + exReload.Message); }
finally { _isReloading = false; }
// ★ 与"重新打开项目"完全相同的完整刷新路径:重算汇总+全表重载+选中联动。
// 插入流程自己的局部刷新覆盖不了全部状态(消耗列/元件表要重开才出现)。
string 记住图号 = _currentTuhao, 记住箱号 = _currentXianghao;
try { EnterProjectCore(_currentProjectId); }
catch (Exception exEnter) { Log("[方案插入] 完整刷新异常: " + exEnter.Message); }
// ★ 完整刷新会把选中重置回第一个图号/箱号。这里不靠"选中联动"(它被
// _isReloading 屏蔽,不触发加载),而是直接按记住的图号/箱号强制重载,
// 保证界面立即显示刚插入的数据。
try
{
if (!string.IsNullOrEmpty(记住图号))
{
_currentTuhao = 记住图号;
ReloadXianghaoByTuhao(_currentProjectId, 记住图号);
if (!string.IsNullOrEmpty(记住箱号))
{
ReloadYuanjianByXianghao(_currentProjectId, 记住图号, 记住箱号);
}
// ★ 关键:重载完成后最后校正当前箱号变量并高亮对应行,
// 保证 箱号表高亮 与 元件表显示 完全一致(之前变量停在旧箱号,误导显示)
_currentXianghao = 记住箱号;
SelectRowByValue(this.advancedDataGridView9, "内部ID", 记住图号);
if (!string.IsNullOrEmpty(记住箱号))
SelectRowByValue(this.advancedDataGridView4, "内部ID", 记住箱号);
}
}
catch (Exception exRestore) { Log("[方案插入] 恢复选中视图异常: " + exRestore.Message); }
string doneMsg = "已把 " + tuhaos.Count + " 个图号从方案库插入到当前项目报价库。\n" +
"箱号行 " + totalXh + " 条,元件行 " + totalYj + " 条。";
Log("[方案插入图号] 成功: " + doneMsg);
MessageBox.Show(this, doneMsg, "插入图号",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[方案插入图号] 失败: " + ex.Message);
MessageBox.Show(this, "插入图号失败:\n" + ex.Message,
"插入图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 箱号菜单:删除 / 插入 ----------
/// <summary>
/// 方案页面 箱号右键菜单「删除箱号」:
/// 对每个选中的箱号(在当前选中的图号下):
/// 1) 从 fak.db 的 xh_N 表 DELETE 该箱号行
/// 2) 查表索引拿该箱号的 yj_M 表名,DROP 它
/// 3) 从表索引 DELETE WHERE 类型='元件' AND 图号=@th AND 箱号=@xh
/// 4) 刷新当前图号的箱号表(ReloadFakXianghao)
/// </summary>
private void FakXianghao_Delete_Click(object sender, EventArgs e)
{
// 必须先有图号选中
DataGridViewRow rowTuhao = GetFirstSelectedRow(this.advancedDataGridView图号);
if (rowTuhao == null)
{
MessageBox.Show(this, "请先在方案图号表中选择图号,再删除箱号。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string tuhao = GetCellValueAsString(rowTuhao, "图号");
if (string.IsNullOrEmpty(tuhao))
{
MessageBox.Show(this, "当前图号为空,无法删除箱号。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var rows = GetAllSelectedRows(this.advancedDataGridView箱号);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先在箱号表中选择要删除的箱号。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 收集箱号名(去重)
var xhs = new System.Collections.Generic.List<string>();
var seen = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string xh = GetCellValueAsString(r, "箱号");
if (!string.IsNullOrEmpty(xh) && seen.Add(xh)) xhs.Add(xh);
}
if (xhs.Count == 0)
{
MessageBox.Show(this, "未能从选中行读取箱号。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string msg = "确认从方案库图号 [" + tuhao + "] 下删除以下 " + xhs.Count + " 个箱号?\n" +
string.Join(", ", xhs) +
"\n\n该箱号下的所有元件数据也会一并删除。";
if (MessageBox.Show(this, msg, "删除箱号",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
string fakDbPath = 方案数据源路径();
if (!System.IO.File.Exists(fakDbPath))
{
MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
// ★ 删除前快照方案库,支持右键「撤销删除」还原
PushUndoSnapshot(fakDbPath, "", true);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
string xhRealTbl = GetXianghaoTableByIndex(tuhao, conn);
if (string.IsNullOrEmpty(xhRealTbl))
{
MessageBox.Show(this, "图号 [" + tuhao + "] 没有箱号表。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var tx = conn.BeginTransaction())
{
try
{
foreach (string xh in xhs)
{
// 1) 从 xh_N 表删除该箱号行
string delXh = "DELETE FROM \"" + xhRealTbl + "\" WHERE 箱号 = @xh;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delXh, conn, tx))
{
cmd.Parameters.AddWithValue("@xh", xh);
cmd.ExecuteNonQuery();
}
// 2) 查表索引拿该箱号的元件表名,DROP
string yjTbl = LookupTableByIndex("元件", tuhao, xh, conn);
if (!string.IsNullOrEmpty(yjTbl))
{
string dropYj = "DROP TABLE IF EXISTS \"" + yjTbl + "\";";
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropYj, conn, tx))
{
dropCmd.ExecuteNonQuery();
}
}
// 3) 从表索引删除该箱号的元件记录
string delIdx = "DELETE FROM 表索引 WHERE 类型=@t AND 图号=@th AND 箱号=@xh;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delIdx, conn, tx))
{
cmd.Parameters.AddWithValue("@t", "元件");
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.Parameters.AddWithValue("@xh", xh);
cmd.ExecuteNonQuery();
}
Log("[方案删除箱号] 图号=" + tuhao + " 箱号=" + xh + " 已删除");
}
tx.Commit();
}
catch
{
try { tx.Rollback(); } catch { }
throw;
}
}
}
// 刷新当前图号的箱号表
ReloadFakXianghao(tuhao);
MessageBox.Show(this, "已删除 " + xhs.Count + " 个箱号。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[方案删除箱号] 失败: " + ex.Message);
MessageBox.Show(this, "删除箱号失败:\n" + ex.Message,
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 方案页面 箱号右键菜单「插入箱号」:
/// 把选中的箱号(含其下所有元件)从 fak.db 插入到报价页面当前图号(_currentTuhao)下。
/// </summary>
private void FakXianghao_Insert_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在报价页面双击选择一个项目,再插入箱号。",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 目标图号 = 报价页面当前选中的图号(用户需要先在报价页面选中一个图号)
if (string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先在报价页面的图号表中选中一个图号,\n再回到方案页面插入箱号。",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string targetTuhao = _currentTuhao;
// 方案页面当前选中的图号(箱号数据来源)
DataGridViewRow rowFakTuhao = GetFirstSelectedRow(this.advancedDataGridView图号);
if (rowFakTuhao == null)
{
MessageBox.Show(this, "请先在方案图号表中选择图号,再插入箱号。",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string fakTuhao = GetCellValueAsString(rowFakTuhao, "图号");
if (string.IsNullOrEmpty(fakTuhao))
{
MessageBox.Show(this, "方案图号为空,无法读取箱号数据。",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
Log("点击[方案插入箱号到报价], 项目=[" + _currentProjectId +
"] 报价图号=[" + targetTuhao + "] 方案图号=[" + fakTuhao + "]");
var rows = GetAllSelectedRows(this.advancedDataGridView箱号);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先在方案箱号表中选择要插入的箱号。",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 顺序修复(2026-09-29):SelectedRows 集合按选中操作逆序返回,
// 按 Grid 行索引(=方案库显示顺序)排序,保证插入顺序与方案库一致
rows.Sort((a, b) => a.Index.CompareTo(b.Index));
// 收集箱号名(去重)
var xhs = new System.Collections.Generic.List<string>();
var seen = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string xh = GetCellValueAsString(r, "箱号");
if (!string.IsNullOrEmpty(xh) && seen.Add(xh)) xhs.Add(xh);
}
if (xhs.Count == 0)
{
MessageBox.Show(this, "未能从选中行读取箱号。",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string srcDbPath = 方案数据源路径();
if (!System.IO.File.Exists(srcDbPath))
{
MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
EnsureQuoteDbExists(_currentProjectId);
string dstDbPath = GetQuoteDbPath(_currentProjectId);
// ★ 定位插入(2026-09-29):新箱号插到报价页面当前选中箱号的下方——
// 选中行序号=基准,序号更大的行整体+n 让位,新行依次用让出的序号。
// (旧逻辑沿用方案库源序号,与已有行序号重复导致排序交错、事后移动错位)
long baseSeq = 0;
{
using (var seqConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath)))
{
seqConn.Open();
string dstXhTbl0 = LookupTableByIndex("箱号", targetTuhao, "", seqConn);
if (!string.IsNullOrEmpty(dstXhTbl0))
{
EnsureColumns(seqConn, "\"" + dstXhTbl0 + "\"", new[] { "内部ID" });
// 选中行序号(双键:内部ID GUID 优先,老数据显示名兜底)
if (!string.IsNullOrWhiteSpace(_currentXianghao))
{
string selSeqSql = "SELECT CAST(序号 AS INTEGER) FROM \"" + dstXhTbl0 + "\"" +
" WHERE 内部ID=@k OR 箱号=@k LIMIT 1;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(selSeqSql, seqConn))
{
cmd.Parameters.AddWithValue("@k", _currentXianghao);
object o = cmd.ExecuteScalar();
if (o != null && o != DBNull.Value) baseSeq = Convert.ToInt64(o);
}
}
if (baseSeq <= 0)
{
// 无选中行(或未命中):追加到末尾 = 当前最大序号
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT MAX(CAST(序号 AS INTEGER)) FROM \"" + dstXhTbl0 + "\";", seqConn))
{
object o = cmd.ExecuteScalar();
if (o != null && o != DBNull.Value) baseSeq = Convert.ToInt64(o);
}
}
// 让位:序号 > baseSeq 的行整体 +n,空出的位置给新行
string shiftSql = "UPDATE \"" + dstXhTbl0 + "\" SET 序号 = CAST(序号 AS INTEGER) + @n" +
" WHERE CAST(序号 AS INTEGER) > @base;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(shiftSql, seqConn))
{
cmd.Parameters.AddWithValue("@n", xhs.Count);
cmd.Parameters.AddWithValue("@base", baseSeq);
int moved = cmd.ExecuteNonQuery();
Log("[方案插入箱号] 定位插入: 选中序号=" + baseSeq + " 让位行数=" + moved +
" 新行序号=" + (baseSeq + 1) + ".." + (baseSeq + xhs.Count));
}
}
}
}
int xhInsertCounter = 0;
try
{
int totalYj = 0;
using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath)))
{
srcConn.Open();
string xhRealTblSrc = GetXianghaoTableByIndex(fakTuhao, srcConn);
if (string.IsNullOrEmpty(xhRealTblSrc))
{
MessageBox.Show(this, "方案图号 [" + fakTuhao + "] 没有箱号表。",
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
foreach (string xh in xhs)
{
// 读箱号行
DataTable xhRow = new DataTable();
string selXh = "SELECT * FROM \"" + xhRealTblSrc + "\" WHERE 箱号 = @xh LIMIT 1;";
using (var cmdXh = new System.Data.SQLite.SQLiteCommand(selXh, srcConn))
{
cmdXh.Parameters.AddWithValue("@xh", xh);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdXh))
{
ad.Fill(xhRow);
}
}
if (xhRow.Rows.Count == 0)
{
Log("[方案插入箱号] fak.db 里找不到箱号=" + xh + ",跳过");
continue;
}
// 读该箱号的元件数据
DataTable yjRows = new DataTable();
string yjRealTblSrc = GetYuanjianTableByIndex(fakTuhao, xh, srcConn);
if (!string.IsNullOrEmpty(yjRealTblSrc))
{
string selYj = "SELECT * FROM \"" + yjRealTblSrc + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn))
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj))
{
ad.Fill(yjRows);
}
}
// ---- 写入项目库目标图号(targetTuhao)----
using (var dstConn = new System.Data.SQLite.SQLiteConnection(GetConnString(dstDbPath)))
{
dstConn.Open();
using (var tx = dstConn.BeginTransaction())
{
try
{
// (1) 确保目标图号有箱号表,没有就建
string dstXhTbl = LookupTableByIndex("箱号", targetTuhao, "", dstConn);
if (string.IsNullOrEmpty(dstXhTbl))
{
int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh");
dstXhTbl = "xh_" + (xhSeq + 1).ToString();
CreateFakXianghaoTable(dstXhTbl, dstConn, tx);
string insXhIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " +
"VALUES (@t, @th, @xh, @real);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(insXhIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@t", "箱号");
cmd.Parameters.AddWithValue("@th", targetTuhao);
cmd.Parameters.AddWithValue("@xh", "");
cmd.Parameters.AddWithValue("@real", dstXhTbl);
cmd.ExecuteNonQuery();
}
}
// ★ 老库结构兼容:目标图号的箱号表若已存在且是老结构(无"箱名"列),
// 幂等补列,否则下面的全列 INSERT 会报 no column named 箱名
// ★ 真隔离(2026-09-29):同时补"内部ID"列,新插入写入 GUID
EnsureColumns(dstConn, "\"" + dstXhTbl + "\"", new[] { "箱名", "内部ID" });
// (2) 插入箱号行(如果箱号已存在,自动重命名)
string actualXhName = xh;
// ★ 真隔离(2026-09-29):每个箱号实例生成独立 GUID 作内部ID,
// 元件表索引按 GUID 登记,跨实例绝不共用元件表(根治串数据)
string newXhId = System.Guid.NewGuid().ToString("N");
{
string chkDup = "SELECT COUNT(*) FROM \"" + dstXhTbl + "\" WHERE 箱号 = @xh;";
using (var chkCmd = new System.Data.SQLite.SQLiteCommand(chkDup, dstConn, tx))
{
chkCmd.Parameters.AddWithValue("@xh", xh);
int dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar());
if (dupCnt > 0)
{
// 自动重命名:新箱号2 → 新箱号2_2, 新箱号2_3...
int suffix = 2;
while (dupCnt > 0)
{
actualXhName = xh + "_" + suffix.ToString();
suffix++;
chkCmd.Parameters["@xh"].Value = actualXhName;
dupCnt = Convert.ToInt32(chkCmd.ExecuteScalar());
}
Log("[方案插入箱号] 箱号=" + xh + " 已存在,重命名为 " + actualXhName);
}
}
// 修改箱号名后插入
DataRow renamedRow = xhRow.Rows[0].Table.NewRow();
foreach (DataColumn col in xhRow.Rows[0].Table.Columns)
{
renamedRow[col.ColumnName] = xhRow.Rows[0][col.ColumnName];
}
renamedRow["箱号"] = actualXhName;
if (!renamedRow.Table.Columns.Contains("内部ID")) renamedRow.Table.Columns.Add("内部ID", typeof(string));
// ★ 真隔离(2026-09-29):内部ID 用 GUID,不再等于显示名,
// 杜绝跨实例共用同一元件表索引导致串数据
newXhId = System.Guid.NewGuid().ToString("N");
renamedRow["内部ID"] = newXhId;
// ★ 定位插入:序号用让位后的连续序号(不用方案库源序号,
// 避免与已有行序号重复导致排序交错)
xhInsertCounter++;
if (renamedRow.Table.Columns.Contains("序号"))
renamedRow["序号"] = (baseSeq + xhInsertCounter).ToString();
Log("[方案插入箱号] 准备插入: 表=" + dstXhTbl + " 箱号=" + actualXhName + " 内部ID=" + newXhId + " 目标图号=" + targetTuhao);
InsertFakXianghaoRow(renamedRow, dstXhTbl, dstConn, tx);
Log("[方案插入箱号] 插入完成: 表=" + dstXhTbl + " 箱号=" + actualXhName + " 内部ID=" + newXhId);
}
// (3) ★ 真隔离:永远创建新元件表(不复用旧表),
// 元件表索引"箱号"字段用新 GUID 登记,跨实例绝不共用元件表
int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj");
string dstYjTbl = "yj_" + (yjSeq + 1).ToString();
CreateFakYuanjianTable(dstYjTbl, dstConn, tx);
string insYjIdx = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) " +
"VALUES (@t, @th, @xh, @real);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(insYjIdx, dstConn, tx))
{
cmd.Parameters.AddWithValue("@t", "元件");
cmd.Parameters.AddWithValue("@th", targetTuhao);
cmd.Parameters.AddWithValue("@xh", newXhId); // ★ 用新 GUID 作索引键
cmd.Parameters.AddWithValue("@real", dstYjTbl);
cmd.ExecuteNonQuery();
}
// (4) 插入元件行
foreach (DataRow yjR in yjRows.Rows)
{
InsertFakYuanjianRow(yjR, dstYjTbl, dstConn, tx);
}
// ★ 插入后按公式重算元件派生价
重算元件表派生价(dstYjTbl, dstConn, tx);
tx.Commit();
totalYj += yjRows.Rows.Count;
Log("[方案插入箱号] 箱号=" + xh + " 写入图号[" + targetTuhao + "], 元件行=" + yjRows.Rows.Count);
}
catch
{
try { tx.Rollback(); } catch { }
throw;
}
}
}
}
}
// ★ 定位插入(2026-09-29):插入前已按选中行序号让位,新行天然在选中行下方,
// 不再需要事后移动(且 ReloadXianghaoByTuhao 会把选中重置回第一行,
// 事后移动的"选中行索引"已失真,会造成误移)
// 刷新报价页面(切到目标图号的箱号表)
// 关键:清空缓存,防止 SaveAllToDatabaseSilent 遍历缓存时覆盖刚插入的数据
_currentTuhao = targetTuhao;
_isReloading = true;
try
{
_xianghaoCache.Clear();
_yuanjianCache.Clear();
if (_xianghaoTable != null) { _xianghaoTable.Clear(); _xianghaoTable.AcceptChanges(); }
if (_yuanjianTable != null) { _yuanjianTable.Clear(); _yuanjianTable.AcceptChanges(); }
if (_tuhaoTable != null) _tuhaoTable.AcceptChanges();
RecalcAllFromDatabase(_currentProjectId); // ★ 重算箱号/图号/项目汇总价
ReloadXianghaoByTuhao(_currentProjectId, targetTuhao);
}
catch (Exception exReload)
{
Log("[方案插入箱号] 刷新报价页面异常: " + exReload.Message);
}
finally
{
_isReloading = false;
}
// ★ 与"重新打开项目"完全相同的完整刷新路径:重算汇总+全表重载+选中联动。
// 插入流程自己的局部刷新覆盖不了全部状态(消耗列/元件表要重开才出现)。
string 记住图号 = _currentTuhao, 记住箱号 = _currentXianghao;
try { EnterProjectCore(_currentProjectId); }
catch (Exception exEnter) { Log("[方案插入] 完整刷新异常: " + exEnter.Message); }
// ★ 完整刷新会把选中重置回第一个图号/箱号。这里不靠"选中联动"(它被
// _isReloading 屏蔽,不触发加载),而是直接按记住的图号/箱号强制重载,
// 保证界面立即显示刚插入的数据。
try
{
if (!string.IsNullOrEmpty(记住图号))
{
_currentTuhao = 记住图号;
ReloadXianghaoByTuhao(_currentProjectId, 记住图号);
if (!string.IsNullOrEmpty(记住箱号))
{
ReloadYuanjianByXianghao(_currentProjectId, 记住图号, 记住箱号);
}
// ★ 关键:重载完成后最后校正当前箱号变量并高亮对应行,
// 保证 箱号表高亮 与 元件表显示 完全一致(之前变量停在旧箱号,误导显示)
_currentXianghao = 记住箱号;
SelectRowByValue(this.advancedDataGridView9, "内部ID", 记住图号);
if (!string.IsNullOrEmpty(记住箱号))
SelectRowByValue(this.advancedDataGridView4, "内部ID", 记住箱号);
}
}
catch (Exception exRestore) { Log("[方案插入] 恢复选中视图异常: " + exRestore.Message); }
string doneMsg = "已把 " + xhs.Count + " 个箱号插入到报价图号 [" + targetTuhao + "] 下。\n" +
"元件行 " + totalYj + " 条。";
Log("[方案插入箱号] 成功: " + doneMsg);
MessageBox.Show(this, doneMsg, "插入箱号",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[方案插入箱号] 失败: " + ex.Message);
MessageBox.Show(this, "插入箱号失败:\n" + ex.Message,
"插入箱号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 元件菜单:删除 / 插入 ----------
/// <summary>
/// 方案页面 元件右键菜单「删除元件」:
/// 从 fak.db 的当前 yj_M 表删除选中的行(可多选),刷新当前箱号的元件表。
/// </summary>
private void FakYuanjian_Delete_Click(object sender, EventArgs e)
{
// 必须先选中图号+箱号(用于定位 yj_M 表)
DataGridViewRow rowTuhao = GetFirstSelectedRow(this.advancedDataGridView图号);
DataGridViewRow rowXianghao = GetFirstSelectedRow(this.advancedDataGridView箱号);
if (rowTuhao == null || rowXianghao == null)
{
MessageBox.Show(this, "请先在方案图号/箱号表中选择图号和箱号,再删除元件。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string tuhao = GetCellValueAsString(rowTuhao, "图号");
string xianghao = GetCellValueAsString(rowXianghao, "箱号");
if (string.IsNullOrEmpty(tuhao) || string.IsNullOrEmpty(xianghao))
{
MessageBox.Show(this, "图号或箱号为空,无法定位元件表。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var rows = GetAllSelectedRows(this.advancedDataGridView元件);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先在元件表中选择要删除的元件。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 收集要删除的序号(元件表通过 序号 定位行)
var seqs = new System.Collections.Generic.List<string>();
var seen = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string seq = GetCellValueAsString(r, "序号");
if (seen.Add(seq)) seqs.Add(seq);
}
if (MessageBox.Show(this, "确认删除选中的 " + seqs.Count + " 条元件?",
"删除元件", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
string fakDbPath = 方案数据源路径();
if (!System.IO.File.Exists(fakDbPath))
{
MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
// ★ 删除前快照方案库,支持右键「撤销删除」还原
PushUndoSnapshot(fakDbPath, "", true);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
string yjRealTbl = GetYuanjianTableByIndex(tuhao, xianghao, conn);
if (string.IsNullOrEmpty(yjRealTbl))
{
MessageBox.Show(this, "未找到图号 [" + tuhao + "] 箱号 [" + xianghao + "] 的元件表。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 按序号删除(序号唯一标识行)
int deleted = 0;
foreach (string seq in seqs)
{
string del = "DELETE FROM \"" + yjRealTbl + "\" WHERE 序号 = @seq;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(del, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
deleted += cmd.ExecuteNonQuery();
}
}
Log("[方案删除元件] 图号=" + tuhao + " 箱号=" + xianghao +
" 删除 " + deleted + " 条(选中 " + seqs.Count + ")");
}
ReloadFakYuanjian(tuhao, xianghao);
MessageBox.Show(this, "已删除 " + seqs.Count + " 条元件。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[方案删除元件] 失败: " + ex.Message);
MessageBox.Show(this, "删除元件失败:\n" + ex.Message,
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 方案页面 元件右键菜单「插入元件」:
/// 把选中的元件行从 fak.db 插入到报价页面当前箱号(_currentTuhao + _currentXianghao)的 yj_M 表。
/// </summary>
private void FakYuanjian_Insert_Click(object sender, EventArgs e)
{
Log("点击[方案插入元件到报价], _currentProjectId=[" + _currentProjectId +
"] _currentTuhao=[" + _currentTuhao + "] _currentXianghao=[" + _currentXianghao + "]");
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在报价页面双击选择一个项目,再插入元件。",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao))
{
MessageBox.Show(this, "请先在报价页面选择目标图号和箱号,再插入元件。",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 先保存当前所有编辑(包括内存里新建的图号/箱号等),确保插入目标的箱号行已在数据库中存在,
// 否则方案库插入元件时读不到这个箱号,元件表建不了、汇总也算不了。
SaveAllToDatabaseSilent();
// 方案页面当前图号+箱号(读取元件数据时要用)
DataGridViewRow rowFakTuhao = GetFirstSelectedRow(this.advancedDataGridView图号);
DataGridViewRow rowFakXianghao = GetFirstSelectedRow(this.advancedDataGridView箱号);
if (rowFakTuhao == null || rowFakXianghao == null)
{
MessageBox.Show(this, "请先在方案图号/箱号表中选择图号和箱号,再插入元件。",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string fakTuhao = GetCellValueAsString(rowFakTuhao, "图号");
string fakXianghao = GetCellValueAsString(rowFakXianghao, "箱号");
if (string.IsNullOrEmpty(fakTuhao) || string.IsNullOrEmpty(fakXianghao))
{
MessageBox.Show(this, "方案图号或箱号为空,无法读取元件数据。",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var rows = GetAllSelectedRows(this.advancedDataGridView元件);
if (rows.Count == 0)
{
MessageBox.Show(this, "请先在方案元件表中选择要插入的元件。",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string srcDbPath = 方案数据源路径();
if (!System.IO.File.Exists(srcDbPath))
{
MessageBox.Show(this, "请先在方案窗口项目页双击打开一个方案",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 从 fak.db 读出选中的元件行(按序号)
var seqs = new System.Collections.Generic.List<string>();
var seenSeq = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string seq = GetCellValueAsString(r, "序号");
if (seenSeq.Add(seq)) seqs.Add(seq);
}
DataTable yjRows = new DataTable();
using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath)))
{
srcConn.Open();
string yjRealTblSrc = GetYuanjianTableByIndex(fakTuhao, fakXianghao, srcConn);
if (string.IsNullOrEmpty(yjRealTblSrc))
{
MessageBox.Show(this, "方案库里未找到图号 [" + fakTuhao + "] 箱号 [" + fakXianghao + "] 的元件表。",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 按序号读取选中的行
// SQLite 不方便做 IN(@p1,@p2,...) 动态参数,这里逐条读
foreach (string seq in seqs)
{
string selYj = "SELECT * FROM \"" + yjRealTblSrc + "\" WHERE 序号 = @seq LIMIT 1;";
using (var cmdYj = new System.Data.SQLite.SQLiteCommand(selYj, srcConn))
{
cmdYj.Parameters.AddWithValue("@seq", seq);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmdYj))
{
DataTable tmp = new DataTable();
ad.Fill(tmp);
// 合并到 yjRows(保证列结构一致)
if (yjRows.Columns.Count == 0 && tmp.Columns.Count > 0)
{
foreach (DataColumn c in tmp.Columns)
yjRows.Columns.Add(c.ColumnName, c.DataType);
}
foreach (DataRow r in tmp.Rows)
yjRows.ImportRow(r);
}
}
}
}
if (yjRows.Rows.Count == 0)
{
MessageBox.Show(this, "未能从方案库读取选中的元件数据。",
"插入元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
EnsureQuoteDbExists(_currentProjectId);
// ============================================================
// ★ 参考 WebView元件库 / GCS母排 同款可行插入:直接写内存 _yuanjianTable,
// 靠 DataGridView 绑定即时显示;保存走正常流程(切窗/关窗落库)。
// 之前"先写数据库再重载"在新建箱号/缓存未登记时会刷新不出来,现改为内存插入最可靠。
// ============================================================
string 记住图号 = _currentTuhao, 记住箱号 = _currentXianghao;
// 元件表不存在(新建的箱号还没建表):走 InsertYuanjian 建表路径再插入
if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_yuanjianTable.TableName))
{
_isReloading = true;
try
{
SaveAllQuoteTablesIfDirty(false);
if (string.IsNullOrWhiteSpace(_currentTuhao) || string.IsNullOrWhiteSpace(_currentXianghao))
throw new Exception("请先选中图号和箱号。");
// ★ 建表不插占位行(InsertYuanjian 会插入"母排"垃圾行,源项目是直接建表)
using (var yc = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId))))
{
yc.Open();
string yTbl = LookupTableByIndex("元件", _currentTuhao, _currentXianghao, yc);
if (string.IsNullOrEmpty(yTbl))
{
using (var ytx = yc.BeginTransaction())
{
int seq2 = GetFakMaxTableSeq(yc, ytx, "yj");
yTbl = "yj_" + (seq2 + 1).ToString();
CreateFakYuanjianTable(yTbl, yc, ytx);
using (var ic = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件',@th,@xh,@t);", yc, ytx))
{
ic.Parameters.AddWithValue("@th", _currentTuhao);
ic.Parameters.AddWithValue("@xh", _currentXianghao);
ic.Parameters.AddWithValue("@t", yTbl);
ic.ExecuteNonQuery();
}
ytx.Commit();
}
}
}
string yjKey0 = _currentTuhao + "\n" + _currentXianghao;
_yuanjianCache.Remove(yjKey0);
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
}
catch (Exception ex)
{
Log("[方案插入元件] 建表失败: " + ex.Message);
MessageBox.Show(this, "插入元件失败:\n" + ex.Message, "插入元件",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
finally
{
_currentTuhao = 记住图号;
_currentXianghao = 记住箱号;
_isReloading = false;
}
}
if (_yuanjianTable == null) return;
// 逐行把 fak 元件写进内存表(选中行下面插入),派生价按 Excel 公式现算
// ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入;
// 单选=false 返回后走原逻辑
System.Action 插入动作 = delegate
{
this.advancedDataGridView5.EndEdit();
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
_isReloading = true;
try
{
foreach (DataRow src in yjRows.Rows)
{
var row = _yuanjianTable.NewRow();
row["序号"] = "1"; // InsertRowBelowSelected 会自动重编号
row["元件名称"] = ColValue(src, "元件名称");
row["规格"] = ColValue(src, "规格");
row["单位"] = ColValue(src, "单位");
row["数量"] = ColValue(src, "数量");
row["表价"] = ColValue(src, "表价");
row["采购系数"] = ColValue(src, "采购系数");
row["报出系数"] = ColValue(src, "报出系数");
row["图纸规格"] = ColValue(src, "图纸规格");
row["品牌"] = ColValue(src, "品牌");
row["系列"] = ColValue(src, "系列");
row["类型"] = ColValue(src, "类型");
row["备注"] = ColValue(src, "备注");
// 派生价按公式现算(与 RecalcYuanjianRow 一致)
decimal bj = TryParseDecimalOrZero(ColValue(src, "表价"));
decimal xs = TryParseDecimalOrZero(ColValue(src, "采购系数")); if (xs == 0m) xs = 1m;
decimal bcs = TryParseDecimalOrZero(ColValue(src, "报出系数")); if (bcs == 0m) bcs = 1m;
decimal sl = TryParseDecimalOrZero(ColValue(src, "数量"));
decimal cgdj = bj * xs;
row["采购单价"] = (double)cgdj;
row["采购价"] = (double)(cgdj * sl);
row["报出单价"] = (double)(cgdj * bcs);
row["报出价"] = (double)(cgdj * bcs * sl);
InsertRowBelowSelected(_yuanjianTable, row, selectedIndex);
Log("[方案插入元件] 已插入: " + ColValue(src, "元件名称") + " 表价=" + bj);
}
}
catch (Exception ex) { Log("[方案插入元件] 内存插入异常: " + ex.Message); throw; }
finally { _isReloading = false; }
// ★ 虚拟模式收尾:同步网格行数+全表保存(循环里插的行才会显示并持久化)
元件表插行收尾();
// 滚动到最后插入的行
try
{
if (_yuanjianTable.Rows.Count > 0)
this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = _yuanjianTable.Rows.Count - 1;
}
catch { }
// 重算箱号/图号/项目聚合并刷新显示(与 WebView/GCS 插入一致);保存走正常流程
重算聚合();
ApplyPriceFormat(this.advancedDataGridView5, _单台小数位文本);
};
if (工具窗口批量插入(插入动作))
{
_currentTuhao = 记住图号;
_currentXianghao = 记住箱号;
return;
}
插入动作();
_currentTuhao = 记住图号;
_currentXianghao = 记住箱号;
string doneMsg = "已把 " + yjRows.Rows.Count + " 条元件插入到报价图号 [" +
记住图号 + "] 箱号 [" + 记住箱号 + "] 下。";
Log("[方案插入元件] 成功: " + doneMsg);
MessageBox.Show(this, doneMsg, "插入元件",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
/// <summary>
/// 确保方案库 fak.db 存在,并创建图号表、表索引、项目信息三张固定表。
/// 数据结构与项目报价库完全一致:
/// - 图号表 advancedDataGridView9Table(不用索引,直接按图号查找)
/// - 表索引(类型/图号/箱号/实际表名),箱号表 xh_N、元件表 yj_N 通过它映射
/// - 项目信息表(沿用项目库结构)
/// 箱号表/元件表不在固定建表之列,它们在「加入方案库」时按 xh_N/yj_M 动态创建。
/// </summary>
private void EnsureFakDbExists(string fakDbPath)
{
// ★ 空路径防御:方案窗口没打开方案时 fakDbPath 为空,CreateDirectory("")/空连接串会裸抛
if (string.IsNullOrWhiteSpace(fakDbPath))
throw new Exception("方案库路径为空(方案窗口未打开方案)");
// 确保目录存在
string dir = System.IO.Path.GetDirectoryName(fakDbPath);
if (!System.IO.Directory.Exists(dir))
{
System.IO.Directory.CreateDirectory(dir);
}
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(fakDbPath)))
{
conn.Open();
// 图号表(与项目库图号表结构一致,含内部ID列)
string sqlTh =
"CREATE TABLE IF NOT EXISTS " + FakTuhaoTable + " (" +
" 序号 TEXT, 图号 TEXT, 图名 TEXT, 台数 TEXT, 箱柜数量 REAL," +
" 单价 REAL, 总价 REAL, 成本总价 REAL, 毛利 REAL, 毛利率 REAL," +
" 内部ID TEXT" +
");";
// ★ 老库兼容:CREATE TABLE IF NOT EXISTS 不加列,需 ALTER TABLE 补内部ID
try
{
using (var checkCmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM pragma_table_info('" + FakTuhaoTable + "') WHERE name='内部ID';", conn))
{
if (Convert.ToInt32(checkCmd.ExecuteScalar()) == 0)
{
using (var alterCmd = new System.Data.SQLite.SQLiteCommand(
"ALTER TABLE " + FakTuhaoTable + " ADD COLUMN \"内部ID\" TEXT;", conn))
alterCmd.ExecuteNonQuery();
}
}
}
catch { /* 表刚建好已有内部ID,或 ALTER 失败(列已存在),忽略 */ }
// 表索引(与项目库结构完全一致)
string sqlIndex =
"CREATE TABLE IF NOT EXISTS 表索引 (" +
" 类型 TEXT, 图号 TEXT, 箱号 TEXT, 实际表名 TEXT" +
");";
// 项目信息表(与项目库结构一致)
string sqlProjectInfo =
"CREATE TABLE IF NOT EXISTS " + ProjectInfoTableName + " (" +
" 项目编号 TEXT, 项目名称 TEXT, 客户名称 TEXT, 客户地址 TEXT," +
" 联系人 TEXT, 联系电话 TEXT, 项目日期 TEXT, 备注 TEXT," +
" 成套厂名称 TEXT, 成套厂地址 TEXT, 成套厂联系人 TEXT, 成套厂联系电话 TEXT," +
" 单台小数位 TEXT, 单台取整方式 TEXT, 项目小数位 TEXT, 项目取整方式 TEXT" +
");";
foreach (string sql in new[] { sqlTh, sqlIndex, sqlProjectInfo })
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.ExecuteNonQuery();
}
}
// ★ B1(2026-10-01):方案库表索引同样建复合索引(方案窗口查表索引高频,此前全表扫描)
try
{
using (var idxCmd = new System.Data.SQLite.SQLiteCommand(
"CREATE INDEX IF NOT EXISTS idx_表索引_类型图号箱号 ON 表索引(类型, 图号, 箱号);", conn))
{
idxCmd.ExecuteNonQuery();
}
}
catch { }
// ★ B3(2026-10-01):方案库数据表同样补"内部ID"索引(幂等)
try { 确保数据表性能索引(conn); }
catch { }
}
}
/// <summary>
/// 查 fak.db 表索引里指定前缀(xh 或 yj)的实际表名最大序号。
/// 用于生成下一个不冲突的 xh_N / yj_M 表名。
/// 返回最大序号;表索引为空或无匹配时返回 0。
/// </summary>
private int GetFakMaxTableSeq(System.Data.SQLite.SQLiteConnection conn,
System.Data.SQLite.SQLiteTransaction tx, string prefix)
{
if (conn == null || string.IsNullOrEmpty(prefix)) return 0;
int maxN = 0;
// 1) 从「表索引」表查最大序号
string sql1 = "SELECT 实际表名 FROM 表索引 WHERE 实际表名 LIKE @p;";
using (var cmd1 = new System.Data.SQLite.SQLiteCommand(sql1, conn, tx))
{
cmd1.Parameters.AddWithValue("@p", prefix + "_%");
using (var reader = cmd1.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;
}
}
}
// 2) ★ 同时从 sqlite_master 查实际存在的物理表的最大序号
// 防止"表索引被清空/部分清理,但物理表遗留"导致新表名与老表冲突
// (例如先 DROP 表索引里的记录但忘了 DROP 物理表,下次算序号会从 1 开始,冲突)
string sql2 = "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE @p;";
using (var cmd2 = new System.Data.SQLite.SQLiteCommand(sql2, conn, tx))
{
cmd2.Parameters.AddWithValue("@p", prefix + "_%");
using (var reader = cmd2.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 maxN;
}
/// <summary>
/// 在 fak.db 里建一张空箱号表(真实表名为 xh_N)。
/// 列结构与项目库箱号表完全一致(无「图号」列)。
/// </summary>
private void CreateFakXianghaoTable(string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string sql =
"CREATE TABLE \"" + realTableName + "\" (" +
" 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," +
" 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," +
" 内部ID TEXT" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
cmd.ExecuteNonQuery();
}
}
/// <summary>
/// 在 fak.db 里建一张空元件表(真实表名为 yj_M)。
/// 列结构与项目库元件表完全一致(无「图号」「箱号」列)。
/// </summary>
private void CreateFakYuanjianTable(string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string sql =
"CREATE TABLE \"" + realTableName + "\" (" +
" 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," +
" 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," +
" 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
cmd.ExecuteNonQuery();
}
}
/// <summary>
/// 把一条箱号源行 INSERT 到 fak.db 的 xh_N 表。
/// 列名与项目库箱号表完全一致(无额外「图号」列)。
/// </summary>
private void InsertFakXianghaoRow(DataRow src, string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string sql = "INSERT INTO \"" + realTableName + "\"" +
" (序号, 箱号, 箱名, 型号, 规格, 单位, 数量," +
" 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 箱柜类型, 备注, 图纸, 内部ID) " +
"VALUES (@序号, @箱号, @箱名, @型号, @规格, @单位, @数量," +
" @单台成本, @成本价, @单台报出, @报出价, @利润, @利润率, @毛利率, @箱柜类型, @备注, @图纸, @内部ID);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
cmd.Parameters.AddWithValue("@序号", ColValue(src, "序号"));
cmd.Parameters.AddWithValue("@箱号", ColValue(src, "箱号"));
cmd.Parameters.AddWithValue("@箱名", ColValue(src, "箱名"));
cmd.Parameters.AddWithValue("@型号", ColValue(src, "型号"));
cmd.Parameters.AddWithValue("@规格", ColValue(src, "规格"));
cmd.Parameters.AddWithValue("@单位", ColValue(src, "单位"));
cmd.Parameters.AddWithValue("@数量", ColValue(src, "数量"));
AddNumericParam(cmd, "@单台成本", src, "单台成本");
AddNumericParam(cmd, "@成本价", src, "成本价");
AddNumericParam(cmd, "@单台报出", src, "单台报出");
AddNumericParam(cmd, "@报出价", src, "报出价");
AddNumericParam(cmd, "@利润", src, "利润");
AddNumericParam(cmd, "@利润率", src, "利润率");
AddNumericParam(cmd, "@毛利率", src, "毛利率");
cmd.Parameters.AddWithValue("@箱柜类型", ColValue(src, "箱柜类型"));
cmd.Parameters.AddWithValue("@备注", ColValue(src, "备注"));
cmd.Parameters.AddWithValue("@图纸", ColValue(src, "图纸"));
cmd.Parameters.AddWithValue("@内部ID", ColValue(src, "内部ID"));
cmd.ExecuteNonQuery();
}
}
/// <summary>
/// 把一条元件源行 INSERT 到 fak.db 的 yj_M 表。
/// 列名与项目库元件表完全一致(无额外「图号」「箱号」列)。
/// </summary>
/// <summary>按元件表 Excel 公式重算整表派生价列(与 RecalcYuanjianRow 公式一致):
/// 采购单价=表价×采购系数;采购价=采购单价×数量;报出单价=采购单价×报出系数;报出价=报出单价×数量。
/// 方案库插入的元件行带的是源表价格,插入后统一按公式重算,保证与手工编辑行为一致。</summary>
private void 重算元件表派生价(string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string sql =
"UPDATE \"" + realTableName + "\" SET " +
" 采购单价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL)," +
" 采购价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL) * CAST(数量 AS REAL)," +
" 报出单价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL) * CAST(报出系数 AS REAL)," +
" 报出价 = CAST(表价 AS REAL) * CAST(采购系数 AS REAL) * CAST(报出系数 AS REAL) * CAST(数量 AS REAL)" +
" WHERE 表价 IS NOT NULL AND 采购系数 IS NOT NULL AND 采购系数 != '' AND 数量 IS NOT NULL AND 数量 != '';";
try
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
int rows = cmd.ExecuteNonQuery();
Log("[方案插入] 已重算元件派生价: 表=" + realTableName + " 行数=" + rows);
}
}
catch (Exception ex) { Log("[方案插入] 重算元件派生价失败: " + ex.Message); }
}
private void InsertFakYuanjianRow(DataRow src, string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string sql = "INSERT INTO \"" + realTableName + "\"" +
" (序号, 元件名称, 规格, 单位, 数量," +
" 表价, 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," +
" 图纸规格, 品牌, 系列, 类型, 备注) " +
"VALUES (@序号, @元件名称, @规格, @单位, @数量," +
" @表价, @采购单价, @采购系数, @采购价, @报出单价, @报出系数, @报出价," +
" @图纸规格, @品牌, @系列, @类型, @备注);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
cmd.Parameters.AddWithValue("@序号", ColValue(src, "序号"));
cmd.Parameters.AddWithValue("@元件名称", ColValue(src, "元件名称"));
cmd.Parameters.AddWithValue("@规格", ColValue(src, "规格"));
cmd.Parameters.AddWithValue("@单位", ColValue(src, "单位"));
cmd.Parameters.AddWithValue("@数量", ColValue(src, "数量"));
AddNumericParam(cmd, "@表价", src, "表价");
AddNumericParam(cmd, "@采购单价", src, "采购单价");
cmd.Parameters.AddWithValue("@采购系数", ColValue(src, "采购系数"));
AddNumericParam(cmd, "@采购价", src, "采购价");
AddNumericParam(cmd, "@报出单价", src, "报出单价");
cmd.Parameters.AddWithValue("@报出系数", ColValue(src, "报出系数"));
AddNumericParam(cmd, "@报出价", src, "报出价");
cmd.Parameters.AddWithValue("@图纸规格", ColValue(src, "图纸规格"));
cmd.Parameters.AddWithValue("@品牌", ColValue(src, "品牌"));
cmd.Parameters.AddWithValue("@系列", ColValue(src, "系列"));
cmd.Parameters.AddWithValue("@类型", ColValue(src, "类型"));
cmd.Parameters.AddWithValue("@备注", ColValue(src, "备注"));
cmd.ExecuteNonQuery();
}
}
/// <summary>InsertFakYuanjianRow 的字典版(虚拟模式选中行用 CopyRowFields 取得)。</summary>
private void InsertFakYuanjianRowDict(System.Collections.Generic.Dictionary<string, object> src,
string realTableName,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string sql = "INSERT INTO \"" + realTableName + "\"" +
" (序号, 元件名称, 规格, 单位, 数量," +
" 表价, 采购单价, 采购系数, 采购价, 报出单价, 报出系数, 报出价," +
" 图纸规格, 品牌, 系列, 类型, 备注) " +
"VALUES (@序号, @元件名称, @规格, @单位, @数量," +
" @表价, @采购单价, @采购系数, @采购价, @报出单价, @报出系数, @报出价," +
" @图纸规格, @品牌, @系列, @类型, @备注);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
cmd.Parameters.AddWithValue("@序号", DictValue(src, "序号"));
cmd.Parameters.AddWithValue("@元件名称", DictValue(src, "元件名称"));
cmd.Parameters.AddWithValue("@规格", DictValue(src, "规格"));
cmd.Parameters.AddWithValue("@单位", DictValue(src, "单位"));
cmd.Parameters.AddWithValue("@数量", DictValue(src, "数量"));
AddNumericParamDict(cmd, "@表价", src, "表价");
AddNumericParamDict(cmd, "@采购单价", src, "采购单价");
cmd.Parameters.AddWithValue("@采购系数", DictValue(src, "采购系数"));
AddNumericParamDict(cmd, "@采购价", src, "采购价");
AddNumericParamDict(cmd, "@报出单价", src, "报出单价");
cmd.Parameters.AddWithValue("@报出系数", DictValue(src, "报出系数"));
AddNumericParamDict(cmd, "@报出价", src, "报出价");
cmd.Parameters.AddWithValue("@图纸规格", DictValue(src, "图纸规格"));
cmd.Parameters.AddWithValue("@品牌", DictValue(src, "品牌"));
cmd.Parameters.AddWithValue("@系列", DictValue(src, "系列"));
cmd.Parameters.AddWithValue("@类型", DictValue(src, "类型"));
cmd.Parameters.AddWithValue("@备注", DictValue(src, "备注"));
cmd.ExecuteNonQuery();
}
}
/// <summary>安全取字典某列的字符串值(键不存在或 null 返回空串)。</summary>
private static string DictValue(System.Collections.Generic.Dictionary<string, object> src, string key)
{
if (src == null || string.IsNullOrEmpty(key)) return "";
object v;
if (!src.TryGetValue(key, out v) || v == null || v == DBNull.Value) return "";
return v.ToString();
}
/// <summary>字典版数值参数(无法解析为 double 时写 0)。</summary>
private static void AddNumericParamDict(System.Data.SQLite.SQLiteCommand cmd, string paramName,
System.Collections.Generic.Dictionary<string, object> src, string key)
{
string s = DictValue(src, key);
double d;
if (!double.TryParse(s, out d)) d = 0;
cmd.Parameters.AddWithValue(paramName, d);
}
/// <summary>按指定列名列表,把源行的对应列 INSERT 到目标表(只插文本列,数值列也按文本写)。</summary>
private void InsertRowByColumns(DataRow src, string dstTableName, string[] cols,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
// 构造 INSERT INTO dstTableName (c1, c2, ...) VALUES (@c1, @c2, ...)
string colList = string.Join(", ", cols);
var paramNames = new System.Collections.Generic.List<string>(cols.Length);
for (int i = 0; i < cols.Length; i++) paramNames.Add("@" + cols[i]);
string paramList = string.Join(", ", paramNames.ToArray());
string sql = "INSERT INTO " + dstTableName + " (" + colList + ") VALUES (" + paramList + ");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx))
{
foreach (string c in cols)
{
cmd.Parameters.AddWithValue("@" + c, ColValue(src, c));
}
cmd.ExecuteNonQuery();
}
}
/// <summary>安全取 DataRow 某列的字符串值(列不存在或 DBNull 返回空串)。</summary>
/// <summary>从箱号行读内部ID;表里没有该列或值为空时退回"箱号"显示名。
/// 方案库(fak.db)的箱号表没有"内部ID"列,内部ID就用箱号名。</summary>
/// <summary>箱号表右键「加入方案库」:把选中的箱号(可多选,含各自元件表)复制到方案库。
/// 若方案库还没有所属图号行,自动从项目库补齐图号行。</summary>
private void MenuXianghao_AddToFak_Click(object sender, EventArgs e)
{
Log("点击[箱号加入方案库], 项目=[" + _currentProjectId + "] 图号=[" + _currentTuhao + "]");
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先打开项目并选择要加入方案库的箱柜。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 多选:收集全部选中箱号(内部ID,去重)
var rows = GetAllSelectedRows(this.advancedDataGridView4);
var 箱号们 = new System.Collections.Generic.List<string>();
var seenId = new System.Collections.Generic.HashSet<string>();
foreach (var r in rows)
{
string id = GetBoundValueAsString(r, "内部ID");
if (!string.IsNullOrEmpty(id) && seenId.Add(id)) 箱号们.Add(id);
}
if (箱号们.Count == 0)
{
MessageBox.Show(this, "请先在箱号表中选择要加入方案库的箱柜(可按 Ctrl 多选)。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try { SaveAllQuoteTablesIfDirty(false); }
catch (Exception exSave) { Log("[箱柜加入方案库] 预保存失败(继续): " + exSave.Message); }
string srcDbPath = GetQuoteDbPath(_currentProjectId);
string fakDbPath = 方案数据源路径();
// ★ 方案窗口没打开方案时给出明确引导(旧逻辑空路径直接炸)
if (string.IsNullOrWhiteSpace(fakDbPath) || !System.IO.File.Exists(fakDbPath))
{
MessageBox.Show(this,
"方案窗口当前没有打开方案,无法加入。\n\n请先在右侧方案窗口的项目页双击打开一个方案\n(或用项目表右键\"加入方案库\"把项目存为新方案),再加入。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
int xhCount = 0, yjCount = 0;
using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath)))
using (var dstConn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath)))
{
srcConn.Open();
dstConn.Open();
EnsureFakDbExists(fakDbPath);
// ★ 方案文件由整库复制而来,可能带旧架构遗留触发器(引用已退役的固定表"箱号")——写入前必清
计算引擎.清理遗留触发器(dstConn);
foreach (string xhId in 箱号们)
{
using (var tx = dstConn.BeginTransaction())
{
try
{
// (a) 确保方案库有该图号行(无则从项目库复制)
int 图号行数 = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM " + FakTuhaoTable + " WHERE 内部ID = @th;", dstConn, tx))
{
// ★ 用图号内部ID查图号表(之前误传箱号内部ID,恒为0 → 每次重复补插图号行)
cmd.Parameters.AddWithValue("@th", _currentTuhao);
图号行数 = Convert.ToInt32(cmd.ExecuteScalar());
}
// 图号行按"内部ID"判断:箱号加入时若方案库没有该图号,先复制图号行
// (图号行复制走 复制图号到方案库 会连带全部箱号——这里只补图号行本身)
if (图号行数 == 0)
{
DataTable thRow = new DataTable();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM " + TuhaoTableName + " WHERE 内部ID = @th LIMIT 1;", srcConn))
{
cmd.Parameters.AddWithValue("@th", _currentTuhao);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) ad.Fill(thRow);
}
var thCols = new List<string>();
foreach (DataColumn c in thRow.Columns) thCols.Add(c.ColumnName);
if (thRow.Rows.Count > 0)
InsertRowByColumns(thRow.Rows[0], FakTuhaoTable, thCols.ToArray(), dstConn, tx);
}
// (b) 读项目库该箱号行 + 元件行
DataTable 箱号行表 = new DataTable();
string xhReal = GetXianghaoTableByIndex(_currentTuhao, srcConn);
if (!string.IsNullOrEmpty(xhReal))
using (var cmd2 = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM \"" + xhReal + "\" WHERE 内部ID = @xh LIMIT 1;", srcConn))
{
cmd2.Parameters.AddWithValue("@xh", xhId);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd2)) ad.Fill(箱号行表);
}
if (箱号行表.Rows.Count == 0) { tx.Rollback(); continue; }
DataRow 箱号行 = 箱号行表.Rows[0];
string 箱号显示名 = 箱号行表.Columns.Contains("箱号") && 箱号行["箱号"] != DBNull.Value ? 箱号行["箱号"].ToString() : xhId;
string yjReal = GetYuanjianTableByIndex(_currentTuhao, xhId, srcConn);
DataTable 元件行表 = new DataTable();
if (!string.IsNullOrEmpty(yjReal))
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM \"" + yjReal + "\" ORDER BY CAST(序号 AS INTEGER);", srcConn))
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) ad.Fill(元件行表);
// (c) fak 箱号表:已有则直接插行,无则建表+登记索引
string fakXhTbl = GetXianghaoTableByIndex(_currentTuhao, dstConn);
if (string.IsNullOrEmpty(fakXhTbl))
{
int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh") + 1;
fakXhTbl = "xh_" + xhSeq.ToString();
CreateFakXianghaoTable(fakXhTbl, dstConn, tx);
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('箱号', @th, '', @real);", dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", _currentTuhao);
cmd.Parameters.AddWithValue("@real", fakXhTbl);
cmd.ExecuteNonQuery();
}
}
InsertFakXianghaoRow(箱号行, fakXhTbl, dstConn, tx);
xhCount++;
// (d) fak 元件表:每箱号新建一张 yj 表 + 登记索引
int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj") + 1;
string fakYjTbl = "yj_" + yjSeq.ToString();
CreateFakYuanjianTable(fakYjTbl, dstConn, tx);
foreach (DataRow r in 元件行表.Rows)
InsertFakYuanjianRow(r, fakYjTbl, dstConn, tx);
yjCount += 元件行表.Rows.Count;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", _currentTuhao);
cmd.Parameters.AddWithValue("@xh", 箱号显示名);
cmd.Parameters.AddWithValue("@real", fakYjTbl);
cmd.ExecuteNonQuery();
}
tx.Commit();
}
catch { try { tx.Rollback(); } catch { } throw; }
}
}
}
Log("[箱柜加入方案库] 完成: 箱柜=" + xhCount + " 元件行=" + yjCount);
ReloadFakData();
MessageBox.Show(this, "已把 " + xhCount + " 个箱柜(含元件 " + yjCount + " 行)加入方案库。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[箱柜加入方案库] 失败: " + ex.Message);
MessageBox.Show(this, "加入方案库失败:\n" + ex.Message,
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>元件表右键「加入方案库」:把选中的元件(可多选)追加到方案库同名方案的元件表。
/// 若方案库还没有该图号/箱号,自动补齐图号行与箱号行。</summary>
private void MenuYuanjian_AddToFak_Click(object sender, EventArgs e)
{
Log("点击[元件加入方案库], 项目=[" + _currentProjectId + "] 图号=[" + _currentTuhao + "] 箱号=[" + _currentXianghao + "]");
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao) ||
string.IsNullOrWhiteSpace(_currentXianghao))
{
MessageBox.Show(this, "请先打开项目并选择要加入方案库的元件。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 多选:收集选中元件行(虚拟模式无 DataBoundItem,用 CopyRowFields 读内存表)
var rows = GetAllSelectedRows(this.advancedDataGridView5);
var exclude = new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase) { "_rowid_" };
var 元件DataRows = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();
foreach (var r in rows)
{
var dict = CopyRowFields(r, exclude);
if (dict != null && dict.Count > 0) 元件DataRows.Add(dict);
}
if (元件DataRows.Count == 0)
{
MessageBox.Show(this, "请先在元件表中选择要加入方案库的元件(可按 Ctrl 多选)。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try { SaveAllQuoteTablesIfDirty(false); }
catch (Exception exSave) { Log("[元件加入方案库] 预保存失败(继续): " + exSave.Message); }
string srcDbPath = GetQuoteDbPath(_currentProjectId);
string fakDbPath = 方案数据源路径();
// ★ 方案窗口没打开方案时给出明确引导(旧逻辑空路径直接炸)
if (string.IsNullOrWhiteSpace(fakDbPath) || !System.IO.File.Exists(fakDbPath))
{
MessageBox.Show(this,
"方案窗口当前没有打开方案,无法加入。\n\n请先在右侧方案窗口的项目页双击打开一个方案\n(或用项目表右键\"加入方案库\"把项目存为新方案),再加入。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 插入目标:优先方案窗口报价页当前选中的图号/箱号——用户在方案里选中哪个箱号,
// 元件就插到那个箱号的元件表(fallback 项目库当前图号/箱号)
string 目标图号 = (this.w8 != null) ? this.w8.方案选中图号 : "";
string 目标箱号 = (this.w8 != null) ? this.w8.方案选中箱号ID : "";
bool 用方案选中 = !string.IsNullOrEmpty(目标图号) && !string.IsNullOrEmpty(目标箱号);
if (!用方案选中) { 目标图号 = _currentTuhao; 目标箱号 = _currentXianghao; }
Log("[元件加入方案库] 插入目标: 图号=[" + 目标图号 + "] 箱号=[" + 目标箱号 + "]" +
(用方案选中 ? "(方案窗口选中)" : "(项目当前)"));
try
{
int yjCount = 0;
using (var srcConn = new System.Data.SQLite.SQLiteConnection(GetConnString(srcDbPath)))
using (var dstConn = new System.Data.SQLite.SQLiteConnection(方案库写连接串(fakDbPath)))
{
srcConn.Open();
dstConn.Open();
EnsureFakDbExists(fakDbPath);
// ★ 方案文件由整库复制而来,可能带旧架构遗留触发器(引用已退役的固定表"箱号")——写入前必清
计算引擎.清理遗留触发器(dstConn);
using (var tx = dstConn.BeginTransaction())
{
try
{
// (a) 确保方案库有图号行
int 图号行数 = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM " + FakTuhaoTable + " WHERE 内部ID = @th OR 图号 = @th;", dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", 目标图号);
图号行数 = Convert.ToInt32(cmd.ExecuteScalar());
}
if (图号行数 == 0)
{
DataTable thRow = new DataTable();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM " + TuhaoTableName + " WHERE 内部ID = @th LIMIT 1;", srcConn))
{
cmd.Parameters.AddWithValue("@th", _currentTuhao);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd)) ad.Fill(thRow);
}
var thCols = new List<string>();
foreach (DataColumn c in thRow.Columns) thCols.Add(c.ColumnName);
if (thRow.Rows.Count > 0)
InsertRowByColumns(thRow.Rows[0], FakTuhaoTable, thCols.ToArray(), dstConn, tx);
}
// (b) 确保方案库有箱号行(方案库没有该图号/箱号时,才从项目库补)
string fakXhTbl = GetXianghaoTableByIndex(目标图号, dstConn);
if (string.IsNullOrEmpty(fakXhTbl))
{
int xhSeq = GetFakMaxTableSeq(dstConn, tx, "xh") + 1;
fakXhTbl = "xh_" + xhSeq.ToString();
CreateFakXianghaoTable(fakXhTbl, dstConn, tx);
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('箱号', @th, '', @real);", dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", 目标图号);
cmd.Parameters.AddWithValue("@real", fakXhTbl);
cmd.ExecuteNonQuery();
}
}
int 箱号行数 = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM \"" + fakXhTbl + "\" WHERE 内部ID = @xh OR 箱号 = @xh;", dstConn, tx))
{
cmd.Parameters.AddWithValue("@xh", 目标箱号);
箱号行数 = Convert.ToInt32(cmd.ExecuteScalar());
}
if (箱号行数 == 0)
{
string srcXhTbl = GetXianghaoTableByIndex(_currentTuhao, srcConn);
if (!string.IsNullOrEmpty(srcXhTbl))
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM \"" + srcXhTbl + "\" WHERE 内部ID = @xh LIMIT 1;", srcConn))
{
cmd.Parameters.AddWithValue("@xh", 目标箱号);
using (var ad = new System.Data.SQLite.SQLiteDataAdapter(cmd))
{
var t = new DataTable();
ad.Fill(t);
foreach (DataRow r in t.Rows)
InsertFakXianghaoRow(r, fakXhTbl, dstConn, tx);
}
}
}
// (c) 方案库元件表:有则追加,无则建表+登记
string fakYjTbl = GetYuanjianTableByIndex(目标图号, 目标箱号, dstConn);
if (string.IsNullOrEmpty(fakYjTbl))
{
int yjSeq = GetFakMaxTableSeq(dstConn, tx, "yj") + 1;
fakYjTbl = "yj_" + yjSeq.ToString();
CreateFakYuanjianTable(fakYjTbl, dstConn, tx);
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES ('元件', @th, @xh, @real);", dstConn, tx))
{
cmd.Parameters.AddWithValue("@th", 目标图号);
cmd.Parameters.AddWithValue("@xh", 目标箱号);
cmd.Parameters.AddWithValue("@real", fakYjTbl);
cmd.ExecuteNonQuery();
}
}
foreach (var r in 元件DataRows)
InsertFakYuanjianRowDict(r, fakYjTbl, dstConn, tx);
yjCount = 元件DataRows.Count;
tx.Commit();
}
catch { try { tx.Rollback(); } catch { } throw; }
}
}
Log("[元件加入方案库] 完成: 元件行=" + yjCount);
ReloadFakData(); // ★ 通知方案窗口刷新
MessageBox.Show(this, "已把 " + yjCount + " 行元件加入方案库。",
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[元件加入方案库] 失败: " + ex.Message);
MessageBox.Show(this, "加入方案库失败:\n" + ex.Message,
"加入方案库", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private static string FakRead内部ID(DataRow xhRow)
{
if (xhRow.Table.Columns.Contains("内部ID"))
{
object v = xhRow["内部ID"];
if (v != DBNull.Value && !string.IsNullOrWhiteSpace(v.ToString())) return v.ToString();
}
if (xhRow.Table.Columns.Contains("箱号"))
{
object n2 = xhRow["箱号"];
if (n2 != DBNull.Value) return n2.ToString();
}
return "";
}
private static string ColValue(DataRow row, string colName)
{
if (row == null || string.IsNullOrEmpty(colName)) return "";
if (!row.Table.Columns.Contains(colName)) return "";
object v = row[colName];
if (v == null || v == DBNull.Value) return "";
return v.ToString();
}
/// <summary>
/// 把源行某列作为数值参数加入命令(REAL 列)。
/// 无法解析为 double 时写 0,避免类型不匹配。
/// </summary>
private static void AddNumericParam(System.Data.SQLite.SQLiteCommand cmd, string paramName,
DataRow row, string colName)
{
string s = ColValue(row, colName);
double d;
if (!double.TryParse(s, out d)) d = 0;
cmd.Parameters.AddWithValue(paramName, d);
}
// ---------- 箱号菜单事件 ----------
private void MenuXianghao_New_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId) ||
string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先选择图号,再新建箱号。",
"新建箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 不弹输入框,直接用默认名(新箱号N)创建
// 序号按当前图号的独立箱号表计数
// 记住当前图号,新建完成后恢复(防止 ADGV 选中抖动)
string rememberTuhao = _currentTuhao;
// ★ 记住选中行索引,用于"在选中行下面插入"
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView4);
string newXh = "新箱号" + GetXianghaoCount(_currentProjectId, _currentTuhao);
try
{
// 整个新建流程用 _isReloading 包裹,挡住所有 SelectionChanged 联动
_isReloading = true;
// 新建前先保存当前三个表的所有编辑,避免用户编辑丢失
// 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。
// 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。
SaveAllQuoteTablesIfDirty(false);
InsertXianghao(_currentProjectId, _currentTuhao, newXh);
// ★ 清除该图号的箱号表缓存,强制下次 Reload 从库读(看到刚 INSERT 的新箱号)
if (!string.IsNullOrEmpty(rememberTuhao)) _xianghaoCache.Remove(rememberTuhao);
ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao);
// ★ 把新行(在表末尾)移动到选中行下面
if (_xianghaoTable != null)
MoveLastRowBelowSelected(this.advancedDataGridView4, _xianghaoTable, selectedIndex, "\"" + _xianghaoTable.TableName + "\"", _currentProjectId);
// 新建后重算当前层并刷新显示
RecalcAllAfterReload();
}
catch (Exception ex)
{
MessageBox.Show(this, "新建箱号失败:\n" + ex.Message,
"新建箱号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// 恢复图号选中,并选中新箱号(新建的箱号会在表末尾)
_currentTuhao = rememberTuhao;
_currentXianghao = newXh;
SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao);
SelectRowByValue(this.advancedDataGridView4, "内部ID", newXh);
_isReloading = false;
// ★ 新建箱号后,元件表必须跳到这个新箱号(显示它里面的元件,新建的箱号默认空)
// SelectRowByValue 在 guard 里没触发 SelectionChanged 联动,这里显式刷新。
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, newXh);
}
}
private void MenuXianghao_Delete_Click(object sender, EventArgs e)
{
// 取所有选中行(支持批量多选)
var rows = GetAllSelectedRows(this.advancedDataGridView4);
Log("删除箱号: GetAllSelectedRows 行数=" + (rows == null ? "null" : rows.Count.ToString()) +
" SelectedCells=" + (this.advancedDataGridView4 == null ? "null" : this.advancedDataGridView4.SelectedCells.Count.ToString()) +
" SelectedRows=" + (this.advancedDataGridView4 == null ? "null" : this.advancedDataGridView4.SelectedRows.Count.ToString()));
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先选择要删除的箱号。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 收集所有选中行的箱号内部ID(去重) ★ 用内部ID,用户改名不影响删除
var xhs = new System.Collections.Generic.List<string>();
// ★ 同时记显示名(删除时按显示名兜底清扒图 Form3 登记的孤儿,2026-09-21 残留根治)
var xhNames = new System.Collections.Generic.List<string>();
foreach (var r in rows)
{
string xh = GetBoundValueAsString(r, "内部ID");
string nm = GetBoundValueAsString(r, "箱号");
if (!string.IsNullOrWhiteSpace(xh) && !xhs.Contains(xh))
{
xhs.Add(xh);
xhNames.Add(nm ?? "");
}
}
if (xhs.Count == 0)
{
MessageBox.Show(this, "选中行没有箱号,无法删除。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 记住当前图号,删除后恢复选中
string rememberTuhao = _currentTuhao;
// 不弹确认框,直接删除
try
{
// 整个删除流程用 _isReloading 包裹,挡住选中抖动
_isReloading = true;
// 删除前先保存当前三个表的所有编辑
// 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。
// 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。
SaveAllQuoteTablesIfDirty(false);
// ★ 删除前快照整库,支持右键「撤销删除」还原
PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false);
// ★ 删除管线放后台线程:单连接+单事务
string pid = _currentProjectId, th = _currentTuhao;
// 当前图号显示名(UI线程解析,兜底清理用)
string thNm = th;
try
{
if (_tuhaoTable != null)
{
foreach (DataRow dr in _tuhaoTable.Rows)
{
if (Convert.ToString(dr["内部ID"]) == th)
{
thNm = Convert.ToString(dr["图号"]);
break;
}
}
}
}
catch { }
var sw = System.Diagnostics.Stopwatch.StartNew();
Cursor = Cursors.WaitCursor;
SetStatus("正在删除 " + xhs.Count + " 个箱号...");
Log("[删除箱号] 开始,共 " + xhs.Count + " 个箱号...");
System.Threading.Tasks.Task.Run(() =>
{
Exception 错 = null;
try
{
string dbPath = GetQuoteDbPath(pid);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
for (int xi = 0; xi < xhs.Count; xi++)
{
string xh = xhs[xi];
string xhNm = xi < xhNames.Count ? xhNames[xi] : "";
DeleteXianghaoTx(pid, th, thNm, xh, xhNm, conn, tx);
_yuanjianCache.Remove(th + "\n" + xh);
if (!string.IsNullOrEmpty(xhNm)) _yuanjianCache.Remove(th + "\n" + xhNm);
}
tx.Commit();
}
}
}
catch (Exception exBg) { 错 = exBg; }
BeginInvoke((MethodInvoker)delegate
{
try
{
_isReloading = true;
if (错 == null)
{
string xhTbl = GetXianghaoTableByIndex(pid, th);
if (!string.IsNullOrEmpty(xhTbl))
RenumberTable(pid, "\"" + xhTbl + "\"");
_xianghaoCache.Remove(th);
}
ReloadXianghaoByTuhao(pid, th);
RecalcAllAfterReload();
sw.Stop();
Cursor = Cursors.Default;
if (错 != null)
{
Log("[删除箱号] 后台异常,完整堆栈: " + 错.ToString());
MessageBox.Show(this, "删除箱号失败:\n" + 错.Message,
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SetStatus("删除完成 " + xhs.Count + " 个箱号, 耗时 " + sw.ElapsedMilliseconds + "ms");
Log("[删除箱号] 完成,数量=" + xhs.Count + ",耗时 " + sw.ElapsedMilliseconds + "ms");
}
finally
{
_currentTuhao = rememberTuhao;
SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao);
_isReloading = false;
}
});
});
}
catch (Exception ex)
{
Cursor = Cursors.Default;
_isReloading = false;
MessageBox.Show(this, "删除箱号失败:\n" + ex.Message,
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 元件菜单事件 ----------
private void MenuYuanjian_New_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId) ||
string.IsNullOrWhiteSpace(_currentTuhao) ||
string.IsNullOrWhiteSpace(_currentXianghao))
{
MessageBox.Show(this, "请先选择箱号,再新建元件。",
"新建元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 批量插入分发(2026-10-01,用户决策):多选箱号时弹确认——
// [全部插入]每个选中箱号各插一条(序号独立) / [取消]不插入
// null=单选→继续走下面原逻辑;非null=已批量完成(或取消)→到此为止
// ★ 防御(2026-10-01):分发在 try 外,异常曾被静默吞(表现为"点了没反应不弹窗")——
// 这里单独包住:异常记日志+提示,并降级走原单箱号逻辑,保证功能可用
try
{
var 批量结果 = 批量插入到选中箱号((th, xh) =>
InsertYuanjian(_currentProjectId, th, xh,
"新元件" + GetYuanjianCount(_currentProjectId, th, xh)));
if (批量结果 != null) return;
}
catch (Exception exBatch)
{
Log("[批量插入] 分发异常(降级为单箱号新建): " + exBatch);
MessageBox.Show(this, "批量插入功能异常,已改为只插入当前箱号。\n异常信息:\n" + exBatch.Message,
"批量插入", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
// 不弹输入框,直接用默认名(新元件N)创建
// 序号按当前箱号的独立元件表计数(不再跨箱号混计)
// 记住当前图号/箱号,新建完成后恢复(防止 ADGV 选中抖动导致状态错乱)
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
// ★ 记住选中行索引,用于"在选中行下面插入"
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
string newName = "新元件" + GetYuanjianCount(_currentProjectId, _currentTuhao, _currentXianghao);
try
{
// 整个新建流程用 _isReloading 包起来,挡住所有 SelectionChanged 联动
// (防止 ReloadYuanjianByXianghao 后 ADGV 选中抖动切到别的箱号/图号)
_isReloading = true;
// 新建前先保存当前三个表的所有编辑,避免用户编辑丢失
// 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。
// 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。
SaveAllQuoteTablesIfDirty(false);
Log("新建元件: InsertYuanjian project=[" + _currentProjectId + "] 图号=[" + _currentTuhao + "] 箱号=[" + _currentXianghao + "] 名=[" + newName + "]");
InsertYuanjian(_currentProjectId, _currentTuhao, _currentXianghao, newName);
// ★ 清除该箱号的元件表缓存,强制下次 Reload 从库读(看到刚 INSERT 的新元件)
string yjKeyNew = _currentTuhao + "\n" + _currentXianghao;
_yuanjianCache.Remove(yjKeyNew);
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
Log("新建元件: Reload后元件表行数=" + (_yuanjianTable == null ? "null" : _yuanjianTable.Rows.Count.ToString()));
// ★ 把新行(在表末尾)移动到选中行下面
if (_yuanjianTable != null)
MoveLastRowBelowSelected(this.advancedDataGridView5, _yuanjianTable, selectedIndex, "\"" + _yuanjianTable.TableName + "\"", _currentProjectId);
// 新建后重算当前层并刷新显示
RecalcAllAfterReload();
// ★ 重新绑定 DataSource 后 Format/ValueType 会丢失,需要重新应用
ApplyPriceFormat(this.advancedDataGridView5, _单台小数位文本);
Log("新建元件: RecalcAfterReload完成");
}
catch (Exception ex)
{
MessageBox.Show(this, "新建元件失败:\n" + ex.Message,
"新建元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// 恢复图号/箱号选中,再解除 guard
_currentTuhao = rememberTuhao;
_currentXianghao = rememberXianghao;
SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao);
SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao);
_isReloading = false;
}
}
private void MenuYuanjian_Delete_Click(object sender, EventArgs e)
{
// 取所有选中行(支持批量多选)
var rows = GetAllSelectedRows(this.advancedDataGridView5);
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先选择要删除的元件。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 合并视图分支(2026-10-01):多选箱号联动时元件表是多箱号合集,
// 序号列显示"箱号-N"且各行属于不同物理元件表——不能按 _currentXianghao+序号 删,
// 按每行的"_来源表名_+rowid/内部ID"行级来源删(与编辑直写同一套定位)
bool isMergedYj = _yuanjianTable != null &&
string.IsNullOrEmpty(_yuanjianTable.TableName) &&
_yuanjianTable.Columns.Contains("_来源表名_");
if (isMergedYj)
{
删除合并视图选中元件(rows);
return;
}
// 收集所有选中行的序号
var seqs = new System.Collections.Generic.List<string>();
foreach (var r in rows)
{
string s = GetCellValueAsString(r, "序号");
if (!string.IsNullOrWhiteSpace(s) && !seqs.Contains(s)) seqs.Add(s);
}
if (seqs.Count == 0)
{
MessageBox.Show(this, "选中行没有序号,无法删除。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 记住当前图号/箱号,删除后恢复选中
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
// 不弹确认框,直接删除
try
{
// 整个删除流程用 _isReloading 包裹,挡住选中抖动
_isReloading = true;
// 删除前先保存当前三个表的所有编辑
// 操作前预保存:用 force=false,只在有未保存改动时才写盘(避免冗余全表覆盖)。
// 后续 RecalcAllAfterReload 结尾会用 force 行为再存一次重算结果,数据不会丢。
SaveAllQuoteTablesIfDirty(false);
// ★ 删除前快照整库,支持右键「撤销删除」还原
PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false);
// ★ 批量模式:开关=1 跳过触发器(删除行无需重算派生值),提交后统一刷聚合
using (var connSw = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId))))
{
connSw.Open();
}
// 批量删除所有选中元件
foreach (string seq in seqs)
{
DeleteYuanjian(_currentProjectId, _currentTuhao, _currentXianghao, seq);
}
Log("批量删除元件: 数量=" + seqs.Count);
// 删除后重新编号该箱号的元件表(让序号连续 1、2、3...)
string yjTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, _currentXianghao);
if (!string.IsNullOrEmpty(yjTbl))
RenumberTable(_currentProjectId, "\"" + yjTbl + "\"");
// ★ 恢复触发器前,补刷该箱号行与图号行聚合(删除后 SUM 变化)
using (var connSw = new System.Data.SQLite.SQLiteConnection(GetConnString(GetQuoteDbPath(_currentProjectId))))
{
connSw.Open();
using (var txSw = connSw.BeginTransaction())
{
计算引擎.刷新箱号行聚合(connSw, txSw, GetQuoteDbPath(_currentProjectId), _currentTuhao, _currentXianghao, TuhaoTableName);
计算引擎.刷新图号行聚合(connSw, txSw, GetQuoteDbPath(_currentProjectId), _currentTuhao, TuhaoTableName);
txSw.Commit();
}
}
// ★ 清除该箱号的元件表缓存,强制下次 Reload 从库读(看到删除后的结果)
string yjKeyDel = _currentTuhao + "\n" + _currentXianghao;
_yuanjianCache.Remove(yjKeyDel);
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
// 删除后重算,更新箱号行(成本价/利润)、图号行(总价/毛利)、项目表
RecalcAllAfterReload();
}
catch (Exception ex)
{
MessageBox.Show(this, "删除元件失败:\n" + ex.Message,
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_currentTuhao = rememberTuhao;
_currentXianghao = rememberXianghao;
SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao);
SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao);
_isReloading = false;
}
}
/// <summary>
/// ★ 合并视图删除(2026-10-01):多选箱号时元件表是多箱号合集,
/// 每个选中行按"_来源表名_ + rowid(内部ID优先)"删自己物理表里的行,
/// 涉及的物理表逐个重编号,最后全库重算 + 按箱号表当前选中重建合并视图。
/// </summary>
private void 删除合并视图选中元件(System.Collections.Generic.List<DataGridViewRow> rows)
{
try
{
// 删除流程用 _isReloading 包裹,挡住选中抖动
_isReloading = true;
// 1) 合并视图的编辑全走直写队列,删除前强制落库(否则残留编辑随行一起丢)
直写队列强制落库();
// 2) 删除前快照整库,支持右键「撤销删除」还原
PushUndoSnapshot(GetQuoteDbPath(_currentProjectId), _currentProjectId, false);
// 3) 收集选中行的行级来源(来源表名/rowid/内部ID),跳过来源不全的行
var targets = new System.Collections.Generic.List<string[]>(); // {表名,rowid,内部ID}
foreach (var r in rows)
{
if (r == null || r.IsNewRow) continue;
string tbl = GetBoundValueAsString(r, "_来源表名_");
if (string.IsNullOrWhiteSpace(tbl)) continue;
long rid = 0;
long.TryParse(GetBoundValueAsString(r, "_rowid_"), out rid);
string nid = GetBoundValueAsString(r, "内部ID");
if (rid <= 0 && string.IsNullOrWhiteSpace(nid)) continue;
targets.Add(new[] { tbl, rid.ToString(), nid });
}
if (targets.Count == 0)
{
MessageBox.Show(this, "选中行没有来源箱号信息,无法删除。\n请改在单个箱号下删除。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 4) 事务批量删除:内部ID优先定位,rowid 兜底(与直写同一套定位体系)
string dbPath = GetQuoteDbPath(_currentProjectId);
int deleted = 0;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
foreach (var t in targets)
{
string q = "\"" + t[0].Replace("\"", "") + "\"";
int n;
if (!string.IsNullOrWhiteSpace(t[2]))
{
using (var del = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + q + " WHERE 内部ID=@nid;", conn, tx))
{
del.Parameters.AddWithValue("@nid", t[2]);
n = del.ExecuteNonQuery();
}
// 内部ID没删到(理论上不会)→rowid 兜底
if (n == 0 && long.Parse(t[1]) > 0)
{
using (var del = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + q + " WHERE rowid=@rid;", conn, tx))
{
del.Parameters.AddWithValue("@rid", long.Parse(t[1]));
n = del.ExecuteNonQuery();
}
}
}
else
{
using (var del = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + q + " WHERE rowid=@rid;", conn, tx))
{
del.Parameters.AddWithValue("@rid", long.Parse(t[1]));
n = del.ExecuteNonQuery();
}
}
deleted += n;
}
tx.Commit();
}
// 5) 涉及的物理表逐个重编号(让各箱号序号连续 1、2、3...)
var doneTbls = new System.Collections.Generic.HashSet<string>();
foreach (var t in targets)
{
if (!doneTbls.Add(t[0])) continue;
try { RenumberTable(_currentProjectId, "\"" + t[0].Replace("\"", "") + "\""); }
catch (Exception exRn) { Log("[合并删除] 重编号 " + t[0] + " 失败: " + exRn.Message); }
}
}
Log("[合并删除] 选中=" + rows.Count + " 实删=" + deleted +
" 涉及表=" + new System.Collections.Generic.HashSet<string>(
System.Linq.Enumerable.Select(targets, t => t[0])).Count);
// 6) 涉及多个箱号,元件缓存全清 + 全库重算(刷全部箱号/图号/项目聚合)
_yuanjianCache.Clear();
_xianghaoCache.Clear();
RecalcAllFromDatabase(_currentProjectId);
// ★ 刷新修复(2026-10-01):库聚合完成后,按内部ID对齐从库刷新
// 箱号表/图号表价格列显示(合并视图也支持)——否则删除后界面价格仍是旧值
刷新箱号表显示从库();
// 7) 按箱号表当前选中重建合并视图(与 SelectionChanged 联动同一套收集逻辑)
重建元件合并视图();
}
catch (Exception ex)
{
MessageBox.Show(this, "删除元件失败:\n" + ex.Message,
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_isReloading = false;
}
}
/// <summary>
/// ★ 收集箱号表(advancedDataGridView4)当前选中的箱号列表(2026-10-01):
/// 返回 [来源图号, 箱号内部ID, 箱号显示名] 三元组列表(按行序,含 CurrentRow 保底)。
/// 合并视图按行级 _来源图号_ 取图号;普通视图统一用 _currentTuhao。
/// </summary>
private System.Collections.Generic.List<string[]> 收集选中箱号列表()
{
var pairs = new System.Collections.Generic.List<string[]>();
var dgv4 = this.advancedDataGridView4;
if (dgv4 == null) return pairs;
bool mergedView = _xianghaoTable != null &&
string.IsNullOrEmpty(_xianghaoTable.TableName) &&
_xianghaoTable.Columns.Contains("_来源图号_");
var rowSet = new System.Collections.Generic.SortedSet<int>();
foreach (DataGridViewCell c in dgv4.SelectedCells)
{
if (c != null && c.RowIndex >= 0) rowSet.Add(c.RowIndex);
}
if (dgv4.CurrentRow != null && dgv4.CurrentRow.Index >= 0) rowSet.Add(dgv4.CurrentRow.Index);
foreach (int idx in rowSet)
{
if (_xianghaoTable == null || idx >= _xianghaoTable.Rows.Count) continue;
string thSrc = mergedView
? Convert.ToString(_xianghaoTable.Rows[idx]["_来源图号_"])
: _currentTuhao;
string xhId = Convert.ToString(_xianghaoTable.Rows[idx]["内部ID"]);
string xhName = Convert.ToString(_xianghaoTable.Rows[idx]["箱号"]);
if (!string.IsNullOrWhiteSpace(xhId))
pairs.Add(new[] { thSrc, xhId, xhName });
}
return pairs;
}
/// <summary>批量插入记忆设置(2026-10-01):不再询问=true 时不再弹窗,直接全部插入。
/// (用户已删"仅当前行"按钮,记忆只剩"不再询问"一个维度,旧配置里的选择字段被忽略)</summary>
private static bool _批量插入设置已加载 = false;
private static bool _批量插入不再询问 = false;
/// <summary>批量插入记忆配置路径(exe 目录,简单 XML,删除文件即重置)。</summary>
private static string 批量插入设置路径()
{
return System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "批量插入设置.xml");
}
private static void 加载批量插入设置()
{
if (_批量插入设置已加载) return;
_批量插入设置已加载 = true;
try
{
string f = 批量插入设置路径();
if (!System.IO.File.Exists(f)) return;
string xml = System.IO.File.ReadAllText(f, System.Text.Encoding.UTF8);
// 简易标签值提取:<标签>值</标签>(配置只有本方法写入,格式固定)
string 开 = "<不再询问>", 关 = "</不再询问>";
int i = xml.IndexOf(开, System.StringComparison.Ordinal);
if (i < 0) return;
int j = xml.IndexOf(关, i + 开.Length, System.StringComparison.Ordinal);
if (j < 0) return;
_批量插入不再询问 = xml.Substring(i + 开.Length, j - i - 开.Length).Trim() == "1";
}
catch { }
}
private static void 保存批量插入设置()
{
try
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
sb.AppendLine("<!-- 批量插入元件记忆设置:不再询问=1 不再弹窗直接全部插入。删除本文件恢复弹窗 -->");
sb.AppendLine("<批量插入>");
sb.AppendLine(" <不再询问>" + (_批量插入不再询问 ? "1" : "0") + "</不再询问>");
sb.AppendLine("</批量插入>");
System.IO.File.WriteAllText(批量插入设置路径(), sb.ToString(), System.Text.Encoding.UTF8);
}
catch { }
}
/// <summary>
/// ★ 弹"批量插入元件"确认框(2026-10-01,"仅当前行"按钮已按用户要求删除):
/// [全部插入]=插入所有选中箱号 / [取消]=不插入。
/// 复选框"以后不再询问"勾选且点了[全部插入]时记忆(持久化,删除
/// 安装目录"批量插入设置.xml"可恢复弹窗)。
/// ★ 窗体布局在 xiangmu\批量插入询问.Designer.cs——可用 SD 设计器打开微调。
/// 返回: 0=全部插入 -1=取消;out 不再询问=复选框勾选状态。
/// </summary>
private int 弹批量插入询问(string 消息, out bool 不再询问)
{
using (var f = new 批量插入询问 { 消息 = 消息 })
{
var r = f.ShowDialog(this);
不再询问 = f.不再询问;
if (r == DialogResult.Yes) return 0;
return -1;
}
}
/// <summary>
/// ★ 多选箱号批量插入分发(2026-10-01,用户决策:批量插入所有选中箱号;
/// 同日按用户要求删除"仅当前行"按钮,只剩 [全部插入]/[取消])。
/// 箱号表多选(>1)时弹确认框;勾选"以后不再询问"会持久化,之后一直
/// 全部插入不再弹窗(删安装目录"批量插入设置.xml"可恢复)。
/// 单箱号插入动作收到 (图号, 箱号内部ID),自行完成库 INSERT(纯库操作,勿带 UI 副作用)。
/// 返回约定:
/// null = 单选→调用方走原单箱号逻辑
/// 空列表 = 用户取消→调用方直接 return(什么都不插)
/// 非空列表 = 已批量插入完成(缓存/重算/合并视图收口已做)→调用方 return
/// </summary>
private System.Collections.Generic.List<string> 批量插入到选中箱号(
System.Action<string, string> 单箱号插入)
{
var pairs = 收集选中箱号列表();
// ★ 诊断日志(2026-10-01):排障"点了新建元件没反应不弹窗"
Log("[批量插入] 分发入口: 选中箱号数=" + pairs.Count + " 不再询问=" + _批量插入不再询问);
if (pairs.Count <= 1) return null; // 单选:调用方原逻辑
string names = "";
foreach (var p in pairs)
{
if (names.Length > 0) names += "、";
names += p[2];
if (names.Length > 200) { names += "…"; break; }
}
// ★ 记忆设置:不再询问时直接全部插入
加载批量插入设置();
if (!_批量插入不再询问)
{
bool 不再询问;
Log("[批量插入] 即将弹出询问框: 箱号数=" + pairs.Count);
int sel = 弹批量插入询问(
"当前选中 " + pairs.Count + " 个箱号:\n" + names + "\n\n" +
"[全部插入] 插入到全部 " + pairs.Count + " 个箱号\n" +
"[取消] 不插入",
out 不再询问);
if (sel != 0) return new System.Collections.Generic.List<string>(); // 取消
// 勾选"以后不再询问"→持久化([取消]不记忆)
if (不再询问)
{
_批量插入不再询问 = true;
保存批量插入设置();
Log("[批量插入] 已记忆选择(不再询问,始终全部插入)");
}
}
else
{
Log("[批量插入] 沿用记忆设置: 全部选中箱号");
}
// 批量插入全部选中箱号
try
{
_isReloading = true;
直写队列强制落库(); // 合并视图残留编辑先落库
int ok = 0;
foreach (var p in pairs)
{
try { 单箱号插入(p[0], p[1]); ok++; }
catch (Exception exIns) { Log("[批量插入] 箱号[" + p[2] + "] 插入失败: " + exIns.Message); }
}
// 收口:全清缓存+全库重算+从库刷价格显示+重建合并视图(与删除合并元件同款)
_yuanjianCache.Clear();
_xianghaoCache.Clear();
RecalcAllFromDatabase(_currentProjectId);
刷新箱号表显示从库();
重建元件合并视图();
Log("[批量插入] 已插入 " + ok + "/" + pairs.Count + " 个箱号: " + names);
return pairs.ConvertAll(p => p[2]);
}
finally
{
_isReloading = false;
}
}
/// <summary>
/// ★ 工具窗口批量插入(2026-10-01):用户库/柜体库/母排/GCS铜排/箱体计算/方案库等
/// 内存表型插入入口的通用批量包装——多选箱号时弹确认,对每个选中箱号
/// 依次"切换当前箱号→装载该箱号内存表→执行原插入动作(NewRow+收尾)"。
/// 插入动作 = 原入口的 确保元件表存在→NewRow→元件表插行收尾 全流程,
/// 动作内引用 _currentTuhao/_currentXianghao 的逻辑自动跟随切换。
/// 返回: false=单选→调用方走原逻辑;true=已批量完成(或用户取消)→调用方直接 return。
/// </summary>
private bool 工具窗口批量插入(System.Action 插入动作)
{
var pairs = 收集选中箱号列表();
if (pairs.Count <= 1) return false; // 单选:调用方原逻辑
// 记忆设置:不再询问时直接全部插入
加载批量插入设置();
if (!_批量插入不再询问)
{
string names = "";
foreach (var p in pairs)
{
if (names.Length > 0) names += "、";
names += p[2];
if (names.Length > 200) { names += "…"; break; }
}
bool 不再询问;
Log("[批量插入] 工具窗口即将弹询问框: 箱号数=" + pairs.Count);
int sel = 弹批量插入询问(
"当前选中 " + pairs.Count + " 个箱号:\n" + names + "\n\n" +
"[全部插入] 插入到全部 " + pairs.Count + " 个箱号\n" +
"[取消] 不插入",
out 不再询问);
if (sel != 0) return true; // 用户取消:调用方直接 return
if (不再询问)
{
_批量插入不再询问 = true;
保存批量插入设置();
Log("[批量插入] 已记忆选择(不再询问,始终全部插入)");
}
}
else
{
Log("[批量插入] 沿用记忆设置: 全部选中箱号");
}
// 批量执行:记住上下文,逐箱号切换→装载内存表→执行插入动作
string memTh = _currentTuhao, memXh = _currentXianghao;
_isReloading = true;
try
{
直写队列强制落库(); // 合并视图残留编辑先落库
int ok = 0;
foreach (var p in pairs)
{
try
{
_currentTuhao = p[0];
_currentXianghao = p[1];
// 强制装载该箱号的内存表(插入动作里的"确保元件表存在"见 TableName
// 非空会跳过重载,所以这里必须先清缓存+Reload 再执行动作)
_yuanjianCache.Remove(p[0] + "\n" + p[1]);
ReloadYuanjianByXianghao(_currentProjectId, p[0], p[1]);
if (_yuanjianTable == null || string.IsNullOrEmpty(_yuanjianTable.TableName))
throw new Exception("箱号[" + p[2] + "]元件表装载失败");
插入动作();
ok++;
}
catch (Exception exIns)
{
Log("[批量插入] 箱号[" + p[2] + "] 插入失败: " + exIns.Message);
}
}
Log("[批量插入] 工具窗口已插入 " + ok + "/" + pairs.Count + " 个箱号");
}
finally
{
// 收口:恢复上下文+全清缓存+全库重算+刷价格显示+重建合并视图
_currentTuhao = memTh;
_currentXianghao = memXh;
_yuanjianCache.Clear();
_xianghaoCache.Clear();
try
{
RecalcAllFromDatabase(_currentProjectId);
刷新箱号表显示从库();
重建元件合并视图();
}
catch (Exception exEnd) { Log("[批量插入] 收口异常: " + exEnd.Message); }
_isReloading = false;
}
return true;
}
/// <summary>
/// ★ G1-1(2026-10-01):功能区「模板费用」入口——弹费用模板管理窗。
/// </summary>
private void 打开费用模板窗口()
{
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在项目列表双击打开一个项目,再使用模板费用。",
"模板费用", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var f = new 费用模板(this))
{
f.ShowDialog(this);
}
}
/// <summary>
/// ★ G1-1(2026-10-01):把启用的费用模板换算成费用行写入当前项目每个箱号的元件表。
/// 费用行=类型'费用'的普通元件行(表价=金额;计入'仅报出'时采购系数=0 → 公式天然算出采购价0),
/// 参与箱号/图号/项目聚合且全库重算稳定。重复应用:先删同模板名旧行再插(幂等)。
/// </summary>
internal string 应用费用模板到当前项目(System.Collections.Generic.List<费用模板.模板行> 模板s)
{
if (string.IsNullOrWhiteSpace(_currentProjectId)) throw new Exception("未打开项目");
string dbPath = GetQuoteDbPath(_currentProjectId);
if (!System.IO.File.Exists(dbPath)) throw new Exception("项目库不存在: " + dbPath);
int 写入箱号 = 0, 写入行 = 0;
var 明细 = new System.Text.StringBuilder(); // 每箱号费用金额(应用结果报告)
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ 费用行置底配套(2026-10-01):老元件表可能没有"类型"列,幂等补列
// (INSERT 列清单与"删除旧费用行"都依赖该列)
{
var yj表s = new System.Collections.Generic.List<string>();
using (var selT = new System.Data.SQLite.SQLiteCommand(
"SELECT DISTINCT 实际表名 FROM 表索引 WHERE 类型='元件';", conn))
using (var rdT = selT.ExecuteReader())
while (rdT.Read())
{
string t = rdT.IsDBNull(0) ? "" : rdT.GetString(0).Trim();
if (!string.IsNullOrWhiteSpace(t)) yj表s.Add(t);
}
foreach (string t in yj表s)
EnsureColumns(conn, "\"" + t + "\"", new[] { "类型", "费用公式" });
}
using (var tx = conn.BeginTransaction())
{
// 全部图号(内部ID)
var tuhaos = new System.Collections.Generic.List<string>();
using (var selTh = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM " + TuhaoTableName + " WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx))
using (var rdTh = selTh.ExecuteReader())
while (rdTh.Read()) tuhaos.Add(rdTh.GetString(0));
foreach (string th in tuhaos)
{
// 该图号全部箱号(内部ID)
string xhTbl = GetXianghaoTableByIndex(th, conn);
if (string.IsNullOrEmpty(xhTbl)) continue;
var xhs = new System.Collections.Generic.List<string>();
using (var selXh = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM \"" + xhTbl + "\" WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx))
using (var rdXh = selXh.ExecuteReader())
while (rdXh.Read()) xhs.Add(rdXh.GetString(0));
foreach (string xh in xhs)
{
try
{
string yjTbl = GetYuanjianTableByIndex(th, xh, conn);
if (string.IsNullOrEmpty(yjTbl)) continue;
string q = "\"" + yjTbl + "\"";
// 1) 该箱号非费用元件的采购价/报出价合计(百分比基数;IFNULL 兼容类型列为 NULL 的老行)
decimal sumCg = 0m, sumBc = 0m;
using (var selYj2 = new System.Data.SQLite.SQLiteCommand(
"SELECT 采购价, 报出价 FROM " + q + " WHERE IFNULL(类型,'')<>'费用';", conn, tx))
using (var rdYj2 = selYj2.ExecuteReader())
{
while (rdYj2.Read())
{
sumCg += ParseDecimal(rdYj2.IsDBNull(0) ? "" : rdYj2.GetValue(0));
sumBc += ParseDecimal(rdYj2.IsDBNull(1) ? "" : rdYj2.GetValue(1));
}
}
// ★ 该箱号数量(台数)——费用行数量跟随台数,价格=表价×系数×数量 全走公式
// (改箱号台数后全库重算会自动联动费用行,不再写死)
decimal 台数 = 1m;
using (var selSl = new System.Data.SQLite.SQLiteCommand(
"SELECT 数量 FROM \"" + xhTbl + "\" WHERE 内部ID=@id;", conn, tx))
{
selSl.Parameters.AddWithValue("@id", xh);
object vSl = selSl.ExecuteScalar();
decimal sl = ParseDecimal(vSl == null || vSl == DBNull.Value ? "" : vSl.ToString());
if (sl > 0m) 台数 = sl;
}
// 2) 删同模板名旧费用行(幂等重应用)
foreach (var t in 模板s)
{
using (var del = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + q + " WHERE 类型='费用' AND 元件名称=@n;", conn, tx))
{
del.Parameters.AddWithValue("@n", t.名称);
del.ExecuteNonQuery();
}
}
// 3) 逐模板换算【单台金额】并 INSERT 费用行
// ★ 修复(2026-10-01 二次):完全公式化——INSERT 只写输入列(表价/系数/数量),
// 采购单价/采购价/报出单价/报出价 一律不写,交给收口的 RecalcAllFromDatabase
// 按统一公式(表价×系数×数量)算出——用户在元件表改费用行的表价/系数/数量也会联动
string seqBase = "SELECT IFNULL(MAX(CAST(序号 AS INTEGER)),0)+1 FROM " + q + ";";
var 本箱号费用明细 = new System.Text.StringBuilder();
decimal 本箱号费用合计 = 0m;
foreach (var t in 模板s)
{
// 单台金额:固定=数值;百分比=该箱号元件单台合计×%
decimal 单台金额;
if (t.方式 == "元件采购价×%") 单台金额 = decimal.Round(sumCg * t.数值 / 100m, 2);
else if (t.方式 == "元件报出价×%") 单台金额 = decimal.Round(sumBc * t.数值 / 100m, 2);
else 单台金额 = t.数值;
int seq;
using (var c = new System.Data.SQLite.SQLiteCommand(seqBase, conn, tx))
seq = Convert.ToInt32(c.ExecuteScalar());
using (var ins = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO " + q + " (序号, 元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数," +
" 品牌, 系列, 类型, 费用公式, 内部ID) VALUES " +
"(@seq,@n,@gg,'项',@sl,@bj,'1','1','','','费用',@gf,@nid);", conn, tx))
{
ins.Parameters.AddWithValue("@seq", seq.ToString());
ins.Parameters.AddWithValue("@n", t.名称);
// ★ 实时联动(2026-10-01):完整公式存专用隐藏列"费用公式"(表格不绑定该列,永不显示;
// 联动解析用,如 元件采购价×13%),"规格"列只显示纯数值(13%),备注保持空;
// 固定金额:规格空,费用公式=固定金额(表价不联动,数量×台数仍走公式)
string 数值文本 = t.数值.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
if (t.方式 == "固定金额(每柜)")
{
ins.Parameters.AddWithValue("@gg", "");
ins.Parameters.AddWithValue("@gf", "固定金额");
}
else
{
ins.Parameters.AddWithValue("@gg", 数值文本 + "%");
ins.Parameters.AddWithValue("@gf", t.方式.Substring(0, t.方式.Length - 1) + 数值文本 + "%");
}
ins.Parameters.AddWithValue("@sl", 台数.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture));
ins.Parameters.AddWithValue("@bj", (double)单台金额);
ins.Parameters.AddWithValue("@nid", System.Guid.NewGuid().ToString("N"));
ins.ExecuteNonQuery();
写入行++;
}
decimal 本行合计 = decimal.Round(单台金额 * 台数, 2);
本箱号费用明细.Append(t.名称 + "=" + 本行合计.ToString("0.##") + "(单台" + 单台金额.ToString("0.##") + "×" + 台数.ToString("0.##") + "台) ");
本箱号费用合计 += 本行合计;
}
写入箱号++;
明细.AppendLine("箱号[" + 取箱号显示名(conn, tx, xhTbl, xh) + "] 费用合计 " + 本箱号费用合计.ToString("0.##") + ": " + 本箱号费用明细);
}
catch (Exception exXh)
{
Log("[模板费用] 箱号[" + xh + "] 应用失败: " + exXh.Message);
}
}
}
tx.Commit();
}
}
// 收口:清缓存+全库重算+刷价格+重建视图(费用行参与全部聚合)
_yuanjianCache.Clear();
_xianghaoCache.Clear();
RecalcAllFromDatabase(_currentProjectId);
刷新箱号表显示从库();
重建元件合并视图();
Log("[模板费用] 已应用: 箱号" + 写入箱号 + "个 新费用行" + 写入行 + "条");
return "已应用到 " + 写入箱号 + " 个箱号,共写入 " + 写入行 + " 条费用行。\n" +
"各箱号成本/报出、图号总价、项目汇总已重算。\n\n各箱号费用明细:\n" + 明细;
}
/// <summary>费用模板应用报告用:按箱号内部ID取箱号显示名。</summary>
private static string 取箱号显示名(System.Data.SQLite.SQLiteConnection conn,
System.Data.SQLite.SQLiteTransaction tx, string xhTbl, string xhId)
{
try
{
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 箱号 FROM \"" + xhTbl + "\" WHERE 内部ID=@id;", conn, tx))
{
sel.Parameters.AddWithValue("@id", xhId);
object v = sel.ExecuteScalar();
string s = v == null || v == DBNull.Value ? "" : v.ToString().Trim();
return s == "" ? xhId : s;
}
}
catch { return xhId; }
}
/// <summary>★ G1-1(2026-10-01):删除当前项目所有箱号元件表里 类型='费用' 的行并重算。</summary>
internal string 清空项目费用行()
{
if (string.IsNullOrWhiteSpace(_currentProjectId)) throw new Exception("未打开项目");
string dbPath = GetQuoteDbPath(_currentProjectId);
int 删除 = 0;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
var tuhaos = new System.Collections.Generic.List<string>();
using (var selTh = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM " + TuhaoTableName + " WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx))
using (var rdTh = selTh.ExecuteReader())
while (rdTh.Read()) tuhaos.Add(rdTh.GetString(0));
foreach (string th in tuhaos)
{
string xhTbl = GetXianghaoTableByIndex(th, conn);
if (string.IsNullOrEmpty(xhTbl)) continue;
var xhs = new System.Collections.Generic.List<string>();
using (var selXh = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM \"" + xhTbl + "\" WHERE 内部ID IS NOT NULL AND 内部ID<>'';", conn, tx))
using (var rdXh = selXh.ExecuteReader())
while (rdXh.Read()) xhs.Add(rdXh.GetString(0));
foreach (string xh in xhs)
{
string yjTbl = GetYuanjianTableByIndex(th, xh, conn);
if (string.IsNullOrEmpty(yjTbl)) continue;
try
{
using (var del = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + yjTbl + "\" WHERE 类型='费用';", conn, tx))
删除 += del.ExecuteNonQuery();
}
catch (Exception exD) { Log("[模板费用] 清空箱号[" + xh + "]失败: " + exD.Message); }
}
}
tx.Commit();
}
}
_yuanjianCache.Clear();
_xianghaoCache.Clear();
RecalcAllFromDatabase(_currentProjectId);
刷新箱号表显示从库();
重建元件合并视图();
Log("[模板费用] 已清空费用行 " + 删除 + " 条");
return "已删除 " + 删除 + " 条费用行,价格已重算。";
}
/// <summary>
/// ★ 按箱号表(advancedDataGridView4)当前选中重建元件表视图:
/// 多选→ReloadYuanjianByXianghaos 合并视图;单选→ReloadYuanjianByXianghao 原路径。
/// (删除/刷新后调用,保持与 SelectionChanged 联动一致的显示规则)
/// </summary>
private void 重建元件合并视图()
{
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
var dgv4 = this.advancedDataGridView4;
if (dgv4 == null) return;
// ★ 兜底:CurrentRow 丢失(理论上不会)时按 _currentXianghao 单箱号重载,保证元件表必刷新
if (dgv4.CurrentRow == null || dgv4.CurrentRow.Index < 0)
{
if (!string.IsNullOrWhiteSpace(_currentXianghao))
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
return;
}
var pairs = 收集选中箱号列表();
if (pairs.Count > 1)
{
ReloadYuanjianByXianghaos(_currentProjectId, pairs);
}
else
{
int idx = dgv4.CurrentRow.Index;
string xhId = (_xianghaoTable != null && idx < _xianghaoTable.Rows.Count)
? Convert.ToString(_xianghaoTable.Rows[idx]["内部ID"]) : _currentXianghao;
bool mergedView = _xianghaoTable != null &&
string.IsNullOrEmpty(_xianghaoTable.TableName) &&
_xianghaoTable.Columns.Contains("_来源图号_");
string thSrc = (mergedView && _xianghaoTable != null && idx < _xianghaoTable.Rows.Count)
? Convert.ToString(_xianghaoTable.Rows[idx]["_来源图号_"]) : _currentTuhao;
if (!string.IsNullOrWhiteSpace(xhId))
ReloadYuanjianByXianghao(_currentProjectId, thSrc, xhId);
}
}
// ============================================================
// 元件排序(button33):弹出「元件排序」窗体(xiangmu\元件排序.cs,设计器制作)。
// 窗体内:左侧箱号列表 + 右侧元件列表,上移/下移/置顶/置底 手动排序,
// 点「确定」由窗体把新顺序写回数据库。这里只负责:
// 1) 校验已打开项目,先把内存编辑落盘
// 2) 传上下文(项目编号/当前图号/当前箱号)给窗体
// 3) 窗体关闭后清缓存刷新报价页,恢复选中
// ============================================================
/// <summary>button33 点击:打开元件排序窗体。</summary>
private void button33_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在项目列表双击打开一个项目,再对元件排序。",
"元件排序", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 记住当前选中,排序窗体关闭后恢复
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
try
{
// 排序前把内存未保存的编辑落盘,保证窗体读到最新数据
SaveAllQuoteTablesIfDirty(false);
using (var dlg = new 元件排序(_currentProjectId, rememberTuhao, rememberXianghao))
{
dlg.ShowDialog(this);
}
}
catch (Exception ex)
{
Log("[元件排序] 失败: " + ex.Message);
MessageBox.Show(this, "打开元件排序失败:\n" + ex.Message,
"元件排序", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// 窗体可能改了元件顺序 → 清缓存,刷新报价页,恢复原选中
try
{
_isReloading = true;
_yuanjianCache.Clear();
if (!string.IsNullOrEmpty(rememberTuhao))
{
ReloadTuhaoByProject(_currentProjectId);
RecalcAllAfterReload();
}
}
finally
{
_currentTuhao = rememberTuhao;
_currentXianghao = rememberXianghao;
SelectRowByValue(this.advancedDataGridView9, "内部ID", rememberTuhao);
SelectRowByValue(this.advancedDataGridView4, "内部ID", rememberXianghao);
_isReloading = false;
}
}
}
// ============================================================
// 加入用户库(右键菜单:把元件表选中行加入 advancedDataGridView7 用户库表)
// ============================================================
/// <summary>
/// 用户库数据库路径(共享库,所有项目通用,存所有可用元件)。
/// 与 ui.cs 的 LoadSqliteData_advancedDataGridView7 一致。
/// </summary>
private string UserLibDbPath
{
get { return GetDbFullPath(@"sjk\advancedDataGridView7Table.db"); }
}
private const string UserLibTableName = "advancedDataGridView7Table";
/// <summary>
/// 右键"加入用户库":把元件表(advancedDataGridView5)中选中的行插入到用户库。
/// </summary>
private void MenuYuanjian_AddToUserLib_Click(object sender, EventArgs e)
{
AddRowsToLib("用户库", UserLibDbPath, UserLibTableName, () => ReloadUserLibFromDatabase());
}
/// <summary>
/// 右键"加入柜体库":把元件表(advancedDataGridView5)中选中的行插入到柜体库。
/// 只插入"类型"为"壳体"的行,非壳体行跳过并提示。
/// </summary>
private void MenuYuanjian_AddToGuiTi_Click(object sender, EventArgs e)
{
// 先取选中行,过滤出类型="壳体"的
var allRows = GetAllSelectedRows(this.advancedDataGridView5);
if (allRows == null || allRows.Count == 0)
{
MessageBox.Show(this, "请先在元件表中选择要加入柜体库的行。",
"加入柜体库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
int skipped = 0;
var shellRows = new System.Collections.Generic.List<DataGridViewRow>();
foreach (var r in allRows)
{
string 类型 = GetCellValueAsString(r, "类型");
if (string.Equals(类型, "壳体", System.StringComparison.Ordinal))
{
shellRows.Add(r);
}
else
{
skipped++;
}
}
if (shellRows.Count == 0)
{
MessageBox.Show(this,
"选中的 " + allRows.Count + " 行中,没有类型为「壳体」的元件。\n" +
"柜体库只接收类型为「壳体」的元件。",
"加入柜体库", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 把过滤后的行传给通用插入方法
AddRowsToLibFiltered("柜体库", GuiTiDbPath, GuiTiTableName, () => ReloadGuiTiFromDatabase(), shellRows);
if (skipped > 0)
{
Log("[加入柜体库] 跳过 " + skipped + " 行(非壳体类型)");
}
}
/// <summary>
/// 通用:把指定的行列表插入到目标库(不做选中行检查,由调用方过滤后传入)。
/// 用于"加入柜体库"(只插入壳体类型的行)。
/// </summary>
private void AddRowsToLibFiltered(string libName, string dbPath, string tableName,
System.Action reloadAction, System.Collections.Generic.IList<DataGridViewRow> rows)
{
if (!System.IO.File.Exists(dbPath))
{
MessageBox.Show(this, "找不到" + libName + "数据库文件:\n" + dbPath,
"加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try { this.advancedDataGridView5.EndEdit(); } catch { }
int successCount = 0;
int failCount = 0;
string lastError = "";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
foreach (var row in rows)
{
try
{
string 元件名称 = GetCellValueAsString(row, "元件名称");
string 规格 = GetCellValueAsString(row, "规格");
string 单位 = GetCellValueAsString(row, "单位");
string 数量 = GetCellValueAsString(row, "数量");
string 表价 = GetCellValueAsString(row, "表价");
string 采购系数 = GetCellValueAsString(row, "采购系数");
string 报出系数 = GetCellValueAsString(row, "报出系数");
string 品牌 = GetCellValueAsString(row, "品牌");
string 系列 = GetCellValueAsString(row, "系列");
string 元件类型 = GetCellValueAsString(row, "类型");
if (string.IsNullOrWhiteSpace(元件名称) && string.IsNullOrWhiteSpace(规格))
{
failCount++;
lastError = "元件名称和规格均为空,跳过";
continue;
}
string sql = "INSERT INTO " + tableName +
" (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " +
"VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@元件名称", 元件名称 ?? "");
cmd.Parameters.AddWithValue("@规格", 规格 ?? "");
cmd.Parameters.AddWithValue("@单位", 单位 ?? "");
cmd.Parameters.AddWithValue("@数量", 数量 ?? "");
cmd.Parameters.AddWithValue("@表价", 表价 ?? "");
cmd.Parameters.AddWithValue("@采购系数", 采购系数 ?? "");
cmd.Parameters.AddWithValue("@报出系数", 报出系数 ?? "");
cmd.Parameters.AddWithValue("@品牌", 品牌 ?? "");
cmd.Parameters.AddWithValue("@系列", 系列 ?? "");
cmd.Parameters.AddWithValue("@元件类型", 元件类型 ?? "");
cmd.ExecuteNonQuery();
}
successCount++;
}
catch (Exception exRow)
{
failCount++;
lastError = exRow.Message;
Log("[加入" + libName + "] 单行插入失败: " + exRow.Message);
}
}
}
reloadAction();
string msg = "已加入 " + successCount + " 条到" + libName + "。";
if (failCount > 0)
{
msg += "\n失败 " + failCount + " 条";
if (!string.IsNullOrEmpty(lastError)) msg += "(最后错误: " + lastError + ")";
}
Log("[加入" + libName + "] " + msg);
MessageBox.Show(this, msg, "加入" + libName,
MessageBoxButtons.OK,
failCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[加入" + libName + "] 整体失败: " + ex.Message);
MessageBox.Show(this, "加入" + libName + "失败:\n" + ex.Message,
"加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 通用:把元件表选中行插入到指定库。被"加入用户库"和"加入柜体库"共用。
/// </summary>
/// <param name="libName">库名(用于提示文字,如"用户库"/"柜体库")</param>
/// <param name="dbPath">目标数据库路径</param>
/// <param name="tableName">目标表名</param>
/// <param name="reloadAction">插入成功后的刷新回调</param>
private void AddRowsToLib(string libName, string dbPath, string tableName, System.Action reloadAction)
{
// 1) 取所有选中行
var rows = GetAllSelectedRows(this.advancedDataGridView5);
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先在元件表中选择要加入" + libName + "的行。",
"加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 2) 校验数据库文件存在
if (!System.IO.File.Exists(dbPath))
{
MessageBox.Show(this, "找不到" + libName + "数据库文件:\n" + dbPath,
"加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 3) 先结束元件表编辑态
try { this.advancedDataGridView5.EndEdit(); } catch { }
// 4) 逐行插入
int successCount = 0;
int failCount = 0;
string lastError = "";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
foreach (var row in rows)
{
try
{
string 元件名称 = GetCellValueAsString(row, "元件名称");
string 规格 = GetCellValueAsString(row, "规格");
string 单位 = GetCellValueAsString(row, "单位");
string 数量 = GetCellValueAsString(row, "数量");
string 表价 = GetCellValueAsString(row, "表价");
string 采购系数 = GetCellValueAsString(row, "采购系数");
string 报出系数 = GetCellValueAsString(row, "报出系数");
string 品牌 = GetCellValueAsString(row, "品牌");
string 系列 = GetCellValueAsString(row, "系列");
string 元件类型 = GetCellValueAsString(row, "类型");
if (string.IsNullOrWhiteSpace(元件名称) && string.IsNullOrWhiteSpace(规格))
{
failCount++;
lastError = "元件名称和规格均为空,跳过";
continue;
}
string sql = "INSERT INTO " + tableName +
" (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " +
"VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@元件名称", 元件名称 ?? "");
cmd.Parameters.AddWithValue("@规格", 规格 ?? "");
cmd.Parameters.AddWithValue("@单位", 单位 ?? "");
cmd.Parameters.AddWithValue("@数量", 数量 ?? "");
cmd.Parameters.AddWithValue("@表价", 表价 ?? "");
cmd.Parameters.AddWithValue("@采购系数", 采购系数 ?? "");
cmd.Parameters.AddWithValue("@报出系数", 报出系数 ?? "");
cmd.Parameters.AddWithValue("@品牌", 品牌 ?? "");
cmd.Parameters.AddWithValue("@系列", 系列 ?? "");
cmd.Parameters.AddWithValue("@元件类型", 元件类型 ?? "");
cmd.ExecuteNonQuery();
}
successCount++;
}
catch (Exception exRow)
{
failCount++;
lastError = exRow.Message;
Log("[加入" + libName + "] 单行插入失败: " + exRow.Message);
}
}
}
// 5) 刷新目标库表格
reloadAction();
// 6) 反馈
string msg = "已加入 " + successCount + " 条到" + libName + "。";
if (failCount > 0)
{
msg += "\n失败 " + failCount + " 条";
if (!string.IsNullOrEmpty(lastError)) msg += "(最后错误: " + lastError + ")";
}
Log("[加入" + libName + "] " + msg);
MessageBox.Show(this, msg, "加入" + libName,
MessageBoxButtons.OK,
failCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("[加入" + libName + "] 整体失败: " + ex.Message);
MessageBox.Show(this, "加入" + libName + "失败:\n" + ex.Message,
"加入" + libName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 从用户库数据库重新查询并绑定到 advancedDataGridView7。
/// 与 ui.cs 的 LoadSqliteData_advancedDataGridView7 同款逻辑,
/// 但放在主窗口.cs 中以便在加入用户库后主动刷新。
/// </summary>
private void ReloadUserLibFromDatabase()
{
string dbPath = UserLibDbPath;
if (!System.IO.File.Exists(dbPath)) return;
try
{
string connStr = GetConnString(dbPath);
var table = new DataTable();
using (var conn = new System.Data.SQLite.SQLiteConnection(connStr))
{
conn.Open();
string sql = "SELECT * FROM " + UserLibTableName + ";";
using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(sql, conn))
{
adapter.Fill(table);
}
}
this.advancedDataGridView7.AutoGenerateColumns = false;
this.advancedDataGridView7.DataSource = table;
// 同步刷新 comboBox3 的不重复元件名称下拉项
LoadUserLibDistinctNames();
}
catch (Exception ex)
{
Log("[加入用户库] 刷新用户库表格失败: " + ex.Message);
}
}
/// <summary>
/// 扫描用户库(advancedDataGridView7)的"元件名称"列,把所有不重复的名称
/// 填入 comboBox3 作为下拉项(首项固定为"全部")。保留当前选中值。
/// </summary>
private void LoadUserLibDistinctNames()
{
// 取当前选中,稍后恢复(避免刷新时选中被重置)
string prev = this.comboBox3.Text;
var names = new System.Collections.Generic.List<string>();
names.Add("全部");
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table != null && table.Columns.Contains("元件名称"))
{
var set = new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
foreach (DataRow r in table.Rows)
{
if (r == null) continue;
object v = r["元件名称"];
if (v == null || v == DBNull.Value) continue;
string s = v.ToString().Trim();
if (s.Length == 0) continue;
// 用原大小写保留首次出现的写法
if (set.Add(s)) names.Add(s);
}
}
// 临时解绑事件,避免填充项时反复触发筛选
this.comboBox3.SelectedIndexChanged -= UserLib_NameFilter_Changed;
try
{
this.comboBox3.Items.Clear();
this.comboBox3.Items.AddRange(names.ToArray());
// 恢复选中项(C# 5 的 List.Contains 只有 1 参数,用 OrdinalIgnoreCase 集合兜底判断)
bool found = false;
if (!string.IsNullOrEmpty(prev))
{
foreach (var n in names)
{
if (string.Equals(n, prev, System.StringComparison.OrdinalIgnoreCase))
{ this.comboBox3.Text = prev; found = true; break; }
}
}
if (!found) this.comboBox3.Text = "全部";
}
finally
{
this.comboBox3.SelectedIndexChanged += UserLib_NameFilter_Changed;
}
}
/// <summary>comboBox3 选择改变:按元件名称筛选用户库。</summary>
private void UserLib_NameFilter_Changed(object sender, EventArgs e)
{
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table == null) return;
string sel = this.comboBox3.Text == null ? "" : this.comboBox3.Text.Trim();
try
{
if (sel.Length == 0 || sel == "全部")
{
if (table.DefaultView.RowFilter.Length > 0)
table.DefaultView.RowFilter = "";
}
else
{
string escaped = EscapeLikeValue(sel);
// 转换为 LIKE 模式;不区分大小写由 DataView 默认不敏感处理
table.DefaultView.RowFilter = "[元件名称] LIKE '%" + escaped + "%'";
}
}
catch (Exception ex) { Log("[用户库筛选] 异常: " + ex.Message); }
}
/// <summary>
/// button30 查找:在用户库按"元件名称"和"规格"两列不区分大小写模糊匹配。
/// 关键词为空则清除筛选。参考项目表 button28_Click 的实现。
/// </summary>
private void UserLib_Search_Click(object sender, EventArgs e)
{
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table == null)
{
MessageBox.Show(this, "用户库暂无数据,无法查找。", "查找",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string keyword = this.textBox2.Text == null ? "" : this.textBox2.Text.Trim();
// 关键词为空:清除筛选,恢复显示全部(同时把名称筛选也复位)
if (keyword.Length == 0)
{
if (table.DefaultView.RowFilter.Length > 0)
table.DefaultView.RowFilter = "";
return;
}
// 转义 LIKE 特殊字符
string escaped = EscapeLikeValue(keyword);
try
{
// 不区分大小写 + 同时匹配元件名称和规格两列(任一命中即显示)
table.DefaultView.RowFilter =
"[元件名称] LIKE '%" + escaped + "%' OR [规格] LIKE '%" + escaped + "%'";
}
catch (System.Data.SyntaxErrorException ex)
{
MessageBox.Show(this,
"查找条件解析失败,请检查输入的关键词。\n详情:" + ex.Message,
"查找", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (table.DefaultView.Count == 0)
{
MessageBox.Show(this,
"没有找到元件名称或规格包含 “" + keyword + "” 的元件。",
"查找", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
// ============================================================
// 用户库工具栏(toolStrip1)事件
// · 插入(toolStripButton1):用户库选中行 → 元件表新增一行
// · 替换(toolStripButton2):用户库选中行 → 元件表选中行字段覆盖
// · 新增(toolStripButton3):用户库表格末尾加一空行(进入编辑)
// · 修改(toolStripButton4):用户库选中行进入编辑
// · 删除(toolStripButton5):删除用户库选中行
// · 导入(toolStripButton6):从 CSV/Excel(xlsx) 导入到用户库
// · 导出(toolStripButton7):用户库全部数据导出为 CSV
// · 打开模板(toolStripButton8):打开/下载用户库模板(CSV 表头)
// ============================================================
/// <summary>从用户库 DataGridViewRow 安全取出一个字段值(字符串)。</summary>
private static string UserLibGetField(DataGridViewRow row, string colName)
{
if (row == null || row.Cells == null) return "";
// 用户库列可能没 Name(只有 HeaderText),用 HeaderText 兜底查找
DataGridViewColumn col = null;
foreach (DataGridViewColumn c in row.DataGridView.Columns)
{
if (c.Name == colName || c.HeaderText == colName
|| (c.DataPropertyName ?? "") == colName)
{
col = c; break;
}
}
if (col == null) return "";
object v = row.Cells[col.Index].Value;
return v == null ? "" : v.ToString();
}
/// <summary>
/// 把用户库选中行解析成 元件表插入所需的参数包。
/// 返回 null 表示行无效或空。
/// </summary>
private struct UserLibRowData
{
public string 元件名称;
public string 规格;
public string 单位;
public string 数量;
public string 表价;
public string 采购系数;
public string 报出系数;
public string 品牌;
public string 系列;
public string 元件类型;
}
private static UserLibRowData? UserLibExtractRow(DataGridViewRow row)
{
if (row == null) return null;
var d = new UserLibRowData
{
元件名称 = UserLibGetField(row, "元件名称"),
规格 = UserLibGetField(row, "规格"),
单位 = UserLibGetField(row, "单位"),
数量 = UserLibGetField(row, "数量"),
表价 = UserLibGetField(row, "表价"),
采购系数 = UserLibGetField(row, "采购系数"),
报出系数 = UserLibGetField(row, "报出系数"),
品牌 = UserLibGetField(row, "品牌"),
系列 = UserLibGetField(row, "系列"),
元件类型 = UserLibGetField(row, "元件类型"),
};
// 元件名称和规格都为空 → 无效行
if (string.IsNullOrWhiteSpace(d.元件名称) && string.IsNullOrWhiteSpace(d.规格))
return null;
return d;
}
// ---------- 插入 ----------
/// <summary>插入:把用户库选中行数据插入到元件表(advancedDataGridView5)末尾。</summary>
private void UserLib_Insert_Click(object sender, EventArgs e)
{
// 必须先打开项目和图号/箱号,否则元件表无数据源
if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从用户库插入。",
"用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 该箱号还没建过元件表时自动建表(否则 NewRow 后写"序号"列会抛异常)
if (!确保元件表存在())
{
MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从用户库插入。",
"用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var row = GetFirstSelectedRow(this.advancedDataGridView7);
if (row == null)
{
MessageBox.Show(this, "请先在用户库表格中选择一行再插入。",
"用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var data = UserLibExtractRow(row);
if (!data.HasValue)
{
MessageBox.Show(this, "选中的用户库行为空(元件名称和规格都为空),无法插入。",
"用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var d = data.Value;
// 结束用户库编辑态(避免选中行的值还没提交)
try { this.advancedDataGridView7.EndEdit(); } catch { }
// 复用现有的"插入元件到报价表"方法(它负责派生列计算 + 重算聚合 + 日志)
// 参数:规格/表价/品牌/采购系数/系列
decimal price = 0m;
decimal.TryParse(d.表价,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out price);
// 用元件名称作为元件名称(而不是规格);调用现有方法时会自动用系列名清理逻辑
// 这里把"元件名称"字段放到系列参数,让现有方法按系列名处理 → 元件名称 = 清理(系列名)
// 但用户库已有完整的元件名称/系列,不需要再清理,所以直接调用并把数据传准
// 为了不改动现有方法,采用直接填充元件表新行的方式:
decimal 系数 = 1m;
decimal.TryParse(d.采购系数,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out 系数);
if (系数 == 0m) 系数 = 1m;
decimal 报出系数Val = 1m;
decimal.TryParse(d.报出系数,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out 报出系数Val);
if (报出系数Val == 0m) 报出系数Val = 1m;
decimal qty = 1m;
decimal.TryParse(d.数量,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out qty);
if (qty == 0m) qty = 1m;
// 派生列计算
decimal 采购单价 = price * 系数;
decimal 采购价 = 采购单价 * qty;
decimal 报出单价 = 采购单价 * 报出系数Val;
decimal 报出价 = 报出单价 * qty;
// ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入;
// 单选=false 返回后走原逻辑
System.Action 插入动作 = delegate
{
this.advancedDataGridView5.EndEdit();
// ★ 记住选中行索引,用于"在选中行下面插入"
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
var newRow = _yuanjianTable.NewRow();
newRow["序号"] = "1"; // InsertRowBelowSelected 会自动重编号
newRow["元件名称"] = d.元件名称 ?? "";
newRow["规格"] = d.规格 ?? "";
newRow["品牌"] = d.品牌 ?? "";
newRow["表价"] = (double)price;
newRow["数量"] = d.数量 ?? "1";
newRow["采购系数"] = 系数.ToString(System.Globalization.CultureInfo.InvariantCulture);
newRow["报出系数"] = 报出系数Val.ToString(System.Globalization.CultureInfo.InvariantCulture);
newRow["单位"] = d.单位 ?? "";
newRow["图纸规格"] = "";
newRow["系列"] = d.系列 ?? "";
// 用户库插入的元件,类型也设为"元件"(与网页插入一致)
newRow["类型"] = "元件";
newRow["备注"] = "";
newRow["采购单价"] = (double)采购单价;
newRow["采购价"] = (double)采购价;
newRow["报出单价"] = (double)报出单价;
newRow["报出价"] = (double)报出价;
// ★ 插入到选中行下面(而不是末尾)
InsertRowBelowSelected(_yuanjianTable, newRow, selectedIndex);
// ★ 虚拟模式收尾:同步网格行数+全表保存
元件表插行收尾();
// 滚动到新行
int idx = _yuanjianTable.Rows.IndexOf(newRow);
if (idx >= 0)
{
try { this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = idx; } catch { }
}
重算聚合();
};
try
{
if (工具窗口批量插入(插入动作)) return;
插入动作();
Log("[用户库] 已插入到元件表: " + d.元件名称 + " / " + d.规格);
}
catch (Exception ex)
{
MessageBox.Show(this, "插入失败:\n" + ex.Message,
"用户库插入", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 替换 ----------
/// <summary>替换:用用户库选中行的数据覆盖元件表(advancedDataGridView5)当前选中行。</summary>
private void UserLib_Replace_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号。",
"用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var libRow = GetFirstSelectedRow(this.advancedDataGridView7);
if (libRow == null)
{
MessageBox.Show(this, "请先在用户库表格中选择一行。",
"用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var data = UserLibExtractRow(libRow);
if (!data.HasValue)
{
MessageBox.Show(this, "选中的用户库行为空,无法替换。",
"用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// ★ 选型页分支:选型页有选中行→目标=选型页选中行(判定用"有选中行"防焦点漂移)
if (替换目标在选型页())
{
var d0 = data.Value;
库替换走选型页(d0.元件名称 ?? "", d0.规格 ?? "", d0.系列 ?? "", d0.品牌 ?? "", d0.表价, d0.采购系数, "用户库");
return;
}
if (_yuanjianTable == null)
{
MessageBox.Show(this, "请先在报价软件中选择图号/箱号。",
"用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var dgvRow = GetFirstSelectedRow(this.advancedDataGridView5);
if (dgvRow == null || dgvRow.IsNewRow)
{
MessageBox.Show(this, "请先在元件表中选中要被替换的行。",
"用户库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var d = data.Value;
try { this.advancedDataGridView7.EndEdit(); } catch { }
try { this.advancedDataGridView5.EndEdit(); } catch { }
decimal price = 0m;
decimal.TryParse(d.表价,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out price);
decimal 系数 = 1m;
decimal.TryParse(d.采购系数,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out 系数);
if (系数 == 0m) 系数 = 1m;
// ★ 虚拟模式:写内存表+刷新行(数量/报出系数保留原值,由重算派生列时用)
写单元格(this.advancedDataGridView5, dgvRow.Index, "元件名称", d.元件名称 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "规格", d.规格 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "单位", d.单位 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "品牌", d.品牌 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "表价", (double)price);
写单元格(this.advancedDataGridView5, dgvRow.Index, "采购系数", 系数.ToString(System.Globalization.CultureInfo.InvariantCulture));
写单元格(this.advancedDataGridView5, dgvRow.Index, "系列", d.系列 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "类型", "元件");
// 重算派生列
RecalcYuanjianRow(dgvRow);
重算聚合();
Log("[用户库] 已替换元件表选中行: " + d.元件名称 + " / " + d.规格);
}
// ---------- 新增 ----------
/// <summary>新增:弹出编辑窗体,填好后添加到用户库表格末尾。</summary>
private void UserLib_New_Click(object sender, EventArgs e)
{
try
{
this.advancedDataGridView7.EndEdit();
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table == null)
{
MessageBox.Show(this, "用户库表格未绑定数据,无法新增。",
"用户库新增", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 弹出编辑窗体(默认值)
using (var dlg = new 用户库元件编辑())
{
dlg.Text = "新增用户库元件";
dlg.单位 = "只";
dlg.数量 = "1";
dlg.表价 = "0";
dlg.采购系数 = "1";
dlg.报出系数 = "1";
dlg.元件类型 = "元件";
if (dlg.ShowDialog(this) != DialogResult.OK) return;
// 把窗体数据写入新行
var newRow = table.NewRow();
newRow["元件名称"] = dlg.元件名称 ?? "";
newRow["规格"] = dlg.规格 ?? "";
newRow["单位"] = dlg.单位 ?? "";
newRow["数量"] = dlg.数量 ?? "1";
newRow["表价"] = dlg.表价 ?? "0";
newRow["采购系数"] = dlg.采购系数 ?? "1";
newRow["报出系数"] = dlg.报出系数 ?? "1";
newRow["品牌"] = dlg.品牌 ?? "";
newRow["系列"] = dlg.系列 ?? "";
newRow["元件类型"] = dlg.元件类型 ?? "元件";
table.Rows.Add(newRow);
table.AcceptChanges();
// 滚动并选中最后一行
int lastIdx = this.advancedDataGridView7.Rows.Count - 1;
if (lastIdx >= 0)
{
this.advancedDataGridView7.FirstDisplayedScrollingRowIndex = lastIdx;
this.advancedDataGridView7.ClearSelection();
this.advancedDataGridView7.Rows[lastIdx].Selected = true;
}
// 保存到数据库
SaveUserLibToDatabase();
Log("[用户库] 新增元件: " + dlg.元件名称 + " / " + dlg.规格);
}
}
catch (Exception ex)
{
MessageBox.Show(this, "新增失败:\n" + ex.Message,
"用户库新增", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 修改 ----------
/// <summary>修改:弹出编辑窗体,把用户库选中行的数据加载进去供用户修改。</summary>
private void UserLib_Modify_Click(object sender, EventArgs e)
{
var row = GetFirstSelectedRow(this.advancedDataGridView7);
if (row == null)
{
MessageBox.Show(this, "请先在用户库中选择要修改的行。",
"用户库修改", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
this.advancedDataGridView7.EndEdit();
using (var dlg = new 用户库元件编辑())
{
dlg.Text = "修改用户库元件";
// 把当前行的数据加载到窗体
dlg.元件名称 = UserLibGetField(row, "元件名称");
dlg.规格 = UserLibGetField(row, "规格");
dlg.单位 = UserLibGetField(row, "单位");
dlg.数量 = UserLibGetField(row, "数量");
dlg.表价 = UserLibGetField(row, "表价");
dlg.采购系数 = UserLibGetField(row, "采购系数");
dlg.报出系数 = UserLibGetField(row, "报出系数");
dlg.品牌 = UserLibGetField(row, "品牌");
dlg.系列 = UserLibGetField(row, "系列");
dlg.元件类型 = UserLibGetField(row, "元件类型");
if (dlg.ShowDialog(this) != DialogResult.OK) return;
// 把窗体数据写回选中行
SetUserLibField(row, "元件名称", dlg.元件名称);
SetUserLibField(row, "规格", dlg.规格);
SetUserLibField(row, "单位", dlg.单位);
SetUserLibField(row, "数量", dlg.数量);
SetUserLibField(row, "表价", dlg.表价);
SetUserLibField(row, "采购系数", dlg.采购系数);
SetUserLibField(row, "报出系数", dlg.报出系数);
SetUserLibField(row, "品牌", dlg.品牌);
SetUserLibField(row, "系列", dlg.系列);
SetUserLibField(row, "元件类型", dlg.元件类型);
// 保存到数据库
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table != null) table.AcceptChanges();
SaveUserLibToDatabase();
Log("[用户库] 修改元件: " + dlg.元件名称 + " / " + dlg.规格);
}
}
catch (Exception ex)
{
MessageBox.Show(this, "修改失败:\n" + ex.Message,
"用户库修改", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>设置用户库行的某个字段值(按 DataPropertyName/HeaderText 匹配列)。</summary>
private static void SetUserLibField(DataGridViewRow row, string colName, string value)
{
if (row == null || row.Cells == null) return;
foreach (DataGridViewColumn c in row.DataGridView.Columns)
{
if (c.Name == colName || c.HeaderText == colName
|| (c.DataPropertyName ?? "") == colName)
{
row.Cells[c.Index].Value = value ?? "";
return;
}
}
}
// ---------- 删除 ----------
/// <summary>删除:删除用户库中所有选中行,并保存到数据库。</summary>
private void UserLib_Delete_Click(object sender, EventArgs e)
{
var rows = GetAllSelectedRows(this.advancedDataGridView7);
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先在用户库中选择要删除的行。",
"用户库删除", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (MessageBox.Show(this, "确认删除选中的 " + rows.Count + " 行吗?",
"用户库删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
try
{
this.advancedDataGridView7.EndEdit();
// 按索引倒序删除(避免索引错位)
var sortedRows = new System.Collections.Generic.List<DataGridViewRow>(rows);
sortedRows.Sort((a, b) => b.Index.CompareTo(a.Index));
foreach (var r in sortedRows)
{
if (r == null || r.IsNewRow) continue;
this.advancedDataGridView7.Rows.RemoveAt(r.Index);
}
SaveUserLibToDatabase();
Log("[用户库] 已删除 " + rows.Count + " 行");
}
catch (Exception ex)
{
MessageBox.Show(this, "删除失败:\n" + ex.Message,
"用户库删除", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>清除:清空用户库表所有数据(不可恢复,二次确认)。</summary>
private void UserLib_Clear_Click(object sender, EventArgs e)
{
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table == null || table.Rows.Count == 0)
{
MessageBox.Show(this, "用户库已经是空的。",
"用户库清除", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 二次确认(高危操作)
if (MessageBox.Show(this,
"确认清空用户库的全部数据吗?\n当前共 " + table.Rows.Count + " 行,清除后不可恢复!",
"用户库清除", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
return;
try
{
this.advancedDataGridView7.EndEdit();
int 已清行数 = table.Rows.Count;
table.Clear(); // 清内存 DataTable
SaveUserLibToDatabase(); // 写库(库会跟着清空)
Log("[用户库] 已清除全部数据: " + 已清行数 + " 行");
MessageBox.Show(this, "已清空用户库 " + 已清行数 + " 行。",
"用户库清除", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, "清空失败:\n" + ex.Message,
"用户库清除", MessageBoxButtons.OK, MessageBoxIcon.Error);
Log("[用户库] 清空异常: " + ex.Message);
}
}
// ---------- 导入 ----------
/// <summary>导入:从 CSV 文件导入数据到用户库(追加,不覆盖现有数据)。</summary>
private void UserLib_Import_Click(object sender, EventArgs e)
{
// 确保数据库存在
string dbPath = UserLibDbPath;
if (!System.IO.File.Exists(dbPath))
{
MessageBox.Show(this,
"用户库数据库不存在,请先在 advancedDataGridView7 上右键「根据列标题创建表」。",
"用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
using (var ofd = new OpenFileDialog
{
Title = "选择要导入的 CSV 文件",
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
RestoreDirectory = true,
})
{
if (ofd.ShowDialog(this) != DialogResult.OK) return;
try
{
string[] allLines = AutoDetectReadAllLines(ofd.FileName);
if (allLines.Length == 0)
{
MessageBox.Show(this, "文件为空。",
"用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 第一行是表头,解析列名 → 列索引
string[] headers = ParseCsvLine(allLines[0]);
var colMap = new System.Collections.Generic.Dictionary<string, int>(System.StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < headers.Length; i++)
{
string h = (headers[i] ?? "").Trim().Trim('"');
if (h.Length > 0) colMap[h] = i;
}
// 用户库的标准列
string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价",
"采购系数", "报出系数", "品牌", "系列", "元件类型" };
// 检查至少有一个匹配的列
bool anyMatch = false;
foreach (string c in userLibCols) if (colMap.ContainsKey(c)) { anyMatch = true; break; }
if (!anyMatch)
{
MessageBox.Show(this,
"CSV 表头与用户库列名不匹配。\n需要的列: " + string.Join(", ", userLibCols),
"用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
int imported = 0;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// 逐行解析并 INSERT
for (int li = 1; li < allLines.Length; li++)
{
string line = allLines[li];
if (string.IsNullOrWhiteSpace(line)) continue;
string[] fields = ParseCsvLine(line);
// 取字段值(列名不存在时用空字符串)
System.Collections.Generic.Dictionary<string, string> vals =
new System.Collections.Generic.Dictionary<string, string>();
foreach (string c in userLibCols)
{
int idx;
string v = (colMap.TryGetValue(c, out idx) && idx < fields.Length)
? (fields[idx] ?? "").Trim().Trim('"') : "";
vals[c] = v;
}
// 元件名称和规格都为空的行跳过
if (string.IsNullOrWhiteSpace(vals["元件名称"]) && string.IsNullOrWhiteSpace(vals["规格"]))
continue;
string sql = "INSERT INTO " + UserLibTableName +
" (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " +
"VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
foreach (string c in userLibCols)
cmd.Parameters.AddWithValue("@" + c, vals[c]);
cmd.ExecuteNonQuery();
}
imported++;
}
}
// 刷新表格
ReloadUserLibFromDatabase();
MessageBox.Show(this, "导入完成,共 " + imported + " 行。",
"用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Information);
Log("[用户库] 导入 " + imported + " 行");
}
catch (Exception ex)
{
MessageBox.Show(this, "导入失败:\n" + ex.Message,
"用户库导入", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
/// <summary>
/// 自动检测编码读取文本文件全部行(UTF-8 优先,GB2312 兜底)。
/// 用途:CSV 导入 — 用户用 Excel 编辑保存的 CSV 默认是 GBK 编码(尤其中文 Windows),
/// 而程序导出的是 UTF-8。这里两种都能正确读取。
/// 检测规则:
/// 1) 文件有 UTF-8 BOM (EF BB BF) → 直接按 UTF-8 读
/// 2) 文件有 GBK BOM (FF FE / FE FF) → 按对应 Unicode 读
/// 3) 无 BOM → 先按 UTF-8 严格解码,失败(出现乱码/异常)则改用 GB2312
/// </summary>
private static string[] AutoDetectReadAllLines(string path)
{
byte[] bytes = System.IO.File.ReadAllBytes(path);
if (bytes.Length == 0) return new string[0];
// 1) BOM 检测
if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
{
return SplitLines(System.Text.Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3));
}
if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE)
{
return SplitLines(System.Text.Encoding.Unicode.GetString(bytes, 2, bytes.Length - 2));
}
if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF)
{
return SplitLines(System.Text.Encoding.BigEndianUnicode.GetString(bytes, 2, bytes.Length - 2));
}
// 2) 无 BOM:先试 UTF-8 严格解码
// UTF-8 解码器遇到非法字节会替换成 \uFFFD,检测到该字符说明不是 UTF-8
try
{
var utf8 = new System.Text.UTF8Encoding(false, true); // throwOnInvalidBytes=true
string s = utf8.GetString(bytes);
if (s.IndexOf('\uFFFD') < 0) return SplitLines(s);
}
catch { /* 不是合法 UTF-8,继续尝试 GBK */ }
// 3) GBK(GB2312)兜底 — 中文 Windows 上 Excel 默认保存的编码
return SplitLines(System.Text.Encoding.GetEncoding("GB2312").GetString(bytes));
}
/// <summary>把文本按 \r\n / \n / \r 拆成行(保留空行)。</summary>
private static string[] SplitLines(string text)
{
if (text == null) return new string[0];
// 用 Universal-line-break 拆分,兼容 Windows/Unix/Mac
var lines = new System.Collections.Generic.List<string>();
var sb = new System.Text.StringBuilder();
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (c == '\r')
{
lines.Add(sb.ToString());
sb.Length = 0;
if (i + 1 < text.Length && text[i + 1] == '\n') i++;
}
else if (c == '\n')
{
lines.Add(sb.ToString());
sb.Length = 0;
}
else
{
sb.Append(c);
}
}
lines.Add(sb.ToString());
return lines.ToArray();
}
/// <summary>简单 CSV 行解析(支持双引号包裹、逗号分隔)。</summary>
private static string[] ParseCsvLine(string line)
{
if (string.IsNullOrEmpty(line)) return new string[0];
var list = new System.Collections.Generic.List<string>();
bool inQuote = false;
var sb = new System.Text.StringBuilder();
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '"')
{
if (inQuote && i + 1 < line.Length && line[i + 1] == '"')
{
sb.Append('"'); i++; // 转义的引号
}
else
{
inQuote = !inQuote;
}
}
else if (c == ',' && !inQuote)
{
list.Add(sb.ToString());
sb.Length = 0;
}
else
{
sb.Append(c);
}
}
list.Add(sb.ToString());
return list.ToArray();
}
// ---------- 导出 ----------
/// <summary>导出:把用户库全部数据导出为 CSV 文件。</summary>
private void UserLib_Export_Click(object sender, EventArgs e)
{
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table == null || table.Rows.Count == 0)
{
MessageBox.Show(this, "用户库没有数据可导出。",
"用户库导出", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var sfd = new SaveFileDialog
{
Title = "导出用户库数据",
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
FileName = "用户库_" + System.DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv",
RestoreDirectory = true,
})
{
if (sfd.ShowDialog(this) != DialogResult.OK) return;
try
{
var sb = new System.Text.StringBuilder();
// 表头(用用户库标准列名)
string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价",
"采购系数", "报出系数", "品牌", "系列", "元件类型" };
// 第一行:列名
for (int i = 0; i < userLibCols.Length; i++)
{
if (i > 0) sb.Append(",");
sb.Append(CsvEscape(userLibCols[i]));
}
sb.Append("\r\n");
// 数据行
foreach (DataRow r in table.Rows)
{
for (int i = 0; i < userLibCols.Length; i++)
{
if (i > 0) sb.Append(",");
// 如果表中没有这一列,直接写空
if (table.Columns.Contains(userLibCols[i]))
sb.Append(CsvEscape(r[userLibCols[i]] == null ? "" : r[userLibCols[i]].ToString()));
else
sb.Append("");
}
sb.Append("\r\n");
}
System.IO.File.WriteAllText(sfd.FileName, sb.ToString(), System.Text.Encoding.UTF8);
MessageBox.Show(this, "导出完成,共 " + table.Rows.Count + " 行。\n" + sfd.FileName,
"用户库导出", MessageBoxButtons.OK, MessageBoxIcon.Information);
Log("[用户库] 导出 " + table.Rows.Count + " 行到 " + sfd.FileName);
}
catch (Exception ex)
{
MessageBox.Show(this, "导出失败:\n" + ex.Message,
"用户库导出", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
/// <summary>CSV 字段转义:含逗号/引号/换行时用双引号包裹。</summary>
private static string CsvEscape(string s)
{
if (s == null) return "";
if (s.IndexOfAny(new char[] { ',', '"', '\r', '\n' }) < 0) return s;
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
// ---------- 打开模板 ----------
/// <summary>打开模板:生成一份只含表头的 CSV 模板文件,并打开所在目录。</summary>
private void UserLib_OpenTemplate_Click(object sender, EventArgs e)
{
using (var sfd = new SaveFileDialog
{
Title = "保存用户库模板",
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
FileName = "用户库模板.csv",
RestoreDirectory = true,
})
{
if (sfd.ShowDialog(this) != DialogResult.OK) return;
try
{
string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价",
"采购系数", "报出系数", "品牌", "系列", "元件类型" };
// 用 UTF-8 BOM,Excel 打开才不会乱码
var sb = new System.Text.StringBuilder();
sb.Append(string.Join(",", userLibCols));
sb.Append("\r\n");
// 加一行示例数据
sb.Append(string.Join(",", new string[] {
"示例元件", "示例规格", "只", "1", "10.5",
"1", "1.2", "XX品牌", "XX系列", "元件" }));
sb.Append("\r\n");
System.IO.File.WriteAllText(sfd.FileName, sb.ToString(),
new System.Text.UTF8Encoding(true));
MessageBox.Show(this, "模板已保存:\n" + sfd.FileName +
"\n\n填好数据后用「导入」功能批量导入到用户库。",
"用户库模板", MessageBoxButtons.OK, MessageBoxIcon.Information);
Log("[用户库] 模板已保存: " + sfd.FileName);
}
catch (Exception ex)
{
MessageBox.Show(this, "保存模板失败:\n" + ex.Message,
"用户库模板", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
/// <summary>
/// 把用户库 DataTable 的所有数据整体写回数据库(DELETE + INSERT 全量)。
/// 用户库没有 PRIMARY KEY,无法 UPDATE 单行,采用全量重建方式。
/// </summary>
private void SaveUserLibToDatabase()
{
string dbPath = UserLibDbPath;
if (!System.IO.File.Exists(dbPath)) return;
try
{
var table = this.advancedDataGridView7.DataSource as DataTable;
if (table == null) return;
string[] userLibCols = { "元件名称", "规格", "单位", "数量", "表价",
"采购系数", "报出系数", "品牌", "系列", "元件类型" };
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// 先清空
using (var cmdDel = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + UserLibTableName + ";", conn))
{
cmdDel.ExecuteNonQuery();
}
// 重新插入所有行
foreach (DataRow r in table.Rows)
{
if (r.RowState == DataRowState.Deleted) continue;
string sql = "INSERT INTO " + UserLibTableName +
" (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " +
"VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
foreach (string c in userLibCols)
cmd.Parameters.AddWithValue("@" + c,
table.Columns.Contains(c) && r[c] != null ? r[c].ToString() : "");
cmd.ExecuteNonQuery();
}
}
}
table.AcceptChanges();
Log("[用户库] 已保存到数据库");
// 保存后同步刷新 comboBox3 的下拉项(让新加进来的元件名称立即出现)
LoadUserLibDistinctNames();
}
catch (Exception ex)
{
Log("[用户库] 保存失败: " + ex.Message);
}
}
// ============================================================
// 柜体库(advancedDataGridView10) — 功能与用户库完全一致
// · 插入(toolStripButton12):柜体库选中行 → 元件表新增一行
// · 替换(toolStripButton13):柜体库选中行 → 元件表选中行字段覆盖
// · 新增(toolStripButton14):弹出窗体新增柜体库元件
// · 修改(toolStripButton15):弹出窗体修改柜体库选中行
// · 删除(toolStripButton16):删除柜体库选中行
// · 筛选(comboBox4):按元件名称筛选
// · 查找(button31 + textBox5):按元件名称/规格模糊匹配
// ============================================================
/// <summary>
/// 柜体库数据库路径(独立库,与用户库分开存储)。
/// 与 ui.cs 的 LoadSqliteData_advancedDataGridView10 一致。
/// </summary>
private string GuiTiDbPath
{
get { return GetDbFullPath(@"sjk\advancedDataGridView10Table.db"); }
}
private const string GuiTiTableName = "advancedDataGridView10Table";
/// <summary>从柜体库 DataGridViewRow 安全取出一个字段值(字符串)。</summary>
private static string GuiTiGetField(DataGridViewRow row, string colName)
{
if (row == null || row.Cells == null) return "";
// 柜体库列可能没 Name(只有 HeaderText),用 HeaderText 兜底查找
DataGridViewColumn col = null;
foreach (DataGridViewColumn c in row.DataGridView.Columns)
{
if (c.Name == colName || c.HeaderText == colName
|| (c.DataPropertyName ?? "") == colName)
{
col = c; break;
}
}
if (col == null) return "";
object v = row.Cells[col.Index].Value;
return v == null ? "" : v.ToString();
}
/// <summary>设置柜体库行的某个字段值(按 DataPropertyName/HeaderText 匹配列)。</summary>
private static void SetGuiTiField(DataGridViewRow row, string colName, string value)
{
if (row == null || row.Cells == null) return;
foreach (DataGridViewColumn c in row.DataGridView.Columns)
{
if (c.Name == colName || c.HeaderText == colName
|| (c.DataPropertyName ?? "") == colName)
{
row.Cells[c.Index].Value = value ?? "";
return;
}
}
}
/// <summary>
/// 把柜体库选中行解析成 元件表插入所需的参数包。
/// 返回 null 表示行无效或空。
/// </summary>
private struct GuiTiRowData
{
public string 元件名称;
public string 规格;
public string 单位;
public string 数量;
public string 表价;
public string 采购系数;
public string 报出系数;
public string 品牌;
public string 系列;
public string 元件类型;
}
private static GuiTiRowData? GuiTiExtractRow(DataGridViewRow row)
{
if (row == null) return null;
var d = new GuiTiRowData
{
元件名称 = GuiTiGetField(row, "元件名称"),
规格 = GuiTiGetField(row, "规格"),
单位 = GuiTiGetField(row, "单位"),
数量 = GuiTiGetField(row, "数量"),
表价 = GuiTiGetField(row, "表价"),
采购系数 = GuiTiGetField(row, "采购系数"),
报出系数 = GuiTiGetField(row, "报出系数"),
品牌 = GuiTiGetField(row, "品牌"),
系列 = GuiTiGetField(row, "系列"),
元件类型 = GuiTiGetField(row, "元件类型"),
};
// 元件名称和规格都为空 → 无效行
if (string.IsNullOrWhiteSpace(d.元件名称) && string.IsNullOrWhiteSpace(d.规格))
return null;
return d;
}
// ---------- 插入 ----------
/// <summary>插入:把柜体库选中行数据插入到元件表(advancedDataGridView5)末尾。</summary>
private void GuiTi_Insert_Click(object sender, EventArgs e)
{
// 必须先打开项目和图号/箱号,否则元件表无数据源
if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从柜体库插入。",
"柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ 该箱号还没建过元件表时自动建表(否则 NewRow 后写"序号"列会抛异常)
if (!确保元件表存在())
{
MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号,再从柜体库插入。",
"柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var row = GetFirstSelectedRow(this.advancedDataGridView10);
if (row == null)
{
MessageBox.Show(this, "请先在柜体库表格中选择一行再插入。",
"柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var data = GuiTiExtractRow(row);
if (!data.HasValue)
{
MessageBox.Show(this, "选中的柜体库行为空(元件名称和规格都为空),无法插入。",
"柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var d = data.Value;
// 结束柜体库编辑态(避免选中行的值还没提交)
try { this.advancedDataGridView10.EndEdit(); } catch { }
decimal price = 0m;
decimal.TryParse(d.表价,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out price);
decimal 系数 = 1m;
decimal.TryParse(d.采购系数,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out 系数);
if (系数 == 0m) 系数 = 1m;
decimal 报出系数Val = 1m;
decimal.TryParse(d.报出系数,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out 报出系数Val);
if (报出系数Val == 0m) 报出系数Val = 1m;
decimal qty = 1m;
decimal.TryParse(d.数量,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out qty);
if (qty == 0m) qty = 1m;
// 派生列计算
decimal 采购单价 = price * 系数;
decimal 采购价 = 采购单价 * qty;
decimal 报出单价 = 采购单价 * 报出系数Val;
decimal 报出价 = 报出单价 * qty;
// ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入;
// 单选=false 返回后走原逻辑
System.Action 插入动作 = delegate
{
this.advancedDataGridView5.EndEdit();
// ★ 记住选中行索引,用于"在选中行下面插入"
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
var newRow = _yuanjianTable.NewRow();
newRow["序号"] = "1"; // InsertRowBelowSelected 会自动重编号
newRow["元件名称"] = d.元件名称 ?? "";
newRow["规格"] = d.规格 ?? "";
newRow["品牌"] = d.品牌 ?? "";
newRow["表价"] = (double)price;
newRow["数量"] = d.数量 ?? "1";
newRow["采购系数"] = 系数.ToString(System.Globalization.CultureInfo.InvariantCulture);
newRow["报出系数"] = 报出系数Val.ToString(System.Globalization.CultureInfo.InvariantCulture);
newRow["单位"] = d.单位 ?? "";
newRow["图纸规格"] = "";
newRow["系列"] = d.系列 ?? "";
// 柜体库插入的元件,类型也设为"元件"(与用户库一致)
newRow["类型"] = "元件";
newRow["备注"] = "";
newRow["采购单价"] = (double)采购单价;
newRow["采购价"] = (double)采购价;
newRow["报出单价"] = (double)报出单价;
newRow["报出价"] = (double)报出价;
// ★ 插入到选中行下面(而不是末尾)
InsertRowBelowSelected(_yuanjianTable, newRow, selectedIndex);
// ★ 虚拟模式收尾:同步网格行数+全表保存
元件表插行收尾();
// 滚动到新行
int idx = _yuanjianTable.Rows.IndexOf(newRow);
if (idx >= 0)
{
try { this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = idx; } catch { }
}
重算聚合();
};
try
{
if (工具窗口批量插入(插入动作)) return;
插入动作();
Log("[柜体库] 已插入到元件表: " + d.元件名称 + " / " + d.规格);
}
catch (Exception ex)
{
MessageBox.Show(this, "插入失败:\n" + ex.Message,
"柜体库插入", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 替换 ----------
/// <summary>替换:用柜体库选中行的数据覆盖元件表(advancedDataGridView5)当前选中行。</summary>
private void GuiTi_Replace_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在报价软件中打开一个项目并选择图号/箱号。",
"柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var libRow = GetFirstSelectedRow(this.advancedDataGridView10);
if (libRow == null)
{
MessageBox.Show(this, "请先在柜体库表格中选择一行。",
"柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var data = GuiTiExtractRow(libRow);
if (!data.HasValue)
{
MessageBox.Show(this, "选中的柜体库行为空,无法替换。",
"柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// ★ 选型页分支:选型页有选中行→目标=选型页选中行(判定用"有选中行"防焦点漂移)
if (替换目标在选型页())
{
var d0 = data.Value;
库替换走选型页(d0.元件名称 ?? "", d0.规格 ?? "", d0.系列 ?? "", d0.品牌 ?? "", d0.表价, d0.采购系数, "柜体库");
return;
}
if (_yuanjianTable == null)
{
MessageBox.Show(this, "请先在报价软件中选择图号/箱号。",
"柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var dgvRow = GetFirstSelectedRow(this.advancedDataGridView5);
if (dgvRow == null || dgvRow.IsNewRow)
{
MessageBox.Show(this, "请先在元件表中选中要被替换的行。",
"柜体库替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var d = data.Value;
try { this.advancedDataGridView10.EndEdit(); } catch { }
try { this.advancedDataGridView5.EndEdit(); } catch { }
decimal price = 0m;
decimal.TryParse(d.表价,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out price);
decimal 系数 = 1m;
decimal.TryParse(d.采购系数,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out 系数);
if (系数 == 0m) 系数 = 1m;
// ★ 虚拟模式:写内存表+刷新行(数量/报出系数保留原值)
写单元格(this.advancedDataGridView5, dgvRow.Index, "元件名称", d.元件名称 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "规格", d.规格 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "单位", d.单位 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "品牌", d.品牌 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "表价", (double)price);
写单元格(this.advancedDataGridView5, dgvRow.Index, "采购系数", 系数.ToString(System.Globalization.CultureInfo.InvariantCulture));
写单元格(this.advancedDataGridView5, dgvRow.Index, "系列", d.系列 ?? "");
写单元格(this.advancedDataGridView5, dgvRow.Index, "类型", "元件");
// 重算派生列
RecalcYuanjianRow(dgvRow);
重算聚合();
Log("[柜体库] 已替换元件表选中行: " + d.元件名称 + " / " + d.规格);
}
// ---------- 新增 ----------
/// <summary>新增:弹出编辑窗体,填好后添加到柜体库表格末尾。</summary>
private void GuiTi_New_Click(object sender, EventArgs e)
{
try
{
this.advancedDataGridView10.EndEdit();
var table = this.advancedDataGridView10.DataSource as DataTable;
if (table == null)
{
MessageBox.Show(this, "柜体库表格未绑定数据,无法新增。",
"柜体库新增", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 弹出编辑窗体(默认值)
using (var dlg = new 用户库元件编辑())
{
dlg.Text = "新增柜体库元件";
dlg.单位 = "只";
dlg.数量 = "1";
dlg.表价 = "0";
dlg.采购系数 = "1";
dlg.报出系数 = "1";
dlg.元件类型 = "元件";
if (dlg.ShowDialog(this) != DialogResult.OK) return;
// 柜体库表格 ReadOnly=true,写入 DataTable 不受影响
// 把窗体数据写入新行
var newRow = table.NewRow();
newRow["元件名称"] = dlg.元件名称 ?? "";
newRow["规格"] = dlg.规格 ?? "";
newRow["单位"] = dlg.单位 ?? "";
newRow["数量"] = dlg.数量 ?? "1";
newRow["表价"] = dlg.表价 ?? "0";
newRow["采购系数"] = dlg.采购系数 ?? "1";
newRow["报出系数"] = dlg.报出系数 ?? "1";
newRow["品牌"] = dlg.品牌 ?? "";
newRow["系列"] = dlg.系列 ?? "";
newRow["元件类型"] = dlg.元件类型 ?? "元件";
table.Rows.Add(newRow);
table.AcceptChanges();
// 滚动并选中最后一行
int lastIdx = this.advancedDataGridView10.Rows.Count - 1;
if (lastIdx >= 0)
{
this.advancedDataGridView10.FirstDisplayedScrollingRowIndex = lastIdx;
this.advancedDataGridView10.ClearSelection();
this.advancedDataGridView10.Rows[lastIdx].Selected = true;
}
// 保存到数据库
SaveGuiTiToDatabase();
Log("[柜体库] 新增元件: " + dlg.元件名称 + " / " + dlg.规格);
}
}
catch (Exception ex)
{
MessageBox.Show(this, "新增失败:\n" + ex.Message,
"柜体库新增", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 修改 ----------
/// <summary>修改:弹出编辑窗体,把柜体库选中行的数据加载进去供用户修改。</summary>
private void GuiTi_Modify_Click(object sender, EventArgs e)
{
var row = GetFirstSelectedRow(this.advancedDataGridView10);
if (row == null)
{
MessageBox.Show(this, "请先在柜体库中选择要修改的行。",
"柜体库修改", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
this.advancedDataGridView10.EndEdit();
using (var dlg = new 用户库元件编辑())
{
dlg.Text = "修改柜体库元件";
// 把当前行的数据加载到窗体
dlg.元件名称 = GuiTiGetField(row, "元件名称");
dlg.规格 = GuiTiGetField(row, "规格");
dlg.单位 = GuiTiGetField(row, "单位");
dlg.数量 = GuiTiGetField(row, "数量");
dlg.表价 = GuiTiGetField(row, "表价");
dlg.采购系数 = GuiTiGetField(row, "采购系数");
dlg.报出系数 = GuiTiGetField(row, "报出系数");
dlg.品牌 = GuiTiGetField(row, "品牌");
dlg.系列 = GuiTiGetField(row, "系列");
dlg.元件类型 = GuiTiGetField(row, "元件类型");
if (dlg.ShowDialog(this) != DialogResult.OK) return;
// 把窗体数据写回选中行(柜体库表格 ReadOnly=true,
// 写入 DataGridViewRow.Cells 需要临时关闭 ReadOnly)
bool prevRO = this.advancedDataGridView10.ReadOnly;
try
{
this.advancedDataGridView10.ReadOnly = false;
SetGuiTiField(row, "元件名称", dlg.元件名称);
SetGuiTiField(row, "规格", dlg.规格);
SetGuiTiField(row, "单位", dlg.单位);
SetGuiTiField(row, "数量", dlg.数量);
SetGuiTiField(row, "表价", dlg.表价);
SetGuiTiField(row, "采购系数", dlg.采购系数);
SetGuiTiField(row, "报出系数", dlg.报出系数);
SetGuiTiField(row, "品牌", dlg.品牌);
SetGuiTiField(row, "系列", dlg.系列);
SetGuiTiField(row, "元件类型", dlg.元件类型);
}
finally
{
this.advancedDataGridView10.ReadOnly = prevRO;
}
// 保存到数据库
var table = this.advancedDataGridView10.DataSource as DataTable;
if (table != null) table.AcceptChanges();
SaveGuiTiToDatabase();
Log("[柜体库] 修改元件: " + dlg.元件名称 + " / " + dlg.规格);
}
}
catch (Exception ex)
{
MessageBox.Show(this, "修改失败:\n" + ex.Message,
"柜体库修改", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 删除 ----------
/// <summary>删除:删除柜体库中所有选中行,并保存到数据库。</summary>
private void GuiTi_Delete_Click(object sender, EventArgs e)
{
var rows = GetAllSelectedRows(this.advancedDataGridView10);
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先在柜体库中选择要删除的行。",
"柜体库删除", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (MessageBox.Show(this, "确认删除选中的 " + rows.Count + " 行吗?",
"柜体库删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
try
{
this.advancedDataGridView10.EndEdit();
// 按索引倒序删除(避免索引错位)
var sortedRows = new System.Collections.Generic.List<DataGridViewRow>(rows);
sortedRows.Sort((a, b) => b.Index.CompareTo(a.Index));
foreach (var r in sortedRows)
{
if (r == null || r.IsNewRow) continue;
this.advancedDataGridView10.Rows.RemoveAt(r.Index);
}
SaveGuiTiToDatabase();
Log("[柜体库] 已删除 " + rows.Count + " 行");
}
catch (Exception ex)
{
MessageBox.Show(this, "删除失败:\n" + ex.Message,
"柜体库删除", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 从柜体库数据库重新查询并绑定到 advancedDataGridView10。
/// 与 ui.cs 的 LoadSqliteData_advancedDataGridView10 同款逻辑,
/// 但放在主窗口.cs 中以便在编辑后主动刷新。
/// </summary>
private void ReloadGuiTiFromDatabase()
{
string dbPath = GuiTiDbPath;
if (!System.IO.File.Exists(dbPath)) return;
try
{
string connStr = GetConnString(dbPath);
var table = new DataTable();
using (var conn = new System.Data.SQLite.SQLiteConnection(connStr))
{
conn.Open();
string sql = "SELECT * FROM " + GuiTiTableName + ";";
using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(sql, conn))
{
adapter.Fill(table);
}
}
this.advancedDataGridView10.AutoGenerateColumns = false;
this.advancedDataGridView10.DataSource = table;
// 同步刷新 comboBox4 的不重复元件名称下拉项
LoadGuiTiDistinctNames();
}
catch (Exception ex)
{
Log("[柜体库] 刷新柜体库表格失败: " + ex.Message);
}
}
/// <summary>
/// 扫描柜体库(advancedDataGridView10)的"元件名称"列,把所有不重复的名称
/// 填入 comboBox4 作为下拉项(首项固定为"全部")。保留当前选中值。
/// </summary>
private void LoadGuiTiDistinctNames()
{
// 取当前选中,稍后恢复(避免刷新时选中被重置)
string prev = this.comboBox4.Text;
var names = new System.Collections.Generic.List<string>();
names.Add("全部");
var table = this.advancedDataGridView10.DataSource as DataTable;
if (table != null && table.Columns.Contains("元件名称"))
{
var set = new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
foreach (DataRow r in table.Rows)
{
if (r == null) continue;
object v = r["元件名称"];
if (v == null || v == DBNull.Value) continue;
string s = v.ToString().Trim();
if (s.Length == 0) continue;
// 用原大小写保留首次出现的写法
if (set.Add(s)) names.Add(s);
}
}
// 临时解绑事件,避免填充项时反复触发筛选
this.comboBox4.SelectedIndexChanged -= GuiTi_NameFilter_Changed;
try
{
this.comboBox4.Items.Clear();
this.comboBox4.Items.AddRange(names.ToArray());
// 恢复选中项
bool found = false;
if (!string.IsNullOrEmpty(prev))
{
foreach (var n in names)
{
if (string.Equals(n, prev, System.StringComparison.OrdinalIgnoreCase))
{ this.comboBox4.Text = prev; found = true; break; }
}
}
if (!found) this.comboBox4.Text = "全部";
}
finally
{
this.comboBox4.SelectedIndexChanged += GuiTi_NameFilter_Changed;
}
}
/// <summary>comboBox4 选择改变:按元件名称筛选柜体库。</summary>
private void GuiTi_NameFilter_Changed(object sender, EventArgs e)
{
var table = this.advancedDataGridView10.DataSource as DataTable;
if (table == null) return;
string sel = this.comboBox4.Text == null ? "" : this.comboBox4.Text.Trim();
try
{
if (sel.Length == 0 || sel == "全部")
{
if (table.DefaultView.RowFilter.Length > 0)
table.DefaultView.RowFilter = "";
}
else
{
string escaped = EscapeLikeValue(sel);
// 转换为 LIKE 模式;不区分大小写由 DataView 默认不敏感处理
table.DefaultView.RowFilter = "[元件名称] LIKE '%" + escaped + "%'";
}
}
catch (Exception ex) { Log("[柜体库筛选] 异常: " + ex.Message); }
}
/// <summary>
/// button31 查找:在柜体库按"元件名称"和"规格"两列不区分大小写模糊匹配。
/// 关键词为空则清除筛选。
/// </summary>
private void GuiTi_Search_Click(object sender, EventArgs e)
{
var table = this.advancedDataGridView10.DataSource as DataTable;
if (table == null)
{
MessageBox.Show(this, "柜体库暂无数据,无法查找。", "查找",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string keyword = this.textBox5.Text == null ? "" : this.textBox5.Text.Trim();
// 关键词为空:清除筛选,恢复显示全部(同时把名称筛选也复位)
if (keyword.Length == 0)
{
if (table.DefaultView.RowFilter.Length > 0)
table.DefaultView.RowFilter = "";
return;
}
// 转义 LIKE 特殊字符
string escaped = EscapeLikeValue(keyword);
try
{
// 不区分大小写 + 同时匹配元件名称和规格两列(任一命中即显示)
table.DefaultView.RowFilter =
"[元件名称] LIKE '%" + escaped + "%' OR [规格] LIKE '%" + escaped + "%'";
}
catch (System.Data.SyntaxErrorException ex)
{
MessageBox.Show(this,
"查找条件解析失败,请检查输入的关键词。\n详情:" + ex.Message,
"查找", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (table.DefaultView.Count == 0)
{
MessageBox.Show(this,
"没有找到元件名称或规格包含 [" + keyword + "] 的元件。",
"查找", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
/// <summary>
/// 把柜体库 DataTable 的所有数据整体写回数据库(DELETE + INSERT 全量)。
/// 柜体库没有 PRIMARY KEY,无法 UPDATE 单行,采用全量重建方式。
/// </summary>
private void SaveGuiTiToDatabase()
{
string dbPath = GuiTiDbPath;
if (!System.IO.File.Exists(dbPath)) return;
try
{
var table = this.advancedDataGridView10.DataSource as DataTable;
if (table == null) return;
string[] guiTiCols = { "元件名称", "规格", "单位", "数量", "表价",
"采购系数", "报出系数", "品牌", "系列", "元件类型" };
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// 先清空
using (var cmdDel = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + GuiTiTableName + ";", conn))
{
cmdDel.ExecuteNonQuery();
}
// 重新插入所有行
foreach (DataRow r in table.Rows)
{
if (r.RowState == DataRowState.Deleted) continue;
string sql = "INSERT INTO " + GuiTiTableName +
" (元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数, 品牌, 系列, 元件类型) " +
"VALUES (@元件名称, @规格, @单位, @数量, @表价, @采购系数, @报出系数, @品牌, @系列, @元件类型);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
foreach (string c in guiTiCols)
cmd.Parameters.AddWithValue("@" + c,
table.Columns.Contains(c) && r[c] != null ? r[c].ToString() : "");
cmd.ExecuteNonQuery();
}
}
}
table.AcceptChanges();
Log("[柜体库] 已保存到数据库");
// 保存后同步刷新 comboBox4 的下拉项(让新加进来的元件名称立即出现)
LoadGuiTiDistinctNames();
}
catch (Exception ex)
{
Log("[柜体库] 保存失败: " + ex.Message);
}
}
// ============================================================
// 数据库 CRUD:图号/箱号/元件(全部带关联字段)
// ============================================================
/// <summary>计算同级别下一个序号字符串(用于默认命名提示)。在当前项目的独立库内计数。</summary>
private string GetNextSeqNumber(string tableName, string nameCol,
string projectId, string tuhao = null)
{
if (string.IsNullOrWhiteSpace(projectId)) return "1";
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return "1";
string sql;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
if (tuhao == null)
{
sql = "SELECT COUNT(*) FROM " + tableName + ";";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
int n = 0;
object o = cmd.ExecuteScalar();
if (o != null && o != DBNull.Value) n = Convert.ToInt32(o);
return (n + 1).ToString();
}
}
else
{
sql = "SELECT COUNT(*) FROM " + tableName + " WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@th", tuhao);
int n = 0;
object o = cmd.ExecuteScalar();
if (o != null && o != DBNull.Value) n = Convert.ToInt32(o);
return (n + 1).ToString();
}
}
}
}
catch
{
return "1";
}
}
// ---------- 图号 CRUD ----------
private void InsertTuhao(string projectId, string tuhao)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
// 台数默认为 1(总价/成本总价/毛利公式依赖台数)
// 公式列给默认值 0,避免空值导致 ValueType=double 时报 DataError
// ★ 内部ID = 创建时的图号名,永远不变;表索引和 _currentTuhao 用它做关联
string sql = "INSERT INTO " + TuhaoTableName +
" (序号, 图号, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 内部ID) " +
"VALUES (@seq, @th, @taishu, 0, 0, 0, 0, 0, 0, @iid);";
string seq = GetNextSeqNumber(TuhaoTableName, "图号", projectId);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.Parameters.AddWithValue("@taishu", "1");
cmd.Parameters.AddWithValue("@iid", tuhao); // 内部ID = 创建时的图号名
cmd.ExecuteNonQuery();
}
}
}
private void DeleteTuhao(string projectId, string tuhao)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// 1) 先查出该图号的所有箱号表/元件表真实表名(收集到列表,避免 reader 嵌套执行)
var tablesToDrop = new System.Collections.Generic.List<string>();
string selIdx = "SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;";
using (var sel = new System.Data.SQLite.SQLiteCommand(selIdx, conn))
{
sel.Parameters.AddWithValue("@th", tuhao);
using (var reader = sel.ExecuteReader())
{
while (reader.Read())
{
string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(realTbl)) tablesToDrop.Add(realTbl);
}
}
}
// 2) reader 关闭后,逐个 DROP
foreach (string realTbl in tablesToDrop)
{
string dropSql = "DROP TABLE IF EXISTS \"" + realTbl + "\";";
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(dropSql, conn))
{
dropCmd.ExecuteNonQuery();
}
}
// 3) 删图号本身(★ 按内部ID删,用户改图号名不影响删除)
string delTh = "DELETE FROM " + TuhaoTableName + " WHERE 内部ID = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delTh, conn))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.ExecuteNonQuery();
}
// 4) 删该图号的所有索引行(箱号+元件)
string delIdx = "DELETE FROM 表索引 WHERE 图号 = @th;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delIdx, conn))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.ExecuteNonQuery();
}
}
}
// ---------- 箱号 CRUD ----------
private void InsertXianghao(string projectId, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(projectId) ||
string.IsNullOrWhiteSpace(tuhao)) return;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
// 查索引表:该图号是否已有箱号表?没有就生成ID建表+登记索引
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);
// 数量默认为 1(成本价/报出价公式依赖数量)
// 单位默认为"台"(对应 Excel 箱号表 F2 的默认值)
// 公式列给默认值 0,避免空值导致 ValueType=double 时报 DataError
// ★ 内部ID = 创建时的箱号名,永远不变;表索引和 _currentXianghao 用它做关联
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 箱号, 箱名, 单位, 数量," +
" 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 内部ID) " +
"VALUES (@seq, @xh, @xm, @unit, @shuliang," +
" 0, 0, 0, 0, 0, 0, 0, @iid);";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@xh", xianghao);
cmd.Parameters.AddWithValue("@xm", ""); // 箱名默认空,由用户在表格里填写
cmd.Parameters.AddWithValue("@unit", "台");
cmd.Parameters.AddWithValue("@shuliang", "1");
cmd.Parameters.AddWithValue("@iid", xianghao); // 内部ID = 创建时的箱号名
cmd.ExecuteNonQuery();
}
}
}
/// <summary>按真实ID表名建一张空箱号表。</summary>
private void CreateXianghaoTable(string projectId, string realTableName)
{
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql =
"CREATE TABLE \"" + realTableName + "\" (" +
" 序号 TEXT, 箱号 TEXT, 箱名 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," +
" 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," +
" 内部ID TEXT" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.ExecuteNonQuery();
}
}
// ★ 公式触发器兜底:重建项目库全部触发器(幂等,新表即刻具备自动计算能力)
EnsureFormulaTriggersQuiet(projectId);
}
catch { }
}
/// <summary>按真实ID表名建一张空元件表。</summary>
private void CreateYuanjianTable(string projectId, string realTableName)
{
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql =
"CREATE TABLE \"" + realTableName + "\" (" +
" 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," +
" 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," +
" 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.ExecuteNonQuery();
}
}
// ★ 公式触发器兜底:重建项目库全部触发器(幂等)
EnsureFormulaTriggersQuiet(projectId);
}
catch { }
}
private void DeleteXianghao(string projectId, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(projectId) ||
string.IsNullOrWhiteSpace(tuhao)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return;
// 查索引表拿该箱号的元件表真实表名,DROP 它,并删索引行
string yjTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
if (!string.IsNullOrEmpty(yjTbl))
{
string dropYj = "DROP TABLE IF EXISTS \"" + yjTbl + "\";";
using (var cmd = new System.Data.SQLite.SQLiteCommand(dropYj, conn))
{
cmd.ExecuteNonQuery();
}
}
// 从该图号的箱号表里删该箱号行(★ 按内部ID删,用户改箱号名不影响删除)
string xhTbl = GetXianghaoTableByIndex(projectId, tuhao);
if (!string.IsNullOrEmpty(xhTbl))
{
string delXh = "DELETE FROM \"" + xhTbl + "\" WHERE 内部ID = @xh;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(delXh, conn))
{
cmd.Parameters.AddWithValue("@xh", xianghao);
cmd.ExecuteNonQuery();
}
}
}
// 删索引行(元件)
RemoveTableIndex(projectId, "元件", tuhao, xianghao);
}
// ---------- 元件 CRUD ----------
/// <summary>确保指定箱号的元件表存在,不存在则创建空表+登记索引</summary>
private void EnsureYuanjianTableExists(string projectId, string tuhao, string xianghao)
{
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(tblName))
{
EnsureQuoteDbExists(projectId);
tblName = GetNextTableId(projectId, "yj");
CreateYuanjianTable(projectId, tblName);
AddTableIndex(projectId, "元件", tuhao, xianghao, tblName);
}
}
/// <summary>在指定箱号的元件表 INSERT 一行默认元件,返回新行 rowid(-1=失败)。
/// ★ 序号用 MAX(CAST(序号))+1 而不是 COUNT+1:删除行后序号有洞(如 1,2,5)时,
/// COUNT+1 会排不到最后,覆盖"末行"的调用方就会盖错行(网页插入规格/表价为空的根因)。</summary>
private long InsertYuanjian(string projectId, string tuhao, string xianghao, string name)
{
if (string.IsNullOrWhiteSpace(projectId) ||
string.IsNullOrWhiteSpace(xianghao)) return -1;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
// 查索引表:该箱号是否已有元件表?没有就生成ID建表+登记索引
EnsureYuanjianTableExists(projectId, tuhao, xianghao);
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
// 元件序号:取最大序号+1(序号有洞时也保证排在最后)
// ★ 费用行置底(2026-10-01):取【非费用行】的最大序号+1——新普通元件排普通区末尾,
// 费用行序号保持更大,永远在最底;顺带幂等补"类型"列(老表可能没有)
string seq;
using (var conn0 = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn0.Open();
EnsureColumns(conn0, "\"" + tblName + "\"", new[] { "类型" });
string cntSql = "SELECT IFNULL(MAX(CAST(序号 AS INTEGER)),0)+1 FROM \"" + tblName +
"\" WHERE IFNULL(类型,'')<>'费用';";
using (var c = new System.Data.SQLite.SQLiteCommand(cntSql, conn0))
{
object o = c.ExecuteScalar();
seq = o == null || o == DBNull.Value ? "1" : Convert.ToString(o);
}
}
// 公式列给默认值 0,避免空值导致 ValueType=double 时报 DataError
// 类型默认填"元件"(右键新建的就是元件,与插入元件按钮一致)
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 元件名称, 单位, 数量, 采购系数, 报出系数," +
" 表价, 采购单价, 采购价, 报出单价, 报出价, 类型) " +
"VALUES (@seq, @name, @unit, @shuliang, @caigou, @baochu," +
" 0, 0, 0, 0, 0, '元件');";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@unit", "只");
cmd.Parameters.AddWithValue("@shuliang", "1");
cmd.Parameters.AddWithValue("@caigou", "1");
cmd.Parameters.AddWithValue("@baochu", "1");
cmd.ExecuteNonQuery();
return conn.LastInsertRowId; // 新行 rowid,调用方按它定位覆盖
}
}
}
/// <summary>网页插入专用:一次 INSERT 完整字段的新行(规格/表价/品牌/系数/系列 全带上),
/// 不再"插默认空行再覆盖"——彻底杜绝空行中间态。返回新行 rowid。
/// 数量默认1、单位只、报出系数默认1、类型元件;采购价/报出价按公式算。</summary>
private long InsertYuanjianFull(string projectId, string tuhao, string xianghao,
string 名称, string 规格, decimal 表价, string 品牌, string 采购系数, string 系列名, decimal 数量 = 1m)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(xianghao)) return -1;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
EnsureYuanjianTableExists(projectId, tuhao, xianghao);
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
// 序号=最大序号+1(删除行后有洞也排最后)
// ★ 费用行置底(2026-10-01):取【非费用行】的最大序号+1,新元件排普通区末尾(费用行永远最底);
// 顺带幂等补"类型"列(老表可能没有)
string seq;
using (var conn0 = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn0.Open();
EnsureColumns(conn0, "\"" + tblName + "\"", new[] { "类型" });
string cntSql = "SELECT IFNULL(MAX(CAST(序号 AS INTEGER)),0)+1 FROM \"" + tblName +
"\" WHERE IFNULL(类型,'')<>'费用';";
using (var c = new System.Data.SQLite.SQLiteCommand(cntSql, conn0))
{
object o = c.ExecuteScalar();
seq = o == null || o == DBNull.Value ? "1" : Convert.ToString(o);
}
}
decimal 系数 = 1m;
decimal.TryParse(采购系数, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out 系数);
if (系数 == 0m) 系数 = 1m;
if (数量 < 1m) 数量 = 1m;
decimal 采购单价 = 表价 * 系数;
decimal 采购价 = 采购单价 * 数量;
decimal 报出单价 = 采购单价 * 1m;
decimal 报出价 = 报出单价 * 数量;
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 元件名称, 规格, 单位, 数量, 表价, 采购系数, 报出系数," +
" 采购单价, 采购价, 报出单价, 报出价, 品牌, 系列, 类型, 内部ID) " +
"VALUES (@seq, @name, @spec, @unit, @shuliang, @biao, @caigou, @baochu," +
" @cgdj, @cgj, @bcdj, @bcj, @brand, @xilie, '元件', @nid);";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ A1(2026-09-30):新元件行写内部ID GUID(统一定位体系;表无该列时迁移已补)
try
{
using (var ec = new System.Data.SQLite.SQLiteCommand(
"ALTER TABLE \"" + tblName + "\" ADD COLUMN 内部ID TEXT;", conn)) { ec.ExecuteNonQuery(); }
}
catch { /* 列已存在 */ }
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@nid", System.Guid.NewGuid().ToString("N"));
cmd.Parameters.AddWithValue("@seq", seq);
cmd.Parameters.AddWithValue("@name", 名称 ?? "");
cmd.Parameters.AddWithValue("@spec", 规格 ?? "");
cmd.Parameters.AddWithValue("@unit", "只");
cmd.Parameters.AddWithValue("@shuliang", 数量.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture));
cmd.Parameters.AddWithValue("@biao", (double)表价);
cmd.Parameters.AddWithValue("@caigou", 系数.ToString(System.Globalization.CultureInfo.InvariantCulture));
cmd.Parameters.AddWithValue("@baochu", "1");
cmd.Parameters.AddWithValue("@cgdj", (double)采购单价);
cmd.Parameters.AddWithValue("@cgj", (double)采购价);
cmd.Parameters.AddWithValue("@bcdj", (double)报出单价);
cmd.Parameters.AddWithValue("@bcj", (double)报出价);
cmd.Parameters.AddWithValue("@brand", 品牌 ?? "");
cmd.Parameters.AddWithValue("@xilie", 系列名 ?? "");
cmd.ExecuteNonQuery();
return conn.LastInsertRowId;
}
}
}
/// <summary>★ 事务版删除图号:DROP 其下箱号表/元件表 + 删图号行 + 清索引(批量删除共用事务)。
/// tuhaoNm=该行图号显示名:按显示名兜底清理扒图 Form3 按显示名登记的残留索引/表
/// (2026-09-21 残留根治——此前这类孤儿元件表被 AI 清单全量枚举读到)。</summary>
private void DeleteTuhaoTx(string projectId, string tuhao, string tuhaoNm,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
// 1) 收集该图号的所有动态表名
var tablesToDrop = new System.Collections.Generic.List<string>();
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名 FROM 表索引 WHERE 图号 = @th;", conn, tx))
{
sel.Parameters.AddWithValue("@th", tuhao);
using (var reader = sel.ExecuteReader())
{
while (reader.Read())
{
string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(realTbl)) tablesToDrop.Add(realTbl);
}
}
}
// 2) 逐个 DROP
foreach (string realTbl in tablesToDrop)
{
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + realTbl + "\";", conn, tx))
dropCmd.ExecuteNonQuery();
}
// 3) 删图号行(按内部ID;显示名兜底在步骤5)
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + TuhaoTableName + " WHERE 内部ID = @th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.ExecuteNonQuery();
}
// 4) 删索引行
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 图号 = @th;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.ExecuteNonQuery();
}
// 5) ★ 显示名兜底:扒图 Form3 按显示名登记 表索引/建表(无内部ID),上面按内部ID
// 匹配不到 → 孤儿 xh_/yj_ 表+索引行残留。按显示名(项目内图号名唯一)再清一遍;
// 同时删掉按显示名登记的图号行(老数据内部ID为空的行)。
if (!string.IsNullOrWhiteSpace(tuhaoNm))
{
var more = new System.Collections.Generic.List<string>();
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名 FROM 表索引 WHERE 图号 = @n;", conn, tx))
{
sel.Parameters.AddWithValue("@n", tuhaoNm);
using (var reader = sel.ExecuteReader())
{
while (reader.Read())
{
string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(realTbl)) more.Add(realTbl);
}
}
}
foreach (string realTbl in more)
{
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + realTbl + "\";", conn, tx))
dropCmd.ExecuteNonQuery();
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + TuhaoTableName + " WHERE 图号 = @n;", conn, tx))
{
cmd.Parameters.AddWithValue("@n", tuhaoNm);
cmd.ExecuteNonQuery();
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 图号 = @n;", conn, tx))
{
cmd.Parameters.AddWithValue("@n", tuhaoNm);
cmd.ExecuteNonQuery();
}
}
}
/// <summary>★ 事务版删除箱号:DROP 元件表 + 删箱号行 + 删元件索引行。
/// thNm/xhNm=图号/箱号显示名:按显示名组合兜底清理扒图 Form3 按显示名登记的
/// 孤儿元件表/索引行(2026-09-21 残留根治,AI 清单此前会读到这些已删数据)。</summary>
private void DeleteXianghaoTx(string projectId, string tuhao, string tuhaoNm, string xianghao, string xianghaoNm,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string yjTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (!string.IsNullOrEmpty(yjTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + yjTbl + "\";", conn, tx))
cmd.ExecuteNonQuery();
}
string xhTbl = GetXianghaoTableByIndex(projectId, tuhao);
if (!string.IsNullOrEmpty(xhTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + xhTbl + "\" WHERE 内部ID = @xh;", conn, tx))
{
cmd.Parameters.AddWithValue("@xh", xianghao);
cmd.ExecuteNonQuery();
}
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 类型='元件' AND 图号=@th AND 箱号=@xh;", conn, tx))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.Parameters.AddWithValue("@xh", xianghao);
cmd.ExecuteNonQuery();
}
// ★ 显示名组合兜底:扒图 Form3 建的元件索引按(图号显示名,箱号显示名)登记,
// 上面按内部ID组合匹配不到 → yj 表与索引行残留。同组合清一遍;
// 箱号表里的显示名行(老数据内部ID为空)一并删除。
if (!string.IsNullOrWhiteSpace(tuhaoNm) && !string.IsNullOrWhiteSpace(xianghaoNm))
{
var more = new System.Collections.Generic.List<string>();
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名 FROM 表索引 WHERE 类型='元件' AND 图号=@tn AND 箱号=@xn;", conn, tx))
{
sel.Parameters.AddWithValue("@tn", tuhaoNm);
sel.Parameters.AddWithValue("@xn", xianghaoNm);
using (var reader = sel.ExecuteReader())
{
while (reader.Read())
{
string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(realTbl)) more.Add(realTbl);
}
}
}
foreach (string realTbl in more)
{
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + realTbl + "\";", conn, tx))
dropCmd.ExecuteNonQuery();
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 类型='元件' AND 图号=@tn AND 箱号=@xn;", conn, tx))
{
cmd.Parameters.AddWithValue("@tn", tuhaoNm);
cmd.Parameters.AddWithValue("@xn", xianghaoNm);
cmd.ExecuteNonQuery();
}
if (!string.IsNullOrEmpty(xhTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + xhTbl + "\" WHERE 箱号 = @xn;", conn, tx))
{
cmd.Parameters.AddWithValue("@xn", xianghaoNm);
cmd.ExecuteNonQuery();
}
}
}
}
/// <summary>★ 事务版删除元件行(按序号)。</summary>
private void DeleteYuanjianTx(string projectId, string tuhao, string xianghao, string seq,
System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx)
{
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(tblName)) return;
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + tblName + "\" WHERE 序号 = @seq;", conn, tx))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.ExecuteNonQuery();
}
}
private void DeleteYuanjian(string projectId, string tuhao, string xianghao, string seq)
{
if (string.IsNullOrWhiteSpace(projectId) ||
string.IsNullOrWhiteSpace(xianghao)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!System.IO.File.Exists(dbPath)) return;
// 查索引表拿该箱号元件表的真实表名,按序号删
string realTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(realTbl)) return;
string sql = "DELETE FROM \"" + realTbl + "\" WHERE 序号 = @seq;";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.ExecuteNonQuery();
}
}
}
// ---------- 项目级联删除(删除项目时直接删除该项目的独立报价库文件)----------
private void CascadeDeleteByProject(string projectId)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
删除报价库文件必须成功(GetQuoteDbPath(projectId), projectId);
}
/// <summary>彻底删除项目报价库文件(.db 及 -wal/-shm 伴随文件)——删不掉必须报错,绝不静默放弃。
/// ★ 池化连接(Pooling=True)会持有文件句柄,直接删必失败——先 ClearAllPools 再删
/// (方案库删除的已验证做法),按项目复制 File.Copy 兜底同款节奏重试 3 次
/// (ClearAllPools + GC + 等待 + 递增延时)。仍失败则抛异常:残留文件会被同编号的
/// 新建项目"复活"旧数据(实测:删了项目再建同编号,打开全是旧数据),必须让用户看到。</summary>
private static void 删除报价库文件必须成功(string dbPath, string projectId)
{
if (!System.IO.File.Exists(dbPath)) return;
System.Exception lastErr = null;
for (int attempt = 1; attempt <= 3; attempt++)
{
try
{
System.Data.SQLite.SQLiteConnection.ClearAllPools();
GC.Collect();
GC.WaitForPendingFinalizers();
System.Threading.Thread.Sleep(50 * attempt);
System.IO.File.Delete(dbPath);
// WAL 日志/共享内存伴随文件一并清掉(残留会让下次打开读到旧事务)
foreach (string ext in new[] { "-wal", "-shm" })
{
string p = dbPath + ext;
if (System.IO.File.Exists(p)) { try { System.IO.File.Delete(p); } catch { } }
}
return;
}
catch (System.Exception ex)
{
lastErr = ex;
写日志("[删除项目] 删库文件第 " + attempt + " 次失败 [" + projectId + "]: " + ex.Message);
}
}
throw new System.Exception("项目数据库文件删除失败(可能被其他程序占用,请关闭后重试):\n" +
dbPath + "\n" + lastErr.Message);
}
// 提示:在设计器的属性窗口(事件页)双击事件名,
// 会自动在此生成事件处理方法骨架。
}
}