564 lines
25 KiB
C#
564 lines
25 KiB
C#
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("无法找到可用的端口");
|
||
}
|
||
}
|
||
} |