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:
1 parent
709ca30f12
commit
71fb7e2db1
22 files changed
+641
-199
No files matched your search
@@ -1,4 +1,3 @@
|
|||||||
VITE_MODE='development'
|
VITE_MODE='development'
|
||||||
VITE_APP_API_URL='http://192.****:8008/api/'
|
VITE_APP_API_URL='http://192.****:8008/api/'
|
||||||
VITE_TAURI_DEV_HOST="0.0.0.0"
|
|
||||||
VITE_API_URL=http://localhost:3000/api
|
VITE_API_URL=http://localhost:3000/api
|
||||||
@@ -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 })
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Ref, ref } from 'vue'
|
import { type Ref, ref } from 'vue'
|
||||||
import { baseInvoke } from '@/utils/commandUtil.ts'
|
|
||||||
import type { CommandType } from '@/types/command'
|
import type { CommandType } from '@/types/command'
|
||||||
import { logN } from '@/utils/logHelper/logUtils.ts'
|
import { logN } from '@/utils/logHelper/logUtils.ts'
|
||||||
|
import CSharpBridgeV2 from '@/correspond/CSharpBridgeV2'
|
||||||
|
|
||||||
// 传递参数
|
// 传递参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -10,6 +10,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
// 使用 ref 使数据变成响应式数据
|
// 使用 ref 使数据变成响应式数据
|
||||||
const modules: Ref<CommandType[]> = ref([...(props.pro || [])])
|
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)
|
logN.success('前端发送的参数', module.name, params)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const s = baseInvoke(module.name, params)
|
bridge.send(module.name, params, (data) => {
|
||||||
s.then((res) => {
|
logN.warning('后端返回的参数', module.name, data)
|
||||||
logN.warning('后端返回的参数', module.name, res)
|
module.result = JSON.stringify(data, null, 2)
|
||||||
module.result = JSON.stringify(res, null, 2)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
module.result = `Error: ${err.message}`
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -35,7 +32,6 @@ const invokeCommand = async (module: CommandType) => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="modules-grid">
|
<div class="modules-grid">
|
||||||
@@ -44,11 +40,7 @@ const invokeCommand = async (module: CommandType) => {
|
|||||||
<p class="module-description">{{ module.description }}</p>
|
<p class="module-description">{{ module.description }}</p>
|
||||||
|
|
||||||
<div class="params-container">
|
<div class="params-container">
|
||||||
<div
|
<div v-for="(param, paramIndex) in module.params" :key="paramIndex" class="param-item">
|
||||||
v-for="(param, paramIndex) in module.params"
|
|
||||||
:key="paramIndex"
|
|
||||||
class="param-item"
|
|
||||||
>
|
|
||||||
<label :for="`${module.name}-${param.name}`" class="param-label">
|
<label :for="`${module.name}-${param.name}`" class="param-label">
|
||||||
{{ param.label }}
|
{{ param.label }}
|
||||||
</label>
|
</label>
|
||||||
@@ -70,12 +62,7 @@ const invokeCommand = async (module: CommandType) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button @click="invokeCommand(module)" class="action-button">触发 {{ module.name }}</button>
|
||||||
@click="invokeCommand(module)"
|
|
||||||
class="action-button"
|
|
||||||
>
|
|
||||||
触发 {{ module.name }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div v-if="module.result" class="result-container">
|
<div v-if="module.result" class="result-container">
|
||||||
<h3 class="result-title">结果</h3>
|
<h3 class="result-title">结果</h3>
|
||||||
@@ -108,9 +95,8 @@ const invokeCommand = async (module: CommandType) => {
|
|||||||
font-size: 1.125rem; // text-lg
|
font-size: 1.125rem; // text-lg
|
||||||
font-weight: 600; // font-semibold
|
font-weight: 600; // font-semibold
|
||||||
margin-bottom: 0.5rem; // mb-2
|
margin-bottom: 0.5rem; // mb-2
|
||||||
user-select: text; /* 允许选中 */
|
user-select: text; /* 允许选中 */
|
||||||
pointer-events: auto; /* 确保可以响应鼠标操作 */
|
pointer-events: auto; /* 确保可以响应鼠标操作 */
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-description {
|
.module-description {
|
||||||
@@ -189,8 +175,8 @@ const invokeCommand = async (module: CommandType) => {
|
|||||||
.result-content {
|
.result-content {
|
||||||
font-size: 0.875rem; // text-sm
|
font-size: 0.875rem; // text-sm
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
user-select: text; /* 允许选中 */
|
user-select: text; /* 允许选中 */
|
||||||
pointer-events: auto; /* 确保可以响应鼠标操作 */
|
pointer-events: auto; /* 确保可以响应鼠标操作 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,3 +24,6 @@ app.use(router)
|
|||||||
app.use(vuetify)
|
app.use(vuetify)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
|
// 注册 C# 函数
|
||||||
|
window.externalFunctions = {};
|
||||||
Vendored
+25
@@ -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;
|
||||||
|
}
|
||||||
@@ -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">
|
<script setup lang="ts">
|
||||||
import CommandManager from '@/components/CommandManager.vue'
|
import CommandManager from '@/components/CommandManager.vue'
|
||||||
import type { CommandType } from '@/types/command'
|
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[] = [
|
const commands: CommandType[] = [
|
||||||
{
|
{
|
||||||
name: 'win_divert_command',
|
name: 'showMessage',
|
||||||
description: '显示所有网络连接信息',
|
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',
|
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
name: 'id',
|
name: 'title',
|
||||||
label: 'id',
|
label: '标题',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: 'Vue',
|
value: 'Vue',
|
||||||
placeholder: 'text'
|
placeholder: 'text'
|
||||||
}
|
},{
|
||||||
|
name: 'content',
|
||||||
|
label: '标题',
|
||||||
|
type: 'text',
|
||||||
|
value: 'Hello C#!',
|
||||||
|
placeholder: 'text'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
result: null,
|
result: null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
|||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd())
|
const env = loadEnv(mode, process.cwd())
|
||||||
const host = env.VITE_TAURI_DEV_HOST;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
@@ -20,35 +19,12 @@ export default defineConfig(({ mode }) => {
|
|||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
// Tauri 需要一个固定端口,如果该端口不可用,则失败
|
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
// 如果设置了 Tauri 期望的主机,请使用它
|
host: "0.0.0.0",
|
||||||
host: host || false,
|
|
||||||
hmr: host
|
|
||||||
? {
|
|
||||||
protocol: 'ws',
|
|
||||||
host,
|
|
||||||
port: 3000,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
// 告诉 Vite 忽略关注 'src-tauri'
|
|
||||||
ignored: ['**/src-tauri/**'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 以 'envPrefix' 项开头的 env 变量将通过 'import.meta.env' 暴露在 tauri 的源代码中。
|
|
||||||
envPrefix: ['VITE_', 'TAURI_ENV_*'],
|
|
||||||
build: {
|
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: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:Shell"
|
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"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="450" Width="800">
|
Title="MainWindow" Height="1000" Width="1300">
|
||||||
<Grid>
|
<Grid>
|
||||||
<webview2:WebView2 Name="webView" />
|
<views:WebPanel/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
@@ -19,26 +20,5 @@ public partial class MainWindow : Window
|
|||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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}");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user