初始化提交
This commit is contained in:
166
.gitignore
vendored
Normal file
166
.gitignore
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
## Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
## Visual Studio cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
## Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
## Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
## User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
## Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
## .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
## NuGet Packages
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
**/packages/*
|
||||
!**/packages/build/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
## MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
## NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
## Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
## .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
## Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
## Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
## Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
## ReSharper
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
## JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
## WebView2 runtime cache
|
||||
*.exe.WebView2/
|
||||
EBWebView/
|
||||
|
||||
## Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
## User-specific project files
|
||||
*.csproj.user
|
||||
*.user
|
||||
|
||||
## Build output
|
||||
publish/
|
||||
*.rar
|
||||
*.zip
|
||||
|
||||
## Config files (if contains sensitive data, uncomment)
|
||||
# config/config.json
|
||||
|
||||
9
App.xaml
Normal file
9
App.xaml
Normal file
@@ -0,0 +1,9 @@
|
||||
<Application x:Class="WebToApp.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:WebToApp"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
14
App.xaml.cs
Normal file
14
App.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace WebToApp
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
10
AssemblyInfo.cs
Normal file
10
AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
13
MainWindow.xaml
Normal file
13
MainWindow.xaml
Normal file
@@ -0,0 +1,13 @@
|
||||
<Window x:Class="WebToApp.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:WebToApp"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
mc:Ignorable="d"
|
||||
Title="WebToApp" Height="800" Width="1200">
|
||||
<Grid>
|
||||
<wv2:WebView2 x:Name="WebView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
564
MainWindow.xaml.cs
Normal file
564
MainWindow.xaml.cs
Normal 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("无法找到可用的端口");
|
||||
}
|
||||
}
|
||||
}
|
||||
183
README.md
Normal file
183
README.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# WebToApp
|
||||
|
||||
一个基于 WPF 和 WebView2 的网页转桌面应用工具,可以将在线网页或本地网页打包成 Windows 桌面应用程序。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🌐 **支持在线网页**:直接加载远程网页地址
|
||||
- 📁 **支持本地网页**:支持本地 HTML 文件,可通过 HTTP 服务器或直接文件方式加载
|
||||
- 🎨 **自定义窗口设置**:可配置窗口大小、置顶、全屏等属性
|
||||
- 🖱️ **自定义右键菜单**:提供返回、刷新、关于等快捷操作
|
||||
- 📥 **下载拦截**:智能拦截下载链接,使用系统保存对话框
|
||||
- 🎯 **滚动条隐藏**:可隐藏网页滚动条,提供更沉浸的体验
|
||||
- 🖼️ **自定义图标和标题**:支持自定义应用图标和窗口标题
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **.NET 8.0** - 跨平台开发框架
|
||||
- **WPF** - Windows Presentation Foundation
|
||||
- **WebView2** - 基于 Chromium 的 WebView 控件
|
||||
|
||||
## 系统要求
|
||||
|
||||
- Windows 10/11
|
||||
- .NET 8.0 Runtime(如果使用独立部署则不需要)
|
||||
- WebView2 Runtime(通常已预装或会自动下载)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/WebToApp.git
|
||||
cd WebToApp
|
||||
```
|
||||
|
||||
### 2. 配置应用
|
||||
|
||||
编辑 `config/config.json` 文件来配置你的应用:
|
||||
|
||||
#### 在线网页配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"软件名称": "我的应用",
|
||||
"软件logo": "config/logo.png",
|
||||
"网页类型": "在线网页",
|
||||
"在线网页": {
|
||||
"链接地址": "https://example.com"
|
||||
},
|
||||
"注入设置": {
|
||||
"隐藏网页滚动条": true,
|
||||
"自定义右键菜单": true,
|
||||
"拦截下载链接": true
|
||||
},
|
||||
"窗口设置": {
|
||||
"窗口宽度": 1200,
|
||||
"窗口高度": 800,
|
||||
"窗口可调整大小": true,
|
||||
"窗口置顶": false,
|
||||
"窗口阴影": true,
|
||||
"全屏": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 本地网页配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"软件名称": "我的应用",
|
||||
"软件logo": "config/logo.png",
|
||||
"网页类型": "本地网页",
|
||||
"本地网页": {
|
||||
"网页目录": "config/web",
|
||||
"网页入口": "index.html",
|
||||
"展示模式": "http服务器"
|
||||
},
|
||||
"注入设置": {
|
||||
"隐藏网页滚动条": true,
|
||||
"自定义右键菜单": true,
|
||||
"拦截下载链接": true
|
||||
},
|
||||
"窗口设置": {
|
||||
"窗口宽度": 1200,
|
||||
"窗口高度": 800,
|
||||
"窗口可调整大小": true,
|
||||
"窗口置顶": false,
|
||||
"窗口阴影": true,
|
||||
"全屏": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 构建项目
|
||||
|
||||
使用 Visual Studio 或命令行构建:
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
### 4. 运行应用
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
或者直接运行编译后的可执行文件。
|
||||
|
||||
### 5. 发布应用
|
||||
|
||||
发布为独立可执行文件:
|
||||
|
||||
```bash
|
||||
dotnet publish -c Release -r win-x64 --self-contained true
|
||||
```
|
||||
|
||||
发布后的文件在 `bin/Release/net8.0-windows/win-x64/publish/` 目录下。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 窗口设置
|
||||
|
||||
- `窗口宽度` / `窗口高度`:设置窗口的初始大小(像素)
|
||||
- `窗口可调整大小`:是否允许用户调整窗口大小
|
||||
- `窗口置顶`:窗口是否始终显示在其他窗口之上
|
||||
- `窗口阴影`:是否显示窗口阴影效果
|
||||
- `全屏`:是否以全屏模式启动
|
||||
|
||||
### 注入设置
|
||||
|
||||
- `隐藏网页滚动条`:隐藏网页内的滚动条
|
||||
- `自定义右键菜单`:启用自定义右键菜单(包含返回、刷新、关于等功能)
|
||||
- `拦截下载链接`:拦截网页中的下载链接,使用系统保存对话框
|
||||
|
||||
### 本地网页展示模式
|
||||
|
||||
- `http服务器`:使用内置 HTTP 服务器提供网页服务(推荐)
|
||||
- `直接本地`:直接使用 `file://` 协议加载本地文件
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
WebToApp/
|
||||
├── config/ # 配置文件目录
|
||||
│ ├── config.json # 主配置文件
|
||||
│ ├── logo.png # 应用图标
|
||||
│ └── aboutpage/ # 关于页面
|
||||
│ └── about.html
|
||||
├── MainWindow.xaml # 主窗口 XAML
|
||||
├── MainWindow.xaml.cs # 主窗口代码
|
||||
├── App.xaml # 应用程序 XAML
|
||||
├── App.xaml.cs # 应用程序代码
|
||||
└── WebToApp.csproj # 项目文件
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Visual Studio 2022 或更高版本
|
||||
- .NET 8.0 SDK
|
||||
|
||||
### 依赖项
|
||||
|
||||
- Microsoft.Web.WebView2 (1.0.3595.46)
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 作者
|
||||
|
||||
[你的名字]
|
||||
|
||||
---
|
||||
|
||||
如果这个项目对你有帮助,请给个 ⭐ Star 支持一下!
|
||||
|
||||
45
WebToApp.csproj
Normal file
45
WebToApp.csproj
Normal file
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- 发布(Release)配置下的体积优化设置 -->
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<!-- 仅发布到 Windows x64,避免跨平台运行时被包含 -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<!-- 避免 ReadyToRun 导致体积增大(换取更小体积) -->
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
|
||||
<!-- WPF 不支持/不推荐修剪,避免 NETSDK1168 错误 -->
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
|
||||
<!-- 使用不变全球化,去掉 ICU 数据(如不依赖复杂本地化可开启) -->
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
|
||||
<!-- 去掉调试符号和文档以减少发布体积 -->
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
|
||||
<!-- 单文件中尽量不自解压本机库(进一步缩减发布内容) -->
|
||||
<IncludeNativeLibrariesForSelfExtract>false</IncludeNativeLibrariesForSelfExtract>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="config\**\*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
22
WebToApp.sln
Normal file
22
WebToApp.sln
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.12.35527.113 d17.12
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebToApp", "WebToApp.csproj", "{ED8C8E3D-BAB9-4937-A38B-E937B87F5CB3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{ED8C8E3D-BAB9-4937-A38B-E937B87F5CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ED8C8E3D-BAB9-4937-A38B-E937B87F5CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ED8C8E3D-BAB9-4937-A38B-E937B87F5CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ED8C8E3D-BAB9-4937-A38B-E937B87F5CB3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
26
config/aboutpage/about.html
Normal file
26
config/aboutpage/about.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>关于 - WebToApp 示例</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 24px; }
|
||||
h1 { margin-bottom: 8px; }
|
||||
p { line-height: 1.6; }
|
||||
.note { padding: 12px; background: #f5f5f5; border-radius: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>关于</h1>
|
||||
<p>这是用于演示的关于页面,展示从应用内打开本地 HTML 的能力。</p>
|
||||
<div class="note">
|
||||
<p>本应用由 WPF + WebView2 构建,支持:</p>
|
||||
<ul>
|
||||
<li>加载在线或本地网页</li>
|
||||
<li>自定义右键菜单</li>
|
||||
<li>拦截并保存 data/blob/同域下载</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
27
config/config.json
Normal file
27
config/config.json
Normal file
File diff suppressed because one or more lines are too long
BIN
config/logo.png
Normal file
BIN
config/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
26
在线网页配置示例/aboutpage/about.html
Normal file
26
在线网页配置示例/aboutpage/about.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>关于 - WebToApp 示例</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 24px; }
|
||||
h1 { margin-bottom: 8px; }
|
||||
p { line-height: 1.6; }
|
||||
.note { padding: 12px; background: #f5f5f5; border-radius: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>关于</h1>
|
||||
<p>这是用于演示的关于页面,展示从应用内打开本地 HTML 的能力。</p>
|
||||
<div class="note">
|
||||
<p>本应用由 WPF + WebView2 构建,支持:</p>
|
||||
<ul>
|
||||
<li>加载在线或本地网页</li>
|
||||
<li>自定义右键菜单</li>
|
||||
<li>拦截并保存 data/blob/同域下载</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
27
在线网页配置示例/config.json
Normal file
27
在线网页配置示例/config.json
Normal file
File diff suppressed because one or more lines are too long
26
本地网页配置示例/aboutpage/about.html
Normal file
26
本地网页配置示例/aboutpage/about.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>关于 - WebToApp 示例</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 24px; }
|
||||
h1 { margin-bottom: 8px; }
|
||||
p { line-height: 1.6; }
|
||||
.note { padding: 12px; background: #f5f5f5; border-radius: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>关于</h1>
|
||||
<p>这是用于演示的关于页面,展示从应用内打开本地 HTML 的能力。</p>
|
||||
<div class="note">
|
||||
<p>本应用由 WPF + WebView2 构建,支持:</p>
|
||||
<ul>
|
||||
<li>加载在线或本地网页</li>
|
||||
<li>自定义右键菜单</li>
|
||||
<li>拦截并保存 data/blob/同域下载</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
27
本地网页配置示例/config.json
Normal file
27
本地网页配置示例/config.json
Normal file
File diff suppressed because one or more lines are too long
1
本地网页配置示例/web/images/sample.png
Normal file
1
本地网页配置示例/web/images/sample.png
Normal file
@@ -0,0 +1 @@
|
||||
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABh0RVh0Q3JlYXRpb24gVGltZQAyMDI1LTAxLTAxVDEyOjAwOjAwW7vWJQAAAQ1JREFUeNrs1kENgDAMQ9G9/5+S0YF4pG3lI1oIuJmYQ8NwJdC0xgq7r9q+3QAAACcWcH9yJwAAABgqY7tJwAAABgqY7tJwAAABgqY7tJwAAABgqY7tJwAAABgqY7tJwAAABgqY7tJwAAABgqY7tJwAAABgqY7tJwAAABgqY7tJwAAABi8BfC8YwAAAOw3oQIAAAD8fV8AAAAAANoYAwAAAPx9XwAAAADaGAMAAAD8fV8AAAAA2hgDAAAA/H1fAAAAANoYAwAAAPx9XwAAAADaGAMAAAD8fV8AAAAA2vgCkHfXfVQyAAAAAElFTkSuQmCC
|
||||
83
本地网页配置示例/web/index.html
Normal file
83
本地网页配置示例/web/index.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>本地网页示例</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 24px; }
|
||||
h1 { margin-bottom: 8px; }
|
||||
.card { border: 1px solid #ddd; border-radius: 12px; padding: 16px; margin: 12px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||||
button, a.btn { display: inline-block; padding: 8px 14px; border-radius: 8px; border: 1px solid #ccc; text-decoration: none; color: #333; background: #f7f7f7; margin-right: 8px; }
|
||||
button:hover, a.btn:hover { background: #eee; }
|
||||
pre { background: #f5f5f5; padding: 12px; border-radius: 8px; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>本地网页示例</h1>
|
||||
<p>用于演示:右键菜单、下载拦截(data/blob/同域)、关于页面。</p>
|
||||
|
||||
<div class="card">
|
||||
<h3>同域下载(静态文件)</h3>
|
||||
<p>
|
||||
<a class="btn" href="sample.txt" download>下载文本 sample.txt</a>
|
||||
<a class="btn" href="images/sample.png" download>下载图片 sample.png</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Data URL 下载(内联文本)</h3>
|
||||
<button onclick="downloadDataUrl()">下载 data: 文本</button>
|
||||
<script>
|
||||
function downloadDataUrl(){
|
||||
const content = encodeURIComponent('这是来自 data:URL 的文本内容\nHello WebView2!');
|
||||
const url = 'data:text/plain;charset=utf-8,' + content;
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = 'data_text.txt';
|
||||
a.click(); a.remove();
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Blob URL 下载(运行时生成)</h3>
|
||||
<button onclick="downloadBlob()">下载 blob: 文本</button>
|
||||
<script>
|
||||
function downloadBlob(){
|
||||
const blob = new Blob(['这是来自 blob:URL 的文本内容\nBlob Download Demo'], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = 'blob_text.txt';
|
||||
a.click(); a.remove(); URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>右键菜单测试</h3>
|
||||
<p>在页面空白处点击右键,弹出自定义菜单(返回/刷新/关于)。</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>关于页面</h3>
|
||||
<a class="btn" href="#" onclick="openAbout()">打开关于</a>
|
||||
<script>
|
||||
function openAbout(){
|
||||
if (window.chrome && window.chrome.webview) {
|
||||
window.chrome.webview.postMessage({ type: 'show_about' });
|
||||
} else {
|
||||
window.open('about.html', '_blank');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>示例文件内容</h3>
|
||||
<pre>
|
||||
sample.txt 将在同目录下提供。
|
||||
images/sample.png 将作为占位图片。
|
||||
</pre>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2
本地网页配置示例/web/sample.txt
Normal file
2
本地网页配置示例/web/sample.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
这是一个用于同域下载的示例文本文件。
|
||||
Hello from WebToApp (WPF + WebView2).
|
||||
6
构建命令.txt
Normal file
6
构建命令.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
dotnet publish -c Release -r win-x64 --self-contained false
|
||||
|
||||
dotnet publish -c Release -r win-x64 --self-contained true
|
||||
|
||||
dotnet build -c Debug
|
||||
0
网页转Windows应用-CSharp
Normal file
0
网页转Windows应用-CSharp
Normal file
Reference in New Issue
Block a user