// ============================================================
// 停靠窗口: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();
}
}
}