9316 lines
500 KiB
C#
9316 lines
500 KiB
C#
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 的静态包装,供静态方法调用。
|
||
/// ★ A2(2026-10-01):统一收敛到 数据库连接.串(一处管理),本方法保留签名做转发。</summary>
|
||
private static string GetConnStringStatic(string dbPath)
|
||
{
|
||
return 数据库连接.串(dbPath);
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
|
||
// 提示:在设计器的属性窗口(事件页)双击事件名,
|
||
// 会自动在此生成事件处理方法骨架。
|
||
}
|
||
}
|