a1c35852ef
完成度审计确认全链零生产调用: - app.go OpenReport(仅被死包装引用)、ReadLocalFile(仅测试引用); desktop.ts openReport 包装、isDesktop 导出(实际用 isMacDesktop)——全删。 绑定重新生成(OpenReport/ReadLocalFile 归零,PrintReportPage/SaveReportAs/ Notify 保留)。删对应的 app_test TestReadLocalFile。 - admin Soon.tsx(规划中占位组件)已成未引用死组件——routes.tsx 9 条路由全 ready:true,import 未用。删组件+import。 openInSystem/filepath/os 仍被 PrintReportPage/download 使用,不孤立。 desktop go test + tsc + 68 vitest、admin tsc + 41 vitest 全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
3.6 KiB
Go
117 lines
3.6 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
goruntime "runtime"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/wailsapp/wails/v3/pkg/application"
|
||
)
|
||
|
||
// App 经 Wails v3 的 Service 绑定暴露给前端,承载只有桌面端能做的原生能力:
|
||
// 文件读写、系统"另存为"框、用系统默认应用打开、原生通知。
|
||
// v3 中不再需要注入 ctx——对话框经 application.Get().Dialog 获取。
|
||
type App struct{}
|
||
|
||
// 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 := application.Get().Dialog.SaveFile().
|
||
SetFilename(filename).
|
||
AddFilter("Word 文档 (*.docx)", "*.docx").
|
||
PromptForSingleSelection()
|
||
if err != nil || path == "" {
|
||
return "", err
|
||
}
|
||
if err := download(url, path); err != nil {
|
||
return "", err
|
||
}
|
||
return path, nil
|
||
}
|
||
|
||
// PrintReportPage 把报告打印视图 HTML 落到临时文件,交系统默认浏览器打开(在那里 ⌘P →
|
||
// 存储为 PDF)。Wails 的 WKWebView 会把 window.open 拦成 null,前端弹打印窗那条路在壳内
|
||
// 走不通;转交系统浏览器后「前端打印出 PDF、CJK 零字体依赖」的原有优势不变。返回落盘路径。
|
||
func (a *App) PrintReportPage(filename, html string) (string, error) {
|
||
if filename == "" {
|
||
filename = "report"
|
||
}
|
||
// 文件名进过滤:主题可能含 / 之类的路径字符。
|
||
safe := strings.Map(func(r rune) rune {
|
||
if strings.ContainsRune(`/\:*?"<>|`, r) {
|
||
return '_'
|
||
}
|
||
return r
|
||
}, filename)
|
||
dst := filepath.Join(os.TempDir(), "sundynix-print-"+safe+".html")
|
||
if err := os.WriteFile(dst, []byte(html), 0o600); err != nil {
|
||
return "", err
|
||
}
|
||
return dst, 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()
|
||
}
|
||
}
|
||
|
||
// downloadClient 给下载加超时:默认 http.Get 用的 client 没有超时,
|
||
// 上游卡住的话「另存为」会永远转下去,用户只能强杀 app。
|
||
var downloadClient = &http.Client{Timeout: 3 * time.Minute}
|
||
|
||
// download 把 url 下载到 dst。失败一律不留残file:
|
||
// 中途断开会在用户选定的路径上留个半截 .docx,带着用户起的名字,以后双击打不开。
|
||
func download(url, dst string) (err error) {
|
||
resp, err := downloadClient.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 func() {
|
||
// Close 的错误不能吞:写文件时 io.Copy 成功不代表数据落了盘,
|
||
// flush 失败只会在 Close 上报出来——吞掉就是静默截断。
|
||
cerr := f.Close()
|
||
if err == nil {
|
||
err = cerr
|
||
}
|
||
if err != nil {
|
||
_ = os.Remove(dst) // 失败不留残file
|
||
}
|
||
}()
|
||
_, 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()
|
||
}
|
||
}
|