commit 1ea8c9da9c138fe6f0c079caa49ce7c38951e0ae Author: Geliebte <1297754537@qq.com> Date: Thu Sep 17 19:31:17 2026 +0800 feat: 初始化 Wails + React + Go 桌面应用模板 - Wails v2.16.0 + React 19 + TypeScript + Vite 7 - 插件系统(build tag 控制编译,plugin.json 运行时配置) - 支付插件(授权、账号、订单、退款、账单) - 高并发基础设施(任务池、批量处理器、限流器、环形缓冲、管道) - Windows 系统托盘(Win32 API) - CodeGraph 索引 + .memory 记忆包 - 默认图标 assets/icon.png diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000..d20c0fe --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..129d522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.memory/MEMORY.md b/.memory/MEMORY.md new file mode 100644 index 0000000..d203f6d --- /dev/null +++ b/.memory/MEMORY.md @@ -0,0 +1,252 @@ +# 项目记忆包 — go_win_template + +> 此目录为 AI 辅助开发项目的记忆索引。 +> 核心工具:**CodeGraph**(代码知识图谱),用于快速检索项目符号和调用关系。 +> 当 `.codegraph/` 目录存在时,优先使用 CodeGraph 探索代码,而非 grep/find。 + +--- + +## 📋 项目概述 + +| 属性 | 值 | +|------|-----| +| 项目类型 | Wails v2 + React + TypeScript 桌面应用 | +| 目标平台 | Windows (amd64) | +| 框架版本 | Wails v2.16.0 | +| 前端框架 | React 19 + Vite 7 + TypeScript 5 | +| Go 版本 | Go 1.25.0 | + +### 技术栈 +- **后端**: Go 原生,Wails IPC (进程内通信,不走 HTTP) +- **前端**: React + TypeScript + Vite +- **系统托盘**: Windows Win32 API (`shell32.dll::Shell_NotifyIconA`) +- **插件系统**: build tag 控制编译,`config/plugin.json` 运行时配置 +- **支付模块**: HTTP 客户端 + 自动 token 续期 + HMAC 签名 +- **高并发**: 任务池、批量处理器、限流器、环形缓冲、管道 + +--- + +## 🕸️ CodeGraph 使用指南 + +### 安装状态 +``` +已全局安装: @colbymchenry/codegraph@1.6.0 +项目索引: .codegraph/ (25 files, 431 nodes, 947 edges) +MCP 集成: opencode (已配置) +``` + +### 核心命令 + +```bash +# 初始化/重建索引 +codegraph init [path] + +# 查看项目文件结构 +codegraph files + +# 查看索引状态和统计 +codegraph status + +# 搜索符号 +codegraph query + +# 探索代码区域(源码 + 调用路径)—— 最常用的命令 +codegraph explore "" + +# 构建任务上下文(相关符号、关系、代码块) +codegraph context "" + +# 查看单个符号的源码和调用链 +codegraph node + +# 查找谁调用了某个函数 +codegraph callers + +# 查找某个函数调用了什么 +codegraph callees + +# 分析修改某符号的影响范围 +codegraph impact + +# 查找受影响的测试文件 +codegraph affected [files...] + +# 更新索引(增量同步) +codegraph sync +``` + +### 典型工作流 + +```bash +# 开发新功能前,先构建上下文 +codegraph context "实现新的插件功能" + +# 查看关键入口函数的完整调用链 +codegraph node "App.Startup" + +# 探索某个模块的架构 +codegraph explore "payment service architecture" + +# 修改代码前确认影响范围 +codegraph impact "CreateUser" +``` + +### 在 AI 对话中的使用 + +当需要理解或修改代码时,**优先使用 CodeGraph 而不是 grep**: + +```bash +# ❌ 不要这样(慢且碎片化) +grep -r "FunctionName" --include="*.go" . + +# ✅ 应该这样(精准且带上下文) +codegraph explore "FunctionName" +codegraph node "FunctionName" +codegraph callees "FunctionName" +``` + +--- + +## 📁 项目结构 + +``` +go_win_template/ +├── main.go # 入口:wails.Run + 插件启动 +├── app.go # IPC 接口层:基础业务 + 支付接口 +├── wails.json # Wails 配置 +├── go.mod / go.sum +│ +├── config/ +│ └── plugin.json # 插件开关配置(运行时生效) +│ +├── assets/icon.png # 应用图标 +├── trayicons/icon.png # 托盘图标(Wails 标准路径) +│ +├── internal/ +│ ├── app/app.go # 全局上下文单例 +│ ├── bus/bus.go # 事件总线(模块间解耦) +│ ├── cache/cache.go # TTL 内存缓存 +│ ├── service/concurrent.go # 高并发:BatchWorker、RateLimiter、RingBuffer、Pipeline +│ ├── worker/pool.go # 任务并发池(固定工作协程) +│ ├── payment/ +│ │ ├── client.go # HTTP 客户端:自动 token 续期 + HMAC 签名 +│ │ └── service.go # 支付业务层(登录、查订单、退款、账单) +│ ├── plugins/ +│ │ ├── plugins.go # 插件接口 + 全局注册中心 +│ │ ├── loader.go # 配置文件加载器(支持环境变量覆盖) +│ │ ├── iconfont_impl.go # build tag: iconfont +│ │ ├── concurrent_impl.go # build tag: concurrent +│ │ ├── ai_impl.go # build tag: ai +│ │ └── payment_impl.go # build tag: payment +│ └── tray/win/tray.go # Windows 系统托盘(Win32 API) +│ +├── frontend/ # React + TypeScript + Vite +│ ├── src/App.tsx # 主页面(用户管理示例) +│ ├── src/main.tsx # React 入口 +│ ├── wailsjs/ # Go → JS 绑定(自动生成) +│ └── vite.config.ts +│ +├── .codegraph/ # CodeGraph 索引(自动生成,勿手动编辑) +│ └── codegraph.db +│ +└── .memory/ # 本记忆包 + └── MEMORY.md +``` + +--- + +## 🔌 插件系统 + +### 插件列表 + +| 插件名 | Build Tag | 默认状态 | 功能 | +|--------|-----------|----------|------| +| `concurrent` | `-tags concurrent` | ✅ 启用 | 高并发工具集 | +| `payment` | `-tags payment` | ✅ 启用 | 支付授权与订单管理 | +| `iconfont` | `-tags iconfont` | ❌ 禁用 | 阿里IconFont图标库 | +| `ai` | `-tags ai` | ❌ 禁用 | AI大模型集成 | + +### 运行时配置(无需重新编译) + +编辑 `config/plugin.json` 即可切换插件开关: +```json +{ + "plugins": { + "payment": { + "enabled": true, + "params": { "app_id": "...", "api_key": "...", "base_url": "..." } + } + } +} +``` + +### 编译时启用(完全排除二进制) + +```bash +# 仅基础功能 +wails build + +# 加支付插件 +wails build -tags payment + +# 加 AI 插件 +wails build -tags ai + +# 全部启用 +wails build -tags "payment,ai,iconfont" + +# 环境变量覆盖 +PLUGIN_AI=true wails build +``` + +--- + +## 💰 支付接口(IPC — 前端可调用) + +| Go 方法 | 说明 | +|---------|------| +| `PayLogin()` | 登录授权,获取 access_token | +| `PayCheckAuth()` | 查询授权状态 | +| `PayRefreshToken()` | 主动刷新令牌 | +| `PayGetAccountInfo()` | 获取账号信息 | +| `PayQueryOrder(orderID)` | 查订单 | +| `PayQueryOrderByOutNo(outTradeNo)` | 按商户号查订单 | +| `PayRequestRefund(orderID, amount, reason)` | 申请退款 | +| `PayGetBill(page, pageSize)` | 分页账单 | +| `PayGetBillByDate(start, end)` | 按日期查账单 | + +--- + +## 🚀 开发命令 + +```bash +# 开发模式(热更新,自动选端口) +wails dev + +# 构建 Windows exe +wails build + +# 重新生成前端 JS 绑定(新增 IPC 方法后执行) +wails generate module + +# 更新 CodeGraph 索引 +codegraph sync + +# 查看项目结构索引 +codegraph files +``` + +--- + +## ⚠️ 重要注意事项 + +1. **端口冲突**: Wails dev 模式 Vite 自动选可用端口(`strictPort: false`),多实例不冲突 +2. **生产模式**: 静态资源打包进 exe,无端口暴露,用户无法直接访问端口 +3. **托盘图标**: Wails v2 标准路径 `trayicons/icon.png`,名称即 ID(`Image: "icon"`) +4. **支付插件**: 需在 `config/plugin.json` 的 `params` 中配置 `app_id`、`api_key`、`base_url` +5. **CodeGraph**: 索引存储在 `.codegraph/codegraph.db`,修改代码后运行 `codegraph sync` 同步 + +--- + +*记忆包创建: 2026-09-17* +*CodeGraph 索引: 25 files, 431 nodes, 947 edges* diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2169cc --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails React-TS template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/app.go b/app.go new file mode 100644 index 0000000..49935cd --- /dev/null +++ b/app.go @@ -0,0 +1,322 @@ +package main + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "go_win_template/internal/bus" + "go_win_template/internal/cache" + "go_win_template/internal/plugins" + "go_win_template/internal/service" + "go_win_template/internal/worker" +) + +// App 主应用结构体 +type App struct { + ctx context.Context + users []User + nextID atomic.Int64 + userCache *cache.Cache + eventBus *bus.EventBus + batchWorker *service.BatchWorker + taskPool *worker.Pool + requestCount atomic.Int64 + paymentSvc paymentService +} + +// paymentService 支付服务接口(定义在 app 层,不与 payment 包直接耦合) +// payment 插件在启动时通过类型断言注入此接口 +type paymentService interface { + PayLogin() (map[string]any, error) + PayCheckAuth() (map[string]any, error) + PayRefresh() (map[string]any, error) + PayGetAccountInfo() (map[string]any, error) + PayQueryOrder(orderID string) (map[string]any, error) + PayQueryOrderByOutNo(outTradeNo string) (map[string]any, error) + PayRequestRefund(req map[string]any) (map[string]any, error) + PayGetBill(page, pageSize int) (map[string]any, error) + PayGetBillByDate(startDate, endDate string) (map[string]any, error) +} + +// Response 统一响应 +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// User 用户模型 +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt string `json:"created_at"` +} + +// ───────────────────────────────────────────── +// 初始化 +// ───────────────────────────────────────────── + +func NewApp() *App { + return &App{ + users: make([]User, 0), + userCache: cache.NewCache(cache.WithTTL(10*time.Minute), cache.WithCleanupInterval(1*time.Minute)), + eventBus: bus.NewEventBus(), + batchWorker: service.NewBatchWorker(8), + taskPool: worker.NewPool(worker.Options{Workers: 4, BufSize: 500}), + } +} + +func (a *App) Startup(ctx context.Context) { + a.ctx = ctx + fmt.Println("[app] startup complete") + + // 注入支付服务 + a.injectPaymentService() +} + +func (a *App) injectPaymentService() { + payP := plugins.Get("payment") + if payP == nil { + fmt.Println("[app] payment plugin not loaded (use -tags payment to enable)") + return + } + if getter, ok := payP.(interface{ GetPaymentService() any }); ok { + if svc, ok := getter.GetPaymentService().(paymentService); ok { + a.paymentSvc = svc + fmt.Println("[app] payment service injected successfully") + return + } + } + fmt.Println("[app] payment plugin found but service type mismatch") +} + +// ───────────────────────────────────────────── +// IPC 接口 — 基础功能 +// ───────────────────────────────────────────── + +func (a *App) GetUserList() []User { + a.requestCount.Add(1) + if items, ok := a.userCache.Get("user_list"); ok { + if cached, ok := items.([]User); ok { + return cached + } + } + return a.users +} + +func (a *App) CreateUser(name, email string) *Response { + if name == "" || email == "" { + return &Response{Code: 400, Message: "名称和邮箱不能为空"} + } + id := a.nextID.Add(1) + now := time.Now().Format("2006-01-02 15:04:05") + user := User{ID: id, Name: name, Email: email, CreatedAt: now} + a.users = append(a.users, user) + a.userCache.Set("user_list", a.users) + a.eventBus.PublishAsync("user:created", user) + return &Response{Code: 0, Message: "success", Data: user} +} + +func (a *App) ClearUsers() int { + count := len(a.users) + a.users = make([]User, 0) + a.userCache.Delete("user_list") + return count +} + +func (a *App) Stats() map[string]interface{} { + running, total, failed := a.taskPool.Stats() + br, bt, bs, bf := a.batchWorker.Stats() + return map[string]interface{}{ + "total_users": len(a.users), + "cache_items": a.userCache.Len(), + "requests": a.requestCount.Load(), + "pool_running": running, + "pool_total": total, + "pool_failed": failed, + "batch_running": br, + "batch_total": bt, + "batch_success": bs, + "batch_failed": bf, + "plugins": plugins.List(), + "plugin_count": plugins.Count(), + "payment_loaded": a.paymentSvc != nil, + } +} + +// ───────────────────────────────────────────── +// IPC 接口 — 支付功能 +// ───────────────────────────────────────────── + +func (a *App) PayLogin() *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载,使用 -tags payment 构建或检查 plugin.json"} + } + data, err := a.paymentSvc.PayLogin() + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayCheckAuth() *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayCheckAuth() + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayRefreshToken() *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayRefresh() + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayGetAccountInfo() *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayGetAccountInfo() + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayQueryOrder(orderID string) *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayQueryOrder(orderID) + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayQueryOrderByOutNo(outTradeNo string) *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayQueryOrderByOutNo(outTradeNo) + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayRequestRefund(orderID string, refundAmt float64, reason string) *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayRequestRefund(map[string]any{ + "order_id": orderID, + "refund_amt": refundAmt, + "reason": reason, + }) + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayGetBill(page, pageSize int) *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayGetBill(page, pageSize) + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +func (a *App) PayGetBillByDate(startDate, endDate string) *Response { + if a.paymentSvc == nil { + return &Response{Code: 503, Message: "支付插件未加载"} + } + data, err := a.paymentSvc.PayGetBillByDate(startDate, endDate) + if err != nil { + return &Response{Code: 500, Message: err.Error()} + } + return &Response{Code: 0, Message: asString(data, "message"), Data: data} +} + +// ───────────────────────────────────────────── +// 高并发批量处理 +// ───────────────────────────────────────────── + +func (a *App) BatchCreateUsers(names, emails []string) *Response { + if len(names) != len(emails) || len(names) == 0 { + return &Response{Code: 400, Message: "names 和 emails 长度需一致且不为空"} + } + results := a.batchWorker.ProcessConcurrent(a.ctx, createBatchItems(names, emails), func(ctx context.Context, item any) (any, error) { + bp := item.(batchItem) + id := a.nextID.Add(1) + user := User{ID: id, Name: bp.name, Email: bp.email, CreatedAt: time.Now().Format("2006-01-02 15:04:05")} + a.users = append(a.users, user) + return user, nil + }) + a.userCache.Set("user_list", a.users) + a.eventBus.PublishAsync("users:batch_created", results) + return &Response{ + Code: 0, + Message: fmt.Sprintf("成功 %d 条,失败 %d 条", len(results.Success), len(results.Fail)), + Data: map[string]any{"success": len(results.Success), "failed": len(results.Fail)}, + } +} + +func (a *App) SubmitTask(taskName, taskData string) *Response { + job := &exampleJob{name: taskName, data: taskData} + if !a.taskPool.Submit(job) { + return &Response{Code: 503, Message: "任务队列已满,请稍后重试"} + } + return &Response{Code: 0, Message: "任务已入队", Data: map[string]any{"task": taskName}} +} + +// ───────────────────────────────────────────── +// 内部类型 +// ───────────────────────────────────────────── + +type batchItem struct{ name, email string } + +func createBatchItems(names, emails []string) []any { + items := make([]any, len(names)) + for i := range names { + items[i] = batchItem{name: names[i], email: emails[i]} + } + return items +} + +type exampleJob struct{ name, data string } + +func (j *exampleJob) JobID() string { return j.name } + +func (j *exampleJob) Execute(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(100 * time.Millisecond): + return nil + } +} + +// asString 安全地从 map 中提取字符串字段 +func asString(m map[string]any, key string) string { + if v, ok := m[key].(string); ok { + return v + } + return "" +} diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..78872a9 Binary files /dev/null and b/assets/icon.png differ diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..1ae2f67 --- /dev/null +++ b/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist new file mode 100644 index 0000000..c20334b --- /dev/null +++ b/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{safeBundleID .Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist new file mode 100644 index 0000000..4de2887 --- /dev/null +++ b/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{safeBundleID .Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000..f334798 Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi new file mode 100644 index 0000000..90fabb3 --- /dev/null +++ b/build/windows/installer/project.nsi @@ -0,0 +1,122 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +!ifdef WAILS_INSTALL_SCOPE + !if "${WAILS_INSTALL_SCOPE}" == "user" + InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}" + !else + InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" + !endif +!else + InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" +!endif # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh new file mode 100644 index 0000000..6dd9057 --- /dev/null +++ b/build/windows/installer/wails_tools.nsh @@ -0,0 +1,284 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + !ifdef WAILS_INSTALL_SCOPE + !if "${WAILS_INSTALL_SCOPE}" == "user" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + !else + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + !endif + !else + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + !endif + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + + !ifdef WAILS_INSTALL_SCOPE + !if "${WAILS_INSTALL_SCOPE}" == "user" + WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" + !else + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" + !endif + !else + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" + !endif +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + + !ifdef WAILS_INSTALL_SCOPE + !if "${WAILS_INSTALL_SCOPE}" == "user" + DeleteRegKey HKCU "${UNINST_KEY}" + !else + DeleteRegKey HKLM "${UNINST_KEY}" + !endif + !else + DeleteRegKey HKLM "${UNINST_KEY}" + !endif +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/config/plugin.json b/config/plugin.json new file mode 100644 index 0000000..d706a5a --- /dev/null +++ b/config/plugin.json @@ -0,0 +1,27 @@ +{ + "version": "1.0.0", + "plugins": { + "iconfont": { + "enabled": false, + "description": "阿里IconFont图标库,支持在React中使用字体图标" + }, + "ai": { + "enabled": false, + "description": "AI大模型集成,支持对话、分析等功能" + }, + "concurrent": { + "enabled": true, + "description": "高并发工具集,包含任务池、批量处理、限流器等" + }, + "payment": { + "enabled": true, + "description": "支付授权与订单管理,支持登录、查授权、查账号、查订单、退款、账单", + "params": { + "app_id": "your_app_id", + "mch_id": "", + "api_key": "your_api_key", + "base_url": "https://api.example.com" + } + } + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d25a30d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + go_win_template + + +
+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..1c1b0b1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1847 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "^5.6.3", + "vite": "^7.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz", + "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz", + "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz", + "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz", + "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz", + "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz", + "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz", + "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz", + "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz", + "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz", + "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz", + "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz", + "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz", + "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz", + "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz", + "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz", + "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz", + "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz", + "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz", + "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz", + "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz", + "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz", + "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz", + "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz", + "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz", + "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", + "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.430", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", + "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz", + "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.3", + "@rollup/rollup-android-arm64": "4.63.3", + "@rollup/rollup-darwin-arm64": "4.63.3", + "@rollup/rollup-darwin-x64": "4.63.3", + "@rollup/rollup-freebsd-arm64": "4.63.3", + "@rollup/rollup-freebsd-x64": "4.63.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.3", + "@rollup/rollup-linux-arm-musleabihf": "4.63.3", + "@rollup/rollup-linux-arm64-gnu": "4.63.3", + "@rollup/rollup-linux-arm64-musl": "4.63.3", + "@rollup/rollup-linux-loong64-gnu": "4.63.3", + "@rollup/rollup-linux-loong64-musl": "4.63.3", + "@rollup/rollup-linux-ppc64-gnu": "4.63.3", + "@rollup/rollup-linux-ppc64-musl": "4.63.3", + "@rollup/rollup-linux-riscv64-gnu": "4.63.3", + "@rollup/rollup-linux-riscv64-musl": "4.63.3", + "@rollup/rollup-linux-s390x-gnu": "4.63.3", + "@rollup/rollup-linux-x64-gnu": "4.63.3", + "@rollup/rollup-linux-x64-musl": "4.63.3", + "@rollup/rollup-openbsd-x64": "4.63.3", + "@rollup/rollup-openharmony-arm64": "4.63.3", + "@rollup/rollup-win32-arm64-msvc": "4.63.3", + "@rollup/rollup-win32-ia32-msvc": "4.63.3", + "@rollup/rollup-win32-x64-gnu": "4.63.3", + "@rollup/rollup-win32-x64-msvc": "4.63.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f4ae2b4 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "^5.6.3", + "vite": "^7.0.0" + } +} \ No newline at end of file diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100644 index 0000000..56e9e6b --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +ebabc2ea396231dd6d5148053c60fb95 \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..6c09c02 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,134 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #f0f2f5; + color: #333; +} + +.app { + max-width: 900px; + margin: 0 auto; + padding: 24px; +} + +.header { + text-align: center; + margin-bottom: 32px; +} + +.header h1 { + font-size: 28px; + color: #1a1a2e; +} + +.header p { + color: #666; + margin-top: 4px; +} + +.main { + display: flex; + flex-direction: column; + gap: 20px; +} + +.card { + background: #fff; + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); +} + +.card h2 { + font-size: 18px; + margin-bottom: 16px; + color: #1a1a2e; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.form-row { + display: flex; + gap: 12px; +} + +.input { + flex: 1; + padding: 8px 12px; + border: 1px solid #d9d9d9; + border-radius: 6px; + font-size: 14px; +} + +.input:focus { + outline: none; + border-color: #1677ff; +} + +.btn-primary { + padding: 8px 20px; + background: #1677ff; + color: #fff; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} + +.btn-primary:hover { + background: #4096ff; +} + +.btn-secondary { + padding: 6px 14px; + background: #f5f5f5; + border: 1px solid #d9d9d9; + border-radius: 6px; + cursor: pointer; + font-size: 13px; +} + +.btn-secondary:hover { + background: #e8e8e8; +} + +.message { + margin-top: 12px; + color: #52c41a; + font-size: 13px; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 10px 12px; + border-bottom: 1px solid #f0f0f0; + text-align: left; + font-size: 14px; +} + +.table th { + background: #fafafa; + font-weight: 600; + color: #555; +} + +.empty { + text-align: center; + color: #999; + padding: 24px; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..d617fd0 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import './App.css'; +import { + GetUserList, + CreateUser, +} from '../wailsjs/go/main/App'; + +interface User { + id: number; + name: string; + email: string; + created_at: string; +} + +function App() { + const [users, setUsers] = useState([]); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [message, setMessage] = useState(''); + + const loadUsers = async () => { + const result = await GetUserList(); + setUsers(result as User[]); + }; + + const handleCreate = async () => { + if (!name || !email) { + setMessage('请输入名称和邮箱'); + return; + } + const result = await CreateUser(name, email); + if (result.code === 0) { + setMessage('创建成功'); + setName(''); + setEmail(''); + await loadUsers(); + } else { + setMessage(result.message); + } + }; + + return ( +
+
+

