A 增加跨平台通信桥接实现(CSharp/Tauri/WebMock)

A 新增全局类型定义文件(global.d.ts)
A 新增 WebPanel 组件及其集成到 Shell UI
A 新增 BridgeFactory 工厂类用于动态创建桥接实例
U 优化 CommandManager 组件使用新通信桥接逻辑
D 移除未使用的旧代码和配置(e.g., 命令相关的 JS 文件)
This commit is contained in:
jimmy committed 2025-05-10 16:19:28 +08:00
1 parent 709ca30f12
commit 71fb7e2db1
22 files changed
+641 -199

No files matched your search

-1
View File
@@ -1,4 +1,3 @@
VITE_MODE='development'
VITE_APP_API_URL='http://192.****:8008/api/'
VITE_TAURI_DEV_HOST="0.0.0.0"
VITE_API_URL=http://localhost:3000/api
-21
View File
@@ -1,21 +0,0 @@
/**
* Time:2025/5/3 19:12 15
* Name:index.ts
* Path:src/commands
* ProjectName:net-src
* Author:charlatans
*
* Il n'ya qu'un héroïsme au monde :
* c'est de voir le monde tel qu'il est et de l'aimer.
*/
import { invoke } from '@tauri-apps/api/core'
import { greetCommand } from '@/constants/command'
/**
* 问候
* @param name
*/
export function greet(name: string) {
return invoke<string>(greetCommand, { name })
}
+11 -25
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { baseInvoke } from '@/utils/commandUtil.ts'
import type { CommandType } from '@/types/command'
import { logN } from '@/utils/logHelper/logUtils.ts'
import CSharpBridgeV2 from '@/correspond/CSharpBridgeV2'
// 传递参数
const props = defineProps({
@@ -10,6 +10,7 @@ const props = defineProps({
})
// 使用 ref 使数据变成响应式数据
const modules: Ref<CommandType[]> = ref([...(props.pro || [])])
const bridge = CSharpBridgeV2.getBridge()
/**
* 指令触发
@@ -20,13 +21,9 @@ const invokeCommand = async (module: CommandType) => {
logN.success('前端发送的参数', module.name, params)
try {
const s = baseInvoke(module.name, params)
s.then((res) => {
logN.warning('后端返回的参数', module.name, res)
module.result = JSON.stringify(res, null, 2)
}).catch((err) => {
console.error(err)
module.result = `Error: ${err.message}`
bridge.send(module.name, params, (data) => {
logN.warning('后端返回的参数', module.name, data)
module.result = JSON.stringify(data, null, 2)
})
} catch (error) {
console.error(err)
@@ -35,7 +32,6 @@ const invokeCommand = async (module: CommandType) => {
}
</script>
<template>
<div class="container">
<div class="modules-grid">
@@ -44,11 +40,7 @@ const invokeCommand = async (module: CommandType) => {
<p class="module-description">{{ module.description }}</p>
<div class="params-container">
<div
v-for="(param, paramIndex) in module.params"
:key="paramIndex"
class="param-item"
>
<div v-for="(param, paramIndex) in module.params" :key="paramIndex" class="param-item">
<label :for="`${module.name}-${param.name}`" class="param-label">
{{ param.label }}
</label>
@@ -70,12 +62,7 @@ const invokeCommand = async (module: CommandType) => {
</div>
</div>
<button
@click="invokeCommand(module)"
class="action-button"
>
触发 {{ module.name }}
</button>
<button @click="invokeCommand(module)" class="action-button">触发 {{ module.name }}</button>
<div v-if="module.result" class="result-container">
<h3 class="result-title">结果</h3>
@@ -108,9 +95,8 @@ const invokeCommand = async (module: CommandType) => {
font-size: 1.125rem; // text-lg
font-weight: 600; // font-semibold
margin-bottom: 0.5rem; // mb-2
user-select: text; /* 允许选中 */
pointer-events: auto; /* 确保可以响应鼠标操作 */
user-select: text; /* 允许选中 */
pointer-events: auto; /* 确保可以响应鼠标操作 */
}
.module-description {
@@ -189,8 +175,8 @@ const invokeCommand = async (module: CommandType) => {
.result-content {
font-size: 0.875rem; // text-sm
white-space: pre-wrap;
user-select: text; /* 允许选中 */
pointer-events: auto; /* 确保可以响应鼠标操作 */
user-select: text; /* 允许选中 */
pointer-events: auto; /* 确保可以响应鼠标操作 */
}
}
}
+66
View File
@@ -0,0 +1,66 @@
import IBridge from "./IBridge";
/**
* 业务层类型定义
*/
export interface UserInfo {
id: string;
name: string;
email?: string;
role: string;
permissions: string[];
}
export interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'warning' | 'error' | 'success';
timestamp: number;
}
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export interface LogPayload {
level: LogLevel;
message: string;
context?: Record<string, unknown>;
}
/**
* 业务功能层 - 提供更高级的通信功能
*/
export class AppBridge {
private bridge: IBridge;
constructor() {
this.bridge = BridgeFactory.create();
}
// 获取用户信息
async getUserInfo(): Promise<UserInfo> {
return this.bridge.send<UserInfo>('user.getInfo');
}
// 监听系统通知
onNotification(listener: (notification: Notification) => void): () => void {
return this.bridge.on<Notification>('system.notification', listener);
}
// 发送日志
async sendLog(payload: LogPayload): Promise<void> {
return this.bridge.send<void, LogPayload>('system.log', payload);
}
// 获取系统配置
async getConfig<T extends Record<string, unknown>>(key: string): Promise<T> {
return this.bridge.send<T, { key: string }>('system.getConfig', { key });
}
// 保存系统配置
async saveConfig<T extends Record<string, unknown>>(key: string, value: T): Promise<void> {
return this.bridge.send<void, { key: string; value: T }>('system.saveConfig', { key, value });
}
}
// 创建单例实例
export const appBridge = new AppBridge();
+88
View File
@@ -0,0 +1,88 @@
/**
* Time:2025/5/10 13:42 55
* Name:BaseBridge.ts
* Path:src/correspond
* ProjectName:ClientApp
* Author:charlatans
*
* Il n'ya qu'un héroïsme au monde :
* c'est de voir le monde tel qu'il est et de l'aimer.
*/
import type MessagePayload from "./IBridge"
import { IBridge } from '@/correspond/IBridge'
/**
* 平台适配层基类
*/
abstract class BaseBridge implements IBridge {
protected listeners: Map<string, Set<(data: unknown) => void>> = new Map();
abstract send<TResponse = void, TRequest extends MessagePayload = MessagePayload>(
channel: string,
data?: TRequest
): Promise<TResponse>;
on<TData extends MessagePayload = MessagePayload>(
channel: string,
listener: (data: TData) => void
): () => void {
if (!this.listeners.has(channel)) {
this.listeners.set(channel, new Set());
}
// TypeScript 不允许将具体类型的函数分配给更通用的类型,因此这里需要类型转换
const typedListener = listener as (data: unknown) => void;
this.listeners.get(channel)!.add(typedListener);
return () => this.removeListener(channel, listener);
}
once<TData extends MessagePayload = MessagePayload>(
channel: string,
listener: (data: TData) => void
): () => void {
// 创建一个一次性监听器
const onceListener = ((data: TData) => {
listener(data);
this.removeListener(channel, onceListener);
});
return this.on(channel, onceListener);
}
off(channel: string): void {
this.listeners.delete(channel);
}
removeListener<TData extends MessagePayload = MessagePayload>(
channel: string,
listener: (data: TData) => void
): void {
const channelListeners = this.listeners.get(channel);
if (channelListeners) {
// 需要类型转换以匹配存储的监听器类型
const typedListener = listener as (data: unknown) => void;
channelListeners.delete(typedListener);
if (channelListeners.size === 0) {
this.listeners.delete(channel);
}
}
}
protected dispatchEvent<TData extends MessagePayload = MessagePayload>(
channel: string,
data: TData
): void {
const listeners = this.listeners.get(channel);
if (listeners) {
listeners.forEach(listener => {
try {
listener(data);
} catch (error) {
console.error(`Error in listener for channel ${channel}:`, error);
}
});
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import CSharpBridge from "./CSharpBridge";
import TauriBridge from "./TauriBridge";
/**
* 桥梁工厂 - 根据运行环境创建适当的桥梁实例
*/
export class BridgeFactory {
static create(): IBridge {
if (typeof window !== 'undefined') {
// 根据环境判断使用哪个桥接实现
if (window.csharpApp) {
return new CSharpBridge();
} else if (window.__TAURI__) {
return new TauriBridge();
}
}
// 默认返回一个Web模拟实现
return new WebMockBridge();
}
}
+44
View File
@@ -0,0 +1,44 @@
import BaseBridge from "./BaseBridge"
/**
* C#套壳浏览器的适配器
*/
class CSharpBridge extends BaseBridge {
constructor() {
super();
// 假设C#注入了window.csharpApp对象
if (typeof window !== 'undefined' && window.csharpApp) {
// 监听C#发来的消息
window.receiveCSharpMessage = (channel: string, dataJson: string) => {
try {
const data = JSON.parse(dataJson) as MessagePayload;
this.dispatchEvent(channel, data);
} catch (error) {
console.error('Failed to parse message from C#:', error);
}
};
}
}
async send<TResponse = void, TRequest extends MessagePayload = MessagePayload>(
channel: string,
data?: TRequest
): Promise<TResponse> {
if (typeof window === 'undefined' || !window.csharpApp) {
throw new BridgeError('CSharp bridge not available', 'BRIDGE_UNAVAILABLE');
}
try {
// 假设C#提供了sendToCSharp方法
const responseJson = window.csharpApp.sendToCSharp(
channel,
JSON.stringify(data || {})
);
return JSON.parse(responseJson) as TResponse;
} catch (error) {
if (error instanceof Error) {
throw new BridgeError(`Failed to send message to C#: ${error.message}`, 'SEND_FAILED');
}
throw new BridgeError('Unknown error when sending message to C#', 'UNKNOWN_ERROR');
}
}
}
@@ -0,0 +1,91 @@
import { MessagePayload } from '@/correspond/IBridge'
import CSharpBridge from '@/correspond/CSharpBridge'
import TauriBridge from '@/correspond/TauriBridge'
type MessagePayload<T> = {
channel: string
payload: T
}
class CSharpBridgeV2 {
private static CSBridge: CSharpBridgeV2 | null = null
static getBridge(): CSharpBridgeV2 | null {
if (CSharpBridgeV2.CSBridge === null || CSharpBridgeV2.CSBridge.CSBridge === undefined) {
if (window.chrome && window.chrome.webview) {
// window.chrome.webview.postMessage("hello");
CSharpBridgeV2.CSBridge = new CSharpBridgeV2()
window.externalFunctions.__BRIDGE_LISTEN__ = CSharpBridgeV2.CSBridge.bridgeInvoke
window.externalFunctions.__BRIDGE_LISTEN__FUNCTIONS__ = new Map()
} else {
console.error('Not running in WebView2 environment')
}
}
return CSharpBridgeV2.CSBridge
}
bridgeInvoke(channel: string, json: string) {
const bridgeListenFunctions = window.externalFunctions.__BRIDGE_LISTEN__FUNCTIONS__
const listeners = bridgeListenFunctions.get(channel)
if (listeners) {
listeners.forEach((listener) => {
try {
listener.callback(json)
if (listener.once && CSharpBridgeV2.CSBridge) {
CSharpBridgeV2.CSBridge.removeListen(channel, listener)
}
} catch (error) {
console.error(`Error in listener for channel ${channel}:`, error)
}
})
}
}
// 发送消息到后端
send<T, V>(channel: string, data?: T | null = null, callback: ((data: V) => void) | null = null) {
const obj: MessagePayload = {
channel,
payload: data,
}
if (callback !== null && callback !== undefined) {
this.listen(channel, callback, true)
}
window.chrome.webview.postMessage(obj)
}
/**
* 监听后端消息
*/
listen<V>(channel: string, callback: (data: V) => void, once: boolean = false) {
const bridgeListenFunctions = window.externalFunctions.__BRIDGE_LISTEN__FUNCTIONS__
if (!bridgeListenFunctions.has(channel)) {
bridgeListenFunctions.set(channel, new Set())
}
bridgeListenFunctions.get(channel)!.add({
callback: callback,
once: once,
})
}
// 移除指定通道的所有监听器
off(channel: string) {
window.externalFunctions.__BRIDGE_LISTEN__FUNCTIONS__.delete(channel)
}
// 移除特定监听器
removeListen(channel: string, listener: unknown) {
const bridgeListenFunctions = window.externalFunctions.__BRIDGE_LISTEN__FUNCTIONS__
const channelListeners = bridgeListenFunctions.get(channel)
if (channelListeners) {
// 需要类型转换以匹配存储的监听器类型
channelListeners.delete(listener)
if (channelListeners.size === 0) {
bridgeListenFunctions.delete(channel)
}
}
}
}
export default CSharpBridgeV2
+80
View File
@@ -0,0 +1,80 @@
/**
* Time:2025/5/10 13:31 41
* Name:IBridge.ts
* Path:src/correspond
* ProjectName:ClientApp
* Author:charlatans
*
* Il n'ya qu'un héroïsme au monde :
* c'est de voir le monde tel qu'il est et de l'aimer.
*/
/**
* 消息类型定义 - 用于类型安全的通信
*/
export type MessagePayload = Record<string, unknown>;
/**
* 核心接口层 - 定义与后端通信的基本能力
*/
export interface IBridge {
// 发送消息到后端
send<TResponse = void, TRequest extends MessagePayload = MessagePayload>(
channel: string,
data?: TRequest
): Promise<TResponse>;
// 监听后端消息
on<TData extends MessagePayload = MessagePayload>(
channel: string,
listener: (data: TData) => void
): () => void;
// 只监听一次后端消息
once<TData extends MessagePayload = MessagePayload>(
channel: string,
listener: (data: TData) => void
): () => void;
// 移除指定通道的所有监听器
off(channel: string): void;
// 移除特定监听器
removeListener<TData extends MessagePayload = MessagePayload>(
channel: string,
listener: (data: TData) => void
): void;
}
/**
* 定义Window扩展,用于与原生平台交互
*/
interface CSharpAppInterface {
sendToCSharp(channel: string, dataJson: string): string;
}
interface TauriInterface {
invoke<T>(cmd: string, args?: unknown): Promise<T>;
event: {
listen(event: string, handler: (event: { payload: unknown }) => void): Promise<() => void>;
once(event: string, handler: (event: { payload: unknown }) => void): Promise<void>;
unlisten(event: string): Promise<void>;
};
}
interface WindowWithNative extends Window {
csharpApp?: CSharpAppInterface;
receiveCSharpMessage?: (channel: string, dataJson: string) => void;
__TAURI__?: TauriInterface;
}
declare let window: WindowWithNative;
/**
* 异常类型定义
*/
export class BridgeError extends Error {
constructor(message: string, public readonly code?: string) {
super(message);
this.name = 'BridgeError';
}
}
+62
View File
@@ -0,0 +1,62 @@
import BaseBridge from "./BaseBridge";
/**
* Tauri适配器
*/
class TauriBridge extends BaseBridge {
constructor() {
super();
// Tauri初始化代码
if (typeof window !== 'undefined' && window.__TAURI__) {
// 设置Tauri事件监听
window.__TAURI__.event.listen('tauri://event', (event: { payload: unknown }) => {
const eventData = event.payload as { channel: string; payload: MessagePayload };
if (eventData.channel) {
this.dispatchEvent(eventData.channel, eventData.payload);
}
});
}
}
async send<TResponse = void, TRequest extends MessagePayload = MessagePayload>(
channel: string,
data?: TRequest
): Promise<TResponse> {
if (typeof window === 'undefined' || !window.__TAURI__) {
throw new BridgeError('Tauri bridge not available', 'BRIDGE_UNAVAILABLE');
}
try {
// 使用Tauri的invoke函数
return await window.__TAURI__.invoke<TResponse>(channel, data || {});
} catch (error) {
if (error instanceof Error) {
throw new BridgeError(`Failed to invoke Tauri command: ${error.message}`, 'INVOKE_FAILED');
}
throw new BridgeError('Unknown error when invoking Tauri command', 'UNKNOWN_ERROR');
}
}
override on<TData extends MessagePayload = MessagePayload>(
channel: string,
listener: (data: TData) => void
): () => void {
const unsubscribe = super.on(channel, listener);
// 向Tauri注册事件监听
if (typeof window !== 'undefined' && window.__TAURI__) {
window.__TAURI__.event.listen(channel, (event: { payload: unknown }) => {
listener(event.payload as TData);
}).catch(console.error);
}
return unsubscribe;
}
override off(channel: string): void {
super.off(channel);
// Tauri的事件注销需要特殊处理
if (typeof window !== 'undefined' && window.__TAURI__) {
window.__TAURI__.event.unlisten(channel).catch(console.error);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
import BaseBridge from "./BaseBridge";
/**
* Web环境的模拟实现 - 用于开发调试
*/
class WebMockBridge extends BaseBridge {
async send<TResponse = void, TRequest extends MessagePayload = MessagePayload>(
channel: string,
data?: TRequest
): Promise<TResponse> {
console.log(`[WebMock] Send to channel ${channel}:`, data);
// 模拟异步响应
return new Promise<TResponse>(resolve => {
setTimeout(() => {
// 默认的模拟响应
const mockResponse = { success: true, mock: true } as unknown as TResponse;
resolve(mockResponse);
}, 100);
});
}
// 用于测试 - 模拟后端发送消息
mockReceive<TData extends MessagePayload = MessagePayload>(channel: string, data: TData): void {
this.dispatchEvent(channel, data);
}
}
+3
View File
@@ -24,3 +24,6 @@ app.use(router)
app.use(vuetify)
app.mount('#app')
// 注册 C# 函数
window.externalFunctions = {};
+25
View File
@@ -0,0 +1,25 @@
/**
* Time:2025/5/10 13:08 02
* Name:global.d.ts
* Path:src/types
* ProjectName:ClientApp
* Author:charlatans
*
* Il n'ya qu'un héroïsme au monde :
* c'est de voir le monde tel qu'il est et de l'aimer.
*/
// global.d.ts
interface WebView2 {
postMessage: (message: string) => void;
addEventListener: <T>(type: "message", handler: (event: T) => void) => void;
}
interface ChromeWithWebView {
webview: WebView2;
}
interface Window {
chrome?: ChromeWithWebView;
}
-15
View File
@@ -1,15 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
/**
* Time:2024/12/14 12:21 30
* Name:commandUtil.ts
* Path:src/utils
* ProjectName:argus-src
* Author:charlatans
*
* Il n'ya qu'un héroïsme au monde :
* c'est de voir le monde tel qu'il est et de l'aimer.
*/
export function baseInvoke<T>(command: string, args: boolean | string | number | object) {
return invoke<T>(command, args)
}
@@ -1,27 +0,0 @@
<script setup lang="ts">
import CommandManager from '@/components/CommandManager.vue'
import type { CommandType } from '@/types/command'
const commands: CommandType[] = [
{
name: 'greet',
description: '查询exif信息',
params: [
{
name: 'name',
label: 'name',
type: 'text',
value: 'Vue',
placeholder: 'string'
}
],
result: null
}
]
</script>
<template>
<CommandManager :pro="commands"></CommandManager>
</template>
<style scoped></style>
@@ -1,25 +0,0 @@
<script setup lang="ts">
import CommandManager from '@/components/CommandManager.vue'
import type { CommandType } from '@/types/command'
const commands: CommandType[] = [
{
name: 'is_elevated_command',
description: '是否有管理员权限',
params: [],
result: null,
},
{
name: 'get_exe_directory_command',
description: '获取程序所在目录',
params: [],
result: null,
},
]
</script>
<template>
<CommandManager :pro="commands"></CommandManager>
</template>
<style scoped></style>
@@ -1,53 +1,50 @@
<script setup lang="ts">
import CommandManager from '@/components/CommandManager.vue'
import type { CommandType } from '@/types/command'
import {onMounted,onUnmounted} from 'vue'
import CSharpBridgeV2 from "@/correspond/CSharpBridgeV2"
const bridge = CSharpBridgeV2.getBridge()
bridge.listen("showMessage",(data) => {
console.log('11111111',data);
})
function func() {
console.log('点击anniu')
bridge.send('showMessage',{
title: "来自前端的问候",
content: "Hello C#!"
},(data) => {
console.log(data,'======');
console.log(window.externalFunctions.__BRIDGE_LISTEN__FUNCTIONS__);
})
}
const commands: CommandType[] = [
{
name: 'win_divert_command',
description: '显示所有网络连接信息',
params: [],
result: null,
},
{
name: 'monitor_network_with_windivert_command',
description: '使用WinDivert监控网络数据包',
params: [],
result: null,
},
{
name: 'recv_packet_command',
description: '安全接收数据包的辅助函数',
params: [],
result: null,
},
{
name: 'recv_packet_command1',
description: '外部包接口!',
params: [],
result: null,
},
{
name: 'get_all_tcp_info',
description: '获取所有 TCP 信息!',
params: [],
result: null,
},
{
name: 'get_more_info',
description: 'get_more_info',
name: 'showMessage',
description: '获取信息!!',
params: [
{
name: 'id',
label: 'id',
name: 'title',
label: '标题',
type: 'text',
value: 'Vue',
placeholder: 'text'
}
},{
name: 'content',
label: '标题',
type: 'text',
value: 'Hello C#!',
placeholder: 'text'
},
],
result: null,
},
]
</script>
<template>
+2 -26
View File
@@ -8,8 +8,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const host = env.VITE_TAURI_DEV_HOST;
return {
plugins: [
vue(),
@@ -20,35 +19,12 @@ export default defineConfig(({ mode }) => {
clearScreen: false,
server: {
port: 3000,
// Tauri 需要一个固定端口,如果该端口不可用,则失败
strictPort: true,
// 如果设置了 Tauri 期望的主机,请使用它
host: host || false,
hmr: host
? {
protocol: 'ws',
host,
port: 3000,
}
: undefined,
host: "0.0.0.0",
watch: {
// 告诉 Vite 忽略关注 'src-tauri'
ignored: ['**/src-tauri/**'],
},
},
// 以 'envPrefix' 项开头的 env 变量将通过 'import.meta.env' 暴露在 tauri 的源代码中。
envPrefix: ['VITE_', 'TAURI_ENV_*'],
build: {
// Tauri 在 Windows 上使用 Chromium,在 macOS 和 Linux 上使用 WebKit
target:
process.env.TAURI_ENV_PLATFORM == 'windows'
? 'chrome105'
: 'safari13',
// 不要缩小调试版本
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
// 生成用于调试版本的 SourceMap
sourcemap: !!process.env.TAURI_ENV_DEBUG,
},
resolve: {
alias: {
+3 -3
View File
@@ -4,10 +4,10 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Shell"
xmlns:webview2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:views="clr-namespace:Shell.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
Title="MainWindow" Height="1000" Width="1300">
<Grid>
<webview2:WebView2 Name="webView" />
<views:WebPanel/>
</Grid>
</Window>
+1 -21
View File
@@ -1,4 +1,5 @@
using System.Text;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
@@ -19,26 +20,5 @@ public partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
InitWebView();
}
private async void InitWebView()
{
// 初始化 WebView2
await webView.EnsureCoreWebView2Async();
// 加载前端页面:可以是本地文件或服务器地址
// 示例 1:加载本地 HTML
// string htmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "frontend", "index.html");
// webView.CoreWebView2.Navigate($"file:///{htmlPath.Replace("\\", "/")}");
// 示例 2:加载远程网页
webView.CoreWebView2.Navigate("http://localhost:3000");
webView.CoreWebView2.WebMessageReceived += (s, e) =>
{
string message = e.TryGetWebMessageAsString();
MessageBox.Show($"收到消息: {message}");
};
}
}
+14
View File
@@ -0,0 +1,14 @@
<UserControl
x:Class="Shell.Views.WebPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Shell.Views"
xmlns:webview2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<webview2:WebView2
Name="webView" />
</UserControl>
+74
View File
@@ -0,0 +1,74 @@
using System.Windows.Controls;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
namespace Shell.Views;
public partial class WebPanel : UserControl
{
public WebPanel()
{
InitializeComponent();
InitWebView();
}
private async void InitWebView()
{
// 初始化 WebView2
await webView.EnsureCoreWebView2Async();
// 加载前端页面:可以是本地文件或服务器地址
// 示例 1:加载本地 HTML
// string htmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "frontend", "index.html");
// webView.CoreWebView2.Navigate($"file:///{htmlPath.Replace("\\", "/")}");
// 示例 2:加载远程网页
webView.CoreWebView2.Navigate("http://localhost:3000");
webView.CoreWebView2.WebMessageReceived += (sender, args) =>
{
string json = args.WebMessageAsJson;
// 反序列化为 C# 对象
var msg = JsonSerializer.Deserialize<JsMessage>(json);
Console.WriteLine(json);
Console.WriteLine(msg?.Channel);
if (msg?.Channel == "showMessage")
{
MessageBox.Show(msg.Payload?.Content ?? "无内容", msg.Payload?.Title ?? "提示");
InitWebView1();
}
};
}
private async void InitWebView1()
{
await webView.ExecuteScriptAsync("""
window.externalFunctions.__BRIDGE_LISTEN__('showMessage','来自 C# 的问候');
""");
}
}
public class JsMessage
{
[JsonPropertyName("channel")] public string Channel { get; set; }
[JsonPropertyName("payload")] public JsData Payload { get; set; }
public override string ToString()
{
return $"Channel:{Channel}, Payload:{Payload}";
}
}
public class JsData
{
[JsonPropertyName("title")] public string Title { get; set; }
[JsonPropertyName("content")] public string Content { get; set; }
public override string ToString()
{
return $"Title:{Title}, Content:{Content}";
}
}