using System; using System.IO; using System.Net; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; using Microsoft.Win32; namespace WebToApp { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { private AppConfig _config = new(); private StaticFileServer? _server; private int? _serverPort; private WebView2? Web => this.FindName("WebView") as WebView2; public MainWindow() { InitializeComponent(); // 提前加载配置并应用窗口设置,确保在窗口显示前生效(避免先显示尺寸再全屏的卡顿) try { LoadConfig(); ApplyWindowSettings(); } catch { } Loaded += MainWindow_Loaded; Closing += MainWindow_Closing; } //主窗口加载 private async void MainWindow_Loaded(object sender, RoutedEventArgs e) { try { // 初始化 WebView2 await InitializeWebView2Async(); // 计算并导航 URL var url = await GetTargetUrlAsync(); if (string.IsNullOrWhiteSpace(url)) { MessageBox.Show("无法获取有效的URL,程序退出", "提示", MessageBoxButton.OK, MessageBoxImage.Information); Close(); return; } Web!.Source = new Uri(url); } catch (Exception ex) { MessageBox.Show($"启动失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); Close(); } } //主窗口关闭 private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) { try { _server?.Stop(); } catch { /* ignore */ } } //加载配置 private void LoadConfig() { try { // 优先使用开发环境相对路径: ./config/config.json var baseDir = AppContext.BaseDirectory; var devConfigPath = Path.Combine(baseDir, "config", "config.json"); if (File.Exists(devConfigPath)) { var json = File.ReadAllText(devConfigPath, Encoding.UTF8); _config = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new AppConfig(); return; } // 默认配置(在线网页) _config = new AppConfig { 软件名称 = "网页应用", 网页类型 = "在线网页", 在线网页 = new AppConfig.在线网页配置 { 链接地址 = "https://www.example.com" }, 窗口设置 = new AppConfig.窗口设置配置() }; } catch { _config = new AppConfig(); } } //将配置应用到窗口设置 private void ApplyWindowSettings() { Title = string.IsNullOrWhiteSpace(_config.软件名称) ? "网页应用" : _config.软件名称; if (_config.窗口设置 is not null) { // 先处理全屏:如果全屏为 true,则直接最大化并忽略宽高设置 if (_config.窗口设置.全屏) { WindowState = WindowState.Maximized; } else { Width = _config.窗口设置.窗口宽度 > 0 ? _config.窗口设置.窗口宽度 + 16 : 1216; Height = _config.窗口设置.窗口高度 > 0 ? _config.窗口设置.窗口高度 + 39 : 839; WindowState = WindowState.Normal; } Topmost = _config.窗口设置.窗口置顶; ResizeMode = _config.窗口设置.窗口可调整大小 ? ResizeMode.CanResize : ResizeMode.NoResize; if (!string.IsNullOrWhiteSpace(_config.软件logo)) { var iconPath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, _config.软件logo)); if (File.Exists(iconPath)) { try { var uri = new Uri(iconPath); Icon = System.Windows.Media.Imaging.BitmapFrame.Create(uri); } catch { /* ignore bad icon */ } } } } } //初始化异步加载webview private async Task InitializeWebView2Async() { await Web!.EnsureCoreWebView2Async(); var settings = Web!.CoreWebView2.Settings; settings.IsScriptEnabled = true; settings.AreDefaultContextMenusEnabled = false; // 禁用默认右键菜单,使用自定义 settings.AreDevToolsEnabled = false; // 设置移动端 UA(与示例一致) Web!.CoreWebView2.Settings.UserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1"; // 处理 JS 消息 Web!.CoreWebView2.WebMessageReceived += CoreWebView2_WebMessageReceived; // 处理下载(普通 a 链接下载) Web!.CoreWebView2.DownloadStarting += CoreWebView2_DownloadStarting; // 文档创建即注入脚本(滚动条隐藏、右键菜单、下载拦截) await Web!.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(BuildInjectedScript()); // 再次在 DOMContentLoaded 时执行一次,确保注入在有些页面上生效 Web!.CoreWebView2.DOMContentLoaded += async (_, __) => { try { await Web!.CoreWebView2.ExecuteScriptAsync(BuildInjectedScript()); } catch { } }; } private async Task GetTargetUrlAsync() { if (_config.网页类型 == "本地网页" && _config.本地网页 is not null) { var webDir = _config.本地网页.网页目录 ?? "config/web"; var entry = _config.本地网页.网页入口 ?? "index.html"; var display = _config.本地网页.展示模式 ?? "http服务器"; var baseDir = AppContext.BaseDirectory; var absWebDir = Path.GetFullPath(Path.Combine(baseDir, webDir)); if (display == "直接本地") { var entryPath = Path.GetFullPath(Path.Combine(absWebDir, entry)); if (File.Exists(entryPath)) { var uri = new Uri(entryPath); return uri.AbsoluteUri; // file:///... 路径 } else { MessageBox.Show($"网页文件不存在: {entryPath}"); return null; } } else { // 启动内置 HTTP 服务器 _server = new StaticFileServer(absWebDir); _serverPort = _server.StartOnAvailablePort(); var url = $"http://0.0.0.0:{_serverPort}/{entry}"; return url; } } else if (_config.网页类型 == "在线网页" && _config.在线网页 is not null) { return _config.在线网页.链接地址; } return null; } private void CoreWebView2_DownloadStarting(object? sender, CoreWebView2DownloadStartingEventArgs e) { try { // 打开保存对话框 var sfd = new SaveFileDialog { FileName = Path.GetFileName(e.ResultFilePath), Filter = "所有文件 (*.*)|*.*" }; if (sfd.ShowDialog() == true) { e.ResultFilePath = sfd.FileName; e.Handled = true; // 使用我们自己的保存,不显示默认 UI } else { e.Cancel = true; } } catch { // 失败则走默认 } } private async void CoreWebView2_WebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e) { try { var json = e.WebMessageAsJson; var msg = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (msg == null) return; switch (msg.Type) { case "download": await HandleInterceptedDownloadAsync(msg); break; case "show_about": ShowAboutPage(); break; case "open_in_browser": if (!string.IsNullOrWhiteSpace(msg.Url)) { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = msg.Url, UseShellExecute = true }); } catch { } } break; } } catch { /* ignore */ } } private async Task HandleInterceptedDownloadAsync(WebMsg msg) { try { var fileName = string.IsNullOrWhiteSpace(msg.Filename) ? $"下载文件_{DateTime.Now:yyyyMMdd_HHmmss}" : SanitizeFileName(msg.Filename!); var filter = "所有文件 (*.*)|*.*"; if (!string.IsNullOrWhiteSpace(msg.ContentType) && msg.ContentType.StartsWith("text/")) { filter = "文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*"; } var sfd = new SaveFileDialog { FileName = fileName, Filter = filter }; if (sfd.ShowDialog() == true) { byte[] bytes; if (msg.IsBase64) { bytes = Convert.FromBase64String(msg.Content ?? string.Empty); } else { bytes = Encoding.UTF8.GetBytes(msg.Content ?? string.Empty); } await File.WriteAllBytesAsync(sfd.FileName, bytes); } } catch { // 忽略错误 } } private static string SanitizeFileName(string name) { var invalid = Path.GetInvalidFileNameChars(); foreach (var ch in invalid) { name = name.Replace(ch, '_'); } return name.Trim(' ', '.'); } private void ShowAboutPage() { try { var baseDir = AppContext.BaseDirectory; var aboutPath = Path.Combine(baseDir, "config/aboutpage", "about.html"); if (File.Exists(aboutPath)) { Web!.CoreWebView2.Navigate(new Uri(aboutPath).AbsoluteUri); } else { // 备用:简单 About 内容 var html = "data:text/html;charset=utf-8," + Uri.EscapeDataString("关于

关于

这是示例的关于页面。

"); Web!.CoreWebView2.Navigate(html); } } catch { /* ignore */ } } private string BuildInjectedScript() { var hideScroll = _config.注入设置?.隐藏网页滚动条 == true; var menuEnabled = _config.注入设置?.自定义右键菜单 == true; var interceptEnabled = _config.注入设置?.拦截下载链接 == true; var css = hideScroll ? @"/* 隐藏所有滚动条 */ ::-webkit-scrollbar { width:0px; height:0px; background:transparent; } html { scrollbar-width: none; } body { -ms-overflow-style: none; } * { scrollbar-width: none; -ms-overflow-style: none; } *::-webkit-scrollbar { width:0px; height:0px; background:transparent; }" : string.Empty; var sb = new StringBuilder(); sb.AppendLine("(() => {"); sb.AppendLine(" function setup(){"); sb.AppendLine(" try {"); if (!string.IsNullOrEmpty(css)) { sb.AppendLine(" try { var style=document.createElement('style'); style.textContent=`" + css.Replace("`", "\\`") + "`; (document.head||document.documentElement).appendChild(style); } catch(e){ console.warn('样式注入失败', e);} "); } if (menuEnabled) { sb.AppendLine(@" try { const oldMenu = document.getElementById('custom-context-menu'); if (oldMenu) oldMenu.remove(); const m = document.createElement('div'); m.id = 'custom-context-menu'; m.style.cssText = 'position:fixed;background:#fff;border:1px solid #ccc;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.15);padding:8px 0;z-index:10000;display:none;min-width:120px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px'; function item(t,fn){ const b=document.createElement('button'); b.textContent=t; b.style.cssText='padding:8px 16px;cursor:pointer;border:none;background:none;width:100%;text-align:left;font-size:14px;color:#333'; b.onmouseenter=()=>b.style.background='#f0f0f0'; b.onmouseleave=()=>b.style.background='transparent'; b.onclick=()=>{ fn(); hide(); }; return b; } function hide(){ m.style.display='none'; } function show(x,y){ m.style.left=x+'px'; m.style.top=y+'px'; m.style.display='block'; const r=m.getBoundingClientRect(); if(r.right>innerWidth) m.style.left=(x-r.width)+'px'; if(r.bottom>innerHeight) m.style.top=(y-r.height)+'px'; } m.appendChild(item('← 返回', ()=>history.back())); m.appendChild(item('🔄 刷新', ()=>location.reload())); m.appendChild(item('ℹ️ 关于', ()=>{ try{ if(window.chrome&&window.chrome.webview){ window.chrome.webview.postMessage({type:'show_about'}); } }catch(_){ } })); (document.body||document.documentElement).appendChild(m); document.addEventListener('click', hide, {passive:true}); document.addEventListener('scroll', hide, {passive:true}); addEventListener('resize', hide, {passive:true}); document.addEventListener('contextmenu', function(e){ e.preventDefault(); show(e.clientX,e.clientY); }, false); } catch(e) { console.warn('右键菜单设置失败', e); } "); } if (interceptEnabled) { sb.AppendLine(@" try { function isDownloadLink(a){ if(!a||!a.href) return false; if(a.hasAttribute('download')) return true; const u=(a.href||'').toLowerCase(); const exts=['.txt','.pdf','.doc','.docx','.xls','.xlsx','.ppt','.pptx','.zip','.rar','.7z','.tar','.gz','.jpg','.jpeg','.png','.gif','.bmp','.svg','.webp','.mp3','.wav','.ogg','.mp4','.avi','.mov','.wmv','.json','.xml','.csv','.md','.log']; for(const e of exts){ if(u.includes(e)) return true; } if(u.startsWith('data:')||u.startsWith('blob:')) return true; const kws=['download','export','save','下载','导出','保存']; const t=(a.textContent||'').toLowerCase(); const ti=(a.title||'').toLowerCase(); return kws.some(k=>t.includes(k)||ti.includes(k)); } async function toBase64FromBlob(b){ const ab=await b.arrayBuffer(); const bytes=new Uint8Array(ab); let bin=''; const size=0x8000; for(let i=0;i AcceptLoopAsync(_cts.Token)); return port; } public void Stop() { try { _cts?.Cancel(); } catch { } try { _listener?.Stop(); } catch { } } private async Task AcceptLoopAsync(CancellationToken ct) { if (_listener == null) return; while (!ct.IsCancellationRequested) { HttpListenerContext? ctx = null; try { ctx = await _listener.GetContextAsync(); } catch { if (ct.IsCancellationRequested) break; } if (ctx == null) continue; _ = Task.Run(() => HandleRequestAsync(ctx)); } } private async Task HandleRequestAsync(HttpListenerContext ctx) { try { var req = ctx.Request; var relPath = req.Url?.AbsolutePath ?? "/"; relPath = WebUtility.UrlDecode(relPath).TrimStart('/'); if (string.IsNullOrEmpty(relPath)) relPath = "index.html"; // 防止越权访问 var fullPath = Path.GetFullPath(Path.Combine(_root, relPath.Replace('/', Path.DirectorySeparatorChar))); if (!fullPath.StartsWith(Path.GetFullPath(_root), StringComparison.OrdinalIgnoreCase)) { ctx.Response.StatusCode = 403; ctx.Response.Close(); return; } if (!File.Exists(fullPath)) { ctx.Response.StatusCode = 404; ctx.Response.Close(); return; } var bytes = await File.ReadAllBytesAsync(fullPath); ctx.Response.ContentType = GetContentType(fullPath); ctx.Response.ContentLength64 = bytes.LongLength; await ctx.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length); ctx.Response.OutputStream.Close(); } catch { try { ctx.Response.StatusCode = 500; ctx.Response.Close(); } catch { } } } private static string GetContentType(string path) { var ext = Path.GetExtension(path).ToLowerInvariant(); return ext switch { ".html" or ".htm" => "text/html; charset=utf-8", ".js" => "application/javascript; charset=utf-8", ".css" => "text/css; charset=utf-8", ".json" => "application/json; charset=utf-8", ".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg", ".gif" => "image/gif", ".svg" => "image/svg+xml", ".ico" => "image/x-icon", ".txt" => "text/plain; charset=utf-8", _ => "application/octet-stream" }; } private static int FindAvailablePort(int start) { for (var p = start; p < 65535; p++) { try { var l = new HttpListener(); l.Prefixes.Add($"http://127.0.0.1:" + p + "/"); l.Start(); l.Stop(); return p; } catch { /* try next */ } } throw new InvalidOperationException("无法找到可用的端口"); } } }