新增项目基础结构与配置,集成 Vue3、Vite、TypeScript、ESLint 等开发环境。实现主页面、样式、图标组件,封装 http 请求,支持 WebView2 与普通浏览器统一 API 调用,便于与 C# 后端通信。完善类型声明与开发文档。
340 lines
11 KiB
C#
340 lines
11 KiB
C#
namespace Avalonia_PC.Views
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
private const string BridgeScript = """
|
|
if (!window.__appBridgeInstalled) {
|
|
window.__appBridgeInstalled = true;
|
|
window.isWebView2 = true;
|
|
const pending = new Map();
|
|
|
|
const tryOpenDevTools = () => {
|
|
window.invokeCSharpAction(JSON.stringify({ kind: 'app-open-devtools' }));
|
|
};
|
|
|
|
window.__dispatchAppResponse = function(jsonStr) {
|
|
const payload = JSON.parse(jsonStr);
|
|
const responseId = payload.id ?? payload.Id;
|
|
const entry = pending.get(responseId);
|
|
if (!entry) return;
|
|
pending.delete(responseId);
|
|
entry.resolve(new Response(payload.body ?? payload.Body ?? '', {
|
|
status: payload.statusCode ?? payload.StatusCode ?? 200,
|
|
statusText: payload.statusMessage ?? payload.StatusMessage ?? 'OK',
|
|
headers: payload.headers ?? payload.Headers ?? { 'Content-Type': 'application/json' }
|
|
}));
|
|
};
|
|
|
|
const nativeFetch = window.fetch ? window.fetch.bind(window) : null;
|
|
const NativeXMLHttpRequest = window.XMLHttpRequest;
|
|
|
|
const sendAppBridgeRequest = ({ requestUrl, method, headers, body, timeoutMs = 30000 }) => {
|
|
const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
|
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
const timeoutId = setTimeout(() => {
|
|
pending.delete(id);
|
|
reject(new Error(`Timed out waiting for ${requestUrl}`));
|
|
}, timeoutMs);
|
|
|
|
pending.set(id, {
|
|
resolve: response => { clearTimeout(timeoutId); resolve(response); },
|
|
reject: error => { clearTimeout(timeoutId); reject(error); }
|
|
});
|
|
});
|
|
|
|
window.invokeCSharpAction(JSON.stringify({
|
|
kind: 'app-request',
|
|
id,
|
|
url: requestUrl,
|
|
method,
|
|
headers,
|
|
body
|
|
}));
|
|
|
|
return responsePromise;
|
|
};
|
|
|
|
document.addEventListener('keydown', event => {
|
|
if (event.key === 'F12' || (event.ctrlKey && event.shiftKey && (event.key === 'I' || event.key === 'i'))) {
|
|
event.preventDefault();
|
|
tryOpenDevTools();
|
|
}
|
|
}, true);
|
|
|
|
document.addEventListener('contextmenu', event => {
|
|
if (event.shiftKey) {
|
|
event.preventDefault();
|
|
tryOpenDevTools();
|
|
}
|
|
}, true);
|
|
|
|
window.fetch = async (input, init) => {
|
|
const request = input instanceof Request ? input : null;
|
|
const requestUrl = typeof input === 'string' || input instanceof URL
|
|
? input.toString()
|
|
: request?.url;
|
|
|
|
if (!requestUrl || !requestUrl.startsWith('app://')) {
|
|
if (!nativeFetch) throw new Error('window.fetch is not available.');
|
|
return nativeFetch(input, init);
|
|
}
|
|
|
|
const combinedHeaders = new Headers(request?.headers);
|
|
if (init?.headers) {
|
|
new Headers(init.headers).forEach((value, key) => combinedHeaders.set(key, value));
|
|
}
|
|
|
|
const headers = {};
|
|
combinedHeaders.forEach((value, key) => headers[key] = value);
|
|
|
|
let body = init?.body;
|
|
if (body === undefined && request) {
|
|
body = await request.clone().text();
|
|
}
|
|
|
|
if (body && typeof body !== 'string') {
|
|
body = await new Response(body).text();
|
|
}
|
|
|
|
return sendAppBridgeRequest({
|
|
requestUrl,
|
|
method: init?.method ?? request?.method ?? 'GET',
|
|
headers,
|
|
body: body ?? null,
|
|
timeoutMs: 30000
|
|
});
|
|
};
|
|
|
|
class BridgeXMLHttpRequest {
|
|
constructor() {
|
|
this._native = new NativeXMLHttpRequest();
|
|
this._isAppRequest = false;
|
|
this._requestUrl = '';
|
|
this._method = 'GET';
|
|
this._headers = {};
|
|
this._responseHeaders = {};
|
|
this._responseHeadersRaw = '';
|
|
this._aborted = false;
|
|
|
|
this.readyState = 0;
|
|
this.status = 0;
|
|
this.statusText = '';
|
|
this.response = null;
|
|
this.responseText = '';
|
|
this.responseType = '';
|
|
this.responseURL = '';
|
|
this.timeout = 0;
|
|
this.withCredentials = false;
|
|
|
|
this.onreadystatechange = null;
|
|
this.onload = null;
|
|
this.onerror = null;
|
|
this.ontimeout = null;
|
|
this.onabort = null;
|
|
this.onloadend = null;
|
|
|
|
this.upload = {
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {}
|
|
};
|
|
|
|
this._native.onreadystatechange = () => {
|
|
if (this._isAppRequest) {
|
|
return;
|
|
}
|
|
|
|
this.readyState = this._native.readyState;
|
|
this.status = this._native.status;
|
|
this.statusText = this._native.statusText;
|
|
this.responseURL = this._native.responseURL ?? '';
|
|
this.response = this._native.response;
|
|
this.responseText = this._native.responseText ?? '';
|
|
this._raiseReadyStateChange();
|
|
};
|
|
|
|
this._native.onload = event => {
|
|
if (!this._isAppRequest && typeof this.onload === 'function') {
|
|
this.onload(event);
|
|
}
|
|
};
|
|
|
|
this._native.onerror = event => {
|
|
if (!this._isAppRequest && typeof this.onerror === 'function') {
|
|
this.onerror(event);
|
|
}
|
|
};
|
|
|
|
this._native.ontimeout = event => {
|
|
if (!this._isAppRequest && typeof this.ontimeout === 'function') {
|
|
this.ontimeout(event);
|
|
}
|
|
};
|
|
|
|
this._native.onabort = event => {
|
|
if (!this._isAppRequest && typeof this.onabort === 'function') {
|
|
this.onabort(event);
|
|
}
|
|
};
|
|
|
|
this._native.onloadend = event => {
|
|
if (!this._isAppRequest && typeof this.onloadend === 'function') {
|
|
this.onloadend(event);
|
|
}
|
|
};
|
|
}
|
|
|
|
open(method, url, async = true, user, password) {
|
|
const requestUrl = typeof url === 'string' || url instanceof URL
|
|
? url.toString()
|
|
: `${url ?? ''}`;
|
|
|
|
this._requestUrl = requestUrl;
|
|
this._method = method ?? 'GET';
|
|
this._isAppRequest = requestUrl.startsWith('app://');
|
|
this._headers = {};
|
|
this._responseHeaders = {};
|
|
this._responseHeadersRaw = '';
|
|
this._aborted = false;
|
|
|
|
if (!this._isAppRequest) {
|
|
this._native.open(method, url, async, user, password);
|
|
return;
|
|
}
|
|
|
|
this.readyState = 1;
|
|
this._raiseReadyStateChange();
|
|
}
|
|
|
|
setRequestHeader(name, value) {
|
|
if (!this._isAppRequest) {
|
|
this._native.setRequestHeader(name, value);
|
|
return;
|
|
}
|
|
|
|
this._headers[name] = value;
|
|
}
|
|
|
|
getAllResponseHeaders() {
|
|
if (!this._isAppRequest) {
|
|
return this._native.getAllResponseHeaders();
|
|
}
|
|
|
|
return this._responseHeadersRaw;
|
|
}
|
|
|
|
getResponseHeader(name) {
|
|
if (!this._isAppRequest) {
|
|
return this._native.getResponseHeader(name);
|
|
}
|
|
|
|
return this._responseHeaders[name.toLowerCase()] ?? null;
|
|
}
|
|
|
|
overrideMimeType(mimeType) {
|
|
if (!this._isAppRequest && typeof this._native.overrideMimeType === 'function') {
|
|
this._native.overrideMimeType(mimeType);
|
|
}
|
|
}
|
|
|
|
abort() {
|
|
if (!this._isAppRequest) {
|
|
this._native.abort();
|
|
return;
|
|
}
|
|
|
|
this._aborted = true;
|
|
if (typeof this.onabort === 'function') {
|
|
this.onabort();
|
|
}
|
|
if (typeof this.onloadend === 'function') {
|
|
this.onloadend();
|
|
}
|
|
}
|
|
|
|
async send(body = null) {
|
|
if (!this._isAppRequest) {
|
|
this._native.send(body);
|
|
return;
|
|
}
|
|
|
|
let requestBody = body;
|
|
if (requestBody && typeof requestBody !== 'string') {
|
|
requestBody = await new Response(requestBody).text();
|
|
}
|
|
|
|
try {
|
|
const response = await sendAppBridgeRequest({
|
|
requestUrl: this._requestUrl,
|
|
method: this._method,
|
|
headers: this._headers,
|
|
body: requestBody ?? null,
|
|
timeoutMs: this.timeout > 0 ? this.timeout : 30000
|
|
});
|
|
|
|
if (this._aborted) {
|
|
return;
|
|
}
|
|
|
|
this.status = response.status;
|
|
this.statusText = response.statusText;
|
|
this.responseURL = this._requestUrl;
|
|
|
|
this._responseHeaders = {};
|
|
this._responseHeadersRaw = '';
|
|
response.headers.forEach((value, key) => {
|
|
this._responseHeaders[key.toLowerCase()] = value;
|
|
this._responseHeadersRaw += `${key}: ${value}\r\n`;
|
|
});
|
|
|
|
const text = await response.text();
|
|
this.responseText = text;
|
|
this.response = this.responseType === 'json'
|
|
? (text ? JSON.parse(text) : null)
|
|
: text;
|
|
|
|
this.readyState = 4;
|
|
this._raiseReadyStateChange();
|
|
|
|
if (typeof this.onload === 'function') {
|
|
this.onload();
|
|
}
|
|
if (typeof this.onloadend === 'function') {
|
|
this.onloadend();
|
|
}
|
|
} catch (error) {
|
|
if (this._aborted) {
|
|
return;
|
|
}
|
|
|
|
this.status = 0;
|
|
this.statusText = '';
|
|
this.readyState = 4;
|
|
this._raiseReadyStateChange();
|
|
|
|
const errorMessage = error?.message ?? '';
|
|
if (errorMessage.includes('Timed out waiting') && typeof this.ontimeout === 'function') {
|
|
this.ontimeout(error);
|
|
} else if (typeof this.onerror === 'function') {
|
|
this.onerror(error);
|
|
}
|
|
|
|
if (typeof this.onloadend === 'function') {
|
|
this.onloadend();
|
|
}
|
|
}
|
|
}
|
|
|
|
_raiseReadyStateChange() {
|
|
if (typeof this.onreadystatechange === 'function') {
|
|
this.onreadystatechange();
|
|
}
|
|
}
|
|
}
|
|
|
|
window.XMLHttpRequest = BridgeXMLHttpRequest;
|
|
}
|
|
""";
|
|
}
|
|
}
|