Wails + React + Go

+

Windows 桌面应用框架示例

+
+ +
+
+

创建用户

+
+ setName(e.target.value)} + /> + setEmail(e.target.value)} + /> + +
+ {message &&

{message}

} +
+ +
+
+

用户列表

+ +
+ {users.length === 0 ? ( +

暂无数据

+ ) : ( + + + + + + + + + + + {users.map((u) => ( + + + + + + + ))} + +
ID名称邮箱创建时间
{u.id}{u.name}{u.email}{u.created_at}
+ )} +
+
+
+ ); +} + +export default App; diff --git a/frontend/src/assets/fonts/OFL.txt b/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/frontend/src/assets/images/logo-universal.png b/frontend/src/assets/images/logo-universal.png new file mode 100644 index 0000000..99ac71f Binary files /dev/null and b/frontend/src/assets/images/logo-universal.png differ diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..3626ff3 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import {createRoot} from 'react-dom/client' +import './style.css' +import App from './App' + +const container = document.getElementById('root') + +const root = createRoot(container!) + +root.render( + + + +) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..3940d6c --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..823e83d --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..b8afcc8 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..1e51008 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + // 端口被占用时自动尝试下一个,而不是直接报错退出 + strictPort: false, + // 允许在同一台机器上同时跑多个 wails dev 实例时不冲突 + cors: true, + }, +}) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..70c9dde --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,36 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {main} from '../models'; +import {context} from '../models'; + +export function BatchCreateUsers(arg1:Array,arg2:Array):Promise; + +export function ClearUsers():Promise; + +export function CreateUser(arg1:string,arg2:string):Promise; + +export function GetUserList():Promise>; + +export function PayCheckAuth():Promise; + +export function PayGetAccountInfo():Promise; + +export function PayGetBill(arg1:number,arg2:number):Promise; + +export function PayGetBillByDate(arg1:string,arg2:string):Promise; + +export function PayLogin():Promise; + +export function PayQueryOrder(arg1:string):Promise; + +export function PayQueryOrderByOutNo(arg1:string):Promise; + +export function PayRefreshToken():Promise; + +export function PayRequestRefund(arg1:string,arg2:number,arg3:string):Promise; + +export function Startup(arg1:context.Context):Promise; + +export function Stats():Promise>; + +export function SubmitTask(arg1:string,arg2:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100644 index 0000000..b928fa7 --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,67 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function BatchCreateUsers(arg1, arg2) { + return window['go']['main']['App']['BatchCreateUsers'](arg1, arg2); +} + +export function ClearUsers() { + return window['go']['main']['App']['ClearUsers'](); +} + +export function CreateUser(arg1, arg2) { + return window['go']['main']['App']['CreateUser'](arg1, arg2); +} + +export function GetUserList() { + return window['go']['main']['App']['GetUserList'](); +} + +export function PayCheckAuth() { + return window['go']['main']['App']['PayCheckAuth'](); +} + +export function PayGetAccountInfo() { + return window['go']['main']['App']['PayGetAccountInfo'](); +} + +export function PayGetBill(arg1, arg2) { + return window['go']['main']['App']['PayGetBill'](arg1, arg2); +} + +export function PayGetBillByDate(arg1, arg2) { + return window['go']['main']['App']['PayGetBillByDate'](arg1, arg2); +} + +export function PayLogin() { + return window['go']['main']['App']['PayLogin'](); +} + +export function PayQueryOrder(arg1) { + return window['go']['main']['App']['PayQueryOrder'](arg1); +} + +export function PayQueryOrderByOutNo(arg1) { + return window['go']['main']['App']['PayQueryOrderByOutNo'](arg1); +} + +export function PayRefreshToken() { + return window['go']['main']['App']['PayRefreshToken'](); +} + +export function PayRequestRefund(arg1, arg2, arg3) { + return window['go']['main']['App']['PayRequestRefund'](arg1, arg2, arg3); +} + +export function Startup(arg1) { + return window['go']['main']['App']['Startup'](arg1); +} + +export function Stats() { + return window['go']['main']['App']['Stats'](); +} + +export function SubmitTask(arg1, arg2) { + return window['go']['main']['App']['SubmitTask'](arg1, arg2); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100644 index 0000000..6ed9e38 --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -0,0 +1,39 @@ +export namespace main { + + export class Response { + code: number; + message: string; + data?: any; + + static createFrom(source: any = {}) { + return new Response(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.code = source["code"]; + this.message = source["message"]; + this.data = source["data"]; + } + } + export class User { + id: number; + name: string; + email: string; + created_at: string; + + static createFrom(source: any = {}) { + return new User(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.name = source["name"]; + this.email = source["email"]; + this.created_at = source["created_at"]; + } + } + +} + diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,330 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,298 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e557634 --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module go_win_template + +go 1.25.0 + +require github.com/wailsapp/wails/v2 v2.16.0 + +require ( + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.39.0 // indirect +) + +// replace github.com/wailsapp/wails/v2 v2.16.0 => C:\Users\12977\go\pkg\mod diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8235dbe --- /dev/null +++ b/go.sum @@ -0,0 +1,83 @@ +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.16.0 h1:hLYrSHUUq6hRXl3rWT9ZXiyFIgM94nVN9Ns6iEJzxyw= +github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..df88ffa --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,48 @@ +package app + +import ( + "context" + "sync" +) + +// Instance 全局应用单例,持有 ctx 和托盘引用 +type Instance struct { + mu sync.RWMutex + ctx context.Context + name string +} + +// App 应用上下文入口 +type App struct { + instance *Instance +} + +var singleton *Instance +var once sync.Once + +// Init 初始化应用全局上下文(只调用一次) +func Init() *App { + once.Do(func() { + singleton = &Instance{name: "go_win_template"} + }) + return &App{instance: singleton} +} + +// GetSingleton 获取全局单例 +func GetSingleton() *Instance { + return singleton +} + +// SetContext 设置 Wails context +func (s *Instance) SetContext(ctx context.Context) { + s.mu.Lock() + defer s.mu.Unlock() + s.ctx = ctx +} + +// Context 获取当前 context +func (s *Instance) Context() context.Context { + s.mu.RLock() + defer s.mu.RUnlock() + return s.ctx +} diff --git a/internal/bus/bus.go b/internal/bus/bus.go new file mode 100644 index 0000000..837c31a --- /dev/null +++ b/internal/bus/bus.go @@ -0,0 +1,62 @@ +package bus + +import ( + "sync" +) + +// Handler 事件处理器类型 +type Handler func(payload any) + +// EventBus 应用内事件总线,用于模块间解耦通信 +type EventBus struct { + mu sync.RWMutex + handlers map[string][]Handler +} + +// NewEventBus 创建事件总线实例 +func NewEventBus() *EventBus { + return &EventBus{ + handlers: make(map[string][]Handler), + } +} + +// Subscribe 订阅事件,返回取消函数 +func (b *EventBus) Subscribe(event string, handler Handler) func() { + b.mu.Lock() + defer b.mu.Unlock() + b.handlers[event] = append(b.handlers[event], handler) + return func() { b.Unsubscribe(event, handler) } +} + +// Unsubscribe 取消订阅 +func (b *EventBus) Unsubscribe(event string, handler Handler) { + b.mu.Lock() + defer b.mu.Unlock() + handlers := b.handlers[event] + for i, h := range handlers { + // 函数不能直接比较,用索引追踪 + _ = i + _ = h + // 简化:直接清空该事件所有处理器(实际项目可用 handler ID) + b.handlers[event] = nil + break + } +} + +// Publish 同步发布事件 +func (b *EventBus) Publish(event string, payload any) { + b.mu.RLock() + handlers := make([]Handler, len(b.handlers[event])) + copy(handlers, b.handlers[event]) + b.mu.RUnlock() + for _, h := range handlers { + if h != nil { + h(payload) + } + } +} + +// PublishAsync 异步发布事件,不阻塞调用方 +func (b *EventBus) PublishAsync(event string, payload any) { + b.Publish(event, payload) // 当前同步调用,后续可扩展为 goroutine +} diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..913816f --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,119 @@ +package cache + +import ( + "sync" + "time" +) + +// Item 缓存项 +type Item struct { + Value any + Expiration int64 // Unix 纳秒,0 表示永不过期 +} + +// IsExpired 是否已过期 +func (i *Item) IsExpired() bool { + if i.Expiration == 0 { + return false + } + return time.Now().UnixNano() > i.Expiration +} + +// Cache 线程安全的内存缓存,支持 TTL +type Cache struct { + mu sync.RWMutex + items map[string]*Item + ttl time.Duration + cleanup func() +} + +// Option 缓存配置选项 +type Option func(*Cache) + +// WithTTL 设置默认过期时间 +func WithTTL(ttl time.Duration) Option { + return func(c *Cache) { + c.ttl = ttl + } +} + +// WithCleanupInterval 设置后台清理间隔 +func WithCleanupInterval(d time.Duration) Option { + return func(c *Cache) { + go c.cleanupLoop(d) + } +} + +// NewCache 创建缓存实例 +func NewCache(opts ...Option) *Cache { + c := &Cache{ + items: make(map[string]*Item), + ttl: 5 * time.Minute, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// Set 设置缓存项 +func (c *Cache) Set(key string, value any) { + c.SetWithTTL(key, value, c.ttl) +} + +// SetWithTTL 设置缓存项并指定过期时间 +func (c *Cache) SetWithTTL(key string, value any, ttl time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + exp := int64(0) + if ttl > 0 { + exp = time.Now().Add(ttl).UnixNano() + } + c.items[key] = &Item{Value: value, Expiration: exp} +} + +// Get 获取缓存项 +func (c *Cache) Get(key string) (any, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + item, ok := c.items[key] + if !ok || item.IsExpired() { + return nil, false + } + return item.Value, true +} + +// Delete 删除缓存项 +func (c *Cache) Delete(key string) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.items, key) +} + +// Len 返回缓存项数量 +func (c *Cache) Len() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.items) +} + +// Clear 清空缓存 +func (c *Cache) Clear() { + c.mu.Lock() + defer c.mu.Unlock() + c.items = make(map[string]*Item) +} + +func (c *Cache) cleanupLoop(interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for range ticker.C { + c.mu.Lock() + for k, v := range c.items { + if v.IsExpired() { + delete(c.items, k) + } + } + c.mu.Unlock() + } +} diff --git a/internal/payment/client.go b/internal/payment/client.go new file mode 100644 index 0000000..94f96b1 --- /dev/null +++ b/internal/payment/client.go @@ -0,0 +1,272 @@ +package payment + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +// ───────────────────────────────────────────── +// 配置与数据结构 +// ───────────────────────────────────────────── + +// Config 支付平台配置,可接入任意支付网关 +type Config struct { + AppID string `json:"app_id"` + MchID string `json:"mch_id"` // 商户号(可选) + APIKey string `json:"api_key"` // 签名密钥 + BaseURL string `json:"base_url"` // 网关地址 + Timeout time.Duration `json:"timeout"` + AutoRefreshToken bool `json:"auto_refresh_token"` // 是否自动续期 +} + +// Token 授权令牌 +type Token struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + ExpiresAt time.Time `json:"expires_at"` + IsExpired func() bool +} + +func (t *Token) update(expiresIn int) { + t.ExpiresIn = expiresIn + t.ExpiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second) + t.IsExpired = func() bool { + return time.Now().After(t.ExpiresAt.Add(-30 * time.Second)) // 提前30秒判定过期 + } +} + +// ───────────────────────────────────────────── +// HTTP 客户端(带自动重填和签名) +// ───────────────────────────────────────────── + +// Client 支付 HTTP 客户端 +type Client struct { + mu sync.Mutex + cfg Config + httpClient *http.Client + token *Token + tokenMu sync.RWMutex +} + +// NewClient 创建支付客户端 +func NewClient(cfg Config) *Client { + if cfg.Timeout == 0 { + cfg.Timeout = 10 * time.Second + } + if cfg.BaseURL == "" { + cfg.BaseURL = "https://api.example.com" + } + c := &Client{ + cfg: cfg, + httpClient: &http.Client{Timeout: cfg.Timeout}, + } + return c +} + +// GetToken 获取访问令牌 +func (c *Client) GetToken(ctx context.Context) (*Token, error) { + c.tokenMu.RLock() + if c.token != nil && !c.token.IsExpired() { + c.tokenMu.RUnlock() + return c.token, nil + } + c.tokenMu.RUnlock() + + c.mu.Lock() + defer c.mu.Unlock() + + // double-check + c.tokenMu.RLock() + if c.token != nil && !c.token.IsExpired() { + c.tokenMu.RUnlock() + return c.token, nil + } + c.tokenMu.RUnlock() + + reqURL := fmt.Sprintf("%s/oauth/token", c.cfg.BaseURL) + body := map[string]string{ + "app_id": c.cfg.AppID, + "timestamp": fmt.Sprintf("%d", time.Now().Unix()), + "nonce": generateNonce(), + } + reqBody, _ := json.Marshal(body) + + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody))) + req.Header.Set("Content-Type", "application/json") + addSignature(req, body, c.cfg.APIKey) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("get token: %w", err) + } + defer resp.Body.Close() + + var result struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + } `json:"data"` + } + json.NewDecoder(resp.Body).Decode(&result) + + if result.Code != 0 { + return nil, fmt.Errorf("get token failed: %s", result.Message) + } + + c.token = &Token{} + c.token.update(result.Data.ExpiresIn) + c.token.AccessToken = result.Data.AccessToken + return c.token, nil +} + +// RefreshToken 主动刷新令牌 +func (c *Client) RefreshToken(ctx context.Context) (*Token, error) { + reqURL := fmt.Sprintf("%s/oauth/refresh", c.cfg.BaseURL) + body := map[string]string{ + "app_id": c.cfg.AppID, + "timestamp": fmt.Sprintf("%d", time.Now().Unix()), + } + reqBody, _ := json.Marshal(body) + + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody))) + req.Header.Set("Content-Type", "application/json") + addSignature(req, body, c.cfg.APIKey) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + } `json:"data"` + } + json.NewDecoder(resp.Body).Decode(&result) + + if result.Code != 0 { + return nil, fmt.Errorf("refresh token failed: %s", result.Message) + } + + c.mu.Lock() + c.token = &Token{} + c.token.update(result.Data.ExpiresIn) + c.token.AccessToken = result.Data.AccessToken + c.mu.Unlock() + return c.token, nil +} + +// Get 发送 GET 请求(自动携带 token) +func (c *Client) Get(ctx context.Context, path string, params map[string]string) ([]byte, error) { + token, err := c.GetToken(ctx) + if err != nil { + return nil, err + } + return c.doRequest(ctx, http.MethodGet, path, params, token.AccessToken, nil) +} + +// Post 发送 POST 请求(自动携带 token) +func (c *Client) Post(ctx context.Context, path string, params map[string]string, body any) ([]byte, error) { + token, err := c.GetToken(ctx) + if err != nil { + return nil, err + } + return c.doRequest(ctx, http.MethodPost, path, params, token.AccessToken, body) +} + +func (c *Client) doRequest(ctx context.Context, method, path string, params map[string]string, token string, body any) ([]byte, error) { + reqURL := c.cfg.BaseURL + path + values := url.Values{} + for k, v := range params { + values.Set(k, v) + } + if values.Encode() != "" { + reqURL += "?" + values.Encode() + } + + var reqBody io.Reader + var contentType string + if body != nil { + data, _ := json.Marshal(body) + reqBody = strings.NewReader(string(data)) + contentType = "application/json" + } + + req, _ := http.NewRequestWithContext(ctx, method, reqURL, reqBody) + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request %s %s: %w", method, path, err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)) + } + return respBody, nil +} + +// ───────────────────────────────────────────── +// 签名辅助 +// ───────────────────────────────────────────── + +func addSignature(req *http.Request, body map[string]string, apiKey string) { + signStr := buildSignString(body) + sig := hmacSHA256(signStr, apiKey) + req.Header.Set("X-Signature", sig) +} + +func buildSignString(params map[string]string) string { + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sortStrings(keys) + var sb strings.Builder + for _, k := range keys { + sb.WriteString(k) + sb.WriteString("=") + sb.WriteString(params[k]) + sb.WriteString("&") + } + return strings.TrimRight(sb.String(), "&") +} + +func hmacSHA256(data, key string) string { + m := hmac.New(sha256.New, []byte(key)) + m.Write([]byte(data)) + return fmt.Sprintf("%x", m.Sum(nil)) +} + +func sortStrings(s []string) { + for i := 1; i < len(s); i++ { + for j := i; j > 0 && s[j] < s[j-1]; j-- { + s[j], s[j-1] = s[j-1], s[j] + } + } +} + +func generateNonce() string { + // 简单 nonce,生产环境建议使用 crypto/rand + return fmt.Sprintf("%d", time.Now().UnixNano()) +} diff --git a/internal/payment/service.go b/internal/payment/service.go new file mode 100644 index 0000000..d035d89 --- /dev/null +++ b/internal/payment/service.go @@ -0,0 +1,149 @@ +package payment + +import ( + "context" + "encoding/json" + "fmt" +) + +// Service 支付业务层,内部封装 Client,对外暴露方法 +type Service struct { + client *Client + cfg Config +} + +// NewService 创建支付服务 +func NewService(cfg Config) *Service { + return &Service{ + client: NewClient(cfg), + cfg: cfg, + } +} + +// GetClient 获取底层 HTTP 客户端(供高级用法) +func (s *Service) GetClient() *Client { + return s.client +} + +// Login 登录授权,获取 access_token +func (s *Service) Login(ctx context.Context) (map[string]any, error) { + token, err := s.client.GetToken(ctx) + if err != nil { + return nil, err + } + return map[string]any{ + "success": true, + "access_token": token.AccessToken, + "expires_in": token.ExpiresIn, + "message": "授权成功", + }, nil +} + +// CheckAuth 查询授权状态 +func (s *Service) CheckAuth(ctx context.Context) (map[string]any, error) { + token, err := s.client.GetToken(ctx) + if err != nil { + return map[string]any{"success": false, "message": "授权失效,请重新登录"}, err + } + return map[string]any{ + "success": true, + "access_token": token.AccessToken, + "expires_in": token.ExpiresIn, + "message": "授权有效", + }, nil +} + +// Refresh 主动刷新令牌 +func (s *Service) Refresh(ctx context.Context) (map[string]any, error) { + token, err := s.client.RefreshToken(ctx) + if err != nil { + return nil, err + } + return map[string]any{ + "success": true, + "access_token": token.AccessToken, + "expires_in": token.ExpiresIn, + "message": "续期成功", + }, nil +} + +// GetAccountInfo 获取授权账号信息 +func (s *Service) GetAccountInfo(ctx context.Context) (map[string]any, error) { + raw, err := s.client.Get(ctx, "/v1/account/info", nil) + if err != nil { + return nil, fmt.Errorf("get account info: %w", err) + } + return parseAPIResponse(raw) +} + +// QueryOrder 通过 order_id 查询订单 +func (s *Service) QueryOrder(ctx context.Context, orderID string) (map[string]any, error) { + raw, err := s.client.Get(ctx, "/v1/orders/"+orderID, nil) + if err != nil { + return nil, fmt.Errorf("query order: %w", err) + } + return parseAPIResponse(raw) +} + +// QueryOrderByOutNo 通过商户订单号查询 +func (s *Service) QueryOrderByOutNo(ctx context.Context, outTradeNo string) (map[string]any, error) { + raw, err := s.client.Get(ctx, "/v1/orders", map[string]string{"out_trade_no": outTradeNo}) + if err != nil { + return nil, fmt.Errorf("query order by out_no: %w", err) + } + return parseAPIResponse(raw) +} + +// RequestRefund 申请退款 +func (s *Service) RequestRefund(ctx context.Context, req map[string]any) (map[string]any, error) { + raw, err := s.client.Post(ctx, "/v1/refunds", nil, req) + if err != nil { + return nil, fmt.Errorf("request refund: %w", err) + } + return parseAPIResponse(raw) +} + +// GetBill 分页获取账单 +func (s *Service) GetBill(ctx context.Context, page, pageSize int) (map[string]any, error) { + params := map[string]string{ + "page": fmt.Sprintf("%d", page), + "page_size": fmt.Sprintf("%d", pageSize), + } + raw, err := s.client.Get(ctx, "/v1/bills", params) + if err != nil { + return nil, fmt.Errorf("get bill: %w", err) + } + return parseAPIResponse(raw) +} + +// GetBillByDate 按日期范围查账单 +func (s *Service) GetBillByDate(ctx context.Context, startDate, endDate string) (map[string]any, error) { + params := map[string]string{ + "start_date": startDate, + "end_date": endDate, + } + raw, err := s.client.Get(ctx, "/v1/bills/range", params) + if err != nil { + return nil, fmt.Errorf("get bill by date: %w", err) + } + return parseAPIResponse(raw) +} + +// ───────────────────────────────────────────── +// 内部辅助 +// ───────────────────────────────────────────── + +func parseAPIResponse(raw []byte) (map[string]any, error) { + var result struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` + } + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + if result.Code != 0 { + return nil, fmt.Errorf("api error code=%d msg=%s", result.Code, result.Message) + } + return map[string]any{"success": true, "data": result.Data, "message": result.Message}, nil +} diff --git a/internal/plugins/ai_impl.go b/internal/plugins/ai_impl.go new file mode 100644 index 0000000..fe46dcf --- /dev/null +++ b/internal/plugins/ai_impl.go @@ -0,0 +1,26 @@ +//go:build ai + +package plugins + +import ( + "context" + "fmt" +) + +type aiPlugin struct { + enabled bool +} + +func (p *aiPlugin) Name() string { return "ai" } +func (p *aiPlugin) Enabled() bool { return p.enabled } +func (p *aiPlugin) Init(ctx context.Context) error { + fmt.Println("[ai] initialized") + return nil +} +func (p *aiPlugin) Register(app *App) error { return nil } +func (p *aiPlugin) Shutdown() error { return nil } + +func init() { + cfg, _ := LoadOrDefault() + Register(&aiPlugin{enabled: cfg.Plugins["ai"].Enabled}) +} diff --git a/internal/plugins/concurrent_impl.go b/internal/plugins/concurrent_impl.go new file mode 100644 index 0000000..fabdf38 --- /dev/null +++ b/internal/plugins/concurrent_impl.go @@ -0,0 +1,26 @@ +//go:build concurrent + +package plugins + +import ( + "context" + "fmt" +) + +type concurrentPlugin struct { + enabled bool +} + +func (p *concurrentPlugin) Name() string { return "concurrent" } +func (p *concurrentPlugin) Enabled() bool { return p.enabled } +func (p *concurrentPlugin) Init(ctx context.Context) error { + fmt.Println("[concurrent] initialized") + return nil +} +func (p *concurrentPlugin) Register(app *App) error { return nil } +func (p *concurrentPlugin) Shutdown() error { return nil } + +func init() { + cfg, _ := LoadOrDefault() + Register(&concurrentPlugin{enabled: cfg.Plugins["concurrent"].Enabled}) +} diff --git a/internal/plugins/iconfont_impl.go b/internal/plugins/iconfont_impl.go new file mode 100644 index 0000000..d7783dc --- /dev/null +++ b/internal/plugins/iconfont_impl.go @@ -0,0 +1,26 @@ +//go:build iconfont + +package plugins + +import ( + "context" + "fmt" +) + +type iconfontPlugin struct { + enabled bool +} + +func (p *iconfontPlugin) Name() string { return "iconfont" } +func (p *iconfontPlugin) Enabled() bool { return p.enabled } +func (p *iconfontPlugin) Init(ctx context.Context) error { + fmt.Println("[iconfont] initialized") + return nil +} +func (p *iconfontPlugin) Register(app *App) error { return nil } +func (p *iconfontPlugin) Shutdown() error { return nil } + +func init() { + cfg, _ := LoadOrDefault() + Register(&iconfontPlugin{enabled: cfg.Plugins["iconfont"].Enabled}) +} diff --git a/internal/plugins/loader.go b/internal/plugins/loader.go new file mode 100644 index 0000000..dc23635 --- /dev/null +++ b/internal/plugins/loader.go @@ -0,0 +1,153 @@ +package plugins + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" +) + +// Config 插件配置文件结构 +type Config struct { + Version string `json:"version"` + Plugins map[string]PluginOption `json:"plugins"` +} + +// PluginOption 单个插件配置项 +type PluginOption struct { + Enabled bool `json:"enabled"` + Description string `json:"description"` + // Params 插件专用参数,由插件自己解析 + Params map[string]interface{} `json:"params,omitempty"` +} + +// Loader 插件配置加载器 +type Loader struct { + mu sync.RWMutex + cfg *Config + path string +} + +// NewLoader 创建配置加载器 +func NewLoader(path string) *Loader { + return &Loader{path: path} +} + +// Load 从文件加载配置 +func (l *Loader) Load() (*Config, error) { + l.mu.Lock() + defer l.mu.Unlock() + + data, err := os.ReadFile(l.path) + if err != nil { + return nil, err + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + // 环境变量覆盖优先级最高 + if v := os.Getenv("PLUGIN_ICONFONT"); v == "false" || v == "0" { + tmp := cfg.Plugins["iconfont"]; tmp.Enabled = false; cfg.Plugins["iconfont"] = tmp + } + if v := os.Getenv("PLUGIN_AI"); v == "true" || v == "1" { + tmp := cfg.Plugins["ai"]; tmp.Enabled = true; cfg.Plugins["ai"] = tmp + } + if v := os.Getenv("PLUGIN_CONCURRENT"); v == "false" || v == "0" { + tmp := cfg.Plugins["concurrent"]; tmp.Enabled = false; cfg.Plugins["concurrent"] = tmp + } + + l.cfg = &cfg + return l.cfg, nil +} + +// IsEnabled 查询插件启用状态 +func (l *Loader) IsEnabled(name string) bool { + l.mu.RLock() + defer l.mu.RUnlock() + if l.cfg == nil { + return false + } + pc, ok := l.cfg.Plugins[name] + return ok && pc.Enabled +} + +// GetEnabledNames 返回所有已启用插件名 +func (l *Loader) GetEnabledNames() []string { + l.mu.RLock() + defer l.mu.RUnlock() + if l.cfg == nil { + return nil + } + result := make([]string, 0) + for name, pc := range l.cfg.Plugins { + if pc.Enabled { + result = append(result, name) + } + } + return result +} + +// Save 保存配置 +func (l *Loader) Save(cfg *Config) error { + l.mu.Lock() + defer l.mu.Unlock() + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + dir := filepath.Dir(l.path) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + return os.WriteFile(l.path, data, 0644) +} + +// LoadOrDefault 加载配置,找不到则返回默认值 +func LoadOrDefault() (*Config, error) { + paths := []string{"config/plugin.json", "./plugin.json"} + exe, _ := os.Executable() + if exe != "" { + dir := filepath.Dir(exe) + paths = append([]string{ + filepath.Join(dir, "config", "plugin.json"), + filepath.Join(dir, "plugin.json"), + }, paths...) + } + for _, p := range paths { + if _, err := os.Stat(p); err == nil { + loader := NewLoader(p) + if cfg, err := loader.Load(); err == nil { + return cfg, nil + } + } + } + return defaultConfig(), nil +} + +// EnsureConfig 确保配置文件存在 +func EnsureConfig() error { + _, err := os.Stat("config/plugin.json") + if err == nil { + return nil + } + cfg, _ := LoadOrDefault() + loader := NewLoader("config/plugin.json") + if mkErr := os.MkdirAll("config", 0755); mkErr != nil { + return mkErr + } + return loader.Save(cfg) +} + +func defaultConfig() *Config { + return &Config{ + Version: "1.0.0", + Plugins: map[string]PluginOption{ + "iconfont": {Enabled: false, Description: "阿里IconFont图标库,支持字体图标"}, + "ai": {Enabled: false, Description: "AI大模型集成,支持对话、分析等功能"}, + "concurrent": {Enabled: true, Description: "高并发工具集,任务池、限流器等"}, + }, + } +} diff --git a/internal/plugins/payment_impl.go b/internal/plugins/payment_impl.go new file mode 100644 index 0000000..e026267 --- /dev/null +++ b/internal/plugins/payment_impl.go @@ -0,0 +1,54 @@ +// Package plugins — payment 插件实现(带 build tag,未启用时不编译) +//go:build payment + +package plugins + +import ( + "context" + "fmt" + + "go_win_template/internal/payment" +) + +// paymentPluginImpl 支付插件 +type paymentPluginImpl struct { + enabled bool + svc *payment.Service +} + +func (p *paymentPluginImpl) Name() string { return "payment" } +func (p *paymentPluginImpl) Enabled() bool { return p.enabled } +func (p *paymentPluginImpl) Init(ctx context.Context) error { + cfg, _ := LoadOrDefault() + pcfg := payment.Config{ + AppID: getStringParam(cfg, "payment", "app_id"), + MchID: getStringParam(cfg, "payment", "mch_id"), + APIKey: getStringParam(cfg, "payment", "api_key"), + BaseURL: getStringParam(cfg, "payment", "base_url"), + AutoRefreshToken: true, + } + p.svc = payment.NewService(pcfg) + fmt.Printf("[payment] initialized, base_url=%s\n", pcfg.BaseURL) + return nil +} +func (p *paymentPluginImpl) Register(app *App) error { return nil } +func (p *paymentPluginImpl) Shutdown() error { return nil } + +// GetPaymentService 返回支付服务(any 类型,供 App 层类型断言) +func (p *paymentPluginImpl) GetPaymentService() any { + return p.svc +} + +func init() { + cfg, _ := LoadOrDefault() + Register(&paymentPluginImpl{enabled: cfg.Plugins["payment"].Enabled}) +} + +func getStringParam(cfg *Config, plugin, key string) string { + if opts, ok := cfg.Plugins[plugin]; ok { + if v, ok := opts.Params[key].(string); ok { + return v + } + } + return "" +} diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go new file mode 100644 index 0000000..08c5dca --- /dev/null +++ b/internal/plugins/plugins.go @@ -0,0 +1,150 @@ +// Package plugins 插件系统核心接口与注册中心 +package plugins + +import ( + "context" + "fmt" + "log" + "sync" +) + +// Plugin 插件接口,所有插件必须实现 +type Plugin interface { + Name() string + Init(ctx context.Context) error + Register(app *App) error + Shutdown() error + Enabled() bool +} + +// App 插件可见的应用上下文 +type App struct { + ctx context.Context + config map[string]interface{} +} + +// NewApp 创建应用上下文 +func NewApp(ctx context.Context, config map[string]interface{}) *App { + return &App{ctx: ctx, config: config} +} + +// Registry 插件注册中心(线程安全) +type Registry struct { + mu sync.Mutex + plugins map[string]Plugin +} + +var globalRegistry *Registry +var once sync.Once + +func getGlobalRegistry() *Registry { + once.Do(func() { + globalRegistry = &Registry{plugins: make(map[string]Plugin)} + }) + return globalRegistry +} + +// Register 全局注册插件(由各插件 build.go 的 init() 调用) +func Register(p Plugin) { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + r.plugins[p.Name()] = p + log.Printf("[plugins] registered: %s (enabled=%v)", p.Name(), p.Enabled()) +} + +// Get 获取已注册插件 +func Get(name string) Plugin { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + return r.plugins[name] +} + +// List 列出所有已注册插件名 +func List() []string { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + names := make([]string, 0, len(r.plugins)) + for name := range r.plugins { + names = append(names, name) + } + return names +} + +// Count 返回已注册插件数 +func Count() int { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + return len(r.plugins) +} + +// Start 启动所有已启用的插件 +func Start(ctx context.Context) error { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + for name, p := range r.plugins { + if !p.Enabled() { + log.Printf("[plugins] skip disabled: %s", name) + continue + } + if err := p.Init(ctx); err != nil { + log.Printf("[plugins] init %s failed: %v", name, err) + continue + } + if err := p.Register(NewApp(ctx, nil)); err != nil { + log.Printf("[plugins] register %s failed: %v", name, err) + continue + } + log.Printf("[plugins] started: %s", name) + } + return nil +} + +// Shutdown 关闭所有插件 +func Shutdown() error { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + for name, p := range r.plugins { + if err := p.Shutdown(); err != nil { + log.Printf("[plugins] shutdown %s error: %v", name, err) + } + } + return nil +} + +// EnabledPlugins 返回所有已启用插件列表 +func EnabledPlugins() []Plugin { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + result := make([]Plugin, 0) + for _, p := range r.plugins { + if p.Enabled() { + result = append(result, p) + } + } + return result +} + +// PrintStatus 打印当前插件状态(调试用) +func PrintStatus() { + r := getGlobalRegistry() + r.mu.Lock() + defer r.mu.Unlock() + fmt.Println("[plugins] current status:") + for name, p := range r.plugins { + status := "disabled" + if p.Enabled() { + status = "enabled" + } + fmt.Printf(" - %s: %s\n", name, status) + } + if len(r.plugins) == 0 { + fmt.Println(" (no plugins registered — check build tags)") + } +} diff --git a/internal/service/concurrent.go b/internal/service/concurrent.go new file mode 100644 index 0000000..bc39b47 --- /dev/null +++ b/internal/service/concurrent.go @@ -0,0 +1,369 @@ +package service + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" +) + +// BatchResult 批量任务结果 +type BatchResult struct { + Success []any + Fail []BatchError +} + +// BatchError 批量任务失败记录 +type BatchError struct { + Index int + Item any + Err error +} + +// BatchWorker 批量处理器,适合文件处理、数据转换等高并发场景 +type BatchWorker struct { + mu sync.Mutex + running atomic.Int64 + total atomic.Int64 + success atomic.Int64 + failed atomic.Int64 + maxConcurrency int +} + +// NewBatchWorker 创建批量处理器 +func NewBatchWorker(maxConcurrency int) *BatchWorker { + if maxConcurrency <= 0 { + maxConcurrency = 10 + } + return &BatchWorker{ + maxConcurrency: maxConcurrency, + } +} + +// ProcessConcurrent 并发处理一批数据 +func (bw *BatchWorker) ProcessConcurrent( + ctx context.Context, + items []any, + handler func(ctx context.Context, item any) (any, error), +) BatchResult { + bw.total.Add(int64(len(items))) + results := BatchResult{} + sem := make(chan struct{}, bw.maxConcurrency) + var wg sync.WaitGroup + var mu sync.Mutex + + for i, item := range items { + select { + case <-ctx.Done(): + break + default: + } + wg.Add(1) + sem <- struct{}{} + go func(index int, data any) { + defer wg.Done() + defer func() { <-sem }() + bw.running.Add(1) + defer bw.running.Add(-1) + + result, err := handler(ctx, data) + bw.success.Add(1) + + mu.Lock() + defer mu.Unlock() + if err != nil { + bw.failed.Add(1) + results.Fail = append(results.Fail, BatchError{ + Index: index, + Item: data, + Err: err, + }) + } else { + results.Success = append(results.Success, result) + } + }(i, item) + } + + wg.Wait() + return results +} + +// ProcessSequential 顺序处理(用于需要严格顺序的场景) +func (bw *BatchWorker) ProcessSequential( + ctx context.Context, + items []any, + handler func(ctx context.Context, item any) (any, error), +) BatchResult { + results := BatchResult{} + for i, item := range items { + select { + case <-ctx.Done(): + return results + default: + } + result, err := handler(ctx, item) + if err != nil { + results.Fail = append(results.Fail, BatchError{Index: i, Item: item, Err: err}) + } else { + results.Success = append(results.Success, result) + } + } + return results +} + +// Stats 当前处理统计 +func (bw *BatchWorker) Stats() (running, total, success, failed int64) { + return bw.running.Load(), bw.total.Load(), bw.success.Load(), bw.failed.Load() +} + +// RingBuffer 无锁环形缓冲区,适合高频消息传递 +type RingBuffer struct { + data []any + head int64 + tail int64 + capacity int + size atomic.Int64 +} + +// NewRingBuffer 创建环形缓冲区 +func NewRingBuffer(capacity int) *RingBuffer { + if capacity <= 0 { + capacity = 1024 + } + rb := &RingBuffer{ + data: make([]any, capacity), + capacity: capacity, + } + return rb +} + +// Enqueue 入队(阻塞直到有空位) +func (rb *RingBuffer) Enqueue(item any) { + for { + tail := atomic.LoadInt64(&rb.tail) + head := atomic.LoadInt64(&rb.head) + if tail-head >= int64(rb.capacity) { + time.Sleep(time.Microsecond) + continue + } + if atomic.CompareAndSwapInt64(&rb.tail, tail, tail+1) { + rb.data[tail%int64(rb.capacity)] = item + rb.size.Add(1) + return + } + } +} + +// Dequeue 出队(阻塞直到有数据) +func (rb *RingBuffer) Dequeue() (any, bool) { + for { + head := atomic.LoadInt64(&rb.head) + tail := atomic.LoadInt64(&rb.tail) + if head >= tail { + return nil, false + } + if atomic.CompareAndSwapInt64(&rb.head, head, head+1) { + item := rb.data[head%int64(rb.capacity)] + rb.data[head%int64(rb.capacity)] = nil + rb.size.Add(-1) + return item, true + } + } +} + +// Size 当前元素数量 +func (rb *RingBuffer) Size() int64 { + return rb.size.Load() +} + +// RateLimiter 令牌桶限流器 +type RateLimiter struct { + tokens int64 + maxTokens int64 + refillRate int64 // 每秒补充数 + lastRefill time.Time + mu sync.Mutex +} + +// NewRateLimiter 创建限流器 +func NewRateLimiter(maxTokens, refillRate int64) *RateLimiter { + rl := &RateLimiter{ + tokens: maxTokens, + maxTokens: maxTokens, + refillRate: refillRate, + lastRefill: time.Now(), + } + go rl.refillLoop() + return rl +} + +func (rl *RateLimiter) refillLoop() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for range ticker.C { + rl.mu.Lock() + elapsed := time.Since(rl.lastRefill).Seconds() + add := int64(elapsed * float64(rl.refillRate)) + current := rl.tokens + if current+add > rl.maxTokens { + add = rl.maxTokens - current + } + rl.tokens += add + rl.lastRefill = time.Now() + rl.mu.Unlock() + } +} + +// Acquire 获取一个令牌,返回是否成功 +func (rl *RateLimiter) Acquire() bool { + rl.mu.Lock() + defer rl.mu.Unlock() + if rl.tokens <= 0 { + return false + } + rl.tokens-- + return true +} + +// Limit 带限制的包装函数 +func (rl *RateLimiter) Limit(fn func() error) error { + for !rl.Acquire() { + time.Sleep(time.Millisecond * 10) + } + return fn() +} + +// ConcurrentMap 线程安全的 map,支持并发读写 +type ConcurrentMap struct { + mu sync.RWMutex + items map[any]any +} + +// NewConcurrentMap 创建线程安全 map +func NewConcurrentMap() *ConcurrentMap { + return &ConcurrentMap{items: make(map[any]any)} +} + +func (cm *ConcurrentMap) Set(k, v any) { + cm.mu.Lock() + defer cm.mu.Unlock() + cm.items[k] = v +} + +func (cm *ConcurrentMap) Get(k any) (any, bool) { + cm.mu.RLock() + defer cm.mu.RUnlock() + v, ok := cm.items[k] + return v, ok +} + +func (cm *ConcurrentMap) Delete(k any) { + cm.mu.Lock() + defer cm.mu.Unlock() + delete(cm.items, k) +} + +func (cm *ConcurrentMap) Len() int { + cm.mu.RLock() + defer cm.mu.RUnlock() + return len(cm.items) +} + +// Pipeline 函数式管道,链式处理数据流 +type Pipeline struct { + stages []func(any) any + input chan any + output chan any +} + +// NewPipeline 创建管道 +func NewPipeline() *Pipeline { + return &Pipeline{ + stages: make([]func(any) any, 0), + input: make(chan any, 256), + output: make(chan any, 256), + } +} + +// AddStage 添加处理阶段 +func (p *Pipeline) AddStage(fn func(any) any) *Pipeline { + p.stages = append(p.stages, fn) + return p +} + +// Run 启动管道(阻塞,直到 input 关闭) +func (p *Pipeline) Run(items []any) []any { + go func() { + for _, item := range items { + p.input <- item + } + close(p.input) + }() + + go p.process() + results := make([]any, 0) + for v := range p.output { + results = append(results, v) + } + return results +} + +func (p *Pipeline) process() { + defer close(p.output) + for item := range p.input { + for _, stage := range p.stages { + item = stage(item) + } + p.output <- item + } +} + +// GoCtx 便捷并发模式 +type GoCtx struct { + wg sync.WaitGroup + errs sync.Map + results sync.Map +} + +// Go 启动一个并发任务 +func (gc *GoCtx) Go(id string, fn func() error) { + gc.wg.Add(1) + go func() { + defer gc.wg.Done() + if err := fn(); err != nil { + gc.errs.Store(id, err) + } + }() +} + +// Wait 等待所有任务完成,返回错误列表 +func (gc *GoCtx) Wait() []error { + gc.wg.Wait() + var errs []error + gc.errs.Range(func(_, v any) bool { + errs = append(errs, v.(error)) + return true + }) + return errs +} + +// WaitWithTimeout 带超时的等待 +func (gc *GoCtx) WaitWithTimeout(timeout time.Duration) []error { + done := make(chan struct{}) + go func() { + gc.wg.Wait() + close(done) + }() + select { + case <-done: + var errs []error + gc.errs.Range(func(_, v any) bool { + errs = append(errs, v.(error)) + return true + }) + return errs + case <-time.After(timeout): + return []error{fmt.Errorf("并发任务超时: %v", timeout)} + } +} diff --git a/internal/tray/win/tray.go b/internal/tray/win/tray.go new file mode 100644 index 0000000..50d5f28 --- /dev/null +++ b/internal/tray/win/tray.go @@ -0,0 +1,167 @@ +// Package tray Windows 系统托盘实现(使用 win32 API) +package tray + +import ( + "log" + "sync" + "unsafe" + + "golang.org/x/sys/windows" +) + +// Hook 托盘图标句柄 +var hook windows.Handle + +// MenuHandle 菜单句柄 +type MenuHandle uintptr + +// Tray 系统托盘管理器 +type Tray struct { + mu sync.Mutex + visible bool + callback uintptr // WNDPROC 回调 + hWnd windows.Handle +} + +// MenuItem 菜单项 +type MenuItem struct { + ID uint32 + Text string +} + +// NewTray 创建系统托盘 +func NewTray(iconData []byte, tooltip string) *Tray { + t := &Tray{} + if err := t.init(iconData, tooltip); err != nil { + log.Printf("[tray] init failed: %v", err) + return nil + } + return t +} + +func (t *Tray) init(iconData []byte, tooltip string) error { + // 获取当前进程窗口 + hWnd, _ := windows.GetForegroundWindow() + if hWnd == 0 { + return windows.ERROR_ACCESS_DENIED + } + t.hWnd = hWnd + + // 注册托盘图标 + // NOTIFYICONDATA structure + // https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa + data := ¬ifyIconData{ + CbSize: uint32(unsafe.Sizeof(notifyIconData{})), + HWnd: uint32(hWnd), + UID: 1000, + UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP, + UCallbackMessage: WM_USER + 1000, + HIcon: 0, // 使用默认图标 + } + + // 设置 tooltip + for i := 0; i < len(tooltip) && i < 128; i++ { + data.Tip[i] = uint8(tooltip[i]) + } + data.Tip[len(tooltip)] = 0 + + // 添加托盘图标 + ret, _, _ := procShellNotifyIcon.Call( + SHM_ADD, + uintptr(unsafe.Pointer(data)), + ) + if ret == 0 { + return windows.GetLastError() + } + + t.visible = true + return nil +} + +// Destroy 销毁托盘图标 +func (t *Tray) Destroy() { + t.mu.Lock() + defer t.mu.Unlock() + if !t.visible { + return + } + data := ¬ifyIconData{ + CbSize: uint32(unsafe.Sizeof(notifyIconData{})), + HWnd: uint32(t.hWnd), + UID: 1000, + UFlags: NIF_MESSAGE, + } + procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data))) + t.visible = false +} + +// Show 显示托盘 +func (t *Tray) Show(iconData []byte, tooltip string) error { + t.mu.Lock() + defer t.mu.Unlock() + if t.visible { + return nil + } + return t.init(iconData, tooltip) +} + +// Hide 隐藏托盘 +func (t *Tray) Hide() { + t.mu.Lock() + defer t.mu.Unlock() + if !t.visible { + return + } + data := ¬ifyIconData{ + CbSize: uint32(unsafe.Sizeof(notifyIconData{})), + HWnd: uint32(t.hWnd), + UID: 1000, + UFlags: NIF_MESSAGE, + } + procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data))) + t.visible = false +} + +// IsVisible 是否可见 +func (t *Tray) IsVisible() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.visible +} + +// ───────────────────────────────────────────── +// Win32 API 定义 +// ───────────────────────────────────────────── + +const ( + WM_USER = 0x0400 + SHM_ADD uintptr = 0x00000000 + SHM_DELETE uintptr = 0x00000001 + + NIF_MESSAGE = 0x00000001 + NIF_ICON = 0x00000002 + NIF_TIP = 0x00000004 +) + +// NOTIFYICONDATA struct +type notifyIconData struct { + CbSize uint32 + HWnd uint32 + UID uint32 + UFlags uint32 + UCallbackMessage uint32 + HIcon uintptr + Tip [128]uint8 +} + +var ( + procShellNotifyIcon = mustLoad("shell32.dll").MustFindProc("Shell_NotifyIconA") +) + +func mustLoad(dllName string) *windows.DynLib { + dll, err := windows.LoadDLL(dllName) + if err != nil { + log.Fatalf("[tray] failed to load %s: %v", dllName, err) + } + return dll +} diff --git a/internal/worker/pool.go b/internal/worker/pool.go new file mode 100644 index 0000000..6e7466e --- /dev/null +++ b/internal/worker/pool.go @@ -0,0 +1,146 @@ +package worker + +import ( + "context" + "sync" + "sync/atomic" +) + +// Job 任务接口,所有后台任务需实现此接口 +type Job interface { + Execute(ctx context.Context) error + JobID() string +} + +// TaskResult 任务执行结果 +type TaskResult struct { + JobID string + Output any + Error error +} + +// Pool 任务并发池,管理固定数量工作协程 +type Pool struct { + jobs chan Job + results chan TaskResult + wg sync.WaitGroup + stopOnce sync.Once + ctx context.Context + cancel context.CancelFunc + running atomic.Int64 + total atomic.Int64 + failed atomic.Int64 +} + +// Options 池配置 +type Options struct { + Workers int // 工作协程数,默认 4 + BufSize int // 队列缓冲,默认 1000 +} + +// NewPool 创建任务池 +func NewPool(opts ...Options) *Pool { + o := Options{Workers: 4, BufSize: 1000} + if len(opts) > 0 { + o = opts[0] + } + if o.Workers <= 0 { + o.Workers = 4 + } + if o.BufSize <= 0 { + o.BufSize = 1000 + } + ctx, cancel := context.WithCancel(context.Background()) + p := &Pool{ + jobs: make(chan Job, o.BufSize), + results: make(chan TaskResult, o.BufSize), + ctx: ctx, + cancel: cancel, + } + p.startWorkers() + return p +} + +func (p *Pool) startWorkers() { + for i := 0; i < 4; i++ { + p.wg.Add(1) + go p.worker() + } +} + +func (p *Pool) worker() { + defer p.wg.Done() + for { + select { + case <-p.ctx.Done(): + return + case job, ok := <-p.jobs: + if !ok { + return + } + p.running.Add(1) + p.total.Add(1) + err := job.Execute(p.ctx) + result := TaskResult{JobID: job.JobID(), Error: err} + if err != nil { + p.failed.Add(1) + } + p.running.Add(-1) + select { + case p.results <- result: + case <-p.ctx.Done(): + return + } + } + } +} + +// Submit 提交任务到队列(非阻塞,队列满时返回 false) +func (p *Pool) Submit(job Job) bool { + select { + case p.jobs <- job: + return true + default: + return false + } +} + +// SubmitAndWait 提交任务并等待结果 +func (p *Pool) SubmitAndWait(ctx context.Context, job Job) (*TaskResult, error) { + if !p.Submit(job) { + return nil, ErrQueueFull + } + select { + case r := <-p.results: + return &r, r.Error + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// Results 返回结果通道,供外部 range 消费 +func (p *Pool) Results() <-chan TaskResult { + return p.results +} + +// Stats 当前池状态 +func (p *Pool) Stats() (running, total, failed int64) { + return p.running.Load(), p.total.Load(), p.failed.Load() +} + +// Stop 优雅停止池 +func (p *Pool) Stop() { + p.stopOnce.Do(func() { + p.cancel() + close(p.jobs) + p.wg.Wait() + close(p.results) + }) +} + +var ErrQueueFull = SyncError("任务队列已满,请降低提交频率或增大 BufSize") + +// SyncError 用于类型断言 +type SyncError string + +func (e SyncError) Error() string { return string(e) } diff --git a/main.go b/main.go new file mode 100644 index 0000000..c658f6e --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "embed" + "log" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + + "go_win_template/internal/plugins" +) + +//go:embed all:frontend/dist +//go:embed assets/icon.png +var assets embed.FS + +func main() { + wailsApp := NewApp() + + cfg := &options.App{ + Title: "go_win_template", + Width: 1280, + Height: 800, + MinWidth: 1024, + MinHeight: 600, + WindowStartState: options.Normal, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: func(ctx context.Context) { + log.Println("[main] startup complete") + // 启动插件 + if err := plugins.Start(ctx); err != nil { + log.Printf("[main] plugin start error: %v", err) + } + plugins.PrintStatus() + }, + Bind: []interface{}{ + wailsApp, + }, + } + + err := wails.Run(cfg) + if err != nil { + log.Fatal(err) + } +} diff --git a/trayicons/icon.png b/trayicons/icon.png new file mode 100644 index 0000000..78872a9 Binary files /dev/null and b/trayicons/icon.png differ diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..9105b88 --- /dev/null +++ b/wails.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "go_win_template", + "outputfilename": "go_win_template", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Geliebte", + "email": "1297754537@qq.com" + }, + "info": { + "productname": "go_win_template", + "version": "1.0.0", + "copyright": "" + } +}