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;
using MyApp;
namespace MyAppPdf
{
///
/// Form3 的交互逻辑(CAD 2018 插件报价窗口)。
/// 功能与主项目报价页面一致:项目表 + 图号/箱号/元件三级联动 + 公式重算 + 右键菜单 + 项目导出。
/// UI 布局定义见 Form3.ui.cs。数据库独立存放在 cadcj\sjk\ 目录。
///
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";
///
/// 插件所在目录(cadcj)。优先从注册表读取 CAD 自动加载的 DLL 路径(最可靠),
/// 然后尝试 CodeBase / Location,最后兜底 BaseDirectory(可能是 CAD 目录,会报错)。
///
private static readonly string _baseDir = GetPluginBaseDir();
private static string GetPluginBaseDir()
{
// ★ 方案0(2026-09-10 主程序托管版):数据根固定为 pdfbt 子目录(与独立 pdfbt.exe 共用同一套 sjk)
// 安装布局 {app}\pdfbt\;开发机回退 主程序bin\Debug 上两级\pdfbt\bin\Debug
try
{
var appDir = AppDomain.CurrentDomain.BaseDirectory;
var cand1 = System.IO.Path.Combine(appDir, "pdfbt");
if (System.IO.Directory.Exists(cand1))
{
WriteDebugLog("[GetPluginBaseDir] pdfbt子目录成功: " + cand1);
return cand1;
}
var cand2 = System.IO.Path.GetFullPath(System.IO.Path.Combine(appDir, @"..\..\pdfbt\bin\Debug"));
if (System.IO.Directory.Exists(cand2))
{
WriteDebugLog("[GetPluginBaseDir] 开发机回退成功: " + cand2);
return cand2;
}
}
catch (Exception ex) { WriteDebugLog("[GetPluginBaseDir] 托管目录解析异常: " + ex.Message); }
// ★ 方案1(历史保留):从注册表读取 CAD 自动加载配置的 MODULE 路径
try
{
using (var key = Registry.LocalMachine.OpenSubKey(CadRegPath))
{
if (key != null)
{
var module = key.GetValue("MODULE") as string;
if (!string.IsNullOrEmpty(module))
{
var dir = Path.GetDirectoryName(module);
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
{
WriteDebugLog("[GetPluginBaseDir] 注册表方案成功: " + dir);
return dir;
}
}
}
}
}
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 _xianghaoCache = new Dictionary();
private Dictionary _yuanjianCache = new Dictionary();
// 右键菜单组件(运行时创建,不在设计器 XML 里)
private ContextMenuStrip _menuTuhao;
private ContextMenuStrip _menuXianghao;
private ContextMenuStrip _menuYuanjian;
private static readonly string[] ProjectInfoColumns =
{
"项目编号", "项目名称", "客户名称", "客户地址",
"联系人", "联系电话", "项目日期", "备注",
"成套厂名称", "成套厂地址", "成套厂联系人", "成套厂联系电话",
"单台小数位", "单台取整方式", "项目小数位", "项目取整方式"
};
/// 公式计算列集合(只包含程序算出来的价格列)。
/// ★ 口径(2026-09-29):毛利率(百分数值)入集合获得小数位格式化;
/// 利润率(小数)不入集合,保持原样显示。
private static readonly HashSet s_公式列集合 =
new HashSet(StringComparer.OrdinalIgnoreCase)
{
"表价", "采购单价", "采购价", "报出单价", "报出价",
"单台成本", "成本价", "单台报出", "利润", "毛利率",
"单价", "总价", "成本总价", "毛利",
"成本总价", "报出总价", "利润"
};
/// 构造函数:初始化 UI 并绑定事件。
public Form3()
{
InitializeComponent();
// ★ ADGV 右键多选保持(消息钩子版):右键点已选中的行不清空多选集合。
// 原做法是项目内子类 AdgvKeepMulti 覆写 WndProc——但"设计器+项目内自定义类型"
// 在 SharpDevelop 设计时解析是死结,故改回 DLL 类型+NativeWindow 钩子(语义一致)。
_keepMultiHooks = new System.Collections.Generic.List();
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();
// ★ 工具栏按钮已搬到主窗口功能区(PDF扒图页签),原工具栏隐藏(保留代码可随时恢复)
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 桥:订阅实体删除事件(用户在网页里删框时联动删行)
成套报价软件.PdfBridge.EntityErased -= Database_ObjectErased;
成套报价软件.PdfBridge.EntityErased += Database_ObjectErased;
// 网页 CAD 桥:订阅扒图结果事件(框选完成立即插入表格)
成套报价软件.PdfBridge.PaPicked -= OnPaTextsPicked;
成套报价软件.PdfBridge.PaPicked += OnPaTextsPicked;
// 网页 CAD 桥:F1/F2 元件合并(焦点在网页时按键由网页转发)
成套报价软件.PdfBridge.F1Requested -= OnF1FromBridge;
成套报价软件.PdfBridge.F1Requested += OnF1FromBridge;
成套报价软件.PdfBridge.F2Requested -= OnF2FromBridge;
成套报价软件.PdfBridge.F2Requested += OnF2FromBridge;
成套报价软件.PdfBridge.F3Requested -= OnF3FromBridge;
成套报价软件.PdfBridge.F3Requested += OnF3FromBridge;
}
catch (System.Exception ex)
{
Log("[Form3.Load] 订阅CadBridge事件失败: " + ex.Message);
}
};
Log("[Form3.init] 构造函数全部完成 ✓");
}
catch (System.Exception ex)
{
Log("[Form3.init] ⚠ 初始化异常: " + ex.ToString());
}
}
///
/// ★ 网页 CAD 中实体被删除时触发。如果删除的是 __CADCJ_PA_FRAME 图层上的扒图框,
/// 通过 Handle 查找当前项目下的图号表/箱号表/元件表对应行,同步删除。
///
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);
}
}
///
/// 根据框Handle在当前项目的图号表/箱号表/元件表中删除对应数据行。
///
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();
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 allXhTables = new List();
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 allYjTables = new List();
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();
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;
}
}
/// 重写 OnLoad:在所有 Load 事件订阅者之前确保项目库文件存在。
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 GetAllSelectedRows(DataGridView dgv)
{
var result = new List();
if (dgv == null || dgv.Rows == null || dgv.Rows.Count == 0) return result;
var seenRows = new HashSet();
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 "";
}
}
/// 开启 DataGridView 的双缓冲(消除闪烁)。
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 { }
}
/// 按列名设置只读(运行时按 DataPropertyName/HeaderText 查找)。
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;
}
}
// ============================================================
// 价格显示格式化
// ============================================================
/// 把"保留2位小数"/"保留整数"等转成 .NET 格式字符串(F0/F2)。
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;
}
}
}
/// 老库兼容:把 DataTable 中公式列的类型从 string 转为 double。
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(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 GetProjectTableColumns(System.Data.SQLite.SQLiteConnection conn)
{
var cols = new List();
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();
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);
}
}
/// 老库迁移辅助:把"显示名列"的值复制到"内部ID列"(仅对内部ID为空的行)。
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, "");
}
/// 查索引表:图号+箱号 → 该箱号的元件表真实表名(找不到返回空)。
/// ★ 真隔离(2026-09-29):双键兼容——先按 xianghao 直接查,
/// 未命中则查箱号表把显示名转成 内部ID 再查(向后兼容老数据)。
///
private string GetYuanjianTableByIndex(string projectId, string tuhao, string xianghao)
{
if (string.IsNullOrWhiteSpace(xianghao)) return "";
// 1) 直接查(命中内部ID GUID 或老数据显示名)
string realTbl = LookupTableByIndex(projectId, "元件", tuhao, xianghao);
if (!string.IsNullOrEmpty(realTbl)) return realTbl;
// 2) 显示名 → 内部ID 兜底
string xhIdByName = ResolveXianghaoInternalId(projectId, tuhao, xianghao);
if (!string.IsNullOrEmpty(xhIdByName) && xhIdByName != xianghao)
return LookupTableByIndex(projectId, "元件", tuhao, xhIdByName);
return "";
}
/// ★ 真隔离(2026-09-29):按图号+箱号显示名查箱号表的"内部ID"列。
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 ""; }
}
/// ResolveXianghaoInternalId 复用连接版本。
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);
}
/// 查索引表:图号+箱号 → 真实表名(复用连接版本)。
/// ★ 真隔离(2026-09-29):双键兼容——先按 xianghao 直接查,
/// 未命中则查箱号表把显示名转成 内部ID 再查(向后兼容老数据)。
///
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();
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 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();
var paras = new List();
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 GetProjectInfoFromQuoteDb(string projectId)
{
var info = new Dictionary();
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(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 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(GetProjectTableColumns(conn), StringComparer.OrdinalIgnoreCase);
var colNames = new List();
var paramNames = new List();
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> ReadAllProjectsFromDatabase()
{
var list = new List>();
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(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 ReadProjectMetaData(string pid)
{
var dict = new Dictionary(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 文本选择交互
// ============================================================
// ---------- 主窗口功能区(PDF扒图页签)按钮的调用入口(转发到原工具栏逻辑) ----------
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(); }
///
/// 向 CAD 发送扒数据命令,触发框选交互。
/// ★ 关键:先发 ESC 取消当前可能正在进行的扒图循环,再发新命令
/// 这样用户从"扒图号"循环里点"扒箱号"也能立即切换
///
private void SendCadPaCommand(string cmd)
{
try
{
if (!成套报价软件.PdfBridge.IsViewerReady)
{
MessageBox.Show(this, "尚未打开 PDF 图纸,请先在文档区打开 PDF 文件。",
"扒数据", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 转发到 PDF 查看器(patu/paxh/payj/pacl 模式)
成套报价软件.PdfBridge.SendPaCommand(cmd);
}
catch (Exception ex)
{
MessageBox.Show(this, "发送扒图命令失败:\n" + ex.Message,
"扒数据", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
///
/// CAD 选中文本后的回调:把选中的文本批量插入到对应表格。
/// 由 CadPlugin.ExecutePaCommand 在 UI 线程调用。
///
public void OnPaTextsPicked(PaMode mode, List 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;
}
}
/// 扒图号:把选中文本作为图号批量插入到当前项目,同时保存框Handle。
/// ★ 重复扒图:如果图号已存在,删除旧框和数据库行(不新增)。
private void PaTuhaoInsert(List 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();
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);
}
}
}
/// 扒箱号:把选中文本作为箱号批量插入到当前图号,同时保存框Handle。
/// ★ 重复扒图:如果箱号已存在,删除旧框和数据库行(不新增)。
private void PaXianghaoInsert(List 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();
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
{
// ★ 图纸列=当前打开的 PDF 图纸名(查看器文件名,去扩展名)
InsertXianghao(_currentProjectId, _currentTuhao, clean, r.FrameHandle, r.TextHandle,
成套报价软件.PdfBridge.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);
}
}
}
/// 扒元件:把选中文本作为【规格】批量插入到当前箱号(写到规格列),同时保存框Handle。
/// ★ 关键:在单个事务里按 results 顺序依次插入,保证序号连续。
/// ★ 重复扒图:如果规格已存在,删除旧框+数据库行(不新增)。
private void PaYuanjianInsert(List 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();
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();
// oldFrameHandlesToDelete: 数据库里匹配到的旧框,要删除
var oldFrameHandlesToDelete = new List();
// toInsert: 不重复、需要插入的项
var toInsert = new List();
// ★ 本地匹配失败的规格(收集后交给 DeepSeek AI 兜底识别)
var aiFailedSpecs = new List();
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();
var orderedHandles = new List();
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(orderedHandles);
sortedHandles.Sort(StringComparer.Ordinal);
string mergedTextHandle = string.Join("|", sortedHandles.ToArray());
// ★ 1. 查"同一批文本实体"的合并规格是否已存在
// (文本Handle拼接串相同 = 同一批文本再次被选中,才是真重复)
var matchFrames = new List();
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();
var perTextSpecs = new List();
foreach (var r in cleanResults)
{
List 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 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(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(_pendingSelectYuanjianHandles);
_pendingSelectYuanjianHandles.Clear();
BeginInvoke((Action)(() =>
{
BeginInvoke((Action)(() =>
{
SelectYuanjianRowsByFrameHandles(snapshot);
Log("[PaYuanjianInsert] 已选中 " + snapshot.Count + " 条新元件行(供F1合并)");
}));
}));
}
}
}
/// 查询图号是否已存在(避免重复插入)。
private bool TuhaoExists(string projectId, string tuhao)
{
return !string.IsNullOrEmpty(GetTuhaoFrameHandle(projectId, tuhao));
}
///
/// ★ 按"同一个文本实体"判断重复:查该文本实体Handle是否已入库。
/// 命中返回旧框Handle(真重复);未命中返回 null(不同位置的相同内容文本,正常插入)。
///
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;
}
/// 查询指定图号的框Handle(不存在返回null)。
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;
}
/// 查询箱号是否已存在(避免重复插入)。
private bool XianghaoExists(string projectId, string tuhao, string xianghao)
{
return !string.IsNullOrEmpty(GetXianghaoFrameHandle(projectId, tuhao, xianghao));
}
///
/// ★ 按"同一个文本实体"判断重复:查该文本实体Handle是否已入库。
/// 命中返回旧框Handle(真重复);未命中返回 null(不同位置的相同内容文本,正常插入)。
///
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;
}
/// 查询指定箱号的框Handle(不存在返回null)。
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;
}
/// 查询指定元件规格的所有框Handle(可能有多个重复行,返回List)。
private List GetYuanjianFrameHandles(string projectId, string tuhao, string xianghao, string spec)
{
var list = new List();
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;
}
///
/// ★ 按"同一个文本实体"判断重复:查该文本实体Handle在元件表中是否已入库。
/// 命中返回旧框Handle列表(真重复);未命中返回空列表(不同位置的相同内容文本,正常插入)。
///
private List GetYuanjianFrameHandlesByTextHandle(string projectId, string tuhao, string xianghao, string spec, string textHandle)
{
var list = new List();
// 旧数据没有文本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)
// ============================================================
/// 用户库条目:规格 → 元件名称
private class SpecNameEntry
{
public string Name;
public string NormSpec; // 归一化后的规格
public string[] Numbers; // 规格中的数字序列(模糊匹配保护用)
public string Skeleton; // 字母骨架(去数字/分隔符/尾部单位,如 DZ47-4P/C80A → DZPC)
}
private static List _specNameLib = null;
private static string _specLibCsvPath = null;
private static DateTime _specLibCsvTime = DateTime.MinValue;
///
/// 确保用户库CSV已加载(取最新修改时间的 用户库_*.csv,
/// 文件时间戳变化时自动重新加载;文件被 Excel 占用时用共享读)。
///
private static void EnsureUserLibCsvLoaded()
{
try
{
// 候选目录:上级目录(成套报价软件根目录) + 插件目录自身
var dirs = new List();
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();
var prefixRules = new List();
// ★ 共享读:文件被 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); }
}
/// 检测CSV编码:有UTF-8 BOM用UTF8,否则按GBK(主程序导出编码)。
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");
}
/// 按CSV规则拆分一行(支持双引号包裹和转义"")。
private static string[] SplitCsvLine(string line)
{
var parts = new List();
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();
}
/// 规格归一化:全角→半角、统一大写、去空白和分隔符号(保留字母/数字/中文)。
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();
}
/// 规格中的分隔符号(归一化时删除;数字提取时作为数字段边界)。
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 == '~';
}
///
/// 轻量归一化(保留分隔符和空格):全角→半角、统一大写。
/// ★ 用于数字序列提取:分隔符和空格是数字段的天然边界,
/// 先删分隔符会导致 HYM1-63/3300 的 63 和 3300 粘连成 633300;
/// 删空格会导致 3300 800 粘连成 3300800(与 3300,800 数字序列不一致)。
///
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();
}
///
/// 提取规格中的所有数字序列(在保留分隔符的版本上提取,
/// 如 HYM1-63/3300 63A → ["1","63","3300","63"])。
///
private static string[] ExtractNumbers(string spec)
{
var list = new List();
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();
}
/// 两个数字序列是否完全一致。
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;
}
///
/// 数字序列兼容(子序列检查):短方的数字按顺序都能在长方中找到即通过。
/// ★ 用于 L2 包含关系:一方文本是另一方的一部分,数字自然也可能少
/// (如图纸只写 HD13BX-1500/31,库里是 HD13BX-1500/31 1500A)。
/// ★ 依然防误配:63A vs 100A 时 63 不在对方序列对应位置,拒绝。
///
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;
}
///
/// 数字序列严格相等 + 两种容错(L3 模糊匹配用):
/// 1.粘连拆分:图纸连写导致数字粘连(如 0.66+1500 提取出 661500),拆成两段后对上即通过;
/// 2.壳架容错:图纸把额定电流写进型号段(如 HYW1-2500/3P 2500A,2500出现两次),
/// 库里型号段是壳架等级(HYW1-3200),删掉图纸型号段那个数字后能对上即通过。
/// ★ 仅当该数字在图纸中出现≥2次才允许删,220V/380V 这类无重复差异依然拒绝。
///
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;
}
///
/// 壳架容错:seq 中存在出现≥2次的数字 X,删掉其中一次后作为子序列能对上 other。
/// 典型:图纸 HYW1-2500/3P 2500A(2500×2) vs 库 HYW1-3200/3P 2500A(3200×1,2500×1)。
///
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(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;
}
/// 把 seq 中某个数字串拆成两段,拆分后作为子序列能对上 other 即返回 true。
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(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;
}
///
/// 提取字母骨架:去数字、去分隔符、去尾部单位词(A/V/kA 等)。
/// ★ 用于图纸省略壳架等级的场景:DZ47-4P/C80A 与 DZ47-100 4P C80
/// 骨架同为 DZPC,配合数字子序列保护可安全匹配。
///
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();
}
/// 判断剩余部分是否为纯尾部单位词(如 "A"、"80A" 里的 A、"0~450V" 里的 V)。
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";
}
/// Levenshtein 编辑距离(滚动数组实现)。
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];
}
///
/// 规格匹配元件名称(模糊):
/// L1 归一化后完全相等 → 直接命中;
/// L2 包含关系(短方长度≥长方60%,数字序列一致);
/// L3 编辑距离相似度≥0.70(数字序列必须完全一致,唯一最高分才命中)。
/// 都不中返回 null(不硬猜)。
///
///
/// 粘连清洗:扒图可能把相邻文本粘连(如 "0mAVigiC65N-...")。
/// 若文本开头是"数字+单位(mA/A/V/W)"且后面紧跟大写字母开头的型号,
/// 从型号处截取;否则原样返回。
///
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 _specMatchCache =
new System.Collections.Generic.Dictionary(System.StringComparer.Ordinal);
private static readonly object _specMatchCacheLock = new object();
/// 规格匹配元件名称(带结果缓存):相同规格文本第二次起直接返回缓存结果。
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"); } }
/// DeepSeek 设置(加载/保存 JSON)
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); }
}
/// AI 匹配缓存:规格 → 元件名称(未命中/失败也缓存,避免反复请求)
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); }
}
/// 极简 JSON 解析(扁平 key:value 结构,支持字符串/数字/布尔值)
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 _prefixRules = new List();
/// 内置最小规则集(仅当用户库无任何前缀行时兜底)。
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", "控制变压器" },
};
///
/// ★ 国标类组代号解析:按 JB/T 2930 定义,取型号前2位字母查类组表。
/// 仅当:①前缀规则未命中 ②开头≥2位字母 ③类组表命中 时返回名称。
/// 数字开头/单字母/进口品牌型号不适用(返回 null 交给后续层)。
///
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;
}
///
/// 按型号前缀规则查元器件名称。
/// ★ 规则来自用户库 CSV"元件类型=前缀"的行(EnsureUserLibCsvLoaded 时提取),
/// 规则前缀直接与规格开头比对(支持字母+数字混合前缀,如 DZ47/CDM3/CJX2),
/// 最长前缀优先(7→2字符)。
/// 返回 null 表示无规则。
///
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;
}
/// 打开 DeepSeek 设置窗口(运行时创建,不改 ui.cs)
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);
}
///
/// 调 DeepSeek API 批量识别规格对应的元器件名称。
/// 返回 规格→名称 的哈希表(失败的规格不在结果里)。
///
private System.Collections.Hashtable CallDeepSeekMatch(List 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;
}
///
/// 从 API 响应中提取模型输出文本。
/// 兼容两种格式:
/// · chat/completions: choices[0].message.content("content":"...")
/// · responses API: output[].content[].text("text":"...")或顶层"output_text"
///
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; }
}
/// 从 start 位置读取一个 JSON 转义字符串(处理 \n \" \\ \uXXXX)。
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();
}
/// 估算 start 位置转义字符串的长度(到未转义引号为止)。
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();
}
///
/// 扒元件插入后:对本地匹配失败的规格异步调用 DeepSeek 补名称,
/// 完成后回填数据库 + 刷新表格(UI 线程)。
/// ★ 单飞+排队:上一个 AI 批次未完成时,后续规格累积到待办队列,
/// 完成后一并提交(防并发互踩/请求堆积)。
///
private static readonly object _aiLock = new object();
private static bool _aiRunning = false;
private static List _aiPendingSpecs = new List();
private static string _aiPendingTbl = null;
private void AiFillMissingNames(List 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);
}
/// 执行一个 AI 批次(完成后处理待办队列)。
private void RunAiBatch(List specs, string tblName)
{
var bgw = new System.ComponentModel.BackgroundWorker();
bgw.DoWork += (s, e) =>
{
try
{
// 先查缓存,未缓存的才提交 AI
var cache = LoadDsCache();
var toAsk = new List();
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 pending;
string pendingTbl;
lock (_aiLock)
{
pending = new List(_aiPendingSpecs);
pendingTbl = _aiPendingTbl;
_aiPendingSpecs = new List();
_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();
}
/// 启动下一个 AI 批次(带单飞保护)。
private void StartNextAiBatch(List 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);
}
///
/// 把 AI 识别结果(规格→名称)追加到最新用户库 CSV(GBK 编码),
/// 并同步更新内存库 _specNameLib(当前会话立即生效)。
/// 已存在的规格跳过;每条占一行:名称,规格,只,1,0,1,1,AI识别,0,元件
///
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();
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(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();
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); }
}
/// 根据项目ID和图号获取箱号表名(查"表索引"表)。
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 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 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();
}
}
}
/// 扒元件专用:把文本写到【规格】列(元件名称留空),同时写入框Handle。
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 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();
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(table.Columns.Count);
foreach (DataColumn c in table.Columns)
if (c.ColumnName != "_rowid_") colNamesU.Add(c.ColumnName);
var setList = new List(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(table.Columns.Count);
foreach (DataColumn c in table.Columns)
if (c.ColumnName != "_rowid_") colNamesI.Add(c.ColumnName);
var insParam = new List(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(table.Columns.Count);
foreach (DataColumn c in table.Columns) colNames.Add(c.ColumnName);
var paramList = new List(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 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();
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();
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; }
}
/// 图号/箱号重名自动后缀:值与 DataTable 其他行(除 自身行 对应行)重复时返回 "名(2)"/"名(3)"…。
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 新值;
}
/// CellEndEdit:把公式列的字符串值转为 double,让 DefaultCellStyle.Format 生效。
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);
}
}
/// 单格实时落库:按内存表该行的 _rowid_ 对库单行 UPDATE(毫秒级,不整表回写)。
/// 无 _rowid_(新插入未回填)的行跳过——由切层/关闭时的整表保存兜底。
/// 派生列(采购单价/采购价/报出单价/报出价)跳过——重算链会连同主列一起写入。
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);
// ★ 切箱号 → 显示该箱号的组虚线框(箱号图框+全部元件图框圈在一起)
显示箱号组虚线框();
}
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)
显示箱号组虚线框(handle);
else if (dgv == this.advancedDataGridView9)
成套报价软件.PdfBridge.ClearGroupFrames();
}
///
/// 根据框 Handle 字符串定位网页 CAD 视图:缩放居中到框的位置并高亮。
/// Handle 是扒图时绘制的彩色矩形框的句柄(十六进制字符串),
/// 与网页端实体 objectId 一致,通过 CadBridge 转发给网页 CAD 执行。
///
private static void ZoomToFrameHandle(string handle)
{
if (string.IsNullOrWhiteSpace(handle)) return;
try
{
成套报价软件.PdfBridge.ZoomToFrameHandle(handle);
}
catch { }
}
// ============================================================
// ★ 箱号组虚线框:点箱号表/元件表行 → 在 PDF 上画一个橙色虚线矩形,
// 把该箱号的箱号图框 + 它元件表全部元件图框 按页求并集圈在一起
// ============================================================
/// 显示当前 图号+箱号 的组虚线框(查库收集全部框Handle)。
/// focusHandle=点中的那个框:查看器会翻到它所在页并把缩放适配到整组框大小。
private void 显示箱号组虚线框(string focusHandle = null)
{
try
{
if (string.IsNullOrWhiteSpace(_currentProjectId) ||
string.IsNullOrWhiteSpace(_currentTuhao) ||
string.IsNullOrWhiteSpace(_currentXianghao)) return;
var handles = new System.Collections.Generic.List();
// 箱号表该行的框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)
成套报价软件.PdfBridge.SetGroupFrames(handles, focusHandle);
}
catch (Exception exGrp) { Log("组虚线框显示失败(忽略): " + exGrp.Message); }
}
/// 从指定元件/箱号表收集框Handle:matchCol/matchVal 非空时只收匹配行,否则收全部非空行。
private void CollectFrameHandles(string realTbl, string matchCol, string matchVal,
System.Collections.Generic.List 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); }
}
///
/// ★ 网页 CAD 会话内的扒图框由网页端持有,无需像 AutoCAD 版那样
/// 定期 SaveAs 整个 DWG(原为防止 CAD 关闭时框丢失)。保留空实现
/// 以兼容调用点。
///
private static DateTime _lastDwgSaveTime = DateTime.MinValue;
// ★ 扒元件后待选中的行(框Handle),finally 延迟执行选中
private List _pendingSelectYuanjianHandles = new List();
private static void SaveCurrentDwg()
{
// 网页版:扒图框保存在网页 CAD 会话中,无需保存 DWG 文件。
}
///
/// ★ 删除指定 Handle 对应的 CAD 框实体(单个,转发到批量方法)。
///
private void DeleteFrameByHandle(string handle)
{
DeleteFramesByHandles(new string[] { handle });
}
///
/// ★ 批量删除多个 Handle 对应的框:打包成一条消息发给网页 CAD,
/// 网页端逐个 erase 后只 regen 一次(保持原版"批量一次刷新"的优化)。
///
private void DeleteFramesByHandles(System.Collections.Generic.IEnumerable handles)
{
var list = new List();
if (handles != null)
foreach (string h in handles)
if (!string.IsNullOrWhiteSpace(h)) list.Add(h);
if (list.Count == 0) { Log("[DeleteFramesByHandles] 无有效 handle,跳过"); return; }
try
{
成套报价软件.PdfBridge.DeleteFramesByHandles(list);
Log("[DeleteFramesByHandles] 已发送网页删除请求: 请求=" + list.Count);
}
catch (System.Exception ex) { Log("[DeleteFramesByHandles] 异常: " + ex.Message); }
}
///
/// ★ 收集指定图号下所有箱号表和元件表的框Handle(级联删框用)。
/// 不包含图号表自身的框(图号行框由调用方从内存表收集)。
///
private void CollectFrameHandlesForTuhao(string projectId, string tuhao, List 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); }
}
///
/// ★ 收集指定箱号下元件表的框Handle(级联删框用)。
/// 不包含箱号表自身的框(箱号行框由调用方从内存表收集)。
///
private void CollectFrameHandlesForXianghao(string projectId, string tuhao, string xianghao, List 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); }
}
/// 检查表是否存在(SQLite)。
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;
/// F1:元件两两配对合并;F2:元件三个一组配对合并。
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;
/// 统一 F1 入口(宿主按键/网页转发共用)。
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();
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();
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();
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(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);
// ★ 图纸列=当前打开的 PDF 图纸名(查看器开着文档才有值,去扩展名)
InsertXianghao(_currentProjectId, _currentTuhao, newXh, "", "",
成套报价软件.PdfBridge.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();
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();
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(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();
var frameHandles = new List();
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(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(合并后是一个元件,数量不变)
// · 其他列(单位/系数/价格):取第一行
// · 删除被合并的其他行,重新编号,保存刷新
// ============================================================
/// 按框Handle集合选中元件表中的行(多选,滚动到第一行)。
private void SelectYuanjianRowsByFrameHandles(List 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); }
}
/// 网页转发的 F1:转到统一入口(焦点在网页时 WinForms 收不到按键)。
private void OnF1FromBridge()
{
try { RunF1Merge(); }
catch (Exception ex) { Log("[F1桥接] " + ex.Message); }
}
/// 网页转发的 F2:三个一组配对合并。
private void OnF2FromBridge()
{
try { MergeYuanjianByGroup(3); }
catch (Exception ex) { Log("[F2桥接] " + ex.Message); }
}
/// 网页转发的 F3:四个一组配对合并。
private void OnF3FromBridge()
{
try { MergeYuanjianByGroup(4); }
catch (Exception ex) { Log("[F3桥接] " + ex.Message); }
}
///
/// F1:合并元件表中相邻的两个元件(按序号顺序)。
/// · 选中 ≥2 行 → 合并前两行
/// · 选中 1 行 → 与它的下一行合并
/// · 未选中 → 合并第 1、2 行
/// 合并时网页 CAD 里的蒙版同步合并(保留行蒙版扩大到并集)。
///
private void MergeAdjacentYuanjianByF1()
{
MergeYuanjianByGroup(2);
}
/// 按 groupSize 分组合并选中元件(F1=两两,F2=三个一组)。
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();
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();
var groupHandles = new List();
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(groupHandles).Count != groupSize) continue;
MergeYuanjianRows(groupRows);
merged++;
}
Log("[F1批量] " + groupSize + " 个一组配对合并完成: " + merged + "/" + groups + " 组");
}
/// 读行的框Handle:优先 DataRow("框Handle"可能不在显示列,Cells 读不到)。
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");
}
/// 按框Handle在元件表中找行(找不到返回 null)。
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;
}
/// 合并指定行(按序号排序):第一行为合并目标,其余删除;蒙版同步合并。
private void MergeYuanjianRows(List 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();
var specs = new List();
var drawingSpecs = new List();
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();
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();
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)
{
成套报价软件.PdfBridge.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 { 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();
var specs = new List();
var drawingSpecs = new List();
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();
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();
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);
}
}
/// 弹出新建项目窗口,收集项目信息 + 价格格式化配置。
private Dictionary 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(主程序托管后与 assembly 同级;原上级目录查找保留作回退)
string mainExePath = null;
try
{
string pluginDir = Path.GetDirectoryName(
new System.Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath);
// 先试同级(主程序托管布局:exe 与 assembly 同目录)
string candidate = Path.Combine(pluginDir, "成套报价软件.exe");
if (File.Exists(candidate)) mainExePath = candidate;
// 回退:原独立布局(assembly 目录的上级)
if (string.IsNullOrEmpty(mainExePath))
{
string parentDir = Path.GetFullPath(Path.Combine(pluginDir, ".."));
string candidate2 = Path.Combine(parentDir, "成套报价软件.exe");
if (File.Exists(candidate2)) mainExePath = candidate2;
}
}
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 _keepMultiHooks;
private void HookKeepMulti(System.Windows.Forms.DataGridView dgv)
{
if (dgv == null) return;
var hook = new AdgvMsgHook(dgv);
_keepMultiHooks.Add(hook); // 存字段防 GC
}
///
/// ADGV 右键多选保持钩子(NativeWindow):吞掉"右键点已选中行"的 WM_RBUTTONDOWN,
/// 多选集合不被清空;右键菜单由 WM_CONTEXTMENU 正常弹出(与原 AdgvKeepMulti 子类语义一致)。
///
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);
}
}
/// 用 SQL ATTACH 复制 SQLite 库,兜底 File.Copy。
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>();
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(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);
}
/// 弹出一个简单对话框,让用户从项目列表中选一个项目,返回项目编号。
private string PickProjectDialog(List> 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("[扒图Pdf] 销毁保存第一次失败,重试成功: " + ex1.Message);
}
catch (Exception ex2)
{
WriteDebugLog("[扒图Pdf] 销毁保存两次失败,数据可能丢失: " + ex2.Message);
}
}
// ★ 取消 CadBridge 事件订阅,避免事件泄漏
try
{
成套报价软件.PdfBridge.EntityErased -= Database_ObjectErased;
成套报价软件.PdfBridge.PaPicked -= OnPaTextsPicked;
成套报价软件.PdfBridge.F1Requested -= OnF1FromBridge;
成套报价软件.PdfBridge.F2Requested -= OnF2FromBridge;
成套报价软件.PdfBridge.F3Requested -= OnF3FromBridge;
}
catch { }
}
}
}