feat(desktop): 迁移 Wails v2 → v3(架构/目录/绑定重写)[WIP]
从 wails/v2 v2.12.0 迁到 v3 v3.0.0-alpha2.117,采用 v3 工程规范:
Go 侧:
- main.go:wails.Run(options.App) → application.New(Options{Services,Assets,Mac})
+ app.Window.NewWithOptions(WebviewWindowOptions) + app.Run();窗口选项照搬
(1440×900/min 1100×700/#0b0d12/Mac TitleBarHiddenInset)
- app.go:App 结构体绑定 → v3 Service(application.NewService);去 startup/ctx 注入;
SaveFileDialog 改 v3 链式 API(application.Get().Dialog.SaveFile().SetFilename().
AddFilter().PromptForSingleSelection());5 个原生方法保留
- go.mod wails/v2→v3、go 1.25
构建:
- wails.json → Taskfile.yml + build/(config.yml 填产品信息 + 各平台配置)
- 删移动端脚手架(ios/android),桌面端只留 darwin/windows/linux
- .gitignore 改为只忽略 build/bin(build/ 构建系统需提交)
前端:
- vite.config 接 @wailsio/runtime/plugins/vite 插件 + WAILS_VITE_PORT
- package.json 加 @wailsio/runtime;wails3 generate bindings → frontend/bindings/
- desktop.ts:window.go.main.App → import { App } from bindings;桌面态检测改
window._wails.environment(原生注入);isMacDesktop 用 System.IsMac()
- version.ts:runtime.BrowserOpenURL → @wailsio/runtime Browser.OpenURL;测试同步更新
- tsconfig 加 allowJs(消费 JSDoc 绑定)
验证进度:Go 编译过 + tsc 干净 + 48 前端测试绿 + 前端 vite build 通过。
**未完成:整体 wails3 build/dev 原生窗口实跑 + 原生能力(另存为/打开/外链)联调**——下次续。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* App 经 Wails v3 的 Service 绑定暴露给前端,承载只有桌面端能做的原生能力:
|
||||
* 文件读写、系统"另存为"框、用系统默认应用打开、原生通知。
|
||||
* v3 中不再需要注入 ctx——对话框经 application.Get().Dialog 获取。
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* Notify 弹一条系统通知(best-effort:macOS 用 osascript,其它平台暂静默)。
|
||||
* @param {string} title
|
||||
* @param {string} body
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Notify(title, body) {
|
||||
return $Call.ByID(2739682372, title, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenReport 把报告下载到临时目录,并用系统默认应用(Word/Pages/WPS)打开。
|
||||
* @param {string} url
|
||||
* @param {string} filename
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function OpenReport(url, filename) {
|
||||
return $Call.ByID(802670751, url, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping 供前端探活 Go 桥是否就绪。
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function Ping() {
|
||||
return $Call.ByID(1003072145);
|
||||
}
|
||||
|
||||
/**
|
||||
* ReadLocalFile 读取本地文件内容(本地文件系统 I/O)。
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function ReadLocalFile(path) {
|
||||
return $Call.ByID(4016121990, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* SaveReportAs 弹原生"另存为"对话框,把 url 指向的报告(.docx)下载到用户选定路径。
|
||||
* 返回保存路径;用户取消则返回空串。
|
||||
* @param {string} url
|
||||
* @param {string} filename
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function SaveReportAs(url, filename) {
|
||||
return $Call.ByID(1437856486, url, filename);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as App from "./app.js";
|
||||
export {
|
||||
App
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
//@ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
Object.freeze($Create.Events);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
+7
@@ -8,6 +8,7 @@
|
||||
"name": "sundynix-desktop-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@wailsio/runtime": "^3.0.0-alpha.97",
|
||||
"@xyflow/react": "^12.3.0",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.0.0",
|
||||
@@ -2142,6 +2143,12 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@wailsio/runtime": {
|
||||
"version": "3.0.0-alpha.97",
|
||||
"resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.97.tgz",
|
||||
"integrity": "sha512-alvEG6B0YqW7AcBcNaUWNMD9q8JWqx5AO6BPOZpijBeq4ZjhQxSom3jvvq8Z1mWSMKPEjSvlFirKIh9NLI3ajA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wailsio/runtime": "^3.0.0-alpha.97",
|
||||
"@xyflow/react": "^12.3.0",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -1,50 +1,44 @@
|
||||
// 桌面端原生能力桥 —— 经 Wails 注入的 window.go.main.App 调 Go 方法。
|
||||
// 关键:在浏览器预览(make web,无 Wails 运行时)下 window.go 不存在,
|
||||
// 所有方法自动降级为纯 Web 行为,保证两种模式都能跑。
|
||||
// 桌面端原生能力桥 —— Wails v3 Service 绑定(App)调 Go 方法。
|
||||
// 关键:在浏览器预览(无 Wails 运行时)下自动降级为纯 Web 行为,保证两种模式都能跑。
|
||||
// v3 检测:原生启动时注入 window._wails.environment;浏览器没有,据此判定是否在桌面壳内。
|
||||
import { App } from "../../bindings/github.com/sundynix/sundynix-desktop";
|
||||
import { System } from "@wailsio/runtime";
|
||||
|
||||
interface WailsApp {
|
||||
SaveReportAs(url: string, filename: string): Promise<string>;
|
||||
OpenReport(url: string, filename: string): Promise<void>;
|
||||
Notify(title: string, body: string): Promise<void>;
|
||||
}
|
||||
|
||||
function app(): WailsApp | null {
|
||||
return ((window as unknown as { go?: { main?: { App?: WailsApp } } }).go?.main?.App as WailsApp) ?? null;
|
||||
function inWails(): boolean {
|
||||
return !!(window as unknown as { _wails?: { environment?: unknown } })._wails?.environment;
|
||||
}
|
||||
|
||||
// isDesktop 是否运行在真实 Wails 桌面窗口(而非浏览器预览)。
|
||||
export function isDesktop(): boolean {
|
||||
return !!app();
|
||||
return inWails();
|
||||
}
|
||||
|
||||
// isMacDesktop 用于交通灯让位等 macOS 专属适配。
|
||||
export function isMacDesktop(): boolean {
|
||||
return isDesktop() && /Mac/i.test(navigator.userAgent);
|
||||
return inWails() && System.IsMac();
|
||||
}
|
||||
|
||||
// saveReportAs:桌面端弹原生"另存为"框落盘;浏览器降级为触发下载。返回保存路径(浏览器/取消为空)。
|
||||
export async function saveReportAs(url: string, filename: string): Promise<string> {
|
||||
const a = app();
|
||||
if (!a) {
|
||||
if (!inWails()) {
|
||||
triggerDownload(url, filename);
|
||||
return "";
|
||||
}
|
||||
return a.SaveReportAs(url, filename);
|
||||
return App.SaveReportAs(url, filename);
|
||||
}
|
||||
|
||||
// openReport:桌面端下载到临时目录并用系统默认应用打开;浏览器降级为新标签打开。
|
||||
export async function openReport(url: string, filename: string): Promise<void> {
|
||||
const a = app();
|
||||
if (!a) {
|
||||
if (!inWails()) {
|
||||
window.open(url, "_blank");
|
||||
return;
|
||||
}
|
||||
return a.OpenReport(url, filename);
|
||||
await App.OpenReport(url, filename);
|
||||
}
|
||||
|
||||
// notify:桌面端弹系统通知;浏览器为空操作(由应用内 Toast 兜底)。
|
||||
export function notify(title: string, body: string): void {
|
||||
app()?.Notify(title, body);
|
||||
if (inWails()) void App.Notify(title, body);
|
||||
}
|
||||
|
||||
// printReportHtml:把已渲染的报告 HTML 在打印视图里出 PDF(浏览器/Webview 的"打印→存为 PDF")。
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { Browser } from "@wailsio/runtime";
|
||||
import { isNewer, checkUpdate, openExternal, APP_VERSION } from "./version";
|
||||
|
||||
// mock v3 runtime 的 Browser.OpenURL(openExternal 桌面态走它)。
|
||||
vi.mock("@wailsio/runtime", () => ({ Browser: { OpenURL: vi.fn() } }));
|
||||
|
||||
describe("isNewer(语义化版本比较)", () => {
|
||||
it.each([
|
||||
["1.0.1", "1.0.0", true],
|
||||
@@ -60,20 +64,17 @@ describe("checkUpdate", () => {
|
||||
|
||||
describe("openExternal", () => {
|
||||
afterEach(() => {
|
||||
delete (window as unknown as { runtime?: unknown }).runtime;
|
||||
vi.restoreAllMocks();
|
||||
delete (window as unknown as { _wails?: unknown })._wails;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("有 Wails runtime → 走 BrowserOpenURL", () => {
|
||||
const spy = vi.fn();
|
||||
(window as unknown as { runtime: { BrowserOpenURL: (u: string) => void } }).runtime = {
|
||||
BrowserOpenURL: spy,
|
||||
};
|
||||
it("桌面态(_wails.environment 存在)→ 走 Browser.OpenURL", () => {
|
||||
(window as unknown as { _wails: { environment: { OS: string } } })._wails = { environment: { OS: "darwin" } };
|
||||
openExternal("https://a");
|
||||
expect(spy).toHaveBeenCalledWith("https://a");
|
||||
expect(Browser.OpenURL).toHaveBeenCalledWith("https://a");
|
||||
});
|
||||
|
||||
it("无 runtime(浏览器模式)→ 回退 window.open", () => {
|
||||
it("浏览器模式(无 _wails.environment)→ 回退 window.open", () => {
|
||||
const open = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
openExternal("https://b");
|
||||
expect(open).toHaveBeenCalledWith("https://b", "_blank");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// 桌面端版本与"检查更新"——查 GitHub Releases 最新版,比当前版本新则提示下载。
|
||||
// 发版流程:bump 此处 APP_VERSION(= package.json version)→ 打 tag vX.Y.Z → release workflow 自动构建+发布。
|
||||
import { Browser } from "@wailsio/runtime";
|
||||
|
||||
export const APP_VERSION = "0.1.0"; // 当前桌面端版本(与 git tag 去掉 v 前缀对齐)
|
||||
|
||||
@@ -27,11 +28,10 @@ export async function checkUpdate(): Promise<ReleaseInfo | null> {
|
||||
}
|
||||
}
|
||||
|
||||
// openExternal 在系统浏览器打开链接(Wails 用 runtime.BrowserOpenURL,浏览器模式回退 window.open)。
|
||||
// openExternal 在系统浏览器打开链接(Wails v3 用 Browser.OpenURL,浏览器模式回退 window.open)。
|
||||
export function openExternal(url: string): void {
|
||||
const w = window as unknown as { runtime?: { BrowserOpenURL?: (u: string) => void } };
|
||||
if (w.runtime?.BrowserOpenURL) {
|
||||
w.runtime.BrowserOpenURL(url);
|
||||
if ((window as unknown as { _wails?: { environment?: unknown } })._wails?.environment) {
|
||||
void Browser.OpenURL(url);
|
||||
} else {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
/// <reference types="vitest/config" />
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import wails from "@wailsio/runtime/plugins/vite";
|
||||
import path from "node:path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
// wails3 dev 传 WAILS_VITE_PORT(默认 9245);浏览器预览(纯 vite)回退 5173。
|
||||
server: {
|
||||
port: Number(process.env.WAILS_VITE_PORT) || 5173,
|
||||
strictPort: !!process.env.WAILS_VITE_PORT,
|
||||
},
|
||||
plugins: [react(), wails("./bindings")],
|
||||
resolve: {
|
||||
alias: { "@": path.resolve(__dirname, "./src") },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user