初始化提交

This commit is contained in:
2025-12-14 16:17:15 +08:00
commit 93cdc9b7b5
22 changed files with 1277 additions and 0 deletions

564
MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,564 @@
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
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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<AppConfig>(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<string?> 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<WebMsg>(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("<html><head><meta charset='utf-8'><title>关于</title></head><body><h1>关于</h1><p>这是示例的关于页面。</p></body></html>");
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<bytes.length;i+=size){ bin+=String.fromCharCode.apply(null, bytes.subarray(i,i+size)); } return btoa(bin); }
async function handle(link){ const url=link.href; const fn=link.download||url.split('/').pop()||'download'; try { if(url.startsWith('data:')) { const [h,d]=url.split(','); const is64 = h.includes('base64'); const m=/data:([^;]+)/.exec(h); const ct=m?m[1]:'application/octet-stream'; const content=is64?d:decodeURIComponent(d); if(window.chrome&&window.chrome.webview){ window.chrome.webview.postMessage({type:'download', filename:fn, contentType:ct, isBase64:is64, content:content}); } return; } if(url.startsWith('blob:')){ const r=await fetch(url); const b=await r.blob(); const ct=b.type||'application/octet-stream'; const content=await toBase64FromBlob(b); if(window.chrome&&window.chrome.webview){ window.chrome.webview.postMessage({type:'download', filename:fn, contentType:ct, isBase64:true, content}); } return; } if(url.startsWith('/')||url.startsWith('./')||url.startsWith('../')||url.startsWith(location.origin)){ const r=await fetch(url); const ct=r.headers.get('content-type')||'application/octet-stream'; if(ct.startsWith('text/')||ct.includes('json')){ const txt=await r.text(); if(window.chrome&&window.chrome.webview){ window.chrome.webview.postMessage({type:'download', filename:fn, contentType:ct, isBase64:false, content:txt}); } } else { const b=await r.blob(); const content=await toBase64FromBlob(b); if(window.chrome&&window.chrome.webview){ window.chrome.webview.postMessage({type:'download', filename:fn, contentType:ct, isBase64:true, content}); } } return; } if(window.chrome&&window.chrome.webview){ window.chrome.webview.postMessage({type:'open_in_browser', url}); } } catch(e) { if(window.chrome&&window.chrome.webview){ window.chrome.webview.postMessage({type:'open_in_browser', url}); } } }
function intercept(){ document.addEventListener('click', function(ev){ const a=ev.target && ev.target.closest ? ev.target.closest('a') : null; if(a && isDownloadLink(a)){ ev.preventDefault(); ev.stopPropagation(); handle(a); } }, true); }
intercept();
} catch(e) { console.warn('下载拦截设置失败', e); }
");
}
sb.AppendLine(" } catch(e) { console.warn('setup执行失败', e); }");
sb.AppendLine(" }");
sb.AppendLine(" if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', setup); } else { setup(); }");
sb.AppendLine("})();");
return sb.ToString();
}
}
internal class WebMsg
{
public string? Type { get; set; }
public string? Filename { get; set; }
public string? ContentType { get; set; }
public bool IsBase64 { get; set; }
public string? Content { get; set; }
public string? Url { get; set; }
}
internal class AppConfig
{
public string { get; set; } = "网页应用";
public string { get; set; } = "在线网页"; // 本地网页 / 在线网页
public string logo { get; set; } = string.Empty; // 可选:窗口图标
// 旧字段兼容(建议改用 注入设置.隐藏网页滚动条)
public bool { get; set; } = false;
public ? { get; set; } = new();
public ? { get; set; } = new();
public ? { get; set; }
public 线? 线 { get; set; }
public class
{
public bool { get; set; } = false;
public bool { get; set; } = true;
public bool { get; set; } = true;
}
public class
{
public int { get; set; } = 1200;
public int { get; set; } = 800;
public bool { get; set; } = true;
public bool { get; set; } = false;
public bool { get; set; } = true;
public bool { get; set; } = false;
}
public class
{
public string? { get; set; }
public string? { get; set; }
public string? { get; set; } // 直接本地 / http服务器
}
public class 线
{
public string { get; set; } = "https://www.example.com";
}
}
internal class StaticFileServer
{
private readonly string _root;
private HttpListener? _listener;
private CancellationTokenSource? _cts;
public StaticFileServer(string rootDirectory)
{
_root = rootDirectory;
}
public int StartOnAvailablePort(int startPort = 8080)
{
var port = FindAvailablePort(startPort);
_cts = new CancellationTokenSource();
_listener = new HttpListener();
_listener.Prefixes.Add($"http://127.0.0.1:" + port + "/");
_listener.Start();
_ = Task.Run(() => 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("无法找到可用的端口");
}
}
}