2129 lines
105 KiB
C#
2129 lines
105 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Diagnostics;
|
||
using System.Drawing;
|
||
using System.IO;
|
||
using System.IO.Compression;
|
||
using System.Windows.Forms;
|
||
using Microsoft.Win32;
|
||
using Smart.CustomComponents;
|
||
|
||
namespace 成套报价软件
|
||
{
|
||
/// <summary>主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。</summary>
|
||
public partial class MainForm
|
||
{
|
||
|
||
// ============================================================
|
||
// 母排页铜价刷新:从长江有色网(ccmn.cn)抓取 1#铜 均价和发布日期
|
||
// 数据接口:https://www.ccmn.cn/shop/historyData/getCjysAndCjysw
|
||
// 返回 JSON 的 body.showPriceList 是 AES-ECB(PKCS7) 加密的 Base64,
|
||
// 密钥=固定字符串 ccmnCjysw1455881(与首页 ccmn-index.js 一致)。
|
||
// 解密后是 [[{productSortName,avgPrice,publishDate,...},...],...],
|
||
// 找 productSortName=="1#铜" 取 avgPrice 和 publishDate。
|
||
// ============================================================
|
||
|
||
/// <summary>铜价数据接口地址。</summary>
|
||
private const string 铜价接口地址 = "https://www.ccmn.cn/shop/historyData/getCjysAndCjysw";
|
||
|
||
/// <summary>首页 JS 里写死的 AES 密钥(ccmn-index.js: keys = "ccmnCjysw1455881")。</summary>
|
||
private const string 铜价AES密钥 = "ccmnCjysw1455881";
|
||
|
||
/// <summary>btn刷新 点击:异步抓取 1#铜 或 A00铝 均价和日期,填入 txt当前铜价 / txt更新日期。
|
||
/// 根据 rb铝排.Checked 决定抓取目标:铜排→1#铜,铝排→A00铝。</summary>
|
||
private async void btn刷新_Click(object sender, EventArgs e)
|
||
{
|
||
this.btn刷新.Enabled = false;
|
||
string 原文本 = this.btn刷新.Text;
|
||
this.btn刷新.Text = "刷新中…";
|
||
// 当前材质:铜排→"1#铜",铝排→"A00铝"
|
||
bool 是铝排 = (this.rb铝排 != null && this.rb铝排.Checked);
|
||
string 目标品种 = 是铝排 ? "A00铝" : "1#铜";
|
||
string 日志标签 = 是铝排 ? "[铝价刷新]" : "[铜价刷新]";
|
||
try
|
||
{
|
||
// ccmn.cn 强制 TLS 1.2,.NET 4.8 默认 SecurityProtocol 不含 Tls12,
|
||
// 不显式设置会报"请求被中止:未能创建 SSL/TLS 安全通道"。
|
||
System.Net.ServicePointManager.SecurityProtocol =
|
||
(System.Net.SecurityProtocolType)3072; // Tls12 = 0xC00 = 3072
|
||
|
||
string json;
|
||
using (var wc = new System.Net.WebClient())
|
||
{
|
||
wc.Encoding = System.Text.Encoding.UTF8;
|
||
wc.Headers[System.Net.HttpRequestHeader.UserAgent] =
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
||
wc.Headers[System.Net.HttpRequestHeader.Referer] = "https://www.ccmn.cn/";
|
||
json = await wc.DownloadStringTaskAsync(铜价接口地址);
|
||
}
|
||
|
||
string showPriceList = 取JSON字段(json, "showPriceList");
|
||
if (string.IsNullOrEmpty(showPriceList))
|
||
{
|
||
Log(日志标签 + " 未取到 showPriceList");
|
||
MessageBox.Show(this, "未获取到" + 目标品种 + "数据", "刷新失败",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
string plain = 铜价AES解密(showPriceList);
|
||
|
||
// 解密后是 [[{...},{...}],[{...}]] 嵌套数组,
|
||
// 用正则逐个对象提取(对象内部无嵌套大括号,可安全用 \{[^}]*\})
|
||
var 对象匹配 = System.Text.RegularExpressions.Regex.Matches(plain, @"\{[^}]*\}");
|
||
bool 找到 = false;
|
||
foreach (System.Text.RegularExpressions.Match m in 对象匹配)
|
||
{
|
||
string obj = m.Value;
|
||
string 名称 = 取JSON字段(obj, "productSortName");
|
||
if (名称 != 目标品种) continue;
|
||
|
||
// avgPrice 是数字,单独用正则取
|
||
var 均价匹配 = System.Text.RegularExpressions.Regex.Match(obj,
|
||
@"""avgPrice""\s*:\s*([0-9]+\.?[0-9]*)");
|
||
string 均价文本 = 均价匹配.Success ? 均价匹配.Groups[1].Value : "";
|
||
string 日期 = 取JSON字段(obj, "publishDate");
|
||
|
||
// 均价转整数(106150.00 → 106150)
|
||
decimal 均价值;
|
||
if (decimal.TryParse(均价文本, out 均价值))
|
||
{
|
||
均价文本 = ((long)均价值).ToString();
|
||
}
|
||
|
||
this.txt当前铜价.Text = 均价文本;
|
||
this.txt更新日期.Text = 日期;
|
||
// ★ 价格缓存到对应材质的配置文件:调母排窗口直接读,不再发网络请求
|
||
Save母排价格缓存(是铝排, 均价文本, 日期);
|
||
Log(日志标签 + " 成功: " + 目标品种 + " 均价=" + 均价文本 + " 日期=" + 日期);
|
||
找到 = true;
|
||
break;
|
||
}
|
||
|
||
if (!找到)
|
||
{
|
||
Log(日志标签 + " 未找到 " + 目标品种);
|
||
MessageBox.Show(this, "未找到 " + 目标品种 + " 的价格数据", "刷新失败",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log(日志标签 + " 异常: " + ex.Message);
|
||
MessageBox.Show(this, "刷新失败: " + ex.Message, 目标品种 + "刷新",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
finally
|
||
{
|
||
this.btn刷新.Enabled = true;
|
||
this.btn刷新.Text = 原文本;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用 AES-ECB(PKCS7) 解密 ccmn 接口返回的 Base64 密文。
|
||
/// 密钥见 铜价AES密钥 常量(与首页 ccmn-index.js 的 decryptAES 一致)。
|
||
/// </summary>
|
||
private static string 铜价AES解密(string 加密Base64)
|
||
{
|
||
byte[] 密钥 = System.Text.Encoding.UTF8.GetBytes(铜价AES密钥);
|
||
byte[] 密文 = System.Convert.FromBase64String(加密Base64);
|
||
using (var aes = System.Security.Cryptography.Aes.Create())
|
||
{
|
||
aes.Mode = System.Security.Cryptography.CipherMode.ECB;
|
||
aes.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
|
||
aes.Key = 密钥;
|
||
using (var 解密器 = aes.CreateDecryptor())
|
||
{
|
||
byte[] 明文 = 解密器.TransformFinalBlock(密文, 0, 密文.Length);
|
||
return System.Text.Encoding.UTF8.GetString(明文);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 铜排参数(加工费等)持久化 —— 配置文件 shezhi\铜排参数.config.xml
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 重算 txt铜价(元/Kg) = (txt当前铜价 + txt加工费) / 1000。
|
||
/// 当前铜价、加工费单位均为 元/吨,1 吨 = 1000 公斤,所以除以 1000 得 元/Kg。
|
||
/// 任一输入为空或非数字时不写入(保留 txt铜价 原值,避免覆盖用户手输)。
|
||
/// 结果四舍五入取整(不显示小数点)。
|
||
/// </summary>
|
||
private void Recalc铜价PerKg()
|
||
{
|
||
decimal 铜价吨;
|
||
if (!decimal.TryParse((this.txt当前铜价.Text ?? "").Trim(), out 铜价吨)) return;
|
||
|
||
decimal 加工费吨;
|
||
if (!decimal.TryParse((this.txt加工费.Text ?? "").Trim(), out 加工费吨)) return;
|
||
|
||
decimal 铜价Kg = (铜价吨 + 加工费吨) / 1000m;
|
||
this.txt铜价.Text = Math.Round(铜价Kg, 0, MidpointRounding.AwayFromZero).ToString("0");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重算 txt单价(元/米) 和 txt重量(kg/米)。
|
||
/// 每米重量 = 层数 × 宽度(mm) × 厚度(mm) × 密度(g/cm³) / 1000
|
||
/// 铜排 TMY:密度 8.9 g/cm³
|
||
/// 铝排 LMY:密度 2.7 g/cm³
|
||
/// 推导:1 米长度对应 1000mm,截面积=宽×厚 mm²,体积=宽×厚×1000 mm³=宽×厚 cm³,
|
||
/// 重量=宽×厚×密度 g = 宽×厚×密度/1000 kg,再乘层数。
|
||
/// txt重量 = 每米重量(保留 3 位小数,如 0.445 kg/m)。
|
||
/// txt单价 = 铜价(元/Kg) × 每米重量(四舍五入保留 2 位小数)。
|
||
/// 任一输入为空/非数字时不写入(保留原值)。
|
||
/// </summary>
|
||
private void Recalc单价PerMeter()
|
||
{
|
||
decimal 铜价Kg;
|
||
if (!decimal.TryParse((this.txt铜价.Text ?? "").Trim(), out 铜价Kg)) return;
|
||
|
||
decimal 层数;
|
||
if (!decimal.TryParse((this.txt层数.Text ?? "").Trim(), out 层数)) return;
|
||
|
||
decimal 宽度;
|
||
if (!decimal.TryParse((this.txt宽度.Text ?? "").Trim(), out 宽度)) return;
|
||
|
||
decimal 厚度;
|
||
if (!decimal.TryParse((this.txt厚度.Text ?? "").Trim(), out 厚度)) return;
|
||
|
||
// 材质密度:rb铜排(默认)= 8.9,rb铝排 = 2.7
|
||
// CheckedChanged 会在切换瞬间旧/新按钮都触发一次,以当前选中的为准
|
||
decimal 密度 = (this.rb铝排 != null && this.rb铝排.Checked) ? 2.7m : 8.9m;
|
||
|
||
// 每米重量 = 层数 × 宽(mm) × 厚(mm) × 密度 / 1000 (kg/m)
|
||
decimal 每米重量 = 层数 * 宽度 * 厚度 * 密度 / 1000m;
|
||
|
||
// txt重量:每米重量保留 2 位小数(铜排小规格每米零点几 kg,2 位够用)
|
||
this.txt重量.Text = Math.Round(每米重量, 2, MidpointRounding.AwayFromZero).ToString("0.##");
|
||
|
||
// txt单价:每米单价四舍五入保留 2 位小数(用户要求不再取整)
|
||
decimal 单价每米 = 铜价Kg * 每米重量;
|
||
this.txt单价.Text = Math.Round(单价每米, 2, MidpointRounding.AwayFromZero).ToString("0.00");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 材质切换(铜排↔铝排)时统一处理:
|
||
/// 1. 切换 lbl当前铜价 标签文本(当前铜价↔当前铝价)
|
||
/// 2. 重新加载 advancedDataGridView8 规格表数据(铜排库/铝排库)
|
||
/// 3. 清空价格/规格/层宽厚等文本框(避免残留旧材质数据)
|
||
/// 4. 加载对应材质的加工费参数
|
||
/// 5. 自动刷新价格(铜价/铝价)
|
||
/// </summary>
|
||
private void 切换母排材质()
|
||
{
|
||
bool 是铝排 = (this.rb铝排 != null && this.rb铝排.Checked);
|
||
|
||
// 1. 切换标签文本
|
||
this.lbl当前铜价.Text = 是铝排 ? "当前铝价(元/吨)" : "当前铜价(元/吨)";
|
||
|
||
// 2. 重新加载规格表(铜排库/铝排库)
|
||
// ★ 不再用 ui.cs 的 LoadSqliteData_advancedDataGridView8()——
|
||
// 它写死了铜排库路径(advancedDataGridView8Table.db),切铝排也不换库,
|
||
// 表格里永远显示 TMY 数据。这里按材质选库自己加载。
|
||
try { Load铜排规格表(是铝排); }
|
||
catch (Exception ex) { Log("[材质切换] 加载规格表失败: " + ex.Message); }
|
||
|
||
// 3. 清空相关文本框(价格/日期/规格/层宽厚/重量/单价)
|
||
this.txt当前铜价.Text = "";
|
||
this.txt更新日期.Text = "";
|
||
this.txt铜价.Text = "";
|
||
this.txt规格.Text = "";
|
||
this.txt层数.Text = "";
|
||
this.txt宽度.Text = "";
|
||
this.txt厚度.Text = "";
|
||
this.txt重量.Text = "";
|
||
this.txt单价.Text = "";
|
||
|
||
// 4. 加载对应材质的加工费参数
|
||
try { Load铜排参数(); }
|
||
catch (Exception ex) { Log("[材质切换] 加载参数失败: " + ex.Message); }
|
||
|
||
// 5. 自动刷新价格(铜价/铝价)——异步,不阻塞 UI
|
||
try { this.btn刷新_Click(this, EventArgs.Empty); }
|
||
catch (Exception ex) { Log("[材质切换] 刷新价格失败: " + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按材质加载铜排规格表(advancedDataGridView8):
|
||
/// 铜排 → sjk\advancedDataGridView8Table.db (TMY 数据)
|
||
/// 铝排 → sjk\advancedDataGridView8Table_LMY.db (LMY 数据)
|
||
/// 与 ui.cs 的 LoadSqliteData_advancedDataGridView8 同绑定方式
|
||
/// (AutoGenerateColumns=false + DataSource),只是路径按材质选。
|
||
/// </summary>
|
||
private void Load铜排规格表(bool 是铝排)
|
||
{
|
||
string 文件名 = 是铝排 ? "advancedDataGridView8Table_LMY.db" : "advancedDataGridView8Table.db";
|
||
string dbPath = System.IO.Path.Combine(
|
||
System.AppDomain.CurrentDomain.BaseDirectory, @"sjk\" + 文件名);
|
||
if (!System.IO.File.Exists(dbPath))
|
||
{
|
||
MessageBox.Show(this, "找不到数据库文件: " + dbPath, "铜排规格表",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
var table = new DataTable();
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection("Data Source=" + dbPath + ";Version=3;"))
|
||
{
|
||
conn.Open();
|
||
using (var adapter = new System.Data.SQLite.SQLiteDataAdapter(
|
||
"SELECT * FROM advancedDataGridView8Table", conn))
|
||
{
|
||
adapter.Fill(table);
|
||
}
|
||
}
|
||
|
||
this.advancedDataGridView8.AutoGenerateColumns = false;
|
||
this.advancedDataGridView8.DataSource = table;
|
||
Log("[材质切换] 规格表已加载: " + 文件名 + " 行数=" + table.Rows.Count);
|
||
}
|
||
|
||
/// <summary>母排参数配置文件路径(铜排/铝排各自独立,exe 同目录的 shezhi 子目录)。
|
||
/// 铜排→铜排参数.config.xml,铝排→铝排参数.config.xml。</summary>
|
||
private string Get母排参数配置路径()
|
||
{
|
||
bool 是铝排 = (this.rb铝排 != null && this.rb铝排.Checked);
|
||
string 文件名 = 是铝排 ? "铝排参数.config.xml" : "铜排参数.config.xml";
|
||
return System.IO.Path.Combine(
|
||
System.AppDomain.CurrentDomain.BaseDirectory, @"shezhi\" + 文件名);
|
||
}
|
||
|
||
/// <summary>最近打开的项目配置文件路径(记录最后一次进入的项目编号)。</summary>
|
||
private static readonly string 最近项目配置路径 = System.IO.Path.Combine(
|
||
System.AppDomain.CurrentDomain.BaseDirectory, @"shezhi\最近项目.config.xml");
|
||
|
||
/// <summary>把项目编号保存为"最近打开的项目"。</summary>
|
||
private void SaveLastProject(string pid)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(pid)) return;
|
||
try
|
||
{
|
||
string dir = System.IO.Path.GetDirectoryName(最近项目配置路径);
|
||
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
|
||
var doc = new System.Xml.XmlDocument();
|
||
doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null));
|
||
var root = doc.CreateElement("最近项目");
|
||
doc.AppendChild(root);
|
||
var node = doc.CreateElement("项目编号");
|
||
node.InnerText = pid;
|
||
root.AppendChild(node);
|
||
doc.Save(最近项目配置路径);
|
||
}
|
||
catch (Exception ex) { Log("[最近项目] 保存失败: " + ex.Message); }
|
||
}
|
||
|
||
/// <summary>读取最近打开的项目编号;没有返回空串。</summary>
|
||
private static string LoadLastProject()
|
||
{
|
||
try
|
||
{
|
||
if (!System.IO.File.Exists(最近项目配置路径)) return "";
|
||
var doc = new System.Xml.XmlDocument();
|
||
doc.Load(最近项目配置路径);
|
||
var node = doc.SelectSingleNode("/最近项目/项目编号");
|
||
return node == null ? "" : (node.InnerText ?? "").Trim();
|
||
}
|
||
catch { return ""; }
|
||
}
|
||
|
||
/// <summary>从配置文件加载母排参数(目前:加工费),填入对应文本框。
|
||
/// 铜排/铝排各有独立配置文件,由 Get母排参数配置路径() 决定。</summary>
|
||
private void Load铜排参数()
|
||
{
|
||
try
|
||
{
|
||
string 配置路径 = Get母排参数配置路径();
|
||
if (!System.IO.File.Exists(配置路径)) return;
|
||
var doc = new System.Xml.XmlDocument();
|
||
doc.Load(配置路径);
|
||
var node = doc.SelectSingleNode("/母排参数/加工费");
|
||
// 兼容旧配置文件(根节点为"铜排参数")
|
||
if (node == null) node = doc.SelectSingleNode("/铜排参数/加工费");
|
||
if (node != null) this.txt加工费.Text = node.InnerText;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log("[母排参数] 加载失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 调母排窗口(xiangmu\调母排.cs)桥接:给子窗体取数据/回调刷新
|
||
// ============================================================
|
||
|
||
/// <summary>当前打开的项目编号(给调母排窗口定位报价库)。</summary>
|
||
public string GetCurrentProjectId()
|
||
{
|
||
return _currentProjectId ?? "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把内存中未保存的三表编辑(图号/箱号/元件,含缓存)强制落盘。
|
||
/// 调母排窗口打开前调用——它直接读数据库,必须先看到最新编辑。
|
||
/// </summary>
|
||
public void FlushQuoteTablesToDb()
|
||
{
|
||
try { SaveAllToDatabaseSilent(); }
|
||
catch (Exception ex) { Log("[调母排] 预落盘失败: " + ex.Message); }
|
||
}
|
||
|
||
/// <summary>母排页当前价格文本(元/吨;铜排模式=1#铜,铝排模式=A00铝)。</summary>
|
||
public string Get母排当前价格文本()
|
||
{
|
||
return this.txt当前铜价 == null ? "" : (this.txt当前铜价.Text ?? "");
|
||
}
|
||
|
||
/// <summary>母排页当前材质:true=铝排,false=铜排。</summary>
|
||
public bool Get母排是铝排()
|
||
{
|
||
return this.rb铝排 != null && this.rb铝排.Checked;
|
||
}
|
||
|
||
/// <summary>读取指定材质的加工费(直接读配置文件,不依赖当前材质状态)。
|
||
/// 铝=false→shezhi\铜排参数.config.xml;铝=true→shezhi\铝排参数.config.xml。
|
||
/// 读不到返回空串(调母排窗口显示空,用户可手填)。</summary>
|
||
public string Get母排加工费(bool 是铝排)
|
||
{
|
||
try
|
||
{
|
||
string 文件名 = 是铝排 ? "铝排参数.config.xml" : "铜排参数.config.xml";
|
||
string 路径 = System.IO.Path.Combine(
|
||
System.AppDomain.CurrentDomain.BaseDirectory, @"shezhi\" + 文件名);
|
||
if (!System.IO.File.Exists(路径)) return "";
|
||
var doc = new System.Xml.XmlDocument();
|
||
doc.Load(路径);
|
||
var node = doc.SelectSingleNode("/母排参数/加工费");
|
||
if (node == null) node = doc.SelectSingleNode("/铜排参数/加工费");
|
||
return node == null ? "" : node.InnerText;
|
||
}
|
||
catch { return ""; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把抓到的金属价格缓存进对应材质的配置文件(铜排参数.config.xml / 铝排参数.config.xml)。
|
||
/// 在 加工费 节点旁增加 价格(元/吨) 和 价格日期 两个节点。
|
||
/// 读配置时按需取,没有就用主窗口当前文本框值。
|
||
/// </summary>
|
||
private static void Save母排价格缓存(bool 是铝排, string 价格吨, string 日期)
|
||
{
|
||
try
|
||
{
|
||
string 文件名 = 是铝排 ? "铝排参数.config.xml" : "铜排参数.config.xml";
|
||
string 路径 = System.IO.Path.Combine(
|
||
System.AppDomain.CurrentDomain.BaseDirectory, @"shezhi\" + 文件名);
|
||
string dir = System.IO.Path.GetDirectoryName(路径);
|
||
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
|
||
|
||
var doc = new System.Xml.XmlDocument();
|
||
if (System.IO.File.Exists(路径)) doc.Load(路径); else
|
||
{
|
||
doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null));
|
||
doc.AppendChild(doc.CreateElement("母排参数"));
|
||
}
|
||
// 根节点兜底(老文件可能是别的根名)
|
||
if (doc.DocumentElement == null)
|
||
{
|
||
doc.AppendChild(doc.CreateElement("母排参数"));
|
||
}
|
||
|
||
// 写/更新 价格(元/吨)
|
||
var n价 = doc.SelectSingleNode("/母排参数/价格元每吨");
|
||
if (n价 == null)
|
||
{
|
||
n价 = doc.CreateElement("价格元每吨");
|
||
doc.DocumentElement.AppendChild(n价);
|
||
}
|
||
n价.InnerText = 价格吨 ?? "";
|
||
// 写/更新 价格日期
|
||
var n日 = doc.SelectSingleNode("/母排参数/价格日期");
|
||
if (n日 == null)
|
||
{
|
||
n日 = doc.CreateElement("价格日期");
|
||
doc.DocumentElement.AppendChild(n日);
|
||
}
|
||
n日.InnerText = 日期 ?? "";
|
||
|
||
doc.Save(路径);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取缓存的金属价格(元/吨):返回 [价格, 日期]。
|
||
/// 铝=false→铜价;铝=true→铝价。没有缓存返回 ["",""]。
|
||
/// 给调母排窗口用——不发网络请求,直接用主窗口上次抓到的价。
|
||
/// </summary>
|
||
public string[] Get母排价格缓存(bool 是铝排)
|
||
{
|
||
try
|
||
{
|
||
string 文件名 = 是铝排 ? "铝排参数.config.xml" : "铜排参数.config.xml";
|
||
string 路径 = System.IO.Path.Combine(
|
||
System.AppDomain.CurrentDomain.BaseDirectory, @"shezhi\" + 文件名);
|
||
if (!System.IO.File.Exists(路径)) return new string[] { "", "" };
|
||
var doc = new System.Xml.XmlDocument();
|
||
doc.Load(路径);
|
||
var n价 = doc.SelectSingleNode("/母排参数/价格元每吨");
|
||
var n日 = doc.SelectSingleNode("/母排参数/价格日期");
|
||
return new string[]
|
||
{
|
||
n价 == null ? "" : n价.InnerText,
|
||
n日 == null ? "" : n日.InnerText,
|
||
};
|
||
}
|
||
catch { return new string[] { "", "" }; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 调母排窗口联网补抓到价格后,把缓存写进配置文件(公开包装)。
|
||
/// 与主窗口抓价时写的 Save母排价格缓存 同一文件同一节点。
|
||
/// </summary>
|
||
public void Save母排价格缓存Pub(bool 是铝排, string 价格吨, string 日期)
|
||
{
|
||
Save母排价格缓存(是铝排, 价格吨, 日期);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 调母排窗口「更新到项目」后的回调:
|
||
/// 清缓存 → 全量重算数据库派生列与汇总 → 重载报价页 → 刷新项目列表。
|
||
/// (与选型页 RefreshQuotePageAfterSelectionEdit 同套路)
|
||
/// </summary>
|
||
public void RefreshAfter母排调价()
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
|
||
|
||
_isReloading = true;
|
||
try
|
||
{
|
||
// 先把数据库里的箱号/图号/项目汇总按新元件价重算
|
||
RecalcAllFromDatabase(_currentProjectId);
|
||
// 清缓存从库重载显示
|
||
_yuanjianCache.Clear();
|
||
_xianghaoCache.Clear();
|
||
_tuhaoTable = null;
|
||
ReloadTuhaoByProject(_currentProjectId);
|
||
// 刷新项目列表汇总列
|
||
ReloadProjectsFromDatabase();
|
||
Log("[调母排] 已重算并刷新报价页");
|
||
}
|
||
finally
|
||
{
|
||
_isReloading = false;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log("[调母排] 刷新失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>把母排参数(目前:加工费)保存到配置文件。Leave 失焦 / 关窗兜底均调用。
|
||
/// 铜排/铝排各有独立配置文件,由 Get母排参数配置路径() 决定。</summary>
|
||
private void Save铜排参数()
|
||
{
|
||
try
|
||
{
|
||
string 配置路径 = Get母排参数配置路径();
|
||
string dir = System.IO.Path.GetDirectoryName(配置路径);
|
||
if (!System.IO.Directory.Exists(dir))
|
||
{
|
||
System.IO.Directory.CreateDirectory(dir);
|
||
}
|
||
var doc = new System.Xml.XmlDocument();
|
||
doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null));
|
||
var root = doc.CreateElement("母排参数");
|
||
doc.AppendChild(root);
|
||
var 加工费Node = doc.CreateElement("加工费");
|
||
加工费Node.InnerText = this.txt加工费.Text ?? "";
|
||
root.AppendChild(加工费Node);
|
||
doc.Save(配置路径);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log("[母排参数] 保存失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 铜排规格表(advancedDataGridView8)选中 → 解析规格填入文本框
|
||
// ============================================================
|
||
|
||
/// <summary>铜排规格表选中变化:解析"规格"列文本,自动填入 txt层数/txt宽度/txt厚度。</summary>
|
||
private void advancedDataGridView8_SelectionChanged(object sender, EventArgs e)
|
||
{
|
||
// 多选拖动时跳过(避免 CurrentRow 抖动反复解析)
|
||
if (this.advancedDataGridView8 != null &&
|
||
this.advancedDataGridView8.SelectedCells.Count > 1) return;
|
||
|
||
DataGridViewRow row = GetFirstSelectedRow(this.advancedDataGridView8);
|
||
if (row == null) return;
|
||
|
||
string 规格 = GetCellValueAsString(row, "规格");
|
||
if (string.IsNullOrWhiteSpace(规格)) return;
|
||
|
||
// 把选中行的规格文本同步到 txt规格(显示当前选中的规格)
|
||
this.txt规格.Text = 规格;
|
||
|
||
int 层数; string 宽度; string 厚度;
|
||
if (!TryParse铜排规格(规格, out 层数, out 宽度, out 厚度)) return;
|
||
|
||
this.txt层数.Text = 层数.ToString();
|
||
this.txt宽度.Text = 宽度;
|
||
this.txt厚度.Text = 厚度;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析铜排规格文本,返回 层数/宽度/厚度 三个分量。
|
||
/// 规则(与 advancedDataGridView8Table 规格列一致):
|
||
/// 无括号 "TMY-15×3" → 去掉类型前缀后 "15×3" → 层数=1, 宽度=15, 厚度=3
|
||
/// 有括号 "TMY-2(60×6)" → 去掉类型前缀后 "2(60×6)"→ 层数=2, 宽度=60, 厚度=6
|
||
/// 乘号支持 × x X * (全角/半角),宽度/厚度支持小数。
|
||
/// </summary>
|
||
private static bool TryParse铜排规格(string 规格, out int 层数, out string 宽度, out string 厚度)
|
||
{
|
||
层数 = 1;
|
||
宽度 = "";
|
||
厚度 = "";
|
||
if (string.IsNullOrWhiteSpace(规格)) return false;
|
||
|
||
string s = 规格.Trim();
|
||
|
||
// 去掉类型前缀:第一个 '-' 之前的部分(含 '-'),如 "TMY-" / "LMY-"
|
||
int dash = s.IndexOf('-');
|
||
if (dash >= 0 && dash < s.Length - 1)
|
||
{
|
||
s = s.Substring(dash + 1).Trim();
|
||
}
|
||
|
||
// 情况1:有括号 "层数(宽度×厚度)"
|
||
var m = System.Text.RegularExpressions.Regex.Match(
|
||
s, @"^(\d+)\s*\(\s*(\d+(?:\.\d+)?)\s*[×xX\*]\s*(\d+(?:\.\d+)?)\s*\)");
|
||
if (m.Success)
|
||
{
|
||
if (!int.TryParse(m.Groups[1].Value, out 层数)) 层数 = 1;
|
||
宽度 = m.Groups[2].Value;
|
||
厚度 = m.Groups[3].Value;
|
||
return true;
|
||
}
|
||
|
||
// 情况2:无括号 "宽度×厚度" → 层数默认 1
|
||
m = System.Text.RegularExpressions.Regex.Match(
|
||
s, @"^(\d+(?:\.\d+)?)\s*[×xX\*]\s*(\d+(?:\.\d+)?)");
|
||
if (m.Success)
|
||
{
|
||
层数 = 1;
|
||
宽度 = m.Groups[1].Value;
|
||
厚度 = m.Groups[2].Value;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// ============================================================
|
||
// 母排页 btn插入 / btn替换:把当前规格作为一行元件插入/替换到元件表
|
||
// 字段对应:元件名称=母排,规格=txt规格,单位=comboBox1,数量=txt长度,表价=txt单价,类型=母排
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 母排规格表中选好规格后,点[插入]把当前规格作为一行新元件插入到元件表(advancedDataGridView5)。
|
||
/// 新行字段:
|
||
/// 序号 = 当前最大序号 + 1
|
||
/// 元件名称 = "母排"
|
||
/// 规格 = txt规格.Text
|
||
/// 单位 = comboBox1.Text
|
||
/// 数量 = txt长度.Text
|
||
/// 表价 = txt单价.Text(每米单价)
|
||
/// 类型 = "母排"
|
||
/// 其余派生列(采购单价/采购价/报出单价/报出价)按"采购系数=1,报出系数=1"和表价×数量算出。
|
||
/// </summary>
|
||
private void btn插入_Click(object sender, EventArgs e)
|
||
{
|
||
if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_currentProjectId))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再插入母排。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// ★ 该箱号还没建过元件表时自动建表(否则 NewRow 后写"序号"列会抛异常)
|
||
if (!确保元件表存在())
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再插入母排。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
string 规格 = (this.txt规格.Text ?? "").Trim();
|
||
if (string.IsNullOrWhiteSpace(规格))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "规格为空,请先在铜排规格表中选择一行。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// 解析表价/数量,缺省按 0 处理(避免异常,不阻断插入)
|
||
decimal 表价 = TryParseDecimalOrZero(this.txt单价.Text);
|
||
decimal 数量 = TryParseDecimalOrZero(this.txt长度.Text);
|
||
string 单位 = (this.comboBox1.Text ?? "").Trim();
|
||
|
||
// 派生列(采购系数=1,报出系数=1):采购单价=表价×1;采购价=采购单价×数量;报出单价=采购单价×1;报出价=报出单价×数量
|
||
decimal 采购单价 = 表价;
|
||
decimal 采购价 = 采购单价 * 数量;
|
||
decimal 报出单价 = 采购单价;
|
||
decimal 报出价 = 报出单价 * 数量;
|
||
|
||
// ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入;
|
||
// 单选=false 返回后走原逻辑
|
||
System.Action 插入动作 = delegate
|
||
{
|
||
// 提交正在编辑的单元格,确保元件表当前编辑态写入 DataTable
|
||
this.advancedDataGridView5.EndEdit();
|
||
|
||
// ★ 记住选中行索引,用于"在选中行下面插入"
|
||
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
|
||
|
||
var row = _yuanjianTable.NewRow();
|
||
row["序号"] = "1"; // InsertRowBelowSelected 会自动重编号
|
||
row["元件名称"] = "母排";
|
||
row["规格"] = 规格;
|
||
row["单位"] = 单位;
|
||
row["数量"] = (this.txt长度.Text ?? "").Trim();
|
||
row["表价"] = (double)表价;
|
||
row["采购系数"] = "1";
|
||
row["采购单价"] = (double)采购单价;
|
||
row["采购价"] = (double)采购价;
|
||
row["报出系数"] = "1";
|
||
row["报出单价"] = (double)报出单价;
|
||
row["报出价"] = (double)报出价;
|
||
row["图纸规格"] = "";
|
||
row["品牌"] = "";
|
||
row["系列"] = "";
|
||
row["类型"] = "母排";
|
||
row["备注"] = "";
|
||
// ★ 插入到选中行下面(而不是末尾)
|
||
InsertRowBelowSelected(_yuanjianTable, row, selectedIndex);
|
||
|
||
// ★ 虚拟模式收尾:同步网格行数+全表保存,并重算聚合
|
||
元件表插行收尾();
|
||
重算聚合();
|
||
|
||
// 滚动到新行
|
||
int idx = _yuanjianTable.Rows.IndexOf(row);
|
||
if (idx >= 0)
|
||
{
|
||
try { this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = idx; } catch { }
|
||
}
|
||
};
|
||
if (工具窗口批量插入(插入动作)) return;
|
||
插入动作();
|
||
Log("[母排插入] 已插入到元件表: 规格=" + 规格 + " 长度=" + 数量 + " 表价=" + 表价);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 母排[替换]:用当前规格覆盖元件表选中行的内容。
|
||
/// 仅当选中行的"类型"列值为"母排"时才允许替换;
|
||
/// 如果"类型"为空或不是"母排",提示用户并跳过(不覆盖非母排元件)。
|
||
/// </summary>
|
||
private void btn替换_Click(object sender, EventArgs e)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再替换母排。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
string 规格0 = (this.txt规格.Text ?? "").Trim();
|
||
if (string.IsNullOrWhiteSpace(规格0))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "规格为空,请先在铜排规格表中选择一行。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// ★ 选型页分支:选型页有选中行→目标=选型页选中行(判定用"有选中行"防焦点漂移)
|
||
if (替换目标在选型页())
|
||
{
|
||
库替换走选型页("母排", 规格0, "", "", TryParseDecimalOrZero(this.txt单价.Text).ToString(
|
||
System.Globalization.CultureInfo.InvariantCulture), "1", "母排");
|
||
return;
|
||
}
|
||
|
||
if (_yuanjianTable == null)
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "请先在报价软件中选择图号/箱号。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
DataGridViewRow dgvRow = GetFirstSelectedRow(this.advancedDataGridView5);
|
||
if (dgvRow == null)
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "请先在元件表中选择一行要替换的母排。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// 类型判定:仅"母排"行才允许替换,空或其它类型跳过
|
||
string 当前类型 = GetCellValueAsString(dgvRow, "类型");
|
||
if (!string.Equals(当前类型, "母排", System.StringComparison.Ordinal))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"选中行的类型为「" + (string.IsNullOrWhiteSpace(当前类型) ? "(空)" : 当前类型) +
|
||
"」,不是「母排」。只允许替换类型为母排的行。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
string 规格 = (this.txt规格.Text ?? "").Trim();
|
||
if (string.IsNullOrWhiteSpace(规格))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "规格为空,请先在铜排规格表中选择一行。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
this.advancedDataGridView5.EndEdit();
|
||
|
||
decimal 表价 = TryParseDecimalOrZero(this.txt单价.Text);
|
||
decimal 数量 = TryParseDecimalOrZero(this.txt长度.Text);
|
||
string 单位 = (this.comboBox1.Text ?? "").Trim();
|
||
|
||
decimal 采购单价 = 表价;
|
||
decimal 采购价 = 采购单价 * 数量;
|
||
decimal 报出单价 = 采购单价;
|
||
decimal 报出价 = 报出单价 * 数量;
|
||
|
||
// 把选中行各列覆盖为当前母排规格的内容(序号保留原值)
|
||
SetCellValue(dgvRow, "元件名称", "母排");
|
||
SetCellValue(dgvRow, "规格", 规格);
|
||
SetCellValue(dgvRow, "单位", 单位);
|
||
SetCellValue(dgvRow, "数量", (this.txt长度.Text ?? "").Trim());
|
||
SetCellValue(dgvRow, "表价", (double)表价);
|
||
SetCellValue(dgvRow, "采购系数", "1");
|
||
SetCellValue(dgvRow, "采购单价", (double)采购单价);
|
||
SetCellValue(dgvRow, "采购价", (double)采购价);
|
||
SetCellValue(dgvRow, "报出系数", "1");
|
||
SetCellValue(dgvRow, "报出单价", (double)报出单价);
|
||
SetCellValue(dgvRow, "报出价", (double)报出价);
|
||
SetCellValue(dgvRow, "类型", "母排");
|
||
Log("[母排替换] 已替换元件表选中行: 规格=" + 规格 + " 长度=" + 数量 + " 表价=" + 表价);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 供 GCS 推荐铜排窗口调用:把选型出的铜排(规格+米数)插入元件表。
|
||
/// 表价按母排界面当前铜价自动计算每米单价:
|
||
/// 每米单价 = 铜价(元/Kg) × 层数 × 宽度 × 厚度 × 密度 / 1000
|
||
/// 密度按规格前缀判断:LMY→铝排 2.7,其余(如 TMY)→铜排 8.9。
|
||
/// </summary>
|
||
public bool InsertGcs母排(string 规格, double 米数)
|
||
{
|
||
if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_currentProjectId))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再插入母排。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return false;
|
||
}
|
||
|
||
// ★ 该箱号还没建过元件表时自动建表(否则 NewRow 后写"序号"列会抛异常)
|
||
if (!确保元件表存在())
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再插入母排。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return false;
|
||
}
|
||
|
||
规格 = (规格 ?? "").Trim();
|
||
if (string.IsNullOrWhiteSpace(规格))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "规格为空,无法插入母排。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return false;
|
||
}
|
||
|
||
// 解析规格得到层数/宽度/厚度
|
||
int 层数; string 宽度; string 厚度;
|
||
if (!TryParse铜排规格(规格, out 层数, out 宽度, out 厚度))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "无法解析铜排规格:「" + 规格 + "」", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return false;
|
||
}
|
||
|
||
this.advancedDataGridView5.EndEdit();
|
||
|
||
// 计算每米单价(与母排界面 Recalc单价PerMeter 公式一致)
|
||
decimal 铜价Kg = TryParseDecimalOrZero(this.txt铜价.Text);
|
||
decimal 宽 = TryParseDecimalOrZero(宽度);
|
||
decimal 厚 = TryParseDecimalOrZero(厚度);
|
||
decimal 密度 = 规格.StartsWith("LMY", System.StringComparison.OrdinalIgnoreCase) ? 2.7m : 8.9m;
|
||
decimal 每米重量 = 层数 * 宽 * 厚 * 密度 / 1000m;
|
||
// ★保留 2 位小数(2026-09-26 用户报与母排页插入不一致:母排页 txt单价 保留 2 位,
|
||
// 此处曾 Round 0 取整;统一为同款四舍五入 2 位,不再取整)
|
||
decimal 表价 = Math.Round(铜价Kg * 每米重量, 2, MidpointRounding.AwayFromZero);
|
||
|
||
decimal 数量 = (decimal)米数;
|
||
decimal 采购单价 = 表价;
|
||
decimal 采购价 = 采购单价 * 数量;
|
||
decimal 报出单价 = 采购单价;
|
||
decimal 报出价 = 报出单价 * 数量;
|
||
|
||
// ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入;
|
||
// 单选=false 返回后走原逻辑
|
||
System.Action 插入动作 = delegate
|
||
{
|
||
this.advancedDataGridView5.EndEdit();
|
||
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
|
||
|
||
var row = _yuanjianTable.NewRow();
|
||
row["序号"] = "1";
|
||
row["元件名称"] = "母排";
|
||
row["规格"] = 规格;
|
||
row["单位"] = "米";
|
||
row["数量"] = 米数.ToString("0.##");
|
||
row["表价"] = (double)表价;
|
||
row["采购系数"] = "1";
|
||
row["采购单价"] = (double)采购单价;
|
||
row["采购价"] = (double)采购价;
|
||
row["报出系数"] = "1";
|
||
row["报出单价"] = (double)报出单价;
|
||
row["报出价"] = (double)报出价;
|
||
row["图纸规格"] = "";
|
||
row["品牌"] = "";
|
||
row["系列"] = "";
|
||
row["类型"] = "母排";
|
||
row["备注"] = "";
|
||
InsertRowBelowSelected(_yuanjianTable, row, selectedIndex);
|
||
|
||
// ★ 虚拟模式收尾:同步网格行数+全表保存,并重算聚合
|
||
元件表插行收尾();
|
||
重算聚合();
|
||
|
||
int idx = _yuanjianTable.Rows.IndexOf(row);
|
||
if (idx >= 0)
|
||
{
|
||
try { this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = idx; } catch { }
|
||
}
|
||
};
|
||
if (工具窗口批量插入(插入动作)) return true;
|
||
插入动作();
|
||
Log("[GCS插入] 已插入母排到元件表: 规格=" + 规格 + " 米数=" + 米数 + " 表价=" + 表价);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>把文本框值解析为 decimal;空/非数字返回 0。</summary>
|
||
private static decimal TryParseDecimalOrZero(string text)
|
||
{
|
||
decimal d;
|
||
if (decimal.TryParse((text ?? "").Trim(), out d)) return d;
|
||
return 0m;
|
||
}
|
||
|
||
/// <summary>取元件表下一个序号 = 已有行的最大数值序号 + 1(空表返回 1)。</summary>
|
||
private static int GetNext元件序号(DataTable table)
|
||
{
|
||
if (table == null || table.Rows.Count == 0) return 1;
|
||
int max = 0;
|
||
foreach (DataRow r in table.Rows)
|
||
{
|
||
if (r.RowState == DataRowState.Deleted) continue;
|
||
object v = r["序号"];
|
||
if (v == null || v == DBNull.Value) continue;
|
||
int n;
|
||
if (int.TryParse(v.ToString().Trim(), out n) && n > max) max = n;
|
||
}
|
||
return max + 1;
|
||
}
|
||
|
||
/// <summary>把值写到 DataGridViewRow 的指定列(按 DataPropertyName 或 HeaderText 匹配)。</summary>
|
||
private void SetCellValue(DataGridViewRow row, string colName, object value)
|
||
{
|
||
if (row == null || row.DataGridView == null || string.IsNullOrEmpty(colName)) return;
|
||
DataGridViewColumn col = FindColumn(row.DataGridView, colName);
|
||
if (col == null) return;
|
||
// ★ 虚拟模式:写内存表+刷新行(写 Cells.Value 无效)
|
||
string realName = !string.IsNullOrEmpty(col.DataPropertyName) ? col.DataPropertyName : colName;
|
||
写单元格(row.DataGridView, row.Index, realName, value);
|
||
}
|
||
|
||
// ============================================================
|
||
// 备份/恢复 库数据(用户库/柜体库/方案库/收藏/企业信息)
|
||
// ============================================================
|
||
|
||
/// <summary>需要备份的文件列表(相对于 exe 目录)。</summary>
|
||
private static readonly string[] BackupFiles = new string[]
|
||
{
|
||
@"sjk\advancedDataGridView7Table.db", // 用户元件库
|
||
@"sjk\advancedDataGridView10Table.db", // 柜体库
|
||
@"sjk\fak.db", // 方案库
|
||
@"shezhi\元件库收藏.config.xml", // 元件库收藏
|
||
@"shezhi\企业信息.config.xml", // 企业信息
|
||
};
|
||
|
||
/// <summary>备份:先选目标(云端/本地,可记住选择),再走对应流程。</summary>
|
||
private void btn备份_Click(object sender, EventArgs e)
|
||
{
|
||
var 目标 = 备份目标选择.询问(false);
|
||
if (目标 == 备份目标.云端) { 备份库到云端(); return; }
|
||
if (目标 == 备份目标.本地文件) { 备份到本地文件(); return; }
|
||
}
|
||
|
||
/// <summary>原本地备份流程(从 btn备份_Click 抽出)。</summary>
|
||
private void 备份到本地文件()
|
||
{
|
||
try
|
||
{
|
||
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||
|
||
// 弹保存对话框
|
||
string desktop = "";
|
||
try
|
||
{
|
||
desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||
}
|
||
catch { }
|
||
|
||
string defaultName = "数据备份_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".zip";
|
||
using (var sfd = new SaveFileDialog
|
||
{
|
||
Title = "备份数据",
|
||
Filter = "备份包 (*.zip)|*.zip",
|
||
FileName = defaultName,
|
||
RestoreDirectory = true,
|
||
})
|
||
{
|
||
if (!string.IsNullOrEmpty(desktop) && System.IO.Directory.Exists(desktop))
|
||
sfd.InitialDirectory = desktop;
|
||
|
||
if (sfd.ShowDialog(this) != DialogResult.OK) return;
|
||
string zipPath = sfd.FileName;
|
||
|
||
// 临时目录
|
||
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||
"成套报价_备份_" + System.Guid.NewGuid().ToString("N"));
|
||
System.IO.Directory.CreateDirectory(tempDir);
|
||
|
||
int fileCount = 0;
|
||
try
|
||
{
|
||
foreach (string relPath in BackupFiles)
|
||
{
|
||
string srcPath = System.IO.Path.Combine(baseDir, relPath);
|
||
if (!System.IO.File.Exists(srcPath)) continue;
|
||
|
||
// 在临时目录里保持相对路径结构(如 sjk\xxx.db, shezhi\xxx.xml)
|
||
string dstPath = System.IO.Path.Combine(tempDir, relPath.Replace('\\', System.IO.Path.DirectorySeparatorChar));
|
||
string dstDir = System.IO.Path.GetDirectoryName(dstPath);
|
||
if (!System.IO.Directory.Exists(dstDir))
|
||
System.IO.Directory.CreateDirectory(dstDir);
|
||
|
||
// 备份 .db 文件用 SQLite Backup API(避免文件锁)
|
||
if (relPath.EndsWith(".db", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
BackupSQLiteDb(srcPath, dstPath);
|
||
}
|
||
else
|
||
{
|
||
System.IO.File.Copy(srcPath, dstPath, true);
|
||
}
|
||
fileCount++;
|
||
Log("[备份] " + relPath + " OK");
|
||
}
|
||
|
||
if (fileCount == 0)
|
||
{
|
||
MessageBox.Show(this, "没有找到可备份的数据文件。",
|
||
"备份", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
// 打包
|
||
if (System.IO.File.Exists(zipPath))
|
||
System.IO.File.Delete(zipPath);
|
||
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, zipPath);
|
||
|
||
MessageBox.Show(this,
|
||
"备份成功!\n\n包含 " + fileCount + " 个文件\n保存到:" + zipPath,
|
||
"备份", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
Log("[备份] 完成: " + fileCount + " 个文件 → " + zipPath);
|
||
}
|
||
finally
|
||
{
|
||
try { System.IO.Directory.Delete(tempDir, true); } catch { }
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, "备份失败:\n" + ex.Message,
|
||
"备份", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
Log("[备份异常] " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>恢复:先选来源(云端/本地,可记住选择),再走对应流程。</summary>
|
||
private void btn恢复_Click(object sender, EventArgs e)
|
||
{
|
||
var 目标 = 备份目标选择.询问(true);
|
||
if (目标 == 备份目标.云端) { 从云端恢复库(); return; }
|
||
if (目标 == 备份目标.本地文件) { 从本地文件恢复(); return; }
|
||
}
|
||
|
||
/// <summary>原本地恢复流程(从 btn恢复_Click 抽出)。</summary>
|
||
private void 从本地文件恢复()
|
||
{
|
||
try
|
||
{
|
||
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||
|
||
string desktop = "";
|
||
try
|
||
{
|
||
desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||
}
|
||
catch { }
|
||
|
||
using (var ofd = new OpenFileDialog
|
||
{
|
||
Title = "选择备份文件恢复数据",
|
||
Filter = "备份包 (*.zip)|*.zip",
|
||
RestoreDirectory = true,
|
||
})
|
||
{
|
||
if (!string.IsNullOrEmpty(desktop) && System.IO.Directory.Exists(desktop))
|
||
ofd.InitialDirectory = desktop;
|
||
|
||
if (ofd.ShowDialog(this) != DialogResult.OK) return;
|
||
string zipPath = ofd.FileName;
|
||
|
||
// 解压到临时目录
|
||
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||
"成套报价_恢复_" + System.Guid.NewGuid().ToString("N"));
|
||
System.IO.Directory.CreateDirectory(tempDir);
|
||
|
||
int fileCount = 0;
|
||
try
|
||
{
|
||
System.IO.Compression.ZipFile.ExtractToDirectory(zipPath, tempDir);
|
||
|
||
// 确认恢复
|
||
DialogResult dr = MessageBox.Show(this,
|
||
"恢复将覆盖当前的用户库/柜体库/方案库/收藏/企业信息数据!\n确定继续吗?",
|
||
"恢复数据", MessageBoxButtons.YesNo, MessageBoxIcon.Question,
|
||
MessageBoxDefaultButton.Button2);
|
||
if (dr != DialogResult.Yes) return;
|
||
|
||
foreach (string relPath in BackupFiles)
|
||
{
|
||
// 在临时目录里找对应文件
|
||
string srcPath = System.IO.Path.Combine(tempDir, relPath.Replace('\\', System.IO.Path.DirectorySeparatorChar));
|
||
if (!System.IO.File.Exists(srcPath)) continue;
|
||
|
||
string dstPath = System.IO.Path.Combine(baseDir, relPath.Replace('\\', System.IO.Path.DirectorySeparatorChar));
|
||
string dstDir = System.IO.Path.GetDirectoryName(dstPath);
|
||
if (!System.IO.Directory.Exists(dstDir))
|
||
System.IO.Directory.CreateDirectory(dstDir);
|
||
|
||
// .db 文件用 SQLite Backup API(避免文件锁)
|
||
if (relPath.EndsWith(".db", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
BackupSQLiteDb(srcPath, dstPath);
|
||
}
|
||
else
|
||
{
|
||
// xml 直接覆盖
|
||
System.IO.File.Copy(srcPath, dstPath, true);
|
||
}
|
||
fileCount++;
|
||
Log("[恢复] " + relPath + " OK");
|
||
}
|
||
|
||
if (fileCount == 0)
|
||
{
|
||
MessageBox.Show(this, "备份包里没有找到可恢复的数据文件。",
|
||
"恢复", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
MessageBox.Show(this,
|
||
"恢复成功!\n\n已恢复 " + fileCount + " 个文件\n请重新启动软件让数据生效。",
|
||
"恢复", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
Log("[恢复] 完成: " + fileCount + " 个文件");
|
||
|
||
// 刷新内存中的表格数据
|
||
try { LoadUserLibDistinctNames(); } catch { }
|
||
try { LoadGuiTiDistinctNames(); } catch { }
|
||
try { ReloadFakData(); } catch { }
|
||
}
|
||
finally
|
||
{
|
||
try { System.IO.Directory.Delete(tempDir, true); } catch { }
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, "恢复失败:\n" + ex.Message,
|
||
"恢复", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
Log("[恢复异常] " + ex.Message);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 自动备份:每天首次启动时自动 zip 全量数据到 backup\ 目录,
|
||
// 保留最近 14 份,超出自动删除最旧的。全程静默,失败只记日志。
|
||
// ============================================================
|
||
|
||
// ============================================================
|
||
// 云端库备份/恢复(会员服务器,按登录手机号隔离;复用项目云接口)
|
||
// ============================================================
|
||
|
||
/// <summary>云端库备份的编号前缀(项目云接口按该前缀区分库条目)。</summary>
|
||
private const string 云库前缀 = "LIB_";
|
||
|
||
/// <summary>收集全部可云备份的库(BackupFiles + 铜排/铝排库 + 方案库目录每个方案 db)。
|
||
/// 返回 (云端编号, 本地相对路径) 列表。</summary>
|
||
private System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, string>> 收集云端库清单()
|
||
{
|
||
var 出 = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, string>>();
|
||
出.Add(new System.Collections.Generic.KeyValuePair<string, string>(云库前缀 + "铜排库", @"sjk\advancedDataGridView8Table.db"));
|
||
出.Add(new System.Collections.Generic.KeyValuePair<string, string>(云库前缀 + "铝排库", @"sjk\advancedDataGridView8Table_LMY.db"));
|
||
foreach (string rel in BackupFiles)
|
||
出.Add(new System.Collections.Generic.KeyValuePair<string, string>(云库前缀 + 编号自相对路径(rel), rel));
|
||
// 方案库目录:每个方案 db 单独一条
|
||
string 方案目录 = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sjk", "方案库");
|
||
if (System.IO.Directory.Exists(方案目录))
|
||
{
|
||
foreach (var f in System.IO.Directory.GetFiles(方案目录, "*.db"))
|
||
出.Add(new System.Collections.Generic.KeyValuePair<string, string>(
|
||
云库前缀 + "方案_" + System.IO.Path.GetFileNameWithoutExtension(f),
|
||
"sjk\\方案库\\" + System.IO.Path.GetFileName(f)));
|
||
}
|
||
return 出;
|
||
}
|
||
|
||
/// <summary>相对路径→云端编号片段(sjk\fak.db → fak方案库(含公式表);其余去扩展名/换可读名)。</summary>
|
||
private static string 编号自相对路径(string relPath)
|
||
{
|
||
string s = relPath.Replace("sjk\\", "").Replace("shezhi\\", "").Replace('\\', '_');
|
||
if (s.EndsWith(".db", System.StringComparison.OrdinalIgnoreCase)) s = s.Substring(0, s.Length - 3);
|
||
if (s.EndsWith(".xml", System.StringComparison.OrdinalIgnoreCase)) s = s.Substring(0, s.Length - 4);
|
||
if (s == "fak") s = "fak方案库(含公式表)";
|
||
if (s == "advancedDataGridView7Table") s = "用户库";
|
||
if (s == "advancedDataGridView8Table") s = "铜排库";
|
||
if (s == "advancedDataGridView8Table_LMY") s = "铝排库";
|
||
if (s == "advancedDataGridView10Table") s = "柜体库";
|
||
if (s == "企业信息_config") s = "企业信息";
|
||
if (s == "元件库收藏_config") s = "元件库收藏";
|
||
return s;
|
||
}
|
||
|
||
private static string 库显示名(string 云编号) { return 云编号.StartsWith(云库前缀) ? 云编号.Substring(云库前缀.Length) : 云编号; }
|
||
|
||
/// <summary>备份全部库到云端(逐个上传;db 用 BackupSQLiteDb 防锁,xml 直接复制)。</summary>
|
||
private void 备份库到云端()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(MainForm.当前登录手机号))
|
||
{
|
||
MessageBox.Show(this, "请先登录会员,再备份到云端。", "备份到云端", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
var 们 = 收集云端库清单();
|
||
int ok = 0, 序 = 0;
|
||
foreach (var kv in 们)
|
||
{
|
||
序++;
|
||
SetStatus("云备份库 " + 序 + "/" + 们.Count + ": " + 库显示名(kv.Key) + " ...");
|
||
if (上传单个库到云端(MainForm.当前登录手机号, kv.Key, kv.Value)) ok++;
|
||
}
|
||
SetStatus("");
|
||
MessageBox.Show(this, "云端备份完成:成功 " + ok + "/" + 们.Count + " 个。",
|
||
"备份到云端", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
Log("[云备份] 完成: " + ok + "/" + 们.Count);
|
||
}
|
||
|
||
/// <summary>上传单个库到云端(meta 名称="显示名|原始路径",还原时定位用)。</summary>
|
||
private bool 上传单个库到云端(string 手机号, string 云编号, string 相对路径)
|
||
{
|
||
try
|
||
{
|
||
string 本地 = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, 相对路径);
|
||
if (!System.IO.File.Exists(本地)) { Log("[云备份] 库不存在,跳过: " + 相对路径); return false; }
|
||
|
||
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||
"成套报价_库传_" + System.Guid.NewGuid().ToString("N"));
|
||
System.IO.Directory.CreateDirectory(tempDir);
|
||
try
|
||
{
|
||
string 打包文件 = System.IO.Path.Combine(tempDir, System.IO.Path.GetFileName(相对路径));
|
||
if (相对路径.EndsWith(".db", System.StringComparison.OrdinalIgnoreCase))
|
||
BackupSQLiteDb(本地, 打包文件);
|
||
else
|
||
System.IO.File.Copy(本地, 打包文件, true);
|
||
|
||
byte[] zip;
|
||
using (var ms = new System.IO.MemoryStream())
|
||
{
|
||
using (var ar = new System.IO.Compression.ZipArchive(ms, System.IO.Compression.ZipArchiveMode.Create, true))
|
||
ar.CreateEntryFromFile(打包文件, System.IO.Path.GetFileName(打包文件), System.IO.Compression.CompressionLevel.Optimal);
|
||
zip = ms.ToArray();
|
||
}
|
||
|
||
var 头 = new System.Text.StringBuilder();
|
||
头.Append("{").Append("\"手机号\":\"").Append(EscapeJsonStringStatic(手机号)).Append("\",")
|
||
.Append("\"编号\":\"").Append(EscapeJsonStringStatic(云编号)).Append("\",")
|
||
.Append("\"名称\":\"").Append(EscapeJsonStringStatic(库显示名(云编号) + "|" + 相对路径)).Append("\",")
|
||
.Append("\"时间\":\"").Append(DateTime.Now.ToString("yyyy-MM-dd")).Append("\"}");
|
||
string resp = 会员服务Host.PostBinary("/api/proj/upload", 头.ToString(), zip);
|
||
bool ok = resp != null && resp.IndexOf("\"ok\":true", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||
Log("[云备份] " + 云编号 + "(" + zip.Length + "B): " + resp);
|
||
return ok;
|
||
}
|
||
finally { try { System.IO.Directory.Delete(tempDir, true); } catch { } }
|
||
}
|
||
catch (Exception ex) { Log("[云备份] 单库失败 " + 云编号 + ": " + ex.Message); return false; }
|
||
}
|
||
|
||
private class 云端库条目 { public string 编号, 显示名, 路径, 时间; public long 大小; }
|
||
|
||
/// <summary>从云端恢复库:列出 LIB_ 条目勾选,下载 zip 按原始路径覆盖(覆盖前 .bak)。</summary>
|
||
private void 从云端恢复库()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(MainForm.当前登录手机号))
|
||
{
|
||
MessageBox.Show(this, "请先登录会员,再从云端恢复。", "从云端恢复", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
string q = System.Uri.EscapeDataString("手机号") + "=" + System.Uri.EscapeDataString(MainForm.当前登录手机号.Trim());
|
||
string resp = 会员服务Host.Get("/api/proj/list?" + q);
|
||
if (resp == null || resp.IndexOf("\"ok\":true", System.StringComparison.OrdinalIgnoreCase) < 0)
|
||
{ MessageBox.Show(this, "云端列表获取失败。", "从云端恢复", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
|
||
|
||
var 条目 = 解析云端库条目(resp);
|
||
if (条目.Count == 0)
|
||
{
|
||
MessageBox.Show(this, "云端还没有库备份。\n请先用\"备份→保存到云端\"上传一次。",
|
||
"从云端恢复", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
using (var dlg = new System.Windows.Forms.Form
|
||
{
|
||
Text = "选择要恢复的库(勾选)",
|
||
StartPosition = FormStartPosition.CenterParent,
|
||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||
MaximizeBox = false, MinimizeBox = false,
|
||
ClientSize = new System.Drawing.Size(640, 400),
|
||
Font = new System.Drawing.Font("宋体", 9F),
|
||
})
|
||
{
|
||
var lv = new System.Windows.Forms.ListView
|
||
{
|
||
Location = new System.Drawing.Point(12, 12),
|
||
Size = new System.Drawing.Size(616, 330),
|
||
View = System.Windows.Forms.View.Details,
|
||
FullRowSelect = true, CheckBoxes = true, HideSelection = false,
|
||
};
|
||
lv.Columns.Add("库", 190);
|
||
lv.Columns.Add("备份时间", 90);
|
||
lv.Columns.Add("大小", 80);
|
||
lv.Columns.Add("原始路径", 240);
|
||
foreach (var t in 条目)
|
||
{
|
||
var it = new System.Windows.Forms.ListViewItem(t.显示名);
|
||
it.SubItems.Add(t.时间);
|
||
it.SubItems.Add(t.大小 + "B");
|
||
it.SubItems.Add(t.路径);
|
||
it.Tag = t;
|
||
lv.Items.Add(it);
|
||
}
|
||
dlg.Controls.Add(lv);
|
||
|
||
var btn全选 = new System.Windows.Forms.Button { Text = "全选", Location = new System.Drawing.Point(12, 356), Size = new System.Drawing.Size(75, 28) };
|
||
btn全选.Click += (s2, e2) => { foreach (System.Windows.Forms.ListViewItem it in lv.Items) it.Checked = true; };
|
||
dlg.Controls.Add(btn全选);
|
||
var btnOk = new System.Windows.Forms.Button { Text = "还原勾选项", Location = new System.Drawing.Point(452, 356), Size = new System.Drawing.Size(100, 28) };
|
||
dlg.Controls.Add(btnOk);
|
||
var btnCancel = new System.Windows.Forms.Button { Text = "取消", DialogResult = DialogResult.Cancel, Location = new System.Drawing.Point(560, 356), Size = new System.Drawing.Size(68, 28) };
|
||
dlg.Controls.Add(btnCancel);
|
||
dlg.CancelButton = btnCancel;
|
||
btnOk.Click += (s2, e2) => { dlg.DialogResult = DialogResult.OK; dlg.Close(); };
|
||
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
var 勾 = new System.Collections.Generic.List<云端库条目>();
|
||
foreach (System.Windows.Forms.ListViewItem it in lv.Items)
|
||
if (it.Checked) 勾.Add((云端库条目)it.Tag);
|
||
if (勾.Count == 0) return;
|
||
|
||
if (MessageBox.Show(this,
|
||
"还原将覆盖本地的 " + 勾.Count + " 个库(同名库先存 .bak 再覆盖)!\n确定继续吗?还原后建议重启软件。",
|
||
"从云端恢复", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
|
||
return;
|
||
|
||
int okCount = 0;
|
||
foreach (var t in 勾)
|
||
{
|
||
try
|
||
{
|
||
string q2 = System.Uri.EscapeDataString("手机号") + "=" + System.Uri.EscapeDataString(MainForm.当前登录手机号.Trim())
|
||
+ "&" + System.Uri.EscapeDataString("编号") + "=" + System.Uri.EscapeDataString(t.编号);
|
||
string zipPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||
"成套报价_库取_" + System.Guid.NewGuid().ToString("N") + ".zip");
|
||
try
|
||
{
|
||
会员服务Host.DownloadToFile("/api/proj/download?" + q2, zipPath);
|
||
string 目标 = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, t.路径);
|
||
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(目标));
|
||
if (System.IO.File.Exists(目标))
|
||
System.IO.File.Copy(目标, 目标 + ".bak", true);
|
||
|
||
string 解压目录 = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||
"成套报价_库解_" + System.Guid.NewGuid().ToString("N"));
|
||
System.IO.Directory.CreateDirectory(解压目录);
|
||
try
|
||
{
|
||
System.IO.Compression.ZipFile.ExtractToDirectory(zipPath, 解压目录);
|
||
string[] 文件们 = System.IO.Directory.GetFiles(解压目录);
|
||
if (文件们.Length > 0)
|
||
{
|
||
System.IO.File.Copy(文件们[0], 目标, true);
|
||
okCount++;
|
||
Log("[云恢复] " + t.编号 + " → " + t.路径);
|
||
}
|
||
}
|
||
finally { try { System.IO.Directory.Delete(解压目录, true); } catch { } }
|
||
}
|
||
finally { try { System.IO.File.Delete(zipPath); } catch { } }
|
||
}
|
||
catch (Exception exOne) { Log("[云恢复] 单库失败 " + t.编号 + ": " + exOne.Message); }
|
||
}
|
||
|
||
MessageBox.Show(this,
|
||
"云端恢复完成:成功 " + okCount + "/" + 勾.Count + " 个。\n请重新启动软件让数据生效。",
|
||
"从云端恢复", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
Log("[云恢复] 完成: " + okCount + "/" + 勾.Count);
|
||
try { LoadUserLibDistinctNames(); } catch { }
|
||
try { LoadGuiTiDistinctNames(); } catch { }
|
||
try { ReloadFakData(); } catch { }
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, "云端恢复失败:\n" + ex.Message, "从云端恢复", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
Log("[云恢复异常] " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>解析 /api/proj/list 响应,取 LIB_ 开头的条目(名称="显示名|路径")。</summary>
|
||
private System.Collections.Generic.List<云端库条目> 解析云端库条目(string json)
|
||
{
|
||
var 出 = new System.Collections.Generic.List<云端库条目>();
|
||
if (string.IsNullOrEmpty(json)) return 出;
|
||
int i = json.IndexOf('[');
|
||
if (i < 0) return 出;
|
||
int 深 = 0, 对象起 = -1;
|
||
for (; i < json.Length; i++)
|
||
{
|
||
char c = json[i];
|
||
if (c == '{') { if (深 == 0) 对象起 = i; 深++; }
|
||
else if (c == '}')
|
||
{
|
||
深--;
|
||
if (深 == 0 && 对象起 >= 0)
|
||
{
|
||
string obj = json.Substring(对象起, i - 对象起 + 1);
|
||
string 编号 = 取JSON串值(obj, "编号");
|
||
if (编号 != null && 编号.StartsWith(云库前缀, System.StringComparison.Ordinal))
|
||
{
|
||
string 名称 = 取JSON串值(obj, "名称") ?? "";
|
||
string 显示名 = 名称, 路径 = "";
|
||
int 竖 = 名称.IndexOf('|');
|
||
if (竖 >= 0) { 显示名 = 名称.Substring(0, 竖); 路径 = 名称.Substring(竖 + 1); }
|
||
long 大小 = 0;
|
||
long.TryParse(取JSON串值(obj, "大小") ?? "0", out 大小);
|
||
出.Add(new 云端库条目 { 编号 = 编号, 显示名 = 显示名, 路径 = 路径, 时间 = 取JSON串值(obj, "时间") ?? "", 大小 = 大小 });
|
||
}
|
||
对象起 = -1;
|
||
}
|
||
}
|
||
}
|
||
return 出;
|
||
}
|
||
|
||
/// <summary>从单个 JSON 对象文本取字符串字段(处理 \uXXXX 转义)。</summary>
|
||
private static string 取JSON串值(string obj, string key)
|
||
{
|
||
int k = obj.IndexOf("\"" + key + "\"", System.StringComparison.Ordinal);
|
||
if (k < 0) return null;
|
||
int 冒 = obj.IndexOf(':', k + key.Length + 2);
|
||
if (冒 < 0) return null;
|
||
int 起 = obj.IndexOf('"', 冒 + 1);
|
||
if (起 < 0) return null;
|
||
var sb = new System.Text.StringBuilder();
|
||
for (int i = 起 + 1; i < obj.Length; i++)
|
||
{
|
||
char c = obj[i];
|
||
if (c == '\\' && i + 1 < obj.Length)
|
||
{
|
||
char n = obj[++i];
|
||
if (n == 'u' && i + 4 < obj.Length) { sb.Append((char)System.Convert.ToInt32(obj.Substring(i + 1, 4), 16)); i += 4; }
|
||
else sb.Append(n == 'n' ? '\n' : n);
|
||
}
|
||
else if (c == '"') break;
|
||
else sb.Append(c);
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
private static string EscapeJsonStringStatic(string s)
|
||
{
|
||
if (s == null) return "";
|
||
return s.Replace("\\", "\\\\").Replace("\"", "\\\"")
|
||
.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
|
||
}
|
||
|
||
/// <summary>自动备份保留的份数(超过则删最旧)。</summary>
|
||
private const int AutoBackupKeepCount = 14;
|
||
|
||
/// <summary>自动备份标记文件:记录上次自动备份日期(yyyy-MM-dd),每天只备一次。</summary>
|
||
private string AutoBackupMarkPath
|
||
{
|
||
get { return System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"backup\last_auto_backup.txt"); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动时检查是否需要自动备份:
|
||
/// 当天没备过 → 静默把 全部数据(sjk\ 目录所有 db + shezhi\ 配置) 打成 zip
|
||
/// 放到 backup\自动备份_yyyyMMdd_HHmmss.zip,然后清理超出保留数的旧包。
|
||
/// </summary>
|
||
private void RunDailyAutoBackupIfNeeded()
|
||
{
|
||
try
|
||
{
|
||
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||
string backupDir = System.IO.Path.Combine(baseDir, "backup");
|
||
if (!System.IO.Directory.Exists(backupDir))
|
||
System.IO.Directory.CreateDirectory(backupDir);
|
||
|
||
// ① 当天已备过 → 跳过
|
||
string today = DateTime.Now.ToString("yyyy-MM-dd");
|
||
string markPath = AutoBackupMarkPath;
|
||
if (System.IO.File.Exists(markPath))
|
||
{
|
||
string last = System.IO.File.ReadAllText(markPath).Trim();
|
||
if (last == today) return;
|
||
}
|
||
|
||
// ② 收集要备份的文件(sjk\ 全部 + shezhi\ 全部,含子目录)
|
||
var files = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, string>>(); // <相对路径, 绝对路径>
|
||
foreach (string pattern in new string[] { @"sjk\*.db", @"sjk\bjb\*.db", @"shezhi\*.xml" })
|
||
{
|
||
string[] hits = System.IO.Directory.GetFiles(baseDir, System.IO.Path.GetFileName(pattern),
|
||
System.IO.SearchOption.TopDirectoryOnly);
|
||
string subDir = System.IO.Path.GetDirectoryName(pattern);
|
||
if (!string.IsNullOrEmpty(subDir) && subDir != ".")
|
||
{
|
||
string absSub = System.IO.Path.Combine(baseDir, subDir);
|
||
if (System.IO.Directory.Exists(absSub))
|
||
hits = System.IO.Directory.GetFiles(absSub, System.IO.Path.GetFileName(pattern));
|
||
else
|
||
hits = new string[0];
|
||
}
|
||
foreach (string f in hits)
|
||
files.Add(new System.Collections.Generic.KeyValuePair<string, string>(
|
||
f.Substring(baseDir.Length).TrimStart('\\', '/'), f));
|
||
}
|
||
if (files.Count == 0)
|
||
{
|
||
Log("[自动备份] 没有找到可备份的数据文件,跳过");
|
||
return;
|
||
}
|
||
|
||
// ③ 复制到临时目录(.db 用 SQLite Backup API 避锁)
|
||
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||
"成套报价_自动备份_" + System.Guid.NewGuid().ToString("N"));
|
||
System.IO.Directory.CreateDirectory(tempDir);
|
||
int okCount = 0;
|
||
try
|
||
{
|
||
foreach (var kv in files)
|
||
{
|
||
try
|
||
{
|
||
string dstPath = System.IO.Path.Combine(tempDir, kv.Key.Replace('\\', System.IO.Path.DirectorySeparatorChar));
|
||
string dstDir = System.IO.Path.GetDirectoryName(dstPath);
|
||
if (!System.IO.Directory.Exists(dstDir))
|
||
System.IO.Directory.CreateDirectory(dstDir);
|
||
if (kv.Key.EndsWith(".db", System.StringComparison.OrdinalIgnoreCase))
|
||
BackupSQLiteDb(kv.Value, dstPath);
|
||
else
|
||
System.IO.File.Copy(kv.Value, dstPath, true);
|
||
okCount++;
|
||
}
|
||
catch (Exception exFile)
|
||
{
|
||
Log("[自动备份] 单文件失败 " + kv.Key + ": " + exFile.Message);
|
||
}
|
||
}
|
||
if (okCount == 0) return;
|
||
|
||
// ④ 打包
|
||
string zipPath = System.IO.Path.Combine(backupDir,
|
||
"自动备份_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".zip");
|
||
if (System.IO.File.Exists(zipPath)) System.IO.File.Delete(zipPath);
|
||
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, zipPath);
|
||
|
||
// ⑤ 写标记 + 清理超出保留数的旧包
|
||
System.IO.File.WriteAllText(markPath, today, new System.Text.UTF8Encoding(false));
|
||
CleanupOldAutoBackups(backupDir);
|
||
Log("[自动备份] 完成: " + okCount + " 个文件 → " + zipPath);
|
||
}
|
||
finally
|
||
{
|
||
try { System.IO.Directory.Delete(tempDir, true); } catch { }
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 自动备份失败绝不阻断启动
|
||
Log("[自动备份] 异常: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>删除 backup\ 下超出保留数量的旧自动备份 zip(按文件名排序,名字含时间戳)。</summary>
|
||
private void CleanupOldAutoBackups(string backupDir)
|
||
{
|
||
try
|
||
{
|
||
var zips = new System.Collections.Generic.List<string>(
|
||
System.IO.Directory.GetFiles(backupDir, "自动备份_*.zip"));
|
||
if (zips.Count <= AutoBackupKeepCount) return;
|
||
zips.Sort(System.StringComparer.OrdinalIgnoreCase); // 文件名含 yyyyMMdd_HHmmss,字典序=时间序
|
||
int remove = zips.Count - AutoBackupKeepCount;
|
||
for (int i = 0; i < remove; i++)
|
||
{
|
||
try
|
||
{
|
||
System.IO.File.Delete(zips[i]);
|
||
Log("[自动备份] 清理旧备份: " + System.IO.Path.GetFileName(zips[i]));
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
// ============================================================
|
||
// 箱体计算面板(tabPage14 / panel箱体计算)
|
||
// ============================================================
|
||
//
|
||
// 计算公式:
|
||
// 面积(㎡) = (H×W×正视 + H×D×侧视 + W×D×俯视) / 1000000
|
||
// 其中 H/W/D 单位为 mm,正视/侧视/俯视为面数(整数)
|
||
// 重量(KG) = 面积 × 板厚(mm) × 密度(g/cm³) / 1000
|
||
// 单台单价(元) = 面积 × 单价(元/㎡) × 报价系数
|
||
//
|
||
// 用户只需输入:高(H)/宽(W)/深(D)/板厚/单价,其余自动计算。
|
||
// 密度默认 7.85 g/cm³(钢板),可修改。
|
||
// ============================================================
|
||
|
||
/// <summary>箱体计算重入 guard,防止联动重算时反复触发 TextChanged。</summary>
|
||
private bool _is箱体Recalculating = false;
|
||
|
||
/// <summary>初始化箱体计算面板:填充下拉框默认项、挂接联动事件、首次计算。</summary>
|
||
private void Init箱体计算面板()
|
||
{
|
||
// ===== 下拉框默认项 =====
|
||
if (this.cbo箱体类型 != null && this.cbo箱体类型.Items.Count == 0)
|
||
{
|
||
this.cbo箱体类型.Items.Clear();
|
||
this.cbo箱体类型.Items.AddRange(new object[] {
|
||
"GGD", "MNS", "GCK", "GCS", "KYN28", "XGN", "HXGN",
|
||
"XL-21", "PGL", "DOMINO", "CUBIC", "8PT", "西门子"
|
||
});
|
||
if (this.cbo箱体类型.Items.Count > 0) this.cbo箱体类型.SelectedIndex = 0;
|
||
}
|
||
if (this.cbo板材 != null && this.cbo板材.Items.Count == 0)
|
||
{
|
||
this.cbo板材.Items.Clear();
|
||
this.cbo板材.Items.AddRange(new object[] {
|
||
"冷轧钢板", "热轧钢板", "镀锌钢板", "不锈钢", "覆铝锌板"
|
||
});
|
||
if (this.cbo板材.Items.Count > 0) this.cbo板材.SelectedIndex = 0;
|
||
}
|
||
if (this.cbo板厚 != null && this.cbo板厚.Items.Count == 0)
|
||
{
|
||
this.cbo板厚.Items.Clear();
|
||
this.cbo板厚.Items.AddRange(new object[] {
|
||
"1.0", "1.2", "1.5", "2.0", "2.5", "3.0", "4.0", "5.0"
|
||
});
|
||
if (this.cbo板厚.Items.Count > 0) this.cbo板厚.SelectedIndex = 2; // 默认 1.5
|
||
}
|
||
if (this.cbo型号 != null && this.cbo型号.Items.Count == 0)
|
||
{
|
||
this.cbo型号.Items.Clear();
|
||
this.cbo型号.Items.AddRange(new object[] {
|
||
"标准型", "加强型", "豪华型", "经济型"
|
||
});
|
||
if (this.cbo型号.Items.Count > 0) this.cbo型号.SelectedIndex = 0;
|
||
}
|
||
|
||
// ===== 尺寸输入框默认值(空时填 0,避免 Parse 失败) =====
|
||
if (this.cboHeightH != null && string.IsNullOrWhiteSpace(this.cboHeightH.Text)) this.cboHeightH.Text = "0";
|
||
if (this.cboWidthW != null && string.IsNullOrWhiteSpace(this.cboWidthW.Text)) this.cboWidthW.Text = "0";
|
||
if (this.cboDepthD != null && string.IsNullOrWhiteSpace(this.cboDepthD.Text)) this.cboDepthD.Text = "0";
|
||
|
||
// ===== 挂接联动事件:任一相关输入变化都触发全量重算 =====
|
||
// 尺寸 / 板厚 / 单价 / 密度 变化 → 重算 面积/重量/单台单价
|
||
if (this.cboHeightH != null) this.cboHeightH.TextChanged += (s, e) => Recalc箱体全部();
|
||
if (this.cboWidthW != null) this.cboWidthW.TextChanged += (s, e) => Recalc箱体全部();
|
||
if (this.cboDepthD != null) this.cboDepthD.TextChanged += (s, e) => Recalc箱体全部();
|
||
if (this.cbo板厚 != null) this.cbo板厚.TextChanged += (s, e) => Recalc箱体全部();
|
||
if (this.txt箱体单价 != null) this.txt箱体单价.TextChanged += (s, e) => Recalc单台单价();
|
||
if (this.txt密度 != null) this.txt密度.TextChanged += (s, e) => Recalc箱体重量();
|
||
// 面数变化 → 重算面积(进而重算重量/单价)
|
||
if (this.nud正视 != null) this.nud正视.ValueChanged += (s, e) => Recalc箱体全部();
|
||
if (this.nud侧视 != null) this.nud侧视.ValueChanged += (s, e) => Recalc箱体全部();
|
||
if (this.nud俯视 != null) this.nud俯视.ValueChanged += (s, e) => Recalc箱体全部();
|
||
// 报价系数变化 → 重算单台单价
|
||
if (this.nud报价系数 != null) this.nud报价系数.ValueChanged += (s, e) => Recalc单台单价();
|
||
|
||
// 箱体类型/型号/板材 变化时刷新规格文本
|
||
// 注意:型号/板材/板厚 是否拼入规格,由它各自旁边的复选框(chk型号/chk板材/chk板厚)控制
|
||
if (this.cbo箱体类型 != null) this.cbo箱体类型.SelectedIndexChanged += (s, e) => Update箱体规格();
|
||
if (this.cbo型号 != null) this.cbo型号.SelectedIndexChanged += (s, e) => Update箱体规格();
|
||
// 板材变化时刷新规格文本 + 同步密度(不同材质密度不同)
|
||
if (this.cbo板材 != null) this.cbo板材.SelectedIndexChanged += (s, e) => { Update箱体规格(); Sync密度By板材(); };
|
||
// 复选框状态切换 → 重新拼接规格(勾上才显示对应信息)
|
||
if (this.chk型号 != null) this.chk型号.CheckedChanged += (s, e) => Update箱体规格();
|
||
if (this.chk板材 != null) this.chk板材.CheckedChanged += (s, e) => Update箱体规格();
|
||
if (this.chk板厚 != null) this.chk板厚.CheckedChanged += (s, e) => Update箱体规格();
|
||
// 尺寸/板厚 变化时也同步规格
|
||
if (this.cboHeightH != null) this.cboHeightH.TextChanged += (s, e) => Update箱体规格();
|
||
if (this.cboWidthW != null) this.cboWidthW.TextChanged += (s, e) => Update箱体规格();
|
||
if (this.cboDepthD != null) this.cboDepthD.TextChanged += (s, e) => Update箱体规格();
|
||
if (this.cbo板厚 != null) this.cbo板厚.TextChanged += (s, e) => Update箱体规格();
|
||
|
||
// ===== 工具栏按钮:插入/替换 =====
|
||
if (this.toolStripButton9 != null) this.toolStripButton9.Click += 箱体插入_Click;
|
||
if (this.toolStripButton10 != null) this.toolStripButton10.Click += 箱体替换_Click;
|
||
|
||
// 首次计算(初始化默认值后立即算一次)
|
||
Recalc箱体全部();
|
||
Update箱体规格();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 箱体面积重算:
|
||
/// 面积(㎡) = (H×W×正视 + H×D×侧视 + W×D×俯视) / 1000000
|
||
/// </summary>
|
||
private void Recalc箱体面积()
|
||
{
|
||
if (this.txt面积 == null) return;
|
||
decimal H = TryParseDecimalOrZero(this.cboHeightH != null ? this.cboHeightH.Text : "0");
|
||
decimal W = TryParseDecimalOrZero(this.cboWidthW != null ? this.cboWidthW.Text : "0");
|
||
decimal D = TryParseDecimalOrZero(this.cboDepthD != null ? this.cboDepthD.Text : "0");
|
||
decimal front = this.nud正视 != null ? this.nud正视.Value : 0m;
|
||
decimal side = this.nud侧视 != null ? this.nud侧视.Value : 0m;
|
||
decimal top = this.nud俯视 != null ? this.nud俯视.Value : 0m;
|
||
|
||
// (H*W*正视 + H*D*侧视 + W*D*俯视) / 1000000
|
||
decimal area = (H * W * front + H * D * side + W * D * top) / 1000000m;
|
||
if (area < 0) area = 0m;
|
||
|
||
_is箱体Recalculating = true;
|
||
try
|
||
{
|
||
this.txt面积.Text = area.ToString("0.######");
|
||
}
|
||
finally { _is箱体Recalculating = false; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 箱体重量重算:
|
||
/// 重量(KG) = 面积(㎡) × 板厚(mm) × 密度(g/cm³) / 1000
|
||
/// </summary>
|
||
private void Recalc箱体重量()
|
||
{
|
||
if (this.txt箱体重量 == null) return;
|
||
decimal area = TryParseDecimalOrZero(this.txt面积 != null ? this.txt面积.Text : "0");
|
||
decimal thick = TryParseDecimalOrZero(this.cbo板厚 != null ? this.cbo板厚.Text : "0");
|
||
decimal density = TryParseDecimalOrZero(this.txt密度 != null ? this.txt密度.Text : "0");
|
||
|
||
decimal weight = area * thick * density / 1000m;
|
||
if (weight < 0) weight = 0m;
|
||
|
||
_is箱体Recalculating = true;
|
||
try
|
||
{
|
||
this.txt箱体重量.Text = weight.ToString("0.######");
|
||
}
|
||
finally { _is箱体Recalculating = false; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单台单价重算:
|
||
/// 单台单价(元) = 面积(㎡) × 单价(元/㎡) × 报价系数
|
||
/// </summary>
|
||
private void Recalc单台单价()
|
||
{
|
||
if (this.txt单台单价 == null) return;
|
||
if (_is箱体Recalculating) return;
|
||
decimal area = TryParseDecimalOrZero(this.txt面积 != null ? this.txt面积.Text : "0");
|
||
decimal price = TryParseDecimalOrZero(this.txt箱体单价 != null ? this.txt箱体单价.Text : "0");
|
||
decimal factor = this.nud报价系数 != null ? this.nud报价系数.Value : 1m;
|
||
|
||
decimal total = area * price * factor;
|
||
if (total < 0) total = 0m;
|
||
|
||
_is箱体Recalculating = true;
|
||
try
|
||
{
|
||
this.txt单台单价.Text = total.ToString("0.##");
|
||
}
|
||
finally { _is箱体Recalculating = false; }
|
||
}
|
||
|
||
/// <summary>触发一次完整的箱体重算:面积 → 重量 + 单台单价。</summary>
|
||
private void Recalc箱体全部()
|
||
{
|
||
if (_is箱体Recalculating) return;
|
||
_is箱体Recalculating = true;
|
||
try
|
||
{
|
||
// 临时解除 guard,让三个方法各自能写入结果
|
||
_is箱体Recalculating = false;
|
||
Recalc箱体面积();
|
||
_is箱体Recalculating = false;
|
||
Recalc箱体重量();
|
||
_is箱体Recalculating = false;
|
||
Recalc单台单价();
|
||
}
|
||
finally
|
||
{
|
||
_is箱体Recalculating = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据当前类型/板材/尺寸/板厚自动生成箱体规格文本。
|
||
/// 默认只显示尺寸(如 "700×900×280");其它信息(型号/板材/板厚)
|
||
/// 必须勾选对应复选框(chk型号/chk板材/chk板厚)才会追加到规格。
|
||
/// </summary>
|
||
private void Update箱体规格()
|
||
{
|
||
if (this.txt箱体规格 == null) return;
|
||
string model = (this.cbo型号 != null ? (this.cbo型号.Text ?? "") : "").Trim();
|
||
string material = (this.cbo板材 != null ? (this.cbo板材.Text ?? "") : "").Trim();
|
||
string thick = (this.cbo板厚 != null ? (this.cbo板厚.Text ?? "") : "").Trim();
|
||
string H = (this.cboHeightH != null ? (this.cboHeightH.Text ?? "") : "").Trim();
|
||
string W = (this.cboWidthW != null ? (this.cboWidthW.Text ?? "") : "").Trim();
|
||
string D = (this.cboDepthD != null ? (this.cboDepthD.Text ?? "") : "").Trim();
|
||
|
||
// 复选框:勾上才把对应字段拼入规格(默认不勾 → 只显示尺寸)
|
||
bool showModel = this.chk型号 != null && this.chk型号.Checked;
|
||
bool showMaterial = this.chk板材 != null && this.chk板材.Checked;
|
||
bool showThick = this.chk板厚 != null && this.chk板厚.Checked;
|
||
|
||
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
||
// 尺寸始终显示,顺序:宽(W)×高(H)×深(D)
|
||
sb.Append(W).Append("×").Append(H).Append("×").Append(D);
|
||
// 勾选后才追加其它信息(空值跳过)
|
||
if (showModel && !string.IsNullOrEmpty(model)) { sb.Append(" ").Append(model); }
|
||
if (showMaterial && !string.IsNullOrEmpty(material)) { sb.Append(" ").Append(material); }
|
||
if (showThick && !string.IsNullOrEmpty(thick)) { sb.Append(" 板厚").Append(thick); }
|
||
|
||
string spec = sb.ToString().Trim();
|
||
// 多个连续空格合并为一个
|
||
while (spec.Contains(" ")) spec = spec.Replace(" ", " ");
|
||
|
||
this.txt箱体规格.Text = spec;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据当前板材自动填入对应密度(g/cm³)。用户也可手动覆盖。
|
||
/// 冷轧/热轧/镀锌钢板 = 7.85,不锈钢 = 7.93,铝板 = 2.7。
|
||
/// 未匹配的板材保持当前密度不变。
|
||
/// </summary>
|
||
private void Sync密度By板材()
|
||
{
|
||
if (this.txt密度 == null || this.cbo板材 == null) return;
|
||
string material = (this.cbo板材.Text ?? "").Trim();
|
||
string density;
|
||
if (string.IsNullOrEmpty(material)) return;
|
||
else if (material.IndexOf("铝") >= 0) density = "2.7";
|
||
else if (material.IndexOf("不锈钢") >= 0) density = "7.93";
|
||
else if (material.IndexOf("钢") >= 0 || material.IndexOf("铁") >= 0
|
||
|| material.IndexOf("镀锌") >= 0 || material.IndexOf("覆铝锌") >= 0) density = "7.85";
|
||
else return; // 未知板材,保持当前值
|
||
|
||
this.txt密度.Text = density; // 触发重量重算(已有 TextChanged 订阅)
|
||
}
|
||
|
||
/// <summary>
|
||
/// 箱体[插入]:把当前箱体计算结果作为一行元件插入元件表(advancedDataGridView5)。
|
||
/// 需要先在报价页打开项目并选中图号/箱号,否则提示。
|
||
/// 映射到元件表字段:
|
||
/// 元件名称 = "箱体" 规格 = 箱体规格 单位 = "台" 数量 = 1
|
||
/// 表价 = 单台单价 类型 = "箱体" 备注 = 备注
|
||
/// 品牌 = 生产厂商 采购系数/报出系数 = 1
|
||
/// </summary>
|
||
private void 箱体插入_Click(object sender, EventArgs e)
|
||
{
|
||
if (_yuanjianTable == null || string.IsNullOrWhiteSpace(_currentProjectId))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再插入箱体。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// ★ 该箱号还没建过元件表时自动建表(否则 NewRow 后写"序号"列会抛异常)
|
||
if (!确保元件表存在())
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再插入箱体。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
string spec = (this.txt箱体规格 != null ? (this.txt箱体规格.Text ?? "") : "").Trim();
|
||
if (string.IsNullOrWhiteSpace(spec))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "箱体规格为空,请先填写尺寸/类型等参数。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// 单台单价作为表价
|
||
decimal danTaiPrice = TryParseDecimalOrZero(this.txt单台单价 != null ? this.txt单台单价.Text : "0");
|
||
// 数量默认 1 台
|
||
decimal count = 1m;
|
||
string unit = "台";
|
||
string remark = (this.txt备注 != null ? (this.txt备注.Text ?? "") : "").Trim();
|
||
string brand = (this.txt箱体厂商 != null ? (this.txt箱体厂商.Text ?? "") : "").Trim();
|
||
|
||
// 派生列(采购系数=1,报出系数=1)
|
||
decimal caiGouDanJia = danTaiPrice;
|
||
decimal caiGouJia = caiGouDanJia * count;
|
||
decimal baoChuDanJia = caiGouDanJia;
|
||
decimal baoChuJia = baoChuDanJia * count;
|
||
|
||
// ★ 批量插入(2026-10-01):多选箱号时对每个选中箱号执行同样的插入;
|
||
// 单选=false 返回后走原逻辑
|
||
System.Action 插入动作 = delegate
|
||
{
|
||
// 提交正在编辑的单元格
|
||
this.advancedDataGridView5.EndEdit();
|
||
|
||
int selectedIndex = GetSelectedRowIndex(this.advancedDataGridView5);
|
||
|
||
var row = _yuanjianTable.NewRow();
|
||
row["序号"] = "1"; // InsertRowBelowSelected 会自动重编号
|
||
row["元件名称"] = "箱体";
|
||
row["规格"] = spec;
|
||
row["单位"] = unit;
|
||
row["数量"] = (double)count;
|
||
row["表价"] = (double)danTaiPrice;
|
||
row["采购系数"] = "1";
|
||
row["采购单价"] = (double)caiGouDanJia;
|
||
row["采购价"] = (double)caiGouJia;
|
||
row["报出系数"] = "1";
|
||
row["报出单价"] = (double)baoChuDanJia;
|
||
row["报出价"] = (double)baoChuJia;
|
||
row["图纸规格"] = "";
|
||
row["品牌"] = brand;
|
||
row["系列"] = "";
|
||
row["类型"] = "箱体";
|
||
row["备注"] = remark;
|
||
|
||
InsertRowBelowSelected(_yuanjianTable, row, selectedIndex);
|
||
|
||
// ★ 虚拟模式收尾:同步网格行数+全表保存,并重算聚合
|
||
元件表插行收尾();
|
||
重算聚合();
|
||
|
||
// 滚动到新行
|
||
int idx = _yuanjianTable.Rows.IndexOf(row);
|
||
if (idx >= 0)
|
||
{
|
||
try { this.advancedDataGridView5.FirstDisplayedScrollingRowIndex = idx; } catch { }
|
||
}
|
||
};
|
||
if (工具窗口批量插入(插入动作)) return;
|
||
插入动作();
|
||
Log("[箱体插入] 已插入到元件表: 规格=" + spec + " 单台单价=" + danTaiPrice);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 箱体[替换]:用当前箱体计算结果覆盖元件表选中行的内容。
|
||
/// 仅当选中行的"类型"列值为"箱体"时才允许替换;
|
||
/// 类型为空或不是"箱体"时提示并跳过(不覆盖非箱体元件)。
|
||
/// </summary>
|
||
private void 箱体替换_Click(object sender, EventArgs e)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"请先在报价软件中打开一个项目并选择图号/箱号,再替换箱体。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
string spec0 = (this.txt箱体规格 != null ? (this.txt箱体规格.Text ?? "") : "").Trim();
|
||
if (string.IsNullOrWhiteSpace(spec0))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "箱体规格为空,请先填写尺寸/类型等参数。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// ★ 选型页分支:选型页有选中行→目标=选型页选中行(判定用"有选中行"防焦点漂移)
|
||
if (替换目标在选型页())
|
||
{
|
||
decimal 价0 = TryParseDecimalOrZero(this.txt单台单价 != null ? this.txt单台单价.Text : "0");
|
||
库替换走选型页("箱体", spec0, "", "", 价0.ToString(System.Globalization.CultureInfo.InvariantCulture), "1", "箱体");
|
||
return;
|
||
}
|
||
|
||
if (_yuanjianTable == null)
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "请先在报价软件中选择图号/箱号。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
DataGridViewRow dgvRow = GetFirstSelectedRow(this.advancedDataGridView5);
|
||
if (dgvRow == null)
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "请先在元件表中选择一行要替换的箱体。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
// 类型判定:仅"箱体"行才允许替换
|
||
string 当前类型 = GetCellValueAsString(dgvRow, "类型");
|
||
if (!string.Equals(当前类型, "箱体", System.StringComparison.Ordinal))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this,
|
||
"选中行的类型为「" + (string.IsNullOrWhiteSpace(当前类型) ? "(空)" : 当前类型) +
|
||
"」,不是「箱体」。只允许替换类型为箱体的行。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
string spec = (this.txt箱体规格 != null ? (this.txt箱体规格.Text ?? "") : "").Trim();
|
||
if (string.IsNullOrWhiteSpace(spec))
|
||
{
|
||
System.Windows.Forms.MessageBox.Show(this, "箱体规格为空,请先填写尺寸/类型等参数。", "提示",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
this.advancedDataGridView5.EndEdit();
|
||
|
||
decimal danTaiPrice = TryParseDecimalOrZero(this.txt单台单价 != null ? this.txt单台单价.Text : "0");
|
||
decimal count = 1m;
|
||
string unit = "台";
|
||
string remark = (this.txt备注 != null ? (this.txt备注.Text ?? "") : "").Trim();
|
||
string brand = (this.txt箱体厂商 != null ? (this.txt箱体厂商.Text ?? "") : "").Trim();
|
||
|
||
decimal caiGouDanJia = danTaiPrice;
|
||
decimal caiGouJia = caiGouDanJia * count;
|
||
decimal baoChuDanJia = caiGouDanJia;
|
||
decimal baoChuJia = baoChuDanJia * count;
|
||
|
||
// 序号保留原值,其余字段覆盖
|
||
SetCellValue(dgvRow, "元件名称", "箱体");
|
||
SetCellValue(dgvRow, "规格", spec);
|
||
SetCellValue(dgvRow, "单位", unit);
|
||
SetCellValue(dgvRow, "数量", (double)count);
|
||
SetCellValue(dgvRow, "表价", (double)danTaiPrice);
|
||
SetCellValue(dgvRow, "采购系数", "1");
|
||
SetCellValue(dgvRow, "采购单价", (double)caiGouDanJia);
|
||
SetCellValue(dgvRow, "采购价", (double)caiGouJia);
|
||
SetCellValue(dgvRow, "报出系数", "1");
|
||
SetCellValue(dgvRow, "报出单价", (double)baoChuDanJia);
|
||
SetCellValue(dgvRow, "报出价", (double)baoChuJia);
|
||
SetCellValue(dgvRow, "品牌", brand);
|
||
SetCellValue(dgvRow, "类型", "箱体");
|
||
SetCellValue(dgvRow, "备注", remark);
|
||
|
||
Log("[箱体替换] 已替换元件表选中行: 规格=" + spec + " 单台单价=" + danTaiPrice);
|
||
}
|
||
|
||
}
|
||
|
||
// ============================================================
|
||
// 简单输入对话框(新建图号/箱号/元件时输入名称)
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 一个极简的输入对话框:返回用户输入的字符串,
|
||
/// 点击取消或关闭窗口返回 null。
|
||
/// </summary>
|
||
internal static class InputDialog
|
||
{
|
||
public static string Show(IWin32Window owner, string title, string label, string defaultValue)
|
||
{
|
||
using (var form = new Form())
|
||
{
|
||
form.Text = title;
|
||
form.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
form.StartPosition = FormStartPosition.CenterParent;
|
||
form.MaximizeBox = false;
|
||
form.MinimizeBox = false;
|
||
form.ClientSize = new System.Drawing.Size(360, 120);
|
||
|
||
var lbl = new Label();
|
||
lbl.Text = label;
|
||
lbl.AutoSize = true;
|
||
lbl.Location = new System.Drawing.Point(12, 15);
|
||
|
||
var txt = new TextBox();
|
||
txt.Text = defaultValue ?? "";
|
||
txt.Location = new System.Drawing.Point(12, 40);
|
||
txt.Size = new System.Drawing.Size(330, 22);
|
||
txt.SelectAll();
|
||
|
||
var btnOk = new Button();
|
||
btnOk.Text = "确定";
|
||
btnOk.DialogResult = DialogResult.OK;
|
||
btnOk.Location = new System.Drawing.Point(180, 80);
|
||
btnOk.Size = new System.Drawing.Size(75, 28);
|
||
|
||
var btnCancel = new Button();
|
||
btnCancel.Text = "取消";
|
||
btnCancel.DialogResult = DialogResult.Cancel;
|
||
btnCancel.Location = new System.Drawing.Point(267, 80);
|
||
btnCancel.Size = new System.Drawing.Size(75, 28);
|
||
|
||
form.Controls.AddRange(new Control[] { lbl, txt, btnOk, btnCancel });
|
||
form.AcceptButton = btnOk;
|
||
form.CancelButton = btnCancel;
|
||
|
||
if (form.ShowDialog(owner) == DialogResult.OK)
|
||
{
|
||
return txt.Text;
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|