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