From 4d9d1ac6154c6d1d9e637c1740dcdd1a6a015c71 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 12 Jun 2026 17:04:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=E5=B7=A5=E4=B8=9A=E5=8C=96?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=20B=20=E2=80=94=E2=80=94=20=E7=9C=9F?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E9=9B=86=E6=88=90=EF=BC=88Wails=20Go=20?= =?UTF-8?q?=E6=A1=A5=20+=20=E5=8E=9F=E7=94=9F=E8=83=BD=E5=8A=9B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 让它真正像桌面 App 而非套壳网页:启用闲置的 Wails Go 桥,接原生文件框/系统打开/ 通知/无边框标题栏,且全部对浏览器预览(make web)优雅降级。 - app.go:SaveReportAs(原生"另存为"框 + 下载落盘)、OpenReport(下到临时目录 + 系统默认应用打开 docx)、Notify(macOS osascript 通知)、download/openInSystem 跨平台 - main.go:macOS TitleBarHiddenInset —— 隐藏标题栏、内容铺满到顶、保留红绿灯交通灯 - lib/desktop.ts:window.go.main.App 运行时桥 + isDesktop/isMacDesktop 探测; saveReportAs/openReport/notify 在无 Wails(浏览器)时分别降级为 /新标签/Toast - ReportView:桌面端「另存为 Word」(原生框) +「用系统打开」+ 完成弹系统通知; 浏览器端保持「下载 Word」 - KbView:拖拽文件入库(HTML5 dataTransfer,两种模式通用)+ 拖拽高亮 +「选择文件」按钮 - TopBar:顶栏设为 Wails 可拖拽区(--wails-draggable),控件标 no-drag; macOS 桌面端左留白让位交通灯 验证:GOWORK=off wails build 打出 .app(绑定生成 + mac 标题栏);启动真实原生窗口 截图确认无边框标题栏 + 交通灯内嵌 + 顶栏可拖拽(见会话截图);浏览器(Preview)确认 window.go 不存在时降级正确(下载链路 + 拖拽占位)。tsc + vite build 通过。 Co-Authored-By: Claude Opus 4.8 --- sundynix-desktop/app.go | 84 ++++++++++++++++++- sundynix-desktop/frontend/src/lib/desktop.ts | 57 +++++++++++++ .../frontend/src/shell/TopBar.tsx | 15 +++- .../frontend/src/views/KbView.tsx | 23 ++++- .../frontend/src/views/ReportView.tsx | 39 +++++++-- sundynix-desktop/main.go | 7 +- 6 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 sundynix-desktop/frontend/src/lib/desktop.ts diff --git a/sundynix-desktop/app.go b/sundynix-desktop/app.go index 966c1ff..f4ee277 100644 --- a/sundynix-desktop/app.go +++ b/sundynix-desktop/app.go @@ -2,10 +2,19 @@ package main import ( "context" + "fmt" + "io" + "net/http" "os" + "os/exec" + "path/filepath" + goruntime "runtime" + + wr "github.com/wailsapp/wails/v2/pkg/runtime" ) -// App 经 Wails 的 TS/Go 强绑定暴露给前端,承载本地文件 I/O 等只有桌面端能做的能力。 +// App 经 Wails 的 TS/Go 强绑定暴露给前端,承载只有桌面端能做的原生能力: +// 文件读写、系统"另存为"框、用系统默认应用打开、原生通知。 type App struct { ctx context.Context } @@ -15,6 +24,9 @@ func NewApp() *App { return &App{} } // startup 在 Wails 启动时注入运行时 ctx。 func (a *App) startup(ctx context.Context) { a.ctx = ctx } +// Ping 供前端探活 Go 桥是否就绪。 +func (a *App) Ping() string { return "sundynix-desktop ok" } + // ReadLocalFile 读取本地文件内容(本地文件系统 I/O)。 func (a *App) ReadLocalFile(path string) (string, error) { b, err := os.ReadFile(path) @@ -24,5 +36,71 @@ func (a *App) ReadLocalFile(path string) (string, error) { return string(b), nil } -// Ping 供前端探活 Go 桥是否就绪。 -func (a *App) Ping() string { return "sundynix-desktop ok" } +// SaveReportAs 弹原生"另存为"对话框,把 url 指向的报告(.docx)下载到用户选定路径。 +// 返回保存路径;用户取消则返回空串。 +func (a *App) SaveReportAs(url, filename string) (string, error) { + if filename == "" { + filename = "report.docx" + } + path, err := wr.SaveFileDialog(a.ctx, wr.SaveDialogOptions{ + Title: "保存报告", + DefaultFilename: filename, + Filters: []wr.FileFilter{{DisplayName: "Word 文档 (*.docx)", Pattern: "*.docx"}}, + }) + if err != nil || path == "" { + return "", err + } + if err := download(url, path); err != nil { + return "", err + } + return path, nil +} + +// OpenReport 把报告下载到临时目录,并用系统默认应用(Word/Pages/WPS)打开。 +func (a *App) OpenReport(url, filename string) error { + if filename == "" { + filename = "report.docx" + } + dst := filepath.Join(os.TempDir(), "sundynix-open-"+filename) + if err := download(url, dst); err != nil { + return err + } + return openInSystem(dst) +} + +// Notify 弹一条系统通知(best-effort:macOS 用 osascript,其它平台暂静默)。 +func (a *App) Notify(title, body string) { + if goruntime.GOOS == "darwin" { + script := fmt.Sprintf("display notification %q with title %q", body, title) + _ = exec.Command("osascript", "-e", script).Start() + } +} + +func download(url, dst string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + return fmt.Errorf("下载失败 HTTP %d", resp.StatusCode) + } + f, err := os.Create(dst) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + return err +} + +func openInSystem(path string) error { + switch goruntime.GOOS { + case "darwin": + return exec.Command("open", path).Start() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start() + default: + return exec.Command("xdg-open", path).Start() + } +} diff --git a/sundynix-desktop/frontend/src/lib/desktop.ts b/sundynix-desktop/frontend/src/lib/desktop.ts new file mode 100644 index 0000000..59b3e53 --- /dev/null +++ b/sundynix-desktop/frontend/src/lib/desktop.ts @@ -0,0 +1,57 @@ +// 桌面端原生能力桥 —— 经 Wails 注入的 window.go.main.App 调 Go 方法。 +// 关键:在浏览器预览(make web,无 Wails 运行时)下 window.go 不存在, +// 所有方法自动降级为纯 Web 行为,保证两种模式都能跑。 + +interface WailsApp { + SaveReportAs(url: string, filename: string): Promise; + OpenReport(url: string, filename: string): Promise; + Notify(title: string, body: string): Promise; +} + +function app(): WailsApp | null { + return ((window as unknown as { go?: { main?: { App?: WailsApp } } }).go?.main?.App as WailsApp) ?? null; +} + +// isDesktop 是否运行在真实 Wails 桌面窗口(而非浏览器预览)。 +export function isDesktop(): boolean { + return !!app(); +} + +// isMacDesktop 用于交通灯让位等 macOS 专属适配。 +export function isMacDesktop(): boolean { + return isDesktop() && /Mac/i.test(navigator.userAgent); +} + +// saveReportAs:桌面端弹原生"另存为"框落盘;浏览器降级为触发下载。返回保存路径(浏览器/取消为空)。 +export async function saveReportAs(url: string, filename: string): Promise { + const a = app(); + if (!a) { + triggerDownload(url, filename); + return ""; + } + return a.SaveReportAs(url, filename); +} + +// openReport:桌面端下载到临时目录并用系统默认应用打开;浏览器降级为新标签打开。 +export async function openReport(url: string, filename: string): Promise { + const a = app(); + if (!a) { + window.open(url, "_blank"); + return; + } + return a.OpenReport(url, filename); +} + +// notify:桌面端弹系统通知;浏览器为空操作(由应用内 Toast 兜底)。 +export function notify(title: string, body: string): void { + app()?.Notify(title, body); +} + +function triggerDownload(url: string, filename: string) { + const el = document.createElement("a"); + el.href = url; + el.download = filename; + document.body.appendChild(el); + el.click(); + el.remove(); +} diff --git a/sundynix-desktop/frontend/src/shell/TopBar.tsx b/sundynix-desktop/frontend/src/shell/TopBar.tsx index e4b08f9..5e06abf 100644 --- a/sundynix-desktop/frontend/src/shell/TopBar.tsx +++ b/sundynix-desktop/frontend/src/shell/TopBar.tsx @@ -1,8 +1,14 @@ +import type { CSSProperties } from "react"; import { User, ChevronDown } from "lucide-react"; import type { Identity } from "../lib/api"; import { useHealth } from "../lib/health"; +import { isMacDesktop } from "../lib/desktop"; import { cn } from "../ui"; +// Wails 拖拽区/控件 CSS 变量(浏览器忽略,桌面端生效)。 +const DRAG = { "--wails-draggable": "drag" } as CSSProperties; +const NODRAG = { "--wails-draggable": "no-drag" } as CSSProperties; + function Light({ on, label, unknown }: { on?: boolean; label: string; unknown?: boolean }) { const dot = unknown ? "bg-slate-600" : on ? "bg-success shadow-[0_0_8px_rgba(52,211,153,0.7)]" : "bg-danger"; return ( @@ -17,14 +23,17 @@ function Light({ on, label, unknown }: { on?: boolean; label: string; unknown?: export function TopBar({ identity, setIdentity }: { identity: Identity; setIdentity: (id: Identity) => void }) { const h = useHealth(); return ( -
+
S
sundynix-agentix
-
+
(null); const [searching, setSearching] = useState(false); const [graph, setGraph] = useState(null); + const [dragOver, setDragOver] = useState(false); const onGraph = async () => { try { @@ -118,7 +119,27 @@ export function KbView() { {/* 左:入库 + 实时监控 */}

入库

-