Files
ctbjrj/成套报价软件/扒图/Cad/Form3.cs
T

7464 lines
370 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using Microsoft.Win32;
using Zuby.ADGV;
namespace MyApp
{
/// <summary>
/// Form3 的交互逻辑(CAD 2018 插件报价窗口)。
/// 功能与主项目报价页面一致:项目表 + 图号/箱号/元件三级联动 + 公式重算 + 右键菜单 + 项目导出。
/// UI 布局定义见 Form3.ui.cs。数据库独立存放在 cadcj\sjk\ 目录。
/// </summary>
public partial class Form3
{
// ============================================================
// 数据库路径与表名常量(独立于主项目,存放在 cadcj\sjk\ 下)
// ============================================================
private const string ProjectsDbPath = @"sjk\advancedDataGridView6Table.db";
private const string ProjectsTableName = "advancedDataGridView6Table";
private const string QuoteDbDir = @"sjk\bjb";
private const string TuhaoTableName = "advancedDataGridView9Table";
private const string ProjectInfoTableName = "项目信息";
private const string IndexTableName = "表索引";
private const string CadRegPath = @"SOFTWARE\Autodesk\AutoCAD\R22.0\ACAD-1001:804\Apps\CADCJ";
/// <summary>
/// 插件所在目录(cadcj)。优先从注册表读取 CAD 自动加载的 DLL 路径(最可靠),
/// 然后尝试 CodeBase / Location,最后兜底 BaseDirectory(可能是 CAD 目录,会报错)。
/// </summary>
private static readonly string _baseDir = GetPluginBaseDir();
private static string GetPluginBaseDir()
{
// ★ 方案0(2026-09-10 主程序托管版):数据根固定为 cadbt 子目录(与独立 cadbt.exe 共用同一套 sjk)
// 安装布局 {app}\cadbt\;开发机回退 主程序bin\Debug 上两级\cadbt\cadbt\bin\Debug
try
{
var appDir = AppDomain.CurrentDomain.BaseDirectory;
var cand1 = System.IO.Path.Combine(appDir, "cadbt");
if (System.IO.Directory.Exists(cand1))
{
WriteDebugLog("[GetPluginBaseDir] cadbt子目录成功: " + cand1);
return cand1;
}
var cand2 = System.IO.Path.GetFullPath(System.IO.Path.Combine(appDir, @"..\..\cadbt\cadbt\bin\Debug"));
if (System.IO.Directory.Exists(cand2))
{
WriteDebugLog("[GetPluginBaseDir] 开发机回退成功: " + cand2);
return cand2;
}
}
catch (Exception ex) { WriteDebugLog("[GetPluginBaseDir] 托管目录解析异常: " + ex.Message); }
// ★ 方案2:CodeBase(不受 shadow copy 影响)
try
{
var codeBase = System.Reflection.Assembly.GetExecutingAssembly().CodeBase;
WriteDebugLog("[GetPluginBaseDir] CodeBase=" + codeBase);
if (!string.IsNullOrEmpty(codeBase))
{
var localPath = new Uri(codeBase).LocalPath;
var dir = Path.GetDirectoryName(localPath);
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
{
WriteDebugLog("[GetPluginBaseDir] CodeBase方案成功: " + dir);
return dir;
}
}
}
catch (Exception ex) { WriteDebugLog("[GetPluginBaseDir] CodeBase方案异常: " + ex.Message); }
// ★ 方案3:Location
try
{
var loc = System.Reflection.Assembly.GetExecutingAssembly().Location;
WriteDebugLog("[GetPluginBaseDir] Location=" + loc);
if (!string.IsNullOrEmpty(loc))
{
var dir = Path.GetDirectoryName(loc);
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
{
WriteDebugLog("[GetPluginBaseDir] Location方案成功: " + dir);
return dir;
}
}
}
catch (Exception ex) { WriteDebugLog("[GetPluginBaseDir] Location方案异常: " + ex.Message); }
// ★ 方案4(最后兜底):BaseDirectory(CAD 目录,数据库会找不到)
var fallback = AppDomain.CurrentDomain.BaseDirectory;
WriteDebugLog("[GetPluginBaseDir] ⚠ 兜底BaseDirectory=" + fallback + " (数据库会找不到!)");
return fallback;
}
private static void WriteDebugLog(string msg)
{
try
{
var logDir = Path.Combine(
Path.GetDirectoryName(
new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath),
"sjk");
if (!Directory.Exists(logDir)) logDir = Path.GetTempPath();
var logPath = Path.Combine(logDir, "form3_path_debug.log");
File.AppendAllText(logPath,
"[" + DateTime.Now.ToString("HH:mm:ss") + "] " + msg + Environment.NewLine);
}
catch { }
}
// ============================================================
// 当前层级选中状态(联动核心)
// ============================================================
private string _currentProjectId = "";
private string _currentTuhao = "";
private string _currentXianghao = "";
private bool _isReloading = false;
private bool _isRecalculating = false;
private DateTime _lastRecalcTime = DateTime.MinValue;
// 价格显示格式化配置(每个项目独立,随项目信息表加载)
private string _单台小数位文本 = "保留2位小数";
private string _单台取整方式 = "不取整";
private string _项目小数位文本 = "保留2位小数";
private string _项目取整方式 = "不取整";
// 当前各表的内存数据(用于关闭/切换时回写数据库)
private DataTable _tuhaoTable;
private DataTable _xianghaoTable;
private DataTable _yuanjianTable;
// 内存缓存:切换图号/箱号时不丢编辑,点保存才写库
private Dictionary<string, DataTable> _xianghaoCache = new Dictionary<string, DataTable>();
private Dictionary<string, DataTable> _yuanjianCache = new Dictionary<string, DataTable>();
// 右键菜单组件(运行时创建,不在设计器 XML 里)
private ContextMenuStrip _menuTuhao;
private ContextMenuStrip _menuXianghao;
private ContextMenuStrip _menuYuanjian;
private static readonly string[] ProjectInfoColumns =
{
"项目编号", "项目名称", "客户名称", "客户地址",
"联系人", "联系电话", "项目日期", "备注",
"成套厂名称", "成套厂地址", "成套厂联系人", "成套厂联系电话",
"单台小数位", "单台取整方式", "项目小数位", "项目取整方式"
};
/// <summary>公式计算列集合(只包含程序算出来的价格列)。
/// ★ 口径(2026-09-29):毛利率(百分数值)入集合获得小数位格式化;
/// 利润率(小数)不入集合,保持原样显示。</summary>
private static readonly HashSet<string> s_公式列集合 =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"表价", "采购单价", "采购价", "报出单价", "报出价",
"单台成本", "成本价", "单台报出", "利润", "毛利率",
"单价", "总价", "成本总价", "毛利",
"成本总价", "报出总价", "利润"
};
/// <summary>构造函数:初始化 UI 并绑定事件。</summary>
public Form3()
{
InitializeComponent();
// ★ ADGV 右键多选保持(消息钩子版):右键点已选中的行不清空多选集合。
// 原做法是项目内子类 AdgvKeepMulti 覆写 WndProc——但"设计器+项目内自定义类型"
// 在 SharpDevelop 设计时解析是死结,故改回 DLL 类型+NativeWindow 钩子(语义一致)。
_keepMultiHooks = new System.Collections.Generic.List<AdgvMsgHook>();
HookKeepMulti(advancedDataGridView6);
HookKeepMulti(advancedDataGridView9);
HookKeepMulti(advancedDataGridView4);
HookKeepMulti(advancedDataGridView5);
// ★ 行头选择(2026-09-30):扒图窗口全部表格改 RowHeaderSelect
// (点行头=选中整行,Ctrl/Shift+点行头=多选整行,Excel 风格)
成套报价软件.MainForm.ApplyRowHeaderSelectAll(this);
// ★ 立即创建数据库(不依赖 Load 事件,UserControl 嵌入 PaletteSet 时 Load 可能延迟)
try
{
Log("[Form3] _baseDir = " + _baseDir);
Log("[Form3] ProjectsDbPath = " + GetDbFullPath(ProjectsDbPath));
EnsureProjectDbExists(GetDbFullPath(ProjectsDbPath));
Log("[Form3] 数据库创建/验证成功");
}
catch (System.Exception ex)
{
Log("[Form3] 数据库创建失败: " + ex.Message);
}
// ★ 诊断:后续初始化步骤逐步打点(定位面板空白问题)
try
{
Log("[Form3.init] 开始双缓冲设置");
EnableDoubleBuffered(this.advancedDataGridView6);
EnableDoubleBuffered(this.advancedDataGridView9);
EnableDoubleBuffered(this.advancedDataGridView4);
EnableDoubleBuffered(this.advancedDataGridView5);
Log("[Form3.init] 双缓冲完成");
ApplyReadOnlyColumns(this.advancedDataGridView6, new string[] {
"项目编号", "项目名称", "客户名称", "客户地址", "联系人", "联系电话",
"项目日期", "箱柜台数", "成本总价", "报出总价", "利润", "利润率", "备注"
});
ApplyReadOnlyColumns(this.advancedDataGridView9, new string[] {
"序号", "箱柜数量", "单价", "总价", "成本总价", "毛利", "毛利率"
});
ApplyReadOnlyColumns(this.advancedDataGridView4, new string[] {
"序号", "单台成本", "成本价", "单台报出", "报出价", "利润", "利润率", "毛利率"
});
ApplyReadOnlyColumns(this.advancedDataGridView5, new string[] {
"序号", "采购单价", "采购价", "报出单价", "报出价"
});
Log("[Form3.init] 只读列完成");
// 工具栏按钮事件
this.toolStripButton1.Click += toolStripButton1_Click;
this.toolStripButton5.Click += toolStripButton5_Click;
this.toolStripButton6.Click += toolStripButton6_Click;
this.toolStripButton2.Click += (s, e) => SendCadPaCommand("CADPATU");
this.toolStripButton3.Click += (s, e) => SendCadPaCommand("CADPAXH");
this.toolStripButton4.Click += (s, e) => SendCadPaCommand("CADPAYJ");
this.toolStripButton7.Click += (s, e) => ShowDeepSeekSettings();
// ★ 工具栏按钮已搬到主窗口功能区(CAD扒图页签),原工具栏隐藏(保留代码可随时恢复)
this.toolStrip1.Visible = false;
Log("[Form3.init] 工具栏事件完成");
this.button28.Click += button28_Click;
this.advancedDataGridView6.CellDoubleClick += advancedDataGridView6_CellDoubleClick;
this.advancedDataGridView9.SelectionChanged += advancedDataGridView9_SelectionChanged;
this.advancedDataGridView4.SelectionChanged += advancedDataGridView4_SelectionChanged;
this.advancedDataGridView5.SelectionChanged += advancedDataGridView5_SelectionChanged;
this.advancedDataGridView9.CellClick += Grid_CellClick_Zoom;
this.advancedDataGridView4.CellClick += Grid_CellClick_Zoom;
this.advancedDataGridView5.CellClick += Grid_CellClick_Zoom;
this.advancedDataGridView5.CellValueChanged += Yuanjian_CellValueChanged;
this.advancedDataGridView4.CurrentCellDirtyStateChanged += Grid_DirtyStateChanged;
this.advancedDataGridView4.CellValueChanged += Xianghao_CellValueChanged;
this.advancedDataGridView9.CurrentCellDirtyStateChanged += Grid_DirtyStateChanged;
this.advancedDataGridView9.CellValueChanged += Tuhao_CellValueChanged;
this.advancedDataGridView5.CellEndEdit += Grid_CellEndEdit;
this.advancedDataGridView4.CellEndEdit += Grid_CellEndEdit;
this.advancedDataGridView9.CellEndEdit += Grid_CellEndEdit;
this.advancedDataGridView5.DataError += (s, e) => { e.Cancel = true; };
this.advancedDataGridView4.DataError += (s, e) => { e.Cancel = true; };
this.advancedDataGridView9.DataError += (s, e) => { e.Cancel = true; };
this.advancedDataGridView6.DataError += (s, e) => { e.Cancel = true; };
this.advancedDataGridView9.MouseDown += Grid_MouseDown_KeepMultiSelect;
this.advancedDataGridView4.MouseDown += Grid_MouseDown_KeepMultiSelect;
this.advancedDataGridView5.MouseDown += Grid_MouseDown_KeepMultiSelect;
this.advancedDataGridView9.KeyDown += Grid_KeyDown_Delete;
this.advancedDataGridView4.KeyDown += Grid_KeyDown_Delete;
this.advancedDataGridView5.KeyDown += Grid_KeyDown_Delete;
Log("[Form3.init] 表格事件完成");
BuildContextMenus();
Log("[Form3.init] 右键菜单完成");
this.Disposed += Form3_Disposed;
this.Load += (s, e) =>
{
try
{
EnsureProjectDbExists(GetDbFullPath(ProjectsDbPath));
ReloadProjectsFromDatabase();
ApplyPriceFormat(this.advancedDataGridView6, _项目小数位文本);
this.advancedDataGridView9.DataSource = null;
this.advancedDataGridView4.DataSource = null;
this.advancedDataGridView5.DataSource = null;
Log("[Form3.Load] 初始化完成");
}
catch (System.Exception ex)
{
Log("[Form3.Load] 初始化异常: " + ex.Message);
}
try
{
// 网页 CAD 桥:订阅实体删除事件(用户在网页里删框时联动删行)
成套报价软件.CadBridge.EntityErased -= Database_ObjectErased;
成套报价软件.CadBridge.EntityErased += Database_ObjectErased;
// 网页 CAD 桥:订阅扒图结果事件(框选完成立即插入表格)
成套报价软件.CadBridge.PaPicked -= OnPaTextsPicked;
成套报价软件.CadBridge.PaPicked += OnPaTextsPicked;
// 网页 CAD 桥:F1/F2 元件合并(焦点在网页时按键由网页转发)
成套报价软件.CadBridge.F1Requested -= OnF1FromBridge;
成套报价软件.CadBridge.F1Requested += OnF1FromBridge;
成套报价软件.CadBridge.F2Requested -= OnF2FromBridge;
成套报价软件.CadBridge.F2Requested += OnF2FromBridge;
成套报价软件.CadBridge.F3Requested -= OnF3FromBridge;
成套报价软件.CadBridge.F3Requested += OnF3FromBridge;
}
catch (System.Exception ex)
{
Log("[Form3.Load] 订阅CadBridge事件失败: " + ex.Message);
}
};
Log("[Form3.init] 构造函数全部完成 ✓");
}
catch (System.Exception ex)
{
Log("[Form3.init] ⚠ 初始化异常: " + ex.ToString());
}
}
/// <summary>
/// ★ 网页 CAD 中实体被删除时触发。如果删除的是 __CADCJ_PA_FRAME 图层上的扒图框,
/// 通过 Handle 查找当前项目下的图号表/箱号表/元件表对应行,同步删除。
/// </summary>
private void Database_ObjectErased(string handle)
{
try
{
if (string.IsNullOrWhiteSpace(handle)) return;
// ★ 如果正在通过表格删除(_isReloading=true),跳过网页 CAD 事件触发的删除
// 避免表格删除和 CAD 事件删除互相干扰导致蒙版/数据不一致
if (_isReloading) return;
Log("[ObjectErased] 触发! Handle=" + handle);
this.BeginInvoke((Action)(() =>
{
try
{
// 再次检查(异步执行时状态可能已变化)
if (_isReloading) { Log("[ObjectErased] _isReloading=true,跳过"); return; }
DeleteRowsByFrameHandle(handle);
}
catch (System.Exception ex)
{
Log("[ObjectErased.BeginInvoke] 异常: " + ex.Message);
}
}));
}
catch (System.Exception ex)
{
Log("[ObjectErased] 异常: " + ex.Message);
}
}
/// <summary>
/// 根据框Handle在当前项目的图号表/箱号表/元件表中删除对应数据行。
/// </summary>
private void DeleteRowsByFrameHandle(string frameHandle)
{
if (string.IsNullOrWhiteSpace(frameHandle)) return;
if (string.IsNullOrWhiteSpace(_currentProjectId)) { Log("[DeleteRowsByFrameHandle] _currentProjectId 为空,放弃"); return; }
_isReloading = true;
try
{
string dbPath = GetQuoteDbPath(_currentProjectId);
if (!File.Exists(dbPath)) { Log("[DeleteRowsByFrameHandle] 数据库不存在: " + dbPath); return; }
bool deleted = false;
string deletedTuhao = null;
string deletedXianghao = null;
// ★ 收集级联删除的框Handle(图号→箱号→元件,删除数据时同步删蒙版)
var cascadeFrames = new List<string>();
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// 1) 图号表
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 图号 FROM " + TuhaoTableName + " WHERE 框Handle=@fh;", conn))
{
sel.Parameters.AddWithValue("@fh", frameHandle);
using (var reader = sel.ExecuteReader())
{
if (reader.Read())
{
deletedTuhao = reader.IsDBNull(0) ? "" : reader.GetString(0);
Log("[DeleteRowsByFrameHandle] 命中图号表: " + deletedTuhao);
}
}
}
if (!string.IsNullOrWhiteSpace(deletedTuhao))
{
// ★ 删除图号前,先收集该图号下所有箱号和元件的框Handle(级联删蒙版)
CollectFrameHandlesForTuhao(_currentProjectId, deletedTuhao, cascadeFrames);
DeleteTuhao(_currentProjectId, deletedTuhao);
deleted = true;
}
if (!deleted)
{
// 2) 箱号表:通过表索引找到所有箱号表,逐个查
List<string> allXhTables = new List<string>();
using (var selIdx = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名, 图号 FROM 表索引 WHERE 类型='箱号';", conn))
using (var rIdx = selIdx.ExecuteReader())
{
while (rIdx.Read())
{
string realTbl = rIdx.IsDBNull(0) ? "" : rIdx.GetString(0);
string th = rIdx.IsDBNull(1) ? "" : rIdx.GetString(1);
if (!string.IsNullOrWhiteSpace(realTbl))
allXhTables.Add(realTbl + "|" + th);
}
}
Log("[DeleteRowsByFrameHandle] 箱号表数量: " + allXhTables.Count);
foreach (string info in allXhTables)
{
string[] parts = info.Split('|');
string realTbl = parts[0];
string th = parts.Length > 1 ? parts[1] : "";
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT 箱号 FROM \"" + realTbl + "\" WHERE 框Handle=@fh;", conn))
{
sel.Parameters.AddWithValue("@fh", frameHandle);
using (var reader = sel.ExecuteReader())
{
if (reader.Read())
{
deletedTuhao = th;
deletedXianghao = reader.IsDBNull(0) ? "" : reader.GetString(0);
Log("[DeleteRowsByFrameHandle] 命中箱号表: 图号=" + th + " 箱号=" + deletedXianghao);
break;
}
}
}
if (!string.IsNullOrWhiteSpace(deletedXianghao)) break;
}
if (!string.IsNullOrWhiteSpace(deletedXianghao))
{
// ★ 删除箱号前,先收集该箱号下元件表的框Handle(级联删蒙版)
CollectFrameHandlesForXianghao(_currentProjectId, deletedTuhao, deletedXianghao, cascadeFrames);
DeleteXianghao(_currentProjectId, deletedTuhao, deletedXianghao);
deleted = true;
}
}
if (!deleted)
{
// 3) 元件表:通过表索引找到所有元件表,逐个查
List<string> allYjTables = new List<string>();
using (var selIdx = new System.Data.SQLite.SQLiteCommand(
"SELECT 实际表名, 图号, 箱号 FROM 表索引 WHERE 类型='元件';", conn))
using (var rIdx = selIdx.ExecuteReader())
{
while (rIdx.Read())
{
string realTbl = rIdx.IsDBNull(0) ? "" : rIdx.GetString(0);
string th = rIdx.IsDBNull(1) ? "" : rIdx.GetString(1);
string xh = rIdx.IsDBNull(2) ? "" : rIdx.GetString(2);
if (!string.IsNullOrWhiteSpace(realTbl))
allYjTables.Add(realTbl + "|" + th + "|" + xh);
}
}
Log("[DeleteRowsByFrameHandle] 元件表数量: " + allYjTables.Count + " 查找Handle=" + frameHandle);
foreach (string info in allYjTables)
{
string[] parts = info.Split('|');
string realTbl = parts[0];
string th = parts.Length > 1 ? parts[1] : "";
string xh = parts.Length > 2 ? parts[2] : "";
// ★ 先列出该表所有的框Handle,排查是否真的写进去了
using (var selAll = new System.Data.SQLite.SQLiteCommand(
"SELECT 序号, 框Handle FROM \"" + realTbl + "\";", conn))
using (var rAll = selAll.ExecuteReader())
{
while (rAll.Read())
{
string s = rAll.IsDBNull(0) ? "" : rAll.GetString(0);
string h = rAll.IsDBNull(1) ? "" : rAll.GetString(1);
Log(" [元件表 " + realTbl + "] 序号=" + s + " 框Handle=[" + h + "]");
}
}
using (var sel = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM \"" + realTbl + "\" WHERE 框Handle=@fh;", conn))
{
sel.Parameters.AddWithValue("@fh", frameHandle);
object cnt = sel.ExecuteScalar();
int matchCount = (cnt != null && cnt != DBNull.Value) ? Convert.ToInt32(cnt) : 0;
if (matchCount > 0)
{
deletedTuhao = th;
deletedXianghao = xh;
Log("[DeleteRowsByFrameHandle] 命中元件表: 图号=" + th + " 箱号=" + xh + " 匹配行数=" + matchCount);
// ★ 直接用框Handle 删除所有匹配行(解决重复行问题)
using (var delCmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + realTbl + "\" WHERE 框Handle=@fh;", conn))
{
delCmd.Parameters.AddWithValue("@fh", frameHandle);
int affected = delCmd.ExecuteNonQuery();
Log("[DeleteRowsByFrameHandle] DELETE 完成,影响行数=" + affected);
}
deleted = true;
break;
}
}
}
}
}
if (deleted)
{
Log("[DeleteRowsByFrameHandle] 删除成功: Handle=" + frameHandle + " cascadeFrames=" + cascadeFrames.Count);
_xianghaoCache.Clear();
_yuanjianCache.Clear();
// 重新编号(直接操作数据库)
if (!string.IsNullOrWhiteSpace(deletedTuhao) && !string.IsNullOrWhiteSpace(deletedXianghao))
{
string yjTbl = GetYuanjianTableByIndex(_currentProjectId, deletedTuhao, deletedXianghao);
if (!string.IsNullOrEmpty(yjTbl))
RenumberTable(_currentProjectId, "\"" + yjTbl + "\"");
string xhTbl = GetXianghaoTableByIndex(_currentProjectId, deletedTuhao);
if (!string.IsNullOrEmpty(xhTbl))
RenumberTable(_currentProjectId, "\"" + xhTbl + "\"");
}
else if (!string.IsNullOrWhiteSpace(deletedTuhao))
{
string xhTbl = GetXianghaoTableByIndex(_currentProjectId, deletedTuhao);
if (!string.IsNullOrEmpty(xhTbl))
RenumberTable(_currentProjectId, "\"" + xhTbl + "\"");
}
// ★ 直接在数据库层面重算(不经过内存表,不触发回写)
try { RecalcAllFromDatabase(_currentProjectId); } catch { }
// ★ 从数据库重新加载到 UI(覆盖内存表,确保 UI 与数据库一致)
if (!string.IsNullOrWhiteSpace(deletedTuhao) && !string.IsNullOrWhiteSpace(deletedXianghao))
{
if (_currentTuhao == deletedTuhao)
{
ReloadXianghaoByTuhao(_currentProjectId, deletedTuhao);
if (_currentXianghao == deletedXianghao)
ReloadYuanjianByXianghao(_currentProjectId, deletedTuhao, deletedXianghao);
}
}
else if (!string.IsNullOrWhiteSpace(deletedTuhao))
{
ReloadTuhaoByProject(_currentProjectId);
if (_currentTuhao == deletedTuhao)
ReloadXianghaoByTuhao(_currentProjectId, deletedTuhao);
}
// ★ 删除级联的框实体(图号删除时,其下箱号和元件的蒙版也要删除)
Log("[DeleteRowsByFrameHandle] cascadeFrames 数量: " + cascadeFrames.Count + " 当前Handle=" + frameHandle);
if (cascadeFrames.Count > 0)
{
// ★ 收集后一次性批量删除(排除当前正在删除的 frameHandle,已由 CAD 端删除)
var toDel = new List<string>();
foreach (string h in cascadeFrames)
{
if (h != frameHandle) toDel.Add(h);
}
if (toDel.Count > 0)
{
Log("[DeleteRowsByFrameHandle] 级联删除框Handle数: " + toDel.Count);
DeleteFramesByHandles(toDel);
}
SaveCurrentDwg();
Log("[DeleteRowsByFrameHandle] 级联删除完成");
}
}
}
finally
{
_isReloading = false;
}
}
/// <summary>重写 OnLoad:在所有 Load 事件订阅者之前确保项目库文件存在。</summary>
protected override void OnLoad(EventArgs e)
{
try { EnsureProjectDbExists(GetDbFullPath(ProjectsDbPath)); }
catch { }
base.OnLoad(e);
}
// ============================================================
// 通用辅助方法
// ============================================================
private void Log(string msg)
{
try
{
string logPath = Path.Combine(_baseDir, "diag.log");
File.AppendAllText(logPath,
DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + "\r\n");
}
catch { }
}
private string GetDbFullPath(string relativePath)
{
return Path.Combine(_baseDir, relativePath);
}
private string GetConnString(string dbPath)
{
// ★ busy_timeout=3000:数据库被 CAD 或其他进程短暂占用时等待重试而非卡死;
// Pooling 关闭,避免连接池长期持有文件句柄
return "Data Source=" + dbPath + ";Version=3;busy_timeout=3000;";
}
private string GetQuoteDbPath(string projectId)
{
string safeName = projectId;
char[] invalidChars = Path.GetInvalidFileNameChars();
foreach (char c in invalidChars) safeName = safeName.Replace(c, '_');
return GetDbFullPath(QuoteDbDir + @"\项目" + safeName + ".db");
}
private static decimal ParseDecimal(object v)
{
if (v == null || v == DBNull.Value) return 0m;
decimal d;
if (decimal.TryParse(v.ToString().Trim(), out d)) return d;
return 0m;
}
private static DataGridViewColumn FindColumn(DataGridView dgv, string name)
{
if (dgv == null || dgv.Columns == null) return null;
foreach (DataGridViewColumn c in dgv.Columns)
{
if (c == null) continue;
if (c.DataPropertyName == name || c.HeaderText == name || c.Name == name) return c;
}
return null;
}
private static string GetCellString(DataGridViewRow row, string name)
{
if (row == null || row.DataGridView == null) return "";
DataGridViewColumn col = FindColumn(row.DataGridView, name);
if (col == null) return "";
object v = row.Cells[col.Index].Value;
return v == null ? "" : v.ToString().Trim();
}
private static decimal GetCellDecimal(DataGridViewRow row, string name)
{
return ParseDecimal(GetCellString(row, name));
}
private static string GetCellValueAsString(DataGridViewRow row, string colName)
{
if (row == null || row.Cells == null || string.IsNullOrEmpty(colName)) return "";
DataGridView grid = row.DataGridView;
if (grid != null)
{
foreach (DataGridViewColumn col in grid.Columns)
{
if (col == null) continue;
string key = !string.IsNullOrEmpty(col.DataPropertyName)
? col.DataPropertyName
: (!string.IsNullOrEmpty(col.HeaderText) ? col.HeaderText : col.Name);
if (key == colName)
{
object v = row.Cells[col.Index].Value;
return v == null ? "" : v.ToString().Trim();
}
}
}
return "";
}
private static void SetCell(DataGridViewRow row, string colName, decimal value)
{
if (row == null || row.DataGridView == null) return;
DataGridViewColumn col = FindColumn(row.DataGridView, colName);
if (col == null) return;
row.Cells[col.Index].Value = value == 0m ? 0d : (double)value;
}
private static DataGridViewRow GetFirstSelectedRow(DataGridView dgv)
{
if (dgv == null || dgv.Rows == null || dgv.Rows.Count == 0) return null;
foreach (DataGridViewRow r in dgv.SelectedRows)
{
if (r == null || r.IsNewRow) continue;
return r;
}
if (dgv.CurrentRow != null && !dgv.CurrentRow.IsNewRow) return dgv.CurrentRow;
return null;
}
private static List<DataGridViewRow> GetAllSelectedRows(DataGridView dgv)
{
var result = new List<DataGridViewRow>();
if (dgv == null || dgv.Rows == null || dgv.Rows.Count == 0) return result;
var seenRows = new HashSet<int>();
if (dgv.SelectedRows != null)
{
foreach (DataGridViewRow r in dgv.SelectedRows)
{
if (r == null || r.IsNewRow) continue;
if (seenRows.Add(r.Index)) result.Add(r);
}
}
// ★ 总是通过 SelectedCells 补充收集(不再限制 result.Count == 0)
// ADGV 多选时 SelectedRows 可能不完整,SelectedCells 能覆盖所有选中行
if (dgv.SelectedCells != null)
{
foreach (DataGridViewCell c in dgv.SelectedCells)
{
if (c == null || c.RowIndex < 0) continue;
DataGridViewRow r = dgv.Rows[c.RowIndex];
if (r == null || r.IsNewRow) continue;
if (seenRows.Add(r.Index)) result.Add(r);
}
}
// 兜底:如果以上都没收集到,取当前行
if (result.Count == 0 && dgv.CurrentRow != null && !dgv.CurrentRow.IsNewRow)
{
result.Add(dgv.CurrentRow);
}
return result;
}
private DataGridViewRow GetFirstSelectedProjectRow()
{
DataGridView dgv = this.advancedDataGridView6;
if (dgv == null || dgv.Rows == null || dgv.Rows.Count == 0) return null;
foreach (DataGridViewRow r in dgv.SelectedRows)
{
if (r == null || r.IsNewRow) continue;
return r;
}
if (dgv.CurrentRow != null && !dgv.CurrentRow.IsNewRow) return dgv.CurrentRow;
return null;
}
private DataGridViewRow FindProjectRowById(string projectId)
{
if (string.IsNullOrWhiteSpace(projectId)) return null;
DataGridView dgv = this.advancedDataGridView6;
if (dgv == null || dgv.Rows == null) return null;
foreach (DataGridViewRow r in dgv.Rows)
{
if (r == null || r.IsNewRow) continue;
if (GetCellValueAsString(r, "项目编号") == projectId) return r;
}
return null;
}
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 { }
}
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;
if (GetCellValueAsString(r, colName) == value)
{
dgv.ClearSelection();
r.Selected = true;
dgv.CurrentCell = r.Cells[0];
return;
}
}
}
catch { }
finally { _isReloading = false; }
}
private static int GetSelectedRowIndex(DataGridView dgv)
{
if (dgv == null) return -1;
if (dgv.CurrentRow == null || dgv.CurrentRow.IsNewRow) return -1;
return dgv.CurrentRow.Index;
}
private static string MakeSafeFileName(string s)
{
if (string.IsNullOrEmpty(s)) return "未命名";
string r = s;
char[] invalid = Path.GetInvalidFileNameChars();
foreach (char c in invalid) r = r.Replace(c, '_');
return r;
}
private static string EscapeJsonString(string s)
{
if (s == null) return "";
return s.Replace("\\", "\\\\").Replace("\"", "\\\"")
.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
}
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 "";
}
}
/// <summary>开启 DataGridView 的双缓冲(消除闪烁)。</summary>
private static void EnableDoubleBuffered(DataGridView dgv)
{
if (dgv == null) return;
try
{
typeof(DataGridView).InvokeMember("DoubleBuffered",
System.Reflection.BindingFlags.SetProperty |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic,
null, dgv, new object[] { true });
}
catch { }
}
/// <summary>按列名设置只读(运行时按 DataPropertyName/HeaderText 查找)。</summary>
private static void ApplyReadOnlyColumns(DataGridView dgv, string[] colNames)
{
if (dgv == null || colNames == null) return;
foreach (string name in colNames)
{
DataGridViewColumn col = FindColumn(dgv, name);
if (col != null) col.ReadOnly = true;
}
}
// ============================================================
// 价格显示格式化
// ============================================================
/// <summary>把"保留2位小数"/"保留整数"等转成 .NET 格式字符串(F0/F2)。</summary>
private static string ToFormatString(string 小数位文本)
{
int n = 解析小数位(小数位文本);
if (n < 0) n = 2;
return "F" + n.ToString();
}
private static int 解析小数位(string text)
{
if (string.IsNullOrWhiteSpace(text)) return 2;
string t = text.Trim();
if (t.Contains("整数")) return 0;
if (t.Contains("1位")) return 1;
if (t.Contains("2位")) return 2;
if (t.Contains("3位")) return 3;
if (t.Contains("4位")) return 4;
return 2;
}
private void ApplyPriceFormat(DataGridView dgv, string 小数位文本)
{
if (dgv == null || dgv.Columns == null) return;
string fmt = ToFormatString(小数位文本);
foreach (DataGridViewColumn col in dgv.Columns)
{
if (col == null) continue;
string name = !string.IsNullOrEmpty(col.DataPropertyName)
? col.DataPropertyName : col.HeaderText;
if (string.IsNullOrEmpty(name)) continue;
if (s_公式列集合.Contains(name))
{
// ★ 毛利率带 % 显示(2026-09-29):值已是百分数值,用字面量 % 格式
if (string.Equals(name, "毛利率", StringComparison.OrdinalIgnoreCase))
col.DefaultCellStyle.Format = "0.##\"%\"";
else
col.DefaultCellStyle.Format = fmt;
}
}
}
/// <summary>老库兼容:把 DataTable 中公式列的类型从 string 转为 double。</summary>
private static DataTable ConvertFormulaColumnsToDouble(DataTable table)
{
if (table == null || table.Columns.Count == 0) return table;
bool needsConvert = false;
foreach (DataColumn dc in table.Columns)
{
if (s_公式列集合.Contains(dc.ColumnName) && dc.DataType == typeof(string))
{
needsConvert = true;
break;
}
}
if (!needsConvert) return table;
var newTable = new DataTable();
foreach (DataColumn dc in table.Columns)
{
Type t = s_公式列集合.Contains(dc.ColumnName) ? typeof(double) : dc.DataType;
newTable.Columns.Add(dc.ColumnName, t);
}
foreach (DataRow row in table.Rows)
{
var newRow = newTable.NewRow();
foreach (DataColumn dc in table.Columns)
{
if (s_公式列集合.Contains(dc.ColumnName))
{
object v = row[dc];
if (v == null || v == DBNull.Value) newRow[dc.ColumnName] = 0d;
else if (v is double) newRow[dc.ColumnName] = v;
else
{
double d;
newRow[dc.ColumnName] = double.TryParse(v.ToString().Trim(), out d) ? d : 0d;
}
}
else newRow[dc.ColumnName] = row[dc];
}
newTable.Rows.Add(newRow);
}
newTable.TableName = table.TableName;
return newTable;
}
// ============================================================
// 数据库结构维护
// ============================================================
private void EnsureProjectDbExists(string dbPath)
{
string dir = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql =
"CREATE TABLE IF NOT EXISTS " + ProjectsTableName + " (" +
" 项目编号 TEXT, 项目名称 TEXT, 客户名称 TEXT, 客户地址 TEXT," +
" 联系人 TEXT, 联系电话 TEXT, 项目日期 TEXT, 箱柜台数 REAL," +
" 成本总价 REAL, 报出总价 REAL, 利润 REAL, 利润率 REAL, 备注 TEXT" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) cmd.ExecuteNonQuery();
EnsureColumns(conn, ProjectsTableName, new[] {
"成套厂名称", "成套厂地址", "成套厂联系人", "成套厂联系电话"
});
}
}
private static void EnsureColumns(System.Data.SQLite.SQLiteConnection conn, string tableName, string[] columns)
{
if (columns == null || columns.Length == 0) return;
var existing = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var infoCmd = new System.Data.SQLite.SQLiteCommand("PRAGMA table_info(" + tableName + ");", conn))
using (var reader = infoCmd.ExecuteReader())
{
while (reader.Read())
{
object nameObj = reader["name"];
if (nameObj != null && nameObj != DBNull.Value) existing.Add(nameObj.ToString());
}
}
foreach (string col in columns)
{
if (!existing.Contains(col))
{
string sql = "ALTER TABLE " + tableName + " ADD COLUMN " + col + " TEXT;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) cmd.ExecuteNonQuery();
}
}
}
private static List<string> GetProjectTableColumns(System.Data.SQLite.SQLiteConnection conn)
{
var cols = new List<string>();
try
{
using (var infoCmd = new System.Data.SQLite.SQLiteCommand("PRAGMA table_info(" + ProjectsTableName + ");", conn))
using (var reader = infoCmd.ExecuteReader())
{
while (reader.Read())
{
object nameObj = reader["name"];
if (nameObj != null && nameObj != DBNull.Value) cols.Add(nameObj.ToString());
}
}
}
catch { }
return cols;
}
private void EnsureQuoteDbExists(string projectId)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetQuoteDbPath(projectId);
string dir = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sqlTuhao =
"CREATE TABLE IF NOT EXISTS " + TuhaoTableName + " (" +
" 序号 TEXT, 图号 TEXT, 图名 TEXT, 台数 TEXT, 箱柜数量 REAL," +
" 单价 REAL, 总价 REAL, 成本总价 REAL, 毛利 REAL, 毛利率 REAL," +
" 框Handle TEXT, 文本Handle TEXT" +
");";
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[] { sqlTuhao, sqlIndex, sqlProjectInfo })
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) cmd.ExecuteNonQuery();
}
EnsureColumns(conn, ProjectInfoTableName, new[] {
"成套厂名称", "成套厂地址", "成套厂联系人", "成套厂联系电话",
"单台小数位", "单台取整方式", "项目小数位", "项目取整方式"
});
// ★ 确保旧库的图号表也有"框Handle"/"文本Handle"列
EnsureColumns(conn, TuhaoTableName, new[] { "框Handle", "文本Handle" });
// ★ 真隔离(2026-09-29):老库迁移——图号表补"内部ID"列并回填(与主窗口同款),
// 所有箱号表(xh_N)也补"内部ID"列并回填显示名,保证旧数据按内部ID 也能定位
EnsureColumns(conn, TuhaoTableName, new[] { "内部ID" });
MigrateInternalIdColumn(conn, TuhaoTableName, "图号", "内部ID");
var xhTables = new System.Collections.Generic.List<string>();
using (var selCmd = new System.Data.SQLite.SQLiteCommand(
"SELECT DISTINCT 实际表名 FROM 表索引 WHERE 类型='箱号';", conn))
using (var selReader = selCmd.ExecuteReader())
{
while (selReader.Read())
{
string t = selReader.IsDBNull(0) ? "" : selReader.GetString(0);
if (!string.IsNullOrWhiteSpace(t)) xhTables.Add(t);
}
}
foreach (string xhTbl in xhTables)
{
string quotedXhTbl = "\"" + xhTbl.Replace("\"", "") + "\"";
EnsureColumns(conn, quotedXhTbl, new[] { "内部ID" });
MigrateInternalIdColumn(conn, quotedXhTbl, "箱号", "内部ID");
}
// ★ A1(2026-09-30):所有元件表补"内部ID"列并回填 GUID(三表统一定位体系)
成套报价软件.MainForm.MigrateYuanjianInternalId(conn);
// ★ A5(2026-10-01):数据库健康自检(断链索引/孤儿表/序号重复,幂等)
成套报价软件.MainForm.数据库健康自检(conn);
}
}
/// <summary>老库迁移辅助:把"显示名列"的值复制到"内部ID列"(仅对内部ID为空的行)。</summary>
private static void MigrateInternalIdColumn(System.Data.SQLite.SQLiteConnection conn,
string quotedTableName, string displayCol, string idCol)
{
try
{
string sql = "UPDATE " + quotedTableName + " SET " + idCol + " = " + displayCol +
" WHERE " + idCol + " IS NULL OR " + idCol + " = '';";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.ExecuteNonQuery();
}
}
catch { }
}
// ---------- 箱号表(每个图号一张独立表,表名=图号原名,双引号括起)----------
private static string GetXianghaoTableName(string tuhao)
{
if (string.IsNullOrWhiteSpace(tuhao)) return "\"图号箱号表\"";
return "\"" + tuhao.Replace("\"", "\"\"") + "\"";
}
private void EnsureXianghaoTableExists(string projectId, string tuhao)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return;
// 先查表索引,已有则跳过
string existing = GetXianghaoTableByIndex(projectId, tuhao);
if (!string.IsNullOrEmpty(existing)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
// 用唯一ID做表名,避免图号名含特殊字符
string realTbl = GetNextTableId(projectId, "箱号");
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql =
"CREATE TABLE \"" + realTbl + "\" (" +
" 序号 TEXT, 箱号 TEXT, 型号 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 单台成本 REAL, 成本价 REAL, 单台报出 REAL, 报出价 REAL," +
" 利润 REAL, 利润率 REAL, 毛利率 REAL, 箱柜类型 TEXT, 备注 TEXT, 图纸 TEXT," +
" 框Handle TEXT, 文本Handle TEXT" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) cmd.ExecuteNonQuery();
}
// ★ 注册到表索引,后续 GetXianghaoTableByIndex 才能查到
AddTableIndex(projectId, "箱号", tuhao, "", realTbl);
}
catch { }
}
private string GetXianghaoCount(string projectId, string tuhao)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return "1";
string realTbl = GetXianghaoTableByIndex(projectId, tuhao);
if (string.IsNullOrEmpty(realTbl)) return "1";
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return "1";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "SELECT COUNT(*) FROM \"" + realTbl + "\";";
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();
}
}
}
catch { return "1"; }
}
// ---------- 元件计数(用于新建元件命名"新元件N")----------
private string GetYuanjianCount(string projectId, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(projectId) ||
string.IsNullOrWhiteSpace(xianghao)) return "1";
// 查索引表拿该箱号元件表的真实表名
string realTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(realTbl)) return "1";
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return "1";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "SELECT COUNT(*) FROM \"" + realTbl + "\";";
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();
}
}
}
catch { return "1"; }
}
// ---------- 元件表(每个箱号一张独立表)----------
private static string GetYuanjianTableName(string xianghao)
{
if (string.IsNullOrWhiteSpace(xianghao)) return "\"元件表\"";
return "\"" + xianghao.Replace("\"", "\"\"") + "\"";
}
private void EnsureYuanjianTableExists(string projectId, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao) || string.IsNullOrWhiteSpace(xianghao)) return;
// ★ 真隔离(2026-09-29):用箱号行的内部ID(GUID)查/登记元件表索引,
// 不再用显示名,避免同图号下同名扒图箱号共用同一元件表导致串数据。
// 流程:InsertXianghao 已先写入箱号行(内部ID=新 GUID),这里查箱号表拿该 GUID。
string xhInternalId = ResolveXianghaoInternalId(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(xhInternalId)) xhInternalId = xianghao; // 兜底:旧数据或异常时按显示名
// 先查表索引,已有则跳过(双键兼容:先按内部ID,未命中按显示名)
string existing = GetYuanjianTableByIndex(projectId, tuhao, xhInternalId);
if (!string.IsNullOrEmpty(existing)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
// 用唯一ID做表名
string realTbl = GetNextTableId(projectId, "元件");
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql =
"CREATE TABLE \"" + realTbl + "\" (" +
" 序号 TEXT, 元件名称 TEXT, 规格 TEXT, 单位 TEXT, 数量 TEXT," +
" 表价 REAL, 采购单价 REAL, 采购系数 TEXT, 采购价 REAL," +
" 报出单价 REAL, 报出系数 TEXT, 报出价 REAL," +
" 图纸规格 TEXT, 品牌 TEXT, 系列 TEXT, 类型 TEXT, 备注 TEXT," +
" 框Handle TEXT, 文本Handle TEXT" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) cmd.ExecuteNonQuery();
}
// ★ 注册到表索引(类型=元件,图号,箱号=内部ID) — 用 GUID 作箱号键
AddTableIndex(projectId, "元件", tuhao, xhInternalId, realTbl);
}
catch { }
}
// ---------- 表索引(用唯一ID做表名,索引表记录 图号/箱号 → 真实表名)----------
private string GetNextTableId(string projectId, string prefix)
{
if (string.IsNullOrWhiteSpace(projectId)) return prefix + "_1";
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return prefix + "_1";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "SELECT 实际表名 FROM 表索引 WHERE 实际表名 LIKE @p;";
int maxN = 0;
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
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();
}
}
catch { return prefix + "_1"; }
}
private void AddTableIndex(string projectId, string type, string tuhao, string xianghao, string realTableName)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(realTableName)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "INSERT INTO 表索引 (类型, 图号, 箱号, 实际表名) VALUES (@t, @th, @xh, @real);";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@t", type);
cmd.Parameters.AddWithValue("@th", tuhao ?? "");
cmd.Parameters.AddWithValue("@xh", xianghao ?? "");
cmd.Parameters.AddWithValue("@real", realTableName);
cmd.ExecuteNonQuery();
}
}
}
catch { }
}
private void RemoveTableIndex(string projectId, string type, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "DELETE FROM 表索引 WHERE 类型=@t AND 图号=@th AND 箱号=@xh;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@t", type);
cmd.Parameters.AddWithValue("@th", tuhao ?? "");
cmd.Parameters.AddWithValue("@xh", xianghao ?? "");
cmd.ExecuteNonQuery();
}
}
}
catch { }
}
private string GetXianghaoTableByIndex(string projectId, string tuhao)
{
return LookupTableByIndex(projectId, "箱号", tuhao, "");
}
/// <summary>查索引表:图号+箱号 → 该箱号的元件表真实表名(找不到返回空)。
/// ★ 真隔离(2026-09-29):双键兼容——先按 xianghao 直接查,
/// 未命中则查箱号表把显示名转成 内部ID 再查(向后兼容老数据)。
/// </summary>
private string GetYuanjianTableByIndex(string projectId, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(xianghao)) return "";
string realTbl = LookupTableByIndex(projectId, "元件", tuhao, xianghao);
if (!string.IsNullOrEmpty(realTbl)) return realTbl;
string xhIdByName = ResolveXianghaoInternalId(projectId, tuhao, xianghao);
if (!string.IsNullOrEmpty(xhIdByName) && xhIdByName != xianghao)
return LookupTableByIndex(projectId, "元件", tuhao, xhIdByName);
return "";
}
/// <summary>★ 真隔离(2026-09-29):按图号+箱号显示名查箱号表的"内部ID"列。</summary>
private string ResolveXianghaoInternalId(string projectId, string tuhao, string xianghaoDisplayName)
{
if (string.IsNullOrWhiteSpace(projectId) ||
string.IsNullOrWhiteSpace(tuhao) ||
string.IsNullOrWhiteSpace(xianghaoDisplayName)) return "";
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return "";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
return ResolveXianghaoInternalId(tuhao, xianghaoDisplayName, conn);
}
}
catch { return ""; }
}
/// <summary>ResolveXianghaoInternalId 复用连接版本。</summary>
private string ResolveXianghaoInternalId(string tuhao, string xianghaoDisplayName,
System.Data.SQLite.SQLiteConnection conn)
{
if (conn == null || string.IsNullOrWhiteSpace(tuhao) ||
string.IsNullOrWhiteSpace(xianghaoDisplayName)) return "";
try
{
string xhTbl = LookupTableByIndex("箱号", tuhao, "", conn);
if (string.IsNullOrEmpty(xhTbl)) return "";
EnsureColumns(conn, "\"" + xhTbl + "\"", new[] { "内部ID" });
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM \"" + xhTbl.Replace("\"", "") + "\" WHERE 箱号=@xh LIMIT 1;", conn))
{
cmd.Parameters.AddWithValue("@xh", xianghaoDisplayName);
object o = cmd.ExecuteScalar();
return (o == null || o == DBNull.Value) ? "" : o.ToString();
}
}
catch { return ""; }
}
private string LookupTableByIndex(string projectId, string type, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return "";
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return "";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
return LookupTableByIndex(type, tuhao, xianghao, conn);
}
}
catch { return ""; }
}
private string LookupTableByIndex(string type, string tuhao, string xianghao, System.Data.SQLite.SQLiteConnection conn)
{
if (conn == null || string.IsNullOrWhiteSpace(tuhao)) return "";
try
{
string sql = "SELECT 实际表名 FROM 表索引 WHERE 类型=@t AND 图号=@th AND 箱号=@xh LIMIT 1;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@t", type);
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.Parameters.AddWithValue("@xh", xianghao ?? "");
object o = cmd.ExecuteScalar();
return (o == null || o == DBNull.Value) ? "" : o.ToString();
}
}
catch { return ""; }
}
private string GetXianghaoTableByIndex(string tuhao, System.Data.SQLite.SQLiteConnection conn)
{
return LookupTableByIndex("箱号", tuhao, "", conn);
}
/// <summary>查索引表:图号+箱号 → 真实表名(复用连接版本)。
/// ★ 真隔离(2026-09-29):双键兼容——先按 xianghao 直接查,
/// 未命中则查箱号表把显示名转成 内部ID 再查(向后兼容老数据)。
/// </summary>
private string GetYuanjianTableByIndex(string tuhao, string xianghao, System.Data.SQLite.SQLiteConnection conn)
{
if (string.IsNullOrWhiteSpace(xianghao)) return "";
string realTbl = LookupTableByIndex("元件", tuhao, xianghao, conn);
if (!string.IsNullOrEmpty(realTbl)) return realTbl;
string xhIdByName = ResolveXianghaoInternalId(tuhao, xianghao, conn);
if (!string.IsNullOrEmpty(xhIdByName) && xhIdByName != xianghao)
return LookupTableByIndex("元件", tuhao, xhIdByName, conn);
return "";
}
private void CreateXianghaoTable(string projectId, string realTableName)
{
string dbPath = GetQuoteDbPath(projectId);
if (!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" +
");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn)) cmd.ExecuteNonQuery();
}
}
catch { }
}
private void RenumberTable(string projectId, string quotedTableName)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(quotedTableName)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string selSql = "SELECT rowid FROM " + quotedTableName + " ORDER BY rowid;";
var rowids = new List<long>();
using (var sel = new System.Data.SQLite.SQLiteCommand(selSql, conn))
using (var reader = sel.ExecuteReader())
{
while (reader.Read()) rowids.Add(reader.GetInt64(0));
}
int newSeq = 1;
foreach (long rowid in rowids)
{
string updSql = "UPDATE " + quotedTableName + " SET 序号 = @seq WHERE rowid = @rid;";
using (var upd = new System.Data.SQLite.SQLiteCommand(updSql, conn))
{
upd.Parameters.AddWithValue("@seq", newSeq.ToString());
upd.Parameters.AddWithValue("@rid", rowid);
upd.ExecuteNonQuery();
}
newSeq++;
}
}
}
catch { }
}
// ============================================================
// 报价库「项目信息」表读写
// ============================================================
private void InsertProjectInfoToQuoteDb(string projectId, Dictionary<string, string> values)
{
if (string.IsNullOrWhiteSpace(projectId) || values == null) return;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string delSql = "DELETE FROM " + ProjectInfoTableName + " WHERE 项目编号 = @id;";
using (var del = new System.Data.SQLite.SQLiteCommand(delSql, conn))
{
del.Parameters.AddWithValue("@id", projectId);
del.ExecuteNonQuery();
}
var cols = new List<string>();
var paras = new List<string>();
foreach (string c in ProjectInfoColumns)
{
if (values.ContainsKey(c)) { cols.Add(c); paras.Add("@" + c); }
}
if (cols.Count == 0) return;
string sql = "INSERT INTO " + ProjectInfoTableName + " (" +
string.Join(", ", cols.ToArray()) + ") VALUES (" +
string.Join(", ", paras.ToArray()) + ");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
foreach (string c in cols) cmd.Parameters.AddWithValue("@" + c, values[c] ?? "");
cmd.ExecuteNonQuery();
}
}
}
private Dictionary<string, string> GetProjectInfoFromQuoteDb(string projectId)
{
var info = new Dictionary<string, string>();
if (string.IsNullOrWhiteSpace(projectId)) return info;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return info;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string chkSql = "SELECT name FROM sqlite_master WHERE type='table' AND name=@n;";
using (var chk = new System.Data.SQLite.SQLiteCommand(chkSql, conn))
{
chk.Parameters.AddWithValue("@n", ProjectInfoTableName);
if (chk.ExecuteScalar() == null) return info;
}
string sql = "SELECT * FROM " + ProjectInfoTableName + " WHERE 项目编号 = @id LIMIT 1;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@id", projectId);
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
var existCols = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++) existCols.Add(reader.GetName(i));
foreach (string c in ProjectInfoColumns)
{
if (!existCols.Contains(c)) continue;
object v = reader[c];
info[c] = (v == null || v == DBNull.Value) ? "" : v.ToString();
}
}
}
}
}
}
catch { }
return info;
}
private void LoadPriceFormatConfig(string projectId)
{
_单台小数位文本 = "保留2位小数";
_单台取整方式 = "不取整";
_项目小数位文本 = "保留2位小数";
_项目取整方式 = "不取整";
try
{
var info = GetProjectInfoFromQuoteDb(projectId);
if (info == null || info.Count == 0) return;
string v;
if (info.TryGetValue("单台小数位", out v) && !string.IsNullOrWhiteSpace(v)) _单台小数位文本 = v;
if (info.TryGetValue("单台取整方式", out v) && !string.IsNullOrWhiteSpace(v)) _单台取整方式 = v;
if (info.TryGetValue("项目小数位", out v) && !string.IsNullOrWhiteSpace(v)) _项目小数位文本 = v;
if (info.TryGetValue("项目取整方式", out v) && !string.IsNullOrWhiteSpace(v)) _项目取整方式 = v;
}
catch { }
}
// ============================================================
// 项目库 CRUD
// ============================================================
private int InsertProjectToDatabase(Dictionary<string, string> values)
{
if (values == null || values.Count == 0) return 0;
string dbPath = GetDbFullPath(ProjectsDbPath);
if (!File.Exists(dbPath))
{
EnsureProjectDbExists(dbPath);
if (!File.Exists(dbPath))
throw new FileNotFoundException("无法创建项目数据库文件: " + dbPath, dbPath);
}
int affected;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
var allowed = new HashSet<string>(GetProjectTableColumns(conn), StringComparer.OrdinalIgnoreCase);
var colNames = new List<string>();
var paramNames = new List<string>();
foreach (string col in values.Keys)
{
if (allowed.Contains(col)) { colNames.Add(col); paramNames.Add("@" + col); }
}
if (colNames.Count == 0) { affected = 0; }
else
{
string sql = "INSERT INTO " + ProjectsTableName + " (" +
string.Join(", ", colNames.ToArray()) + ") VALUES (" +
string.Join(", ", paramNames.ToArray()) + ");";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
foreach (string col in colNames) cmd.Parameters.AddWithValue("@" + col, values[col]);
affected = cmd.ExecuteNonQuery();
}
}
}
string projectId;
values.TryGetValue("项目编号", out projectId);
if (!string.IsNullOrWhiteSpace(projectId)) InsertProjectInfoToQuoteDb(projectId, values);
return affected;
}
private int DeleteProjectFromDatabase(string projectId)
{
if (string.IsNullOrWhiteSpace(projectId)) return 0;
string dbPath = GetDbFullPath(ProjectsDbPath);
if (!File.Exists(dbPath)) throw new FileNotFoundException("找不到项目数据库文件: " + dbPath, dbPath);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "DELETE FROM " + ProjectsTableName + " WHERE 项目编号 = @id;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@id", projectId);
return cmd.ExecuteNonQuery();
}
}
}
private void CascadeDeleteByProject(string projectId)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
// ★ 必须删掉(方案库删除的已验证做法):主窗口的池化连接(Pooling=True)
// 会持有该文件句柄——ClearAllPools(全进程)后重试 3 次,仍失败抛异常,
// 绝不静默吞掉(残留文件会被同编号新建项目"复活"旧数据)。
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);
File.Delete(dbPath);
foreach (string ext in new[] { "-wal", "-shm" })
{
string p = dbPath + ext;
if (File.Exists(p)) { try { File.Delete(p); } catch { } }
}
return;
}
catch (Exception ex) { lastErr = ex; }
}
throw new Exception("项目数据库文件删除失败(可能被其他程序占用,请关闭后重试):\n" + dbPath + "\n" + lastErr.Message);
}
private List<Dictionary<string, object>> ReadAllProjectsFromDatabase()
{
var list = new List<Dictionary<string, object>>();
try
{
string dbPath = GetDbFullPath(ProjectsDbPath);
if (!File.Exists(dbPath)) return list;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM " + ProjectsTableName + " ORDER BY rowid;", conn))
using (var rd = cmd.ExecuteReader())
{
while (rd.Read())
{
var dict = new Dictionary<string, object>(StringComparer.Ordinal);
for (int i = 0; i < rd.FieldCount; i++)
{
object v = rd.GetValue(i);
if (v == null || v == DBNull.Value) v = "";
dict[rd.GetName(i)] = v;
}
list.Add(dict);
}
}
}
}
catch { }
return list;
}
private Dictionary<string, object> ReadProjectMetaData(string pid)
{
var dict = new Dictionary<string, object>(StringComparer.Ordinal);
try
{
string dbPath = GetDbFullPath(ProjectsDbPath);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT * FROM " + ProjectsTableName + " WHERE 项目编号 = @p LIMIT 1;", conn))
{
cmd.Parameters.AddWithValue("@p", pid);
using (var rd = cmd.ExecuteReader())
{
if (!rd.Read()) return null;
for (int i = 0; i < rd.FieldCount; i++)
{
object v = rd.GetValue(i);
if (v == null || v == DBNull.Value) v = "";
dict[rd.GetName(i)] = v;
}
}
}
}
}
catch { return null; }
return dict;
}
// ============================================================
// 图号/箱号/元件 CRUD
// ============================================================
private string GetNextSeqNumber(string tableName, string nameCol, string projectId, string tuhao = null)
{
if (string.IsNullOrWhiteSpace(projectId)) return "1";
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return "1";
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql;
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"; }
}
// ============================================================
// 扒图号/扒箱号/扒元件:CAD 文本选择交互
// ============================================================
// ---------- 主窗口功能区(CAD扒图页签)按钮的调用入口(转发到原工具栏逻辑) ----------
public void 功能区_导出数据() { toolStripButton1_Click(null, null); }
public void 功能区_新建项目() { toolStripButton5_Click(null, null); }
public void 功能区_删除项目() { toolStripButton6_Click(null, null); }
public void 功能区_扒图号() { SendCadPaCommand("CADPATU"); }
public void 功能区_扒箱号() { SendCadPaCommand("CADPAXH"); }
public void 功能区_扒元件() { SendCadPaCommand("CADPAYJ"); }
public void 功能区_设置() { ShowDeepSeekSettings(); }
/// <summary>
/// 向 CAD 发送扒数据命令,触发框选交互。
/// ★ 关键:先发 ESC 取消当前可能正在进行的扒图循环,再发新命令
/// 这样用户从"扒图号"循环里点"扒箱号"也能立即切换
/// </summary>
private void SendCadPaCommand(string cmd)
{
try
{
if (!成套报价软件.CadBridge.IsViewerReady)
{
MessageBox.Show(this, "网页 CAD 尚未就绪,请先在文档区打开图纸。",
"扒数据", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 转发到网页 CAD 桥(patu/paxh/payj/pacl 命令)
成套报价软件.CadBridge.SendPaCommand(cmd);
}
catch (Exception ex)
{
MessageBox.Show(this, "发送扒图命令失败:\n" + ex.Message,
"扒数据", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// CAD 选中文本后的回调:把选中的文本批量插入到对应表格。
/// 由 CadPlugin.ExecutePaCommand 在 UI 线程调用。
/// </summary>
public void OnPaTextsPicked(PaMode mode, List<PaTextResult> results)
{
if (results == null || results.Count == 0) return;
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在项目表中双击选择一个项目,再扒数据。",
"扒数据", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
switch (mode)
{
case PaMode.Tuhao:
PaTuhaoInsert(results);
break;
case PaMode.Xianghao:
PaXianghaoInsert(results);
break;
case PaMode.Yuanjian:
PaYuanjianInsert(results);
break;
}
}
/// <summary>扒图号:把选中文本作为图号批量插入到当前项目,同时保存框Handle。
/// ★ 重复扒图:如果图号已存在,删除旧框和数据库行(不新增)。</summary>
private void PaTuhaoInsert(List<PaTextResult> results)
{
string lastInsertedTuhao = null; // ★ 记录最后插入的图号,用于自动选中联动
try
{
_isReloading = true;
// 先 AcceptChanges,避免 SaveAllQuoteTablesIfDirty 把旧内存表回写
if (_tuhaoTable != null) _tuhaoTable.AcceptChanges();
if (_xianghaoTable != null) _xianghaoTable.AcceptChanges();
if (_yuanjianTable != null) _yuanjianTable.AcceptChanges();
// ★ 新增框架Handle列表: PaTextSelector刚画的框,如果是重复,需要删除这些新框
var newFramesToErase = new List<string>();
int deletedCount = 0;
int inserted = 0;
foreach (var r in results)
{
if (string.IsNullOrWhiteSpace(r.Text)) continue;
string clean = r.Text.Trim();
// ★ 按"同一个文本实体"判断重复:查该文本实体Handle是否已入库。
// 不同位置的相同内容文本 Handle 不同 → 不算重复,正常插入。
string oldFrame = GetTuhaoFrameHandleByTextHandle(_currentProjectId, clean, r.TextHandle);
if (!string.IsNullOrEmpty(oldFrame))
{
// ★ 重复(同一个文本再次选中):删除旧框+数据库行+新框
DeleteTuhao(_currentProjectId, clean);
DeleteFrameByHandle(oldFrame);
if (!string.IsNullOrWhiteSpace(r.FrameHandle)) newFramesToErase.Add(r.FrameHandle);
deletedCount++;
}
else
{
InsertTuhao(_currentProjectId, clean, r.FrameHandle, r.TextHandle);
lastInsertedTuhao = clean; // ★ 记录最后插入的图号
inserted++;
}
}
// ★ 删除重复图号对应的新框(PaTextSelector已经画出来了,但不需要保留)——批量一次删
DeleteFramesByHandles(newFramesToErase);
_xianghaoCache.Clear();
_yuanjianCache.Clear();
// 直接在数据库层面重算(不经过内存回写)
try { RecalcAllFromDatabase(_currentProjectId); } catch { }
ReloadTuhaoByProject(_currentProjectId);
// 保存
try
{
if (this.advancedDataGridView9 != null) this.advancedDataGridView9.EndEdit();
if (_tuhaoTable != null)
{
SaveTableToDatabase(_tuhaoTable, _currentProjectId, TuhaoTableName, null, null, true);
_tuhaoTable.AcceptChanges();
}
}
catch { }
SaveCurrentDwg();
if (deletedCount > 0 && inserted == 0)
{
Log("[PaTuhaoInsert] 删除了 " + deletedCount + " 条重复图号");
}
}
catch (Exception ex)
{
MessageBox.Show(this, "扒图号失败:\n" + ex.Message,
"扒图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_isReloading = false;
// ★ 扒完图号后自动选中刚插入的图号行,联动刷新箱号表
if (!string.IsNullOrWhiteSpace(lastInsertedTuhao))
{
_currentTuhao = lastInsertedTuhao;
SelectRowByValue(this.advancedDataGridView9, "图号", lastInsertedTuhao);
// 手动联动刷新箱号表(SelectRowByValue 在 _isReloading 下不触发 SelectionChanged)
ReloadXianghaoByTuhao(_currentProjectId, lastInsertedTuhao);
}
}
}
/// <summary>扒箱号:把选中文本作为箱号批量插入到当前图号,同时保存框Handle。
/// ★ 重复扒图:如果箱号已存在,删除旧框和数据库行(不新增)。</summary>
private void PaXianghaoInsert(List<PaTextResult> results)
{
string lastInsertedXianghao = null; // ★ 记录最后插入的箱号,用于自动选中联动
if (string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先在图号表中选择一个图号,再扒箱号。",
"扒箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
_isReloading = true;
// 先 AcceptChanges,避免 SaveAllQuoteTablesIfDirty 把旧内存表回写
if (_tuhaoTable != null) _tuhaoTable.AcceptChanges();
if (_xianghaoTable != null) _xianghaoTable.AcceptChanges();
if (_yuanjianTable != null) _yuanjianTable.AcceptChanges();
var newFramesToErase = new List<string>();
int deletedCount = 0;
int inserted = 0;
foreach (var r in results)
{
if (string.IsNullOrWhiteSpace(r.Text)) continue;
string clean = r.Text.Trim();
// ★ 按"同一个文本实体"判断重复(不同位置的相同内容文本不算重复)
string oldFrame = GetXianghaoFrameHandleByTextHandle(_currentProjectId, _currentTuhao, clean, r.TextHandle);
if (!string.IsNullOrEmpty(oldFrame))
{
// ★ 重复(同一个文本再次选中):删除旧框+数据库行+新框
DeleteXianghao(_currentProjectId, _currentTuhao, clean);
DeleteFrameByHandle(oldFrame);
if (!string.IsNullOrWhiteSpace(r.FrameHandle)) newFramesToErase.Add(r.FrameHandle);
deletedCount++;
}
else
{
// ★ 图纸列=当前打开的图纸名(网页 CAD 随批上报,去扩展名)
InsertXianghao(_currentProjectId, _currentTuhao, clean, r.FrameHandle, r.TextHandle,
成套报价软件.CadBridge.CurrentDrawingName);
lastInsertedXianghao = clean; // ★ 记录最后插入的箱号
inserted++;
}
}
DeleteFramesByHandles(newFramesToErase);
_xianghaoCache.Clear();
_yuanjianCache.Clear();
try { RecalcAllFromDatabase(_currentProjectId); } catch { }
ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao);
try
{
if (this.advancedDataGridView4 != null) this.advancedDataGridView4.EndEdit();
if (_xianghaoTable != null && !string.IsNullOrWhiteSpace(_xianghaoTable.TableName))
{
SaveTableToDatabase(_xianghaoTable, _currentProjectId,
"\"" + _xianghaoTable.TableName + "\"", null, null, true);
_xianghaoTable.AcceptChanges();
}
}
catch { }
SaveCurrentDwg();
if (deletedCount > 0 && inserted == 0)
{
Log("[PaXianghaoInsert] 删除了 " + deletedCount + " 条重复箱号");
}
}
catch (Exception ex)
{
MessageBox.Show(this, "扒箱号失败:\n" + ex.Message,
"扒箱号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_isReloading = false;
// ★ 扒完箱号后自动选中刚插入的箱号行,联动刷新元件表
if (!string.IsNullOrWhiteSpace(lastInsertedXianghao))
{
_currentXianghao = lastInsertedXianghao;
SelectRowByValue(this.advancedDataGridView4, "箱号", lastInsertedXianghao);
// 手动联动刷新元件表(SelectRowByValue 在 _isReloading 下不触发 SelectionChanged)
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, lastInsertedXianghao);
}
}
}
/// <summary>扒元件:把选中文本作为【规格】批量插入到当前箱号(写到规格列),同时保存框Handle。
/// ★ 关键:在单个事务里按 results 顺序依次插入,保证序号连续。
/// ★ 重复扒图:如果规格已存在,删除旧框+数据库行(不新增)。</summary>
private void PaYuanjianInsert(List<PaTextResult> results)
{
if (string.IsNullOrWhiteSpace(_currentXianghao))
{
MessageBox.Show(this, "请先在箱号表中选择一个箱号,再扒元件。",
"扒元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
_isReloading = true;
// ★ 关键:先 AcceptChanges 清掉内存表的改动标记,避免 SaveAllQuoteTablesIfDirty
// 用旧的内存数据覆盖数据库
if (_yuanjianTable != null) _yuanjianTable.AcceptChanges();
if (_xianghaoTable != null) _xianghaoTable.AcceptChanges();
if (_tuhaoTable != null) _tuhaoTable.AcceptChanges();
EnsureQuoteDbExists(_currentProjectId);
string dbPath = GetQuoteDbPath(_currentProjectId);
EnsureYuanjianTableExists(_currentProjectId, _currentTuhao, _currentXianghao);
string tblName = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, _currentXianghao);
if (string.IsNullOrEmpty(tblName))
{
MessageBox.Show("元件表创建失败(表索引未注册),图号=" + _currentTuhao + " 箱号=" + _currentXianghao);
return;
}
// ★ 确保旧元件表也有"框Handle"/"文本Handle"/"图纸规格"列
using (var connEns = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
connEns.Open();
EnsureColumns(connEns, "\"" + tblName + "\"", new[] { "框Handle", "文本Handle", "图纸规格" });
}
// 过滤掉空文本,保留原始顺序(同时保留框Handle+文本Handle)
var cleanResults = new List<PaTextResult>();
foreach (var r in results)
{
if (!string.IsNullOrWhiteSpace(r.Text))
{
cleanResults.Add(new PaTextResult
{
Text = r.Text.Trim(),
FrameHandle = r.FrameHandle ?? "",
TextHandle = r.TextHandle ?? ""
});
}
}
if (cleanResults.Count == 0) return;
// ★ 判断是否是逆向合并模式(所有result共用同一个框Handle且数量>1)
bool isMergeMode = cleanResults.Count > 1;
string mergeHandle = cleanResults[0].FrameHandle;
if (isMergeMode)
{
foreach (var r in cleanResults)
{
if (r.FrameHandle != mergeHandle) { isMergeMode = false; break; }
}
}
// ★ 新增:重复检测辅助集合
// newFramesToErase: PaTextSelector刚画的框,如果是重复项,这些框要删除
var newFramesToErase = new List<string>();
// oldFrameHandlesToDelete: 数据库里匹配到的旧框,要删除
var oldFrameHandlesToDelete = new List<string>();
// toInsert: 不重复、需要插入的项
var toInsert = new List<PaTextResult>();
// ★ 本地匹配失败的规格(收集后交给 DeepSeek AI 兜底识别)
var aiFailedSpecs = new List<string>();
int deletedCount = 0;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
if (isMergeMode)
{
// ★ 合并模式:所有文本按图纸阅读顺序拼成一个规格
// (PaTextSelector 已按列分组、列内从上到下排好序,保持此顺序
// 与库中自然书写顺序一致,如 HD13BX-1500/31 + 1500A)
// 文本Handle串与文本一一对应拼接(同一批文本Handle集合相同即重复)。
var orderedTexts = new List<string>();
var orderedHandles = new List<string>();
foreach (var r in cleanResults)
{
orderedTexts.Add(r.Text);
orderedHandles.Add(r.TextHandle ?? "");
}
string mergedSpec = string.Join(" ", orderedTexts.ToArray().Where(x => x.Trim().Length > 0));
// ★ Handle串排序后拼接(Handle集合相同即同一批文本,与顺序无关)
var sortedHandles = new List<string>(orderedHandles);
sortedHandles.Sort(StringComparer.Ordinal);
string mergedTextHandle = string.Join("|", sortedHandles.ToArray());
// ★ 1. 查"同一批文本实体"的合并规格是否已存在
// (文本Handle拼接串相同 = 同一批文本再次被选中,才是真重复)
var matchFrames = new List<string>();
using (var selCmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + tblName + "\" WHERE 规格=@spec AND 文本Handle=@th;", conn))
{
selCmd.Parameters.AddWithValue("@spec", mergedSpec);
selCmd.Parameters.AddWithValue("@th", mergedTextHandle);
using (var reader = selCmd.ExecuteReader())
{
while (reader.Read())
{
object v = reader["框Handle"];
if (v != null && v != DBNull.Value)
{
string s = v.ToString();
if (!string.IsNullOrWhiteSpace(s) && !matchFrames.Contains(s))
matchFrames.Add(s);
}
}
}
}
// ★ 2. 再查:本批每个文本实体是否已作为"独立元件"入库(按文本Handle精确匹配)
var perTextFrames = new List<string>();
var perTextSpecs = new List<string>();
foreach (var r in cleanResults)
{
List<string> fhs = GetYuanjianFrameHandlesByTextHandle(
_currentProjectId, _currentTuhao, _currentXianghao, r.Text, r.TextHandle);
foreach (string fh in fhs)
{
if (!perTextFrames.Contains(fh)) perTextFrames.Add(fh);
}
if (fhs.Count > 0 && !perTextSpecs.Contains(r.Text)) perTextSpecs.Add(r.Text);
}
// ★ 只要匹配到(1.同一批文本的合并规格 或 2.同一文本实体的独立元件) → 执行删除
bool hitMergeMatch = matchFrames.Count > 0;
bool hitPerTextMatch = perTextSpecs.Count > 0;
if (hitMergeMatch || hitPerTextMatch)
{
// ★ 删除:同一批文本的合并规格匹配的行(文本Handle+规格双条件)
if (hitMergeMatch)
{
using (var delCmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + tblName + "\" WHERE 规格=@spec AND 文本Handle=@th;", conn))
{
delCmd.Parameters.AddWithValue("@spec", mergedSpec);
delCmd.Parameters.AddWithValue("@th", mergedTextHandle);
int aff = delCmd.ExecuteNonQuery();
deletedCount += aff;
}
foreach (string fh in matchFrames)
if (!oldFrameHandlesToDelete.Contains(fh)) oldFrameHandlesToDelete.Add(fh);
}
// ★ 删除:同一文本实体的独立元件行(按文本Handle删除)
if (hitPerTextMatch)
{
foreach (var r in cleanResults)
{
if (string.IsNullOrWhiteSpace(r.TextHandle)) continue;
using (var selCmd2 = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM \"" + tblName + "\" WHERE 文本Handle=@th;", conn))
{
selCmd2.Parameters.AddWithValue("@th", r.TextHandle);
object cnt = selCmd2.ExecuteScalar();
int n = (cnt != null && cnt != DBNull.Value) ? Convert.ToInt32(cnt) : 0;
if (n > 0)
{
using (var delCmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + tblName + "\" WHERE 文本Handle=@th;", conn))
{
delCmd.Parameters.AddWithValue("@th", r.TextHandle);
int aff = delCmd.ExecuteNonQuery();
deletedCount += aff;
}
}
}
}
foreach (string fh in perTextFrames)
if (!oldFrameHandlesToDelete.Contains(fh)) oldFrameHandlesToDelete.Add(fh);
}
// ★ 删除 PaTextSelector 刚画的新合并框
if (!string.IsNullOrWhiteSpace(mergeHandle))
if (!newFramesToErase.Contains(mergeHandle))
newFramesToErase.Add(mergeHandle);
Log("[PaYuanjianInsert] 合并模式:删除重复项 mergedMatch=" + hitMergeMatch
+ " perTextMatch=" + hitPerTextMatch + ", 删除行数=" + deletedCount
+ ", 删除旧框数=" + oldFrameHandlesToDelete.Count);
}
else
{
// ★ 没有匹配:新增一行(合并规格)
toInsert.Add(new PaTextResult
{
Text = mergedSpec,
FrameHandle = mergeHandle,
TextHandle = mergedTextHandle
});
}
}
else
{
// ★ 正向模式:每个文本一条记录,按"同一个文本实体"判断是否重复
foreach (var r in cleanResults)
{
// ★ 按文本Handle精确匹配:同一文本实体才删除,
// 不同位置的相同内容文本(Handle不同)正常插入
List<string> matchFrames = GetYuanjianFrameHandlesByTextHandle(
_currentProjectId, _currentTuhao, _currentXianghao, r.Text, r.TextHandle);
if (matchFrames.Count > 0)
{
// 重复:按文本Handle删除数据库行 + 旧框 + 新画的框
using (var delCmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + tblName + "\" WHERE 文本Handle=@th;", conn))
{
delCmd.Parameters.AddWithValue("@th", r.TextHandle);
int aff = delCmd.ExecuteNonQuery();
deletedCount += aff;
}
foreach (string fh in matchFrames) oldFrameHandlesToDelete.Add(fh);
if (!string.IsNullOrWhiteSpace(r.FrameHandle)) newFramesToErase.Add(r.FrameHandle);
}
else
{
toInsert.Add(r);
}
}
}
// ★ 写入需要新增的项
if (toInsert.Count > 0)
{
// 先取当前最大序号(用MAX函数)
int startSeq = 0;
using (var cntCmd = new System.Data.SQLite.SQLiteCommand(
"SELECT COALESCE(MAX(CAST(序号 AS INTEGER)), 0) FROM \"" + tblName + "\";", conn))
{
object o = cntCmd.ExecuteScalar();
if (o != null && o != DBNull.Value) startSeq = Convert.ToInt32(o);
}
// ★ 图纸规格=识别出的原图文本(与规格列同值,主程序惯例:规格可后续替换,图纸规格留图纸原文)
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 元件名称, 规格, 图纸规格, 单位, 数量, 采购系数, 报出系数," +
" 表价, 采购单价, 采购价, 报出单价, 报出价, 类型, 框Handle, 文本Handle) " +
"VALUES (@seq, @name, @spec, @tzg, @unit, @shuliang, @caigou, @baochu, 0, 0, 0, 0, 0, '元件', @fh, @th);";
for (int i = 0; i < toInsert.Count; i++)
{
// ★ 自动匹配元件名称(用户库CSV模糊匹配,匹配不上为空)
string matchedName = MatchSpecName(toInsert[i].Text) ?? "";
if (matchedName.Length == 0) aiFailedSpecs.Add(toInsert[i].Text);
using (var insCmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
insCmd.Parameters.AddWithValue("@seq", (startSeq + i + 1).ToString());
insCmd.Parameters.AddWithValue("@name", matchedName);
insCmd.Parameters.AddWithValue("@spec", toInsert[i].Text);
insCmd.Parameters.AddWithValue("@tzg", toInsert[i].Text);
insCmd.Parameters.AddWithValue("@unit", "只");
insCmd.Parameters.AddWithValue("@shuliang", "1");
insCmd.Parameters.AddWithValue("@caigou", "1");
insCmd.Parameters.AddWithValue("@baochu", "1");
insCmd.Parameters.AddWithValue("@fh", toInsert[i].FrameHandle ?? "");
insCmd.Parameters.AddWithValue("@th", toInsert[i].TextHandle ?? "");
insCmd.ExecuteNonQuery();
}
}
}
}
// ★ 删除旧框和重复新框——合并成一次批量删除(单事务+单次刷新)
var framesToErase = new List<string>(oldFrameHandlesToDelete);
framesToErase.AddRange(newFramesToErase);
DeleteFramesByHandles(framesToErase);
// ★ 重新编号(删除后序号可能不连续)
RenumberTable(_currentProjectId, "\"" + tblName + "\"");
_yuanjianCache.Clear();
_xianghaoCache.Clear();
// 直接在数据库层面重算(不经过内存回写)
try { RecalcAllFromDatabase(_currentProjectId); } catch { }
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
// ★ 收集新插入元件的框Handle(finally 里再统一选中,
// 避免被后续 SaveTable/AcceptChanges/表格刷新清掉选中)
foreach (var r in toInsert)
if (!string.IsNullOrWhiteSpace(r.FrameHandle))
_pendingSelectYuanjianHandles.Add(r.FrameHandle);
// 保存
try
{
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (_yuanjianTable != null)
{
SaveTableToDatabase(_yuanjianTable, _currentProjectId,
"\"" + _yuanjianTable.TableName + "\"", null, null, true);
_yuanjianTable.AcceptChanges();
}
}
catch { }
SaveCurrentDwg();
if (deletedCount > 0)
{
Log("[PaYuanjianInsert] 删除了 " + deletedCount + " 条重复元件(匹配框:" + oldFrameHandlesToDelete.Count + ")");
}
// ★ DeepSeek AI 兜底:本地未匹配到名称的规格,异步提交 AI 识别并回填
if (aiFailedSpecs.Count > 0)
{
Log("[PaYuanjianInsert] " + aiFailedSpecs.Count + " 条本地未匹配,提交AI识别");
AiFillMissingNames(aiFailedSpecs, tblName);
}
}
catch (Exception ex)
{
MessageBox.Show(this, "扒元件失败:\n" + ex.Message,
"扒元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_isReloading = false;
// ★ 扒到的元件行保持选中(供 F1 合并):延迟两拍执行,
// 等保存/刷新/绑定事件链全部走完,否则选中会被冲掉
if (_pendingSelectYuanjianHandles.Count > 0)
{
var snapshot = new List<string>(_pendingSelectYuanjianHandles);
_pendingSelectYuanjianHandles.Clear();
BeginInvoke((Action)(() =>
{
BeginInvoke((Action)(() =>
{
SelectYuanjianRowsByFrameHandles(snapshot);
Log("[PaYuanjianInsert] 已选中 " + snapshot.Count + " 条新元件行(供F1合并)");
}));
}));
}
}
}
/// <summary>查询图号是否已存在(避免重复插入)。</summary>
private bool TuhaoExists(string projectId, string tuhao)
{
return !string.IsNullOrEmpty(GetTuhaoFrameHandle(projectId, tuhao));
}
/// <summary>
/// ★ 按"同一个文本实体"判断重复:查该文本实体Handle是否已入库。
/// 命中返回旧框Handle(真重复);未命中返回 null(不同位置的相同内容文本,正常插入)。
/// </summary>
private string GetTuhaoFrameHandleByTextHandle(string projectId, string tuhao, string textHandle)
{
// 旧数据没有文本Handle → 按旧逻辑(文本内容)判断,保持旧行为
if (string.IsNullOrWhiteSpace(textHandle)) return GetTuhaoFrameHandle(projectId, tuhao);
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return null;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ 先确保旧图号表有"文本Handle"列,避免查询报错
EnsureColumns(conn, TuhaoTableName, new[] { "文本Handle" });
// ★ 双条件:同一文本实体(Handle精确匹配) 或 旧数据行(无文本Handle,按内容兜底)
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM " + TuhaoTableName +
" WHERE 文本Handle=@th OR (图号=@t AND (文本Handle IS NULL OR 文本Handle='')) LIMIT 1;", conn))
{
cmd.Parameters.AddWithValue("@th", textHandle);
cmd.Parameters.AddWithValue("@t", tuhao);
using (var rdr = cmd.ExecuteReader())
{
if (rdr.Read())
{
object v = rdr["框Handle"];
if (v != null && v != DBNull.Value)
{
string s = v.ToString();
if (!string.IsNullOrWhiteSpace(s)) return s;
}
}
}
}
}
}
catch { }
return null;
}
/// <summary>查询指定图号的框Handle(不存在返回null)。</summary>
private string GetTuhaoFrameHandle(string projectId, string tuhao)
{
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return null;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM " + TuhaoTableName + " WHERE 图号=@t LIMIT 1;", conn))
{
cmd.Parameters.AddWithValue("@t", tuhao);
using (var rdr = cmd.ExecuteReader())
{
if (rdr.Read())
{
object v = rdr["框Handle"];
if (v != null && v != DBNull.Value)
{
string s = v.ToString();
if (!string.IsNullOrWhiteSpace(s)) return s;
}
}
}
}
}
}
catch { }
return null;
}
/// <summary>查询箱号是否已存在(避免重复插入)。</summary>
private bool XianghaoExists(string projectId, string tuhao, string xianghao)
{
return !string.IsNullOrEmpty(GetXianghaoFrameHandle(projectId, tuhao, xianghao));
}
/// <summary>
/// ★ 按"同一个文本实体"判断重复:查该文本实体Handle是否已入库。
/// 命中返回旧框Handle(真重复);未命中返回 null(不同位置的相同内容文本,正常插入)。
/// </summary>
private string GetXianghaoFrameHandleByTextHandle(string projectId, string tuhao, string xianghao, string textHandle)
{
// 旧数据没有文本Handle → 按旧逻辑(文本内容)判断,保持旧行为
if (string.IsNullOrWhiteSpace(textHandle)) return GetXianghaoFrameHandle(projectId, tuhao, xianghao);
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return null;
string tableName = GetQuoteTableName(projectId, tuhao);
if (string.IsNullOrEmpty(tableName)) return null;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ 先确保旧箱号表有"文本Handle"列,避免查询报错
EnsureColumns(conn, "\"" + tableName + "\"", new[] { "文本Handle" });
// ★ 双条件:同一文本实体(Handle精确匹配) 或 旧数据行(无文本Handle,按内容兜底)
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + tableName + "\"" +
" WHERE 文本Handle=@th OR (箱号=@x AND (文本Handle IS NULL OR 文本Handle='')) LIMIT 1;", conn))
{
cmd.Parameters.AddWithValue("@th", textHandle);
cmd.Parameters.AddWithValue("@x", xianghao);
using (var rdr = cmd.ExecuteReader())
{
if (rdr.Read())
{
object v = rdr["框Handle"];
if (v != null && v != DBNull.Value)
{
string s = v.ToString();
if (!string.IsNullOrWhiteSpace(s)) return s;
}
}
}
}
}
}
catch { }
return null;
}
/// <summary>查询指定箱号的框Handle(不存在返回null)。</summary>
private string GetXianghaoFrameHandle(string projectId, string tuhao, string xianghao)
{
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return null;
string tableName = GetQuoteTableName(projectId, tuhao);
if (string.IsNullOrEmpty(tableName)) return null;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + tableName + "\" WHERE 箱号=@x LIMIT 1;", conn))
{
cmd.Parameters.AddWithValue("@x", xianghao);
using (var rdr = cmd.ExecuteReader())
{
if (rdr.Read())
{
object v = rdr["框Handle"];
if (v != null && v != DBNull.Value)
{
string s = v.ToString();
if (!string.IsNullOrWhiteSpace(s)) return s;
}
}
}
}
}
}
catch { }
return null;
}
/// <summary>查询指定元件规格的所有框Handle(可能有多个重复行,返回List)。</summary>
private List<string> GetYuanjianFrameHandles(string projectId, string tuhao, string xianghao, string spec)
{
var list = new List<string>();
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return list;
string tableName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(tableName)) return list;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + tableName + "\" WHERE 规格=@s;", conn))
{
cmd.Parameters.AddWithValue("@s", spec);
using (var rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
object v = rdr["框Handle"];
if (v != null && v != DBNull.Value)
{
string s = v.ToString();
if (!string.IsNullOrWhiteSpace(s)) list.Add(s);
}
}
}
}
}
}
catch { }
return list;
}
/// <summary>
/// ★ 按"同一个文本实体"判断重复:查该文本实体Handle在元件表中是否已入库。
/// 命中返回旧框Handle列表(真重复);未命中返回空列表(不同位置的相同内容文本,正常插入)。
/// </summary>
private List<string> GetYuanjianFrameHandlesByTextHandle(string projectId, string tuhao, string xianghao, string spec, string textHandle)
{
var list = new List<string>();
// 旧数据没有文本Handle → 按旧逻辑(文本内容)判断,保持旧行为
if (string.IsNullOrWhiteSpace(textHandle)) return GetYuanjianFrameHandles(projectId, tuhao, xianghao, spec);
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return list;
string tableName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(tableName)) return list;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ 先确保旧元件表有"文本Handle"列,避免查询报错
EnsureColumns(conn, "\"" + tableName + "\"", new[] { "文本Handle" });
// ★ 双条件:同一文本实体(Handle精确匹配) 或 旧数据行(无文本Handle,按规格兜底)
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + tableName + "\"" +
" WHERE 文本Handle=@th OR (规格=@s AND (文本Handle IS NULL OR 文本Handle=''));", conn))
{
cmd.Parameters.AddWithValue("@th", textHandle);
cmd.Parameters.AddWithValue("@s", spec);
using (var rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
object v = rdr["框Handle"];
if (v != null && v != DBNull.Value)
{
string s = v.ToString();
if (!string.IsNullOrWhiteSpace(s)) list.Add(s);
}
}
}
}
}
}
catch { }
return list;
}
// ============================================================
// 扒元件自动匹配元件名称(用户库CSV模糊匹配)
// · 扫描插件上级目录/自身目录下最新的 用户库_*.csv(GBK编码)
// · 匹配策略:L1归一化相等 → L2包含(60%长度) → L3编辑距离≥0.70
// · 硬保护:模糊匹配时数字序列必须完全一致(63A 不会配到 100A)
// ============================================================
/// <summary>用户库条目:规格 → 元件名称</summary>
private class SpecNameEntry
{
public string Name;
public string NormSpec; // 归一化后的规格
public string[] Numbers; // 规格中的数字序列(模糊匹配保护用)
public string Skeleton; // 字母骨架(去数字/分隔符/尾部单位,如 DZ47-4P/C80A → DZPC)
}
private static List<SpecNameEntry> _specNameLib = null;
private static string _specLibCsvPath = null;
private static DateTime _specLibCsvTime = DateTime.MinValue;
/// <summary>
/// 确保用户库CSV已加载(取最新修改时间的 用户库_*.csv,
/// 文件时间戳变化时自动重新加载;文件被 Excel 占用时用共享读)。
/// </summary>
private static void EnsureUserLibCsvLoaded()
{
try
{
// 候选目录:上级目录(成套报价软件根目录) + 插件目录自身
var dirs = new List<string>();
try
{
var parent = Path.GetDirectoryName(_baseDir);
if (!string.IsNullOrEmpty(parent) && Directory.Exists(parent)) dirs.Add(parent);
}
catch { }
dirs.Add(_baseDir);
string best = null;
DateTime bestTime = DateTime.MinValue;
foreach (string d in dirs)
{
try
{
string[] files = Directory.GetFiles(d, "用户库*.csv");
foreach (string f in files)
{
DateTime t = File.GetLastWriteTime(f);
if (best == null || t > bestTime) { best = f; bestTime = t; }
}
}
catch { }
}
if (best == null) return; // 没找到CSV,保持空库
// 时间戳没变且已加载 → 不重复加载
if (_specNameLib != null &&
string.Equals(best, _specLibCsvPath, StringComparison.OrdinalIgnoreCase) &&
bestTime == _specLibCsvTime) return;
var lib = new List<SpecNameEntry>();
var prefixRules = new List<string[]>();
// ★ 共享读:文件被 Excel 打开也能读
using (var fs = new FileStream(best, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var sr = new StreamReader(fs, DetectCsvEncoding(fs)))
{
string line;
bool first = true;
while ((line = sr.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
string[] cols = SplitCsvLine(line);
if (cols.Length < 2) continue;
string name = cols[0].Trim();
string spec = cols[1].Trim();
if (first)
{
first = false;
// 跳过表头
if (name == "元件名称") continue;
}
if (name.Length == 0 || spec.Length == 0) continue;
// ★ "元件类型"列 = "前缀" 的行是型号前缀规则(不是库条目)
string elemType = cols.Length >= 10 ? cols[9].Trim() : "";
if (elemType == "前缀")
{
string pfx = spec.ToUpperInvariant();
if (pfx.Length >= 2) prefixRules.Add(new[] { pfx, name });
continue;
}
string normSpec = NormalizeSpec(spec);
lib.Add(new SpecNameEntry
{
Name = name,
NormSpec = normSpec,
// ★ 必须用原始 spec 提取数字(保留空格/符号作数字边界);
// 用 normSpec 会导致 3300 800 粘连成 3300800,数字序列错位
Numbers = ExtractNumbers(spec),
Skeleton = ExtractSkeleton(spec)
});
}
}
_specNameLib = lib;
_prefixRules = prefixRules;
_specLibCsvPath = best;
_specLibCsvTime = bestTime;
WriteDebugLog("[用户库匹配] 加载CSV: " + best + " 条数=" + lib.Count +
" 前缀规则=" + prefixRules.Count);
}
catch (Exception ex) { WriteDebugLog("[用户库匹配] 加载CSV异常: " + ex.Message); }
}
/// <summary>检测CSV编码:有UTF-8 BOM用UTF8,否则按GBK(主程序导出编码)。</summary>
private static System.Text.Encoding DetectCsvEncoding(FileStream fs)
{
try
{
long pos = fs.Position;
int b1 = fs.ReadByte();
int b2 = fs.ReadByte();
int b3 = fs.ReadByte();
fs.Position = pos;
if (b1 == 0xEF && b2 == 0xBB && b3 == 0xBF) return System.Text.Encoding.UTF8;
}
catch { }
return System.Text.Encoding.GetEncoding("GBK");
}
/// <summary>按CSV规则拆分一行(支持双引号包裹和转义"")。</summary>
private static string[] SplitCsvLine(string line)
{
var parts = new List<string>();
var sb = new System.Text.StringBuilder();
bool inQuotes = false;
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '"')
{
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') { sb.Append('"'); i++; }
else inQuotes = !inQuotes;
}
else if (c == ',' && !inQuotes)
{
parts.Add(sb.ToString());
sb.Length = 0;
}
else sb.Append(c);
}
parts.Add(sb.ToString());
return parts.ToArray();
}
/// <summary>规格归一化:全角→半角、统一大写、去空白和分隔符号(保留字母/数字/中文)。</summary>
private static string NormalizeSpec(string s)
{
if (string.IsNullOrEmpty(s)) return "";
var sb = new System.Text.StringBuilder(s.Length);
foreach (char c0 in s)
{
char c = c0;
// 全角→半角
if (c >= 0xFF01 && c <= 0xFF5E) c = (char)(c - 0xFEE0);
else if (c == 0x3000) c = ' ';
if (c == ' ' || c == '\t') continue;
if (IsSpecSeparator(c)) continue;
sb.Append(char.ToUpperInvariant(c));
}
return sb.ToString();
}
/// <summary>规格中的分隔符号(归一化时删除;数字提取时作为数字段边界)。</summary>
private static bool IsSpecSeparator(char c)
{
return c == '-' || c == '/' || c == '*' || c == '×' || c == '·' || c == '.' ||
c == '、' || c == ',' || c == ',' || c == '(' || c == ')' || c == '(' ||
c == ')' || c == '#' || c == '+' || c == '_' || c == ':' || c == ':' ||
c == '~' || c == '~';
}
/// <summary>
/// 轻量归一化(保留分隔符和空格):全角→半角、统一大写。
/// ★ 用于数字序列提取:分隔符和空格是数字段的天然边界,
/// 先删分隔符会导致 HYM1-63/3300 的 63 和 3300 粘连成 633300;
/// 删空格会导致 3300 800 粘连成 3300800(与 3300,800 数字序列不一致)。
/// </summary>
private static string NormalizeKeepSeparators(string s)
{
if (string.IsNullOrEmpty(s)) return "";
var sb = new System.Text.StringBuilder(s.Length);
foreach (char c0 in s)
{
char c = c0;
if (c >= 0xFF01 && c <= 0xFF5E) c = (char)(c - 0xFEE0);
else if (c == 0x3000) c = ' ';
if (c == '\t') c = ' '; // 制表符当空格
sb.Append(char.ToUpperInvariant(c));
}
return sb.ToString();
}
/// <summary>
/// 提取规格中的所有数字序列(在保留分隔符的版本上提取,
/// 如 HYM1-63/3300 63A → ["1","63","3300","63"])。
/// </summary>
private static string[] ExtractNumbers(string spec)
{
var list = new List<string>();
if (string.IsNullOrEmpty(spec)) return list.ToArray();
string light = NormalizeKeepSeparators(spec);
var sb = new System.Text.StringBuilder();
foreach (char c in light)
{
if (c >= '0' && c <= '9') sb.Append(c);
else if (sb.Length > 0) { list.Add(sb.ToString()); sb.Length = 0; }
}
if (sb.Length > 0) list.Add(sb.ToString());
return list.ToArray();
}
/// <summary>两个数字序列是否完全一致。</summary>
private static bool NumbersEqual(string[] a, string[] b)
{
if (a == null || b == null) return false;
if (a.Length != b.Length) return false;
for (int i = 0; i < a.Length; i++)
if (a[i] != b[i]) return false;
return true;
}
/// <summary>
/// 数字序列兼容(子序列检查):短方的数字按顺序都能在长方中找到即通过。
/// ★ 用于 L2 包含关系:一方文本是另一方的一部分,数字自然也可能少
/// (如图纸只写 HD13BX-1500/31,库里是 HD13BX-1500/31 1500A)。
/// ★ 依然防误配:63A vs 100A 时 63 不在对方序列对应位置,拒绝。
/// </summary>
private static bool NumbersCompatible(string[] a, string[] b)
{
if (a == null || b == null) return false;
string[] shorter = a.Length <= b.Length ? a : b;
string[] longer = a.Length <= b.Length ? b : a;
if (longer.Length == 0) return shorter.Length == 0;
int i = 0;
foreach (string n in longer)
{
if (i < shorter.Length && n == shorter[i]) i++;
}
return i == shorter.Length;
}
/// <summary>
/// 数字序列严格相等 + 两种容错(L3 模糊匹配用):
/// 1.粘连拆分:图纸连写导致数字粘连(如 0.66+1500 提取出 661500),拆成两段后对上即通过;
/// 2.壳架容错:图纸把额定电流写进型号段(如 HYW1-2500/3P 2500A,2500出现两次),
/// 库里型号段是壳架等级(HYW1-3200),删掉图纸型号段那个数字后能对上即通过。
/// ★ 仅当该数字在图纸中出现≥2次才允许删,220V/380V 这类无重复差异依然拒绝。
/// </summary>
private static bool NumbersEqualEx(string[] a, string[] b)
{
if (a == null || b == null) return false;
if (NumbersEqual(a, b)) return true;
// 容错1:粘连拆分
if (TrySplitAndCompatible(a, b)) return true;
if (TrySplitAndCompatible(b, a)) return true;
// 容错2:壳架(删图纸中重复出现的电流数字之一)
if (TryDropFrameCurrent(a, b)) return true;
if (TryDropFrameCurrent(b, a)) return true;
return false;
}
/// <summary>
/// 壳架容错:seq 中存在出现≥2次的数字 X,删掉其中一次后作为子序列能对上 other。
/// 典型:图纸 HYW1-2500/3P 2500A(2500×2) vs 库 HYW1-3200/3P 2500A(3200×1,2500×1)。
/// </summary>
private static bool TryDropFrameCurrent(string[] seq, string[] other)
{
for (int i = 0; i < seq.Length; i++)
{
string s = seq[i];
if (s == null || s.Length < 2) continue;
// 该数字必须在 seq 中出现≥2次(型号段+电流段重复)
int cnt = 0;
foreach (string n in seq) if (n == s) cnt++;
if (cnt < 2) continue;
// 删掉第 i 个,其余保持顺序
var list = new List<string>(seq.Length - 1);
for (int k = 0; k < seq.Length; k++)
if (k != i) list.Add(seq[k]);
if (NumbersCompatible(list.ToArray(), other)) return true;
}
return false;
}
/// <summary>把 seq 中某个数字串拆成两段,拆分后作为子序列能对上 other 即返回 true。</summary>
private static bool TrySplitAndCompatible(string[] seq, string[] other)
{
for (int i = 0; i < seq.Length; i++)
{
string s = seq[i];
if (s == null || s.Length < 2) continue;
for (int cut = 1; cut < s.Length; cut++)
{
var list = new List<string>(seq.Length + 1);
for (int k = 0; k < seq.Length; k++)
{
if (k == i)
{
list.Add(s.Substring(0, cut));
list.Add(s.Substring(cut));
}
else list.Add(seq[k]);
}
if (NumbersCompatible(list.ToArray(), other)) return true;
}
}
return false;
}
/// <summary>
/// 提取字母骨架:去数字、去分隔符、去尾部单位词(A/V/kA 等)。
/// ★ 用于图纸省略壳架等级的场景:DZ47-4P/C80A 与 DZ47-100 4P C80
/// 骨架同为 DZPC,配合数字子序列保护可安全匹配。
/// </summary>
private static string ExtractSkeleton(string spec)
{
if (string.IsNullOrEmpty(spec)) return "";
string light = NormalizeKeepSeparators(spec);
var sb = new System.Text.StringBuilder();
for (int i = 0; i < light.Length; i++)
{
char c = light[i];
if (c >= 'A' && c <= 'Z')
{
// 单位词必须紧跟在数字后面才有效(如 80A、450V);
// 6L2-V 的 V 前面是分隔符,属于型号的一部分,不能截断
bool prevIsDigit = i > 0 && light[i - 1] >= '0' && light[i - 1] <= '9';
if (prevIsDigit && IsTailUnit(light.Substring(i))) break;
sb.Append(c);
}
// 数字、分隔符、中文等其他字符全部跳过
}
return sb.ToString();
}
/// <summary>判断剩余部分是否为纯尾部单位词(如 "A"、"80A" 里的 A、"0~450V" 里的 V)。</summary>
private static bool IsTailUnit(string rest)
{
if (string.IsNullOrEmpty(rest)) return false;
string r = rest.Trim();
if (r.Length > 2) return false; // 单位最多2字符(HZ/KA/KW/VA)
return r == "A" || r == "V" || r == "W" || r == "VA" || r == "HZ" || r == "KA" || r == "KW";
}
/// <summary>Levenshtein 编辑距离(滚动数组实现)。</summary>
private static int LevenshteinDistance(string a, string b)
{
if (string.IsNullOrEmpty(a)) return string.IsNullOrEmpty(b) ? 0 : b.Length;
if (string.IsNullOrEmpty(b)) return a.Length;
int n = a.Length, m = b.Length;
int[] prev = new int[m + 1];
int[] cur = new int[m + 1];
for (int j = 0; j <= m; j++) prev[j] = j;
for (int i = 1; i <= n; i++)
{
cur[0] = i;
for (int j = 1; j <= m; j++)
{
int cost = (a[i - 1] == b[j - 1]) ? 0 : 1;
int v1 = prev[j] + 1;
int v2 = cur[j - 1] + 1;
int v3 = prev[j - 1] + cost;
cur[j] = Math.Min(v1, Math.Min(v2, v3));
}
int[] tmp = prev; prev = cur; cur = tmp;
}
return prev[m];
}
/// <summary>
/// 规格匹配元件名称(模糊):
/// L1 归一化后完全相等 → 直接命中;
/// L2 包含关系(短方长度≥长方60%,数字序列一致);
/// L3 编辑距离相似度≥0.70(数字序列必须完全一致,唯一最高分才命中)。
/// 都不中返回 null(不硬猜)。
/// </summary>
/// <summary>
/// 粘连清洗:扒图可能把相邻文本粘连(如 "0mAVigiC65N-...")。
/// 若文本开头是"数字+单位(mA/A/V/W)"且后面紧跟大写字母开头的型号,
/// 从型号处截取;否则原样返回。
/// </summary>
private static string CleanLeadingNoise(string spec)
{
if (string.IsNullOrEmpty(spec)) return spec;
string s = spec.Trim();
// ★ 先剥"数量×"前缀:如 "3xLMZ1-0.5" → "LMZ1-0.5"、"2×AD16" → "AD16"
// (图纸常在型号前写元件数量,3x=3只)
if (s.Length > 2)
{
char c0 = s[0];
if (c0 >= '0' && c0 <= '9')
{
int i = 0;
while (i < s.Length && s[i] >= '0' && s[i] <= '9') i++;
if (i < s.Length && (s[i] == 'x' || s[i] == 'X' || s[i] == '×' || s[i] == '*'))
{
int after = i + 1;
if (after < s.Length && s[after] >= 'A' && s[after] <= 'Z')
return s.Substring(after);
}
}
}
// 找开头最长的"数字(可含小数点)+单位"模式
int j = 0;
while (j < s.Length && ((s[j] >= '0' && s[j] <= '9') || s[j] == '.')) j++;
if (j == 0 || j >= s.Length) return spec; // 开头不是数字
// 单位段(1~3个字母)
int u = j;
while (u < s.Length && u - j <= 3 && s[u] >= 'A' && s[u] <= 'z') u++;
if (u == j || u >= s.Length) return spec; // 没有单位或后面没内容
string unit = s.Substring(j, u - j).ToUpperInvariant();
if (unit != "MA" && unit != "A" && unit != "V" && unit != "W" && unit != "KW" && unit != "HZ") return spec;
// 单位后必须紧跟大写字母(型号开头)
char next = s[u];
if (next < 'A' || next > 'Z') return spec;
return s.Substring(u); // 从型号处截取
}
// ★ 匹配结果缓存:同一规格文本(一批框选里重复文本很常见)只完整跑一次 L0-L5。
// L3 会按库逐条算编辑距离(库大时是 CPU 热点),缓存能直接砍掉重复计算。
private static readonly System.Collections.Generic.Dictionary<string, string> _specMatchCache =
new System.Collections.Generic.Dictionary<string, string>(System.StringComparer.Ordinal);
private static readonly object _specMatchCacheLock = new object();
/// <summary>规格匹配元件名称(带结果缓存):相同规格文本第二次起直接返回缓存结果。</summary>
private static string MatchSpecName(string spec)
{
if (string.IsNullOrWhiteSpace(spec)) return null;
string cached;
lock (_specMatchCacheLock)
{
if (_specMatchCache.TryGetValue(spec, out cached)) return cached;
}
string result = MatchSpecNameCore(spec);
lock (_specMatchCacheLock)
{
// 上限保护:超过2万条清空重来(规格文本总量有限,防内存无限增长)
if (_specMatchCache.Count > 20000) _specMatchCache.Clear();
_specMatchCache[spec] = result;
}
return result;
}
private static string MatchSpecNameCore(string spec)
{
if (string.IsNullOrWhiteSpace(spec)) return null;
// ★ 粘连清洗:扒图可能把相邻文本粘连(如 "0mAVigiC65N-...")。
// 若文本前部含"数字+单位(mA/A/V)"后跟已知型号,从型号处截取
spec = CleanLeadingNoise(spec);
EnsureUserLibCsvLoaded();
if (_specNameLib == null || _specNameLib.Count == 0) return null;
string norm = NormalizeSpec(spec);
if (norm.Length == 0) return null;
string[] nums = ExtractNumbers(spec);
string result = null;
string via = "none";
// L0: 型号前缀规则(本地权威,最优先,覆盖国标型号命名体系)
string ruleName = MatchByPrefixRule(spec);
if (ruleName != null)
{
WriteDebugLog("[用户库匹配] 「" + spec + "」 → " + ruleName + " via L0规则");
return ruleName;
}
// L0.5: JB/T 2930 国标类组代号(官方定义:前2位字母=品类代号)
string gbName = MatchByGbCode(spec);
if (gbName != null)
{
WriteDebugLog("[用户库匹配] 「" + spec + "」 → " + gbName + " via L0.5国标");
return gbName;
}
// L1: 归一化后完全相等
foreach (var e in _specNameLib)
{
if (e.NormSpec == norm) { result = e.Name; via = "L1相等"; break; }
}
// L2: 包含关系(如图纸只写型号主体,库里有完整版)
if (result == null)
{
foreach (var e in _specNameLib)
{
if (e.NormSpec.Length == 0) continue;
bool contains = e.NormSpec.Contains(norm) || norm.Contains(e.NormSpec);
if (!contains) continue;
int shortLen = Math.Min(e.NormSpec.Length, norm.Length);
int longLen = Math.Max(e.NormSpec.Length, norm.Length);
if (longLen == 0 || (double)shortLen / longLen < 0.6) continue;
if (!NumbersCompatible(nums, e.Numbers)) continue; // ★ 数字子序列兼容(允许少,不允许错)
result = e.Name; via = "L2包含"; break;
}
}
// L3: 编辑距离模糊匹配(字母/符号容错,数字绝对不放过)
if (result == null)
{
string bestName = null;
double bestScore = 0.0;
int bestCount = 0;
foreach (var e in _specNameLib)
{
if (e.NormSpec.Length == 0) continue;
if (!NumbersEqualEx(nums, e.Numbers)) continue; // ★ 数字一致(含粘连拆分容错)
// ★ 长度差剪枝:编辑距离 ≥ 长度差,长度差已超过 0.30×maxLen 时
// score 必然 < 0.70,直接跳过(编辑距离是 O(m×n),库里条目多时全算是卡顿热点)
int maxLen = Math.Max(norm.Length, e.NormSpec.Length);
if (Math.Abs(norm.Length - e.NormSpec.Length) * 10 > maxLen * 3) continue;
int dist = LevenshteinDistance(norm, e.NormSpec);
double score = 1.0 - (double)dist / maxLen;
if (score >= 0.70)
{
if (score > bestScore + 0.0001) { bestScore = score; bestName = e.Name; bestCount = 1; }
else if (Math.Abs(score - bestScore) <= 0.0001 && bestName != e.Name) bestCount++;
}
}
if (bestCount == 1 && bestName != null) { result = bestName; via = "L3模糊"; }
}
// L4: 字母骨架匹配(图纸省略壳架等级的场景)
// DZ47-4P/C80A 与 DZ47-100 4P C80:骨架同为 DZPC,
// 且图纸数字 [47,4,80] 是库数字 [47,100,4,80] 的子序列 → 命中
if (result == null)
{
string skel = ExtractSkeleton(spec);
if (skel.Length >= 3)
{
string bestName = null;
int bestLen = int.MaxValue;
foreach (var e in _specNameLib)
{
if (e.Skeleton == null || e.Skeleton.Length < 3) continue;
if (e.Skeleton != skel) continue;
if (!NumbersCompatible(nums, e.Numbers)) continue; // ★ 数字子序列保护
// 多条同骨架时取数字数量最接近的
int diff = Math.Abs(nums.Length - e.Numbers.Length);
if (diff < bestLen) { bestLen = diff; bestName = e.Name; }
}
if (bestName != null) { result = bestName; via = "L4骨架"; }
}
}
// L5: 大电流启发式(最后兜底)
// 框架级特征:数字≥1600 + 3P/4P + 型号含W(万能式标记) → 万能式/智能断路器
if (result == null)
{
string sUp = spec.Trim().ToUpperInvariant();
bool hasBig = false;
foreach (string n in nums)
{
long v;
if (long.TryParse(n, out v) && v >= 1600 && v <= 6300) { hasBig = true; break; }
}
bool hasPole = sUp.Contains("/3P") || sUp.Contains("/4P") || sUp.Contains(" 3P") || sUp.Contains(" 4P") ||
sUp.EndsWith("3P") || sUp.EndsWith("4P");
bool hasW = sUp.IndexOf('W') >= 0;
if (hasBig && hasPole && hasW)
{
// 含 I/E(智能型标记,如 HUWI/CW1I) → 智能断路器
bool hasSmart = sUp.IndexOf('I') >= 0 || sUp.IndexOf('E') >= 0;
result = hasSmart ? "智能断路器" : "万能式断路器";
via = "L5大电流启发";
}
}
// ★ 匹配日志:输入/归一化/数字序列/结果,失败可从日志直接定位原因
WriteDebugLog("[用户库匹配] 「" + spec + "」 norm=" + norm +
" nums=[" + string.Join(",", nums) + "] → " +
(result == null ? "(未匹配)" : result + " via " + via));
return result;
}
// ============================================================
// DeepSeek AI 智能匹配(本地匹配失败后的兜底)
// · 设置窗口填 API Key,存插件目录 deepseek.json
// · 扒元件后本地未命中的规格批量提交 AI 识别元器件名称
// · 结果缓存到 deepseek_cache.json(同规格只问一次)
// · AI 结果回填元件表"元件名称"列
// ============================================================
private string DeepSeekConfigPath { get { return GetDbFullPath("deepseek.json"); } }
private string DeepSeekCachePath { get { return GetDbFullPath("deepseek_cache.json"); } }
/// <summary>DeepSeek 设置(加载/保存 JSON)</summary>
private class DeepSeekConfig
{
public string ApiKey = "";
public string BaseUrl = "https://api.deepseek.com";
public string Model = "deepseek-v4-flash"; // ★ 支持联网搜索的模型
public bool Enabled = false;
public bool WebSearch = true; // ★ 联网搜索(罕见型号也能查)
}
private DeepSeekConfig _dsConfig;
private System.Collections.Hashtable _dsCache;
private DeepSeekConfig LoadDsConfig()
{
if (_dsConfig != null) return _dsConfig;
var cfg = new DeepSeekConfig();
try
{
if (File.Exists(DeepSeekConfigPath))
{
string json = File.ReadAllText(DeepSeekConfigPath, System.Text.Encoding.UTF8);
var map = SimpleJsonParse(json);
if (map.ContainsKey("ApiKey")) cfg.ApiKey = (string)map["ApiKey"] ?? "";
if (map.ContainsKey("BaseUrl")) cfg.BaseUrl = (string)map["BaseUrl"] ?? "https://api.deepseek.com";
if (map.ContainsKey("Model")) cfg.Model = (string)map["Model"] ?? "deepseek-v4-flash";
// ★ 布尔值无引号,解析出的 val 可能带换行/空格前缀,比较前必须 Trim
if (map.ContainsKey("Enabled")) cfg.Enabled = ((string)map["Enabled"] ?? "").Trim() == "true";
if (map.ContainsKey("WebSearch")) cfg.WebSearch = ((string)map["WebSearch"] ?? "").Trim() != "false";
// ★ 旧模型(deepseek-chat 不支持联网)自动迁移到 v4
if (cfg.Model == "deepseek-chat" || cfg.Model == "deepseek-reasoner") cfg.Model = "deepseek-v4-flash";
}
}
catch { }
_dsConfig = cfg;
return cfg;
}
private void SaveDsConfig(DeepSeekConfig cfg)
{
try
{
string json = "{\n" +
" \"ApiKey\": \"" + (cfg.ApiKey ?? "").Replace("\"", "\\\"") + "\",\n" +
" \"BaseUrl\": \"" + (cfg.BaseUrl ?? "").Replace("\"", "\\\"") + "\",\n" +
" \"Model\": \"" + (cfg.Model ?? "").Replace("\"", "\\\"") + "\",\n" +
" \"Enabled\": " + (cfg.Enabled ? "true" : "false") + ",\n" +
" \"WebSearch\": " + (cfg.WebSearch ? "true" : "false") + "\n" +
"}";
File.WriteAllText(DeepSeekConfigPath, json, System.Text.Encoding.UTF8);
_dsConfig = cfg;
}
catch (Exception ex) { Log("[DeepSeek配置] 保存失败: " + ex.Message); }
}
/// <summary>AI 匹配缓存:规格 → 元件名称(未命中/失败也缓存,避免反复请求)</summary>
private System.Collections.Hashtable LoadDsCache()
{
if (_dsCache != null) return _dsCache;
var ht = new System.Collections.Hashtable();
try
{
if (File.Exists(DeepSeekCachePath))
{
string json = File.ReadAllText(DeepSeekCachePath, System.Text.Encoding.UTF8);
var map = SimpleJsonParse(json);
foreach (System.Collections.DictionaryEntry kv in map)
ht[kv.Key.ToString()] = (kv.Value ?? "").ToString();
}
}
catch { }
_dsCache = ht;
return ht;
}
private void SaveDsCache()
{
if (_dsCache == null) return;
try
{
var sb = new System.Text.StringBuilder("{\n");
bool first = true;
foreach (System.Collections.DictionaryEntry kv in _dsCache)
{
if (!first) sb.Append(",\n");
sb.Append(" \"" + kv.Key.ToString().Replace("\"", "\\\"") + "\": \"" +
kv.Value.ToString().Replace("\"", "\\\"") + "\"");
first = false;
}
sb.Append("\n}");
File.WriteAllText(DeepSeekCachePath, sb.ToString(), System.Text.Encoding.UTF8);
}
catch (Exception ex) { Log("[DeepSeek缓存] 保存失败: " + ex.Message); }
}
/// <summary>极简 JSON 解析(扁平 key:value 结构,支持字符串/数字/布尔值)</summary>
private static System.Collections.Hashtable SimpleJsonParse(string json)
{
var ht = new System.Collections.Hashtable();
if (string.IsNullOrEmpty(json)) return ht;
int depth = 0; bool inStr = false;
var key = new System.Text.StringBuilder();
var val = new System.Text.StringBuilder();
bool inKey = true;
for (int i = 0; i < json.Length; i++)
{
char c = json[i];
if (inStr)
{
if (c == '\\' && i + 1 < json.Length) { val.Append(json[i + 1]); i++; continue; }
if (c == '"') { inStr = false; continue; }
(inKey ? key : val).Append(c);
continue;
}
if (c == '"') { inStr = true; continue; }
if (c == '{' || c == '[') { depth++; continue; }
if (c == '}' || c == ']')
{
// ★ 闭合括号收掉最后一个键值对(布尔值结尾无逗号)
if (depth == 1 && key.Length > 0)
{
ht[key.ToString()] = val.ToString().Trim();
key.Length = 0; val.Length = 0; inKey = true;
}
depth--; continue;
}
if (c == ':' && depth == 1) { inKey = false; val.Length = 0; continue; }
if (c == ',' && depth == 1)
{
if (key.Length > 0) ht[key.ToString()] = val.ToString().Trim();
key.Length = 0; val.Length = 0; inKey = true;
continue;
}
// ★ 无引号值(true/false/数字)必须累积,否则解析成空
if (!inKey && !char.IsWhiteSpace(c)) val.Append(c);
}
if (key.Length > 0) ht[key.ToString()] = val.ToString().Trim();
return ht;
}
// ============================================================
// 电器型号前缀规则(与用户库共用同一个 CSV 数据文件)
// ★ 用户库 CSV 中"元件类型"列 = "前缀" 的行即为前缀规则:
// 隔离开关,HGL,只,1,0,1,1,规则,0,前缀
// (元件名称列=名称,规格列=型号前缀)
// ★ Excel 里直接编辑用户库即可增删规则,随库自动重载
// ★ 规则结果与 AI 冲突时,以规则为准(根治 AI 误判)
// ============================================================
private static List<string[]> _prefixRules = new List<string[]>();
/// <summary>内置最小规则集(仅当用户库无任何前缀行时兜底)。</summary>
private static readonly string[,] s_BuiltinPrefixRules =
{
{ "HGL", "隔离开关" }, { "DZ47", "小型断路器" }, { "NM1", "塑壳断路器" },
{ "CJX2", "交流接触器" }, { "RT16", "熔断器" }, { "LMZ", "电流互感器" },
};
// ============================================================
// ★ JB/T 2930 国标类组代号表(低压电器型号编制方法的官方定义)
// 国标型号前2位字母=品类代号:第1位类别+第2位组别。
// 在前缀规则(L0)之后、CSV 库(L1)之前执行,覆盖所有国标型号。
// ============================================================
private static readonly System.Collections.Hashtable s_GbClassCodes = new System.Collections.Hashtable(StringComparer.OrdinalIgnoreCase)
{
// H 刀开关/隔离开关族
{ "HD", "隔离开关" }, // 单投刀开关
{ "HS", "隔离开关" }, // 双投刀开关(转换)
{ "HR", "隔离开关" }, // 熔断器式刀开关
{ "HH", "隔离开关" }, // 封闭式负荷开关
{ "HK", "隔离开关" }, // 开启式负荷开关(胶盖闸)
{ "HZ", "转换开关" }, // 组合开关
{ "HG", "隔离开关" }, // 熔断器式隔离器(另:HG1/HG13)
// R 熔断器族
{ "RC", "熔断器" }, // 插入式
{ "RL", "熔断器" }, // 螺旋式(RL6/RL8)
{ "RM", "熔断器" }, // 密封管式
{ "RS", "快速熔断器" }, // 快速(RS0/RS3)
{ "RT", "熔断器" }, // 有填料管式(RT0/RT16/RT18)
{ "RX", "熔断器" }, // 限流
{ "RTO", "熔断器" },
// D 断路器族
{ "DW", "万能式断路器" }, // 框架式(DW15/DW45)
{ "DZ", "塑壳断路器" }, // 装置式/塑料外壳(DZ15/DZ20)
{ "DX", "小型断路器" },
{ "DM", "塑壳断路器" },
{ "DC", "断路器" },
// C 接触器族
{ "CJ", "交流接触器" }, // 交流(CJ20/CJX)
{ "CZ", "直流接触器" }, // 直流
{ "CG", "接触器" }, // 高压
{ "CP", "接触器" }, // 中频
{ "CS", "接触器" }, // 时间
// Q 起动器族
{ "QC", "电磁起动器" }, { "QS", "手动起动器" }, { "QX", "星三角起动器" },
{ "QJ", "自耦减压起动器" }, { "QT", "起动器" }, { "QU", "起动器" },
// J 控制继电器族
{ "JL", "电流继电器" }, { "JR", "热过载继电器" }, { "JS", "时间继电器" },
{ "JT", "通用继电器" }, { "JW", "温度继电器" }, { "JZ", "中间继电器" },
{ "JX", "继电器" },
// L 主令电器族
{ "LA", "按钮" }, // 按钮(LA38/LA39)
{ "LK", "主令控制器" }, { "LT", "主令开关" }, { "LW", "转换开关" },
{ "LX", "行程开关" }, { "LU", "足踏开关" }, { "LS", "主令开关" },
// Z 电阻/终端族
{ "ZB", "电阻器" }, { "ZG", "电阻器" }, { "ZC", "电阻器" },
// B 变阻器族
{ "BL", "励磁变阻器" }, { "BP", "频敏变阻器" }, { "BQ", "起动变阻器" },
{ "BT", "变阻器" }, { "BC", "旋臂变阻器" },
// M 电磁铁族
{ "MQ", "牵引电磁铁" }, { "MZ", "制动电磁铁" }, { "MW", "起重电磁铁" },
// A 其他族
{ "AD", "指示灯" }, // 信号灯(AD11/AD16 习惯用)
{ "AN", "按钮" },
// P 仪用互感器(行业习惯,P 不在 JB/T2930 主表,单列)
{ "LQG", "电流互感器" },
// 变压器(控制变压器 BK 不在 JB/T2930,行业通用)
{ "BK", "控制变压器" },
};
/// <summary>
/// ★ 国标类组代号解析:按 JB/T 2930 定义,取型号前2位字母查类组表。
/// 仅当:①前缀规则未命中 ②开头≥2位字母 ③类组表命中 时返回名称。
/// 数字开头/单字母/进口品牌型号不适用(返回 null 交给后续层)。
/// </summary>
private static string MatchByGbCode(string spec)
{
if (string.IsNullOrWhiteSpace(spec)) return null;
string s = spec.Trim();
// 前2位必须是字母(国标类组代号为2位拼音)
if (s.Length < 3) return null;
if (!((s[0] >= 'A' && s[0] <= 'Z') || (s[0] >= 'a' && s[0] <= 'z'))) return null;
if (!((s[1] >= 'A' && s[1] <= 'Z') || (s[1] >= 'a' && s[1] <= 'z'))) return null;
// 第3位应是数字或分隔符(确认是类组代号+设计序号结构,排除品牌词)
if (s.Length > 2)
{
char c2 = s[2];
bool ok = (c2 >= '0' && c2 <= '9') || c2 == '-' || c2 == ' ' || c2 == '/';
if (!ok) return null;
}
string code = s.Substring(0, 2).ToUpperInvariant();
object v = s_GbClassCodes[code];
return v as string;
}
/// <summary>
/// 按型号前缀规则查元器件名称。
/// ★ 规则来自用户库 CSV"元件类型=前缀"的行(EnsureUserLibCsvLoaded 时提取),
/// 规则前缀直接与规格开头比对(支持字母+数字混合前缀,如 DZ47/CDM3/CJX2),
/// 最长前缀优先(7→2字符)。
/// 返回 null 表示无规则。
/// </summary>
private static string MatchByPrefixRule(string spec)
{
if (string.IsNullOrWhiteSpace(spec)) return null;
string s = spec.Trim().ToUpperInvariant();
if (s.Length < 2) return null;
var rules = _prefixRules;
if (rules.Count == 0)
{
// 用户库无前缀行 → 内置兜底
for (int i = 0; i < s_BuiltinPrefixRules.GetLength(0); i++)
rules.Add(new[] { s_BuiltinPrefixRules[i, 0], s_BuiltinPrefixRules[i, 1] });
_prefixRules = rules;
}
for (int plen = 7; plen >= 2; plen--)
{
if (s.Length < plen) continue;
string head = s.Substring(0, plen);
foreach (var rule in rules)
{
if (rule[0].Length != plen || rule[0] != head) continue;
return rule[1];
}
}
return null;
}
/// <summary>打开 DeepSeek 设置窗口(运行时创建,不改 ui.cs)</summary>
private void ShowDeepSeekSettings()
{
var cfg = LoadDsConfig();
var form = new System.Windows.Forms.Form();
form.Text = "DeepSeek AI 匹配设置";
form.Size = new System.Drawing.Size(420, 260);
form.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
form.MaximizeBox = false; form.MinimizeBox = false;
var lblKey = new System.Windows.Forms.Label { Text = "API Key:", Location = new System.Drawing.Point(15, 18), AutoSize = true };
var txtKey = new System.Windows.Forms.TextBox
{
Location = new System.Drawing.Point(80, 15), Size = new System.Drawing.Size(310, 23),
Text = cfg.ApiKey, UseSystemPasswordChar = true
};
var chkShow = new System.Windows.Forms.CheckBox
{
Text = "显示密钥", Location = new System.Drawing.Point(80, 44), AutoSize = true
};
chkShow.CheckedChanged += (s2, e2) => txtKey.UseSystemPasswordChar = !chkShow.Checked;
var lblModel = new System.Windows.Forms.Label { Text = "模型:", Location = new System.Drawing.Point(15, 78), AutoSize = true };
var cboModel = new System.Windows.Forms.ComboBox
{
Location = new System.Drawing.Point(80, 74), Size = new System.Drawing.Size(180, 23),
DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList
};
cboModel.Items.Add("deepseek-v4-flash");
cboModel.Items.Add("deepseek-v4-pro");
cboModel.Items.Add("deepseek-chat");
cboModel.SelectedItem = string.IsNullOrEmpty(cfg.Model) ? "deepseek-v4-flash" : cfg.Model;
var chkEnable = new System.Windows.Forms.CheckBox
{
Text = "启用 AI 匹配(本地匹配失败后自动提交 DeepSeek 识别)",
Location = new System.Drawing.Point(15, 108), AutoSize = true, Checked = cfg.Enabled
};
var lblTip = new System.Windows.Forms.Label
{
Text = "提示:AI 结果会缓存到本地,相同规格只请求一次。\nAPI Key 从 platform.deepseek.com 获取。",
Location = new System.Drawing.Point(15, 138), AutoSize = true, ForeColor = System.Drawing.Color.Gray
};
var btnClear = new System.Windows.Forms.Button { Text = "清空AI缓存", Location = new System.Drawing.Point(80, 178), Size = new System.Drawing.Size(100, 28) };
btnClear.Click += (s2, e2) =>
{
try { if (File.Exists(DeepSeekCachePath)) File.Delete(DeepSeekCachePath); _dsCache = null; }
catch { }
MessageBox.Show(form, "AI 缓存已清空。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
};
var btnOk = new System.Windows.Forms.Button { Text = "保存", Location = new System.Drawing.Point(240, 178), Size = new System.Drawing.Size(75, 28) };
var btnCancel = new System.Windows.Forms.Button { Text = "取消", Location = new System.Drawing.Point(325, 178), Size = new System.Drawing.Size(75, 28) };
btnOk.Click += (s2, e2) =>
{
cfg.ApiKey = txtKey.Text.Trim();
cfg.Model = cboModel.SelectedItem == null ? "deepseek-v4-flash" : cboModel.SelectedItem.ToString();
cfg.Enabled = chkEnable.Checked && cfg.ApiKey.Length > 0;
SaveDsConfig(cfg);
form.Close();
};
btnCancel.Click += (s2, e2) => form.Close();
form.Controls.AddRange(new System.Windows.Forms.Control[] { lblKey, txtKey, chkShow, lblModel, cboModel, chkEnable, lblTip, btnClear, btnOk, btnCancel });
form.ShowDialog(this);
}
/// <summary>
/// 调 DeepSeek API 批量识别规格对应的元器件名称。
/// 返回 规格→名称 的哈希表(失败的规格不在结果里)。
/// </summary>
private System.Collections.Hashtable CallDeepSeekMatch(List<string> specs)
{
var result = new System.Collections.Hashtable();
var cfg = LoadDsConfig();
if (!cfg.Enabled || string.IsNullOrWhiteSpace(cfg.ApiKey)) return result;
// 构造提问:证据链推理法(查厂商→确认型号→按厂商定义归类)
var sb = new System.Text.StringBuilder();
sb.Append("你是低压电器型号识别专家,精通 JB/T 2930《低压电器产品型号编制方法》。为每个规格型号返回【元器件名称】(中文标准品类词)。\n\n");
sb.Append("【推理方法】(对每个型号按证据链三步走,不要直接猜):\n");
sb.Append("第1步·识别厂商:从型号前缀判断是哪家厂(如 HUWI→沪工/环宇系、NM→正泰、CDM→德力西、CM→常熟、CW→常熟框架、SZRM→上海人民、XBK→湘湖/新宝凯、QAM→七仪);\n");
sb.Append("第2步·确认系列:该厂商的此系列号是否存在、属于什么产品线(用你的产品知识回忆厂商型录:如 HUWI=沪工智能型万能式、YLEM=某厂智能框架、BM3=塑壳);\n");
sb.Append("第3步·按厂商定义归类:厂商型录里该系列挂哪个品类就答什么,不要凭字母表面猜。\n\n");
sb.Append("【辅助规律】(第2步无记忆时用):\n");
sb.Append("· 国标类组:前2位字母=品类(DW万能断路器/DZ塑壳/CJ交流接触器/JR热继/LA按钮/HD-HS-HR隔离开关/RT熔断器);\n");
sb.Append("· 结构特征:电流≥1600A+3P/4P+含W → 框架级(万能式/智能断路器);壳架≤630A+3P → 塑壳;63A以下+1P/2P → 微型;\n");
sb.Append("· 派生代号:L/E=漏电或电子式,I=智能型,Z=直流/智能,N=可逆;\n");
sb.Append("· 进口系列:施耐德(NSX/iC65/Vigi/LC1/LR)、ABB(S/SH)、西门子(3RT/5SL)、伊顿(IS)。\n\n");
sb.Append("【输出格式】(严格遵守):\n");
sb.Append("只返回纯 JSON 对象 {\"规格原文\":\"元器件名称\",...},不要任何解释、思考过程或其他文字;\n");
sb.Append("每个规格都必须给出判断:按证据链推理,无法确认厂商和系列时按结构特征推断,实在无据可依才给空字符串;\n");
sb.Append("名称用标准品类词:万能式断路器/智能断路器/塑壳断路器/小型断路器/漏电断路器/交流接触器/直流接触器/热过载继电器/时间继电器/中间继电器/电流互感器/电压互感器/熔断器/快速熔断器/隔离开关/负荷开关/转换开关/按钮/指示灯/行程开关/双电源/电磁起动器/自耦减压起动器/控制变压器/干式变压器/电涌保护器/电容器/电能表/软启动器/变频器等。\n");
sb.Append("规格列表:\n");
for (int i = 0; i < specs.Count; i++)
sb.Append((i + 1) + ". " + specs[i] + "\n");
try
{
// ★ 纯AI知识模式:chat/completions(不走联网搜索,快且稳)
// deepseek-v4 系列是思考模型:正式回答在 content,思考过程在 reasoning_content;
// 有时 content 空、答案藏在 reasoning_content 末尾 → 两处都提取
string body = "{" +
"\"model\":\"" + cfg.Model + "\"," +
"\"messages\":[" +
"{\"role\":\"user\",\"content\":\"" + JsonEscape(sb.ToString()) + "\"}" +
"]," +
"\"temperature\":0.1," +
"\"max_tokens\":3000" +
"}";
string url = (cfg.BaseUrl ?? "https://api.deepseek.com").TrimEnd('/') + "/chat/completions";
var req = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url);
req.Method = "POST";
req.ContentType = "application/json";
req.Headers.Add("Authorization", "Bearer " + cfg.ApiKey);
req.Timeout = 60000; // 60秒
req.ReadWriteTimeout = 60000;
req.Proxy = null; // ★ 不走系统代理(防代理环境卡死)
byte[] data = System.Text.Encoding.UTF8.GetBytes(body);
req.ContentLength = data.Length;
Log("[DeepSeek] 发起请求: " + specs.Count + " 条规格(AI知识模式), url=" + url);
using (var rs = req.GetRequestStream()) rs.Write(data, 0, data.Length);
string respText;
using (var resp = (System.Net.HttpWebResponse)req.GetResponse())
using (var sr = new StreamReader(resp.GetResponseStream(), System.Text.Encoding.UTF8))
respText = sr.ReadToEnd();
Log("[DeepSeek] 响应长度: " + respText.Length + " 字符");
// 从响应提取 content(手工解析,取 choices[0].message.content)
string content = ExtractJsonContent(respText);
if (string.IsNullOrEmpty(content))
{
Log("[DeepSeek] 响应解析失败: " + (respText.Length > 400 ? respText.Substring(0, 400) : respText));
return result;
}
// ★ 诊断:AI原始输出(排查"返回0条"问题,只打前400字符)
Log("[DeepSeek] AI输出内容: " + (content.Length > 400 ? content.Substring(0, 400) + "..." : content));
// content 里是 {"规格":"名称",...}
// ★ 思考模型(deepseek-v4)的 content 可能为空,答案在 reasoning_content 末尾:
// 思维链最后一般会写出最终 JSON,从 reasoning_content 里找 {...} 提取
var map = SimpleJsonParse(content);
if (map.Count == 0)
{
// 从 reasoning_content 提取最后一个完整 JSON 对象
int rIdx = respText.IndexOf("\"reasoning_content\":\"", System.StringComparison.Ordinal);
if (rIdx >= 0)
{
string reasoning = ExtractEscapedString(respText, rIdx + 21);
int braceStart = reasoning.LastIndexOf('{');
if (braceStart >= 0)
{
// 从最后一个 { 找到配对的 }
int depth2 = 0; int end = -1;
for (int k = braceStart; k < reasoning.Length; k++)
{
if (reasoning[k] == '{') depth2++;
else if (reasoning[k] == '}')
{
depth2--;
if (depth2 == 0) { end = k; break; }
}
}
if (end > braceStart)
{
string jsonPart = reasoning.Substring(braceStart, end - braceStart + 1);
map = SimpleJsonParse(jsonPart);
Log("[DeepSeek] 从思维链提取到 " + map.Count + " 条结果");
}
}
}
}
foreach (System.Collections.DictionaryEntry kv in map)
{
string key = kv.Key.ToString();
string name = (kv.Value ?? "").ToString().Trim();
if (name.Length == 0) continue;
// ★ 防线:AI 结果先过本地前缀规则校验,冲突时以规则为准
// (根治 AI 偶发误判,如把 HGL 隔离开关答成熔断器)
string rule = MatchByPrefixRule(key);
if (rule != null && rule != name)
{
Log("[DeepSeek校验] 「" + key + "」 AI答[" + name + "] 与规则冲突,已改用[" + rule + "]");
name = rule;
}
result[key] = name;
}
Log("[DeepSeek] 识别 " + specs.Count + " 条,返回 " + result.Count + " 条结果");
}
catch (Exception ex)
{
Log("[DeepSeek] 请求失败: " + ex.Message);
}
return result;
}
/// <summary>
/// 从 API 响应中提取模型输出文本。
/// 兼容两种格式:
/// · chat/completions: choices[0].message.content("content":"...")
/// · responses API: output[].content[].text("text":"...")或顶层"output_text"
/// </summary>
private static string ExtractJsonContent(string respText)
{
try
{
// 方式1:responses API 的 "output_text":"..."
int idx = respText.IndexOf("\"output_text\":\"", System.StringComparison.Ordinal);
if (idx >= 0)
{
string s = ExtractEscapedString(respText, idx + 15);
if (!string.IsNullOrEmpty(s)) return s;
}
// 方式2:responses API 的输出项文本 "text":"..."(跳过 summary/reasoning,
// 直接找最后一个大 text——输出 message 的正文通常最大)
int best = -1, bestLen = 0;
int pos = 0;
while (true)
{
int t = respText.IndexOf("\"text\":\"", pos, System.StringComparison.Ordinal);
if (t < 0) break;
// 取这个字符串的长度估算
int len = EstimateEscapedLen(respText, t + 8);
if (len > bestLen) { bestLen = len; best = t; }
pos = t + 8;
}
if (best >= 0)
{
string s = ExtractEscapedString(respText, best + 8);
if (!string.IsNullOrEmpty(s)) return s;
}
// 方式3:chat 的 "content":"..."
idx = respText.IndexOf("\"content\":\"", System.StringComparison.Ordinal);
if (idx >= 0)
{
return ExtractEscapedString(respText, idx + 11);
}
return null;
}
catch { return null; }
}
/// <summary>从 start 位置读取一个 JSON 转义字符串(处理 \n \" \\ \uXXXX)。</summary>
private static string ExtractEscapedString(string text, int start)
{
var sb = new System.Text.StringBuilder();
int i = start;
while (i < text.Length)
{
char c = text[i];
if (c == '\\' && i + 1 < text.Length)
{
char n = text[i + 1];
if (n == 'n') sb.Append('\n');
else if (n == 't') sb.Append('\t');
else if (n == 'r') sb.Append('\r');
else if (n == '"') sb.Append('"');
else if (n == '\\') sb.Append('\\');
else if (n == '/' ) sb.Append('/');
else if (n == 'u' && i + 5 < text.Length)
{
int code = Convert.ToInt32(text.Substring(i + 2, 4), 16);
sb.Append((char)code);
i += 4;
}
else sb.Append(n);
i += 2;
continue;
}
if (c == '"') break;
sb.Append(c);
i++;
}
return sb.ToString();
}
/// <summary>估算 start 位置转义字符串的长度(到未转义引号为止)。</summary>
private static int EstimateEscapedLen(string text, int start)
{
int i = start, len = 0;
while (i < text.Length)
{
char c = text[i];
if (c == '\\' && i + 1 < text.Length) { i += 2; len += 2; continue; }
if (c == '"') break;
i++; len++;
}
return len;
}
private static string JsonEscape(string s)
{
if (string.IsNullOrEmpty(s)) return "";
var sb = new System.Text.StringBuilder();
foreach (char c in s)
{
if (c == '"') sb.Append("\\\"");
else if (c == '\\') sb.Append("\\\\");
else if (c == '\n') sb.Append("\\n");
else if (c == '\t') sb.Append("\\t");
else if (c == '\r') sb.Append("");
else sb.Append(c);
}
return sb.ToString();
}
/// <summary>
/// 扒元件插入后:对本地匹配失败的规格异步调用 DeepSeek 补名称,
/// 完成后回填数据库 + 刷新表格(UI 线程)。
/// ★ 单飞+排队:上一个 AI 批次未完成时,后续规格累积到待办队列,
/// 完成后一并提交(防并发互踩/请求堆积)。
/// </summary>
private static readonly object _aiLock = new object();
private static bool _aiRunning = false;
private static List<string> _aiPendingSpecs = new List<string>();
private static string _aiPendingTbl = null;
private void AiFillMissingNames(List<string> failedSpecs, string tblName)
{
if (failedSpecs == null || failedSpecs.Count == 0) return;
var cfg = LoadDsConfig();
if (!cfg.Enabled || string.IsNullOrWhiteSpace(cfg.ApiKey)) return;
lock (_aiLock)
{
// 并发防抖:AI 批次进行中 → 累积到待办,由运行中的批次完成后接力
if (_aiRunning)
{
foreach (string sp in failedSpecs)
if (!_aiPendingSpecs.Contains(sp)) _aiPendingSpecs.Add(sp);
if (_aiPendingTbl == null) _aiPendingTbl = tblName;
Log("[AI队列] 批次进行中," + failedSpecs.Count + " 条已加入待办(累计 " + _aiPendingSpecs.Count + ")");
return;
}
_aiRunning = true;
_aiPendingTbl = tblName;
}
RunAiBatch(failedSpecs, tblName);
}
/// <summary>执行一个 AI 批次(完成后处理待办队列)。</summary>
private void RunAiBatch(List<string> specs, string tblName)
{
var bgw = new System.ComponentModel.BackgroundWorker();
bgw.DoWork += (s, e) =>
{
try
{
// 先查缓存,未缓存的才提交 AI
var cache = LoadDsCache();
var toAsk = new List<string>();
var localHits = new System.Collections.Hashtable();
lock (_aiLock)
{
foreach (string sp in specs)
{
if (cache.ContainsKey(sp))
{
string cached = (cache[sp] ?? "").ToString();
// ★ 规则有结果但缓存为空(AI曾未识别) → 用规则(补漏)
string rule = MatchByPrefixRule(sp);
if (rule != null && rule.Length > 0 && rule != cached)
{
Log("[缓存校验] 「" + sp + "」 缓存[" + cached + "]与规则冲突,已纠正为[" + rule + "]");
cached = rule;
cache[sp] = rule;
}
if (cached.Length > 0) localHits[sp] = cached;
}
else if (!toAsk.Contains(sp)) toAsk.Add(sp);
}
}
var ai = new System.Collections.Hashtable();
if (toAsk.Count > 0)
{
ai = CallDeepSeekMatch(toAsk);
// 写缓存(包括未命中的,避免反复问)
lock (_aiLock)
{
foreach (string sp in toAsk)
cache[sp] = ai.Contains(sp) ? ai[sp].ToString() : "";
SaveDsCache();
}
}
// 合并结果
var final = new System.Collections.Hashtable();
foreach (System.Collections.DictionaryEntry kv in localHits) final[kv.Key] = kv.Value;
foreach (System.Collections.DictionaryEntry kv in ai) final[kv.Key] = kv.Value;
e.Result = final;
}
catch (Exception ex)
{
Log("[AI回填] 后台异常: " + ex);
e.Result = new System.Collections.Hashtable();
}
};
bgw.RunWorkerCompleted += (s, e) =>
{
// ★ 无论成败都处理:先取走待办队列并释放单飞标记
List<string> pending;
string pendingTbl;
lock (_aiLock)
{
pending = new List<string>(_aiPendingSpecs);
pendingTbl = _aiPendingTbl;
_aiPendingSpecs = new List<string>();
_aiPendingTbl = null;
_aiRunning = false;
}
try
{
var final = e.Result as System.Collections.Hashtable;
if (final == null || final.Count == 0) { if (pending.Count > 0) StartNextAiBatch(pending, pendingTbl); return; }
int updated = 0;
string dbPath = GetQuoteDbPath(_currentProjectId);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
foreach (System.Collections.DictionaryEntry kv in final)
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"UPDATE \"" + tblName + "\" SET 元件名称=@name WHERE 规格=@spec AND (元件名称 IS NULL OR 元件名称='');", conn))
{
cmd.Parameters.AddWithValue("@name", kv.Value.ToString());
cmd.Parameters.AddWithValue("@spec", kv.Key.ToString());
updated += cmd.ExecuteNonQuery();
}
}
tx.Commit();
}
}
if (updated > 0)
{
Log("[AI回填] 已回填 " + updated + " 条元件名称");
_yuanjianCache.Clear();
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
}
// ★ AI 识别成功的结果追加进用户库 CSV(库越用越全,
// 下次同规格本地直接命中,不再消耗 AI)
AppendAiResultsToUserLibCsv(final);
}
catch (Exception ex) { Log("[AI回填] 回填异常: " + ex.Message); }
// ★ 接力:待办队列有货 → 继续下一个批次
if (pending.Count > 0) StartNextAiBatch(pending, pendingTbl);
};
bgw.RunWorkerAsync();
}
/// <summary>启动下一个 AI 批次(带单飞保护)。</summary>
private void StartNextAiBatch(List<string> specs, string tblName)
{
lock (_aiLock)
{
if (_aiRunning)
{
foreach (string sp in specs)
if (!_aiPendingSpecs.Contains(sp)) _aiPendingSpecs.Add(sp);
return;
}
_aiRunning = true;
_aiPendingTbl = tblName;
}
Log("[AI队列] 接力下一批 " + specs.Count + " 条");
RunAiBatch(specs, tblName);
}
/// <summary>
/// 把 AI 识别结果(规格→名称)追加到最新用户库 CSV(GBK 编码),
/// 并同步更新内存库 _specNameLib(当前会话立即生效)。
/// 已存在的规格跳过;每条占一行:名称,规格,只,1,0,1,1,AI识别,0,元件
/// </summary>
private void AppendAiResultsToUserLibCsv(System.Collections.Hashtable aiResults)
{
try
{
if (aiResults == null || aiResults.Count == 0) return;
// 找最新 CSV(与 EnsureUserLibCsvLoaded 同规则:上级目录+插件目录取最新)
string best = null; DateTime bestTime = DateTime.MinValue;
var dirs = new List<string>();
try
{
var parent = Path.GetDirectoryName(_baseDir);
if (!string.IsNullOrEmpty(parent) && Directory.Exists(parent)) dirs.Add(parent);
}
catch { }
dirs.Add(_baseDir);
foreach (string d in dirs)
{
try
{
foreach (string f in Directory.GetFiles(d, "用户库*.csv"))
{
DateTime t = File.GetLastWriteTime(f);
if (best == null || t > bestTime) { best = f; bestTime = t; }
}
}
catch { }
}
if (best == null) { Log("[AI入库] 未找到用户库CSV,跳过"); return; }
// ★ 共享读写:文件可能被 Excel 打开
var gbk = System.Text.Encoding.GetEncoding("GBK");
var existing = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var fs = System.IO.File.Open(best, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using (var sr = new StreamReader(fs, gbk))
{
string line;
while ((line = sr.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
int comma = line.IndexOf(',');
if (comma > 0) existing.Add(line.Substring(comma + 1).Trim());
}
}
// 组装新增行
var newLines = new List<string>();
foreach (System.Collections.DictionaryEntry kv in aiResults)
{
string spec = kv.Key.ToString().Trim();
string name = kv.Value.ToString().Trim();
if (spec.Length == 0 || name.Length == 0) continue;
if (existing.Contains(spec)) continue;
newLines.Add(name + "," + spec + ",只,1,0,1,1,AI识别,0,元件");
existing.Add(spec); // 防同批重复
}
if (newLines.Count == 0) { Log("[AI入库] 全部已存在,无新增"); return; }
// 追加写(共享读写防 Excel 锁;失败重试3次——扒图进程可能正读CSV)
bool written = false;
for (int attempt = 0; attempt < 3 && !written; attempt++)
{
try
{
using (var fs2 = new FileStream(best, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
using (var sw = new StreamWriter(fs2, gbk))
{
foreach (string nl in newLines) sw.WriteLine(nl);
}
written = true;
}
catch (System.IO.IOException)
{
System.Threading.Thread.Sleep(300); // 等 CSV 读取方释放
}
}
if (!written) Log("[AI入库] 写入失败(文件被占用),跳过本次(下次扒图会再尝试)");
// ★ 同步内存库:当前会话的本地匹配立即可用
try
{
EnsureUserLibCsvLoaded();
if (_specNameLib != null)
{
foreach (string nl in newLines)
{
string[] cols = nl.Split(',');
if (cols.Length >= 2)
_specNameLib.Add(new SpecNameEntry
{
Name = cols[0],
NormSpec = NormalizeSpec(cols[1]),
Numbers = ExtractNumbers(cols[1]),
Skeleton = ExtractSkeleton(cols[1])
});
}
}
}
catch { }
Log("[AI入库] 新增 " + newLines.Count + " 条到 " + Path.GetFileName(best));
}
catch (Exception ex) { Log("[AI入库] 失败: " + ex.Message); }
}
/// <summary>根据项目ID和图号获取箱号表名(查"表索引"表)。</summary>
private string GetQuoteTableName(string projectId, string tuhao)
{
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return "";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 表名 FROM " + IndexTableName + " WHERE 图号=@t LIMIT 1;", conn))
{
cmd.Parameters.AddWithValue("@t", tuhao);
object o = cmd.ExecuteScalar();
return o == null ? "" : o.ToString();
}
}
}
catch { return ""; }
}
private void InsertTuhao(string projectId, string tuhao, string frameHandle, string textHandle)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
// ★ 内部ID=创建时的图号名(与主窗口同款约定,改名不影响归属/删除)
using (var connEns = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
connEns.Open();
EnsureColumns(connEns, TuhaoTableName, new[] { "内部ID" });
}
string sql = "INSERT INTO " + TuhaoTableName +
" (序号, 图号, 内部ID, 台数, 箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率, 框Handle, 文本Handle) " +
"VALUES (@seq, @th, @nid, @taishu, 0, 0, 0, 0, 0, 0, @frameHandle, @textHandle);";
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);
// ★ 真隔离修正(2026-09-30):图号内部ID=显示名(与箱号/元件表索引的图号键一致,不能用 GUID)
cmd.Parameters.AddWithValue("@nid", tuhao);
cmd.Parameters.AddWithValue("@taishu", "1");
cmd.Parameters.AddWithValue("@frameHandle", frameHandle ?? "");
cmd.Parameters.AddWithValue("@textHandle", textHandle ?? "");
cmd.ExecuteNonQuery();
}
}
}
private void DeleteTuhao(string projectId, string tuhao)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ 双键(2026-09-21 残留根治):先查该图号的内部ID,显示名+内部ID两种键
// 都清——老数据按显示名登记、主窗口按内部ID登记,单一键必留孤儿
string thId = "";
using (var selId = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM " + TuhaoTableName + " WHERE 图号 = @th LIMIT 1;", conn))
{
selId.Parameters.AddWithValue("@th", tuhao);
using (var rd = selId.ExecuteReader())
{
if (rd.Read() && !rd.IsDBNull(0)) thId = rd.GetString(0);
}
}
var tablesToDrop = new List<string>();
string selIdx = "SELECT 实际表名 FROM 表索引 WHERE 图号 = @k;";
foreach (string key in new[] { tuhao, thId })
{
if (string.IsNullOrWhiteSpace(key)) continue;
using (var sel = new System.Data.SQLite.SQLiteCommand(selIdx, conn))
{
sel.Parameters.AddWithValue("@k", key);
using (var reader = sel.ExecuteReader())
{
while (reader.Read())
{
string realTbl = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(realTbl) && !tablesToDrop.Contains(realTbl))
tablesToDrop.Add(realTbl);
}
}
}
}
foreach (string realTbl in tablesToDrop)
{
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + realTbl + "\";", conn))
dropCmd.ExecuteNonQuery();
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM " + TuhaoTableName + " WHERE 图号 = @th OR 内部ID = @nid;", conn))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.Parameters.AddWithValue("@nid", thId.Length > 0 ? thId : tuhao);
cmd.ExecuteNonQuery();
}
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 图号 = @th OR 图号 = @nid;", conn))
{
cmd.Parameters.AddWithValue("@th", tuhao);
cmd.Parameters.AddWithValue("@nid", thId.Length > 0 ? thId : tuhao);
cmd.ExecuteNonQuery();
}
}
}
private void InsertXianghao(string projectId, string tuhao, string xianghao, string frameHandle, string textHandle,
string drawingName = null)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return;
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);
}
// ★ 确保旧箱号表也有"箱名"/"图纸"/"框Handle"/"文本Handle"/"内部ID"列
using (var connEns = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
connEns.Open();
EnsureColumns(connEns, "\"" + tblName + "\"", new[] { "箱名", "图纸", "框Handle", "文本Handle", "内部ID" });
}
string seq = GetXianghaoCount(projectId, tuhao);
// drawingName=扒图时当前打开的图纸名(去扩展名),写入「图纸」列;右键新建等其它调用不传=空
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 箱号, 内部ID, 箱名, 单位, 数量, 图纸," +
" 单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率, 框Handle, 文本Handle) " +
"VALUES (@seq, @xh, @nid, @xm, @unit, @shuliang, @tz, 0, 0, 0, 0, 0, 0, 0, @frameHandle, @textHandle);";
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);
// ★ 真隔离(2026-09-29):内部ID 用新 GUID,不再等于显示名,
// 杜绝同名箱号实例共用同一元件表索引(根治串数据)
cmd.Parameters.AddWithValue("@nid", System.Guid.NewGuid().ToString("N"));
cmd.Parameters.AddWithValue("@xm", ""); // 箱名默认空,由用户在表格里填写
cmd.Parameters.AddWithValue("@unit", "台");
cmd.Parameters.AddWithValue("@shuliang", "1");
cmd.Parameters.AddWithValue("@tz", drawingName ?? "");
cmd.Parameters.AddWithValue("@frameHandle", frameHandle ?? "");
cmd.Parameters.AddWithValue("@textHandle", textHandle ?? "");
cmd.ExecuteNonQuery();
}
}
}
private void DeleteXianghao(string projectId, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
string yjTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string xhTbl = GetXianghaoTableByIndex(projectId, tuhao);
// ★ 双键(2026-09-21 残留根治):查内部ID,显示名+内部ID两种组合都清,
// 单一键匹配不到另一套登记的元件表/索引行必留孤儿(AI 清单会读到)
string thId = "";
using (var selId = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM " + TuhaoTableName + " WHERE 图号 = @th LIMIT 1;", conn))
{
selId.Parameters.AddWithValue("@th", tuhao);
using (var rd = selId.ExecuteReader())
{
if (rd.Read() && !rd.IsDBNull(0)) thId = rd.GetString(0);
}
}
string xhId = "";
if (!string.IsNullOrEmpty(xhTbl))
{
using (var selId = new System.Data.SQLite.SQLiteCommand(
"SELECT 内部ID FROM \"" + xhTbl + "\" WHERE 箱号 = @xh LIMIT 1;", conn))
{
selId.Parameters.AddWithValue("@xh", xianghao);
using (var rd = selId.ExecuteReader())
{
if (rd.Read() && !rd.IsDBNull(0)) xhId = rd.GetString(0);
}
}
}
// 元件表:两套键的索引行都找出来 DROP
var yjDrop = new List<string>();
string selYj = "SELECT 实际表名 FROM 表索引 WHERE 类型='元件' AND 图号=@t AND 箱号=@x;";
var keys = new[] {
new[] { tuhao, xianghao },
thId.Length > 0 && xhId.Length > 0 ? new[] { thId, xhId } : null
};
foreach (var k in keys)
{
if (k == null) continue;
using (var sel = new System.Data.SQLite.SQLiteCommand(selYj, conn))
{
sel.Parameters.AddWithValue("@t", k[0]);
sel.Parameters.AddWithValue("@x", k[1]);
using (var rd = sel.ExecuteReader())
{
while (rd.Read())
{
string t = rd.IsDBNull(0) ? "" : rd.GetString(0);
if (!string.IsNullOrWhiteSpace(t) && !yjDrop.Contains(t)) yjDrop.Add(t);
}
}
}
}
foreach (string t in yjDrop)
{
using (var dropCmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + t + "\";", conn))
dropCmd.ExecuteNonQuery();
}
if (!string.IsNullOrEmpty(yjTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DROP TABLE IF EXISTS \"" + yjTbl + "\";", conn))
cmd.ExecuteNonQuery();
}
if (!string.IsNullOrEmpty(xhTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + xhTbl + "\" WHERE 箱号 = @xh OR 内部ID = @nid;", conn))
{
cmd.Parameters.AddWithValue("@xh", xianghao);
cmd.Parameters.AddWithValue("@nid", xhId.Length > 0 ? xhId : xianghao);
cmd.ExecuteNonQuery();
}
}
// 元件索引行:两套键都清
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM 表索引 WHERE 类型='元件' AND ((图号=@t AND 箱号=@x) OR (图号=@t2 AND 箱号=@x2));", conn))
{
cmd.Parameters.AddWithValue("@t", tuhao);
cmd.Parameters.AddWithValue("@x", xianghao);
cmd.Parameters.AddWithValue("@t2", thId.Length > 0 ? thId : tuhao);
cmd.Parameters.AddWithValue("@x2", xhId.Length > 0 ? xhId : xianghao);
cmd.ExecuteNonQuery();
}
}
}
private void InsertYuanjian(string projectId, string tuhao, string xianghao, string name)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao) || string.IsNullOrWhiteSpace(xianghao)) return;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
EnsureYuanjianTableExists(projectId, tuhao, xianghao);
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(tblName))
{
System.Windows.Forms.MessageBox.Show("元件表创建失败(表索引未注册),图号=" + tuhao + " 箱号=" + xianghao);
return;
}
string seq;
using (var conn0 = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn0.Open();
using (var c = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM \"" + tblName + "\";", conn0))
{
int n = 0;
object o = c.ExecuteScalar();
if (o != null && o != DBNull.Value) n = Convert.ToInt32(o);
seq = (n + 1).ToString();
}
}
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();
}
}
}
/// <summary>扒元件专用:把文本写到【规格】列(元件名称留空),同时写入框Handle。</summary>
private void InsertYuanjianSpec(string projectId, string tuhao, string xianghao, string spec, string frameHandle)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao) || string.IsNullOrWhiteSpace(xianghao)) return;
EnsureQuoteDbExists(projectId);
string dbPath = GetQuoteDbPath(projectId);
EnsureYuanjianTableExists(projectId, tuhao, xianghao);
string tblName = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(tblName))
{
System.Windows.Forms.MessageBox.Show("元件表创建失败(表索引未注册),图号=" + tuhao + " 箱号=" + xianghao);
return;
}
string seq;
using (var conn0 = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn0.Open();
using (var c = new System.Data.SQLite.SQLiteCommand(
"SELECT COUNT(*) FROM \"" + tblName + "\";", conn0))
{
int n = 0;
object o = c.ExecuteScalar();
if (o != null && o != DBNull.Value) n = Convert.ToInt32(o);
seq = (n + 1).ToString();
}
}
// ★ 写入"规格"列 + 框Handle
string sql = "INSERT INTO \"" + tblName + "\"" +
" (序号, 元件名称, 规格, 单位, 数量, 采购系数, 报出系数," +
" 表价, 采购单价, 采购价, 报出单价, 报出价, 类型, 框Handle) " +
"VALUES (@seq, '', @spec, @unit, @shuliang, @caigou, @baochu, 0, 0, 0, 0, 0, '元件', @frameHandle);";
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("@spec", spec);
cmd.Parameters.AddWithValue("@unit", "只");
cmd.Parameters.AddWithValue("@shuliang", "1");
cmd.Parameters.AddWithValue("@caigou", "1");
cmd.Parameters.AddWithValue("@baochu", "1");
cmd.Parameters.AddWithValue("@frameHandle", frameHandle ?? "");
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 (!File.Exists(dbPath)) return;
string realTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
if (string.IsNullOrEmpty(realTbl)) return;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"DELETE FROM \"" + realTbl + "\" WHERE 序号 = @seq;", conn))
{
cmd.Parameters.AddWithValue("@seq", seq);
cmd.ExecuteNonQuery();
}
}
}
// ============================================================
// 数据库回写:把内存 DataTable 的改动写回报价库
// ============================================================
private void SaveTableToDatabase(DataTable table, string projectId, string tableName,
string whereClause, Dictionary<string, string> whereParams, bool force = false)
{
if (table == null || string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
DataGridView dgv = null;
if (table == _tuhaoTable) dgv = this.advancedDataGridView9;
else if (table == _xianghaoTable) dgv = this.advancedDataGridView4;
else if (table == _yuanjianTable) dgv = this.advancedDataGridView5;
if (dgv != null) dgv.EndEdit();
if (table.Rows.Count == 0) return;
if (!force && table.GetChanges() == null) return;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
// ★ 保留式同步(替代整表 DELETE+INSERT):库里有而内存没有的行才删,
// 内存已有的行按 rowid UPDATE——不再盲目清表,消除对其他窗口
// (主程序实时直写)写入的整表级覆盖。
// 兼容:无 _rowid_ 列(旧快照)时退回原整表重写。
bool 保留式 = table.Columns.Contains("_rowid_");
if (保留式)
{
// 1) 内存表有效行 rowid 集合(排除新增未回填的行)
var 内存rowid = new List<long>();
foreach (DataRow row in table.Rows)
{
if (row.RowState == DataRowState.Deleted) continue;
object rid = row["_rowid_"];
if (rid == null || rid == DBNull.Value) continue;
long id;
try { id = Convert.ToInt64(rid); } catch { continue; }
if (id > 0) 内存rowid.Add(id);
}
// 2) 删库里多出来的行(rowid 不在内存集合中)
string 删列 = "_rowid_ NOT IN (" + (内存rowid.Count > 0 ? string.Join(",", 内存rowid.ConvertAll(x => x.ToString()).ToArray()) : "0") + ")";
string delSql2 = "DELETE FROM " + tableName + (string.IsNullOrWhiteSpace(whereClause) ? "" : " WHERE " + whereClause)
+ (string.IsNullOrWhiteSpace(whereClause) ? " WHERE " : " AND ") + 删列 + ";";
using (var delCmd2 = new System.Data.SQLite.SQLiteCommand(delSql2, conn))
{
if (whereParams != null)
foreach (var kv in whereParams) delCmd2.Parameters.AddWithValue(kv.Key, kv.Value);
delCmd2.ExecuteNonQuery();
}
// 3) 内存已有 rowid 的行 → 按 rowid UPDATE 全字段
var colNamesU = new List<string>(table.Columns.Count);
foreach (DataColumn c in table.Columns)
if (c.ColumnName != "_rowid_") colNamesU.Add(c.ColumnName);
var setList = new List<string>(colNamesU.Count);
foreach (string c in colNamesU) setList.Add("\"" + c + "\"=@v_" + c);
string updSql = "UPDATE " + tableName + " SET " + string.Join(", ", setList.ToArray()) + " WHERE _rowid_=@rid;";
using (var updCmd = new System.Data.SQLite.SQLiteCommand(updSql, conn))
{
foreach (string c in colNamesU) updCmd.Parameters.Add("@v_" + c, System.Data.DbType.String);
updCmd.Parameters.Add("@rid", System.Data.DbType.Int64);
try { updCmd.Prepare(); } catch { }
foreach (DataRow row in table.Rows)
{
if (row.RowState == DataRowState.Deleted) continue;
object rid = row["_rowid_"];
if (rid == null || rid == DBNull.Value) continue;
long id;
try { id = Convert.ToInt64(rid); } catch { continue; }
if (id <= 0) continue;
foreach (string c in colNamesU)
{
object v = row[c];
updCmd.Parameters["@v_" + c].Value = (v == null || v == DBNull.Value) ? "" : v.ToString();
}
updCmd.Parameters["@rid"].Value = id;
updCmd.ExecuteNonQuery();
}
}
// 4) 内存新增的行(无 rowid) → INSERT,并把新 rowid 回填内存(后续编辑可单行 UPDATE)
var colNamesI = new List<string>(table.Columns.Count);
foreach (DataColumn c in table.Columns)
if (c.ColumnName != "_rowid_") colNamesI.Add(c.ColumnName);
var insParam = new List<string>(colNamesI.Count);
foreach (string c in colNamesI) insParam.Add("@" + c);
string insSql = "INSERT INTO " + tableName + " (" + string.Join(", ", colNamesI.ToArray()) +
") VALUES (" + string.Join(", ", insParam.ToArray()) + ");";
using (var insCmd = new System.Data.SQLite.SQLiteCommand(insSql, conn))
{
foreach (string c in colNamesI) insCmd.Parameters.Add("@" + c, System.Data.DbType.String);
try { insCmd.Prepare(); } catch { }
foreach (DataRow row in table.Rows)
{
if (row.RowState == DataRowState.Deleted) continue;
object rid = row["_rowid_"];
if (rid != null && rid != DBNull.Value)
{
long 已有;
try { 已有 = Convert.ToInt64(rid); } catch { 已有 = 0; }
if (已有 > 0) continue; // 已 UPDATE 过
}
foreach (string c in colNamesI)
{
object v = row[c];
insCmd.Parameters["@" + c].Value = (v == null || v == DBNull.Value) ? "" : v.ToString();
}
insCmd.ExecuteNonQuery();
// 回填新 rowid
using (var idCmd = new System.Data.SQLite.SQLiteCommand("SELECT last_insert_rowid();", conn))
{
object o = idCmd.ExecuteScalar();
if (o != null && o != DBNull.Value)
row["_rowid_"] = Convert.ToInt64(o);
}
}
}
tx.Commit();
}
else
{
// —— 旧路径:整表重写(无 _rowid_ 列的兜底,行为与历史一致) ——
string delSql = "DELETE FROM " + tableName;
if (!string.IsNullOrWhiteSpace(whereClause)) delSql += " WHERE " + whereClause;
delSql += ";";
using (var delCmd = new System.Data.SQLite.SQLiteCommand(delSql, conn))
{
if (whereParams != null)
{
foreach (var kv in whereParams) delCmd.Parameters.AddWithValue(kv.Key, kv.Value);
}
delCmd.ExecuteNonQuery();
}
if (table.Rows.Count > 0)
{
var colNames = new List<string>(table.Columns.Count);
foreach (DataColumn c in table.Columns) colNames.Add(c.ColumnName);
var paramList = new List<string>(colNames.Count);
foreach (string c in colNames) paramList.Add("@" + c);
string insertSql = "INSERT INTO " + tableName + " (" +
string.Join(", ", colNames.ToArray()) + ") VALUES (" +
string.Join(", ", paramList.ToArray()) + ");";
using (var insCmd = new System.Data.SQLite.SQLiteCommand(insertSql, conn))
{
foreach (string col in colNames)
insCmd.Parameters.Add("@" + col, System.Data.DbType.String);
try { insCmd.Prepare(); } catch { }
foreach (DataRow row in table.Rows)
{
if (row.RowState == DataRowState.Deleted) continue;
foreach (string col in colNames)
{
object v = row[col];
insCmd.Parameters["@" + col].Value =
(v == null || v == DBNull.Value) ? "" : v.ToString();
}
insCmd.ExecuteNonQuery();
}
}
}
tx.Commit();
}
}
table.AcceptChanges();
}
}
private void SaveTuhaoTableIfDirty()
{
SaveTableToDatabase(_tuhaoTable, _currentProjectId, TuhaoTableName, null, null);
}
private void SaveXianghaoTableIfDirty()
{
if (_xianghaoTable == null || string.IsNullOrWhiteSpace(_xianghaoTable.TableName)) return;
SaveTableToDatabase(_xianghaoTable, _currentProjectId,
"\"" + _xianghaoTable.TableName + "\"", null, null);
}
private void SaveYuanjianTableIfDirty()
{
if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_yuanjianTable.TableName)) return;
SaveTableToDatabase(_yuanjianTable, _currentProjectId,
"\"" + _yuanjianTable.TableName + "\"", null, null);
}
private void SaveAllQuoteTablesIfDirty(bool force = true)
{
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
try
{
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (this.advancedDataGridView4 != null) this.advancedDataGridView4.EndEdit();
if (this.advancedDataGridView9 != null) this.advancedDataGridView9.EndEdit();
if (_yuanjianTable != null && !string.IsNullOrWhiteSpace(_yuanjianTable.TableName))
SaveTableToDatabase(_yuanjianTable, _currentProjectId,
"\"" + _yuanjianTable.TableName + "\"", null, null, force);
if (_xianghaoTable != null && !string.IsNullOrWhiteSpace(_xianghaoTable.TableName))
SaveTableToDatabase(_xianghaoTable, _currentProjectId,
"\"" + _xianghaoTable.TableName + "\"", null, null, force);
if (_tuhaoTable != null)
SaveTableToDatabase(_tuhaoTable, _currentProjectId, TuhaoTableName, null, null, force);
}
catch { }
}
private void SaveAllToDatabaseSilent()
{
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (this.advancedDataGridView4 != null) this.advancedDataGridView4.EndEdit();
if (this.advancedDataGridView9 != null) this.advancedDataGridView9.EndEdit();
try
{
if (_tuhaoTable != null)
SaveTableToDatabase(_tuhaoTable, _currentProjectId, TuhaoTableName, null, null, true);
foreach (var kv in _xianghaoCache)
if (kv.Value != null && !string.IsNullOrWhiteSpace(kv.Value.TableName))
SaveTableToDatabase(kv.Value, _currentProjectId,
"\"" + kv.Value.TableName + "\"", null, null, true);
foreach (var kv in _yuanjianCache)
if (kv.Value != null && !string.IsNullOrWhiteSpace(kv.Value.TableName))
SaveTableToDatabase(kv.Value, _currentProjectId,
"\"" + kv.Value.TableName + "\"", null, null, true);
RecalcAllFromDatabase(_currentProjectId);
ReloadProjectsFromDatabase();
}
catch { }
}
// ============================================================
// 联动刷新:项目 → 图号 → 箱号 → 元件
// ============================================================
private void ReloadProjectsFromDatabase()
{
string dbPath = GetDbFullPath(ProjectsDbPath);
if (!File.Exists(dbPath)) return;
var indexTable = new DataTable();
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "SELECT 项目编号, 箱柜台数, 成本总价, 报出总价, 利润, 利润率 FROM " +
ProjectsTableName + " ORDER BY rowid;";
using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(sql, conn))
adapter.Fill(indexTable);
}
var table = new DataTable();
table.Columns.Add("项目编号", typeof(string));
table.Columns.Add("项目名称", typeof(string));
table.Columns.Add("客户名称", typeof(string));
table.Columns.Add("客户地址", typeof(string));
table.Columns.Add("联系人", typeof(string));
table.Columns.Add("联系电话", typeof(string));
table.Columns.Add("项目日期", typeof(string));
table.Columns.Add("箱柜台数", typeof(string));
table.Columns.Add("成本总价", typeof(string));
table.Columns.Add("报出总价", typeof(string));
table.Columns.Add("利润", typeof(string));
table.Columns.Add("利润率", typeof(string));
table.Columns.Add("备注", typeof(string));
foreach (DataRow idxRow in indexTable.Rows)
{
string projectId = idxRow["项目编号"] == null ? "" : idxRow["项目编号"].ToString().Trim();
if (string.IsNullOrEmpty(projectId)) continue;
var info = GetProjectInfoFromQuoteDb(projectId);
var row = table.NewRow();
row["项目编号"] = projectId;
row["项目名称"] = GetInfoOrDefault(info, "项目名称");
row["客户名称"] = GetInfoOrDefault(info, "客户名称");
row["客户地址"] = GetInfoOrDefault(info, "客户地址");
row["联系人"] = GetInfoOrDefault(info, "联系人");
row["联系电话"] = GetInfoOrDefault(info, "联系电话");
row["项目日期"] = GetInfoOrDefault(info, "项目日期");
row["备注"] = GetInfoOrDefault(info, "备注");
row["箱柜台数"] = idxRow["箱柜台数"] == null ? "" : idxRow["箱柜台数"].ToString();
row["成本总价"] = idxRow["成本总价"] == null ? "" : idxRow["成本总价"].ToString();
row["报出总价"] = idxRow["报出总价"] == null ? "" : idxRow["报出总价"].ToString();
row["利润"] = idxRow["利润"] == null ? "" : idxRow["利润"].ToString();
row["利润率"] = idxRow["利润率"] == null ? "" : idxRow["利润率"].ToString();
table.Rows.Add(row);
}
this.advancedDataGridView6.AutoGenerateColumns = false;
table = ConvertFormulaColumnsToDouble(table);
this.advancedDataGridView6.DataSource = table;
}
private static string GetInfoOrDefault(Dictionary<string, string> info, string key)
{
if (info == null) return "";
string v;
return info.TryGetValue(key, out v) ? (v ?? "") : "";
}
private void ReloadTuhaoByProject(string projectId)
{
bool outerGuard = _isReloading;
_isReloading = true;
try
{
if (string.IsNullOrWhiteSpace(projectId) || !File.Exists(GetQuoteDbPath(projectId)))
{
this.advancedDataGridView9.DataSource = null;
_tuhaoTable = null;
_currentTuhao = "";
ReloadXianghaoByTuhao("", "");
return;
}
string dbPath = GetQuoteDbPath(projectId);
string sql = "SELECT rowid AS _rowid_, * FROM " + TuhaoTableName + " ORDER BY CAST(序号 AS INTEGER);";
var table = new DataTable();
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(cmd))
adapter.Fill(table);
}
table = ConvertFormulaColumnsToDouble(table);
this.advancedDataGridView9.AutoGenerateColumns = false;
this.advancedDataGridView9.DataSource = table;
_tuhaoTable = table;
ApplyPriceFormat(this.advancedDataGridView9, _单台小数位文本);
_currentTuhao = "";
if (this.advancedDataGridView9.Rows.Count > 0)
{
SetCurrentCell(this.advancedDataGridView9, 0);
_currentTuhao = GetCellValueAsString(this.advancedDataGridView9.Rows[0], "图号");
}
ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao);
}
finally { _isReloading = outerGuard; }
}
private void ReloadXianghaoByTuhao(string projectId, string tuhao)
{
bool outerGuard = _isReloading;
_isReloading = true;
try
{
if (this.advancedDataGridView4 != null) this.advancedDataGridView4.EndEdit();
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao) ||
!File.Exists(GetQuoteDbPath(projectId)))
{
this.advancedDataGridView4.DataSource = null;
_xianghaoTable = null;
_currentXianghao = "";
ReloadYuanjianByXianghao("", "", "");
return;
}
string dbPath = GetQuoteDbPath(projectId);
string realTbl = GetXianghaoTableByIndex(projectId, tuhao);
if (string.IsNullOrEmpty(realTbl))
{
realTbl = GetNextTableId(projectId, "xh");
CreateXianghaoTable(projectId, realTbl);
AddTableIndex(projectId, "箱号", tuhao, "", realTbl);
}
DataTable table;
// ★ 禁用缓存命中:每次都从数据库重新读取,确保删除/变更能及时反映到UI
if (_xianghaoCache.ContainsKey(tuhao)) _xianghaoCache.Remove(tuhao);
{
string sql = "SELECT rowid AS _rowid_, * FROM \"" + realTbl + "\" ORDER BY CAST(序号 AS INTEGER);";
table = new DataTable();
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ 老库结构兼容:箱号表可能还没有"箱名"列,读取前幂等补列(表格绑定依赖该列)
EnsureColumns(conn, "\"" + realTbl + "\"", new[] { "箱名" });
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(cmd))
adapter.Fill(table);
}
table = ConvertFormulaColumnsToDouble(table);
table.TableName = realTbl;
_xianghaoCache[tuhao] = table;
}
this.advancedDataGridView4.AutoGenerateColumns = false;
this.advancedDataGridView4.DataSource = table;
_xianghaoTable = table;
ApplyPriceFormat(this.advancedDataGridView4, _单台小数位文本);
_currentXianghao = "";
if (this.advancedDataGridView4.Rows.Count > 0)
{
SetCurrentCell(this.advancedDataGridView4, 0);
_currentXianghao = GetCellValueAsString(this.advancedDataGridView4.Rows[0], "箱号");
}
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
}
finally { _isReloading = outerGuard; }
}
private void ReloadYuanjianByXianghao(string projectId, string tuhao, string xianghao)
{
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao) ||
string.IsNullOrWhiteSpace(xianghao) || !File.Exists(GetQuoteDbPath(projectId)))
{
this.advancedDataGridView5.DataSource = null;
_yuanjianTable = null;
return;
}
string realTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
string dbPath = GetQuoteDbPath(projectId);
string cacheKey = tuhao + "\n" + xianghao;
DataTable table;
// ★ 禁用缓存命中:每次都从数据库重新读取,确保删除/变更能及时反映到UI
if (_yuanjianCache.ContainsKey(cacheKey)) _yuanjianCache.Remove(cacheKey);
{
table = new DataTable();
if (!string.IsNullOrEmpty(realTbl))
{
string sql = "SELECT rowid AS _rowid_, * FROM \"" + realTbl + "\" ORDER BY CAST(序号 AS INTEGER);";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(cmd))
adapter.Fill(table);
}
table = ConvertFormulaColumnsToDouble(table);
}
table.TableName = realTbl;
_yuanjianCache[cacheKey] = table;
}
this.advancedDataGridView5.AutoGenerateColumns = false;
this.advancedDataGridView5.DataSource = table;
_yuanjianTable = table;
ApplyPriceFormat(this.advancedDataGridView5, _单台小数位文本);
}
// ============================================================
// 公式重算
// ============================================================
private void RecalcYuanjianRow(DataGridViewRow row)
{
if (row == null) return;
decimal biaoJia = GetCellDecimal(row, "表价");
decimal caiGouXiShu = GetCellDecimal(row, "采购系数");
decimal baoChuXiShu = GetCellDecimal(row, "报出系数");
decimal shuLiang = GetCellDecimal(row, "数量");
// ★ A2(2026-09-30):公式统一调 报价公式(与主窗口单一来源)
decimal caiGouDanJia, caiGouJia, baoChuDanJia, baoChuJia;
成套报价软件.报价公式.元件(biaoJia, caiGouXiShu, baoChuXiShu, shuLiang,
out caiGouDanJia, out caiGouJia, out baoChuDanJia, out baoChuJia);
SetCell(row, "采购单价", caiGouDanJia);
SetCell(row, "采购价", caiGouJia);
SetCell(row, "报出单价", baoChuDanJia);
SetCell(row, "报出价", baoChuJia);
}
private void RecalcXianghaoRowFromYuanjian(DataGridViewRow xianghaoRow)
{
if (xianghaoRow == null) return;
decimal sumCaiGouJia = 0m;
decimal sumBaoChuJia = 0m;
DataGridView dgv5 = this.advancedDataGridView5;
if (dgv5 != null && dgv5.Rows != null)
{
foreach (DataGridViewRow yj in dgv5.Rows)
{
if (yj == null || yj.IsNewRow) continue;
sumCaiGouJia += GetCellDecimal(yj, "采购价");
sumBaoChuJia += GetCellDecimal(yj, "报出价");
}
}
decimal shuLiang = GetCellDecimal(xianghaoRow, "数量");
// ★ A2(2026-09-30):公式统一调 报价公式(利润率小数/毛利率百分数值)
decimal danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia, liRun, liRunLv, maoLiLv;
成套报价软件.报价公式.箱号(sumCaiGouJia, sumBaoChuJia, shuLiang,
out danTaiChengBen, out chengBenJia, out danTaiBaoChu, out baoChuJia,
out liRun, out liRunLv, out maoLiLv);
SetCell(xianghaoRow, "单台成本", danTaiChengBen);
SetCell(xianghaoRow, "成本价", chengBenJia);
SetCell(xianghaoRow, "单台报出", danTaiBaoChu);
SetCell(xianghaoRow, "报出价", baoChuJia);
SetCell(xianghaoRow, "利润", liRun);
SetCell(xianghaoRow, "利润率", liRunLv);
SetCell(xianghaoRow, "毛利率", maoLiLv);
}
private void RecalcTuhaoRowFromXianghao(DataGridViewRow tuhaoRow)
{
if (tuhaoRow == null) return;
decimal sumShuLiang = 0m;
decimal sumBaoChuJia = 0m;
decimal sumChengBenJia = 0m;
DataGridView dgv4 = this.advancedDataGridView4;
if (dgv4 != null && dgv4.Rows != null)
{
foreach (DataGridViewRow xh in dgv4.Rows)
{
if (xh == null || xh.IsNewRow) continue;
sumShuLiang += GetCellDecimal(xh, "数量");
sumBaoChuJia += GetCellDecimal(xh, "报出价");
sumChengBenJia += GetCellDecimal(xh, "成本价");
}
}
decimal taiShu = GetCellDecimal(tuhaoRow, "台数");
// ★ A2(2026-09-30):公式统一调 报价公式(总价=单价×台数)
decimal xiangGuiShuLiang, danJia, zongJia, chengBenZongJia, maoLi, maoLiLv;
成套报价软件.报价公式.图号(sumShuLiang, sumBaoChuJia, sumChengBenJia, taiShu,
out xiangGuiShuLiang, out danJia, out zongJia, out chengBenZongJia,
out maoLi, out maoLiLv);
SetCell(tuhaoRow, "箱柜数量", xiangGuiShuLiang);
SetCell(tuhaoRow, "单价", danJia);
SetCell(tuhaoRow, "总价", zongJia);
SetCell(tuhaoRow, "成本总价", chengBenZongJia);
SetCell(tuhaoRow, "毛利", maoLi);
SetCell(tuhaoRow, "毛利率", maoLiLv);
RecalcProjectRowFromTuhao();
}
private void RecalcProjectRowFromTuhao()
{
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
decimal sumXiangGuiShuLiang = 0m;
decimal sumChengBenZongJia = 0m;
decimal sumZongJia = 0m;
DataGridView dgv9 = this.advancedDataGridView9;
if (dgv9 != null && dgv9.Rows != null)
{
foreach (DataGridViewRow th in dgv9.Rows)
{
if (th == null || th.IsNewRow) continue;
sumXiangGuiShuLiang += GetCellDecimal(th, "箱柜数量");
sumChengBenZongJia += GetCellDecimal(th, "成本总价");
sumZongJia += GetCellDecimal(th, "总价");
}
}
decimal xiangGuiTaiShu = sumXiangGuiShuLiang;
decimal baoChuZongJia = sumZongJia;
decimal liRun = baoChuZongJia - sumChengBenZongJia;
// ★ 口径(2026-09-29):项目利润率存小数(保留2位,0.05=5%)
decimal liRunLv = sumChengBenZongJia == 0m ? 0m : Math.Round(liRun / sumChengBenZongJia, 2);
DataGridViewRow projRow = FindProjectRowById(_currentProjectId);
if (projRow != null)
{
SetCell(projRow, "箱柜台数", xiangGuiTaiShu);
SetCell(projRow, "成本总价", sumChengBenZongJia);
SetCell(projRow, "报出总价", baoChuZongJia);
SetCell(projRow, "利润", liRun);
SetCell(projRow, "利润率", liRunLv);
}
}
private void RecalcAllAfterReload()
{
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
_isRecalculating = true;
_isReloading = true;
try
{
DataGridView dgv5 = this.advancedDataGridView5;
if (dgv5 != null && dgv5.Rows != null)
{
foreach (DataGridViewRow yj in dgv5.Rows)
{
if (yj == null || yj.IsNewRow) continue;
RecalcYuanjianRow(yj);
}
}
var xhRow = GetFirstSelectedRow(this.advancedDataGridView4);
if (xhRow != null) RecalcXianghaoRowFromYuanjian(xhRow);
var thRow = GetFirstSelectedRow(this.advancedDataGridView9);
if (thRow != null) RecalcTuhaoRowFromXianghao(thRow);
SaveYuanjianTableIfDirty();
SaveXianghaoTableIfDirty();
SaveTuhaoTableIfDirty();
if (_yuanjianTable != null) _yuanjianTable.AcceptChanges();
if (_xianghaoTable != null) _xianghaoTable.AcceptChanges();
if (_tuhaoTable != null) _tuhaoTable.AcceptChanges();
}
finally
{
_isRecalculating = false;
_lastRecalcTime = DateTime.Now;
_isReloading = false;
}
}
// ---------- 全量数据库重算 ----------
private class TuhaoCalcRow { public long RowId; public decimal TaiShu; public string Tuhao; }
private class XianghaoCalcRow { public long RowId; public decimal ShuLiang; public string Xianghao; }
private void RecalcAllFromDatabase(string projectId)
{
if (string.IsNullOrWhiteSpace(projectId)) projectId = _currentProjectId;
if (string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) return;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
var tuhaoRows = new List<TuhaoCalcRow>();
string selTuhao = "SELECT rowid, 台数, 图号 FROM " + TuhaoTableName + " ORDER BY rowid;";
using (var sel = new System.Data.SQLite.SQLiteCommand(selTuhao, conn))
using (var rdr = sel.ExecuteReader())
{
while (rdr.Read())
{
var t = new TuhaoCalcRow();
t.RowId = rdr.GetInt64(0);
t.TaiShu = ParseDecimal(rdr.IsDBNull(1) ? "" : rdr.GetValue(1));
t.Tuhao = rdr.IsDBNull(2) ? "" : rdr.GetValue(2).ToString().Trim();
tuhaoRows.Add(t);
}
}
decimal sumProjXiangGui = 0m;
decimal sumProjChengBen = 0m;
decimal sumProjZongJia = 0m;
foreach (var t in tuhaoRows)
{
if (string.IsNullOrWhiteSpace(t.Tuhao)) continue;
try
{
string xhTable = GetXianghaoTableByIndex(t.Tuhao, conn);
if (string.IsNullOrEmpty(xhTable))
{
UpdateTuhaoCalc(conn, t.RowId, 0m, 0m, 0m, 0m, 0m, 0m);
continue;
}
string xhQuoted = "\"" + xhTable + "\"";
decimal sumThXiangGuiShuLiang = 0m;
decimal sumThBaoChuJia = 0m;
decimal sumThChengBenJia = 0m;
var xhRows = new List<XianghaoCalcRow>();
string selXh = "SELECT rowid, 数量, 箱号 FROM " + xhQuoted + " ORDER BY rowid;";
using (var sel = new System.Data.SQLite.SQLiteCommand(selXh, conn))
using (var rdr = sel.ExecuteReader())
{
while (rdr.Read())
{
var x = new XianghaoCalcRow();
x.RowId = rdr.GetInt64(0);
x.ShuLiang = ParseDecimal(rdr.IsDBNull(1) ? "" : rdr.GetValue(1));
x.Xianghao = rdr.IsDBNull(2) ? "" : rdr.GetValue(2).ToString();
xhRows.Add(x);
}
}
foreach (var x in xhRows)
{
try
{
sumThXiangGuiShuLiang += x.ShuLiang;
string yjTable = GetYuanjianTableByIndex(t.Tuhao, x.Xianghao, conn);
decimal sumCaiGou = 0m;
decimal sumBaoChu = 0m;
if (!string.IsNullOrEmpty(yjTable))
{
string yjQuoted = "\"" + yjTable + "\"";
string selYj = "SELECT rowid, 表价, 采购系数, 报出系数, 数量 FROM " +
yjQuoted + " ORDER BY rowid;";
using (var sel = new System.Data.SQLite.SQLiteCommand(selYj, conn))
using (var sr = sel.ExecuteReader())
{
while (sr.Read())
{
long yjRid = sr.GetInt64(0);
decimal yjBiaoJia = ParseDecimal(sr.IsDBNull(1) ? "" : sr.GetValue(1));
decimal yjCgXs = ParseDecimal(sr.IsDBNull(2) ? "" : sr.GetValue(2));
decimal yjBcXs = ParseDecimal(sr.IsDBNull(3) ? "" : sr.GetValue(3));
decimal yjSl = ParseDecimal(sr.IsDBNull(4) ? "" : sr.GetValue(4));
// ★ A2(2026-09-30):公式统一调 报价公式
decimal yjCaiGouDanJia, yjCaiGouJia, yjBaoChuDanJia, yjBaoChuJia;
成套报价软件.报价公式.元件(yjBiaoJia, yjCgXs, yjBcXs, yjSl,
out yjCaiGouDanJia, out yjCaiGouJia, out yjBaoChuDanJia, out yjBaoChuJia);
sumCaiGou += yjCaiGouJia;
sumBaoChu += yjBaoChuJia;
UpdateYuanjianCalc(conn, yjQuoted, yjRid,
yjCaiGouDanJia, yjCaiGouJia, yjBaoChuDanJia, yjBaoChuJia);
}
}
}
// ★ A2(2026-09-30):公式统一调 报价公式(利润率小数/毛利率百分数值)
decimal danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia, xhLiRun, xhLiRunLv, xhMaoLiLv;
成套报价软件.报价公式.箱号(sumCaiGou, sumBaoChu, x.ShuLiang,
out danTaiChengBen, out chengBenJia, out danTaiBaoChu, out baoChuJia,
out xhLiRun, out xhLiRunLv, out xhMaoLiLv);
sumThBaoChuJia += baoChuJia;
sumThChengBenJia += chengBenJia;
UpdateXianghaoCalc(conn, xhQuoted, x.RowId,
danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia,
xhLiRun, xhLiRunLv, xhMaoLiLv);
}
catch { }
}
// 图号行聚合 ★ A2(2026-09-30):公式统一调 报价公式(总价=单价×台数)
decimal xiangGuiShuLiang, danJia, zongJia, chengBenZongJia, maoLi, maoLiLv;
成套报价软件.报价公式.图号(sumThXiangGuiShuLiang, sumThBaoChuJia, sumThChengBenJia, t.TaiShu,
out xiangGuiShuLiang, out danJia, out zongJia, out chengBenZongJia,
out maoLi, out maoLiLv);
sumProjXiangGui += xiangGuiShuLiang;
sumProjChengBen += chengBenZongJia;
sumProjZongJia += zongJia;
UpdateTuhaoCalc(conn, t.RowId, xiangGuiShuLiang, danJia,
zongJia, chengBenZongJia, maoLi, maoLiLv);
}
catch { }
}
decimal projLiRun = sumProjZongJia - sumProjChengBen;
// ★ 口径(2026-09-29):项目利润率存小数(保留2位,0.05=5%)
decimal projLiRunLv = sumProjChengBen == 0m ? 0m : Math.Round(projLiRun / sumProjChengBen, 2);
UpdateProjectQuoteTotalsById(projectId, sumProjXiangGui,
sumProjChengBen, sumProjZongJia, projLiRun, projLiRunLv);
}
}
catch { }
}
private static void UpdateYuanjianCalc(System.Data.SQLite.SQLiteConnection conn,
string yjQuoted, long rowId,
decimal caiGouDanJia, decimal caiGouJia, decimal baoChuDanJia, decimal baoChuJia)
{
string sql = "UPDATE " + yjQuoted + " SET " +
"采购单价=@a, 采购价=@b, 报出单价=@c, 报出价=@d WHERE rowid=@rid;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@a", caiGouDanJia == 0m ? (object)DBNull.Value : (object)caiGouDanJia);
cmd.Parameters.AddWithValue("@b", caiGouJia == 0m ? (object)DBNull.Value : (object)caiGouJia);
cmd.Parameters.AddWithValue("@c", baoChuDanJia == 0m ? (object)DBNull.Value : (object)baoChuDanJia);
cmd.Parameters.AddWithValue("@d", baoChuJia == 0m ? (object)DBNull.Value : (object)baoChuJia);
cmd.Parameters.AddWithValue("@rid", rowId);
cmd.ExecuteNonQuery();
}
}
private static void UpdateXianghaoCalc(System.Data.SQLite.SQLiteConnection conn,
string xhQuoted, long rowId,
decimal danTaiChengBen, decimal chengBenJia, decimal danTaiBaoChu, decimal baoChuJia,
decimal liRun, decimal liRunLv, decimal maoLiLv)
{
string sql = "UPDATE " + xhQuoted + " SET " +
"单台成本=@a, 成本价=@b, 单台报出=@c, 报出价=@d, " +
"利润=@e, 利润率=@f, 毛利率=@g WHERE rowid=@rid;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@a", danTaiChengBen == 0m ? (object)DBNull.Value : (object)danTaiChengBen);
cmd.Parameters.AddWithValue("@b", chengBenJia == 0m ? (object)DBNull.Value : (object)chengBenJia);
cmd.Parameters.AddWithValue("@c", danTaiBaoChu == 0m ? (object)DBNull.Value : (object)danTaiBaoChu);
cmd.Parameters.AddWithValue("@d", baoChuJia == 0m ? (object)DBNull.Value : (object)baoChuJia);
cmd.Parameters.AddWithValue("@e", liRun == 0m ? (object)DBNull.Value : (object)liRun);
cmd.Parameters.AddWithValue("@f", liRunLv.ToString());
cmd.Parameters.AddWithValue("@g", maoLiLv.ToString());
cmd.Parameters.AddWithValue("@rid", rowId);
cmd.ExecuteNonQuery();
}
}
private static void UpdateTuhaoCalc(System.Data.SQLite.SQLiteConnection conn,
long rowId, decimal xiangGuiShuLiang, decimal danJia,
decimal zongJia, decimal chengBenZongJia, decimal maoLi, decimal maoLiLv)
{
string sql = "UPDATE " + TuhaoTableName + " SET " +
"箱柜数量=@a, 单价=@b, 总价=@c, 成本总价=@d, 毛利=@e, 毛利率=@f " +
"WHERE rowid=@rid;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@a", xiangGuiShuLiang == 0m ? (object)DBNull.Value : (object)xiangGuiShuLiang);
cmd.Parameters.AddWithValue("@b", danJia == 0m ? (object)DBNull.Value : (object)danJia);
cmd.Parameters.AddWithValue("@c", zongJia == 0m ? (object)DBNull.Value : (object)zongJia);
cmd.Parameters.AddWithValue("@d", chengBenZongJia == 0m ? (object)DBNull.Value : (object)chengBenZongJia);
cmd.Parameters.AddWithValue("@e", maoLi == 0m ? (object)DBNull.Value : (object)maoLi);
cmd.Parameters.AddWithValue("@f", maoLiLv.ToString());
cmd.Parameters.AddWithValue("@rid", rowId);
cmd.ExecuteNonQuery();
}
}
private void UpdateProjectQuoteTotalsById(string projectId,
decimal xiangGuiTaiShu, decimal chengBenZongJia,
decimal baoChuZongJia, decimal liRun, decimal liRunLv)
{
if (string.IsNullOrWhiteSpace(projectId)) return;
string dbPath = GetDbFullPath(ProjectsDbPath);
if (!File.Exists(dbPath)) return;
try
{
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
string sql = "UPDATE " + ProjectsTableName + " SET " +
"箱柜台数=@xgts, 成本总价=@cbzj, 报出总价=@bczj, " +
"利润=@lr, 利润率=@lrl WHERE 项目编号=@id;";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@xgts", xiangGuiTaiShu == 0m ? (object)DBNull.Value : (object)xiangGuiTaiShu);
cmd.Parameters.AddWithValue("@cbzj", chengBenZongJia == 0m ? (object)DBNull.Value : (object)chengBenZongJia);
cmd.Parameters.AddWithValue("@bczj", baoChuZongJia == 0m ? (object)DBNull.Value : (object)baoChuZongJia);
cmd.Parameters.AddWithValue("@lr", liRun == 0m ? (object)DBNull.Value : (object)liRun);
cmd.Parameters.AddWithValue("@lrl", liRunLv.ToString());
cmd.Parameters.AddWithValue("@id", projectId);
cmd.ExecuteNonQuery();
}
}
}
catch { }
}
// ============================================================
// 事件:CellValueChanged / DirtyStateChanged / CellEndEdit
// ============================================================
private void Grid_DirtyStateChanged(object sender, EventArgs e)
{
if (_isRecalculating || _isReloading) return;
var msSinceRecalc = (DateTime.Now - _lastRecalcTime).TotalMilliseconds;
if (msSinceRecalc < 2000) return;
var dgv = sender as DataGridView;
if (dgv != null && dgv.IsCurrentCellDirty)
dgv.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
private void Yuanjian_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (_isRecalculating || _isReloading) return;
if (e.RowIndex < 0) return;
var dgv = this.advancedDataGridView5;
if (dgv == null || e.RowIndex >= dgv.Rows.Count) return;
var row = dgv.Rows[e.RowIndex];
if (row == null || row.IsNewRow) return;
_isRecalculating = true;
try
{
RecalcYuanjianRow(row);
var xhRow = GetFirstSelectedRow(this.advancedDataGridView4);
if (xhRow != null)
{
RecalcXianghaoRowFromYuanjian(xhRow);
var thRow = GetFirstSelectedRow(this.advancedDataGridView9);
if (thRow != null) RecalcTuhaoRowFromXianghao(thRow);
}
}
finally { _isRecalculating = false; _lastRecalcTime = DateTime.Now; }
}
private void Xianghao_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (_isRecalculating || _isReloading) return;
if (e.RowIndex < 0) return;
var dgv = this.advancedDataGridView4;
if (dgv == null || e.RowIndex >= dgv.Rows.Count) return;
var row = dgv.Rows[e.RowIndex];
if (row == null || row.IsNewRow) return;
// ★ 箱号重名自动加后缀(同表其他行已有该箱号):改值+刷新,不弹窗
if (_xianghaoTable != null && e.ColumnIndex >= 0)
{
var col = dgv.Columns[e.ColumnIndex];
string cn = col == null ? "" : (!string.IsNullOrEmpty(col.DataPropertyName) ? col.DataPropertyName : col.HeaderText);
if (cn == "箱号")
{
var 新 = 重名自动后缀(_xianghaoTable, row, "箱号", row.Cells[e.ColumnIndex].Value);
if (!Equals(新, row.Cells[e.ColumnIndex].Value))
{
_isRecalculating = true;
try { row.Cells[e.ColumnIndex].Value = 新; } finally { _isRecalculating = false; }
}
}
}
_isRecalculating = true;
try
{
var thRow = GetFirstSelectedRow(this.advancedDataGridView9);
if (thRow != null) RecalcTuhaoRowFromXianghao(thRow);
}
finally { _isRecalculating = false; _lastRecalcTime = DateTime.Now; }
}
private void Tuhao_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (_isRecalculating || _isReloading) return;
if (e.RowIndex < 0) return;
var dgv = this.advancedDataGridView9;
if (dgv == null || e.RowIndex >= dgv.Rows.Count) return;
var row = dgv.Rows[e.RowIndex];
if (row == null || row.IsNewRow) return;
DataGridViewColumn col = e.ColumnIndex >= 0 ? dgv.Columns[e.ColumnIndex] : null;
string colName = col == null ? "" :
(!string.IsNullOrEmpty(col.DataPropertyName) ? col.DataPropertyName : col.HeaderText);
// ★ 图号重名自动加后缀(同表其他行已有该图号):改值+刷新,不弹窗
if (colName == "图号" && _tuhaoTable != null)
{
var 新 = 重名自动后缀(_tuhaoTable, row, "图号", row.Cells[e.ColumnIndex].Value);
if (!Equals(新, row.Cells[e.ColumnIndex].Value))
{
_isRecalculating = true;
try { row.Cells[e.ColumnIndex].Value = 新; } finally { _isRecalculating = false; }
}
}
if (colName != "台数") return;
_isRecalculating = true;
try { RecalcTuhaoRowFromXianghao(row); }
finally { _isRecalculating = false; _lastRecalcTime = DateTime.Now; }
}
/// <summary>图号/箱号重名自动后缀:值与 DataTable 其他行(除 自身行 对应行)重复时返回 "名(2)"/"名(3)"…。</summary>
private static object 重名自动后缀(System.Data.DataTable table, DataGridViewRow 自身行, string colName, object 新值)
{
if (新值 == null || 新值 == DBNull.Value) return 新值;
string v = 新值.ToString().Trim();
if (v.Length == 0) return 新值;
int 自身Index = 自身行.Index;
bool 撞 = false;
foreach (System.Data.DataRow r in table.Rows)
{
if (r.RowState == System.Data.DataRowState.Deleted) continue;
if (table.Rows.IndexOf(r) == 自身Index) continue;
if (string.Equals((r[colName] == null || r[colName] == DBNull.Value) ? "" : r[colName].ToString().Trim(), v, StringComparison.Ordinal))
{ 撞 = true; break; }
}
if (!撞) return 新值;
for (int n = 2; n < 1000; n++)
{
string 候 = v + "(" + n + ")";
bool 再撞 = false;
foreach (System.Data.DataRow r in table.Rows)
{
if (r.RowState == System.Data.DataRowState.Deleted) continue;
if (string.Equals((r[colName] == null || r[colName] == DBNull.Value) ? "" : r[colName].ToString().Trim(), 候, StringComparison.Ordinal))
{ 再撞 = true; break; }
}
if (!再撞) return 候;
}
return 新值;
}
/// <summary>CellEndEdit:把公式列的字符串值转为 double,让 DefaultCellStyle.Format 生效。</summary>
private void Grid_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
var dgv = sender as DataGridView;
if (dgv == null || e.ColumnIndex < 0 || e.ColumnIndex >= dgv.Columns.Count) return;
string dp = dgv.Columns[e.ColumnIndex].DataPropertyName;
string cn = dgv.Columns[e.ColumnIndex].HeaderText;
string colKey = !string.IsNullOrEmpty(dp) ? dp : cn;
// ★ 实时落库:每编辑完一格立即按 rowid 单行 UPDATE(改哪行写哪行,不整表回写)
单格实时落库(dgv, e.RowIndex, colKey);
if (!s_公式列集合.Contains(colKey)) return;
if (e.RowIndex < 0 || e.RowIndex >= dgv.Rows.Count) return;
object v = dgv.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
if (v == null || v is double) return;
string str = v.ToString().Trim();
if (string.IsNullOrEmpty(str)) return;
double d;
if (double.TryParse(str, out d))
{
dgv.Rows[e.RowIndex].Cells[e.ColumnIndex].Value = d;
// 公式列转 double 后同样实时落库
单格实时落库(dgv, e.RowIndex, colKey);
}
}
/// <summary>单格实时落库:按内存表该行的 _rowid_ 对库单行 UPDATE(毫秒级,不整表回写)。
/// 无 _rowid_(新插入未回填)的行跳过——由切层/关闭时的整表保存兜底。
/// 派生列(采购单价/采购价/报出单价/报出价)跳过——重算链会连同主列一起写入。</summary>
private void 单格实时落库(DataGridView dgv, int rowIndex, string colKey)
{
try
{
if (dgv == null || rowIndex < 0 || rowIndex >= dgv.Rows.Count) return;
if (string.IsNullOrEmpty(colKey) || s_公式列集合.Contains(colKey)) return;
if (colKey == "序号" || colKey == "_rowid_") return;
System.Data.DataTable table = dgv.DataSource as System.Data.DataTable;
if (table == null || string.IsNullOrWhiteSpace(table.TableName)) return;
if (!table.Columns.Contains("_rowid_") || !table.Columns.Contains(colKey)) return;
if (rowIndex >= table.Rows.Count) return;
object rid = table.Rows[rowIndex]["_rowid_"];
if (rid == null || rid == DBNull.Value) return;
long rowid;
try { rowid = Convert.ToInt64(rid); } catch { return; }
if (rowid <= 0) return;
object val = table.Rows[rowIndex][colKey];
string dbPath = GetQuoteDbPath(_currentProjectId);
if (!File.Exists(dbPath)) return;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"UPDATE \"" + table.TableName.Replace("\"", "") + "\" SET \"" + colKey.Replace("\"", "") + "\"=@v WHERE rowid=@r;", conn))
{
cmd.Parameters.AddWithValue("@v", (val == null || val == DBNull.Value) ? "" : val.ToString());
cmd.Parameters.AddWithValue("@r", rowid);
cmd.ExecuteNonQuery();
}
}
}
catch (Exception exCell) { Log("单格实时落库失败(忽略,整表保存兜底): " + exCell.Message); }
}
// ============================================================
// SelectionChanged 联动
// ============================================================
private void advancedDataGridView9_SelectionChanged(object sender, EventArgs e)
{
if (_isReloading) return;
if (this.advancedDataGridView9 != null && this.advancedDataGridView9.SelectedCells.Count > 1) return;
DataGridViewRow row = GetFirstSelectedRow(this.advancedDataGridView9);
if (row == null) return;
string tuhao = GetCellValueAsString(row, "图号");
if (tuhao == _currentTuhao) return;
// ★ 切图号前先落库当前层(箱号+元件):切层 Reload 换 DataSource,未保存的内存编辑直接丢
try
{
if (this.advancedDataGridView4 != null) this.advancedDataGridView4.EndEdit();
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (_xianghaoTable != null && !string.IsNullOrWhiteSpace(_xianghaoTable.TableName))
{
SaveTableToDatabase(_xianghaoTable, _currentProjectId,
"\"" + _xianghaoTable.TableName + "\"", null, null, true);
_xianghaoTable.AcceptChanges();
}
if (_yuanjianTable != null && !string.IsNullOrWhiteSpace(_yuanjianTable.TableName))
{
SaveTableToDatabase(_yuanjianTable, _currentProjectId,
"\"" + _yuanjianTable.TableName + "\"", null, null, true);
_yuanjianTable.AcceptChanges();
}
}
catch (Exception exSaveLayer) { Log("切图号前保存失败(忽略): " + exSaveLayer.Message); }
_currentTuhao = tuhao;
ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao);
}
private void advancedDataGridView4_SelectionChanged(object sender, EventArgs e)
{
if (_isReloading) return;
if (this.advancedDataGridView4 != null && this.advancedDataGridView4.SelectedCells.Count > 1) return;
DataGridViewRow row = GetFirstSelectedRow(this.advancedDataGridView4);
if (row == null) return;
string xh = GetCellValueAsString(row, "箱号");
if (xh == _currentXianghao) return;
// ★ 切箱号前先落库当前元件层(切层 Reload 换 DataSource,未保存编辑直接丢)
try
{
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (_yuanjianTable != null && !string.IsNullOrWhiteSpace(_yuanjianTable.TableName))
{
SaveTableToDatabase(_yuanjianTable, _currentProjectId,
"\"" + _yuanjianTable.TableName + "\"", null, null, true);
_yuanjianTable.AcceptChanges();
}
}
catch (Exception exSaveLayer) { Log("切箱号前保存失败(忽略): " + exSaveLayer.Message); }
_currentXianghao = xh;
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
// ★ 切箱号 → 显示该箱号的组虚线框(只画框不跳转)
显示箱号组虚线框(false);
}
private void advancedDataGridView5_SelectionChanged(object sender, EventArgs e) { }
// ============================================================
// ★ 点击表格行 → 定位到对应的扒图框(缩放居中)
// 读取数据行的"框Handle"列,用 Handle 反查 CAD 实体,缩放到实体范围
// ============================================================
private void Grid_CellClick_Zoom(object sender, DataGridViewCellEventArgs e)
{
if (_isReloading) return;
if (e.RowIndex < 0) return;
DataGridView dgv = sender as DataGridView;
if (dgv == null) return;
DataGridViewRow row = dgv.Rows[e.RowIndex];
if (row == null || row.IsNewRow) return;
// ★ 直接从 DataRow 读取"框Handle"(不依赖 DataGridView 列映射)
string handle = "";
var drv = row.DataBoundItem as DataRowView;
if (drv != null && drv.Row != null && drv.Row.Table.Columns.Contains("框Handle"))
{
object v = drv.Row["框Handle"];
if (v != null && v != DBNull.Value) handle = v.ToString().Trim();
}
if (!string.IsNullOrWhiteSpace(handle) && dgv == this.advancedDataGridView9)
{
ZoomToFrameHandle(handle); // 图号行:维持原单框定位
}
// ★ 箱号组虚线框:点箱号表/元件表行 → 跳转缩放到"装下整个组框"
// + 点中的框黄色高亮;点图号表行 → 只清组框
if (dgv == this.advancedDataGridView4 || dgv == this.advancedDataGridView5)
显示箱号组虚线框(true, handle);
else if (dgv == this.advancedDataGridView9)
成套报价软件.CadBridge.ClearGroupFrames();
}
// ============================================================
// ★ 箱号组虚线框:点箱号表/元件表行 → 网页 CAD 画一个橙色虚线矩形,
// 把该箱号的箱号图框 + 它元件表全部元件图框 求并集圈在一起
// ============================================================
/// <summary>显示当前 图号+箱号 的组虚线框(查库收集全部框Handle)。
/// 跳转=true:画框同时缩放到装下整个组框(点箱号/元件行跳转);
/// focusHandle=点中的那个框:网页端对它画黄色高亮框。</summary>
private void 显示箱号组虚线框(bool 跳转, string focusHandle = null)
{
try
{
if (string.IsNullOrWhiteSpace(_currentProjectId) ||
string.IsNullOrWhiteSpace(_currentTuhao) ||
string.IsNullOrWhiteSpace(_currentXianghao)) return;
var handles = new System.Collections.Generic.List<string>();
// 箱号表该行的框Handle(组框包含箱号图框)
string xhTbl = GetXianghaoTableByIndex(_currentProjectId, _currentTuhao);
if (!string.IsNullOrEmpty(xhTbl))
CollectFrameHandles(xhTbl, "箱号", _currentXianghao, handles);
// 元件表全部行的框Handle
string yjTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, _currentXianghao);
if (!string.IsNullOrEmpty(yjTbl))
CollectFrameHandles(yjTbl, null, null, handles);
if (handles.Count > 0)
{
if (跳转)
成套报价软件.CadBridge.ZoomToGroupFrames(handles, focusHandle);
else
成套报价软件.CadBridge.SetGroupFrames(handles);
}
}
catch (Exception exGrp) { Log("组虚线框显示失败(忽略): " + exGrp.Message); }
}
/// <summary>从指定元件/箱号表收集框Handle:matchCol/matchVal 非空时只收匹配行,否则收全部非空行。</summary>
private void CollectFrameHandles(string realTbl, string matchCol, string matchVal,
System.Collections.Generic.List<string> into)
{
try
{
string dbPath = GetQuoteDbPath(_currentProjectId);
if (!File.Exists(dbPath)) return;
string where = matchCol != null ? " WHERE TRIM(" + matchCol + ")=@mv" : "";
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + realTbl.Replace("\"", "") + "\"" + where + ";", conn))
{
if (matchCol != null) cmd.Parameters.AddWithValue("@mv", (matchVal ?? "").Trim());
using (var rd = cmd.ExecuteReader())
{
while (rd.Read())
{
string h = rd["框Handle"] == null || rd["框Handle"] == DBNull.Value
? "" : rd["框Handle"].ToString().Trim();
if (!string.IsNullOrEmpty(h) && !into.Contains(h)) into.Add(h);
}
}
}
}
}
catch (Exception exC) { Log("收集框Handle失败(" + realTbl + ",忽略): " + exC.Message); }
}
/// <summary>
/// 根据框 Handle 字符串定位网页 CAD 视图:缩放居中到框的位置并高亮。
/// Handle 是扒图时绘制的彩色矩形框的句柄(十六进制字符串),
/// 与网页端实体 objectId 一致,通过 CadBridge 转发给网页 CAD 执行。
/// </summary>
private static void ZoomToFrameHandle(string handle)
{
if (string.IsNullOrWhiteSpace(handle)) return;
try
{
成套报价软件.CadBridge.ZoomToFrameHandle(handle);
}
catch { }
}
/// <summary>
/// ★ 网页 CAD 会话内的扒图框由网页端持有,无需像 AutoCAD 版那样
/// 定期 SaveAs 整个 DWG(原为防止 CAD 关闭时框丢失)。保留空实现
/// 以兼容调用点。
/// </summary>
private static DateTime _lastDwgSaveTime = DateTime.MinValue;
// ★ 扒元件后待选中的行(框Handle),finally 延迟执行选中
private List<string> _pendingSelectYuanjianHandles = new List<string>();
private static void SaveCurrentDwg()
{
// 网页版:扒图框保存在网页 CAD 会话中,无需保存 DWG 文件。
}
/// <summary>
/// ★ 删除指定 Handle 对应的 CAD 框实体(单个,转发到批量方法)。
/// </summary>
private void DeleteFrameByHandle(string handle)
{
DeleteFramesByHandles(new string[] { handle });
}
/// <summary>
/// ★ 批量删除多个 Handle 对应的框:打包成一条消息发给网页 CAD,
/// 网页端逐个 erase 后只 regen 一次(保持原版"批量一次刷新"的优化)。
/// </summary>
private void DeleteFramesByHandles(System.Collections.Generic.IEnumerable<string> handles)
{
var list = new List<string>();
if (handles != null)
foreach (string h in handles)
if (!string.IsNullOrWhiteSpace(h)) list.Add(h);
if (list.Count == 0) { Log("[DeleteFramesByHandles] 无有效 handle,跳过"); return; }
try
{
成套报价软件.CadBridge.DeleteFramesByHandles(list);
Log("[DeleteFramesByHandles] 已发送网页删除请求: 请求=" + list.Count);
}
catch (System.Exception ex) { Log("[DeleteFramesByHandles] 异常: " + ex.Message); }
}
/// <summary>
/// ★ 收集指定图号下所有箱号表和元件表的框Handle(级联删框用)。
/// 不包含图号表自身的框(图号行框由调用方从内存表收集)。
/// </summary>
private void CollectFrameHandlesForTuhao(string projectId, string tuhao, List<string> handles)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao)) return;
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) { Log("[CollectFrameHandlesForTuhao] 数据库不存在: " + dbPath); return; }
int beforeCount = handles.Count;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// 箱号表
string xhTbl = GetXianghaoTableByIndex(projectId, tuhao);
Log("[CollectFrameHandlesForTuhao] 图号=" + tuhao + " 箱号表名=" + (xhTbl ?? "(null)"));
if (!string.IsNullOrEmpty(xhTbl) && TableExists(conn, xhTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle, 箱号 FROM \"" + xhTbl + "\";", conn))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
string h = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(h)) handles.Add(h);
string xh = reader.IsDBNull(1) ? "" : reader.GetString(1);
Log("[CollectFrameHandlesForTuhao] 箱号=" + xh + " 框Handle=" + h);
// 查该箱号的元件表
if (!string.IsNullOrWhiteSpace(xh))
{
string yjTbl = GetYuanjianTableByIndex(projectId, tuhao, xh);
if (!string.IsNullOrEmpty(yjTbl) && TableExists(conn, yjTbl))
{
using (var yjCmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + yjTbl + "\";", conn))
using (var yjReader = yjCmd.ExecuteReader())
while (yjReader.Read())
{
string yh = yjReader.IsDBNull(0) ? "" : yjReader.GetString(0);
if (!string.IsNullOrWhiteSpace(yh)) handles.Add(yh);
Log("[CollectFrameHandlesForTuhao] 元件框Handle=" + yh);
}
}
}
}
}
}
}
Log("[CollectFrameHandlesForTuhao] 新增框Handle数: " + (handles.Count - beforeCount));
}
catch (Exception ex) { Log("[CollectFrameHandlesForTuhao] 异常: " + ex.Message); }
}
/// <summary>
/// ★ 收集指定箱号下元件表的框Handle(级联删框用)。
/// 不包含箱号表自身的框(箱号行框由调用方从内存表收集)。
/// </summary>
private void CollectFrameHandlesForXianghao(string projectId, string tuhao, string xianghao, List<string> handles)
{
if (string.IsNullOrWhiteSpace(projectId) || string.IsNullOrWhiteSpace(tuhao) ||
string.IsNullOrWhiteSpace(xianghao)) return;
try
{
string dbPath = GetQuoteDbPath(projectId);
if (!File.Exists(dbPath)) { Log("[CollectFrameHandlesForXianghao] 数据库不存在: " + dbPath); return; }
int beforeCount = handles.Count;
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
{
conn.Open();
// ★ 先收集箱号表自身该箱号的框Handle(之前遗漏,导致删箱号时蒙版不删)
string xhTbl = GetXianghaoTableByIndex(projectId, tuhao);
Log("[CollectFrameHandlesForXianghao] 图号=" + tuhao + " 箱号=" + xianghao + " 箱号表名=" + (xhTbl ?? "(null)"));
if (!string.IsNullOrEmpty(xhTbl) && TableExists(conn, xhTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + xhTbl + "\" WHERE 箱号=@xh;", conn))
{
cmd.Parameters.AddWithValue("@xh", xianghao);
using (var reader = cmd.ExecuteReader())
while (reader.Read())
{
string h = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(h)) handles.Add(h);
Log("[CollectFrameHandlesForXianghao] 箱号自身框Handle=" + h);
}
}
}
// ★ 再收集该箱号下元件表的框Handle
string yjTbl = GetYuanjianTableByIndex(projectId, tuhao, xianghao);
Log("[CollectFrameHandlesForXianghao] 元件表名=" + (yjTbl ?? "(null)"));
if (!string.IsNullOrEmpty(yjTbl) && TableExists(conn, yjTbl))
{
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT 框Handle FROM \"" + yjTbl + "\";", conn))
using (var reader = cmd.ExecuteReader())
while (reader.Read())
{
string h = reader.IsDBNull(0) ? "" : reader.GetString(0);
if (!string.IsNullOrWhiteSpace(h)) handles.Add(h);
Log("[CollectFrameHandlesForXianghao] 元件框Handle=" + h);
}
}
}
Log("[CollectFrameHandlesForXianghao] 新增框Handle数: " + (handles.Count - beforeCount));
}
catch (Exception ex) { Log("[CollectFrameHandlesForXianghao] 异常: " + ex.Message); }
}
/// <summary>检查表是否存在(SQLite)。</summary>
private static bool TableExists(System.Data.SQLite.SQLiteConnection conn, string tableName)
{
if (conn == null || string.IsNullOrWhiteSpace(tableName)) return false;
try
{
string name = tableName.Trim().Trim('"');
using (var cmd = new System.Data.SQLite.SQLiteCommand(
"SELECT name FROM sqlite_master WHERE type='table' AND name=@n;", conn))
{
cmd.Parameters.AddWithValue("@n", name);
object o = cmd.ExecuteScalar();
return o != null && o != DBNull.Value;
}
}
catch { return false; }
}
// ============================================================
// 双击项目行 → 进入项目
// ============================================================
private void advancedDataGridView6_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0) return;
DataGridViewRow row = this.advancedDataGridView6.Rows[e.RowIndex];
string pid = GetCellValueAsString(row, "项目编号");
if (string.IsNullOrWhiteSpace(pid)) return;
// 切项目前:先把当前项目的所有改动回写库,然后清空缓存
if (!string.IsNullOrWhiteSpace(_currentProjectId) && _currentProjectId != pid)
{
try
{
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (this.advancedDataGridView4 != null) this.advancedDataGridView4.EndEdit();
if (this.advancedDataGridView9 != null) this.advancedDataGridView9.EndEdit();
if (_tuhaoTable != null)
SaveTableToDatabase(_tuhaoTable, _currentProjectId, TuhaoTableName, null, null, true);
foreach (var kv in _xianghaoCache)
if (kv.Value != null && !string.IsNullOrWhiteSpace(kv.Value.TableName))
SaveTableToDatabase(kv.Value, _currentProjectId,
"\"" + kv.Value.TableName + "\"", null, null, true);
foreach (var kv in _yuanjianCache)
if (kv.Value != null && !string.IsNullOrWhiteSpace(kv.Value.TableName))
SaveTableToDatabase(kv.Value, _currentProjectId,
"\"" + kv.Value.TableName + "\"", null, null, true);
}
catch { }
_xianghaoCache.Clear();
_yuanjianCache.Clear();
_tuhaoTable = null;
}
_currentProjectId = pid;
EnterProjectCore(pid);
}
private void EnterProjectCore(string pid)
{
// 切换主区域到报价页(tabPage4)
this.tabControl2.SelectedTab = this.tabPage4;
EnsureQuoteDbExists(pid);
LoadPriceFormatConfig(pid);
RecalcAllFromDatabase(pid);
ReloadTuhaoByProject(pid);
ApplyPriceFormat(this.advancedDataGridView9, _单台小数位文本);
ApplyPriceFormat(this.advancedDataGridView4, _单台小数位文本);
ApplyPriceFormat(this.advancedDataGridView5, _单台小数位文本);
ApplyPriceFormat(this.advancedDataGridView6, _项目小数位文本);
}
// ============================================================
// ★ 右键点击时保持多行选中
// WinForms 默认行为:右键点击未选中行 → 清除多选,只选一行
// 修正:右键点击已选中行 → 保持多选;点击未选中行 → 正常切换
// ============================================================
private void Grid_MouseDown_KeepMultiSelect(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Right) return;
var dgv = sender as DataGridView;
if (dgv == null) return;
var hit = dgv.HitTest(e.X, e.Y);
if (hit.Type != DataGridViewHitTestType.Cell && hit.Type != DataGridViewHitTestType.RowHeader) return;
if (hit.RowIndex < 0) return;
// 如果右键点击的行已经在选中行中,不做任何操作(保持多选)
// 如果不在选中行中,让默认行为生效(选中该行)
foreach (DataGridViewRow r in dgv.SelectedRows)
{
if (r.Index == hit.RowIndex) return;
}
// 选中行中没有包含右键点击的行 → 手动选中该行
dgv.ClearSelection();
dgv.Rows[hit.RowIndex].Selected = true;
dgv.CurrentCell = dgv.Rows[hit.RowIndex].Cells[0];
}
// ============================================================
// ★ 键盘 Delete 键批量删除选中行
// 图号表→MenuTuhao_Delete_Click;箱号表→MenuXianghao_Delete_Click;元件表→MenuYuanjian_Delete_Click
// ============================================================
// F1 连按计时(1 秒内连按判定)
private DateTime _lastF1Time = DateTime.MinValue;
/// <summary>F1:元件两两配对合并;F2:元件三个一组配对合并。</summary>
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if ((keyData & Keys.KeyCode) == Keys.F1 && this.advancedDataGridView5 != null)
{
try { RunF1Merge(); } catch (Exception ex) { Log("[F1] " + ex.Message); }
return true;
}
if ((keyData & Keys.KeyCode) == Keys.F2 && this.advancedDataGridView5 != null)
{
try { MergeYuanjianByGroup(3); } catch (Exception ex) { Log("[F2] " + ex.Message); }
return true;
}
if ((keyData & Keys.KeyCode) == Keys.F3 && this.advancedDataGridView5 != null)
{
try { MergeYuanjianByGroup(4); } catch (Exception ex) { Log("[F3] " + ex.Message); }
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
// ★ 合并防抖时间戳(MergeYuanjianByGroup 统一闸门用)
private DateTime _lastFKeyTime = DateTime.MinValue;
/// <summary>统一 F1 入口(宿主按键/网页转发共用)。</summary>
private void RunF1Merge()
{
MergeAdjacentYuanjianByF1();
}
private void Grid_KeyDown_Delete(object sender, KeyEventArgs e)
{
if (e.KeyCode != Keys.Delete) return;
var dgv = sender as DataGridView;
if (dgv == null) return;
if (dgv.SelectedRows.Count == 0 && dgv.SelectedCells.Count == 0) return;
if (dgv.IsCurrentCellInEditMode) return; // 编辑模式下不拦截 Delete
e.SuppressKeyPress = true;
e.Handled = true;
if (dgv == this.advancedDataGridView9)
MenuTuhao_Delete_Click(null, null);
else if (dgv == this.advancedDataGridView4)
MenuXianghao_Delete_Click(null, null);
else if (dgv == this.advancedDataGridView5)
MenuYuanjian_Delete_Click(null, null);
}
// ============================================================
// 右键菜单:创建 + 行移动
// ============================================================
private void BuildContextMenus()
{
// 图号菜单(advancedDataGridView9)
_menuTuhao = new ContextMenuStrip();
_menuTuhao.Items.Add("新建图号", null, MenuTuhao_New_Click);
_menuTuhao.Items.Add("删除图号", null, MenuTuhao_Delete_Click);
_menuTuhao.Items.Add(new ToolStripSeparator());
_menuTuhao.Items.Add("置顶", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Top));
_menuTuhao.Items.Add("向上移动", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Up));
_menuTuhao.Items.Add("向下移动", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Down));
_menuTuhao.Items.Add("置底", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.Bottom));
_menuTuhao.Items.Add("移动到...", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView9, RowMoveAction.To));
this.advancedDataGridView9.ContextMenuStrip = _menuTuhao;
// 箱号菜单(advancedDataGridView4)
_menuXianghao = new ContextMenuStrip();
_menuXianghao.Items.Add("新建箱号", null, MenuXianghao_New_Click);
_menuXianghao.Items.Add("删除箱号", null, MenuXianghao_Delete_Click);
_menuXianghao.Items.Add(new ToolStripSeparator());
_menuXianghao.Items.Add("置顶", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Top));
_menuXianghao.Items.Add("向上移动", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Up));
_menuXianghao.Items.Add("向下移动", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Down));
_menuXianghao.Items.Add("置底", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.Bottom));
_menuXianghao.Items.Add("移动到...", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView4, RowMoveAction.To));
this.advancedDataGridView4.ContextMenuStrip = _menuXianghao;
// 元件菜单(advancedDataGridView5)
_menuYuanjian = new ContextMenuStrip();
_menuYuanjian.Items.Add("新建元件", null, MenuYuanjian_New_Click);
_menuYuanjian.Items.Add("删除元件", null, MenuYuanjian_Delete_Click);
_menuYuanjian.Items.Add("合并元件", null, MenuYuanjian_Merge_Click);
_menuYuanjian.Items.Add(new ToolStripSeparator());
_menuYuanjian.Items.Add("置顶", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Top));
_menuYuanjian.Items.Add("向上移动", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Up));
_menuYuanjian.Items.Add("向下移动", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Down));
_menuYuanjian.Items.Add("置底", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.Bottom));
_menuYuanjian.Items.Add("移动到...", null, (s, e) => MoveSelectedRowInGrid(this.advancedDataGridView5, RowMoveAction.To));
this.advancedDataGridView5.ContextMenuStrip = _menuYuanjian;
}
private enum RowMoveAction { Top, Up, Down, Bottom, To }
private void MoveSelectedRowInGrid(DataGridView dgv, RowMoveAction action)
{
if (dgv == null || string.IsNullOrWhiteSpace(_currentProjectId)) return;
DataTable table = null;
System.Action reloadAction = null;
if (dgv == this.advancedDataGridView5)
{
table = _yuanjianTable;
reloadAction = () => ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
}
else if (dgv == this.advancedDataGridView4)
{
table = _xianghaoTable;
reloadAction = () => ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao);
}
else if (dgv == this.advancedDataGridView9)
{
table = _tuhaoTable;
reloadAction = () => ReloadTuhaoByProject(_currentProjectId);
}
if (table == null || table.Rows.Count == 0)
{
MessageBox.Show(this, "没有数据可移动。", "移动行", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var dgvRow = GetFirstSelectedRow(dgv);
if (dgvRow == null)
{
MessageBox.Show(this, "请先选择要移动的行。", "移动行", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
int dataCount = dgv.Rows.Count;
if (dgv.AllowUserToAddRows) dataCount--;
if (dataCount <= 0) return;
int oldIndex = dgvRow.Index;
if (oldIndex >= dataCount) return;
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
int newIndex = -1;
_isReloading = true;
try
{
dgv.EndEdit();
newIndex = oldIndex;
switch (action)
{
case RowMoveAction.Top: newIndex = 0; break;
case RowMoveAction.Up: newIndex = Math.Max(0, oldIndex - 1); break;
case RowMoveAction.Down: newIndex = Math.Min(dataCount - 1, oldIndex + 1); break;
case RowMoveAction.Bottom: newIndex = dataCount - 1; break;
case RowMoveAction.To:
string input = ShowInputDialog("移动到",
"请输入目标行号(1-" + dataCount + "):", (oldIndex + 1).ToString());
if (string.IsNullOrEmpty(input)) { newIndex = -1; return; }
int target;
if (!int.TryParse(input.Trim(), out target))
{
MessageBox.Show(this, "请输入有效数字。", "移动到", MessageBoxButtons.OK, MessageBoxIcon.Warning);
newIndex = -1;
return;
}
newIndex = Math.Max(0, Math.Min(dataCount - 1, target - 1));
break;
}
if (newIndex == oldIndex) { newIndex = -1; return; }
DataRow rowToMove = table.Rows[oldIndex];
object[] savedValues = rowToMove.ItemArray;
table.Rows.Remove(rowToMove);
DataRow newRow = table.NewRow();
newRow.ItemArray = savedValues;
table.Rows.InsertAt(newRow, newIndex);
if (table.Columns.Contains("序号"))
{
for (int i = 0; i < table.Rows.Count; i++) table.Rows[i]["序号"] = (i + 1).ToString();
}
if (dgv == this.advancedDataGridView9)
SaveTableToDatabase(table, _currentProjectId, TuhaoTableName, null, null, true);
else
SaveTableToDatabase(table, _currentProjectId, "\"" + table.TableName + "\"", null, null, true);
}
catch (Exception ex)
{
MessageBox.Show(this, "移动行失败:\n" + ex.Message, "移动行", MessageBoxButtons.OK, MessageBoxIcon.Error);
newIndex = -1;
}
finally
{
_isReloading = false;
if (newIndex >= 0)
{
if (dgv == this.advancedDataGridView9)
SelectRowByValue(this.advancedDataGridView9, "图号", rememberTuhao);
else if (dgv == this.advancedDataGridView4)
SelectRowByValue(this.advancedDataGridView4, "箱号", rememberXianghao);
else if (dgv == this.advancedDataGridView5)
SelectRowByValue(this.advancedDataGridView5, "序号", (newIndex + 1).ToString());
}
}
}
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;
int lastIndex = table.Rows.Count - 1;
int targetIndex = afterIndex + 1;
if (targetIndex >= lastIndex) return;
object[] savedValues = table.Rows[lastIndex].ItemArray;
table.Rows.RemoveAt(lastIndex);
DataRow newRow = table.NewRow();
newRow.ItemArray = savedValues;
table.Rows.InsertAt(newRow, targetIndex);
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 { }
}
// ---------- 图号菜单事件 ----------
private void MenuTuhao_New_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在项目表中双击选择一个项目,再新建图号。",
"新建图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string newTuhao = "新图号" + GetNextSeqNumber(TuhaoTableName, "图号", _currentProjectId);
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView9);
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
InsertTuhao(_currentProjectId, newTuhao, "", "");
_xianghaoCache.Clear();
_yuanjianCache.Clear();
ReloadTuhaoByProject(_currentProjectId);
MoveLastRowBelowSelected(this.advancedDataGridView9, _tuhaoTable, selectedIndex, TuhaoTableName, _currentProjectId);
RecalcAllAfterReload();
}
catch (Exception ex)
{
MessageBox.Show(this, "新建图号失败:\n" + ex.Message,
"新建图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_currentTuhao = newTuhao;
SelectRowByValue(this.advancedDataGridView9, "图号", newTuhao);
_isReloading = false;
ReloadXianghaoByTuhao(_currentProjectId, newTuhao);
}
}
private void MenuTuhao_Delete_Click(object sender, EventArgs e)
{
var rows = GetAllSelectedRows(this.advancedDataGridView9);
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先选择要删除的图号。",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var tuhaos = new List<string>();
foreach (var r in rows)
{
string th = GetCellValueAsString(r, "图号");
if (!string.IsNullOrWhiteSpace(th) && !tuhaos.Contains(th)) tuhaos.Add(th);
}
if (tuhaos.Count == 0)
{
MessageBox.Show(this, "选中行没有图号,无法删除。",
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// ★ 收集选中图号行自己的框Handle
var frameHandles = new List<string>();
foreach (var r in rows)
{
var drv = r.DataBoundItem as DataRowView;
if (drv != null && drv.Row != null && drv.Row.Table.Columns.Contains("框Handle"))
{
object v = drv.Row["框Handle"];
if (v != null && v != DBNull.Value)
{
string h = v.ToString().Trim();
if (!string.IsNullOrWhiteSpace(h)) frameHandles.Add(h);
}
}
}
// ★ 收集每个图号下所有箱号和元件的框Handle(级联删框)
foreach (string tuhao in tuhaos)
{
CollectFrameHandlesForTuhao(_currentProjectId, tuhao, frameHandles);
}
Log("[MenuTuhao_Delete] 删除图号: " + string.Join(",", tuhaos) + " 收集到框Handle数: " + frameHandles.Count);
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
foreach (string tuhao in tuhaos)
{
DeleteTuhao(_currentProjectId, tuhao);
if (!string.IsNullOrEmpty(tuhao))
{
_xianghaoCache.Remove(tuhao);
var keysToRemove = new List<string>();
foreach (var key in _yuanjianCache.Keys)
if (key.StartsWith(tuhao + "\n")) keysToRemove.Add(key);
foreach (var key in keysToRemove) _yuanjianCache.Remove(key);
}
}
RenumberTable(_currentProjectId, TuhaoTableName);
ReloadTuhaoByProject(_currentProjectId);
RecalcAllAfterReload();
// ★ 删除所有收集到的框实体(去重)
var tuhaoDistinct = new HashSet<string>(frameHandles);
foreach (string h in tuhaoDistinct)
{
Log("[MenuTuhao_Delete] 准备删除框Handle: " + h);
DeleteFrameByHandle(h);
}
Log("[MenuTuhao_Delete] 框实体删除完成,共 " + tuhaoDistinct.Count + " 个");
// ★ 保存DWG,确保删框操作持久化
SaveCurrentDwg();
}
catch (Exception ex)
{
MessageBox.Show(this, "删除图号失败:\n" + ex.Message,
"删除图号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally { _isReloading = false; }
}
// ---------- 箱号菜单事件 ----------
private void MenuXianghao_New_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_currentProjectId) || string.IsNullOrWhiteSpace(_currentTuhao))
{
MessageBox.Show(this, "请先选择图号,再新建箱号。",
"新建箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string rememberTuhao = _currentTuhao;
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView4);
string newXh = "新箱号" + GetXianghaoCount(_currentProjectId, _currentTuhao);
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
// ★ 图纸列=当前打开的 CAD 图纸名(查看器开过图就有值,去扩展名)
InsertXianghao(_currentProjectId, _currentTuhao, newXh, "", "",
成套报价软件.CadBridge.CurrentDrawingName);
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, "图号", rememberTuhao);
SelectRowByValue(this.advancedDataGridView4, "箱号", newXh);
_isReloading = false;
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, newXh);
}
}
private void MenuXianghao_Delete_Click(object sender, EventArgs e)
{
var rows = GetAllSelectedRows(this.advancedDataGridView4);
if (rows == null || rows.Count == 0)
{
MessageBox.Show(this, "请先选择要删除的箱号。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var xhs = new List<string>();
foreach (var r in rows)
{
string xh = GetCellValueAsString(r, "箱号");
if (!string.IsNullOrWhiteSpace(xh) && !xhs.Contains(xh)) xhs.Add(xh);
}
if (xhs.Count == 0)
{
MessageBox.Show(this, "选中行没有箱号,无法删除。",
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// ★ 收集选中箱号行自己的框Handle
var frameHandles = new List<string>();
foreach (var r in rows)
{
var drv = r.DataBoundItem as DataRowView;
if (drv != null && drv.Row != null && drv.Row.Table.Columns.Contains("框Handle"))
{
object v = drv.Row["框Handle"];
if (v != null && v != DBNull.Value)
{
string h = v.ToString().Trim();
if (!string.IsNullOrWhiteSpace(h)) frameHandles.Add(h);
}
}
}
// ★ 收集每个箱号下元件表的框Handle(级联删框)
foreach (string xh in xhs)
{
CollectFrameHandlesForXianghao(_currentProjectId, _currentTuhao, xh, frameHandles);
}
Log("[MenuXianghao_Delete] 删除箱号: " + string.Join(",", xhs) + " 图号=" + _currentTuhao + " 收集到框Handle数: " + frameHandles.Count);
string rememberTuhao = _currentTuhao;
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
foreach (string xh in xhs)
{
DeleteXianghao(_currentProjectId, _currentTuhao, xh);
string yjKey = _currentTuhao + "\n" + xh;
_yuanjianCache.Remove(yjKey);
}
string xhTbl = GetXianghaoTableByIndex(_currentProjectId, _currentTuhao);
if (!string.IsNullOrEmpty(xhTbl))
RenumberTable(_currentProjectId, "\"" + xhTbl + "\"");
if (!string.IsNullOrEmpty(_currentTuhao)) _xianghaoCache.Remove(_currentTuhao);
Log("[MenuXianghao_Delete] 开始 ReloadXianghaoByTuhao...");
ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao);
Log("[MenuXianghao_Delete] ReloadXianghaoByTuhao 完成,开始删除框实体...");
RecalcAllAfterReload();
// ★ 删除所有收集到的框实体(去重,避免重复删除)
var distinctHandles = new HashSet<string>(frameHandles);
foreach (string h in distinctHandles)
{
Log("[MenuXianghao_Delete] 准备删除框Handle: " + h);
DeleteFrameByHandle(h);
}
Log("[MenuXianghao_Delete] 框实体删除完成,共 " + distinctHandles.Count + " 个");
// ★ 保存DWG,确保删框操作持久化
SaveCurrentDwg();
}
catch (Exception ex)
{
Log("[MenuXianghao_Delete] 异常: " + ex.Message + "\n" + ex.StackTrace);
MessageBox.Show(this, "删除箱号失败:\n" + ex.Message,
"删除箱号", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_currentTuhao = rememberTuhao;
SelectRowByValue(this.advancedDataGridView9, "图号", rememberTuhao);
_isReloading = false;
}
}
// ---------- 元件菜单事件 ----------
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;
}
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
string newName = "新元件" + GetYuanjianCount(_currentProjectId, _currentTuhao, _currentXianghao);
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
InsertYuanjian(_currentProjectId, _currentTuhao, _currentXianghao, newName);
string cacheKey = _currentTuhao + "\n" + _currentXianghao;
_yuanjianCache.Remove(cacheKey);
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
if (_yuanjianTable != null)
MoveLastRowBelowSelected(this.advancedDataGridView5, _yuanjianTable, selectedIndex,
"\"" + _yuanjianTable.TableName + "\"", _currentProjectId);
RecalcAllAfterReload();
}
catch (Exception ex)
{
MessageBox.Show(this, "新建元件失败:\n" + ex.Message,
"新建元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_currentTuhao = rememberTuhao;
_currentXianghao = rememberXianghao;
SelectRowByValue(this.advancedDataGridView9, "图号", rememberTuhao);
SelectRowByValue(this.advancedDataGridView4, "箱号", rememberXianghao);
_isReloading = false;
}
}
private void MenuYuanjian_Delete_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 == null || rows.Count == 0)
{
MessageBox.Show(this, "请先选择要删除的元件。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var seqs = new List<string>();
var frameHandles = new List<string>();
foreach (var r in rows)
{
string seq = GetCellValueAsString(r, "序号");
if (!string.IsNullOrWhiteSpace(seq) && !seqs.Contains(seq)) seqs.Add(seq);
// ★ 收集元件行的框Handle
var drv = r.DataBoundItem as DataRowView;
if (drv != null && drv.Row != null && drv.Row.Table.Columns.Contains("框Handle"))
{
object v = drv.Row["框Handle"];
if (v != null && v != DBNull.Value)
{
string h = v.ToString().Trim();
if (!string.IsNullOrWhiteSpace(h)) frameHandles.Add(h);
}
}
}
if (seqs.Count == 0)
{
MessageBox.Show(this, "选中行没有序号,无法删除。",
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
foreach (string seq in seqs)
DeleteYuanjian(_currentProjectId, _currentTuhao, _currentXianghao, seq);
string realTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, _currentXianghao);
if (!string.IsNullOrEmpty(realTbl))
RenumberTable(_currentProjectId, "\"" + realTbl + "\"");
string cacheKey = _currentTuhao + "\n" + _currentXianghao;
_yuanjianCache.Remove(cacheKey);
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
RecalcAllAfterReload();
// ★ 删除所有收集到的框实体(去重)
foreach (string h in new HashSet<string>(frameHandles)) DeleteFrameByHandle(h);
// ★ 保存DWG,确保删框操作持久化
SaveCurrentDwg();
}
catch (Exception ex)
{
MessageBox.Show(this, "删除元件失败:\n" + ex.Message,
"删除元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_currentTuhao = rememberTuhao;
_currentXianghao = rememberXianghao;
SelectRowByValue(this.advancedDataGridView9, "图号", rememberTuhao);
SelectRowByValue(this.advancedDataGridView4, "箱号", rememberXianghao);
_isReloading = false;
}
}
// ============================================================
// 合并元件:把选中的多行元件合并为一行
// · 元件名称:所有非空名称直接拼接(去重,不加符号)
// · 规格:所有非空规格直接拼接(去重,不加符号)
// · 数量:保持为 1(合并后是一个元件,数量不变)
// · 其他列(单位/系数/价格):取第一行
// · 删除被合并的其他行,重新编号,保存刷新
// ============================================================
/// <summary>按框Handle集合选中元件表中的行(多选,滚动到第一行)。</summary>
private void SelectYuanjianRowsByFrameHandles(List<string> frameHandles)
{
var dgv = this.advancedDataGridView5;
if (dgv == null || frameHandles == null || frameHandles.Count == 0) return;
try
{
dgv.MultiSelect = true;
if (dgv.SelectionMode != DataGridViewSelectionMode.RowHeaderSelect &&
dgv.SelectionMode != DataGridViewSelectionMode.FullRowSelect)
{
dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
}
dgv.ClearSelection();
DataGridViewRow first = null;
foreach (DataGridViewRow row in dgv.Rows)
{
var drv = row.DataBoundItem as DataRowView;
if (drv == null || drv.Row == null) continue;
if (!drv.Row.Table.Columns.Contains("框Handle")) continue;
object v = drv.Row["框Handle"];
string h = v != null && v != DBNull.Value ? v.ToString().Trim() : "";
if (!string.IsNullOrWhiteSpace(h) && frameHandles.Contains(h))
{
row.Selected = true;
if (first == null) first = row;
}
}
if (first != null && first.DataGridView != null)
first.DataGridView.FirstDisplayedScrollingRowIndex = Math.Max(0, first.Index);
}
catch (Exception ex) { Log("[SelectYuanjianRows] " + ex.Message); }
}
/// <summary>网页转发的 F1:转到统一入口(焦点在网页时 WinForms 收不到按键)。</summary>
private void OnF1FromBridge()
{
try { RunF1Merge(); }
catch (Exception ex) { Log("[F1桥接] " + ex.Message); }
}
/// <summary>网页转发的 F2:三个一组配对合并。</summary>
private void OnF2FromBridge()
{
try { MergeYuanjianByGroup(3); }
catch (Exception ex) { Log("[F2桥接] " + ex.Message); }
}
/// <summary>网页转发的 F3:四个一组配对合并。</summary>
private void OnF3FromBridge()
{
try { MergeYuanjianByGroup(4); }
catch (Exception ex) { Log("[F3桥接] " + ex.Message); }
}
/// <summary>
/// F1:合并元件表中相邻的两个元件(按序号顺序)。
/// · 选中 ≥2 行 → 合并前两行
/// · 选中 1 行 → 与它的下一行合并
/// · 未选中 → 合并第 1、2 行
/// 合并时网页 CAD 里的蒙版同步合并(保留行蒙版扩大到并集)。
/// </summary>
private void MergeAdjacentYuanjianByF1()
{
MergeYuanjianByGroup(2);
}
/// <summary>按 groupSize 分组合并选中元件(F1=两两,F2=三个一组)。</summary>
private void MergeYuanjianByGroup(int groupSize)
{
// ★ 防抖(300ms):所有触发路径(按键/桥/过滤器)的统一闸门,
// 防止一次按键双跑两遍合并(第二遍因选中已清而弹误提示)
if ((DateTime.Now - _lastFKeyTime).TotalMilliseconds < 300) return;
_lastFKeyTime = DateTime.Now;
var dgv = this.advancedDataGridView5;
if (dgv == null) return;
var rows = GetAllSelectedRows(dgv);
if (rows == null || rows.Count < groupSize)
{
MessageBox.Show(this, "请先选中至少 " + groupSize + " 行元件(扒元件后会自动全选)。", "合并元件",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// ★ F1 = 两两配对批量合并(1+2、3+4、5+6...),一次处理完所有选中行;
// 只想合并单独一对时,只选中那一对两行即可。
// 合并会刷新表格使行对象失效,所以先记下配对的框Handle,再用 Handle 逐对定位。
var orderedHandles = new List<string>();
foreach (var r in rows)
{
orderedHandles.Add(GetFrameHandleOfRow(r));
}
int groups = orderedHandles.Count / groupSize;
int merged = 0;
for (int i = 0; i < groups; i++)
{
var groupRows = new List<DataGridViewRow>();
var groupHandles = new List<string>();
for (int k = 0; k < groupSize; k++)
{
string h = orderedHandles[i * groupSize + k];
if (string.IsNullOrWhiteSpace(h)) { groupRows = null; break; }
var rowK = FindRowByFrameHandle(dgv, h);
if (rowK == null) { groupRows = null; break; }
groupRows.Add(rowK);
groupHandles.Add(h);
}
if (groupRows == null) continue;
// 组内若含重复行(同句柄)跳过
if (new HashSet<string>(groupHandles).Count != groupSize) continue;
MergeYuanjianRows(groupRows);
merged++;
}
Log("[F1批量] " + groupSize + " 个一组配对合并完成: " + merged + "/" + groups + " 组");
}
/// <summary>读行的框Handle:优先 DataRow("框Handle"可能不在显示列,Cells 读不到)。</summary>
private static string GetFrameHandleOfRow(DataGridViewRow row)
{
if (row == null) return "";
var drv = row.DataBoundItem as DataRowView;
if (drv != null && drv.Row != null && drv.Row.Table.Columns.Contains("框Handle"))
{
object v = drv.Row["框Handle"];
return v == null || v == DBNull.Value ? "" : v.ToString().Trim();
}
return GetCellValueAsString(row, "框Handle");
}
/// <summary>按框Handle在元件表中找行(找不到返回 null)。</summary>
private DataGridViewRow FindRowByFrameHandle(DataGridView dgv, string frameHandle)
{
if (string.IsNullOrWhiteSpace(frameHandle)) return null;
foreach (DataGridViewRow row in dgv.Rows)
{
if (row == null || row.IsNewRow) continue;
string h = GetFrameHandleOfRow(row);
if (!string.IsNullOrWhiteSpace(h) && h == frameHandle) return row;
}
return null;
}
/// <summary>合并指定行(按序号排序):第一行为合并目标,其余删除;蒙版同步合并。</summary>
private void MergeYuanjianRows(List<DataGridViewRow> rows)
{
// 按序号排序(保证合并顺序与表格顺序一致)
rows.Sort((a, b) =>
{
int ia = 0, ib = 0;
int.TryParse(GetCellValueAsString(a, "序号"), out ia);
int.TryParse(GetCellValueAsString(b, "序号"), out ib);
return ia.CompareTo(ib);
});
// 收集要合并的内容(直接拼接,不加任何符号)
var names = new List<string>();
var specs = new List<string>();
var drawingSpecs = new List<string>();
foreach (var r in rows)
{
string nm = GetCellValueAsString(r, "元件名称");
if (!string.IsNullOrWhiteSpace(nm) && !names.Contains(nm)) names.Add(nm);
string sp = GetCellValueAsString(r, "规格");
if (!string.IsNullOrWhiteSpace(sp) && !specs.Contains(sp)) specs.Add(sp);
// ★ 图纸规格同步收集(旧数据该列为空时用规格文本兜底);
// 不去重——合并后保留每条原文拼接(用户要求,规格列的去重是原设计不动)
string dzg = GetCellValueAsString(r, "图纸规格");
if (string.IsNullOrWhiteSpace(dzg)) dzg = sp;
if (!string.IsNullOrWhiteSpace(dzg)) drawingSpecs.Add(dzg);
}
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
string keepHandle = GetFrameHandleOfRow(rows[0]);
var absorbedHandles = new List<string>();
for (int i = 1; i < rows.Count; i++)
{
string h = GetFrameHandleOfRow(rows[i]);
if (!string.IsNullOrWhiteSpace(h)) absorbedHandles.Add(h);
}
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
if (_yuanjianTable == null || _yuanjianTable.Rows.Count == 0)
{
MessageBox.Show(this, "元件表为空,无法合并。",
"合并元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 取出选中行对应的 DataRow
var dataRows = new List<DataRow>();
foreach (var dgvRow in rows)
{
var drv = dgvRow.DataBoundItem as DataRowView;
if (drv != null && drv.Row != null) dataRows.Add(drv.Row);
}
if (dataRows.Count < 2)
{
MessageBox.Show(this, "无法获取选中行的数据,请重试。",
"合并元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 第一行作为合并目标,更新名称/规格(直接拼接,不加任何符号)
// 数量保持为 1(合并后是一个元件)
DataRow target = dataRows[0];
if (_yuanjianTable.Columns.Contains("元件名称"))
target["元件名称"] = string.Join(" ", names.Where(x => x.Trim().Length > 0).ToArray());
if (_yuanjianTable.Columns.Contains("规格"))
target["规格"] = string.Join(" ", specs.Where(x => x.Trim().Length > 0).ToArray());
// ★ 合并:图纸规格同步拼接被合并各行的型号原文(与规格列同步)
if (_yuanjianTable.Columns.Contains("图纸规格"))
target["图纸规格"] = string.Join(" ", drawingSpecs.Where(x => x.Trim().Length > 0).ToArray());
if (_yuanjianTable.Columns.Contains("数量"))
target["数量"] = "1";
// 删除其他被合并的行
for (int i = 1; i < dataRows.Count; i++)
{
dataRows[i].Delete();
}
// 保存到数据库
string realTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, _currentXianghao);
if (!string.IsNullOrEmpty(realTbl))
{
SaveTableToDatabase(_yuanjianTable, _currentProjectId,
"\"" + realTbl + "\"", null, null, true);
_yuanjianTable.AcceptChanges();
RenumberTable(_currentProjectId, "\"" + realTbl + "\"");
}
// ★ 网页 CAD 蒙版同步合并:保留行蒙版扩大到并集,被合并行蒙版移除
if (!string.IsNullOrWhiteSpace(keepHandle) && absorbedHandles.Count > 0)
{
成套报价软件.CadBridge.MergeMasks(keepHandle, absorbedHandles);
Log("[MergeYuanjianRows] 蒙版合并: 保留=" + keepHandle);
}
// 刷新缓存与表格
string cacheKey = _currentTuhao + "\n" + _currentXianghao;
_yuanjianCache.Remove(cacheKey);
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, _currentXianghao);
RecalcAllAfterReload();
}
catch (Exception ex)
{
MessageBox.Show(this, "合并元件失败:\n" + ex.Message,
"合并元件", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_currentTuhao = rememberTuhao;
_currentXianghao = rememberXianghao;
_isReloading = false;
// ★ 合并后的行保持选中(连续按 F1 可继续与下一行合并):
// 延迟到刷新事件链走完再选,否则会被清掉
if (!string.IsNullOrWhiteSpace(keepHandle))
{
var kh = keepHandle;
BeginInvoke((Action)(() =>
{
BeginInvoke((Action)(() =>
{
SelectYuanjianRowsByFrameHandles(new List<string> { kh });
}));
}));
}
}
}
private void MenuYuanjian_Merge_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 == null || rows.Count < 2)
{
MessageBox.Show(this, "请先选择至少两行要合并的元件(按住 Ctrl/Shift 多选)。",
"合并元件", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 按序号排序(保证合并顺序与表格顺序一致)
rows.Sort((a, b) =>
{
int ia = 0, ib = 0;
int.TryParse(GetCellValueAsString(a, "序号"), out ia);
int.TryParse(GetCellValueAsString(b, "序号"), out ib);
return ia.CompareTo(ib);
});
// 收集要合并的内容(直接拼接,不加任何符号)
var names = new List<string>();
var specs = new List<string>();
var drawingSpecs = new List<string>();
foreach (var r in rows)
{
string nm = GetCellValueAsString(r, "元件名称");
if (!string.IsNullOrWhiteSpace(nm) && !names.Contains(nm)) names.Add(nm);
string sp = GetCellValueAsString(r, "规格");
if (!string.IsNullOrWhiteSpace(sp) && !specs.Contains(sp)) specs.Add(sp);
// ★ 图纸规格同步收集(旧数据该列为空时用规格文本兜底);
// 不去重——合并后保留每条原文拼接(用户要求,规格列的去重是原设计不动)
string dzg = GetCellValueAsString(r, "图纸规格");
if (string.IsNullOrWhiteSpace(dzg)) dzg = sp;
if (!string.IsNullOrWhiteSpace(dzg)) drawingSpecs.Add(dzg);
}
string rememberTuhao = _currentTuhao;
string rememberXianghao = _currentXianghao;
try
{
_isReloading = true;
SaveAllQuoteTablesIfDirty(false);
if (_yuanjianTable == null || _yuanjianTable.Rows.Count == 0)
{
MessageBox.Show(this, "元件表为空,无法合并。",
"合并元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 取出选中行对应的 DataRow
var dataRows = new List<DataRow>();
foreach (var dgvRow in rows)
{
var drv = dgvRow.DataBoundItem as DataRowView;
if (drv != null && drv.Row != null) dataRows.Add(drv.Row);
}
if (dataRows.Count < 2)
{
MessageBox.Show(this, "无法获取选中行的数据,请重试。",
"合并元件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 第一行作为合并目标,更新名称/规格(直接拼接,不加符号)
// 数量保持为 1(合并后是一个元件)
DataRow target = dataRows[0];
if (_yuanjianTable.Columns.Contains("元件名称"))
target["元件名称"] = string.Join(" ", names.Where(x => x.Trim().Length > 0).ToArray());
if (_yuanjianTable.Columns.Contains("规格"))
target["规格"] = string.Join(" ", specs.Where(x => x.Trim().Length > 0).ToArray());
// ★ 合并:图纸规格同步拼接被合并各行的型号原文(与规格列同步)
if (_yuanjianTable.Columns.Contains("图纸规格"))
target["图纸规格"] = string.Join(" ", drawingSpecs.Where(x => x.Trim().Length > 0).ToArray());
if (_yuanjianTable.Columns.Contains("数量"))
target["数量"] = "1";
// ★ 收集被合并行(第2行起)的框Handle,合并后删除这些框
var mergedFrameHandles = new List<string>();
if (_yuanjianTable.Columns.Contains("框Handle"))
{
for (int i = 1; i < dataRows.Count; i++)
{
object v = dataRows[i]["框Handle"];
if (v != null && v != DBNull.Value)
{
string h = v.ToString().Trim();
if (!string.IsNullOrWhiteSpace(h)) mergedFrameHandles.Add(h);
}
}
}
// 删除其他被合并的行
for (int i = 1; i < dataRows.Count; i++)
{
dataRows[i].Delete();
}
// 保存到数据库
string realTbl = GetYuanjianTableByIndex(_currentProjectId, _currentTuhao, _currentXianghao);
if (!string.IsNullOrEmpty(realTbl))
{
SaveTableToDatabase(_yuanjianTable, _currentProjectId,
"\"" + realTbl + "\"", null, null, true);
_yuanjianTable.AcceptChanges();
RenumberTable(_currentProjectId, "\"" + realTbl + "\"");
}
// ★ 删除被合并行的框实体
foreach (string h in mergedFrameHandles) DeleteFrameByHandle(h);
// ★ 保存DWG,确保删框操作持久化
SaveCurrentDwg();
// 刷新缓存与表格
string cacheKey = _currentTuhao + "\n" + _currentXianghao;
_yuanjianCache.Remove(cacheKey);
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, "图号", rememberTuhao);
SelectRowByValue(this.advancedDataGridView4, "箱号", rememberXianghao);
_isReloading = false;
}
}
// ============================================================
// 工具栏:新建项目 / 删除项目 / 导出数据
// ============================================================
private void toolStripButton5_Click(object sender, EventArgs e)
{
// 用自定义对话框收集项目信息字段(无主项目的"新建项目"窗体可用)
var fields = ShowNewProjectDialog();
if (fields == null) return;
try
{
int affected = InsertProjectToDatabase(fields);
if (affected <= 0)
{
MessageBox.Show(this, "未写入任何字段,请检查输入。",
"新建项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
ReloadProjectsFromDatabase();
ApplyPriceFormat(this.advancedDataGridView6, _项目小数位文本);
}
catch (Exception ex)
{
MessageBox.Show(this, "保存项目失败:\n" + ex.Message,
"新建项目", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>弹出新建项目窗口,收集项目信息 + 价格格式化配置。</summary>
private Dictionary<string, string> ShowNewProjectDialog()
{
string dbPath = GetDbFullPath(ProjectsDbPath);
using (var dlg = new 新建项目(dbPath, ProjectsTableName))
{
if (dlg.ShowDialog(this) != DialogResult.OK) return null;
return dlg.GetInputValues();
}
}
private void toolStripButton6_Click(object sender, EventArgs e)
{
DataGridViewRow row = GetFirstSelectedProjectRow();
if (row == null)
{
MessageBox.Show(this, "请先在项目表格中选择要删除的项目。",
"删除项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string projectId = GetCellValueAsString(row, "项目编号");
if (string.IsNullOrWhiteSpace(projectId))
{
MessageBox.Show(this, "选中行没有“项目编号”,无法定位要删除的数据库记录。",
"删除项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
DialogResult dr = MessageBox.Show(this,
"确定要删除项目编号为 “" + projectId + "” 的项目吗?\n" +
"该项目下的所有图号/箱号/元件数据也会一并删除!\n此操作不可恢复。",
"删除项目", MessageBoxButtons.YesNo, MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2);
if (dr != DialogResult.Yes) return;
try
{
int affected = DeleteProjectFromDatabase(projectId);
CascadeDeleteByProject(projectId);
_currentProjectId = "";
_currentTuhao = "";
_currentXianghao = "";
_xianghaoCache.Clear();
_yuanjianCache.Clear();
_tuhaoTable = null;
ReloadProjectsFromDatabase();
ReloadTuhaoByProject("");
}
catch (Exception ex)
{
MessageBox.Show(this, "删除项目失败:\n" + ex.Message,
"删除项目", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---------- 项目表查找(button28)----------
private void button28_Click(object sender, EventArgs e)
{
string field = this.comboBox2.Text == null ? "" : this.comboBox2.Text.Trim();
string colName = MapSearchFieldToColumn(field);
if (string.IsNullOrEmpty(colName))
{
MessageBox.Show(this,
"请先在搜索条件下拉框中选择有效的搜索字段(项目名称 / 项目编号 / 客户名称)。",
"查找", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string keyword = this.textBox1.Text == null ? "" : this.textBox1.Text.Trim();
DataTable table = this.advancedDataGridView6.DataSource as DataTable;
if (table == null)
{
MessageBox.Show(this, "项目表暂无数据,无法搜索。", "查找",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (keyword.Length == 0)
{
if (table.DefaultView.RowFilter.Length > 0) table.DefaultView.RowFilter = "";
return;
}
string escaped = EscapeLikeValue(keyword);
try { table.DefaultView.RowFilter = "[" + colName + "] 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, "没有找到 " + field + " 包含 “" + keyword + "” 的项目。",
"查找", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private static string MapSearchFieldToColumn(string field)
{
if (string.IsNullOrEmpty(field)) return "";
string f = field.Replace(" ", "").Trim();
if (f == "项目名称" || f == "名称") return "项目名称";
if (f == "项目编号" || f == "编号") return "项目编号";
if (f == "客户名称" || f == "客户") return "客户名称";
return "";
}
private static string EscapeLikeValue(string value)
{
if (string.IsNullOrEmpty(value)) return "";
var sb = new System.Text.StringBuilder(value.Length * 2);
foreach (char c in value)
{
if (c == '\'') sb.Append("''");
else if (c == '%' || c == '_' || c == '[' || c == ']') sb.Append('[').Append(c).Append(']');
else sb.Append(c);
}
return sb.ToString();
}
// ---------- 导出数据(toolStripButton1)----------
private void toolStripButton1_Click(object sender, EventArgs e)
{
string pid = "";
try
{
var projects = ReadAllProjectsFromDatabase();
if (projects == null || projects.Count == 0)
{
MessageBox.Show(this, "项目列表为空,无项目可导出。",
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (projects.Count == 1)
{
pid = projects[0].ContainsKey("项目编号") ? (projects[0]["项目编号"] ?? "").ToString() : "";
}
else
{
pid = PickProjectDialog(projects);
if (string.IsNullOrWhiteSpace(pid)) return;
}
if (string.IsNullOrWhiteSpace(pid))
{
MessageBox.Show(this, "未能确定要导出的项目编号。",
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 先保存当前项目的所有改动
SaveAllToDatabaseSilent();
string dbPath = GetQuoteDbPath(pid);
if (!File.Exists(dbPath))
{
MessageBox.Show(this, "项目数据库文件不存在:\n" + dbPath,
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var meta = ReadProjectMetaData(pid);
if (meta == null)
{
MessageBox.Show(this, "项目元数据读取失败,项目编号:" + pid,
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string projName = meta.ContainsKey("项目名称") ? (meta["项目名称"] ?? "").ToString() : "";
string safeName = MakeSafeFileName(projName + "_" + pid);
string defaultFileName = safeName + "_" + DateTime.Now.ToString("yyyyMMdd") + ".zip";
string initialDir = "";
try
{
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
if (!string.IsNullOrEmpty(desktop) && Directory.Exists(desktop)) initialDir = desktop;
}
catch { }
if (string.IsNullOrEmpty(initialDir))
{
try
{
string docs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
if (!string.IsNullOrEmpty(docs) && Directory.Exists(docs)) initialDir = docs;
}
catch { }
}
// ★ 打包到临时 zip(不弹保存对话框,直接联动主窗口)
string tempZip = Path.Combine(Path.GetTempPath(), "cadcj_导出_" + Guid.NewGuid().ToString("N") + ".zip");
string tempDir = Path.Combine(Path.GetTempPath(), "cadcj_导出_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
// 复制 db(用 SQL ATTACH 绕开文件锁,兜底 File.Copy)
string dstDb = Path.Combine(tempDir, "项目" + MakeSafeFileName(pid) + ".db");
BackupSQLiteDb(dbPath, dstDb);
// 写 meta.json
var sb = new System.Text.StringBuilder();
sb.Append("{\n \"项目编号\": \"");
sb.Append(EscapeJsonString(pid));
sb.Append("\",\n \"字段\": {");
bool firstKv = true;
foreach (var kv in meta)
{
if (firstKv) firstKv = false; else sb.Append(",");
sb.Append("\n \"");
sb.Append(EscapeJsonString(kv.Key));
sb.Append("\": \"");
sb.Append(EscapeJsonString((kv.Value ?? "").ToString()));
sb.Append("\"");
}
sb.Append("\n }\n}");
File.WriteAllText(Path.Combine(tempDir, "meta.json"), sb.ToString(), new System.Text.UTF8Encoding(false));
// 打包 zip
if (File.Exists(tempZip)) File.Delete(tempZip);
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, tempZip);
// ★ 查找主窗口 exe(cadcj 目录的上级目录)
string mainExePath = null;
try
{
string pluginDir = Path.GetDirectoryName(
new System.Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath);
// ★ 主程序托管后 assembly 目录=主程序目录,exe 在同级,先试同级
string candidateSameDir = Path.Combine(pluginDir, "成套报价软件.exe");
if (File.Exists(candidateSameDir)) mainExePath = candidateSameDir;
if (string.IsNullOrEmpty(mainExePath))
{
string parentDir = Path.GetFullPath(Path.Combine(pluginDir, ".."));
string candidate = Path.Combine(parentDir, "成套报价软件.exe");
if (File.Exists(candidate)) mainExePath = candidate;
}
}
catch { }
if (!string.IsNullOrEmpty(mainExePath))
{
// ★ 启动主窗口并传递 zip 路径,主窗口自动导入并打开项目
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = mainExePath,
Arguments = "\"" + tempZip + "\"",
UseShellExecute = true,
};
System.Diagnostics.Process.Start(psi);
MessageBox.Show(this,
"已发送到主窗口!\n\n项目:" + projName + " (" + pid + ")\n主程序已打开该项目(若主程序未开会自动启动)。",
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
// ★ 找不到主窗口 exe,退回到保存对话框模式
using (var sfd = new SaveFileDialog
{
Title = "导出项目",
Filter = "项目包 (*.zip)|*.zip",
FileName = defaultFileName,
RestoreDirectory = true,
})
{
if (!string.IsNullOrEmpty(initialDir)) sfd.InitialDirectory = initialDir;
if (sfd.ShowDialog(this) != DialogResult.OK) return;
string zipPath = sfd.FileName;
File.Copy(tempZip, zipPath, true);
MessageBox.Show(this,
"导出成功!\n\n项目:" + projName + " (" + pid + ")\n文件:" + zipPath +
"\n\n(未找到主窗口程序,已保存为zip文件,可手动导入)",
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
finally
{
try { Directory.Delete(tempDir, true); } catch { }
// 临时 zip 不立即删除(主窗口可能正在读取)
}
}
catch (Exception ex)
{
MessageBox.Show(this, "导出失败:\n" + ex.Message,
"导出项目", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private System.Collections.Generic.List<AdgvMsgHook> _keepMultiHooks;
private void HookKeepMulti(System.Windows.Forms.DataGridView dgv)
{
if (dgv == null) return;
var hook = new AdgvMsgHook(dgv);
_keepMultiHooks.Add(hook); // 存字段防 GC
}
/// <summary>
/// ADGV 右键多选保持钩子(NativeWindow):吞掉"右键点已选中行"的 WM_RBUTTONDOWN,
/// 多选集合不被清空;右键菜单由 WM_CONTEXTMENU 正常弹出(与原 AdgvKeepMulti 子类语义一致)。
/// </summary>
private class AdgvMsgHook : System.Windows.Forms.NativeWindow
{
private const int WM_RBUTTONDOWN = 0x0204;
private readonly System.Windows.Forms.DataGridView _dgv;
public AdgvMsgHook(System.Windows.Forms.DataGridView dgv)
{
_dgv = dgv;
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.SelectedRows != null)
{
foreach (System.Windows.Forms.DataGridViewRow r in _dgv.SelectedRows)
{
if (r != null && r.Index == hit.RowIndex)
return; // ★ 吞掉:ADGV 不清多选
}
}
}
catch { /* 异常时退回默认行为 */ }
}
base.WndProc(ref m);
}
}
/// <summary>用 SQL ATTACH 复制 SQLite 库,兜底 File.Copy。</summary>
private void BackupSQLiteDb(string srcDbPath, string dstDbPath)
{
try { if (File.Exists(dstDbPath)) File.Delete(dstDbPath); } catch { }
Exception sqlErr = null;
try
{
using (var srcConn = new System.Data.SQLite.SQLiteConnection(
"Data Source=" + srcDbPath + ";Version=3;Pooling=False;ReadOnly=True;"))
{
srcConn.Open();
using (var att = new System.Data.SQLite.SQLiteCommand("ATTACH DATABASE @path AS dst;", srcConn))
{
att.Parameters.AddWithValue("@path", dstDbPath);
att.ExecuteNonQuery();
}
string selAll = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';";
var tables = new List<KeyValuePair<string, string>>();
using (var sel = new System.Data.SQLite.SQLiteCommand(selAll, srcConn))
using (var rd = sel.ExecuteReader())
{
while (rd.Read())
{
string tn = rd.IsDBNull(0) ? "" : rd.GetString(0);
string sql = rd.IsDBNull(1) ? "" : rd.GetString(1);
if (!string.IsNullOrEmpty(tn)) tables.Add(new KeyValuePair<string, string>(tn, sql));
}
}
foreach (var kv in tables)
{
string tn = kv.Key;
string sql = kv.Value;
if (!string.IsNullOrEmpty(sql))
{
string dstSql = sql.Replace("CREATE TABLE " + tn, "CREATE TABLE dst." + tn);
using (var c = new System.Data.SQLite.SQLiteCommand(dstSql, srcConn)) c.ExecuteNonQuery();
}
using (var c = new System.Data.SQLite.SQLiteCommand(
"INSERT INTO dst.\"" + tn + "\" SELECT * FROM main.\"" + tn + "\";", srcConn))
c.ExecuteNonQuery();
}
using (var det = new System.Data.SQLite.SQLiteCommand("DETACH DATABASE dst;", srcConn))
det.ExecuteNonQuery();
return;
}
}
catch (Exception ex) { sqlErr = ex; }
// 兜底:File.Copy
try { System.Data.SQLite.SQLiteConnection.ClearAllPools(); } catch { }
try { if (File.Exists(dstDbPath)) File.Delete(dstDbPath); } catch { }
File.Copy(srcDbPath, dstDbPath, true);
}
/// <summary>弹出一个简单对话框,让用户从项目列表中选一个项目,返回项目编号。</summary>
private string PickProjectDialog(List<Dictionary<string, object>> projects)
{
try
{
using (var dlg = new Form
{
Text = "选择要导出的项目",
StartPosition = FormStartPosition.CenterParent,
FormBorderStyle = FormBorderStyle.FixedDialog,
MaximizeBox = false,
MinimizeBox = false,
ClientSize = new Size(520, 360),
})
{
var lbl = new Label
{
Text = "请选择要导出的项目:",
Location = new Point(12, 12),
Size = new Size(496, 23),
TextAlign = ContentAlignment.MiddleLeft,
};
dlg.Controls.Add(lbl);
var lv = new ListView
{
Location = new Point(12, 40),
Size = new Size(496, 260),
View = View.Details,
FullRowSelect = true,
MultiSelect = false,
HideSelection = false,
};
lv.Columns.Add("项目编号", 100);
lv.Columns.Add("项目名称", 160);
lv.Columns.Add("客户名称", 130);
lv.Columns.Add("项目日期", 90);
foreach (var p in projects)
{
string bh = p.ContainsKey("项目编号") ? (p["项目编号"] ?? "").ToString() : "";
string mc = p.ContainsKey("项目名称") ? (p["项目名称"] ?? "").ToString() : "";
string kh = p.ContainsKey("客户名称") ? (p["客户名称"] ?? "").ToString() : "";
string rq = p.ContainsKey("项目日期") ? (p["项目日期"] ?? "").ToString() : "";
var item = new ListViewItem(bh);
item.SubItems.Add(mc);
item.SubItems.Add(kh);
item.SubItems.Add(rq);
lv.Items.Add(item);
}
if (lv.Items.Count > 0) lv.Items[0].Selected = true;
dlg.Controls.Add(lv);
var btnOk = new Button
{
Text = "导出选中项",
DialogResult = DialogResult.OK,
Location = new Point(330, 310),
Size = new Size(85, 28),
};
dlg.Controls.Add(btnOk);
var btnCancel = new Button
{
Text = "取消",
DialogResult = DialogResult.Cancel,
Location = new Point(425, 310),
Size = new Size(75, 28),
};
dlg.Controls.Add(btnCancel);
dlg.AcceptButton = btnOk;
dlg.CancelButton = btnCancel;
if (dlg.ShowDialog(this) == DialogResult.OK && lv.SelectedItems.Count > 0)
return lv.SelectedItems[0].Text;
return "";
}
}
catch { return ""; }
}
// ============================================================
// 窗体关闭:回写当前项目的所有改动
// ============================================================
private void Form3_Disposed(object sender, EventArgs e)
{
// ★ 窗体销毁是最后的保存机会:失败重试一次并写日志(原来静默吞掉=丢数据无迹可查)
try { SaveAllToDatabaseSilent(); }
catch (Exception ex1)
{
try
{
System.Threading.Thread.Sleep(200);
SaveAllToDatabaseSilent();
WriteDebugLog("[扒图Cad] 销毁保存第一次失败,重试成功: " + ex1.Message);
}
catch (Exception ex2)
{
WriteDebugLog("[扒图Cad] 销毁保存两次失败,数据可能丢失: " + ex2.Message);
}
}
// ★ 取消 CadBridge 事件订阅,避免事件泄漏
try
{
成套报价软件.CadBridge.EntityErased -= Database_ObjectErased;
成套报价软件.CadBridge.PaPicked -= OnPaTextsPicked;
成套报价软件.CadBridge.F1Requested -= OnF1FromBridge;
成套报价软件.CadBridge.F2Requested -= OnF2FromBridge;
成套报价软件.CadBridge.F3Requested -= OnF3FromBridge;
}
catch { }
}
}
}