// ============================================================ // 停靠窗口:CAD 查看器(中央文档区) // WebView2 承载网页版 CAD-Viewer(localhost:5173),并挂到 CadBridge。 // ★ 网页服务自启动:安装布局 {app}\cadbt\node\node.exe + {app}\cadbt\CAD-Viewer\dist\server.cjs // (静态产物零依赖,客户机无需 Node 环境/pnpm dev;主程序同款端口探测模式) // ============================================================ using System; using System.Diagnostics; using System.IO; using System.Net.Sockets; using System.Windows.Forms; using Microsoft.Web.WebView2.WinForms; namespace 成套报价软件 { /// 停靠窗口:网页 CAD 查看器(DockState.Document)。 public partial class CadViewerWindow : WeifenLuo.WinFormsUI.Docking.DockContent { private WebView2 _webView; private Label _hint; /// 静态服务进程句柄(退出时杀进程树)。 private static Process _serverProcess; public CadViewerWindow() { InitializeComponent(); this.BackColor = System.Drawing.Color.White; Text = "CAD 查看器"; TabText = "CAD 查看器"; FormClosed += (s, e) => 停止网页服务(); BuildToolbar(); 挂加载状态(); LoadViewer(); } /// CAD 网页服务端口(与 server.cjs 一致)。 private const int 服务端口 = 5173; /// 顶部工具栏:显示蒙版按钮(蒙版存档就位后手动加载—— /// 打开图纸统一走网页自带的上传界面,实测渲染无问题)。 /// 右侧状态标签实时显示加载渲染进度(大图 40~60 秒,期间黑屏是正常加载态)。 private ToolStripLabel _statusLabel; private void BuildToolbar() { var ts = new ToolStrip(); ts.Dock = DockStyle.Top; ts.GripStyle = ToolStripGripStyle.Hidden; var btnMask = new ToolStripButton("显示蒙版"); btnMask.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F); btnMask.Click += (s, e) => 成套报价软件.CadBridge.ReloadMasks(); ts.Items.Add(btnMask); _statusLabel = new ToolStripLabel("等待打开图纸(用网页里的选择文件按钮)"); _statusLabel.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F); _statusLabel.ForeColor = System.Drawing.Color.DimGray; ts.Items.Add(new ToolStripSeparator()); ts.Items.Add(_statusLabel); Controls.Add(ts); } /// 订阅网页加载进度,实时刷到工具栏状态标签(完成后显示总耗时)。 private void 挂加载状态() { 成套报价软件.CadBridge.加载状态 += On加载状态; FormClosed += (s, e) => 成套报价软件.CadBridge.加载状态 -= On加载状态; } private void On加载状态(string text) { try { if (_statusLabel == null || IsDisposed) return; if (InvokeRequired) BeginInvoke((MethodInvoker)delegate { 刷状态(text); }); else 刷状态(text); } catch { } } private void 刷状态(string text) { if (_statusLabel == null) return; _statusLabel.Text = text; // 完成显示绿,失败红,进行中灰 if (text.IndexOf("渲染完成") >= 0) _statusLabel.ForeColor = System.Drawing.Color.Green; else if (text.IndexOf("失败") >= 0) _statusLabel.ForeColor = System.Drawing.Color.Red; else _statusLabel.ForeColor = System.Drawing.Color.DimGray; } private async void LoadViewer() { try { _webView = new WebView2(); _webView.Dock = DockStyle.Fill; Controls.Add(_webView); // ★ WinForms 停靠按控件集合逆序排版:Fill 控件必须处在最前(最低索引), // 最后分剩余空间;不加这句,先加的工具栏(Top)会叠在 WebView 上面盖住 CAD 画面 _webView.BringToFront(); await _webView.EnsureCoreWebView2Async(null); CadBridge.Attach(_webView); // ★ 禁用浏览器快捷键(F5/Ctrl+R/Ctrl+F 等):刷新会丢掉所有扒图蒙版 _webView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = false; // ★ 先确保静态服务在线(复刻主程序 StartPriceWebView 的探测+拉起模式) 启动网页服务(); // 服务不可达时给用户明确提示 _webView.CoreWebView2.NavigationCompleted += (s, e) => { if (!e.IsSuccess && _hint == null) ShowLoadHint("无法连接网页 CAD 服务(端口 " + 服务端口 + ")。\r\n请确认 cadbt\\node\\node.exe 与 cadbt\\CAD-Viewer\\dist 目录完整,然后点击\"重新加载\"。"); }; // ★ URL 带时间戳:dist 网页热更新后强制 WebView2 拉新 index.html // (旧 node 进程无 no-store 头时会启发式缓存,网页端跑旧 bundle // 导致宿主新消息无人处理——组框/高亮全失效) _webView.CoreWebView2.Navigate("http://localhost:" + 服务端口 + "/?v=" + DateTime.Now.Ticks); } catch (Exception ex) { ShowLoadHint("WebView2 初始化失败: " + ex.Message + "\r\n请确认系统已安装 Microsoft Edge WebView2 运行时。"); } } /// 启动 CAD 网页静态服务(端口已通=复用;不通=node server.cjs 后台拉起)。 private void 启动网页服务() { try { if (端口已通()) return; // 上次实例或手动 dev server 已在服务 string baseDir = AppDomain.CurrentDomain.BaseDirectory; // 安装布局:{app}\cadbt\node\node.exe + {app}\cadbt\CAD-Viewer\dist\server.cjs // (make_package 已展平 packages 层,与独立 cadbt.exe 共用同一套文件) string nodeExe = Path.Combine(baseDir, "cadbt", "node", "node.exe"); string serverScript = Path.Combine(baseDir, "cadbt", "CAD-Viewer", "dist", "server.cjs"); // ★ 开发机回退:仓库根在 bin\Debug 上三级(仓库根\成套报价软件\bin\Debug)。 // 旧代码只回退两级,算出 ...\成套报价软件\成套报价软件\cadbt\...(多一层项目目录), // 开发版一直起不了服务——此前全靠装机目录实例顶着 5173 端口才没暴露 // (2026-09-24 cad服务.log 实证:缺少 node.exe 或 server.cjs 路径带双层项目名)。 // 两级三级都探测,哪个存在用哪个(防未来目录层级再变)。 if (!File.Exists(nodeExe)) { string 三级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\..\cadbt\cadbt\bin\Debug\node\node.exe")); string 二级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\cadbt\cadbt\bin\Debug\node\node.exe")); nodeExe = File.Exists(三级) ? 三级 : (File.Exists(二级) ? 二级 : 三级); } if (!File.Exists(serverScript)) { string 三级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\..\cadbt\cadbt\CAD-Viewer\packages\cad-viewer-example\dist\server.cjs")); string 二级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\cadbt\cadbt\CAD-Viewer\packages\cad-viewer-example\dist\server.cjs")); serverScript = File.Exists(三级) ? 三级 : (File.Exists(二级) ? 二级 : 三级); } if (!File.Exists(nodeExe) || !File.Exists(serverScript)) { WriteDiag("[CAD服务] 缺少 node.exe 或 server.cjs: node=" + nodeExe + " script=" + serverScript); return; } var psi = new ProcessStartInfo { FileName = nodeExe, Arguments = "\"" + Path.GetFullPath(serverScript) + "\"", WorkingDirectory = Path.GetDirectoryName(Path.GetFullPath(serverScript)), UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, }; _serverProcess = Process.Start(psi); WriteDiag("[CAD服务] 已启动 node PID=" + (_serverProcess == null ? "?" : _serverProcess.Id.ToString())); // 等端口就绪(最多 8 秒) for (int i = 0; i < 40; i++) { System.Threading.Thread.Sleep(200); if (端口已通()) { WriteDiag("[CAD服务] 端口就绪, 耗时 " + (i + 1) * 200 + "ms"); return; } } WriteDiag("[CAD服务] 端口 8 秒未就绪(继续导航,由 NavigationCompleted 提示)"); } catch (Exception ex) { WriteDiag("[CAD服务] 启动异常: " + ex.Message); } } /// 快速探测本机端口。 private static bool 端口已通() { try { using (var client = new TcpClient()) { var ar = client.BeginConnect("127.0.0.1", 服务端口, null, null); bool ok = ar.AsyncWaitHandle.WaitOne(400); if (ok && client.Connected) { client.EndConnect(ar); return true; } } } catch { } return false; } /// 退出清理:杀 node 静态服务进程树(taskkill /T 覆盖可能的子进程)。 private void 停止网页服务() { try { if (_serverProcess != null && !_serverProcess.HasExited) { int pid = _serverProcess.Id; Process.Start(new ProcessStartInfo("taskkill", "/PID " + pid + " /T /F") { CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden }); WriteDiag("[CAD服务] 已停止 PID=" + pid); } _serverProcess = null; } catch (Exception ex) { WriteDiag("[CAD服务] 停止异常: " + ex.Message); } } /// 简单诊断日志(logs\cad服务.log)。 private static void WriteDiag(string msg) { try { string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); Directory.CreateDirectory(dir); File.AppendAllText(Path.Combine(dir, "cad服务.log"), DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + Environment.NewLine); } catch { } } private void ShowLoadHint(string text) { if (_hint != null) return; _hint = new Label(); _hint.Dock = DockStyle.Fill; _hint.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; _hint.Text = text; var reload = new Button(); reload.Text = "重新加载"; reload.Dock = DockStyle.Bottom; reload.Click += (s, e) => { Controls.Remove(_hint); _hint.Dispose(); _hint = null; 启动网页服务(); if (_webView != null && _webView.CoreWebView2 != null) _webView.CoreWebView2.Navigate("http://localhost:" + 服务端口 + "/"); }; Controls.Add(reload); Controls.Add(_hint); _hint.BringToFront(); } } }