首次提交: OnebotCatalog 项目代码与文档(含 NX 按需生成服务二期、后台、一键启动)
This commit is contained in:
122
src/App/AdminWindow.cs
Normal file
122
src/App/AdminWindow.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
#if !CUSTOMER_BUILD
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ounibo.Catalog.Core;
|
||||
|
||||
namespace Ounibo.Catalog.App
|
||||
{
|
||||
/// <summary>
|
||||
/// 维护入口 (M1): 数据包信息 + 系列清单 + 加载/替换 + 资源完整性校验。
|
||||
/// M2 将扩展 Builder 编辑功能。
|
||||
/// </summary>
|
||||
public class AdminWindow : Window
|
||||
{
|
||||
TextBlock _info;
|
||||
DataGrid _seriesGrid;
|
||||
ListBox _report;
|
||||
MainWindow _owner;
|
||||
|
||||
public AdminWindow(MainWindow owner)
|
||||
{
|
||||
_owner = owner;
|
||||
Title = "维护入口 - 数据包管理 / Admin - Package";
|
||||
Width = 640; Height = 520; MinWidth = 520; MinHeight = 400;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
Owner = owner;
|
||||
|
||||
var panel = new DockPanel { Margin = new Thickness(12) };
|
||||
_info = new TextBlock();
|
||||
DockPanel.SetDock(_info, Dock.Top);
|
||||
panel.Children.Add(_info);
|
||||
|
||||
var btns = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 8, 0, 8) };
|
||||
var btnLoad = new Button { Content = "加载新数据包 / Load Package", Width = 170, Margin = new Thickness(0, 0, 8, 0) };
|
||||
btnLoad.Click += (s, e) => LoadPackage();
|
||||
var btnCheck = new Button { Content = "校验 / Validate", Width = 120, Margin = new Thickness(0, 0, 8, 0) };
|
||||
btnCheck.Click += (s, e) => Validate();
|
||||
var btnBuilder = new Button { Content = "目录制作器 / Builder", Width = 150, Margin = new Thickness(0, 0, 8, 0) };
|
||||
btnBuilder.Click += (s, e) => { var w = new BuilderWindow(_owner); w.ShowDialog(); };
|
||||
var btnClose = new Button { Content = "关闭 / Close", Width = 90 };
|
||||
btnClose.Click += (s, e) => Close();
|
||||
btns.Children.Add(btnLoad); btns.Children.Add(btnCheck); btns.Children.Add(btnBuilder); btns.Children.Add(btnClose);
|
||||
DockPanel.SetDock(btns, Dock.Top);
|
||||
panel.Children.Add(btns);
|
||||
|
||||
_seriesGrid = new DataGrid { IsReadOnly = true, AutoGenerateColumns = false, Height = 220, Margin = new Thickness(0, 4, 0, 4) };
|
||||
_seriesGrid.Columns.Add(new DataGridTextColumn { Header = "系列代码", Binding = new System.Windows.Data.Binding("Code"), Width = 80 });
|
||||
_seriesGrid.Columns.Add(new DataGridTextColumn { Header = "中文名", Binding = new System.Windows.Data.Binding("Zh"), Width = 210 });
|
||||
_seriesGrid.Columns.Add(new DataGridTextColumn { Header = "变体数", Binding = new System.Windows.Data.Binding("Variants"), Width = 70 });
|
||||
_seriesGrid.Columns.Add(new DataGridTextColumn { Header = "参数", Binding = new System.Windows.Data.Binding("Params"), Width = 70 });
|
||||
_seriesGrid.Columns.Add(new DataGridTextColumn { Header = "规则", Binding = new System.Windows.Data.Binding("Rules"), Width = 60 });
|
||||
_seriesGrid.Columns.Add(new DataGridTextColumn { Header = "附件", Binding = new System.Windows.Data.Binding("Atts"), Width = 60 });
|
||||
panel.Children.Add(_seriesGrid);
|
||||
|
||||
_report = new ListBox();
|
||||
panel.Children.Add(_report);
|
||||
|
||||
Content = panel;
|
||||
RefreshInfo();
|
||||
}
|
||||
|
||||
void RefreshInfo()
|
||||
{
|
||||
var p = _owner.Package;
|
||||
if (p == null)
|
||||
{
|
||||
_info.Text = "未加载数据包";
|
||||
_seriesGrid.ItemsSource = null;
|
||||
return;
|
||||
}
|
||||
var c = p.Catalog;
|
||||
_info.Text = string.Format(
|
||||
"数据包: {0} 文件: {1}\n目录版本: {2} | 格式版本: {3} 语言: {4}\n系列数: {5} | 变体数: {6} (与主窗口当前加载一致)",
|
||||
c.catalogName, System.IO.Path.GetFileName(p.Path), c.catalogVersion, c.schemaVersion,
|
||||
string.Join(",", c.langs ?? new List<string>()),
|
||||
c.series.Count, c.series.Sum(s => s.variants == null ? 0 : s.variants.Count));
|
||||
var dt = new DataTable();
|
||||
dt.Columns.Add("Code"); dt.Columns.Add("Zh"); dt.Columns.Add("Variants", typeof(int));
|
||||
dt.Columns.Add("Params", typeof(int)); dt.Columns.Add("Rules", typeof(int)); dt.Columns.Add("Atts", typeof(int));
|
||||
foreach (var s in c.series)
|
||||
dt.Rows.Add(s.code, s.nameZh, s.variants == null ? 0 : s.variants.Count,
|
||||
s.parameters == null ? 0 : s.parameters.Count, s.rules == null ? 0 : s.rules.Count,
|
||||
s.attachments == null ? 0 : s.attachments.Count);
|
||||
_seriesGrid.ItemsSource = dt.DefaultView;
|
||||
}
|
||||
|
||||
void Validate()
|
||||
{
|
||||
var p = _owner.Package;
|
||||
_report.Items.Clear();
|
||||
if (p == null) { _report.Items.Add("未加载数据包"); return; }
|
||||
var issues = PackageValidator.Validate(p);
|
||||
foreach (var i in issues)
|
||||
_report.Items.Add("[" + i.Severity + "] " + (string.IsNullOrEmpty(i.Target) ? "" : i.Target + " ") + i.Message);
|
||||
_report.Items.Add(string.Format("校验完成: {0} 个错误, {1} 个警告, 共 {2} 个变体",
|
||||
issues.Count(x => x.Severity == "ERROR"), issues.Count(x => x.Severity == "WARNING"),
|
||||
p.Catalog.series.Sum(s => s.variants == null ? 0 : s.variants.Count)));
|
||||
}
|
||||
|
||||
void LoadPackage()
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Filter = "目录数据包 (*.opc)|*.opc",
|
||||
Title = "选择 .opc 数据包"
|
||||
};
|
||||
if (dlg.ShowDialog() == true)
|
||||
{
|
||||
if (_owner.ReplacePackage(dlg.FileName))
|
||||
{
|
||||
MessageBox.Show("数据包已替换", "维护入口", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
RefreshInfo();
|
||||
_report.Items.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
284
src/App/App.cs
Normal file
284
src/App/App.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ounibo.Catalog.Core;
|
||||
|
||||
namespace Ounibo.Catalog.App
|
||||
{
|
||||
/// <summary>嵌入资源加载 (logo 等编译进 exe 的资源)。</summary>
|
||||
public static class AppResources
|
||||
{
|
||||
public static System.Windows.Media.Imaging.BitmapImage LoadPng(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name);
|
||||
if (stream == null) return null;
|
||||
var bmp = new System.Windows.Media.Imaging.BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.StreamSource = stream;
|
||||
bmp.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
||||
bmp.EndInit();
|
||||
return bmp;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 程序入口。
|
||||
/// 开发版: OnebotCatalog.exe [数据包.opc] [--admin] [--selftest 数据包.opc]
|
||||
/// 客户版 (CUSTOMER_BUILD): 启动时先找 exe 旁 catalog.opc, 找不到读嵌入资源;
|
||||
/// 无维护入口、无自测。
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
#if !CUSTOMER_BUILD
|
||||
// 自测模式: 无界面运行核心逻辑, 结果写 selftest.log
|
||||
if (args.Length >= 2 && args[0] == "--selftest")
|
||||
{
|
||||
Environment.ExitCode = SelfTest.Run(args[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// CLI 打包模式 (与向导同一 Builder 内核):
|
||||
// --build <csv> <stepDir> <out.opc> [--series CODE] [--meta 元数据.json]
|
||||
// 无 --meta 时参数自动识别 (无规则/附件/关键词); 结果写 <out 同目录>\build.log
|
||||
if (args.Length >= 4 && args[0] == "--build")
|
||||
{
|
||||
var csv = args[1]; var stepDir = args[2]; var outP = args[3];
|
||||
string seriesCode = "S1";
|
||||
string metaPath = null;
|
||||
for (int i = 4; i + 1 < args.Length; i++)
|
||||
{
|
||||
if (args[i] == "--series") seriesCode = args[i + 1];
|
||||
if (args[i] == "--meta") metaPath = args[i + 1];
|
||||
}
|
||||
|
||||
Series series;
|
||||
var atts = new List<BuildAttachmentSpec>();
|
||||
if (metaPath != null)
|
||||
{
|
||||
var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||||
var meta = ser.Deserialize<SeriesMeta>(File.ReadAllText(metaPath, Encoding.UTF8));
|
||||
series = meta.series ?? new Series();
|
||||
if (series.parameters == null || series.parameters.Count == 0)
|
||||
series.parameters = OpcBuilder.AutoDetectParameters(csv);
|
||||
atts = meta.attachments ?? new List<BuildAttachmentSpec>();
|
||||
}
|
||||
else
|
||||
{
|
||||
series = new Series { parameters = OpcBuilder.AutoDetectParameters(csv) };
|
||||
}
|
||||
series.code = string.IsNullOrEmpty(series.code) ? seriesCode : series.code;
|
||||
series.nameZh = string.IsNullOrEmpty(series.nameZh) ? seriesCode : series.nameZh;
|
||||
series.nameEn = string.IsNullOrEmpty(series.nameEn) ? seriesCode : series.nameEn;
|
||||
series.naming = series.naming ?? new Naming { stepNameTemplate = "{model}.step" };
|
||||
series.attachments = series.attachments ?? new List<Attachment>();
|
||||
series.variants = new List<Variant>();
|
||||
|
||||
var res = OpcBuilder.Build(outP, "欧霓博气动目录", "0.1", "ACT", "产品", "Products",
|
||||
series, csv, stepDir, atts);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(res.Ok ? "BUILD OK: " + outP : "BUILD FAILED");
|
||||
foreach (var e in res.Errors) sb.AppendLine(" [错误] " + e);
|
||||
foreach (var w in res.Warnings) sb.AppendLine(" [警告] " + w);
|
||||
if (res.Ok)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var built = OpcPackage.Open(outP))
|
||||
{
|
||||
var issues = PackageValidator.Validate(built);
|
||||
sb.AppendLine("变体数: " + built.Catalog.series.Sum(s2 => s2.variants.Count));
|
||||
sb.AppendLine("校验: " + issues.Count(i => i.Severity == "ERROR") + " 错误, " + issues.Count(i => i.Severity == "WARNING") + " 警告");
|
||||
foreach (var i in issues) sb.AppendLine(" [" + i.Severity + "] " + i.Target + " " + i.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { sb.AppendLine("产物校验失败: " + ex.Message); Environment.ExitCode = 1; return; }
|
||||
}
|
||||
else Environment.ExitCode = 1;
|
||||
File.WriteAllText(Path.Combine(Path.GetDirectoryName(outP) ?? ".", "build.log"), sb.ToString(), Encoding.UTF8);
|
||||
return;
|
||||
}
|
||||
|
||||
// CLI 全量打包 (统一路线): --buildfull <源目录> <out.opc>
|
||||
// 源目录结构见 FullCatalogBuilder; 结果写 <out 同目录>\build.log
|
||||
if (args.Length >= 3 && args[0] == "--buildfull")
|
||||
{
|
||||
var res = FullCatalogBuilder.BuildFromSourceDir(args[1], args[2]);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(res.Ok ? "BUILDFULL OK: " + args[2] : "BUILDFULL FAILED");
|
||||
foreach (var e in res.Errors) sb.AppendLine(" [错误] " + e);
|
||||
foreach (var w in res.Warnings) sb.AppendLine(" [警告] " + w);
|
||||
if (res.Ok)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var built = OpcPackage.Open(args[2]))
|
||||
{
|
||||
sb.AppendLine("系列数: " + built.Catalog.series.Count + " | 变体数: " + built.Catalog.series.Sum(s2 => s2.variants.Count));
|
||||
var issues = PackageValidator.Validate(built);
|
||||
sb.AppendLine("校验: " + issues.Count(i => i.Severity == "ERROR") + " 错误, " + issues.Count(i => i.Severity == "WARNING") + " 警告");
|
||||
foreach (var i in issues) sb.AppendLine(" [" + i.Severity + "] " + i.Target + " " + i.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { sb.AppendLine("产物校验失败: " + ex.Message); Environment.ExitCode = 1; }
|
||||
}
|
||||
else Environment.ExitCode = 1;
|
||||
File.WriteAllText(Path.Combine(Path.GetDirectoryName(args[2]) ?? ".", "build.log"), sb.ToString(), Encoding.UTF8);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
string opc = null;
|
||||
foreach (var a in args)
|
||||
{
|
||||
if (a.EndsWith(".opc", StringComparison.OrdinalIgnoreCase)) opc = a;
|
||||
}
|
||||
|
||||
#if CUSTOMER_BUILD
|
||||
OpcPackage pkg = null;
|
||||
if (opc != null)
|
||||
{
|
||||
try { pkg = OpcPackage.Open(opc); }
|
||||
catch (Exception ex) { ShowFatal(ex.Message); return; }
|
||||
}
|
||||
else pkg = TryOpenEmbedded();
|
||||
if (pkg == null)
|
||||
{
|
||||
// 无嵌入数据且无外置数据包 → 让用户选择 .opc
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog { Filter = "目录数据包 (*.opc)|*.opc", Title = "选择 .opc 数据包" };
|
||||
if (dlg.ShowDialog() == true)
|
||||
{
|
||||
try { pkg = OpcPackage.Open(dlg.FileName); }
|
||||
catch (Exception ex) { ShowFatal(ex.Message); return; }
|
||||
}
|
||||
else return;
|
||||
}
|
||||
Run(pkg, false);
|
||||
#else
|
||||
bool admin = false;
|
||||
foreach (var a in args) if (a == "--admin") admin = true;
|
||||
if (admin && !PromptPassword())
|
||||
return;
|
||||
if (opc == null)
|
||||
opc = FindDefaultPackage();
|
||||
OpcPackage pkg = null;
|
||||
if (opc != null)
|
||||
{
|
||||
try { pkg = OpcPackage.Open(opc); }
|
||||
catch (Exception ex) { ShowFatal(ex.Message); return; }
|
||||
}
|
||||
else pkg = TryOpenEmbedded();
|
||||
Run(pkg, admin);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void Run(OpcPackage pkg, bool admin)
|
||||
{
|
||||
var app = new Application { ShutdownMode = ShutdownMode.OnMainWindowClose };
|
||||
// 全局异常日志 (crash.log, 与 exe 同目录), UI 异常不崩溃
|
||||
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "crash.log");
|
||||
app.DispatcherUnhandledException += (s, e) =>
|
||||
{
|
||||
try { File.AppendAllText(logPath, DateTime.Now + " UI异常: " + e.Exception + "\r\n", Encoding.UTF8); } catch { }
|
||||
e.Handled = true;
|
||||
};
|
||||
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
|
||||
{
|
||||
try { File.AppendAllText(logPath, DateTime.Now + " 未处理异常: " + e.ExceptionObject + "\r\n", Encoding.UTF8); } catch { }
|
||||
};
|
||||
app.Run(new MainWindow(pkg, admin));
|
||||
}
|
||||
|
||||
static void ShowFatal(string msg)
|
||||
{
|
||||
MessageBox.Show("数据包打开失败: " + msg, "欧霓博气动目录 ONEBOT Catalog", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 先找 exe 旁 catalog.opc (外置数据包, 支持增量更新), 找不到读嵌入资源。
|
||||
/// </summary>
|
||||
static OpcPackage TryOpenEmbedded()
|
||||
{
|
||||
var external = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "catalog.opc");
|
||||
if (File.Exists(external))
|
||||
{
|
||||
try { return OpcPackage.Open(external); }
|
||||
catch { /* 外置包损坏则回退嵌入资源 */ }
|
||||
}
|
||||
try
|
||||
{
|
||||
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("catalog.opc");
|
||||
if (stream != null)
|
||||
return OpcPackage.OpenStream(stream, "内置数据包 (catalog.opc)");
|
||||
}
|
||||
catch { /* 无嵌入资源 */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
#if !CUSTOMER_BUILD
|
||||
static string FindDefaultPackage()
|
||||
{
|
||||
var dirs = new[]
|
||||
{
|
||||
AppDomain.CurrentDomain.BaseDirectory,
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sample"),
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "sample")
|
||||
};
|
||||
foreach (var d in dirs)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(d)) continue;
|
||||
var f = Directory.GetFiles(d, "*.opc").FirstOrDefault();
|
||||
if (f != null) return f;
|
||||
}
|
||||
catch { /* 忽略目录访问失败 */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 维护入口密码。TODO: 移到配置文件, 支持修改。
|
||||
/// </summary>
|
||||
static bool PromptPassword()
|
||||
{
|
||||
var win = new Window
|
||||
{
|
||||
Title = "维护入口 Admin",
|
||||
Width = 360, Height = 180, MinWidth = 320, MinHeight = 150,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen
|
||||
};
|
||||
var panel = new StackPanel { Margin = new Thickness(16) };
|
||||
panel.Children.Add(new TextBlock { Text = "请输入维护密码 / Enter admin password:", Margin = new Thickness(0, 0, 0, 8) });
|
||||
var box = new PasswordBox { Margin = new Thickness(0, 0, 0, 12) };
|
||||
panel.Children.Add(box);
|
||||
var btns = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
var ok = new Button { Content = "确定 OK", Width = 90, Margin = new Thickness(0, 0, 8, 0), IsDefault = true };
|
||||
var cancel = new Button { Content = "取消 Cancel", Width = 90, IsCancel = true };
|
||||
btns.Children.Add(ok); btns.Children.Add(cancel);
|
||||
panel.Children.Add(btns);
|
||||
win.Content = panel;
|
||||
|
||||
bool granted = false;
|
||||
ok.Click += (s, e) =>
|
||||
{
|
||||
if (box.Password == "onebot888") { granted = true; win.Close(); }
|
||||
else MessageBox.Show("密码错误 / Wrong password", "维护入口", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
};
|
||||
win.ShowDialog();
|
||||
return granted;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
840
src/App/BuilderWindow.cs
Normal file
840
src/App/BuilderWindow.cs
Normal file
@@ -0,0 +1,840 @@
|
||||
#if !CUSTOMER_BUILD
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Microsoft.Win32;
|
||||
using Ounibo.Catalog.Core;
|
||||
|
||||
namespace Ounibo.Catalog.App
|
||||
{
|
||||
public class ParamRow
|
||||
{
|
||||
public string Code { get; set; }
|
||||
public string NameZh { get; set; }
|
||||
public string NameEn { get; set; }
|
||||
public string Unit { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Values { get; set; }
|
||||
public string Keywords { get; set; }
|
||||
public string Display { get; set; } // 显示名映射, 格式 值:显示,值:显示 (如 :基本型,M:附磁石 M)
|
||||
}
|
||||
|
||||
public class RuleRow
|
||||
{
|
||||
public string CondParam { get; set; }
|
||||
public string CondOp { get; set; }
|
||||
public string CondValue { get; set; }
|
||||
public string ThenParam { get; set; }
|
||||
public string ThenOp { get; set; }
|
||||
public string ThenValues { get; set; }
|
||||
}
|
||||
|
||||
public class AttachmentRow
|
||||
{
|
||||
public string Kind { get; set; }
|
||||
public string Lang { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string Model { get; set; }
|
||||
}
|
||||
|
||||
public class SeriesChoice
|
||||
{
|
||||
public Series Series;
|
||||
public override string ToString()
|
||||
{
|
||||
return Series.code + " - " + Series.nameZh + " (" + (Series.variants == null ? 0 : Series.variants.Count) + " 变体)";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 维护入口内的 Builder (M2): 5 步向导
|
||||
/// 导入 CSV+STEP 目录 → 参数定义 → 编码确认 → 选型规则 → 附件与生成 .opc。
|
||||
/// </summary>
|
||||
public class BuilderWindow : Window
|
||||
{
|
||||
MainWindow _owner;
|
||||
TextBox _tbSeriesCode, _tbSeriesZh, _tbSeriesEn, _tbCatName, _tbVer;
|
||||
TextBox _tbCsv, _tbStepDir;
|
||||
TextBlock _lbPreviewCount;
|
||||
DataGrid _gridPreview, _gridParams, _gridRules, _gridAttach;
|
||||
TextBox _tbOut;
|
||||
TextBlock _tbResult;
|
||||
TextBox _tbSrcDir;
|
||||
ComboBox _cbSeries;
|
||||
string _loadedMetaPath;
|
||||
BindingList<ParamRow> _paramRows = new BindingList<ParamRow>();
|
||||
BindingList<RuleRow> _ruleRows = new BindingList<RuleRow>();
|
||||
BindingList<AttachmentRow> _attRows = new BindingList<AttachmentRow>();
|
||||
string[] _header;
|
||||
List<string[]> _csvRows = new List<string[]>();
|
||||
|
||||
public BuilderWindow(MainWindow owner)
|
||||
{
|
||||
_owner = owner;
|
||||
Title = "目录制作器 Builder (维护入口)";
|
||||
Width = 900; Height = 720; MinWidth = 760; MinHeight = 560;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
Owner = owner;
|
||||
|
||||
var root = new DockPanel { Margin = new Thickness(10) };
|
||||
|
||||
// 系列与目录信息
|
||||
var info = new Grid { Margin = new Thickness(0, 0, 0, 8) };
|
||||
for (int i = 0; i < 4; i++) info.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
info.RowDefinitions.Add(new RowDefinition()); info.RowDefinitions.Add(new RowDefinition()); info.RowDefinitions.Add(new RowDefinition());
|
||||
_tbSeriesCode = AddField(info, 0, 0, "系列代码 Series code");
|
||||
_tbSeriesZh = AddField(info, 1, 0, "系列中文名 Name (zh)");
|
||||
_tbSeriesEn = AddField(info, 2, 0, "系列英文名 Name (en)");
|
||||
_tbCatName = AddField(info, 3, 0, "分类名 Category");
|
||||
_tbVer = AddField(info, 0, 1, "目录版本 Version");
|
||||
var cfgBtns = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 0) };
|
||||
var btnSaveCfg = new Button { Content = "保存配置", Width = 100, Margin = new Thickness(0, 0, 8, 0) };
|
||||
btnSaveCfg.Click += (s, e) => SaveConfig();
|
||||
var btnLoadCfg = new Button { Content = "加载配置", Width = 100 };
|
||||
btnLoadCfg.Click += (s, e) => LoadConfig();
|
||||
cfgBtns.Children.Add(btnSaveCfg); cfgBtns.Children.Add(btnLoadCfg);
|
||||
Grid.SetColumn(cfgBtns, 1); Grid.SetRow(cfgBtns, 1);
|
||||
info.Children.Add(cfgBtns);
|
||||
DockPanel.SetDock(info, Dock.Top);
|
||||
root.Children.Add(info);
|
||||
|
||||
var tabs = new TabControl();
|
||||
tabs.Items.Add(BuildStep1());
|
||||
tabs.Items.Add(BuildStep2());
|
||||
tabs.Items.Add(BuildStep3());
|
||||
tabs.Items.Add(BuildStep4());
|
||||
tabs.Items.Add(BuildStep5());
|
||||
root.Children.Add(tabs);
|
||||
Content = root;
|
||||
ReloadSeriesList();
|
||||
}
|
||||
|
||||
TextBox AddField(Grid g, int col, int row, string hint)
|
||||
{
|
||||
var tb = new TextBox { Margin = new Thickness(0, 0, 8, 4) };
|
||||
Grid.SetColumn(tb, col); Grid.SetRow(tb, row);
|
||||
g.Children.Add(tb);
|
||||
if (!string.IsNullOrEmpty(hint)) tb.ToolTip = hint;
|
||||
return tb;
|
||||
}
|
||||
|
||||
// ---- Step1: 导入 ----
|
||||
TabItem BuildStep1()
|
||||
{
|
||||
var panel = new DockPanel { Margin = new Thickness(6) };
|
||||
|
||||
// 数据源目录 (统一路线): 选目录 → 选系列 → 编辑 → 重建全量目录
|
||||
var srcRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
|
||||
srcRow.Children.Add(new TextBlock { Text = "数据源目录:", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0) });
|
||||
_tbSrcDir = new TextBox { Width = 300, Margin = new Thickness(0, 0, 6, 0), Text = DefaultSourceDir() };
|
||||
var btnSrc = new Button { Content = "选择...", Width = 60, Margin = new Thickness(0, 0, 10, 0) };
|
||||
btnSrc.Click += (s, e) =>
|
||||
{
|
||||
var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "选择目录源目录 (含 catalog.json + series\\ + csv\\ + step\\)" };
|
||||
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
_tbSrcDir.Text = dlg.SelectedPath;
|
||||
ReloadSeriesList();
|
||||
}
|
||||
};
|
||||
_cbSeries = new ComboBox { Width = 150, Margin = new Thickness(0, 0, 10, 0) };
|
||||
_cbSeries.SelectionChanged += (s, e) => OpenSeriesFromSource();
|
||||
var btnRebuild = new Button { Content = "重建全量目录", Width = 120, FontWeight = System.Windows.FontWeights.Bold };
|
||||
btnRebuild.Click += (s, e) => RebuildFull();
|
||||
srcRow.Children.Add(_tbSrcDir); srcRow.Children.Add(btnSrc); srcRow.Children.Add(_cbSeries); srcRow.Children.Add(btnRebuild);
|
||||
DockPanel.SetDock(srcRow, Dock.Top);
|
||||
panel.Children.Add(srcRow);
|
||||
|
||||
var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
|
||||
var btnCsv = new Button { Content = "选择参数表 CSV", Width = 130, Margin = new Thickness(0, 0, 6, 0) };
|
||||
btnCsv.Click += (s, e) => LoadCsv();
|
||||
_tbCsv = new TextBox { Width = 320, Margin = new Thickness(0, 0, 10, 0), IsReadOnly = true };
|
||||
var btnDir = new Button { Content = "选择 STEP 目录", Width = 130, Margin = new Thickness(0, 0, 6, 0) };
|
||||
btnDir.Click += (s, e) => PickStepDir();
|
||||
_tbStepDir = new TextBox { Width = 240, IsReadOnly = true };
|
||||
top.Children.Add(btnCsv); top.Children.Add(_tbCsv); top.Children.Add(btnDir); top.Children.Add(_tbStepDir);
|
||||
var btnImport = new Button { Content = "从当前数据包导入系列", Width = 170, Margin = new Thickness(10, 0, 0, 0) };
|
||||
btnImport.Click += (s, e) => ImportFromPackage();
|
||||
top.Children.Add(btnImport);
|
||||
DockPanel.SetDock(top, Dock.Top);
|
||||
panel.Children.Add(top);
|
||||
|
||||
_lbPreviewCount = new TextBlock { Margin = new Thickness(0, 0, 0, 4) };
|
||||
DockPanel.SetDock(_lbPreviewCount, Dock.Top);
|
||||
panel.Children.Add(_lbPreviewCount);
|
||||
|
||||
_gridPreview = new DataGrid { IsReadOnly = true, AutoGenerateColumns = true };
|
||||
panel.Children.Add(_gridPreview);
|
||||
var tab = new TabItem { Header = "1. 导入 Import" };
|
||||
tab.Content = panel;
|
||||
return tab;
|
||||
}
|
||||
|
||||
void LoadCsv()
|
||||
{
|
||||
var dlg = new OpenFileDialog { Filter = "CSV 参数表 (*.csv)|*.csv", Title = "选择参数表 (首列=型号编码, 含 step_file 列)" };
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
try
|
||||
{
|
||||
_tbCsv.Text = dlg.FileName;
|
||||
PreviewCsv(dlg.FileName);
|
||||
AutoParams();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("CSV 读取失败: " + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewCsv(string path)
|
||||
{
|
||||
var lines = File.ReadAllLines(path, System.Text.Encoding.UTF8).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray();
|
||||
_header = lines[0].Split(',').Select(x => x.Trim()).ToArray();
|
||||
_csvRows = lines.Skip(1).Select(l => l.Split(',')).Where(c => c.Length >= _header.Length && !string.IsNullOrWhiteSpace(c[0])).ToList();
|
||||
|
||||
var dt = new DataTable();
|
||||
foreach (var h in _header) dt.Columns.Add(h);
|
||||
foreach (var r in _csvRows.Take(50))
|
||||
{
|
||||
var row = dt.NewRow();
|
||||
for (int i = 0; i < _header.Length; i++) row[i] = r[i];
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
_gridPreview.ItemsSource = dt.DefaultView;
|
||||
_lbPreviewCount.Text = string.Format("共 {0} 行数据, 预览前 50 行", _csvRows.Count);
|
||||
}
|
||||
|
||||
// ---- 从当前数据包导入系列: 向导数据与运行目录保持一致 ----
|
||||
void ImportFromPackage()
|
||||
{
|
||||
var pkg = _owner.Package;
|
||||
if (pkg == null) { MessageBox.Show("主窗口未加载数据包, 请先打开 .opc", Title); return; }
|
||||
var cat = pkg.Catalog;
|
||||
|
||||
var picker = new Window { Title = "从当前数据包选择系列", Width = 420, Height = 420, MinWidth = 300, MinHeight = 300, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this };
|
||||
var list = new ListBox { Margin = new Thickness(8) };
|
||||
foreach (var s in cat.series) list.Items.Add(new SeriesChoice { Series = s });
|
||||
var ok = new Button { Content = "确定 OK", Width = 90, IsDefault = true, Margin = new Thickness(8), HorizontalAlignment = HorizontalAlignment.Right };
|
||||
var dp = new DockPanel();
|
||||
ok.Click += (s2, e2) => picker.DialogResult = true;
|
||||
DockPanel.SetDock(ok, Dock.Bottom);
|
||||
dp.Children.Add(ok); dp.Children.Add(list);
|
||||
picker.Content = dp;
|
||||
if (picker.ShowDialog() != true || list.SelectedItem == null) return;
|
||||
var series = ((SeriesChoice)list.SelectedItem).Series;
|
||||
|
||||
try
|
||||
{
|
||||
var tmp = Path.Combine(Path.GetTempPath(), "ounibo_builder", series.code);
|
||||
Directory.CreateDirectory(Path.Combine(tmp, "step"));
|
||||
|
||||
// CSV: model_code + 参数列 + step_file
|
||||
var sb = new StringBuilder();
|
||||
var header = new List<string> { "model_code" };
|
||||
foreach (var p in series.parameters ?? new List<ParameterDef>()) header.Add(p.code);
|
||||
header.Add("step_file");
|
||||
sb.AppendLine(string.Join(",", header));
|
||||
foreach (var v in series.variants ?? new List<Variant>())
|
||||
{
|
||||
var row = new List<string> { v.modelCode };
|
||||
foreach (var p in series.parameters ?? new List<ParameterDef>())
|
||||
{
|
||||
object val; string vs = v.@params != null && v.@params.TryGetValue(p.code, out val) ? Convert.ToString(val, CultureInfo.InvariantCulture) : "";
|
||||
if (vs.Contains(",") || vs.Contains("\"")) vs = "\"" + vs.Replace("\"", "\"\"") + "\"";
|
||||
row.Add(vs);
|
||||
}
|
||||
row.Add(Path.GetFileName((v.step ?? "").Replace('\\', '/')));
|
||||
sb.AppendLine(string.Join(",", row));
|
||||
}
|
||||
var csvPath = Path.Combine(tmp, series.code + ".csv");
|
||||
File.WriteAllText(csvPath, sb.ToString(), Encoding.UTF8);
|
||||
|
||||
// STEP 导出 (去重)
|
||||
var seen = new HashSet<string>();
|
||||
foreach (var v in series.variants ?? new List<Variant>())
|
||||
{
|
||||
if (v == null || string.IsNullOrEmpty(v.step) || !seen.Add(v.step)) continue;
|
||||
try { File.WriteAllBytes(Path.Combine(tmp, "step", Path.GetFileName(v.step.Replace('\\', '/'))), pkg.ReadAsset(v.step)); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
// 填向导字段
|
||||
_tbSeriesCode.Text = series.code;
|
||||
_tbSeriesZh.Text = series.nameZh ?? "";
|
||||
_tbSeriesEn.Text = series.nameEn ?? "";
|
||||
_tbVer.Text = cat.catalogVersion ?? "";
|
||||
var catName = cat.categories.FirstOrDefault(c2 => c2.series != null && c2.series.Contains(series.code));
|
||||
_tbCatName.Text = catName != null ? (catName.nameZh ?? "") : "";
|
||||
_tbCsv.Text = csvPath;
|
||||
_tbStepDir.Text = Path.Combine(tmp, "step");
|
||||
PreviewCsv(csvPath);
|
||||
|
||||
// 参数行
|
||||
_paramRows.Clear();
|
||||
foreach (var p in series.parameters ?? new List<ParameterDef>())
|
||||
{
|
||||
_paramRows.Add(new ParamRow
|
||||
{
|
||||
Code = p.code,
|
||||
NameZh = p.nameZh ?? "",
|
||||
NameEn = p.nameEn ?? "",
|
||||
Unit = p.unit ?? "",
|
||||
Type = p.type == "number" ? "number" : "enum",
|
||||
Values = string.Join(",", (p.values ?? new List<object>()).Select(x => Convert.ToString(x, CultureInfo.InvariantCulture))),
|
||||
Keywords = p.keywords ?? ""
|
||||
});
|
||||
}
|
||||
_gridParams.ItemsSource = _paramRows;
|
||||
|
||||
// 规则行
|
||||
_ruleRows.Clear();
|
||||
foreach (var r in series.rules ?? new List<Ounibo.Catalog.Core.Rule>())
|
||||
{
|
||||
_ruleRows.Add(new RuleRow
|
||||
{
|
||||
CondParam = r.If != null ? r.If.Param : "",
|
||||
CondOp = r.If != null ? r.If.Op : "eq",
|
||||
CondValue = r.If != null ? ValToStr(r.If.Value) : "",
|
||||
ThenParam = r.Then != null ? r.Then.Param : "",
|
||||
ThenOp = r.Then != null ? r.Then.Op : "in",
|
||||
ThenValues = r.Then != null ? ValToStr(r.Then.Value) : ""
|
||||
});
|
||||
}
|
||||
_gridRules.ItemsSource = _ruleRows;
|
||||
|
||||
// 附件 (manual 等包内附件导出为文件)
|
||||
_attRows.Clear();
|
||||
foreach (var a in series.attachments ?? new List<Attachment>())
|
||||
{
|
||||
if (string.IsNullOrEmpty(a.path)) continue;
|
||||
var f = Path.Combine(tmp, "manual", Path.GetFileName(a.path.Replace('\\', '/')));
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(f));
|
||||
File.WriteAllBytes(f, pkg.ReadAsset(a.path));
|
||||
_attRows.Add(new AttachmentRow { Kind = a.kind, Lang = a.lang ?? "", Source = f, Model = "" });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
_gridAttach.ItemsSource = _attRows;
|
||||
|
||||
_tbOut.Text = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "sample", series.code + "_edited.opc");
|
||||
MessageBox.Show(string.Format("已导入系列 {0}: {1} 个变体, {2} 个参数, {3} 条规则, {4} 个附件\n\n源文件在 {5}", series.code,
|
||||
series.variants == null ? 0 : series.variants.Count, _paramRows.Count, _ruleRows.Count, _attRows.Count, tmp), Title);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("导入失败: " + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
void PickStepDir()
|
||||
{
|
||||
var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "选择 STEP 文件目录" };
|
||||
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
_tbStepDir.Text = dlg.SelectedPath;
|
||||
}
|
||||
|
||||
// ---- 数据源目录 (统一路线) ----
|
||||
|
||||
string DefaultSourceDir()
|
||||
{
|
||||
// exe: <工作区>\OnebotCatalog\bin\ → 源目录 OnebotCatalog\onebot-data\catalog (自包含布局)
|
||||
return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "onebot-data", "catalog"));
|
||||
}
|
||||
|
||||
string GetSourceDir()
|
||||
{
|
||||
var d = _tbSrcDir != null ? _tbSrcDir.Text.Trim() : "";
|
||||
return Directory.Exists(d) ? d : null;
|
||||
}
|
||||
|
||||
void ReloadSeriesList()
|
||||
{
|
||||
_cbSeries.Items.Clear();
|
||||
var src = GetSourceDir();
|
||||
if (src == null) return;
|
||||
var dir = Path.Combine(src, "series");
|
||||
if (!Directory.Exists(dir)) return;
|
||||
foreach (var f in Directory.GetFiles(dir, "*.meta.json").OrderBy(x => x))
|
||||
{
|
||||
// KC.meta.json → KC (GetFileNameWithoutExtension 只剥 .json, 需再剥 .meta)
|
||||
_cbSeries.Items.Add(Path.GetFileName(f).Replace(".meta.json", ""));
|
||||
}
|
||||
}
|
||||
|
||||
void OpenSeriesFromSource()
|
||||
{
|
||||
if (_cbSeries.SelectedItem == null) return;
|
||||
var src = GetSourceDir();
|
||||
if (src == null) return;
|
||||
var code = (string)_cbSeries.SelectedItem;
|
||||
try
|
||||
{
|
||||
LoadMetaFile(Path.Combine(src, "series", code + ".meta.json"));
|
||||
var csvPath = Path.Combine(src, "csv", code + ".csv");
|
||||
if (File.Exists(csvPath)) { _tbCsv.Text = csvPath; PreviewCsv(csvPath); }
|
||||
_tbStepDir.Text = Path.Combine(src, "step");
|
||||
_tbVer.Text = ReadCatalogVersion(src) ?? "";
|
||||
_tbOut.Text = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "sample", code + "_edited.opc");
|
||||
}
|
||||
catch (Exception ex) { MessageBox.Show("打开系列失败: " + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error); }
|
||||
}
|
||||
|
||||
string ReadCatalogVersion(string src)
|
||||
{
|
||||
try
|
||||
{
|
||||
var f = Path.Combine(src, "catalog.json");
|
||||
if (!File.Exists(f)) return null;
|
||||
var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||||
var cat = ser.Deserialize<Ounibo.Catalog.Core.Catalog>(File.ReadAllText(f, Encoding.UTF8));
|
||||
return cat == null ? null : cat.catalogVersion;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
void RebuildFull()
|
||||
{
|
||||
var src = GetSourceDir();
|
||||
if (src == null) { MessageBox.Show("数据源目录无效: " + (_tbSrcDir == null ? "" : _tbSrcDir.Text) + "\n(需含 catalog.json + series\\ + csv\\ + step\\)", Title); return; }
|
||||
var ver = ReadCatalogVersion(src) ?? "0.1";
|
||||
var outPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "sample", "OnebotCatalog_" + ver + ".opc");
|
||||
var res = FullCatalogBuilder.BuildFromSourceDir(src, outPath);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(res.Ok ? "全量重建成功: " + outPath : "全量重建失败");
|
||||
foreach (var e in res.Errors) sb.AppendLine(" [错误] " + e);
|
||||
foreach (var w in res.Warnings) sb.AppendLine(" [警告] " + w);
|
||||
if (res.Ok)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var built = OpcPackage.Open(outPath))
|
||||
{
|
||||
sb.AppendLine("系列数: " + built.Catalog.series.Count + " | 变体数: " + built.Catalog.series.Sum(s2 => s2.variants.Count));
|
||||
var issues = PackageValidator.Validate(built);
|
||||
sb.AppendLine("校验: " + issues.Count(i => i.Severity == "ERROR") + " 错误, " + issues.Count(i => i.Severity == "WARNING") + " 警告");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { sb.AppendLine("产物校验失败: " + ex.Message); }
|
||||
}
|
||||
if (res.Ok && MessageBox.Show(sb.ToString() + "\n\n是否加载到主窗口?", Title, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
||||
_owner.ReplacePackage(outPath);
|
||||
else
|
||||
MessageBox.Show(sb.ToString(), Title, MessageBoxButton.OK, res.Ok ? MessageBoxImage.Information : MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
// 自动识别参数 (与 OpcBuilder.AutoDetectParameters 共用逻辑): 数值列 → number, 其他 → enum
|
||||
void AutoParams()
|
||||
{
|
||||
_paramRows.Clear();
|
||||
if (string.IsNullOrWhiteSpace(_tbCsv.Text)) return;
|
||||
foreach (var d in OpcBuilder.AutoDetectParameters(_tbCsv.Text))
|
||||
{
|
||||
_paramRows.Add(new ParamRow
|
||||
{
|
||||
Code = d.code,
|
||||
NameZh = d.code,
|
||||
NameEn = d.code,
|
||||
Unit = d.unit,
|
||||
Type = d.type,
|
||||
Values = string.Join(",", (d.values ?? new List<object>()).Select(v => Convert.ToString(v, CultureInfo.InvariantCulture))),
|
||||
Keywords = d.keywords ?? ""
|
||||
});
|
||||
}
|
||||
_gridParams.ItemsSource = _paramRows;
|
||||
}
|
||||
|
||||
// ---- Step2: 参数定义 ----
|
||||
TabItem BuildStep2()
|
||||
{
|
||||
_gridParams = new DataGrid { AutoGenerateColumns = false, Margin = new Thickness(6) };
|
||||
_gridParams.Columns.Add(new DataGridTextColumn { Header = "参数代码 Code", Binding = new System.Windows.Data.Binding("Code"), IsReadOnly = true, Width = 100 });
|
||||
_gridParams.Columns.Add(new DataGridTextColumn { Header = "中文名 NameZh", Binding = new System.Windows.Data.Binding("NameZh"), Width = 110 });
|
||||
_gridParams.Columns.Add(new DataGridTextColumn { Header = "英文名 NameEn", Binding = new System.Windows.Data.Binding("NameEn"), Width = 110 });
|
||||
_gridParams.Columns.Add(new DataGridTextColumn { Header = "单位 Unit", Binding = new System.Windows.Data.Binding("Unit"), Width = 55 });
|
||||
_gridParams.Columns.Add(new DataGridComboBoxColumn { Header = "类型 Type", SelectedItemBinding = new System.Windows.Data.Binding("Type"), ItemsSource = new[] { "number", "enum" }, Width = 85 });
|
||||
_gridParams.Columns.Add(new DataGridTextColumn { Header = "取值 Values (逗号分隔)", Binding = new System.Windows.Data.Binding("Values"), Width = 170 });
|
||||
_gridParams.Columns.Add(new DataGridTextColumn { Header = "搜索关键词 Keywords (拼音/英文)", Binding = new System.Windows.Data.Binding("Keywords"), Width = 150 });
|
||||
_gridParams.Columns.Add(new DataGridTextColumn { Header = "显示名映射 Display (值:显示,值:显示)", Binding = new System.Windows.Data.Binding("Display"), Width = 200 });
|
||||
_gridParams.ItemsSource = _paramRows;
|
||||
var hint = new TextBlock
|
||||
{
|
||||
Text = "参数定义: Code 不可改; 补全中英文名; Type: number(数值, 支持范围搜索) / enum(枚举); Values 逗号分隔; Keywords 填拼音等供搜索; Display 格式 值:显示,值:显示 (如 :基本型,M:附磁石)。",
|
||||
Margin = new Thickness(6), TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
var panel = new DockPanel();
|
||||
DockPanel.SetDock(hint, Dock.Top);
|
||||
panel.Children.Add(hint);
|
||||
panel.Children.Add(_gridParams);
|
||||
var tab = new TabItem { Header = "2. 参数 Parameters" };
|
||||
tab.Content = panel;
|
||||
return tab;
|
||||
}
|
||||
|
||||
// ---- Step3: 编码确认 ----
|
||||
TabItem BuildStep3()
|
||||
{
|
||||
var panel = new StackPanel { Margin = new Thickness(6) };
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "型号编码 = 参数表首列 (model_code), 直接沿用, 不做模板反解。\nSTEP 文件命名 = 型号编码 + .step (对应 step_file 列)。\n生成时将校验: 编码唯一、STEP 文件存在、参数完整。",
|
||||
TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 8)
|
||||
});
|
||||
var tab = new TabItem { Header = "3. 编码 Coding" };
|
||||
tab.Content = panel;
|
||||
return tab;
|
||||
}
|
||||
|
||||
// ---- Step4: 选型规则 ----
|
||||
TabItem BuildStep4()
|
||||
{
|
||||
var panel = new DockPanel { Margin = new Thickness(6) };
|
||||
var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
|
||||
var btnAdd = new Button { Content = "+ 添加规则", Width = 100, Margin = new Thickness(0, 0, 6, 0) };
|
||||
btnAdd.Click += (s, e) => _ruleRows.Add(new RuleRow { CondParam = "", CondOp = "eq", CondValue = "", ThenParam = "", ThenOp = "in", ThenValues = "" });
|
||||
var btnDel = new Button { Content = "- 删除所选", Width = 100 };
|
||||
btnDel.Click += (s, e) =>
|
||||
{
|
||||
var r = _gridRules.SelectedItem as RuleRow;
|
||||
if (r != null) _ruleRows.Remove(r);
|
||||
};
|
||||
top.Children.Add(btnAdd); top.Children.Add(btnDel);
|
||||
DockPanel.SetDock(top, Dock.Top);
|
||||
panel.Children.Add(top);
|
||||
|
||||
var hint = new TextBlock
|
||||
{
|
||||
Text = "规则: 若 条件(参数 运算符 值) 满足, 则 约束(参数 运算符 值列表) 必须满足, 否则该变体无效。\n例: 条件 bore eq 16 → 约束 stroke in 25,50 (即缸径16时行程仅可选25/50)。in 的值用逗号分隔。",
|
||||
TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 6)
|
||||
};
|
||||
DockPanel.SetDock(hint, Dock.Top);
|
||||
panel.Children.Add(hint);
|
||||
|
||||
_gridRules = new DataGrid { AutoGenerateColumns = false };
|
||||
_gridRules.Columns.Add(new DataGridTextColumn { Header = "条件参数", Binding = new System.Windows.Data.Binding("CondParam"), Width = 105 });
|
||||
_gridRules.Columns.Add(new DataGridComboBoxColumn { Header = "条件运算符", SelectedItemBinding = new System.Windows.Data.Binding("CondOp"), ItemsSource = new[] { "eq", "ne", "in", "gt", "ge", "lt", "le", "between" }, Width = 95 });
|
||||
_gridRules.Columns.Add(new DataGridTextColumn { Header = "条件值", Binding = new System.Windows.Data.Binding("CondValue"), Width = 85 });
|
||||
_gridRules.Columns.Add(new DataGridTextColumn { Header = "约束参数", Binding = new System.Windows.Data.Binding("ThenParam"), Width = 105 });
|
||||
_gridRules.Columns.Add(new DataGridComboBoxColumn { Header = "约束运算符", SelectedItemBinding = new System.Windows.Data.Binding("ThenOp"), ItemsSource = new[] { "eq", "in" }, Width = 95 });
|
||||
_gridRules.Columns.Add(new DataGridTextColumn { Header = "约束值(逗号分隔)", Binding = new System.Windows.Data.Binding("ThenValues"), Width = 150 });
|
||||
_gridRules.ItemsSource = _ruleRows;
|
||||
panel.Children.Add(_gridRules);
|
||||
var tab = new TabItem { Header = "4. 规则 Rules" };
|
||||
tab.Content = panel;
|
||||
return tab;
|
||||
}
|
||||
|
||||
// ---- Step5: 附件与生成 ----
|
||||
TabItem BuildStep5()
|
||||
{
|
||||
var panel = new DockPanel { Margin = new Thickness(6) };
|
||||
var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
|
||||
var btnAtt = new Button { Content = "+ 添加附件", Width = 100, Margin = new Thickness(0, 0, 6, 0) };
|
||||
btnAtt.Click += (s, e) =>
|
||||
{
|
||||
var dlg = new OpenFileDialog { Filter = "附件 (*.png;*.jpg;*.pdf)|*.png;*.jpg;*.pdf", Title = "选择附件 (尺寸图/数据表)" };
|
||||
if (dlg.ShowDialog() == true)
|
||||
_attRows.Add(new AttachmentRow { Kind = "dimDrawing", Lang = "zh", Source = dlg.FileName, Model = "" });
|
||||
};
|
||||
var btnDel = new Button { Content = "- 删除所选", Width = 100, Margin = new Thickness(0, 0, 10, 0) };
|
||||
btnDel.Click += (s, e) =>
|
||||
{
|
||||
var r = _gridAttach.SelectedItem as AttachmentRow;
|
||||
if (r != null) _attRows.Remove(r);
|
||||
};
|
||||
_tbOut = new TextBox { Width = 340, Text = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "sample", "NewCatalog.opc") };
|
||||
top.Children.Add(btnAtt); top.Children.Add(btnDel); top.Children.Add(new TextBlock { Text = "输出 .opc:", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0) }); top.Children.Add(_tbOut);
|
||||
var btnBuild = new Button { Content = "生成数据包 Build", Width = 130, Margin = new Thickness(10, 0, 0, 0) };
|
||||
btnBuild.Click += (s, e) => DoBuild();
|
||||
top.Children.Add(btnBuild);
|
||||
DockPanel.SetDock(top, Dock.Top);
|
||||
panel.Children.Add(top);
|
||||
|
||||
_gridAttach = new DataGrid { AutoGenerateColumns = false, Height = 200 };
|
||||
_gridAttach.Columns.Add(new DataGridComboBoxColumn { Header = "类型 Kind", SelectedItemBinding = new System.Windows.Data.Binding("Kind"), ItemsSource = new[] { "dimDrawing", "datasheet", "note", "manual" }, Width = 110 });
|
||||
_gridAttach.Columns.Add(new DataGridComboBoxColumn { Header = "语言 Lang", SelectedItemBinding = new System.Windows.Data.Binding("Lang"), ItemsSource = new[] { "zh", "en" }, Width = 65 });
|
||||
_gridAttach.Columns.Add(new DataGridTextColumn { Header = "源文件 Source", Binding = new System.Windows.Data.Binding("Source"), IsReadOnly = true, Width = 330 });
|
||||
_gridAttach.Columns.Add(new DataGridTextColumn { Header = "绑定型号 Model(可选)", Binding = new System.Windows.Data.Binding("Model"), Width = 120 });
|
||||
_gridAttach.ItemsSource = _attRows;
|
||||
DockPanel.SetDock(_gridAttach, Dock.Top);
|
||||
panel.Children.Add(_gridAttach);
|
||||
|
||||
_tbResult = new TextBlock { TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 8, 0, 0) };
|
||||
panel.Children.Add(_tbResult);
|
||||
var tab = new TabItem { Header = "5. 附件与生成 Build" };
|
||||
tab.Content = panel;
|
||||
return tab;
|
||||
}
|
||||
|
||||
object ToNumOrStr(string s)
|
||||
{
|
||||
// 前导零的编码 (00/02/03) 保持字符串, 否则会被数值化成 0/2/3 破坏型号匹配
|
||||
if (s.Length > 1 && s.StartsWith("0") && s.All(char.IsDigit)) return s;
|
||||
double d;
|
||||
if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out d)) return d;
|
||||
return s;
|
||||
}
|
||||
|
||||
List<object> ParseValues(string s)
|
||||
{
|
||||
// 注意: 保留空串 (枚举值 "" 合法, 如 磁石=无磁石)
|
||||
return s.Split(new[] { ',', ',', ';', ';' })
|
||||
.Select(x => x.Trim())
|
||||
.Select(x => (object)ToNumOrStr(x))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
Ounibo.Catalog.Core.Rule BuildRule(RuleRow r)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(r.CondParam) || string.IsNullOrWhiteSpace(r.ThenParam)) return null;
|
||||
RuleCond If, Then;
|
||||
If = new RuleCond { Param = r.CondParam.Trim(), Op = r.CondOp, Value = ToNumOrStr(r.CondValue.Trim()) };
|
||||
if (r.ThenOp == "in")
|
||||
Then = new RuleCond { Param = r.ThenParam.Trim(), Op = "in", Value = ParseValues(r.ThenValues).ToArray() };
|
||||
else
|
||||
Then = new RuleCond { Param = r.ThenParam.Trim(), Op = r.ThenOp, Value = ToNumOrStr(r.ThenValues.Trim()) };
|
||||
return new Ounibo.Catalog.Core.Rule { If = If, Then = Then };
|
||||
}
|
||||
|
||||
// ---- 配置保存/加载 (SeriesMeta 格式, 与源目录 series\*.meta.json 及 CLI --meta 完全通用) ----
|
||||
|
||||
SeriesMeta BuildMetaFromUi()
|
||||
{
|
||||
return new SeriesMeta
|
||||
{
|
||||
series = new Series
|
||||
{
|
||||
code = _tbSeriesCode.Text.Trim(),
|
||||
nameZh = _tbSeriesZh.Text.Trim(),
|
||||
nameEn = _tbSeriesEn.Text.Trim(),
|
||||
keywords = "",
|
||||
parameters = _paramRows.Where(p => !string.IsNullOrWhiteSpace(p.Code)).Select(p => new ParameterDef
|
||||
{
|
||||
code = p.Code,
|
||||
nameZh = p.NameZh,
|
||||
nameEn = p.NameEn,
|
||||
unit = p.Unit ?? "",
|
||||
type = p.Type == "number" ? "number" : "enum",
|
||||
keywords = p.Keywords ?? "",
|
||||
values = ParseValues(p.Values ?? ""),
|
||||
display = ParseDisplay(p.Display ?? "")
|
||||
}).ToList(),
|
||||
rules = _ruleRows.Select(BuildRule).Where(r => r != null).ToList(),
|
||||
naming = new Naming { stepNameTemplate = "{model}.step" },
|
||||
attachments = new List<Attachment>(),
|
||||
variants = new List<Variant>()
|
||||
},
|
||||
attachments = _attRows.Where(a => !string.IsNullOrWhiteSpace(a.Source)).Select(a => new BuildAttachmentSpec
|
||||
{
|
||||
kind = a.Kind,
|
||||
lang = a.Lang,
|
||||
source = a.Source,
|
||||
model = string.IsNullOrWhiteSpace(a.Model) ? null : a.Model.Trim()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
Dictionary<string, string> ParseDisplay(string s)
|
||||
{
|
||||
var d = new Dictionary<string, string>();
|
||||
if (string.IsNullOrWhiteSpace(s)) return d;
|
||||
foreach (var part in s.Split(new[] { ',', ',', ';', ';' }))
|
||||
{
|
||||
var kv = part.Split(new[] { ':' }, 2);
|
||||
if (kv.Length == 2) d[kv[0].Trim()] = kv[1].Trim();
|
||||
}
|
||||
return d.Count == 0 ? null : d;
|
||||
}
|
||||
|
||||
string FormatDisplay(Dictionary<string, string> d)
|
||||
{
|
||||
if (d == null || d.Count == 0) return "";
|
||||
return string.Join(",", d.Select(kv => kv.Key + ":" + kv.Value));
|
||||
}
|
||||
|
||||
// 保存到源目录 (series\<code>.meta.json) 或另存为
|
||||
void SaveConfig()
|
||||
{
|
||||
var code = _tbSeriesCode.Text.Trim();
|
||||
var srcDir = GetSourceDir();
|
||||
var target = Directory.Exists(srcDir) && !string.IsNullOrEmpty(code)
|
||||
? Path.Combine(srcDir, "series", code + ".meta.json")
|
||||
: null;
|
||||
if (target == null || (!File.Exists(target) && !string.IsNullOrEmpty(_loadedMetaPath)))
|
||||
{
|
||||
// 系列代码改名 → 沿用已加载文件路径
|
||||
if (!string.IsNullOrEmpty(_loadedMetaPath)) target = _loadedMetaPath;
|
||||
}
|
||||
if (target == null || !File.Exists(target))
|
||||
{
|
||||
var dlg = new SaveFileDialog { Filter = "Builder 配置 (*.json)|*.json", FileName = "builder-config.json", Title = "保存配置" };
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
target = dlg.FileName;
|
||||
}
|
||||
try
|
||||
{
|
||||
var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||||
var meta = BuildMetaFromUi();
|
||||
// 覆盖已有配置时保留系列关键词 (向导无此字段, 避免清空搜索关键词)
|
||||
if (File.Exists(target))
|
||||
{
|
||||
try
|
||||
{
|
||||
var old = ser.Deserialize<SeriesMeta>(File.ReadAllText(target, Encoding.UTF8));
|
||||
if (old != null && old.series != null && string.Equals(old.series.code, meta.series.code, StringComparison.OrdinalIgnoreCase))
|
||||
meta.series.keywords = old.series.keywords;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
||||
File.WriteAllText(target, ser.Serialize(meta), Encoding.UTF8);
|
||||
_loadedMetaPath = target;
|
||||
MessageBox.Show("配置已保存: " + target + (target.IndexOf("series" + Path.DirectorySeparatorChar) >= 0 ? "\n(点【重建全量目录】后生效)" : ""), Title);
|
||||
}
|
||||
catch (Exception ex) { MessageBox.Show("保存失败: " + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error); }
|
||||
}
|
||||
|
||||
static string ValToStr(object v)
|
||||
{
|
||||
var arr = v as object[];
|
||||
if (arr != null) return string.Join(",", arr.Select(x => Convert.ToString(x, CultureInfo.InvariantCulture)));
|
||||
return v == null ? "" : Convert.ToString(v, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
void LoadConfig()
|
||||
{
|
||||
var dlg = new OpenFileDialog { Filter = "Builder 配置 (*.json)|*.json", Title = "加载配置" };
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
try { LoadMetaFile(dlg.FileName); }
|
||||
catch (Exception ex) { MessageBox.Show("加载失败: " + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error); }
|
||||
}
|
||||
|
||||
void LoadMetaFile(string metaPath)
|
||||
{
|
||||
var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||||
var meta = ser.Deserialize<SeriesMeta>(File.ReadAllText(metaPath, Encoding.UTF8));
|
||||
if (meta == null || meta.series == null) { MessageBox.Show("配置格式无效", Title); return; }
|
||||
_loadedMetaPath = metaPath;
|
||||
_tbSeriesCode.Text = meta.series.code ?? "";
|
||||
_tbSeriesZh.Text = meta.series.nameZh ?? "";
|
||||
_tbSeriesEn.Text = meta.series.nameEn ?? "";
|
||||
|
||||
_paramRows.Clear();
|
||||
foreach (var p in meta.series.parameters ?? new List<ParameterDef>())
|
||||
{
|
||||
_paramRows.Add(new ParamRow
|
||||
{
|
||||
Code = p.code,
|
||||
NameZh = p.nameZh,
|
||||
NameEn = p.nameEn,
|
||||
Unit = p.unit ?? "",
|
||||
Type = p.type,
|
||||
Keywords = p.keywords ?? "",
|
||||
Values = string.Join(",", (p.values ?? new List<object>()).Select(v => Convert.ToString(v, CultureInfo.InvariantCulture))),
|
||||
Display = FormatDisplay(p.display)
|
||||
});
|
||||
}
|
||||
_gridParams.ItemsSource = _paramRows;
|
||||
|
||||
_ruleRows.Clear();
|
||||
foreach (var r in meta.series.rules ?? new List<Ounibo.Catalog.Core.Rule>())
|
||||
{
|
||||
_ruleRows.Add(new RuleRow
|
||||
{
|
||||
CondParam = r.If != null ? r.If.Param : "",
|
||||
CondOp = r.If != null ? r.If.Op : "eq",
|
||||
CondValue = r.If != null ? ValToStr(r.If.Value) : "",
|
||||
ThenParam = r.Then != null ? r.Then.Param : "",
|
||||
ThenOp = r.Then != null ? r.Then.Op : "in",
|
||||
ThenValues = r.Then != null ? ValToStr(r.Then.Value) : ""
|
||||
});
|
||||
}
|
||||
_gridRules.ItemsSource = _ruleRows;
|
||||
|
||||
_attRows.Clear();
|
||||
foreach (var a in meta.attachments ?? new List<BuildAttachmentSpec>())
|
||||
{
|
||||
_attRows.Add(new AttachmentRow { Kind = a.kind, Lang = a.lang, Source = a.source, Model = a.model ?? "" });
|
||||
}
|
||||
_gridAttach.ItemsSource = _attRows;
|
||||
}
|
||||
|
||||
void DoBuild()
|
||||
{
|
||||
_tbResult.Text = "";
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("开始打包...");
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
code = _tbSeriesCode.Text.Trim(),
|
||||
nameZh = _tbSeriesZh.Text.Trim(),
|
||||
nameEn = _tbSeriesEn.Text.Trim(),
|
||||
keywords = "",
|
||||
parameters = _paramRows.Where(p => !string.IsNullOrWhiteSpace(p.Code)).Select(p => new ParameterDef
|
||||
{
|
||||
code = p.Code,
|
||||
nameZh = string.IsNullOrWhiteSpace(p.NameZh) ? p.Code : p.NameZh,
|
||||
nameEn = string.IsNullOrWhiteSpace(p.NameEn) ? p.Code : p.NameEn,
|
||||
unit = p.Unit ?? "",
|
||||
type = p.Type == "number" ? "number" : "enum",
|
||||
keywords = p.Keywords ?? "",
|
||||
values = ParseValues(p.Values ?? "")
|
||||
}).ToList(),
|
||||
rules = _ruleRows.Select(BuildRule).Where(r => r != null).ToList(),
|
||||
naming = new Naming { stepNameTemplate = "{model}.step" },
|
||||
attachments = new List<Attachment>(),
|
||||
variants = new List<Variant>()
|
||||
};
|
||||
if (string.IsNullOrEmpty(series.code)) { MessageBox.Show("请填写系列代码", Title); return; }
|
||||
if (_paramRows.Count == 0) { MessageBox.Show("请先在 Step1 导入参数表", Title); return; }
|
||||
|
||||
var res = OpcBuilder.Build(
|
||||
_tbOut.Text.Trim(),
|
||||
"欧霓博气动目录", _tbVer.Text.Trim(),
|
||||
"ACT", _tbCatName.Text.Trim(), _tbCatName.Text.Trim(),
|
||||
series,
|
||||
_tbCsv.Text, _tbStepDir.Text,
|
||||
_attRows.Where(a => !string.IsNullOrWhiteSpace(a.Source)).Select(a => new BuildAttachmentSpec
|
||||
{
|
||||
kind = a.Kind, lang = a.Lang, source = a.Source, model = string.IsNullOrWhiteSpace(a.Model) ? null : a.Model.Trim()
|
||||
}).ToList());
|
||||
|
||||
if (!res.Ok)
|
||||
{
|
||||
sb.AppendLine("打包失败:");
|
||||
foreach (var e in res.Errors.Take(20)) sb.AppendLine(" [错误] " + e);
|
||||
_tbResult.Text = sb.ToString();
|
||||
return;
|
||||
}
|
||||
sb.AppendLine("打包成功: " + res.OutputPath);
|
||||
foreach (var w in res.Warnings.Take(10)) sb.AppendLine(" [警告] " + w);
|
||||
|
||||
// 打包后立即校验
|
||||
try
|
||||
{
|
||||
using (var pkg = OpcPackage.Open(res.OutputPath))
|
||||
{
|
||||
var issues = PackageValidator.Validate(pkg);
|
||||
sb.AppendLine("校验: " + issues.Count(i => i.Severity == "ERROR") + " 错误, " + issues.Count(i => i.Severity == "WARNING") + " 警告");
|
||||
foreach (var i in issues.Take(15)) sb.AppendLine(" [" + i.Severity + "] " + i.Target + " " + i.Message);
|
||||
sb.AppendLine("变体数: " + pkg.Catalog.series.Sum(s2 => s2.variants.Count));
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { sb.AppendLine("校验失败: " + ex.Message); }
|
||||
_tbResult.Text = sb.ToString();
|
||||
|
||||
if (MessageBox.Show("数据包已生成, 是否加载到主窗口?\n注意: 将替换主窗口当前数据包 (若只做了单系列, 加载后目录只剩该系列)", Title, MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes)
|
||||
_owner.ReplacePackage(res.OutputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
119
src/App/Cart.cs
Normal file
119
src/App/Cart.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ounibo.Catalog.App
|
||||
{
|
||||
/// <summary>
|
||||
/// 购物车条目 (清单式批量导出; 2026-08-24 与网页版语义一致)。
|
||||
/// Qty 仅体现在 BOM 数量列, zip 中每型号只打包一份。
|
||||
/// </summary>
|
||||
public class CartItem : INotifyPropertyChanged
|
||||
{
|
||||
public string ModelCode; // 型号编码
|
||||
public string SeriesCode; // 系列代码
|
||||
public string SeriesName; // 系列名
|
||||
public string StepPath; // 包内 STEP 路径 (非标档位为 null, 打包时跳过并注明)
|
||||
public string ParamsSummary; // 参数摘要 "key=value; ..."
|
||||
public bool IsStandard = true;// 非标档位标记 (参数化引擎, 2026-08-25)
|
||||
public Dictionary<string, object> Params; // 参数原始值 (非标批量在线生成提交用, 二期)
|
||||
int _qty = 1;
|
||||
public int Qty { get { return _qty; } set { _qty = Math.Max(1, value); OnChanged("Qty"); } }
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
void OnChanged(string n) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(n)); }
|
||||
}
|
||||
|
||||
/// <summary>清单窗口 (非模态; 购物车数据在主窗口, 本窗口只是视图)。</summary>
|
||||
public class CartWindow : Window
|
||||
{
|
||||
List<CartItem> _cart;
|
||||
Action _onChanged; // 数量/删除/清空后回写角标
|
||||
Func<System.Threading.Tasks.Task<string>> _onDownload; // 打包回调 (异步, 含在线生成), 返回 null 成功 / 错误提示
|
||||
DataGrid _grid;
|
||||
TextBlock _countText;
|
||||
|
||||
public CartWindow(List<CartItem> cart, Action onChanged, Func<System.Threading.Tasks.Task<string>> onDownload)
|
||||
{
|
||||
_cart = cart;
|
||||
_onChanged = onChanged;
|
||||
_onDownload = onDownload;
|
||||
Title = "我的清单 My Cart";
|
||||
Width = 800; Height = 500; MinWidth = 580; MinHeight = 320;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
Owner = Application.Current != null ? Application.Current.MainWindow : null;
|
||||
|
||||
var root = new DockPanel { Margin = new Thickness(10) };
|
||||
var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 8) };
|
||||
_countText = new TextBlock { VerticalAlignment = VerticalAlignment.Center, FontWeight = FontWeights.Bold, FontSize = 14 };
|
||||
top.Children.Add(_countText);
|
||||
DockPanel.SetDock(top, Dock.Top);
|
||||
root.Children.Add(top);
|
||||
|
||||
var bottom = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 8, 0, 0) };
|
||||
var btnClear = new Button { Content = "清空 Clear All", Width = 100, Margin = new Thickness(0, 0, 8, 0) };
|
||||
btnClear.Click += (s, e) => { _cart.Clear(); Refresh(); };
|
||||
var btnDownload = new Button { Content = "下载全部 (zip + BOM)", Width = 190, Padding = new Thickness(12, 6, 12, 6), FontSize = 14 };
|
||||
btnDownload.Click += async (s, e) =>
|
||||
{
|
||||
if (_cart.Count == 0) { MessageBox.Show("清单为空, 请先加入型号", Title); return; }
|
||||
btnDownload.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
var err = await _onDownload();
|
||||
if (err != null) MessageBox.Show(err, Title, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
finally { btnDownload.IsEnabled = true; }
|
||||
};
|
||||
bottom.Children.Add(btnClear); bottom.Children.Add(btnDownload);
|
||||
DockPanel.SetDock(bottom, Dock.Bottom);
|
||||
root.Children.Add(bottom);
|
||||
|
||||
_grid = new DataGrid { AutoGenerateColumns = false, IsReadOnly = false, CanUserAddRows = false, CanUserDeleteRows = false };
|
||||
_grid.Columns.Add(new DataGridTextColumn { Header = "型号 Model", Binding = new System.Windows.Data.Binding("ModelCode"), Width = 150, IsReadOnly = true });
|
||||
_grid.Columns.Add(new DataGridTextColumn { Header = "系列 Series", Binding = new System.Windows.Data.Binding("SeriesName"), Width = new System.Windows.Controls.DataGridLength(1, System.Windows.Controls.DataGridLengthUnitType.Star), IsReadOnly = true });
|
||||
_grid.Columns.Add(new DataGridTextColumn { Header = "规格 Params", Binding = new System.Windows.Data.Binding("ParamsSummary"), Width = 200, IsReadOnly = true });
|
||||
_grid.Columns.Add(new DataGridTextColumn { Header = "数量 Qty", Binding = new System.Windows.Data.Binding("Qty") { UpdateSourceTrigger = System.Windows.Data.UpdateSourceTrigger.LostFocus }, Width = 70 });
|
||||
var delCol = new DataGridTemplateColumn { Header = "操作", Width = 80 };
|
||||
delCol.CellTemplate = MakeTemplate(item => { _cart.Remove(item); Refresh(); });
|
||||
_grid.Columns.Add(delCol);
|
||||
_grid.CellEditEnding += (s, e) => { Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, new Action(Refresh)); };
|
||||
root.Children.Add(_grid);
|
||||
|
||||
Content = root;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
System.Windows.DataTemplate MakeTemplate(Action<CartItem> del)
|
||||
{
|
||||
var fact = new System.Windows.FrameworkElementFactory(typeof(Button));
|
||||
fact.SetValue(Button.ContentProperty, "删除");
|
||||
fact.SetValue(Button.PaddingProperty, new Thickness(6, 2, 6, 2));
|
||||
fact.SetValue(Button.FontSizeProperty, 12.0);
|
||||
fact.SetValue(Button.FocusableProperty, false); // 不抢焦点, 避免干扰网格行交互
|
||||
fact.AddHandler(Button.ClickEvent, new RoutedEventHandler((s, e) =>
|
||||
{
|
||||
var dc = ((FrameworkElement)s).DataContext as CartItem; // 模板列取 DataContext 删行
|
||||
if (dc != null) del(dc);
|
||||
e.Handled = true;
|
||||
}));
|
||||
var tpl = new System.Windows.DataTemplate();
|
||||
tpl.VisualTree = fact;
|
||||
return tpl;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_countText.Text = "共 " + _cart.Count + " 项 | 数量只在 BOM 中体现, zip 每型号一份";
|
||||
_grid.ItemsSource = null;
|
||||
_grid.ItemsSource = _cart;
|
||||
if (_onChanged != null) _onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/App/LocalViewerServer.cs
Normal file
133
src/App/LocalViewerServer.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ounibo.Catalog.App
|
||||
{
|
||||
/// <summary>
|
||||
/// 本机 3D 预览微型服务器 (真实数模用, 完全离线):
|
||||
/// 在 127.0.0.1 随机端口服务 新迪查看器本地副本 + 临时 STEP 文件,
|
||||
/// 桌面版点【在浏览器中打开 3D 预览】即用系统浏览器打开, 无外部依赖。
|
||||
/// </summary>
|
||||
public class LocalViewerServer : IDisposable
|
||||
{
|
||||
readonly TcpListener _listener;
|
||||
readonly Thread _thread;
|
||||
readonly string _viewerRoot;
|
||||
readonly string _stepDir;
|
||||
volatile bool _running = true;
|
||||
public int Port { get; private set; }
|
||||
|
||||
static readonly Dictionary<string, string> Mime = new Dictionary<string, string>
|
||||
{
|
||||
{ ".html", "text/html; charset=utf-8" }, { ".js", "application/javascript; charset=utf-8" },
|
||||
{ ".css", "text/css; charset=utf-8" }, { ".png", "image/png" }, { ".jpg", "image/jpeg" },
|
||||
{ ".wasm", "application/wasm" }, { ".ico", "image/x-icon" }, { ".json", "application/json; charset=utf-8" },
|
||||
{ ".step", "application/octet-stream" }, { ".stp", "application/octet-stream" }
|
||||
};
|
||||
|
||||
public LocalViewerServer(string viewerRoot)
|
||||
{
|
||||
_viewerRoot = Path.GetFullPath(viewerRoot);
|
||||
_stepDir = Path.Combine(Path.GetTempPath(), "onebot_preview");
|
||||
Directory.CreateDirectory(_stepDir);
|
||||
_listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
_listener.Start();
|
||||
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||
_thread = new Thread(Loop) { IsBackground = true, Name = "LocalViewerServer" };
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
void Loop()
|
||||
{
|
||||
while (_running)
|
||||
{
|
||||
TcpClient client = null;
|
||||
try { client = _listener.AcceptTcpClient(); }
|
||||
catch { if (!_running) break; Thread.Sleep(500); continue; }
|
||||
try { Handle(client); }
|
||||
catch { }
|
||||
try { client.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
void Handle(TcpClient client)
|
||||
{
|
||||
var stream = client.GetStream();
|
||||
stream.ReadTimeout = 3000;
|
||||
stream.WriteTimeout = 15000;
|
||||
var reader = new StreamReader(stream, Encoding.ASCII, false, 4096, true);
|
||||
var reqLine = reader.ReadLine();
|
||||
if (string.IsNullOrEmpty(reqLine)) return;
|
||||
var line = reader.ReadLine();
|
||||
var guard = 0;
|
||||
while (line != null && line != "")
|
||||
{
|
||||
line = reader.ReadLine();
|
||||
if (++guard > 100) break;
|
||||
}
|
||||
|
||||
var path = "/";
|
||||
var parts = reqLine.Split(' ');
|
||||
if (parts.Length >= 2) path = parts[1];
|
||||
var qi = path.IndexOf('?');
|
||||
if (qi >= 0) path = path.Substring(0, qi);
|
||||
if (path == "/") path = "/STEPViewer/index.html";
|
||||
|
||||
// 双根: /STEPViewer/... → 查看器副本; /step/... → 临时 STEP 目录
|
||||
string root, rel;
|
||||
if (path.StartsWith("/step/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
root = _stepDir;
|
||||
rel = path.Substring("/step/".Length).Replace('/', '\\');
|
||||
}
|
||||
else
|
||||
{
|
||||
root = _viewerRoot;
|
||||
rel = path.Substring(1).Replace('/', '\\');
|
||||
}
|
||||
|
||||
byte[] bytes; string status = "200 OK"; string ctype = "application/octet-stream";
|
||||
var full = Path.GetFullPath(Path.Combine(root, rel));
|
||||
if (full.StartsWith(Path.GetFullPath(root), StringComparison.OrdinalIgnoreCase) && File.Exists(full))
|
||||
{
|
||||
bytes = File.ReadAllBytes(full);
|
||||
var ext = Path.GetExtension(full).ToLowerInvariant();
|
||||
if (Mime.ContainsKey(ext)) ctype = Mime[ext];
|
||||
}
|
||||
else
|
||||
{
|
||||
status = "404 Not Found";
|
||||
bytes = Encoding.UTF8.GetBytes("404 Not Found");
|
||||
ctype = "text/plain; charset=utf-8";
|
||||
}
|
||||
var head = Encoding.ASCII.GetBytes(
|
||||
"HTTP/1.1 " + status + "\r\nContent-Type: " + ctype +
|
||||
"\r\nCache-Control: no-cache\r\nContent-Length: " + bytes.Length +
|
||||
"\r\nConnection: close\r\n\r\n");
|
||||
stream.Write(head, 0, head.Length);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
stream.Flush();
|
||||
}
|
||||
|
||||
/// <summary>把 STEP 内容写入临时服务目录, 返回浏览器可打开的查看器 URL (嵌入+精简菜单+无模型树)。</summary>
|
||||
public string PublishStep(string fileName, byte[] content)
|
||||
{
|
||||
var safe = Path.GetFileName(fileName.Replace('\\', '/'));
|
||||
File.WriteAllBytes(Path.Combine(_stepDir, safe), content);
|
||||
return string.Format(
|
||||
"http://127.0.0.1:{0}/STEPViewer/index.html?file=/step/{1}&embed=1&bg=grey&menu=min",
|
||||
Port, Uri.EscapeDataString(safe));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_running = false;
|
||||
try { _listener.Stop(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
1576
src/App/MainWindow.cs
Normal file
1576
src/App/MainWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
108
src/App/ViewerControl.cs
Normal file
108
src/App/ViewerControl.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Media3D;
|
||||
using Ounibo.Catalog.Viewer3D;
|
||||
|
||||
namespace Ounibo.Catalog.App
|
||||
{
|
||||
/// <summary>
|
||||
/// 3D 预览控件: Viewport3D + 鼠标左键拖拽旋转 + 滚轮缩放。
|
||||
/// </summary>
|
||||
public class ViewerControl : UserControl
|
||||
{
|
||||
Viewport3D _vp;
|
||||
PerspectiveCamera _cam;
|
||||
Model3DGroup _root;
|
||||
Point _last;
|
||||
double _theta = 0.6, _phi = 0.9, _dist = 300;
|
||||
bool _dragging;
|
||||
|
||||
public ViewerControl()
|
||||
{
|
||||
_root = new Model3DGroup();
|
||||
_root.Children.Add(new DirectionalLight(Colors.White, new Vector3D(-1, -1, -2)));
|
||||
_root.Children.Add(new DirectionalLight(Colors.White, new Vector3D(1, 0.5, 1)));
|
||||
|
||||
_cam = new PerspectiveCamera
|
||||
{
|
||||
Position = new Point3D(0, 0, 300),
|
||||
LookDirection = new Vector3D(0, 0, -1),
|
||||
UpDirection = new Vector3D(0, 1, 0),
|
||||
FieldOfView = 45
|
||||
};
|
||||
_vp = new Viewport3D { Camera = _cam };
|
||||
_vp.Children.Add(new ModelVisual3D { Content = _root });
|
||||
Content = new Border { Background = Brushes.White, Child = _vp };
|
||||
|
||||
_vp.MouseLeftButtonDown += (s, e) => { _dragging = true; _last = e.GetPosition(_vp); _vp.CaptureMouse(); };
|
||||
_vp.MouseMove += (s, e) =>
|
||||
{
|
||||
if (!_dragging) return;
|
||||
var p = e.GetPosition(_vp);
|
||||
var dx = p.X - _last.X;
|
||||
var dy = p.Y - _last.Y;
|
||||
_last = p;
|
||||
_theta -= dx * 0.01;
|
||||
_phi = Math.Max(-1.45, Math.Min(1.45, _phi + dy * 0.01));
|
||||
UpdateCamera();
|
||||
};
|
||||
_vp.MouseLeftButtonUp += (s, e) => { _dragging = false; _vp.ReleaseMouseCapture(); };
|
||||
_vp.MouseWheel += (s, e) =>
|
||||
{
|
||||
_dist *= (e.Delta > 0) ? 0.88 : 1.13;
|
||||
_dist = Math.Max(20, Math.Min(4000, _dist));
|
||||
UpdateCamera();
|
||||
};
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
var lights = new List<Light>();
|
||||
foreach (var m in _root.Children)
|
||||
if (m is Light) lights.Add((Light)m);
|
||||
_root.Children.Clear();
|
||||
foreach (var l in lights) _root.Children.Add(l);
|
||||
}
|
||||
|
||||
/// <summary>加载圆柱列表并自动取景。</summary>
|
||||
public void Load(List<Cyl> cyls)
|
||||
{
|
||||
Clear();
|
||||
if (cyls == null || cyls.Count == 0) return;
|
||||
|
||||
// 颜色: 0 后盖 / 1 缸体 / 2 前盖 / 3 活塞杆
|
||||
var brushes = new[]
|
||||
{
|
||||
new SolidColorBrush(Color.FromRgb(0x8C, 0x8C, 0x8C)),
|
||||
new SolidColorBrush(Color.FromRgb(0x2F, 0x6F, 0xB3)),
|
||||
new SolidColorBrush(Color.FromRgb(0x8C, 0x8C, 0x8C)),
|
||||
new SolidColorBrush(Color.FromRgb(0xC8, 0xC8, 0xC8))
|
||||
};
|
||||
for (int i = 0; i < cyls.Count; i++)
|
||||
{
|
||||
var mesh = StepMesh.BuildMesh(cyls[i]);
|
||||
var mat = new DiffuseMaterial(brushes[i % brushes.Length]);
|
||||
_root.Children.Add(new GeometryModel3D(mesh, mat) { BackMaterial = mat });
|
||||
}
|
||||
|
||||
double cx, cy, cz, r, h;
|
||||
StepMesh.Bounds(cyls, out cx, out cy, out cz, out r, out h);
|
||||
_dist = Math.Max(120, Math.Max(r * 5.0, h * 1.6));
|
||||
_theta = 0.55; _phi = 0.85;
|
||||
UpdateCamera();
|
||||
}
|
||||
|
||||
void UpdateCamera()
|
||||
{
|
||||
double x = _dist * Math.Sin(_theta) * Math.Cos(_phi);
|
||||
double y = _dist * Math.Sin(_phi);
|
||||
double z = _dist * Math.Cos(_theta) * Math.Cos(_phi);
|
||||
_cam.Position = new Point3D(x, y, z);
|
||||
_cam.LookDirection = new Vector3D(-x, -y, -z);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user