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
This commit is contained in:
2026-09-17 19:31:17 +08:00
commit 1ea8c9da9c
57 changed files with 6305 additions and 0 deletions

5
.codegraph/.gitignore vendored Normal file
View File

@@ -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

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
build/bin
node_modules
frontend/dist

10
.idea/.gitignore generated vendored Normal file
View File

@@ -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

252
.memory/MEMORY.md Normal file
View File

@@ -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 <search_term>
# 探索代码区域(源码 + 调用路径)—— 最常用的命令
codegraph explore "<topic>"
# 构建任务上下文(相关符号、关系、代码块)
codegraph context "<task>"
# 查看单个符号的源码和调用链
codegraph node <symbol_name>
# 查找谁调用了某个函数
codegraph callers <symbol>
# 查找某个函数调用了什么
codegraph callees <symbol>
# 分析修改某符号的影响范围
codegraph impact <symbol>
# 查找受影响的测试文件
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*

19
README.md Normal file
View File

@@ -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`.

322
app.go Normal file
View File

@@ -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 ""
}

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

35
build/README.md Normal file
View File

@@ -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.

BIN
build/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -0,0 +1,68 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.OutputFilename}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{safeBundleID .Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

63
build/darwin/Info.plist Normal file
View File

@@ -0,0 +1,63 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.OutputFilename}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{safeBundleID .Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

15
build/windows/info.json Normal file
View File

@@ -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}}"
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

27
config/plugin.json Normal file
View File

@@ -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"
}
}
}
}

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>go_win_template</title>
</head>
<body>
<div id="root"></div>
<script src="./src/main.tsx" type="module"></script>
</body>
</html>

1847
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
frontend/package.json Normal file
View File

@@ -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"
}
}

View File

@@ -0,0 +1 @@
ebabc2ea396231dd6d5148053c60fb95

134
frontend/src/App.css Normal file
View File

@@ -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;
}

109
frontend/src/App.tsx Normal file
View File

@@ -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<User[]>([]);
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 (
<div className="app">
<header className="header">
<h1>Wails + React + Go</h1>
<p>Windows 桌面应用框架示例</p>
</header>
<main className="main">
<section className="card">
<h2>创建用户</h2>
<div className="form-row">
<input
className="input"
placeholder="名称"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
className="input"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button className="btn-primary" onClick={handleCreate}>
创建
</button>
</div>
{message && <p className="message">{message}</p>}
</section>
<section className="card">
<div className="card-header">
<h2>用户列表</h2>
<button className="btn-secondary" onClick={loadUsers}>
刷新
</button>
</div>
{users.length === 0 ? (
<p className="empty">暂无数据</p>
) : (
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>邮箱</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id}>
<td>{u.id}</td>
<td>{u.name}</td>
<td>{u.email}</td>
<td>{u.created_at}</td>
</tr>
))}
</tbody>
</table>
)}
</section>
</main>
</div>
);
}
export default App;

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

14
frontend/src/main.tsx Normal file
View File

@@ -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(
<React.StrictMode>
<App/>
</React.StrictMode>
)

26
frontend/src/style.css Normal file
View File

@@ -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;
}

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

31
frontend/tsconfig.json Normal file
View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

13
frontend/vite.config.ts Normal file
View File

@@ -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,
},
})

36
frontend/wailsjs/go/main/App.d.ts vendored Normal file
View File

@@ -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<string>,arg2:Array<string>):Promise<main.Response>;
export function ClearUsers():Promise<number>;
export function CreateUser(arg1:string,arg2:string):Promise<main.Response>;
export function GetUserList():Promise<Array<main.User>>;
export function PayCheckAuth():Promise<main.Response>;
export function PayGetAccountInfo():Promise<main.Response>;
export function PayGetBill(arg1:number,arg2:number):Promise<main.Response>;
export function PayGetBillByDate(arg1:string,arg2:string):Promise<main.Response>;
export function PayLogin():Promise<main.Response>;
export function PayQueryOrder(arg1:string):Promise<main.Response>;
export function PayQueryOrderByOutNo(arg1:string):Promise<main.Response>;
export function PayRefreshToken():Promise<main.Response>;
export function PayRequestRefund(arg1:string,arg2:number,arg3:string):Promise<main.Response>;
export function Startup(arg1:context.Context):Promise<void>;
export function Stats():Promise<Record<string, any>>;
export function SubmitTask(arg1:string,arg2:string):Promise<main.Response>;

View File

@@ -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);
}

View File

@@ -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"];
}
}
}

View File

@@ -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 <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}

330
frontend/wailsjs/runtime/runtime.d.ts vendored Normal file
View File

@@ -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<boolean>;
// [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<Size>;
// [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<Position>;
// [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<boolean>;
// [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<boolean>;
// [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<boolean>;
// [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<Screen[]>;
// [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<EnvironmentInfo>;
// [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<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [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<void>;
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
// Cleans up notification resources and releases any held connections.
export function CleanupNotifications(): Promise<void>;
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
// Checks if notifications are available on the current platform.
export function IsNotificationAvailable(): Promise<boolean>;
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
// Requests notification authorization from the user (macOS only).
export function RequestNotificationAuthorization(): Promise<boolean>;
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
// Checks the current notification authorization status (macOS only).
export function CheckNotificationAuthorization(): Promise<boolean>;
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
// Sends a basic notification with the given options.
export function SendNotification(options: NotificationOptions): Promise<void>;
// [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<void>;
// [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<void>;
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
// Removes a previously registered notification category.
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
// Removes all pending notifications from the notification center.
export function RemoveAllPendingNotifications(): Promise<void>;
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
// Removes a specific pending notification by its identifier.
export function RemovePendingNotification(identifier: string): Promise<void>;
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
// Removes all delivered notifications from the notification center.
export function RemoveAllDeliveredNotifications(): Promise<void>;
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
// Removes a specific delivered notification by its identifier.
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
// [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<void>;

View File

@@ -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);
}

38
go.mod Normal file
View File

@@ -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

83
go.sum Normal file
View File

@@ -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=

48
internal/app/app.go Normal file
View File

@@ -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
}

62
internal/bus/bus.go Normal file
View File

@@ -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
}

119
internal/cache/cache.go vendored Normal file
View File

@@ -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()
}
}

272
internal/payment/client.go Normal file
View File

@@ -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())
}

149
internal/payment/service.go Normal file
View File

@@ -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
}

View File

@@ -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})
}

View File

@@ -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})
}

View File

@@ -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})
}

153
internal/plugins/loader.go Normal file
View File

@@ -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: "高并发工具集,任务池、限流器等"},
},
}
}

View File

@@ -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 ""
}

150
internal/plugins/plugins.go Normal file
View File

@@ -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)")
}
}

View File

@@ -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)}
}
}

167
internal/tray/win/tray.go Normal file
View File

@@ -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 := &notifyIconData{
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 := &notifyIconData{
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 := &notifyIconData{
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
}

146
internal/worker/pool.go Normal file
View File

@@ -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) }

50
main.go Normal file
View File

@@ -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)
}
}

BIN
trayicons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

18
wails.json Normal file
View File

@@ -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": ""
}
}