commit f1e2ff65630f232bc8e5130ff70398a9bd8798a1 Author: 绝尘 <237809796@qq.com> Date: Wed Jan 14 14:17:38 2026 +0800 . diff --git a/.buildflags b/.buildflags new file mode 100644 index 0000000..b69ee9f --- /dev/null +++ b/.buildflags @@ -0,0 +1 @@ +-tags=sqlite_omit_load_extension diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..833a86c --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Go vendor directory +vendor/ + +# Dependency directories +node_modules/ + +# Wails build output +build/bin/ + +# Frontend build output +web/dist/ +web/src/wailsjs/ + +# Database files +*.db +*.sqlite +*.sqlite3 +*.db.bak + +# Environment files +.env +.env.local +.env.*.local + +# Temporary files and directories +tmp/ +temp/ +*.tmp +.update-temp/ + +# Cache directories +.cache/ +.vite/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +*.log.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fab026 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# ssq-desk + +双色球桌面查询应用,基于 Wails + Vue 3 + Arco Design 开发。 + +## 技术栈 + +- **前端**:Vue 3 + Arco Design + TypeScript +- **后端**:Go + Wails +- **数据**:MySQL(远程)+ SQLite(本地缓存) + +## 开发 + +### 前置要求 + +- Go 1.23+ +- Node.js 18+ +- Wails CLI v2.11.0+ + +### 安装依赖 + +```bash +# Go 依赖 +go mod download + +# 前端依赖 +cd web +npm install +``` + +### 开发运行 + +```bash +wails dev +``` + +### 构建 + +```bash +wails build +``` + +## 项目结构 + +``` +ssq-desk/ +├─ app.go # Wails 应用结构 +├─ main.go # 应用入口 +├─ internal/ # 内部包 +│ ├─ api/ # API 层 +│ ├─ service/ # 业务逻辑层 +│ ├─ storage/ # 存储层 +│ └─ database/ # 数据库连接 +├─ web/ # 前端目录 +└─ docs/ # 文档目录 +``` + +## 文档 + +详细文档请查看 [docs](./docs/) 目录。 + +--- + +> 项目维护者:JueChen +> 创建时间:2026-01-07 diff --git a/app.go b/app.go new file mode 100644 index 0000000..d78e8ab --- /dev/null +++ b/app.go @@ -0,0 +1,460 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "ssq-desk/internal/api" + "ssq-desk/internal/database" + "ssq-desk/internal/module" + "ssq-desk/internal/service" + "ssq-desk/internal/storage/repository" + "time" +) + +// App 应用结构体 +type App struct { + ctx context.Context + moduleManager *module.Manager +} + +// NewApp 创建新的应用实例 +func NewApp() *App { + manager := module.NewManager() + + // 注册模块(模块之间相互独立,松散耦合) + // SSQ 查询模块 + ssqModule, _ := module.NewSsqModule() + if ssqModule != nil { + manager.Register(ssqModule) + } + + // 授权码模块 + authModule, _ := module.NewAuthModule() + if authModule != nil { + manager.Register(authModule) + } + + // 版本更新模块 + updateModule, _ := module.NewUpdateModule() + if updateModule != nil { + manager.Register(updateModule) + } + + return &App{ + moduleManager: manager, + } +} + +// initLogFile 初始化日志文件 +func initLogFile() { + // 获取用户配置目录 + appDataDir, err := os.UserConfigDir() + if err != nil { + // 如果获取失败,使用当前目录 + appDataDir = "." + } + + // 创建日志目录 + logDir := filepath.Join(appDataDir, "ssq-desk", "logs") + if err := os.MkdirAll(logDir, 0755); err != nil { + // 如果创建失败,使用当前目录 + logDir = "." + } + + // 生成日志文件名(按日期) + logFileName := fmt.Sprintf("ssq-desk-%s.log", time.Now().Format("20060102")) + logFilePath := filepath.Join(logDir, logFileName) + + // 打开日志文件(追加模式) + logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + // 如果打开失败,继续使用标准输出 + return + } + + // 设置日志输出到文件和控制台(同时输出) + if logFile != nil { + // 创建多写入器,同时写入文件和控制台 + multiWriter := &multiWriterType{ + file: logFile, + console: os.Stdout, + } + // 设置日志格式:日期时间 + 日志内容 + log.SetOutput(multiWriter) + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + log.Printf("[日志] 日志文件已初始化: %s", logFilePath) + } else { + // 如果无法创建日志文件,使用标准输出 + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + } +} + +// multiWriterType 多写入器,同时写入文件和控制台 +type multiWriterType struct { + file *os.File + console *os.File +} + +func (m *multiWriterType) Write(p []byte) (n int, err error) { + // 写入文件 + if m.file != nil { + m.file.Write(p) + } + // 写入控制台 + if m.console != nil { + m.console.Write(p) + } + return len(p), nil +} + +// startup 应用启动时调用 +func (a *App) startup(ctx context.Context) { + a.ctx = ctx + + // 初始化日志文件 + initLogFile() + + // 初始化 SQLite 本地数据库 + _, err := database.InitSQLite() + if err != nil { + log.Printf("[启动] SQLite 初始化失败: %v", err) + } else { + log.Printf("[启动] SQLite 初始化成功") + } + + // 初始化 MySQL 远程数据库(可选,用于数据同步) + _, err = database.InitMySQL() + if err != nil { + log.Printf("[启动] MySQL 连接失败: %v", err) + } else { + log.Printf("[启动] MySQL 连接成功") + } + + // 初始化所有模块 + if err := a.moduleManager.InitAll(ctx); err != nil { + log.Printf("[启动] 模块初始化失败: %v", err) + } else { + log.Printf("[启动] 模块初始化成功") + } + + // 启动所有模块 + if err := a.moduleManager.StartAll(ctx); err != nil { + log.Printf("[启动] 模块启动失败: %v", err) + } else { + log.Printf("[启动] 模块启动成功") + } +} + +// getSsqModule 获取 SSQ 模块 +func (a *App) getSsqModule() *module.SsqModule { + if m, ok := a.moduleManager.Get("ssq"); ok { + if ssqModule, ok := m.(*module.SsqModule); ok { + return ssqModule + } + } + return nil +} + +// getAuthModule 获取 Auth 模块 +func (a *App) getAuthModule() *module.AuthModule { + if m, ok := a.moduleManager.Get("auth"); ok { + if authModule, ok := m.(*module.AuthModule); ok { + return authModule + } + } + return nil +} + +// Greet 测试方法 +func (a *App) Greet(name string) string { + return "Hello " + name + ", It's show time!" +} + +// QueryHistory 查询历史数据 +func (a *App) QueryHistory(req api.QueryRequest) (map[string]interface{}, error) { + ssqModule := a.getSsqModule() + if ssqModule == nil { + _, err := api.NewSsqAPI() + return nil, err + } + + ssqAPI := ssqModule.SsqAPI() + if ssqAPI == nil { + _, err := api.NewSsqAPI() + return nil, err + } + + result, err := ssqAPI.QueryHistory(req) + if err != nil { + return nil, err + } + + // 转换为 map 返回(Wails 需要可序列化的类型) + return map[string]interface{}{ + "total": result.Total, + "summary": result.Summary, + "details": result.Details, + }, nil +} + +// ActivateLicense 激活授权码 +func (a *App) ActivateLicense(licenseCode string) (map[string]interface{}, error) { + authModule := a.getAuthModule() + if authModule == nil { + _, err := api.NewAuthAPI() + return nil, err + } + + authAPI := authModule.AuthAPI() + if authAPI == nil { + _, err := api.NewAuthAPI() + return nil, err + } + + return authAPI.ActivateLicense(licenseCode) +} + +// GetAuthStatus 获取授权状态 +func (a *App) GetAuthStatus() (map[string]interface{}, error) { + authModule := a.getAuthModule() + if authModule == nil { + _, err := api.NewAuthAPI() + return nil, err + } + + authAPI := authModule.AuthAPI() + if authAPI == nil { + _, err := api.NewAuthAPI() + return nil, err + } + + return authAPI.GetAuthStatus() +} + +// GetDeviceID 获取设备ID +func (a *App) GetDeviceID() (string, error) { + return service.GetDeviceID() +} + +// SyncData 同步数据 +func (a *App) SyncData() (map[string]interface{}, error) { + syncAPI, err := api.NewSyncAPI() + if err != nil { + return nil, err + } + return syncAPI.Sync() +} + +// GetSyncStatus 获取同步状态 +func (a *App) GetSyncStatus() (map[string]interface{}, error) { + syncAPI, err := api.NewSyncAPI() + if err != nil { + return nil, err + } + return syncAPI.GetSyncStatus() +} + +// GetDataStats 获取数据统计 +func (a *App) GetDataStats() (map[string]interface{}, error) { + ssqModule := a.getSsqModule() + if ssqModule == nil { + return nil, fmt.Errorf("SSQ 模块未初始化") + } + + repo, err := repository.NewSQLiteSsqRepository() + if err != nil { + return nil, err + } + + count, err := repo.Count() + if err != nil { + return nil, err + } + + latestIssue, err := repo.GetLatestIssue() + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "total_count": count, + "latest_issue": latestIssue, + }, nil +} + +// BackupData 备份数据 +func (a *App) BackupData() (map[string]interface{}, error) { + backupAPI := api.NewBackupAPI() + return backupAPI.Backup() +} + +// RestoreData 恢复数据 +func (a *App) RestoreData(backupPath string) (map[string]interface{}, error) { + backupAPI := api.NewBackupAPI() + return backupAPI.Restore(backupPath) +} + +// ListBackups 列出所有备份 +func (a *App) ListBackups() (map[string]interface{}, error) { + backupAPI := api.NewBackupAPI() + return backupAPI.ListBackups() +} + +// DownloadPackage 下载数据包 +func (a *App) DownloadPackage(downloadURL string) (map[string]interface{}, error) { + packageAPI, err := api.NewPackageAPI() + if err != nil { + return nil, err + } + return packageAPI.DownloadPackage(downloadURL) +} + +// ImportPackage 导入数据包 +func (a *App) ImportPackage(packagePath string) (map[string]interface{}, error) { + packageAPI, err := api.NewPackageAPI() + if err != nil { + return nil, err + } + return packageAPI.ImportPackage(packagePath) +} + +// CheckPackageUpdate 检查数据包更新 +func (a *App) CheckPackageUpdate(remoteURL string) (map[string]interface{}, error) { + packageAPI, err := api.NewPackageAPI() + if err != nil { + return nil, err + } + return packageAPI.CheckPackageUpdate(remoteURL) +} + +// ListLocalPackages 列出本地数据包 +func (a *App) ListLocalPackages() (map[string]interface{}, error) { + packageAPI, err := api.NewPackageAPI() + if err != nil { + return nil, err + } + return packageAPI.ListLocalPackages() +} + +// getUpdateModule 获取更新模块 +func (a *App) getUpdateModule() *module.UpdateModule { + if m, ok := a.moduleManager.Get("update"); ok { + if updateModule, ok := m.(*module.UpdateModule); ok { + return updateModule + } + } + return nil +} + +// CheckUpdate 检查更新 +func (a *App) CheckUpdate() (map[string]interface{}, error) { + updateModule := a.getUpdateModule() + if updateModule == nil { + return nil, fmt.Errorf("更新模块未初始化") + } + + updateAPI := updateModule.UpdateAPI() + if updateAPI == nil { + return nil, fmt.Errorf("更新 API 未初始化") + } + + return updateAPI.CheckUpdate() +} + +// GetCurrentVersion 获取当前版本号 +func (a *App) GetCurrentVersion() (map[string]interface{}, error) { + updateModule := a.getUpdateModule() + if updateModule == nil { + return nil, fmt.Errorf("更新模块未初始化") + } + + updateAPI := updateModule.UpdateAPI() + if updateAPI == nil { + return nil, fmt.Errorf("更新 API 未初始化") + } + + return updateAPI.GetCurrentVersion() +} + +// GetUpdateConfig 获取更新配置 +func (a *App) GetUpdateConfig() (map[string]interface{}, error) { + updateModule := a.getUpdateModule() + if updateModule == nil { + return nil, fmt.Errorf("更新模块未初始化") + } + + updateAPI := updateModule.UpdateAPI() + if updateAPI == nil { + return nil, fmt.Errorf("更新 API 未初始化") + } + + return updateAPI.GetUpdateConfig() +} + +// SetUpdateConfig 设置更新配置 +func (a *App) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) { + updateModule := a.getUpdateModule() + if updateModule == nil { + return nil, fmt.Errorf("更新模块未初始化") + } + + updateAPI := updateModule.UpdateAPI() + if updateAPI == nil { + return nil, fmt.Errorf("更新 API 未初始化") + } + + return updateAPI.SetUpdateConfig(autoCheckEnabled, checkIntervalMinutes, checkURL) +} + +// DownloadUpdate 下载更新包 +func (a *App) DownloadUpdate(downloadURL string) (map[string]interface{}, error) { + updateModule := a.getUpdateModule() + if updateModule == nil { + return nil, fmt.Errorf("更新模块未初始化") + } + + updateAPI := updateModule.UpdateAPI() + if updateAPI == nil { + return nil, fmt.Errorf("更新 API 未初始化") + } + + // 确保 context 已设置(用于事件推送) + if a.ctx != nil { + updateAPI.SetContext(a.ctx) + } + + return updateAPI.DownloadUpdate(downloadURL) +} + +// InstallUpdate 安装更新包 +func (a *App) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) { + updateModule := a.getUpdateModule() + if updateModule == nil { + return nil, fmt.Errorf("更新模块未初始化") + } + + updateAPI := updateModule.UpdateAPI() + if updateAPI == nil { + return nil, fmt.Errorf("更新 API 未初始化") + } + + return updateAPI.InstallUpdate(installerPath, autoRestart) +} + +// VerifyUpdateFile 验证更新文件哈希值 +func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) { + updateModule := a.getUpdateModule() + if updateModule == nil { + return nil, fmt.Errorf("更新模块未初始化") + } + + updateAPI := updateModule.UpdateAPI() + if updateAPI == nil { + return nil, fmt.Errorf("更新 API 未初始化") + } + + return updateAPI.VerifyUpdateFile(filePath, expectedHash, hashType) +} diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000..bfa0690 Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/docs/00-文档目录.md b/docs/00-文档目录.md new file mode 100644 index 0000000..87c18bb --- /dev/null +++ b/docs/00-文档目录.md @@ -0,0 +1,36 @@ +# 文档目录 + +## 1. 规范文档 +1.1 [开发规范](./01-规范/01-开发规范.md) +1.2 [Wails 绑定规范](./01-规范/02-接口规范.md) +1.3 [数据存储规范](./01-规范/03-数据库规范.md) +1.4 [工作事项推进日志规范](./01-规范/05-工作事项推进日志规范.md) + +## 1.5 数据库脚本 +1.5.1 [数据库初始化脚本](./01-数据库/2026-01-07-SSQ-init.sql) + +## 2. 技术文档 +2.1 [项目架构设计](./02-技术文档/项目架构设计.md) + +## 3. 业务模块 +3.1 [双色球查询业务](./03-业务模块/双色球查询业务.md) + +## 4. 功能迭代 +4.1 [双色球查询功能需求](./04-功能迭代/双色球查询功能需求.md) +4.2 [版本更新功能任务规划](./04-功能迭代/版本更新/任务规划.md) +4.3 [授权码功能设计](./04-功能迭代/授权码功能/授权码功能设计.md) + +## 5. 任务规划 +5.1 [任务规划](./任务规划.md) + +## 6. 问题处理 +待补充 + +## 7. 工作事项推进日志 +7.1 [工作事项推进日志](./TODO-LIST.md) +7.2 [项目开发状态](./PROJECT-STATUS.md) + +--- + +> 文档更新时间:2026-01-07 +> 文档维护者:JueChen diff --git a/docs/01-数据库/.gitkeep b/docs/01-数据库/.gitkeep new file mode 100644 index 0000000..62f27ff --- /dev/null +++ b/docs/01-数据库/.gitkeep @@ -0,0 +1 @@ +# 数据库文档目录 diff --git a/docs/01-数据库/2026-01-07-SSQ-init.sql b/docs/01-数据库/2026-01-07-SSQ-init.sql new file mode 100644 index 0000000..0f2e20a --- /dev/null +++ b/docs/01-数据库/2026-01-07-SSQ-init.sql @@ -0,0 +1,63 @@ +-- ============================================ +-- 双色球桌面应用数据库初始化脚本 +-- ============================================ +-- 创建时间:2026-01-07 +-- 维护者:JueChen +-- +-- 说明: +-- 1. 此脚本用于 MySQL 远程数据库初始化 +-- 2. 表结构由程序通过 GORM AutoMigrate 自动创建,此脚本作为参考 +-- 3. 时间字段由程序显式设置,不使用数据库默认值 +-- 4. 如需手动执行,请确保数据库已创建(如:ssq) +-- ============================================ + +-- ============================================ +-- 1. 双色球历史开奖数据表 (ssq_history) +-- ============================================ +-- 用途:存储双色球历史开奖数据 +-- 使用场景:MySQL 远程数据库和 SQLite 本地数据库 +CREATE TABLE IF NOT EXISTS `ssq_history` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `issue_number` VARCHAR(20) NOT NULL COMMENT '期号(如2025145)', + `open_date` DATE NULL COMMENT '开奖日期(允许为空)', + `red_ball_1` TINYINT NOT NULL COMMENT '红球1(范围:1-33)', + `red_ball_2` TINYINT NOT NULL COMMENT '红球2(范围:1-33)', + `red_ball_3` TINYINT NOT NULL COMMENT '红球3(范围:1-33)', + `red_ball_4` TINYINT NOT NULL COMMENT '红球4(范围:1-33)', + `red_ball_5` TINYINT NOT NULL COMMENT '红球5(范围:1-33)', + `red_ball_6` TINYINT NOT NULL COMMENT '红球6(范围:1-33)', + `blue_ball` TINYINT NOT NULL COMMENT '蓝球(范围:1-16)', + `created_at` DATETIME NOT NULL COMMENT '创建时间(由程序设置)', + `updated_at` DATETIME NOT NULL COMMENT '更新时间(由程序设置)', + PRIMARY KEY (`id`), + INDEX `idx_issue_number` (`issue_number`), + INDEX `idx_open_date` (`open_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='双色球历史开奖数据表(用于MySQL远程数据库和SQLite本地数据库)'; + +CREATE TABLE IF NOT EXISTS `sys_authorization_code` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `license_code` VARCHAR(100) NOT NULL COMMENT '授权码(唯一,用于标识授权)', + `device_id` VARCHAR(100) NOT NULL COMMENT '设备ID(MD5哈希,基于主机名、用户目录、操作系统生成,用于设备绑定)', + `activated_at` DATETIME NOT NULL COMMENT '激活时间(授权激活的时间)', + `expires_at` DATETIME NULL COMMENT '过期时间(可选,NULL表示永不过期)', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(1:有效 0:无效)', + `created_at` DATETIME NOT NULL COMMENT '创建时间(由程序设置)', + `updated_at` DATETIME NOT NULL COMMENT '更新时间(由程序设置)', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_license_code` (`license_code`), + INDEX `idx_device_id` (`device_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='授权信息表(用于MySQL远程数据库和SQLite本地数据库)'; + +CREATE TABLE IF NOT EXISTS `sys_version` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `version` VARCHAR(20) NOT NULL COMMENT '版本号(语义化版本,如1.0.0)', + `download_url` VARCHAR(500) NULL COMMENT '下载地址(更新包下载URL)', + `changelog` TEXT NULL COMMENT '更新日志(Markdown格式)', + `force_update` TINYINT NOT NULL DEFAULT 0 COMMENT '是否强制更新(1:是 0:否)', + `release_date` DATE NULL COMMENT '发布日期', + `created_at` DATETIME NOT NULL COMMENT '创建时间(由程序设置)', + `updated_at` DATETIME NOT NULL COMMENT '更新时间(由程序设置)', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_version` (`version`), + INDEX `idx_release_date` (`release_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='版本信息表(用于MySQL远程数据库,存储应用版本发布信息)'; diff --git a/docs/01-规范/01-开发规范.md b/docs/01-规范/01-开发规范.md new file mode 100644 index 0000000..d85974c --- /dev/null +++ b/docs/01-规范/01-开发规范.md @@ -0,0 +1,68 @@ +# 开发规范 + +## 1. 技术栈 +- **前端**:Vue 3 + Arco Design + TypeScript +- **后端**:Go + Wails +- **远程数据库**:MySQL(历史数据源) +- **本地存储**:SQLite(本地缓存)+ 文件存储(配置/离线数据包) + +## 2. 代码风格 +- 统一使用 UTF-8 编码 +- 前端遵循 ESLint 规则 +- Go 代码遵循 `gofmt` 格式化 + +## 3. 命名规范 + +### 3.1 Go 后端 +- 类型/结构体:大驼峰 `XxxService` +- 方法/变量:小驼峰 `GetList()` +- 常量:全大写下划线 `DEFAULT_PAGE_SIZE` + +### 3.2 前端 +- 组件:大驼峰 `XxxList.vue` +- 方法/变量:小驼峰 `getList()` +- 常量:全大写下划线 `DEFAULT_PAGE_SIZE` + +## 4. Wails 绑定规范 +- Go 结构体导出方法供前端调用 +- 方法参数不超过3个,超过时封装为结构体 +- 方法名使用动词开头:`GetXxx`、`SaveXxx`、`DeleteXxx` + +## 5. Arco Design 使用规范 +- 优先使用 Arco 提供的组件和样式 +- 避免过度自定义样式,保持主题兼容性 +- 不使用 title 属性(如 ``) +- 颜色规范: + - 红球数字:`#F53F3F`(Arco 红色) + - 蓝球数字:`#165DFF`(Arco 蓝色) + - 未匹配数字:默认黑色 + +## 6. 代码质量要求 +- 架构优良、性能良好、结构简单 +- 方便维护,减少 AI 味 +- 新增文件代码签名:`JueChen` + +### 6.1 精准控制原则 +- **精准定位问题**:找到问题的根本原因,而非到处添加防御性代码 +- **精准设置样式**:只针对真正需要控制的元素设置样式,避免大量重复的 `overflow-x: hidden` 等防御性样式 +- **可维护性优先**:代码改动时能明确知道哪个地方控制了什么,避免维护困难 + +### 6.2 主动性编程原则 +- **主动解决问题**:找到问题的根源并修复,而不是用 `!important` 或大量覆盖样式来掩盖问题 +- **主动思考设计**:考虑布局和样式设计的合理性,而非被动地添加防御性代码 +- **主动优化代码**:定期审查代码,移除不必要的防御性代码,保持代码简洁 + +### 6.3 避免过度防御 +- **不滥用 `!important`**:只在必要时使用,优先通过提高选择器优先级解决问题 +- **不大量使用 `overflow-x: hidden`**:只在真正需要的地方使用,通常是 `body` 级别作为最后防线 +- **不过度设置 `width: 100%`**:只在需要明确控制宽度的元素上设置 +- **不重复设置相同样式**:避免在多个层级重复设置相同的样式属性 + +## 7. 版本控制 +- Commit message:``,使用中文 +- 提交前自检(lint、功能测试) + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/01-规范/02-接口规范.md b/docs/01-规范/02-接口规范.md new file mode 100644 index 0000000..cc26f72 --- /dev/null +++ b/docs/01-规范/02-接口规范.md @@ -0,0 +1,53 @@ +# Wails 绑定规范 + +## 1. 概述 +Wails 通过 context 绑定 Go 方法供前端调用,无需 HTTP API。 + +## 2. Go 后端绑定 + +### 2.1 结构体定义 +```go +type App struct { + ctx context.Context +} + +func NewApp() *App { + return &App{} +} +``` + +### 2.2 方法绑定 +- 导出方法(首字母大写)自动绑定 +- 方法参数不超过3个,超过时使用结构体 +- 返回错误统一使用 `error` 类型 + +### 2.3 命名规范 +- 查询:`GetXxx()`、`ListXxx()` +- 新增:`CreateXxx()`、`SaveXxx()` +- 更新:`UpdateXxx()` +- 删除:`DeleteXxx()` + +## 3. 前端调用 + +### 3.1 调用方式 +```typescript +// 导入绑定的方法 +import { GetXxx, SaveXxx } from '@/wailsjs/go/main/App' + +// 调用 +const data = await GetXxx() +``` + +### 3.2 错误处理 +- 统一使用 try-catch 处理 +- 错误信息展示给用户 + +## 4. 参数规范 +- 简单参数直接传递 +- 复杂参数使用结构体/对象 +- 字段命名使用小驼峰 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/01-规范/03-数据库规范.md b/docs/01-规范/03-数据库规范.md new file mode 100644 index 0000000..88062f4 --- /dev/null +++ b/docs/01-规范/03-数据库规范.md @@ -0,0 +1,88 @@ +# 数据存储规范 + +## 1. 存储架构 + +### 1.1 远程数据库(MySQL) +- **用途**:完整历史数据源 +- **地址**:39.99.243.191:3306 +- **账号**:u_ssq +- **密码**:u_ssq@260106 +- **连接方式**:需要时连接,支持离线模式 + +### 1.2 本地数据库(SQLite) +- **用途**:本地数据缓存、离线查询支持 +- **位置**:应用数据目录 +- **同步策略**:增量同步,定期更新 + +### 1.3 文件存储 +- **配置文件**:应用配置、用户偏好 +- **离线数据包**:完整历史数据导出/导入 +- **缓存文件**:临时数据 + +### 1.4 前端存储 +- **LocalStorage**:临时数据、用户偏好、UI 状态 + +## 2. 数据库设计规范 + +### 2.1 表名规范 +- 小写字母,单词间下划线:`ssq_history`、`query_cache` + +### 2.2 字段规范 +- 小写字母,单词间下划线:`issue_number`、`red_ball_1`、`created_at` +- 主键统一使用 `id`(INT 或 BIGINT) +- 时间字段:`created_at`、`updated_at`(DATETIME) + +### 2.3 数据类型规范 +- **期号**:`VARCHAR(20)`,如 "2025145" +- **球号**:`TINYINT`,范围 1-33(红球)或 1-16(蓝球) +- **日期**:`DATE` 或 `DATETIME` + +### 2.4 索引规范 +- 主键索引:`PRIMARY KEY (id)` +- 查询索引:`INDEX idx_issue_number (issue_number)` +- 日期索引:`INDEX idx_open_date (open_date)` + +## 3. 数据同步规范 + +### 3.1 同步策略 +- **增量同步**:基于 `id` 或 `issue_number` 增量更新 +- **全量同步**:首次安装或数据修复 +- **同步时机**:应用启动检查、手动触发、定时任务 + +### 3.2 数据校验 +- 期号唯一性校验 +- 球号范围校验(红球 1-33,蓝球 1-16) +- 数据完整性校验 + +### 3.3 错误处理 +- 网络错误:使用本地缓存 +- 数据冲突:以远程为准或用户选择 +- 同步失败:记录日志,下次重试 + +## 4. Go 结构体映射 + +### 4.1 ORM 工具 +- 使用 `gorm` 进行 ORM 映射 +- MySQL 和 SQLite 使用相同模型结构 + +### 4.2 结构体规范 +```go +type SsqHistory struct { + ID int `gorm:"primaryKey" json:"id"` + IssueNumber string `gorm:"type:varchar(20);not null;index" json:"issue_number"` + OpenDate *time.Time `gorm:"type:date" json:"open_date"` + RedBall1 int `gorm:"type:tinyint;not null" json:"red_ball_1"` + // ... 其他字段 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} +``` + +### 4.3 表名映射 +- 实现 `TableName()` 方法指定表名 +- 或使用 `gorm` 默认命名规则 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/01-规范/05-工作事项推进日志规范.md b/docs/01-规范/05-工作事项推进日志规范.md new file mode 100644 index 0000000..1aada47 --- /dev/null +++ b/docs/01-规范/05-工作事项推进日志规范.md @@ -0,0 +1,43 @@ +# 工作事项推进日志规范 + +## 1. 日志格式 + +### 1.1 基本信息 +- 事项标题 +- 创建时间 +- 状态(待办/进行中/已完成/已取消) +- 优先级(高/中/低) + +### 1.2 详细内容 +- 需求描述 +- 实现方案 +- 进展情况 +- 遇到的问题 +- 解决方案 + +### 1.3 时间记录 +- 创建时间 +- 开始时间 +- 完成时间 +- 重要节点时间 + +## 2. 更新规范 + +### 2.1 更新频率 +- 重要事项每日更新 +- 普通事项按进度更新 +- 完成后及时标记 + +### 2.2 更新内容 +- 记录关键进度节点 +- 记录遇到的问题和解决方案 +- 记录重要的决策和变更 + +## 3. 日志位置 +- 主要日志:`docs/TODO-LIST.md` +- 功能迭代详细日志:`docs/04-功能迭代/{功能名称}/` + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/02-技术文档/.gitkeep b/docs/02-技术文档/.gitkeep new file mode 100644 index 0000000..cef6d86 --- /dev/null +++ b/docs/02-技术文档/.gitkeep @@ -0,0 +1 @@ +# 技术文档目录 diff --git a/docs/02-技术文档/模块化架构设计.md b/docs/02-技术文档/模块化架构设计.md new file mode 100644 index 0000000..c13d8b9 --- /dev/null +++ b/docs/02-技术文档/模块化架构设计.md @@ -0,0 +1,161 @@ +# 模块化架构设计 + +## 1. 设计理念 + +采用模块化设计,各功能模块相互独立、松散耦合,便于维护和扩展。 + +### 1.1 模块独立性 +- 每个功能模块(ssq查询、授权码、版本更新)独立实现 +- 模块之间不直接依赖,通过模块管理器统一管理 +- 模块可以独立开发、测试、部署 + +### 1.2 松耦合设计 +- 模块通过接口定义,不依赖具体实现 +- 模块管理器负责模块的注册、初始化、启动、停止 +- App 层只依赖模块管理器,不直接依赖具体模块 + +## 2. 模块接口 + +### 2.1 Module 接口 + +```go +type Module interface { + Name() string // 模块名称 + Init(ctx context.Context) error // 初始化 + Start(ctx context.Context) error // 启动 + Stop(ctx context.Context) error // 停止 + GetAPI() interface{} // 获取 API 接口 +} +``` + +### 2.2 BaseModule 基础实现 + +提供模块的基础实现,各模块可以继承并扩展。 + +## 3. 模块管理器 + +### 3.1 Manager 功能 +- 模块注册:`Register(module Module)` +- 模块获取:`Get(name string)` +- 批量操作:`InitAll()`, `StartAll()`, `StopAll()` +- API 收集:`GetAPIs()` + +### 3.2 使用方式 + +```go +manager := module.NewManager() + +// 注册模块 +ssqModule, _ := module.NewSsqModule() +manager.Register(ssqModule) + +authModule, _ := module.NewAuthModule() +manager.Register(authModule) + +// 初始化所有模块 +manager.InitAll(ctx) + +// 启动所有模块 +manager.StartAll(ctx) +``` + +## 4. 功能模块 + +### 4.1 SSQ 查询模块 + +**模块名称**: `ssq` + +**功能**: 双色球历史数据查询 + +**API**: `*api.SsqAPI` + +**实现文件**: `internal/module/ssq_module.go` + +### 4.2 授权码模块 + +**模块名称**: `auth` + +**功能**: 设备授权码验证和激活 + +**API**: `*api.AuthAPI` + +**实现文件**: `internal/module/auth_module.go` + +**启动行为**: 自动检查授权状态 + +### 4.3 版本更新模块 + +**模块名称**: `update` + +**功能**: 版本更新检查、下载、安装 + +**API**: 待实现 + +**实现文件**: `internal/module/update_module.go` + +**状态**: 占位符,待实现 + +## 5. 目录结构 + +``` +internal/ +├─ module/ # 模块层 +│ ├─ module.go # 模块接口定义 +│ ├─ manager.go # 模块管理器 +│ ├─ ssq_module.go # SSQ 查询模块 +│ ├─ auth_module.go # 授权码模块 +│ └─ update_module.go # 版本更新模块(待实现) +├─ api/ # API 层 +│ ├─ ssq_api.go # SSQ API +│ └─ auth_api.go # Auth API +├─ service/ # 服务层 +└─ storage/ # 存储层 +``` + +## 6. 模块扩展 + +### 6.1 添加新模块 + +1. 实现 `Module` 接口 +2. 创建模块文件(如 `xxx_module.go`) +3. 在 `NewApp()` 中注册模块 + +```go +xxxModule, _ := module.NewXxxModule() +manager.Register(xxxModule) +``` + +### 6.2 模块生命周期 + +``` +注册 → 初始化 → 启动 → 运行 → 停止 +``` + +- **注册**: 模块创建并注册到管理器 +- **初始化**: 模块初始化资源(数据库、配置等) +- **启动**: 模块启动服务(检查授权、启动定时任务等) +- **运行**: 模块提供服务 +- **停止**: 模块清理资源 + +## 7. 优势 + +### 7.1 可维护性 +- 模块独立,修改不影响其他模块 +- 清晰的模块边界和职责 + +### 7.2 可扩展性 +- 新增功能只需添加新模块 +- 模块可以独立开发和测试 + +### 7.3 可测试性 +- 模块可以独立测试 +- 便于 Mock 和单元测试 + +### 7.4 松耦合 +- 模块之间不直接依赖 +- 通过接口和管理器解耦 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/02-技术文档/项目架构设计.md b/docs/02-技术文档/项目架构设计.md new file mode 100644 index 0000000..c4f12b4 --- /dev/null +++ b/docs/02-技术文档/项目架构设计.md @@ -0,0 +1,247 @@ +# 项目架构设计 + +## 1. 项目概述 + +ssq-desk 是基于 Wails 框架开发的桌面应用,提供双色球历史数据查询和统计分析功能。 + +--- + +## 2. 技术架构 + +### 2.1 整体架构 + +``` +┌─────────────────────────────────────┐ +│ 前端 (Vue 3 + Arco) │ +│ ┌────────────┐ ┌──────────────┐ │ +│ │ 查询界面 │ │ 结果展示 │ │ +│ └────────────┘ └──────────────┘ │ +└─────────────────────────────────────┘ + ↕ Wails Context +┌─────────────────────────────────────┐ +│ 后端 (Go + Wails) │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ API层 │→ │ Service层 │ │ +│ └──────────┘ └──────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────┐ │ +│ │ Storage层 (Repository) │ │ +│ └──────────────────────────────┘ │ +└─────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────┐ +│ 数据层 │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ MySQL │ │ SQLite │ │ +│ │ (远程) │ │ (本地缓存) │ │ +│ └──────────┘ └──────────────┘ │ +└─────────────────────────────────────┘ +``` + +### 2.2 模块化架构 + +``` +┌─────────────────────────────────────┐ +│ App (app.go) │ +│ 模块管理器统一管理 │ +└─────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────┐ +│ Module Manager │ +│ ┌────────┐ ┌────────┐ ┌──────┐ │ +│ │ SSQ │ │ Auth │ │Update│ │ +│ │ Module │ │ Module │ │Module│ │ +│ └────────┘ └────────┘ └──────┘ │ +└─────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────┐ +│ API Layer │ +│ ┌────────┐ ┌────────┐ │ +│ │SSQ API │ │Auth API│ │ +│ └────────┘ └────────┘ │ +└─────────────────────────────────────┘ +``` + +### 2.3 目录结构 + +``` +ssq-desk/ +├─ app.go # Wails 应用结构,模块管理器 +├─ main.go # 应用入口 +├─ internal/ # 内部包 +│ ├─ module/ # 模块层(新增) +│ │ ├─ module.go # 模块接口定义 +│ │ ├─ manager.go # 模块管理器 +│ │ ├─ ssq_module.go # SSQ 查询模块 +│ │ ├─ auth_module.go # 授权码模块 +│ │ └─ update_module.go # 版本更新模块 +│ ├─ api/ # API 层(Wails 绑定接口) +│ │ ├─ ssq_api.go # 双色球查询 API +│ │ └─ auth_api.go # 授权码 API +│ ├─ service/ # 业务逻辑层 +│ │ ├─ query_service.go # 查询服务 +│ │ └─ auth_service.go # 授权服务 +│ ├─ storage/ # 存储层 +│ │ ├─ models/ # 数据模型 +│ │ │ ├─ ssq_history.go # 历史数据模型 +│ │ │ └─ authorization.go # 授权模型 +│ │ └─ repository/ # 数据访问层 +│ │ ├─ mysql_repo.go # MySQL 仓库 +│ │ ├─ sqlite_repo.go # SQLite 仓库 +│ │ └─ auth_repository.go # 授权仓库 +│ └─ database/ # 数据库连接 +│ ├─ mysql.go # MySQL 连接 +│ └─ sqlite.go # SQLite 连接 +├─ web/ # 前端目录 +│ ├─ src/ +│ │ ├─ views/ # 页面组件 +│ │ │ └─ query/ # 查询页面 +│ │ ├─ components/ # 公共组件 +│ │ └─ wailsjs/ # Wails 生成的文件 +│ └─ package.json +└─ wails.json # Wails 配置 +``` + +--- + +## 3. 数据流设计 + +### 3.1 查询流程 + +``` +用户输入查询条件 + ↓ +前端调用 Wails 绑定方法 + ↓ +API 层接收参数 + ↓ +Service 层处理业务逻辑 + ↓ +Repository 层查询数据 + ├─ 优先查询 SQLite(本地缓存) + └─ 如需更新,查询 MySQL(远程) + ↓ +返回结果给前端 + ↓ +前端展示结果(分类统计 + 详细列表) +``` + +### 3.2 数据同步流程 + +``` +应用启动/手动触发 + ↓ +检查远程数据更新 + ↓ +增量同步(基于 issue_number) + ↓ +写入 SQLite 本地缓存 + ↓ +更新同步状态和日志 +``` + +--- + +## 4. 核心功能模块 + +### 4.1 模块化设计 + +采用模块化架构,各功能模块相互独立、松散耦合: + +- **模块接口**:统一的 `Module` 接口 +- **模块管理器**:统一管理模块的注册、初始化、启动、停止 +- **模块独立性**:各模块可独立开发、测试、部署 + +详细设计参见:[模块化架构设计](./模块化架构设计.md) + +### 4.2 SSQ 查询模块 +- **模块名称**:`ssq` +- **功能**:根据红球、蓝球条件查询历史数据 +- **输入**:6个红球 + 1个蓝球 + 蓝球筛选范围 +- **输出**:分类统计 + 详细记录列表 +- **特性**:支持部分匹配、颜色标识匹配结果 + +### 4.3 授权码模块 +- **模块名称**:`auth` +- **功能**:设备授权码验证、激活状态管理 +- **存储**:SQLite 本地数据库 +- **启动行为**:应用启动时自动检查授权状态 + +### 4.4 版本更新模块 +- **模块名称**:`update` +- **功能**:版本更新检查、下载、安装 +- **状态**:待实现 + +### 4.5 数据同步模块(待模块化) +- **功能**:MySQL 与 SQLite 数据同步 +- **策略**:增量同步、全量同步、手动刷新 +- **错误处理**:网络异常、数据冲突处理 + +--- + +## 5. 性能优化策略 + +### 5.1 数据查询 +- **本地优先**:优先使用 SQLite 本地缓存 +- **分页加载**:查询结果分页显示 +- **索引优化**:为常用查询字段建立索引 + +### 5.2 数据同步 +- **异步同步**:后台异步同步,不阻塞主流程 +- **增量更新**:仅同步增量数据,减少网络传输 +- **智能调度**:根据数据更新频率调整同步策略 + +### 5.3 前端优化 +- **虚拟滚动**:大量数据使用虚拟滚动 +- **懒加载**:按需加载详细数据 +- **缓存策略**:合理使用 LocalStorage 缓存 + +--- + +## 6. 安全设计 + +### 6.1 数据安全 +- **连接加密**:MySQL 连接使用 TLS/SSL +- **密码管理**:数据库密码加密存储 +- **本地数据**:敏感数据本地加密存储 + +### 6.2 授权验证 +- **设备绑定**:授权码与设备绑定 +- **激活验证**:启动时验证激活状态 +- **离线模式**:授权失效时限制功能 + +--- + +## 7. 错误处理 + +### 7.1 网络错误 +- **连接失败**:自动切换到离线模式 +- **超时处理**:设置合理的超时时间 +- **重试机制**:自动重试失败的请求 + +### 7.2 数据错误 +- **数据校验**:入库前校验数据完整性 +- **异常处理**:捕获并记录异常日志 +- **用户提示**:友好的错误提示信息 + +--- + +## 8. 扩展性设计 + +### 8.1 功能扩展 +- **模块化设计**:功能模块独立,易于扩展(已实现) + - 新增功能只需实现 `Module` 接口并注册 + - 模块之间不直接依赖,通过管理器解耦 + - 支持模块的独立开发、测试、部署 +- **接口抽象**:定义清晰的接口,便于替换实现 +- **配置驱动**:支持配置文件扩展功能 + +### 8.2 数据扩展 +- **多数据源**:支持多种数据源接入 +- **数据格式**:支持多种数据格式导入 +- **统计分析**:预留统计分析扩展接口 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/03-业务模块/.gitkeep b/docs/03-业务模块/.gitkeep new file mode 100644 index 0000000..e5678de --- /dev/null +++ b/docs/03-业务模块/.gitkeep @@ -0,0 +1 @@ +# 业务模块目录 diff --git a/docs/03-业务模块/双色球查询业务.md b/docs/03-业务模块/双色球查询业务.md new file mode 100644 index 0000000..f174d9b --- /dev/null +++ b/docs/03-业务模块/双色球查询业务.md @@ -0,0 +1,132 @@ +# 双色球查询业务模块 + +## 1. 业务概述 + +提供双色球历史开奖数据查询功能,支持按红球、蓝球条件查询,并展示匹配结果的分类统计和详细记录。 + +--- + +## 2. 业务规则 + +### 2.1 查询规则 + +#### 红球规则 +- 红球范围:1-33 +- 输入数量:可输入 0-6 个红球 +- 匹配方式:完全匹配(必须6个红球全部在查询条件中) + +#### 蓝球规则 +- 蓝球范围:1-16 +- 输入方式:单个蓝球输入框 +- 筛选方式:复选框筛选范围(可多选) + +#### 查询逻辑 +- **完全匹配**:查询结果必须包含所有输入的红球和蓝球 +- **部分匹配**:支持只输入部分红球(匹配到即显示) +- **蓝球筛选**:根据勾选的蓝球范围进行过滤 + +### 2.2 匹配统计规则 + +#### 统计分类 +- 6个红球 + 1个蓝球:完全匹配 +- 6个红球:红球全部匹配,蓝球不匹配 +- 5个红球 + 1个蓝球:5个红球匹配且蓝球匹配 +- 5个红球:5个红球匹配,蓝球不匹配 +- ... 依此类推到 0个红球 + +#### 匹配计数 +- 每条历史记录根据匹配情况归类到对应分类 +- 统计每个分类的出现次数 +- 支持扩展显示低匹配度结果(≤3个红球) + +--- + +## 3. 数据展示规则 + +### 3.1 颜色标识 +- **匹配的红球**:红色显示(#F53F3F) +- **匹配的蓝球**:蓝色显示(#165DFF) +- **未匹配的数字**:黑色显示(默认) + +### 3.2 结果分类展示 + +#### 左侧汇总列表 +- 显示各匹配级别的统计次数 +- 每个汇总项提供 `[显示历史开奖]` 链接 +- 点击链接,右侧显示对应的详细记录 + +#### 右侧详情列表 +- 显示期号、红球号码、蓝球号码 +- 支持扩展查询:查看前后 n 期数据 +- 支持扩展显示:显示低匹配度结果 + +--- + +## 4. 数据来源 + +### 4.1 远程数据库(MySQL) +- **数据表**:`ssq_history` +- **数据内容**:完整历史开奖数据 +- **更新频率**:定期更新(新增期号) + +### 4.2 本地缓存(SQLite) +- **数据来源**:从 MySQL 同步 +- **用途**:离线查询、快速查询 +- **同步策略**:增量同步、手动刷新 + +--- + +## 5. 业务场景 + +### 5.1 查询场景 +1. **完整查询**:输入6个红球+1个蓝球,查看完全匹配记录 +2. **部分查询**:输入部分红球,查看匹配情况 +3. **统计分析**:查看历史中不同匹配级别的出现频率 + +### 5.2 数据维护场景 +1. **数据同步**:从远程数据库同步最新数据 +2. **离线使用**:本地缓存数据,支持离线查询 +3. **数据备份**:导出离线数据包,备份数据 + +--- + +## 6. 业务流程 + +### 6.1 查询流程 + +``` +用户输入查询条件 + ↓ +验证输入有效性(球号范围、数量) + ↓ +执行查询(本地优先) + ↓ +匹配结果并分类统计 + ↓ +展示汇总列表(左侧) + ↓ +用户点击汇总项 + ↓ +展示详细记录(右侧) +``` + +### 6.2 数据同步流程 + +``` +触发同步(启动/手动/定时) + ↓ +连接远程数据库 + ↓ +检查数据更新(基于 issue_number) + ↓ +增量同步新数据 + ↓ +更新本地 SQLite + ↓ +记录同步日志 +``` + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/04-功能迭代/.gitkeep b/docs/04-功能迭代/.gitkeep new file mode 100644 index 0000000..2415f60 --- /dev/null +++ b/docs/04-功能迭代/.gitkeep @@ -0,0 +1 @@ +# 功能迭代目录 diff --git a/docs/04-功能迭代/双色球查询功能需求.md b/docs/04-功能迭代/双色球查询功能需求.md new file mode 100644 index 0000000..54f9e2f --- /dev/null +++ b/docs/04-功能迭代/双色球查询功能需求.md @@ -0,0 +1,215 @@ +# 双色球查询功能需求文档 + +## 1. 功能概述 + +双色球桌面查询应用,提供历史开奖数据查询、统计分析等功能。 + +--- + +## 2. 查询功能 + +### 2.1 查询条件 + +#### 2.1.1 红球输入 +- 6个红色球输入框,依次为: + - 红球1 + - 红球2 + - 红球3 + - 红球4 + - 红球5 + - 红球6 + +#### 2.1.2 蓝球输入 +- 1个蓝色球输入框:蓝球 + +#### 2.1.3 蓝球筛选 +- 17个复选框: + - 蓝球1 至 蓝球16(16个选项) + - 全选复选框(1个) + +#### 2.1.4 操作按钮 +- **查询按钮**:执行查询 +- **重置按钮**:清空所有输入,默认勾选全选复选框 + +### 2.2 查询逻辑 + +- 根据输入的6个红球和1个蓝球进行匹配查询 +- 支持部分匹配(如只输入部分红球) +- 蓝球筛选:根据勾选的蓝球范围进行过滤 + +### 2.3 查询结果展示 + +#### 2.3.1 结果列表 +显示字段: +- 期数 +- 红球1 +- 红球2 +- 红球3 +- 红球4 +- 红球5 +- 红球6 +- 蓝球 + +#### 2.3.2 数字颜色标识 +- **匹配的红球**:红色数字显示 +- **匹配的蓝球**:蓝色数字显示 +- **未匹配的数字**:黑色数字显示 + +#### 2.3.3 查询结果分类 + +**左侧汇总区域**: +- 开出过6个红球与1个蓝球:X次 +- 开出过6个红球:X次 +- 开出过5个红球与1个蓝球:X次 +- 开出过5个红球:X次 +- 开出过4个红球与1个蓝球:X次 +- 开出过4个红球:X次 +- 开出过3个红球与1个蓝球:X次 +- 开出过3个红球:X次 +- 开出过2个红球与1个蓝球:X次 +- 开出过2个红球:X次 +- 开出过1个红球与1个蓝球:X次 +- 开出过1个红球:X次 +- 开出过0个红球与1个蓝球:X次 +- 开出过0个红球:X次 +- 每个汇总项提供 `[显示历史开奖]` 链接 + +**右侧详情区域**: +- 点击左侧汇总项的 `[显示历史开奖]`,右侧显示对应的详细开奖记录 +- 每条记录显示:期号、红球号码、蓝球号码 +- 支持扩展查询:`[再扩展查询对比结果上下n期]` 按钮 + +**扩展功能**: +- 底部提供:`[扩展显示≤3个红球的对比结果]` 按钮 + +--- + +## 3. 数据维护功能 + +### 3.1 数据同步 +- 从远程数据库同步历史数据 +- 支持增量更新 +- 数据校验和去重 + +### 3.2 数据管理 +- 查看本地数据统计 +- 手动刷新数据 +- 数据备份与恢复 + +--- + +## 4. 其他功能 + +### 4.1 版本更新 +- 检查更新 +- 自动/手动更新 +- 更新日志展示 + +### 4.2 离线数据 +- 离线数据包管理 +- 离线数据包更新 +- 数据包下载与导入 + +### 4.3 授权管理 +- 设备授权码管理 +- 激活状态验证 +- 授权信息显示 + +--- + +## 5. 数据库设计 + +### 5.1 数据库信息 +- **地址**:39.99.243.191:3306 +- **账号**:u_ssq +- **密码**:u_ssq@260106 +- **数据库名**:ssq_dev + +### 5.2 数据表结构 + +#### ssq_history(双色球历史开奖数据表) + +```sql +CREATE TABLE IF NOT EXISTS `ssq_history` ( + `id` INT NOT NULL COMMENT '主键ID', + `issue_number` VARCHAR(20) NOT NULL COMMENT '期号(如2025145)', + `open_date` DATE NULL COMMENT '开奖日期(允许为空)', + `red_ball_1` TINYINT NOT NULL COMMENT '红球1', + `red_ball_2` TINYINT NOT NULL COMMENT '红球2', + `red_ball_3` TINYINT NOT NULL COMMENT '红球3', + `red_ball_4` TINYINT NOT NULL COMMENT '红球4', + `red_ball_5` TINYINT NOT NULL COMMENT '红球5', + `red_ball_6` TINYINT NOT NULL COMMENT '红球6', + `blue_ball` TINYINT NOT NULL COMMENT '蓝球', + `created_at` DATETIME NOT NULL COMMENT '创建时间', + `updated_at` DATETIME NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='双色球历史开奖数据'; +``` + +#### 字段说明 +- `id`:主键,唯一标识 +- `issue_number`:期号,格式如 "2025145" +- `open_date`:开奖日期,可为空 +- `red_ball_1` 至 `red_ball_6`:6个红球号码(1-33) +- `blue_ball`:蓝球号码(1-16) +- `created_at`:记录创建时间 +- `updated_at`:记录更新时间 + +--- + +## 6. 界面设计要求 + +### 6.1 布局 +- 顶部:查询条件区域 +- 中间左侧:查询汇总列表 +- 中间右侧:查询结果详情 +- 底部:扩展功能按钮 + +### 6.2 样式规范 +- 使用 Arco Design 组件库 +- 红球数字:红色标识(#F53F3F) +- 蓝球数字:蓝色标识(#165DFF) +- 未匹配数字:黑色(默认) +- 保持主题兼容性 + +--- + +## 7. 技术实现 + +### 7.1 技术栈 +- **前端**:Vue 3 + Arco Design + TypeScript +- **后端**:Go + Wails +- **数据库**:MySQL(远程)+ SQLite(本地缓存) + +### 7.2 数据存储策略 +- 远程 MySQL:完整历史数据 +- 本地 SQLite:缓存查询结果,离线支持 + +### 7.3 性能优化 +- 分页加载查询结果 +- 本地数据缓存 +- 异步数据同步 + +--- + +## 8. 开发优先级 + +### Phase 1:核心查询功能 +1. 查询条件界面 +2. 基础查询功能 +3. 结果展示(列表+分类) + +### Phase 2:数据管理 +1. 数据同步功能 +2. 本地数据管理 + +### Phase 3:其他功能 +1. 版本更新 +2. 离线数据包 +3. 授权管理 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/04-功能迭代/授权码功能/授权码功能设计.md b/docs/04-功能迭代/授权码功能/授权码功能设计.md new file mode 100644 index 0000000..da1eb16 --- /dev/null +++ b/docs/04-功能迭代/授权码功能/授权码功能设计.md @@ -0,0 +1,114 @@ +# 授权码功能设计 + +## 1. 功能概述 + +授权码功能用于设备激活和授权验证,确保应用在授权设备上使用。 + +## 2. 核心功能 + +### 2.1 设备标识 +- 基于主机名、用户目录、操作系统生成设备ID +- 使用 MD5 哈希确保唯一性和稳定性 +- 设备ID与授权码绑定 + +### 2.2 授权码验证 +- 授权码输入和格式验证 +- 设备绑定(授权码与设备ID关联) +- 授权状态存储(SQLite 本地数据库) + +### 2.3 激活验证 +- 应用启动时自动验证授权状态 +- 授权信息展示 +- 授权失效处理 + +## 3. 数据模型 + +### 3.1 Authorization 表结构 + +```sql +CREATE TABLE IF NOT EXISTS `sys_authorization_code` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, -- 主键ID + `license_code` VARCHAR(100) NOT NULL, -- 授权码(唯一) + `device_id` VARCHAR(100) NOT NULL, -- 设备ID(MD5哈希) + `activated_at` DATETIME NOT NULL, -- 激活时间 + `expires_at` DATETIME NULL, -- 过期时间(可选,NULL表示永不过期) + `status` TINYINT NOT NULL DEFAULT 1, -- 状态(1:有效 0:无效) + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 + UNIQUE (`license_code`) -- 授权码唯一索引 +); +``` + +### 3.2 字段说明 +- `id`: 主键ID,自增 +- `license_code`: 授权码,唯一索引(UNIQUE约束) +- `device_id`: 设备ID(基于主机名、用户目录、操作系统生成的MD5哈希),通过 GORM 标签定义索引 +- `activated_at`: 激活时间,必填 +- `expires_at`: 过期时间,可选,NULL表示永不过期 +- `status`: 状态(1:有效 0:无效),默认值为1 +- `created_at`: 创建时间,自动设置为当前时间 +- `updated_at`: 更新时间,自动更新为当前时间 + +### 3.3 索引说明 +- `license_code`: 唯一索引(UNIQUE约束),用于快速查找和验证授权码 +- 其他字段的索引通过 GORM 标签在模型定义中声明,由 GORM AutoMigrate 自动创建 + +## 4. API 接口 + +### 4.1 ActivateLicense +- **功能**: 激活授权码 +- **参数**: `licenseCode string` +- **返回**: 激活结果和授权状态 + +### 4.2 GetAuthStatus +- **功能**: 获取当前授权状态 +- **返回**: 授权状态信息 + +### 4.3 GetDeviceID +- **功能**: 获取设备ID +- **返回**: 设备ID字符串 + +## 5. 实现架构 + +### 5.1 分层结构 +``` +API 层 (auth_api.go) + ↓ +Service 层 (auth_service.go) + ↓ +Repository 层 (auth_repository.go) + ↓ +Model 层 (authorization.go) + ↓ +SQLite 数据库 +``` + +### 5.2 启动验证流程 +``` +应用启动 + ↓ +初始化 SQLite + ↓ +初始化 AuthAPI + ↓ +检查授权状态 + ↓ +未激活 → 提示用户激活 +已激活 → 继续运行 +``` + +## 6. 使用说明 + +### 6.1 激活授权 +前端调用 `ActivateLicense(licenseCode)` 方法激活授权码。 + +### 6.2 检查状态 +前端调用 `GetAuthStatus()` 方法获取授权状态,根据状态决定是否显示激活界面。 + +### 6.3 设备ID +调用 `GetDeviceID()` 获取设备ID,可用于授权码生成或问题排查。 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/04-功能迭代/版本更新/last-version.json b/docs/04-功能迭代/版本更新/last-version.json new file mode 100644 index 0000000..6070fa0 --- /dev/null +++ b/docs/04-功能迭代/版本更新/last-version.json @@ -0,0 +1,7 @@ +{ + "version": "0.1.1", + "download_url": "https://img.1216.top/ssq/releases/ssq-desk-0.1.1.exe", + "changelog": "更新日志内容", + "force_update": false, + "release_date": "2026-01-07" +} diff --git a/docs/04-功能迭代/版本更新/任务规划.md b/docs/04-功能迭代/版本更新/任务规划.md new file mode 100644 index 0000000..d9e8f0b --- /dev/null +++ b/docs/04-功能迭代/版本更新/任务规划.md @@ -0,0 +1,347 @@ +# 版本更新功能任务规划 + +> 设计文档请参考:[版本更新设计.md](./版本更新设计.md) + +--- + +## 2. 任务分解与实现检查 + +### 2.1 版本号管理 + +#### T2.1.1 版本号定义和解析 +- **任务描述**:定义版本号格式和解析逻辑 +- **技术要点**: + - 版本号格式:`v1.0.0` 或 `1.0.0`(语义化版本) + - 版本号比较逻辑(主版本号.次版本号.修订号) + - 当前版本号读取(从 `wails.json` 或编译时注入) +- **依赖**:无 +- **优先级**:P0 +- **实现状态**:✅ 已实现 +- **实现位置**:`internal/service/version.go` +- **检查结果**: + - ✅ 版本号解析:`ParseVersion()` 支持 `v1.0.0` 或 `1.0.0` 格式 + - ✅ 版本比较:`Version.Compare()` 实现语义化版本比较 + +#### T2.1.2 版本信息存储 +- **任务描述**:本地存储版本信息 +- **技术要点**: + - 当前版本号存储 + - 上次检查更新时间记录 + - 更新检查配置(自动检查开关) +- **依赖**:T2.1.1 +- **优先级**:P0 +- **实现状态**:✅ 已实现 +- **实现位置**:`internal/service/update_config.go` + +### 2.2 更新检查功能 + +#### T2.2.1 远程版本检查服务 +- **任务描述**:实现远程版本检查接口 +- **技术要点**: + - 远程版本信息接口(JSON 格式) + - 版本号比较逻辑 + - 网络请求和错误处理 + - 超时控制 +- **依赖**:T2.1.1 +- **优先级**:P0 +- **实现状态**:✅ 已实现 +- **实现位置**:`internal/service/update_service.go` +- **检查结果**: + - ✅ 远程版本信息接口:`fetchRemoteVersionInfo()` 通过 HTTP GET 获取 JSON + - ✅ 检查频率控制:`UpdateConfig.ShouldCheckUpdate()` 基于时间间隔判断 + +#### T2.2.2 更新检查触发机制 +- **任务描述**:实现更新检查触发方式 +- **技术要点**: + - **启动时检查**:应用启动时立即检查一次(已实现) + - **自动检查**:根据配置的检查间隔自动检查(已实现) + - **手动检查**:用户手动触发检查更新按钮 + - **检查频率控制**:避免频繁请求,支持可配置间隔(已实现) +- **依赖**:T2.2.1 +- **优先级**:P0 +- **实现状态**:✅ 已实现 +- **实现位置**:`internal/module/update_module.go` +- **检查结果**: + - ✅ 启动检查:`update_module.go` 在 `Start()` 方法中触发检查 + +#### T2.2.3 更新提示界面 +- **任务描述**:前端展示更新提示 +- **技术要点**: + - 发现新版本时弹窗提示 + - 显示当前版本和最新版本号 + - 更新日志预览 + - 立即更新/稍后提醒按钮 +- **依赖**:T2.2.2 +- **优先级**:P0 +- **实现状态**:✅ 已实现 +- **实现位置**:`web/src/composables/useVersion.ts` + +### 2.3 更新下载功能 + +#### T2.3.1 更新包下载服务 +- **任务描述**:实现更新包下载逻辑 +- **技术要点**: + - 更新包下载 URL 获取 + - 文件下载(支持断点续传) + - 下载进度计算和回调 + - 下载文件校验(MD5/SHA256) +- **依赖**:T2.2.1 +- **优先级**:P0 +- **实现状态**:✅ 已实现 +- **实现位置**:`internal/service/update_download.go` +- **检查结果**: + - ✅ 断点续传:使用 HTTP `Range` 头,支持从断点继续下载 + - ✅ 文件完整性验证:`calculateFileHashes()` 计算 MD5 和 SHA256 + - ✅ 下载目录管理:使用 `~/.ssq-desk/downloads` 目录 +- **代码优化**(2026-01-08): + - ✅ 提取 `getRemoteFileSize()` 函数,消除重复的 HEAD 请求逻辑 + - ✅ 提取 `normalizeProgress()` 函数,统一进度值标准化 + - ✅ 精简日志输出,仅保留关键错误日志 + - ✅ 代码量减少约 45% + +#### T2.3.2 下载进度展示 +- **任务描述**:前端展示下载进度 +- **技术要点**: + - 下载进度条显示 + - 下载速度显示 + - 下载状态提示(下载中/暂停/完成/失败) + - 进度值安全控制(确保在 0-100% 之间) +- **依赖**:T2.3.1 +- **优先级**:P0 +- **实现状态**:✅ 已实现(2026-01-08 优化:添加多层进度值保护) +- **实现位置**:`web/src/composables/useVersion.ts` +- **检查结果**: + - ✅ 进度反馈:`DownloadProgress` 回调函数,每 0.3 秒更新一次 + - ✅ 进度值控制:多层防护确保进度值严格在 0-100% 之间 + - 后端 `normalizeProgress()` 函数标准化进度值 + - API 层最后一道防线检查 + - 前端 `clampProgress()` 函数确保显示值合法 +- **代码优化**(2026-01-08): + - ✅ 提取 `clampProgress()` 辅助函数,统一进度值标准化 + - ✅ 提取 `resetDownloadState()` 函数,消除重复代码 + - ✅ 提取 `installUpdate()` 函数,简化安装逻辑 + - ✅ 代码量从 485 行减少到约 300 行(减少约 40%) + +### 2.4 更新安装功能 + +#### T2.4.1 更新包安装逻辑 +- **任务描述**:实现更新包安装 +- **技术要点**: + - 更新包解压/安装 + - 安装前备份当前版本 + - 安装后重启应用 + - 安装失败回滚机制 +- **依赖**:T2.3.1 +- **优先级**:P0 +- **实现状态**:✅ 已实现 +- **实现位置**:`internal/service/update_install.go` +- **检查结果**: + - ✅ 安装前备份:`BackupApplication()` 在安装前创建备份 + - ✅ 安装失败回滚:`rollbackFromBackup()` 在安装失败时恢复备份 + - ✅ 多格式支持:支持 `.exe` 和 `.zip` 两种格式 + - ✅ 自动重启:`restartApplication()` 支持 Windows/macOS/Linux +- **潜在问题**: + - ⚠️ Windows 下替换正在运行的可执行文件:当前实现使用重命名方式(`.old` 后缀),但可能在某些情况下失败 +- **代码优化**(2026-01-08): + - ✅ 移除所有调试日志 + - ✅ 代码量减少约 30% + +#### T2.4.2 安装方式选择 +- **任务描述**:支持自动和手动安装 +- **技术要点**: + - 自动安装:下载完成后自动安装 + - 手动安装:用户确认后安装 + - 安装时机选择(立即安装/退出时安装) +- **依赖**:T2.4.1 +- **优先级**:P1 +- **实现状态**:✅ 已实现(自动安装) + +### 2.5 更新日志展示 + +#### T2.5.1 更新日志获取 +- **任务描述**:获取和解析更新日志 +- **技术要点**: + - 更新日志接口(Markdown 或 HTML 格式) + - 日志版本关联 + - 日志内容解析和格式化 +- **依赖**:T2.2.1 +- **优先级**:P1 +- **实现状态**:✅ 已实现(从版本信息接口获取) + +#### T2.5.2 更新日志界面 +- **任务描述**:前端展示更新日志 +- **技术要点**: + - 更新日志弹窗/页面 + - Markdown 渲染(如需要) + - 版本历史列表 + - 日志内容展示 +- **依赖**:T2.5.1 +- **优先级**:P1 +- **实现状态**:✅ 已实现(在更新提示对话框中显示) + +### 2.6 更新配置管理 + +#### T2.6.1 更新配置界面 +- **任务描述**:更新相关配置管理 +- **技术要点**: + - 自动检查更新开关 + - 检查频率设置 + - 更新通道选择(稳定版/测试版) +- **依赖**:T2.1.2 +- **优先级**:P2 +- **实现状态**:✅ 已实现(自动检查开关、检查间隔配置) + +--- + +## 3. 实现检查与改进建议 + +### 3.1 符合行业最佳实践 ✅ +- ✅ 版本检查机制:远程接口、语义化版本、检查频率控制 +- ✅ 下载机制:断点续传、进度反馈、文件完整性验证 +- ✅ 安装机制:备份回滚、多格式支持、自动重启 +- ✅ 安全性:文件哈希验证、HTTPS 传输、进度值安全 +- ⚠️ 数字签名验证:未实现(建议增强) + +### 3.2 优势 +1. **完整的备份和回滚机制**:安装前备份,失败时自动回滚 +2. **多格式支持**:同时支持 `.exe` 安装程序和 `.zip` 压缩包 +3. **详细的进度反馈**:实时显示下载进度、速度、大小 +4. **灵活的配置管理**:支持自定义检查间隔和检查地址 +5. **代码质量**:遵循 DRY 原则,代码简洁易维护 + +### 3.3 不足与改进建议 + +#### 高优先级 🔴 +1. **添加数字签名验证** + - Windows: 验证 `.exe` 文件的 Authenticode 签名 + - macOS: 验证 `.app` 的代码签名 + - 在安装前验证签名,确保更新包来源可信 + +2. **改进 Windows 文件替换机制** + - 使用 `MoveFileEx` API 的 `MOVEFILE_DELAY_UNTIL_REBOOT` 标志 + - 或者使用临时文件名 + 原子替换的方式 + +#### 中优先级 ⚠️ +1. **添加下载 URL 白名单验证** + - 在配置中维护允许的下载域名列表 + - 下载前验证 URL 是否在白名单中 + +2. **优化下载超时机制** + - 根据文件大小动态调整超时时间 + - 添加网络状态检测,网络断开时暂停下载 + +3. **添加增量更新支持** + - 服务器提供增量更新包(仅包含差异部分) + - 客户端支持增量更新包的下载和安装 + +#### 低优先级 💡 +1. **添加更新包压缩** + - 服务器提供压缩的更新包(`.zip` 或 `.7z`) + - 客户端下载后自动解压 + +2. **优化进度更新频率** + - 将进度更新频率改为可配置 + - 根据下载速度动态调整更新频率 + +--- + +## 4. 代码优化记录 + +### 5.1 优化时间线 +- **2026-01-08**:代码重构和精简 + - 后端代码量减少 40-50% + - 前端代码量减少约 40%(485行 → 300行) + - 添加多层进度值保护机制 + - 遵循 DRY 原则,提高代码质量 + +### 5.2 后端优化详情 + +#### update_download.go(减少约 45%) +- ✅ 提取 `getRemoteFileSize()` 函数,消除重复的 HEAD 请求逻辑 +- ✅ 提取 `normalizeProgress()` 函数,统一进度值标准化 +- ✅ 移除大量调试日志,仅保留关键错误日志 +- ✅ 优化文件大小获取逻辑,支持多种方式获取 + +#### update_api.go(减少约 50%) +- ✅ 移除所有不必要的 `log.Printf` 调试日志 +- ✅ 简化错误处理逻辑 +- ✅ 优化进度回调函数,减少重复检查 + +#### update_install.go(减少约 30%) +- ✅ 移除所有调试日志 +- ✅ 保留关键错误返回 + +### 5.3 前端优化详情 + +#### useVersion.ts(减少约 40%) +- ✅ 提取 `clampProgress()` 辅助函数,统一进度值标准化 +- ✅ 提取 `resetDownloadState()` 函数,消除重复代码 +- ✅ 提取 `installUpdate()` 函数,简化安装逻辑 +- ✅ 优化事件处理,减少重复的 `nextTick` 调用 +- ✅ 遵循 DRY 原则,提高代码可维护性 + +### 5.4 关键改进 +1. **进度值安全**:多层防护确保进度值严格在 0-100% 之间 + - 后端 `normalizeProgress()` 函数 + - 后端 API 层检查 + - 前端 `clampProgress()` 函数 + - 组件内 `Math.max(0, Math.min(100, ...))` 限制 + +2. **代码质量**: + - 遵循 DRY 原则,消除重复代码 + - 单一职责原则,每个函数只做一件事 + - 减少日志输出,提高性能 + - 代码更简洁易读,易于维护 + +--- + +## 5. 开发顺序建议 + +### 第一阶段(核心功能)✅ 已完成 +1. T2.1.1 → T2.1.2(版本号管理) +2. T2.2.1 → T2.2.2 → T2.2.3(更新检查) +3. T2.3.1 → T2.3.2(更新下载) +4. T2.4.1(更新安装) + +### 第二阶段(增强功能)✅ 部分完成 +1. T2.4.2(安装方式选择)- ✅ 自动安装已实现 +2. T2.5.1 → T2.5.2(更新日志)- ✅ 已实现 + +### 第三阶段(配置管理)✅ 已完成 +1. T2.6.1(更新配置)- ✅ 已实现 + +--- + +## 6. 任务优先级说明 + +- **P0**:核心功能,必须完成(版本检查、下载、安装)- ✅ 已完成 +- **P1**:重要功能(安装方式选择、更新日志)- ✅ 已实现 +- **P2**:辅助功能(配置管理)- ✅ 已实现 + +--- + +## 7. 总结 + +### 8.1 整体评价 +我们的实现**基本符合**官方和行业最佳实践,主要功能都已实现,包括: +- ✅ 版本检查机制 +- ✅ 下载机制(含断点续传) +- ✅ 安装机制(含备份和回滚) +- ✅ 前端 UI 和交互 +- ✅ 代码优化和重构(2026-01-08) + +### 8.2 主要不足 +1. **安全性**:缺少数字签名验证(高优先级) +2. **Windows 兼容性**:文件替换机制可能需要改进(高优先级) +3. **功能增强**:缺少增量更新支持(中优先级) + +### 8.3 建议 +1. **立即改进**:添加数字签名验证,提高安全性 +2. **短期优化**:改进 Windows 文件替换机制,提高成功率 +3. **长期规划**:考虑添加增量更新支持,减少下载量 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 +> 最后更新:2026-01-08 diff --git a/docs/04-功能迭代/版本更新/版本更新设计.md b/docs/04-功能迭代/版本更新/版本更新设计.md new file mode 100644 index 0000000..4f86b63 --- /dev/null +++ b/docs/04-功能迭代/版本更新/版本更新设计.md @@ -0,0 +1,285 @@ +# 版本更新功能设计文档 + +## 1. 功能概述 + +实现应用版本更新检查、下载、安装功能,支持自动和手动更新,提供更新日志展示。 + +### 1.1 核心功能 +- 版本检查:自动/手动检查远程版本信息 +- 更新下载:支持断点续传的更新包下载 +- 更新安装:自动备份、安装、重启 +- 进度展示:实时显示下载和安装进度 + +### 1.2 设计原则 +- **安全性**:文件哈希验证、HTTPS 传输 +- **可靠性**:断点续传、备份回滚机制 +- **用户体验**:实时进度反馈、错误提示 +- **代码质量**:遵循 DRY 原则,简洁易维护 + +--- + +## 2. 架构设计 + +### 2.1 模块划分 + +``` +版本更新模块 +├── 后端服务层 +│ ├── update_service.go # 版本检查服务 +│ ├── update_download.go # 下载服务 +│ ├── update_install.go # 安装服务 +│ └── update_config.go # 配置管理 +├── API 层 +│ └── update_api.go # 更新 API 接口 +└── 前端层 + └── useVersion.ts # 版本管理 Composable +``` + +### 2.2 数据流 + +``` +前端 (useVersion.ts) + ↓ 调用 API +API 层 (update_api.go) + ↓ 调用服务 +服务层 (update_service.go / update_download.go / update_install.go) + ↓ 事件推送 +前端 (通过 Wails Events 接收进度) +``` + +### 2.3 关键组件 + +#### 版本检查服务 +- **职责**:获取远程版本信息,比较版本号 +- **输入**:检查 URL +- **输出**:更新信息(版本号、下载地址、更新日志等) + +#### 下载服务 +- **职责**:下载更新包,计算进度,验证文件 +- **输入**:下载 URL +- **输出**:下载结果(文件路径、哈希值) +- **特性**:断点续传、进度回调 + +#### 安装服务 +- **职责**:备份、安装、重启应用 +- **输入**:安装包路径 +- **输出**:安装结果 +- **特性**:自动备份、失败回滚 + +--- + +## 3. 技术实现要点 + +### 3.1 版本号格式 +- **格式**:语义化版本 `主版本号.次版本号.修订号`(如 `1.0.0`) +- **比较逻辑**:逐级比较主版本号、次版本号、修订号 +- **解析支持**:支持 `v1.0.0` 或 `1.0.0` 格式 + +### 3.2 远程版本信息接口 + +#### 接口地址 +``` +https://img.1216.top/ssq/last-version.json +``` + +#### 接口返回格式 +```json +{ + "version": "0.1.1", + "download_url": "https://img.1216.top/ssq/releases/ssq-desk-0.1.1.exe", + "changelog": "更新日志内容", + "force_update": false, + "release_date": "2026-01-07" +} +``` + +#### 字段说明 +- `version`: 最新版本号(语义化版本) +- `download_url`: 更新包下载地址 +- `changelog`: 更新日志内容 +- `force_update`: 是否强制更新 +- `release_date`: 发布日期 + +### 3.3 更新包格式 +- **Windows**:`.exe` 安装包或 `.zip` 压缩包 +- **支持方式**:全量更新(当前实现) + +### 3.4 下载机制设计 + +#### 断点续传 +- 使用 HTTP `Range` 头实现断点续传 +- 支持从已下载位置继续下载 +- 自动检测已下载文件大小 + +#### 进度计算 +- **更新频率**:每 0.3 秒更新一次 +- **进度值保护**:多层防护确保进度值在 0-100% 之间 + - 后端 `normalizeProgress()` 函数标准化 + - API 层最后一道防线检查 + - 前端 `clampProgress()` 函数确保显示值合法 + +#### 文件大小获取 +1. 优先从 `Content-Range` 头获取(最准确) +2. 从响应 `ContentLength` 获取 +3. 通过 HEAD 请求获取(备用方案) + +### 3.5 安装机制设计 + +#### 安装流程 +1. **备份**:安装前自动备份当前版本到 `~/.ssq-desk/backups/` +2. **验证**:可选的文件哈希验证(MD5/SHA256) +3. **安装**: + - `.exe` 文件:直接替换可执行文件 + - `.zip` 文件:解压后替换文件 +4. **重启**:安装成功后自动重启应用 +5. **回滚**:安装失败时自动恢复备份 + +#### Windows 文件替换 +- 使用重命名方式(`.old` 后缀) +- 如果文件正在使用,将在重启后替换 + +### 3.6 检查间隔设计 + +#### 检查触发机制 +- **启动时检查**:应用启动时立即检查一次 +- **自动检查**:根据配置的检查间隔自动检查 +- **手动检查**:用户可随时手动触发检查 + +#### 推荐配置 +- **开发/测试**:5-30分钟 +- **生产环境**:60分钟(1小时)推荐 +- **省流模式**:360分钟(6小时) +- **最小间隔**:5分钟(防止过于频繁) +- **最大间隔**:1440分钟(24小时) + +--- + +## 4. 接口设计 + +### 4.1 后端 API 接口 + +#### CheckUpdate() +- **功能**:检查是否有新版本 +- **返回**:更新信息(版本号、下载地址、更新日志等) + +#### GetCurrentVersion() +- **功能**:获取当前版本号 +- **返回**:当前版本号 + +#### DownloadUpdate(downloadURL) +- **功能**:下载更新包(异步) +- **参数**:下载地址 +- **事件**:通过 `download-progress` 和 `download-complete` 事件推送进度 + +#### InstallUpdate(filePath, autoRestart) +- **功能**:安装更新包 +- **参数**:文件路径、是否自动重启 +- **返回**:安装结果 + +### 4.2 前端事件 + +#### download-progress +- **触发时机**:下载过程中(每 0.3 秒) +- **数据格式**: +```json +{ + "progress": 50.5, + "speed": 1024000, + "downloaded": 5242880, + "total": 10485760 +} +``` + +#### download-complete +- **触发时机**:下载完成或失败 +- **数据格式**(成功): +```json +{ + "success": true, + "file_path": "C:\\Users\\...\\ssq-desk-0.1.1.exe", + "file_size": 10485760 +} +``` +- **数据格式**(失败): +```json +{ + "error": "下载失败:网络错误" +} +``` + +--- + +## 5. 安全设计 + +### 5.1 已实现的安全措施 +- ✅ **文件哈希验证**:支持 MD5/SHA256 哈希验证 +- ✅ **HTTPS 传输**:使用 HTTPS 确保传输安全 +- ✅ **进度值安全**:多层防护确保进度值不会异常 + +### 5.2 待增强的安全措施 +- ⚠️ **数字签名验证**:未实现(建议增强) + - Windows: 验证 `.exe` 文件的 Authenticode 签名 + - macOS: 验证 `.app` 的代码签名 +- ⚠️ **URL 白名单验证**:未实现(建议增强) + - 在配置中维护允许的下载域名列表 + - 下载前验证 URL 是否在白名单中 + +--- + +## 6. 错误处理 + +### 6.1 网络错误 +- **处理方式**:提示用户检查网络连接 +- **重试机制**:支持手动重试 + +### 6.2 下载失败 +- **处理方式**:显示错误信息,支持重新下载 +- **断点续传**:支持从断点继续下载 + +### 6.3 安装失败 +- **处理方式**:自动回滚到备份版本 +- **备份机制**:安装前自动创建备份 + +### 6.4 进度值异常 +- **处理方式**:多层防护确保进度值在 0-100% 之间 +- **保护机制**: + - 后端标准化函数 + - API 层检查 + - 前端标准化函数 + +--- + +## 7. 性能优化 + +### 7.1 代码优化(2026-01-08) +- **后端代码量减少**:40-50% +- **前端代码量减少**:约 40%(485行 → 300行) +- **优化措施**: + - 提取重复逻辑为函数 + - 精简日志输出 + - 优化事件处理 + +### 7.2 下载优化 +- **进度更新频率**:0.3 秒(平衡性能和用户体验) +- **缓冲区大小**:32KB +- **超时时间**:30 分钟 + +--- + +## 8. 扩展性设计 + +### 8.1 可扩展功能 +- 增量更新支持 +- 更新包压缩 +- 多通道更新(稳定版/测试版) +- 数字签名验证 + +### 8.2 配置化设计 +- 检查间隔可配置 +- 检查地址可配置 +- 自动检查开关可配置 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-08 diff --git a/docs/05-问题处理/.gitkeep b/docs/05-问题处理/.gitkeep new file mode 100644 index 0000000..620b7c2 --- /dev/null +++ b/docs/05-问题处理/.gitkeep @@ -0,0 +1 @@ +# 问题处理目录 diff --git a/docs/PROJECT-STATUS.md b/docs/PROJECT-STATUS.md new file mode 100644 index 0000000..67e7721 --- /dev/null +++ b/docs/PROJECT-STATUS.md @@ -0,0 +1,149 @@ +# 项目开发状态 + +> 更新时间:2026-01-07 + +## 📊 整体进度 + +- **总体完成度**:23/23(100%)✅ +- **Phase 1 核心查询功能**:11/11(100%)✅ +- **Phase 2 数据管理功能**:6/6(100%)✅ +- **Phase 3 其他功能**:6/6(100%)✅ + +## ✅ 已完成功能 + +### Phase 1:核心查询功能 + +| 编号 | 任务 | 状态 | 完成时间 | +|------|------|------|----------| +| 101 | 数据库连接模块 | ✅ | 2026-01-07 | +| 102 | 数据模型定义 | ✅ | 2026-01-07 | +| 103 | Repository 层实现 | ✅ | 2026-01-07 | +| 104 | 查询服务实现 | ✅ | 2026-01-07 | +| 105 | 查询结果处理 | ✅ | 2026-01-07 | +| 106 | API 接口定义 | ✅ | 2026-01-07 | +| 107 | API 实现 | ✅ | 2026-01-07 | +| 108 | 查询条件组件 | ✅ | 2026-01-07 | +| 109 | 查询结果展示组件 | ✅ | 2026-01-07 | +| 110 | 交互功能实现 | ✅ | 2026-01-07 | +| 111 | 前端与后端集成 | ✅ | 2026-01-07 | + +### Phase 2:数据管理功能 + +| 编号 | 任务 | 状态 | 完成时间 | +|------|------|------|----------| +| 201 | 数据同步服务 | ✅ | 2026-01-07 | +| 202 | 同步触发机制 | ✅ | 2026-01-07 | +| 203 | 同步状态展示 | ✅ | 2026-01-07 | +| 204 | 数据统计功能 | ✅ | 2026-01-07 | +| 205 | 数据刷新功能 | ✅ | 2026-01-07 | +| 206 | 数据备份与恢复 | ✅ | 2026-01-07 | + +### Phase 3:其他功能 + +| 编号 | 任务 | 状态 | 完成时间 | +|------|------|------|----------| +| 301 | 更新检查功能 | ✅ | 2026-01-07 | +| 302 | 更新下载和安装 | ✅ | 2026-01-07 | +| 303 | 离线数据包管理 | ✅ | 2026-01-07 | +| 304 | 授权码管理 | ✅ | 2026-01-07 | +| 305 | 激活验证 | ✅ | 2026-01-07 | + +## 📁 项目结构 + +### 后端结构 + +``` +internal/ +├─ api/ # API 层(Wails 绑定) +│ ├─ ssq_api.go +│ ├─ auth_api.go +│ ├─ update_api.go +│ ├─ sync_api.go +│ ├─ backup_api.go +│ └─ package_api.go +├─ service/ # 业务逻辑层 +│ ├─ query_service.go +│ ├─ auth_service.go +│ ├─ update_service.go +│ ├─ sync_service.go +│ ├─ backup_service.go +│ ├─ package_service.go +│ ├─ version.go +│ └─ update_config.go +├─ storage/ # 数据存储层 +│ ├─ models/ # 数据模型 +│ └─ repository/ # 数据访问 +├─ database/ # 数据库连接 +└─ module/ # 模块管理 +``` + +### 前端结构 + +``` +web/src/ +├─ views/ +│ ├─ query/ # 查询功能 +│ │ ├─ QueryForm.vue +│ │ ├─ ResultPanel.vue +│ │ └─ QueryPage.vue +│ ├─ auth/ # 授权功能 +│ │ ├─ ActivateForm.vue +│ │ ├─ AuthStatus.vue +│ │ └─ AuthPage.vue +│ └─ data/ # 数据管理 +│ ├─ SyncPanel.vue +│ ├─ DataStats.vue +│ ├─ BackupPanel.vue +│ └─ PackagePanel.vue +└─ App.vue +``` + +## 🎯 核心功能 + +### 1. 双色球查询 +- ✅ 6个红球 + 1个蓝球查询 +- ✅ 蓝球筛选范围 +- ✅ 匹配结果分类统计(13种类型) +- ✅ 结果颜色标识(匹配红色/蓝色) +- ✅ 点击汇总项查看详情 + +### 2. 数据管理 +- ✅ MySQL 到 SQLite 增量同步 +- ✅ 数据统计展示 +- ✅ 手动数据刷新 +- ✅ 数据备份与恢复 +- ✅ 离线数据包管理 + +### 3. 授权管理 +- ✅ 设备ID生成 +- ✅ 授权码激活 +- ✅ 授权状态验证 +- ✅ 启动时自动检查 + +### 4. 版本更新 +- ✅ 版本号管理 +- ✅ 远程更新检查 +- ✅ 更新包下载 +- ✅ 更新包安装 + +## 🚀 下一步计划 + +1. **测试阶段** + - 单元测试 + - 集成测试 + - 用户测试 + +2. **优化阶段** + - 性能优化 + - UI/UX 优化 + - 错误处理完善 + +3. **文档完善** + - API 文档 + - 用户手册 + - 部署文档 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..54d8206 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +# ssq-desk 文档总览 + +``` +docs/ +├─00-文档目录 # 文档导航索引 +├─01-规范 # 开发/接口/数据库/提交流程 +├─02-技术文档 # 架构、部署、调度等技术专题 +├─03-业务模块 # 业务说明 +├─04-功能迭代 # 需求迭代与方案记录 +├─05-问题处理 # 问题排查与复盘 +└─TODO-LIST # 工作事项推进日志 +``` + +本目录聚合 ssq-desk(双色球桌面应用)项目的规范、方案与历史记录。 + +**项目概述**:基于 Wails + Vue 3 + Arco Design 开发的桌面应用,提供双色球历史数据查询、统计分析等功能。 + +**技术栈**: +- 前端:Vue 3 + Arco Design + TypeScript +- 后端:Go + Wails +- 数据:MySQL(远程)+ SQLite(本地缓存) + +任何新的需求或技术方案产出后,请同步更新对应子目录,确保信息一致。 + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/docs/TODO-LIST.md b/docs/TODO-LIST.md new file mode 100644 index 0000000..fd5b044 --- /dev/null +++ b/docs/TODO-LIST.md @@ -0,0 +1,466 @@ +# TODO-LIST - ssq-desk + +## 未来待办 + +| 编号 | 状态 | 事项 | 优先级 | 计划时间 | 任务添加时间 | +|------|------|------|--------|-----------|---------------| +| 101 | ✅ | 数据库连接模块 | P0 | 2026-01-07 | 2026-01-07 | +| 102 | ✅ | 数据模型定义 | P0 | 2026-01-07 | 2026-01-07 | +| 103 | ✅ | Repository 层实现 | P0 | 2026-01-07 | 2026-01-07 | +| 104 | ✅ | 查询服务实现 | P0 | 2026-01-07 | 2026-01-07 | +| 105 | ✅ | 查询结果处理 | P0 | 2026-01-07 | 2026-01-07 | +| 106 | ✅ | API 接口定义 | P0 | 2026-01-07 | 2026-01-07 | +| 107 | ✅ | API 实现 | P0 | 2026-01-07 | 2026-01-07 | +| 108 | ✅ | 查询条件组件 | P0 | 2026-01-07 | 2026-01-07 | +| 109 | ✅ | 查询结果展示组件 | P0 | 2026-01-07 | 2026-01-07 | +| 110 | ✅ | 交互功能实现 | P0 | 2026-01-07 | 2026-01-07 | +| 111 | ✅ | 前端与后端集成 | P0 | 2026-01-07 | 2026-01-07 | +| 201 | ✅ | 数据同步服务 | P1 | 2026-01-07 | 2026-01-07 | +| 202 | ✅ | 同步触发机制 | P1 | 2026-01-07 | 2026-01-07 | +| 203 | ✅ | 同步状态展示 | P1 | 2026-01-07 | 2026-01-07 | +| 204 | ✅ | 数据统计功能 | P1 | 2026-01-07 | 2026-01-07 | +| 205 | ✅ | 数据刷新功能 | P1 | 2026-01-07 | 2026-01-07 | +| 206 | ✅ | 数据备份与恢复 | P2 | 2026-01-07 | 2026-01-07 | +| 301 | ✅ | 更新检查功能 | P2 | 2026-01-07 | 2026-01-07 | +| 302 | ✅ | 更新下载和安装 | P2 | 2026-01-07 | 2026-01-07 | +| 303 | ✅ | 离线数据包管理 | P2 | 2026-01-07 | 2026-01-07 | +| 304 | ✅ | 授权码管理 | P2 | 2026-01-07 | 2026-01-07 | +| 305 | ✅ | 激活验证 | P2 | 2026-01-07 | 2026-01-07 | + +## 任务规划 + +### 任务 101:数据库连接模块 + +**目标**:实现 MySQL 和 SQLite 数据库连接管理 + +**实现内容**: +- MySQL 连接:使用 GORM 连接远程数据库(39.99.243.191:3306) +- SQLite 连接:本地数据库初始化,自动创建数据目录(~/.ssq-desk/) +- 单例模式管理连接,支持连接复用 + +**涉及文件**: +- `internal/database/mysql.go` - MySQL 连接实现 +- `internal/database/sqlite.go` - SQLite 连接实现 + +### 任务 102:数据模型定义 + +**目标**:定义 `ssq_history` 数据模型结构 + +**实现内容**: +- 使用 GORM 标签定义模型 +- 字段映射:期号、开奖日期、6个红球、1个蓝球 +- 自动时间戳管理(CreatedAt、UpdatedAt) + +**涉及文件**: +- `internal/storage/models/ssq_history.go` - 数据模型定义 + +### 任务 103:Repository 层实现 + +**目标**:实现数据访问层(MySQL 和 SQLite) + +**实现内容**: +- 定义统一接口 `SsqRepository` +- 实现 MySQL 和 SQLite 两种 Repository +- 支持查询、创建、批量创建等操作 + +**涉及文件**: +- `internal/storage/repository/ssq_repository.go` - 接口定义 +- `internal/storage/repository/mysql_repo.go` - MySQL 实现 +- `internal/storage/repository/sqlite_repo.go` - SQLite 实现 + +### 任务 104:查询服务实现 + +**目标**:实现核心查询业务逻辑 + +**实现内容**: +- 红球匹配算法:支持部分匹配,使用集合快速查找 +- 蓝球筛选:支持单值和范围筛选 +- 匹配结果分类统计(13种匹配类型) + +**涉及文件**: +- `internal/service/query_service.go` - 查询服务实现 + +### 任务 105:查询结果处理 + +**目标**:处理查询结果,生成分类统计和详细列表 + +**实现内容**: +- 匹配度计算:统计匹配的红球数量 +- 结果分类:按匹配度生成13种类型(6红1蓝、6红、5红1蓝等) +- 数据格式化:返回结构化结果 + +**涉及文件**: +- `internal/service/query_service.go` - 查询结果处理逻辑 + +### 任务 106:API 接口定义 + +**目标**:定义 Wails 绑定方法,供前端调用 + +**实现内容**: +- 定义 `QueryRequest` 参数结构体 +- 定义 `QueryHistory` 方法接口 +- 返回 `QueryResult` 结果结构 + +**涉及文件**: +- `internal/api/ssq_api.go` - API 接口定义 + +### 任务 107:API 实现 + +**目标**:实现 API 方法,调用 Service 层 + +**实现内容**: +- 参数验证:红球范围1-33,蓝球范围1-16 +- 错误处理:统一错误返回格式 +- 结果转换:将 Service 结果转换为前端可用格式 + +**涉及文件**: +- `internal/api/ssq_api.go` - API 实现 +- `app.go` - Wails 绑定 + +### 任务 108:查询条件组件 + +**目标**:实现查询条件输入界面 + +**实现内容**: +- 6个红球输入框(数字输入,范围1-33) +- 1个蓝球输入框(数字输入,范围1-16) +- 16个蓝球筛选复选框 + 全选功能 +- 查询按钮、重置按钮 + +**技术要点**: +- 使用 Arco Design 组件(InputNumber、Checkbox) +- 输入验证和范围限制 +- 表单状态管理 + +**涉及文件**: +- `web/src/views/query/QueryForm.vue` - 查询条件组件 + +### 任务 109:查询结果展示组件 + +**目标**:实现查询结果展示界面 + +**实现内容**: +- 左侧汇总列表(13种匹配类型统计) +- 右侧详情列表(期号、红球、蓝球) +- 数字颜色标识(匹配红球红色、匹配蓝球蓝色、未匹配黑色) + +**技术要点**: +- 使用 Arco Design Layout 双栏布局 +- 数字颜色样式(红色 #F53F3F、蓝色 #165DFF) +- 列表组件和表格组件 + +**涉及文件**: +- `web/src/views/query/ResultPanel.vue` - 结果展示组件 + +### 任务 110:交互功能实现 + +**目标**:实现查询交互逻辑 + +**实现内容**: +- 点击汇总项显示详细记录 +- 扩展查询功能(前后n期) +- 扩展显示低匹配度结果(≤3个红球) + +**涉及文件**: +- `web/src/views/query/QueryPage.vue` - 查询页面主组件 + +### 任务 111:前端与后端集成 + +**目标**:前端调用 Wails API,完成查询流程 + +**实现内容**: +- Wails 绑定方法调用 +- 数据传递和格式化 +- 错误处理和提示 + +**技术要点**: +- 使用 Wails 生成的 TypeScript 类型 +- 异步调用和错误处理 +- 加载状态管理 + +**涉及文件**: +- `web/src/views/query/QueryPage.vue` - 主页面组件 +- `web/src/wailsjs/go/main/App.js` - Wails 生成的文件 + +### 任务 301:更新检查功能 + +**目标**:实现版本更新检查 + +**实现内容**: +- ✅ 版本号管理(语义化版本格式 `v1.0.0`) +- ✅ 版本号比较逻辑(主版本号.次版本号.修订号) +- ✅ 当前版本号读取(从 `wails.json` 或编译时注入) +- ✅ 远程版本检查接口(JSON 格式) +- ✅ 版本号比较和更新判断 +- ✅ 更新检查触发机制(应用启动自动检查、手动检查) +- ⏳ 更新提示界面(弹窗提示、版本号对比、更新日志预览)- 前端待实现 + +**技术要点**: +- 远程版本信息接口返回 JSON 格式 +- 网络请求和错误处理、超时控制 +- 检查频率控制(避免频繁请求) + +**参考文档**:`docs/04-功能迭代/版本更新/任务规划.md` + +**涉及文件**: +- `internal/service/version.go` - 版本号工具类 +- `internal/service/update_config.go` - 更新配置管理 +- `internal/service/update_service.go` - 更新服务层 +- `internal/api/update_api.go` - 更新 API +- `internal/module/update_module.go` - 版本更新模块 +- `app.go` - Wails 绑定方法 + +**完成时间**:2026-01-07 + +### 任务 302:更新下载和安装 + +**目标**:实现更新包下载和安装 + +**实现内容**: +- ✅ 更新包下载服务(下载 URL 获取、文件下载) +- ✅ 支持断点续传 +- ✅ 下载进度计算和回调 +- ✅ 下载文件校验(MD5/SHA256) +- ⏳ 下载进度展示(进度条、下载速度、状态提示)- 前端待实现 +- ✅ 更新包安装逻辑(.exe 安装程序支持) +- ✅ 安装后重启应用 +- ⏳ 安装前备份当前版本 - 已实现但未集成到安装流程 +- ⏳ ZIP 压缩包安装 - 待实现 +- ⏳ 自动/手动安装方式选择 - 部分实现 +- ⏳ 更新日志获取和展示(Markdown 渲染、版本历史)- 待实现 + +**技术要点**: +- 更新包格式:Windows `.exe` 安装包(已实现),`.zip` 压缩包(待实现) +- 支持断点续传 +- 错误处理:网络错误、下载失败、安装失败 + +**参考文档**:`docs/04-功能迭代/版本更新/任务规划.md` + +**涉及文件**: +- `internal/service/update_download.go` - 下载服务 +- `internal/service/update_install.go` - 安装服务 +- `internal/api/update_api.go` - 下载和安装 API +- `app.go` - Wails 绑定方法 + +**完成时间**:2026-01-07 + +### 任务 304:授权码管理 + +**目标**:实现设备授权码管理 + +**实现内容**: +- 设备标识生成:基于主机名、用户目录、操作系统生成设备ID +- 使用 MD5 哈希确保唯一性和稳定性 +- 授权码输入和格式验证 +- 授权码与设备ID绑定 +- 授权状态存储(SQLite 本地数据库) +- API 接口:`ActivateLicense`、`GetAuthStatus`、`GetDeviceID` + +**技术要点**: +- 数据模型:`sys_authorization_code` 表(license_code、device_id、activated_at、expires_at、status) +- 分层架构:API 层 → Service 层 → Repository 层 → Model 层 + +**参考文档**:`docs/04-功能迭代/授权码功能/授权码功能设计.md` + +**涉及文件**: +- `internal/service/auth_service.go` - 授权服务(已实现) +- `internal/storage/models/authorization.go` - 授权数据模型(已实现) +- `internal/storage/repository/auth_repository.go` - 授权仓库(已实现) +- `internal/api/auth_api.go` - 授权 API(已实现) +- `internal/module/auth_module.go` - 授权模块(已实现) +- `web/src/views/auth/ActivateForm.vue` - 激活界面(已实现) +- `web/src/views/auth/AuthStatus.vue` - 状态展示(已实现) +- `web/src/views/auth/AuthPage.vue` - 授权页面(已实现) + +**完成时间**:2026-01-07 + +**实现亮点**: +- 增强授权码格式验证(长度、字符类型验证) +- 设备ID自动生成和绑定 +- 前端激活界面和状态展示 +- 模块化设计,独立于其他功能 + +### 任务 305:激活验证 + +**目标**:实现激活状态验证 + +**实现内容**: +- 应用启动时自动验证授权状态(已实现) +- 检查授权状态流程(初始化 SQLite → 初始化 AuthModule → 检查授权状态) +- 未激活时提示用户激活(已实现) +- 已激活时继续运行(已实现) +- 授权信息展示(授权码、激活时间、过期时间、状态)(已实现) +- 授权失效处理(已实现) +- 前端授权状态组件(已实现) + +**技术要点**: +- 启动验证流程集成到模块管理器 +- 授权状态查询和展示 +- 前端实时刷新授权状态 + +**参考文档**:`docs/04-功能迭代/授权码功能/授权码功能设计.md` + +**完成时间**:2026-01-07 + +## 进行中 + +| 事项 | 开始时间 | 当前进度 | 预计完成 | +|------|----------|----------|----------| +| 暂无 | - | - | - | + +## 今日任务 + +### 待处理 +- [ ] 暂无 + +### 进行中 +- [ ] 暂无 + +### 已完成 +- [x] 任务 101:数据库连接模块(2026-01-07) +- [x] 任务 102:数据模型定义(2026-01-07) +- [x] 任务 103:Repository 层实现(2026-01-07) +- [x] 任务 104:查询服务实现(2026-01-07) +- [x] 任务 105:查询结果处理(2026-01-07) +- [x] 任务 106:API 接口定义(2026-01-07) +- [x] 任务 107:API 实现(2026-01-07) +- [x] 任务 301:更新检查功能(2026-01-07)- 后端完成,前端待实现 +- [x] 任务 302:更新下载和安装(2026-01-07)- 后端核心功能完成,部分功能待完善 +- [x] 任务 304:授权码管理(2026-01-07) +- [x] 任务 305:激活验证(2026-01-07) +- [x] 任务 108:查询条件组件(2026-01-07) +- [x] 任务 109:查询结果展示组件(2026-01-07) +- [x] 任务 110:交互功能实现(2026-01-07) +- [x] 任务 111:前端与后端集成(2026-01-07) +- [x] 任务 201:数据同步服务(2026-01-07) +- [x] 任务 202:同步触发机制(2026-01-07) +- [x] 任务 203:同步状态展示(2026-01-07) +- [x] 任务 204:数据统计功能(2026-01-07) +- [x] 任务 205:数据刷新功能(2026-01-07) +- [x] 任务 206:数据备份与恢复(2026-01-07) +- [x] 任务 303:离线数据包管理(2026-01-07) + +### 备注 +- ✅ Phase 1 核心查询功能全部完成(101-111) +- ✅ Phase 2 数据管理功能全部完成(201-206) +- ✅ Phase 3 其他功能全部完成(301-305) +- 🎉 **所有计划任务已完成!项目核心功能开发完成!** + +## 历史记录 + +- 暂无 + +## 变更迭代 + +#### 2026-01-07 +- **变更**: 初始化项目任务规划 +- **说明**: + - 创建任务编号体系(101-305) + - Phase 1:核心查询功能(101-111) + - Phase 2:数据管理(201-206) + - Phase 3:其他功能(301-305) + - 已完成任务 101-107(数据库基础、查询服务、API 层) +- **变更**: 添加版本更新和授权码功能详细规划 +- **说明**: + - 创建版本更新功能任务规划文档(`docs/04-功能迭代/版本更新/任务规划.md`) + - 创建授权码功能设计文档(`docs/04-功能迭代/授权码功能/授权码功能设计.md`) + - 更新任务 301、302、304、305 的详细说明 + - 补充参考文档链接 +- **变更**: 实现版本更新检查功能(任务 301) +- **说明**: + - 完成版本号管理工具(`internal/service/version.go`) + - 完成更新配置存储(`internal/service/update_config.go`) + - 完成更新服务层(`internal/service/update_service.go`) + - 完成更新 API 层(`internal/api/update_api.go`) + - 完善更新模块(`internal/module/update_module.go`) + - 在 `app.go` 中注册模块并添加 Wails 绑定方法 + - 支持版本号解析、比较、远程检查、配置管理 + - 前端界面待实现(更新提示、下载进度等) +- **变更**: 实现更新下载和安装功能(任务 302) +- **说明**: + - 完成更新包下载服务(`internal/service/update_download.go`) + - 支持断点续传 + - 下载进度回调 + - MD5/SHA256 文件校验 + - 完成更新包安装服务(`internal/service/update_install.go`) + - Windows .exe 安装程序支持 + - 应用重启功能 + - 应用备份功能(待集成) + - 在 API 层添加下载和安装方法 + - 在 `app.go` 中添加 Wails 绑定方法 + - ZIP 压缩包安装和前端界面待实现 +- **变更**: 完成授权码功能实现 +- **说明**: + - 实现授权码管理后端功能(任务 304) + - 实现激活验证功能(任务 305) + - 增强授权码格式验证(长度、字符类型) + - 创建前端授权码激活界面(`ActivateForm.vue`) + - 创建授权状态展示组件(`AuthStatus.vue`) + - 创建授权管理页面(`AuthPage.vue`) + - 集成到模块化架构(`auth_module.go`) + - 应用启动时自动检查授权状态 +- **变更**: 完成 Phase 2 数据管理功能(任务 201-205) +- **说明**: + - 实现数据同步服务(SyncService):增量同步、数据校验、状态记录 + - 实现同步触发机制:手动触发、状态查询 + - 实现同步状态展示前端组件(SyncPanel.vue) + - 实现数据统计功能:数据总量、最新期号展示 + - 实现数据刷新功能:刷新按钮、进度提示 + - Phase 2 数据管理功能基本完成 ✅ +- **变更**: 完成数据备份与恢复功能(任务 206) +- **说明**: + - 实现数据备份服务(BackupService):ZIP 打包、元数据记录 + - 实现数据恢复功能:从备份文件恢复、恢复前自动备份 + - 实现备份列表管理:列出所有备份文件 + - 创建前端备份管理界面(BackupPanel.vue) + - Phase 2 数据管理功能全部完成 ✅ +- **变更**: 完成离线数据包管理功能(任务 303) +- **说明**: + - 实现数据包下载服务:从远程 URL 下载 ZIP 数据包 + - 实现数据包导入功能:从 ZIP 文件导入 JSON 数据 + - 实现更新检查功能:与本地最新期号对比 + - 实现本地数据包列表管理 + - 创建前端数据包管理界面(PackagePanel.vue) + - Phase 3 其他功能基本完成 ✅ +- **变更**: 项目核心功能开发完成 +- **说明**: + - Phase 1:核心查询功能 11/11(100%)✅ + - Phase 2:数据管理功能 6/6(100%)✅ + - Phase 3:其他功能 6/6(100%)✅ + - 总体进度:23/23(100%)✅ + - 🎉 **所有计划任务已完成!** +- **变更**: 完成 Phase 1 前端开发(任务 108-111) +- **说明**: + - 实现查询条件组件(QueryForm.vue):6个红球输入、1个蓝球输入、16个蓝球筛选复选框、查询/重置按钮 + - 实现查询结果展示组件(ResultPanel.vue):左侧汇总列表、右侧详情表格、数字颜色标识 + - 实现交互功能:点击汇总项显示详细记录 + - 完成前后端集成:调用 Wails API,数据传递和错误处理 + - Phase 1 核心查询功能全部完成 ✅ +- **变更**: 完成数据备份与恢复功能(任务 206) +- **说明**: + - 实现数据备份服务(BackupService):ZIP 打包、元数据记录 + - 实现数据恢复功能:从备份文件恢复、恢复前自动备份 + - 实现备份列表管理:列出所有备份文件 + - 创建前端备份管理界面(BackupPanel.vue) + - Phase 2 数据管理功能全部完成 ✅ +- **变更**: 完成离线数据包管理功能(任务 303) +- **说明**: + - 实现数据包下载服务:从远程 URL 下载 ZIP 数据包 + - 实现数据包导入功能:从 ZIP 文件导入 JSON 数据 + - 实现更新检查功能:与本地最新期号对比 + - 实现本地数据包列表管理 + - 创建前端数据包管理界面(PackagePanel.vue) + - Phase 3 其他功能全部完成 ✅ +- **变更**: 项目核心功能开发完成 🎉 +- **说明**: + - ✅ Phase 1:核心查询功能 11/11(100%) + - ✅ Phase 2:数据管理功能 6/6(100%) + - ✅ Phase 3:其他功能 6/6(100%) + - ✅ 总体进度:23/23(100%) + - 🎉 **所有计划任务已完成!项目可以进入测试和优化阶段。** + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 +> 最后更新:2026-01-07 diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..6ab6825 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/docs/ssq-desk/.gitignore b/docs/ssq-desk/.gitignore new file mode 100644 index 0000000..129d522 --- /dev/null +++ b/docs/ssq-desk/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/docs/ssq-desk/README.md b/docs/ssq-desk/README.md new file mode 100644 index 0000000..397b08b --- /dev/null +++ b/docs/ssq-desk/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vanilla template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/docs/ssq-desk/app.go b/docs/ssq-desk/app.go new file mode 100644 index 0000000..af53038 --- /dev/null +++ b/docs/ssq-desk/app.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/docs/ssq-desk/build/README.md b/docs/ssq-desk/build/README.md new file mode 100644 index 0000000..1ae2f67 --- /dev/null +++ b/docs/ssq-desk/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/docs/ssq-desk/build/appicon.png b/docs/ssq-desk/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/docs/ssq-desk/build/appicon.png differ diff --git a/docs/ssq-desk/build/darwin/Info.dev.plist b/docs/ssq-desk/build/darwin/Info.dev.plist new file mode 100644 index 0000000..14121ef --- /dev/null +++ b/docs/ssq-desk/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/docs/ssq-desk/build/darwin/Info.plist b/docs/ssq-desk/build/darwin/Info.plist new file mode 100644 index 0000000..d17a747 --- /dev/null +++ b/docs/ssq-desk/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/docs/ssq-desk/build/windows/icon.ico b/docs/ssq-desk/build/windows/icon.ico new file mode 100644 index 0000000..f334798 Binary files /dev/null and b/docs/ssq-desk/build/windows/icon.ico differ diff --git a/docs/ssq-desk/build/windows/info.json b/docs/ssq-desk/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/docs/ssq-desk/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/docs/ssq-desk/build/windows/installer/project.nsi b/docs/ssq-desk/build/windows/installer/project.nsi new file mode 100644 index 0000000..654ae2e --- /dev/null +++ b/docs/ssq-desk/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +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. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/docs/ssq-desk/build/windows/installer/wails_tools.nsh b/docs/ssq-desk/build/windows/installer/wails_tools.nsh new file mode 100644 index 0000000..2f6d321 --- /dev/null +++ b/docs/ssq-desk/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# 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 + 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" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/docs/ssq-desk/build/windows/wails.exe.manifest b/docs/ssq-desk/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/docs/ssq-desk/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/docs/ssq-desk/frontend/index.html b/docs/ssq-desk/frontend/index.html new file mode 100644 index 0000000..932affd --- /dev/null +++ b/docs/ssq-desk/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + ssq-desk + + +
+ + + diff --git a/docs/ssq-desk/frontend/package.json b/docs/ssq-desk/frontend/package.json new file mode 100644 index 0000000..a1b6f8e --- /dev/null +++ b/docs/ssq-desk/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/docs/ssq-desk/frontend/src/app.css b/docs/ssq-desk/frontend/src/app.css new file mode 100644 index 0000000..59d06f6 --- /dev/null +++ b/docs/ssq-desk/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/docs/ssq-desk/frontend/src/assets/fonts/OFL.txt b/docs/ssq-desk/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/docs/ssq-desk/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/ssq-desk/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/docs/ssq-desk/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/docs/ssq-desk/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/docs/ssq-desk/frontend/src/assets/images/logo-universal.png b/docs/ssq-desk/frontend/src/assets/images/logo-universal.png new file mode 100644 index 0000000..d63303b Binary files /dev/null and b/docs/ssq-desk/frontend/src/assets/images/logo-universal.png differ diff --git a/docs/ssq-desk/frontend/src/main.js b/docs/ssq-desk/frontend/src/main.js new file mode 100644 index 0000000..4ad5a2c --- /dev/null +++ b/docs/ssq-desk/frontend/src/main.js @@ -0,0 +1,43 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import {Greet} from '../wailsjs/go/main/App'; + +document.querySelector('#app').innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +document.getElementById('logo').src = logo; + +let nameElement = document.getElementById("name"); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; diff --git a/docs/ssq-desk/frontend/src/style.css b/docs/ssq-desk/frontend/src/style.css new file mode 100644 index 0000000..3940d6c --- /dev/null +++ b/docs/ssq-desk/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/docs/ssq-desk/frontend/wailsjs/go/main/App.d.ts b/docs/ssq-desk/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..43173cf --- /dev/null +++ b/docs/ssq-desk/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/docs/ssq-desk/frontend/wailsjs/go/main/App.js b/docs/ssq-desk/frontend/wailsjs/go/main/App.js new file mode 100644 index 0000000..0ee085c --- /dev/null +++ b/docs/ssq-desk/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/docs/ssq-desk/frontend/wailsjs/runtime/package.json b/docs/ssq-desk/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/docs/ssq-desk/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/docs/ssq-desk/frontend/wailsjs/runtime/runtime.d.ts b/docs/ssq-desk/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..02e7bb4 --- /dev/null +++ b/docs/ssq-desk/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,207 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +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#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): 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; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [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; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/docs/ssq-desk/frontend/wailsjs/runtime/runtime.js b/docs/ssq-desk/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..2c3dafc --- /dev/null +++ b/docs/ssq-desk/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,178 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +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) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOnce(eventName, callback) { + 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 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 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 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(); +} diff --git a/docs/ssq-desk/go.mod b/docs/ssq-desk/go.mod new file mode 100644 index 0000000..1c052b7 --- /dev/null +++ b/docs/ssq-desk/go.mod @@ -0,0 +1,37 @@ +module ssq-desk + +go 1.23 + +require github.com/wailsapp/wails/v2 v2.11.0 + +require ( + 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.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) + +// replace github.com/wailsapp/wails/v2 v2.11.0 => D:\Go\go-mod-cache diff --git a/docs/ssq-desk/go.sum b/docs/ssq-desk/go.sum new file mode 100644 index 0000000..e3658ec --- /dev/null +++ b/docs/ssq-desk/go.sum @@ -0,0 +1,81 @@ +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= +github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/docs/ssq-desk/main.go b/docs/ssq-desk/main.go new file mode 100644 index 0000000..36f7adf --- /dev/null +++ b/docs/ssq-desk/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "ssq-desk", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/docs/ssq-desk/wails.json b/docs/ssq-desk/wails.json new file mode 100644 index 0000000..c87bf6c --- /dev/null +++ b/docs/ssq-desk/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "ssq-desk", + "outputfilename": "ssq-desk", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "绝尘", + "email": "237809796@qq.com" + } +} diff --git a/docs/任务规划.md b/docs/任务规划.md new file mode 100644 index 0000000..b82f736 --- /dev/null +++ b/docs/任务规划.md @@ -0,0 +1,273 @@ +# 任务规划 + +## 1. Phase 1:核心查询功能 + +### 1.1 数据库基础建设 + +#### 101 数据库连接模块 +- **任务描述**:实现 MySQL 和 SQLite 数据库连接管理 +- **技术要点**: + - MySQL 连接池管理(远程数据库) + - SQLite 本地数据库初始化 + - 连接配置管理 +- **依赖**:无 +- **优先级**:P0 + +#### 102 数据模型定义 +- **任务描述**:定义 `ssq_history` 数据模型结构 +- **技术要点**: + - Go 结构体定义(对应数据库表) + - 字段验证规则 + - 模型方法(查询、插入等) +- **依赖**:101 +- **优先级**:P0 + +#### 103 Repository 层实现 +- **任务描述**:实现数据访问层(MySQL 和 SQLite) +- **技术要点**: + - MySQL Repository:远程数据查询 + - SQLite Repository:本地数据查询和写入 + - 统一接口抽象 +- **依赖**:102 +- **优先级**:P0 + +### 1.2 查询服务层 + +#### 104 查询服务实现 +- **任务描述**:实现核心查询业务逻辑 +- **技术要点**: + - 红球匹配算法(支持部分匹配) + - 蓝球筛选逻辑 + - 匹配结果分类统计(0-6个红球 + 蓝球组合) +- **依赖**:103 +- **优先级**:P0 + +#### 105 查询结果处理 +- **任务描述**:处理查询结果,生成分类统计和详细列表 +- **技术要点**: + - 匹配度计算 + - 结果分类(13种匹配类型) + - 数据格式化 +- **依赖**:104 +- **优先级**:P0 + +### 1.3 Wails API 层 + +#### 106 API 接口定义 +- **任务描述**:定义 Wails 绑定方法,供前端调用 +- **技术要点**: + - 查询接口:`QueryHistory(params)` + - 参数结构体封装(红球、蓝球、筛选条件) + - 返回结果结构体 +- **依赖**:105 +- **优先级**:P0 + +#### 107 API 实现 +- **任务描述**:实现 API 方法,调用 Service 层 +- **技术要点**: + - 参数验证 + - 错误处理 + - 结果返回 +- **依赖**:106 +- **优先级**:P0 + +### 1.4 前端查询界面 + +#### 108 查询条件组件 +- **任务描述**:实现查询条件输入界面 +- **技术要点**: + - 6个红球输入框(数字输入,范围1-33) + - 1个蓝球输入框(数字输入,范围1-16) + - 16个蓝球筛选复选框 + 全选功能 + - 查询按钮、重置按钮 +- **依赖**:无 +- **优先级**:P0 + +#### 109 查询结果展示组件 +- **任务描述**:实现查询结果展示界面 +- **技术要点**: + - 左侧汇总列表(13种匹配类型统计) + - 右侧详情列表(期号、红球、蓝球) + - 数字颜色标识(匹配红球红色、匹配蓝球蓝色、未匹配黑色) +- **依赖**:108 +- **优先级**:P0 + +#### 110 交互功能实现 +- **任务描述**:实现查询交互逻辑 +- **技术要点**: + - 点击汇总项显示详细记录 + - 扩展查询功能(前后n期) + - 扩展显示低匹配度结果(≤3个红球) +- **依赖**:109, 107 +- **优先级**:P0 + +#### 111 前端与后端集成 +- **任务描述**:前端调用 Wails API,完成查询流程 +- **技术要点**: + - Wails 绑定方法调用 + - 数据传递和格式化 + - 错误处理和提示 +- **依赖**:110 +- **优先级**:P0 + +--- + +## 2. Phase 2:数据管理 + +### 2.1 数据同步功能 + +#### 201 数据同步服务 +- **任务描述**:实现 MySQL 到 SQLite 的数据同步逻辑 +- **技术要点**: + - 增量同步(基于 `issue_number`) + - 数据校验和去重 + - 同步状态记录 +- **依赖**:103 +- **优先级**:P1 + +#### 202 同步触发机制 +- **任务描述**:实现同步触发方式 +- **技术要点**: + - 应用启动时自动同步 + - 手动触发同步 + - 定时同步(可选) +- **依赖**:201 +- **优先级**:P1 + +#### 203 同步状态展示 +- **任务描述**:前端展示同步状态和进度 +- **技术要点**: + - 同步进度条 + - 同步日志显示 + - 同步结果提示 +- **依赖**:202 +- **优先级**:P1 + +### 2.2 本地数据管理 + +#### 204 数据统计功能 +- **任务描述**:展示本地数据统计信息 +- **技术要点**: + - 数据总量统计 + - 最新期号显示 + - 数据更新时间 +- **依赖**:103 +- **优先级**:P1 + +#### 205 数据刷新功能 +- **任务描述**:手动刷新本地数据 +- **技术要点**: + - 刷新按钮 + - 刷新进度提示 + - 刷新结果反馈 +- **依赖**:202 +- **优先级**:P1 + +#### 206 数据备份与恢复 +- **任务描述**:实现数据备份和恢复功能 +- **技术要点**: + - 导出 SQLite 数据包 + - 导入数据包 + - 备份文件管理 +- **依赖**:103 +- **优先级**:P2 + +--- + +## 3. Phase 3:其他功能 + +### 3.1 版本更新 + +#### 301 更新检查功能 +- **任务描述**:实现版本更新检查 +- **技术要点**: + - 版本号管理(语义化版本格式) + - 远程版本检查接口(JSON 格式) + - 版本号比较逻辑 + - 更新提示界面 + - 检查触发机制(启动检查、手动检查) +- **依赖**:无 +- **优先级**:P2 +- **参考文档**:`docs/04-功能迭代/版本更新/任务规划.md` + +#### 302 更新下载和安装 +- **任务描述**:实现更新包下载和安装 +- **技术要点**: + - 更新包下载(支持断点续传) + - 下载进度展示 + - 文件校验(MD5/SHA256) + - 自动/手动安装 + - 安装前备份和失败回滚 + - 更新日志展示 +- **依赖**:301 +- **优先级**:P2 +- **参考文档**:`docs/04-功能迭代/版本更新/任务规划.md` + +### 3.2 离线数据包 + +#### 303 离线数据包管理 +- **任务描述**:离线数据包的下载、导入、更新 +- **技术要点**: + - 数据包下载 + - 数据包导入 + - 数据包更新检查 +- **依赖**:206 +- **优先级**:P2 + +### 3.3 授权管理 + +#### 304 授权码管理 +- **任务描述**:实现设备授权码管理 +- **技术要点**: + - 设备标识生成(基于主机名、用户目录、操作系统) + - 授权码输入和格式验证 + - 授权码与设备ID绑定 + - 授权状态存储(SQLite 本地数据库) + - 授权信息查询接口 +- **依赖**:无 +- **优先级**:P2 +- **参考文档**:`docs/04-功能迭代/授权码功能/授权码功能设计.md` + +#### 305 激活验证 +- **任务描述**:实现激活状态验证 +- **技术要点**: + - 应用启动时自动验证授权状态 + - 授权信息展示 + - 授权失效处理 + - 设备ID查询接口 +- **依赖**:304 +- **优先级**:P2 +- **参考文档**:`docs/04-功能迭代/授权码功能/授权码功能设计.md` + +--- + +## 4. 任务优先级说明 + +- **P0**:核心功能,必须完成,Phase 1 所有任务 +- **P1**:重要功能,Phase 2 主要任务 +- **P2**:辅助功能,Phase 3 所有任务 + +--- + +## 5. 开发顺序建议 + +### 第一阶段(核心功能) +1. 101 → 102 → 103(数据库基础) +2. 104 → 105(查询服务) +3. 106 → 107(API 层) +4. 108 → 109 → 110 → 111(前端界面) + +### 第二阶段(数据管理) +1. 201 → 202 → 203(数据同步) +2. 204 → 205(数据管理) +3. 206(数据备份,可选) + +### 第三阶段(其他功能) +1. 301 → 302(版本更新) +2. 303(离线数据包) +3. 304 → 305(授权管理) + +--- + +> 文档维护者:JueChen +> 创建时间:2026-01-07 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0b3455e --- /dev/null +++ b/go.mod @@ -0,0 +1,52 @@ +module ssq-desk + +go 1.24.0 + +require ( + github.com/wailsapp/wails/v2 v2.11.0 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.30.0 + modernc.org/sqlite v1.42.2 +) + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-sql-driver/mysql v1.7.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/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // 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/mattn/go-sqlite3 v1.14.22 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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.33.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.22.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1dac141 --- /dev/null +++ b/go.sum @@ -0,0 +1,138 @@ +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= +github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74= +modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/api/auth_api.go b/internal/api/auth_api.go new file mode 100644 index 0000000..ecd5115 --- /dev/null +++ b/internal/api/auth_api.go @@ -0,0 +1,87 @@ +package api + +import ( + "ssq-desk/internal/service" + "ssq-desk/internal/storage/repository" +) + +// AuthAPI 授权码 API +type AuthAPI struct { + authService *service.AuthService +} + +// NewAuthAPI 创建授权码 API +func NewAuthAPI() (*AuthAPI, error) { + repo, err := repository.NewSQLiteAuthRepository() + if err != nil { + return nil, err + } + + authService := service.NewAuthService(repo) + + return &AuthAPI{ + authService: authService, + }, nil +} + +// ActivateLicense 激活授权码 +func (api *AuthAPI) ActivateLicense(licenseCode string) (map[string]interface{}, error) { + if api.authService == nil { + newAPI, err := NewAuthAPI() + if err != nil { + return nil, err + } + api.authService = newAPI.authService + } + + err := api.authService.ValidateLicense(licenseCode) + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + status, err := api.authService.CheckAuthStatus() + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + return map[string]interface{}{ + "success": true, + "message": "激活成功", + "data": status, + }, nil +} + +// GetAuthStatus 获取授权状态 +func (api *AuthAPI) GetAuthStatus() (map[string]interface{}, error) { + if api.authService == nil { + newAPI, err := NewAuthAPI() + if err != nil { + return nil, err + } + api.authService = newAPI.authService + } + + status, err := api.authService.CheckAuthStatus() + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + return map[string]interface{}{ + "success": true, + "data": status, + }, nil +} + +// GetDeviceID 获取设备ID +func (api *AuthAPI) GetDeviceID() (string, error) { + return service.GetDeviceID() +} diff --git a/internal/api/backup_api.go b/internal/api/backup_api.go new file mode 100644 index 0000000..17f200e --- /dev/null +++ b/internal/api/backup_api.go @@ -0,0 +1,67 @@ +package api + +import ( + "ssq-desk/internal/service" +) + +// BackupAPI 数据备份 API +type BackupAPI struct { + backupService *service.BackupService +} + +// NewBackupAPI 创建数据备份 API +func NewBackupAPI() *BackupAPI { + return &BackupAPI{ + backupService: service.NewBackupService(), + } +} + +// Backup 备份数据 +func (api *BackupAPI) Backup() (map[string]interface{}, error) { + result, err := api.backupService.Backup() + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "backup_path": result.BackupPath, + "file_name": result.FileName, + "file_size": result.FileSize, + "created_at": result.CreatedAt, + }, nil +} + +// Restore 恢复数据 +func (api *BackupAPI) Restore(backupPath string) (map[string]interface{}, error) { + if err := api.backupService.Restore(backupPath); err != nil { + return nil, err + } + + return map[string]interface{}{ + "success": true, + "message": "数据恢复成功", + }, nil +} + +// ListBackups 列出所有备份 +func (api *BackupAPI) ListBackups() (map[string]interface{}, error) { + backups, err := api.backupService.ListBackups() + if err != nil { + return nil, err + } + + backupList := make([]map[string]interface{}, len(backups)) + for i, backup := range backups { + backupList[i] = map[string]interface{}{ + "backup_path": backup.BackupPath, + "file_name": backup.FileName, + "file_size": backup.FileSize, + "created_at": backup.CreatedAt, + } + } + + return map[string]interface{}{ + "backups": backupList, + "count": len(backupList), + }, nil +} diff --git a/internal/api/package_api.go b/internal/api/package_api.go new file mode 100644 index 0000000..689d55d --- /dev/null +++ b/internal/api/package_api.go @@ -0,0 +1,121 @@ +package api + +import ( + "ssq-desk/internal/service" +) + +// PackageAPI 离线数据包 API +type PackageAPI struct { + packageService *service.PackageService +} + +// NewPackageAPI 创建数据包 API +func NewPackageAPI() (*PackageAPI, error) { + packageService, err := service.NewPackageService() + if err != nil { + return nil, err + } + + return &PackageAPI{ + packageService: packageService, + }, nil +} + +// DownloadPackage 下载数据包 +func (api *PackageAPI) DownloadPackage(downloadURL string) (map[string]interface{}, error) { + if api.packageService == nil { + newAPI, err := NewPackageAPI() + if err != nil { + return nil, err + } + api.packageService = newAPI.packageService + } + + result, err := api.packageService.DownloadPackage(downloadURL, nil) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "file_path": result.FilePath, + "file_size": result.FileSize, + "duration": result.Duration, + }, nil +} + +// ImportPackage 导入数据包 +func (api *PackageAPI) ImportPackage(packagePath string) (map[string]interface{}, error) { + if api.packageService == nil { + newAPI, err := NewPackageAPI() + if err != nil { + return nil, err + } + api.packageService = newAPI.packageService + } + + result, err := api.packageService.ImportPackage(packagePath) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "imported_count": result.ImportedCount, + "updated_count": result.UpdatedCount, + "error_count": result.ErrorCount, + "duration": result.Duration, + }, nil +} + +// CheckPackageUpdate 检查数据包更新 +func (api *PackageAPI) CheckPackageUpdate(remoteURL string) (map[string]interface{}, error) { + if api.packageService == nil { + newAPI, err := NewPackageAPI() + if err != nil { + return nil, err + } + api.packageService = newAPI.packageService + } + + info, err := api.packageService.CheckPackageUpdate(remoteURL) + if err != nil { + return nil, err + } + + if info == nil { + return map[string]interface{}{ + "need_update": false, + }, nil + } + + return map[string]interface{}{ + "need_update": true, + "version": info.Version, + "total_count": info.TotalCount, + "latest_issue": info.LatestIssue, + "package_size": info.PackageSize, + "download_url": info.DownloadURL, + "release_date": info.ReleaseDate, + "checksum": info.CheckSum, + }, nil +} + +// ListLocalPackages 列出本地数据包 +func (api *PackageAPI) ListLocalPackages() (map[string]interface{}, error) { + if api.packageService == nil { + newAPI, err := NewPackageAPI() + if err != nil { + return nil, err + } + api.packageService = newAPI.packageService + } + + packages, err := api.packageService.ListLocalPackages() + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "packages": packages, + "count": len(packages), + }, nil +} diff --git a/internal/api/ssq_api.go b/internal/api/ssq_api.go new file mode 100644 index 0000000..205ad64 --- /dev/null +++ b/internal/api/ssq_api.go @@ -0,0 +1,55 @@ +package api + +import ( + "ssq-desk/internal/service" + "ssq-desk/internal/storage/repository" +) + +// SsqAPI 双色球查询 API +type SsqAPI struct { + queryService *service.QueryService +} + +// NewSsqAPI 创建双色球查询 API +func NewSsqAPI() (*SsqAPI, error) { + // 优先使用 MySQL(数据源) + repo, err := repository.NewMySQLSsqRepository() + if err != nil { + // MySQL 连接失败,降级使用本地 SQLite + repo, err = repository.NewSQLiteSsqRepository() + if err != nil { + return nil, err + } + } + + queryService := service.NewQueryService(repo) + + return &SsqAPI{ + queryService: queryService, + }, nil +} + +// QueryRequest 查询请求参数 +type QueryRequest struct { + RedBalls []int `json:"red_balls"` // 红球列表 + BlueBall int `json:"blue_ball"` // 蓝球(0表示不限制) + BlueBallRange []int `json:"blue_ball_range"` // 蓝球筛选范围 +} + +// QueryHistory 查询历史数据 +func (api *SsqAPI) QueryHistory(req QueryRequest) (*service.QueryResult, error) { + if api.queryService == nil { + // 重新初始化 + newAPI, err := NewSsqAPI() + if err != nil { + return nil, err + } + api.queryService = newAPI.queryService + } + + return api.queryService.Query(service.QueryRequest{ + RedBalls: req.RedBalls, + BlueBall: req.BlueBall, + BlueBallRange: req.BlueBallRange, + }) +} diff --git a/internal/api/sync_api.go b/internal/api/sync_api.go new file mode 100644 index 0000000..66c7a64 --- /dev/null +++ b/internal/api/sync_api.go @@ -0,0 +1,69 @@ +package api + +import ( + "ssq-desk/internal/service" + "ssq-desk/internal/storage/repository" +) + +// SyncAPI 数据同步 API +type SyncAPI struct { + syncService *service.SyncService +} + +// NewSyncAPI 创建数据同步 API +func NewSyncAPI() (*SyncAPI, error) { + // 获取 MySQL 和 SQLite Repository + mysqlRepo, err := repository.NewMySQLSsqRepository() + if err != nil { + return nil, err + } + + sqliteRepo, err := repository.NewSQLiteSsqRepository() + if err != nil { + return nil, err + } + + syncService := service.NewSyncService(mysqlRepo, sqliteRepo) + + return &SyncAPI{ + syncService: syncService, + }, nil +} + +// Sync 执行数据同步 +func (api *SyncAPI) Sync() (map[string]interface{}, error) { + if api.syncService == nil { + newAPI, err := NewSyncAPI() + if err != nil { + return nil, err + } + api.syncService = newAPI.syncService + } + + result, err := api.syncService.Sync() + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "total_count": result.TotalCount, + "synced_count": result.SyncedCount, + "new_count": result.NewCount, + "updated_count": result.UpdatedCount, + "error_count": result.ErrorCount, + "latest_issue": result.LatestIssue, + }, nil +} + +// GetSyncStatus 获取同步状态 +func (api *SyncAPI) GetSyncStatus() (map[string]interface{}, error) { + if api.syncService == nil { + newAPI, err := NewSyncAPI() + if err != nil { + return nil, err + } + api.syncService = newAPI.syncService + } + + return api.syncService.GetSyncStatus() +} diff --git a/internal/api/update_api.go b/internal/api/update_api.go new file mode 100644 index 0000000..667e40a --- /dev/null +++ b/internal/api/update_api.go @@ -0,0 +1,254 @@ +package api + +import ( + "context" + "encoding/json" + "ssq-desk/internal/service" + "time" + + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +// UpdateAPI 版本更新 API +type UpdateAPI struct { + updateService *service.UpdateService + ctx context.Context +} + +// NewUpdateAPI 创建版本更新 API +func NewUpdateAPI(checkURL string) (*UpdateAPI, error) { + updateService := service.NewUpdateService(checkURL) + + return &UpdateAPI{ + updateService: updateService, + }, nil +} + +// SetContext 设置上下文(用于事件推送) +func (api *UpdateAPI) SetContext(ctx context.Context) { + api.ctx = ctx +} + +// CheckUpdate 检查更新 +func (api *UpdateAPI) CheckUpdate() (map[string]interface{}, error) { + if api.updateService == nil { + return map[string]interface{}{ + "success": false, + "message": "更新服务未初始化", + }, nil + } + + result, err := api.updateService.CheckUpdate() + if err != nil { + errorMsg := err.Error() + if errorMsg == "" { + errorMsg = "未知错误" + } + return map[string]interface{}{ + "success": false, + "message": errorMsg, + }, nil + } + + if result == nil { + return map[string]interface{}{ + "success": false, + "message": "检查更新返回结果为空", + }, nil + } + + return map[string]interface{}{ + "success": true, + "data": result, + }, nil +} + +// GetCurrentVersion 获取当前版本号 +func (api *UpdateAPI) GetCurrentVersion() (map[string]interface{}, error) { + version := service.GetCurrentVersion() + + // 更新配置中的版本号 + if config, err := service.LoadUpdateConfig(); err == nil && config.CurrentVersion != version { + config.CurrentVersion = version + service.SaveUpdateConfig(config) + } + + return map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "version": version, + }, + }, nil +} + +// GetUpdateConfig 获取更新配置 +func (api *UpdateAPI) GetUpdateConfig() (map[string]interface{}, error) { + config, err := service.LoadUpdateConfig() + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + // 确保版本号是最新的 + latestVersion := service.GetCurrentVersion() + if config.CurrentVersion != latestVersion { + config.CurrentVersion = latestVersion + service.SaveUpdateConfig(config) + } + + return map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "current_version": config.CurrentVersion, + "last_check_time": config.LastCheckTime.Format("2006-01-02 15:04:05"), + "auto_check_enabled": config.AutoCheckEnabled, + "check_interval_minutes": config.CheckIntervalMinutes, + "check_url": config.CheckURL, + }, + }, nil +} + +// SetUpdateConfig 设置更新配置 +func (api *UpdateAPI) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) { + config, err := service.LoadUpdateConfig() + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + config.AutoCheckEnabled = autoCheckEnabled + config.CheckIntervalMinutes = checkIntervalMinutes + if checkURL != "" { + config.CheckURL = checkURL + // 如果 URL 改变,需要重新创建服务 + api.updateService = service.NewUpdateService(checkURL) + } + + if err := service.SaveUpdateConfig(config); err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + return map[string]interface{}{ + "success": true, + "message": "配置保存成功", + }, nil +} + +// DownloadUpdate 下载更新包(异步,通过事件推送进度) +func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}, error) { + if downloadURL == "" { + return map[string]interface{}{ + "success": false, + "message": "下载地址不能为空", + }, nil + } + + go func() { + progressCallback := func(progress float64, speed float64, downloaded int64, total int64) { + if api.ctx == nil { + return + } + // 确保进度值在 0-100 之间 + if progress < 0 { + progress = 0 + } else if progress > 100 { + progress = 100 + } + + progressInfo := map[string]interface{}{ + "progress": progress, + "speed": speed, + "downloaded": downloaded, + "total": total, + } + progressJSON, _ := json.Marshal(progressInfo) + runtime.EventsEmit(api.ctx, "download-progress", string(progressJSON)) + time.Sleep(10 * time.Millisecond) + } + + time.Sleep(100 * time.Millisecond) + result, err := service.DownloadUpdate(downloadURL, progressCallback) + + if api.ctx != nil { + if err != nil { + errorInfo := map[string]interface{}{"error": err.Error()} + errorJSON, _ := json.Marshal(errorInfo) + runtime.EventsEmit(api.ctx, "download-complete", string(errorJSON)) + } else { + resultInfo := map[string]interface{}{ + "success": true, + "file_path": result.FilePath, + "file_size": result.FileSize, + } + resultJSON, _ := json.Marshal(resultInfo) + runtime.EventsEmit(api.ctx, "download-complete", string(resultJSON)) + } + } + }() + + return map[string]interface{}{ + "success": true, + "message": "下载已开始", + }, nil +} + +// InstallUpdate 安装更新包 +func (api *UpdateAPI) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) { + return api.InstallUpdateWithHash(installerPath, autoRestart, "", "") +} + +// InstallUpdateWithHash 安装更新包(带哈希验证) +func (api *UpdateAPI) InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (map[string]interface{}, error) { + if installerPath == "" { + return map[string]interface{}{ + "success": false, + "message": "安装文件路径不能为空", + }, nil + } + + result, err := service.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType) + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + return map[string]interface{}{ + "success": result.Success, + "message": result.Message, + "data": result, + }, nil +} + +// VerifyUpdateFile 验证更新文件哈希值 +func (api *UpdateAPI) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) { + if filePath == "" { + return map[string]interface{}{ + "success": false, + "message": "文件路径不能为空", + }, nil + } + + valid, err := service.VerifyFileHash(filePath, expectedHash, hashType) + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, nil + } + + return map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "valid": valid, + }, + }, nil +} diff --git a/internal/database/mysql.go b/internal/database/mysql.go new file mode 100644 index 0000000..bb1aba3 --- /dev/null +++ b/internal/database/mysql.go @@ -0,0 +1,91 @@ +package database + +import ( + "fmt" + "ssq-desk/internal/storage/models" + "sync" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +var ( + mysqlDB *gorm.DB + mysqlOnce sync.Once +) + +// MySQLConfig MySQL 连接配置 +type MySQLConfig struct { + Host string + Port int + User string + Password string + Database string +} + +// GetMySQLConfig 获取 MySQL 配置(从配置文件或环境变量) +func GetMySQLConfig() *MySQLConfig { + return &MySQLConfig{ + Host: "39.99.243.191", + Port: 3306, + User: "u_ssq", + Password: "u_ssq@260106", + Database: "ssq_dev", // 需要根据实际情况修改数据库名 + } +} + +// InitMySQL 初始化 MySQL 连接 +func InitMySQL() (*gorm.DB, error) { + var err error + mysqlOnce.Do(func() { + config := GetMySQLConfig() + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.User, config.Password, config.Host, config.Port, config.Database) + + mysqlDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + return + } + + // 测试连接 + sqlDB, err2 := mysqlDB.DB() + if err2 != nil { + err = err2 + return + } + + if err2 = sqlDB.Ping(); err2 != nil { + err = err2 + return + } + + // 自动迁移表结构 + err2 = mysqlDB.AutoMigrate( + &models.SsqHistory{}, + &models.Authorization{}, + &models.Version{}, + ) + if err2 != nil { + err = fmt.Errorf("MySQL 表迁移失败: %v", err2) + return + } + }) + + if err != nil { + return nil, fmt.Errorf("MySQL 连接初始化失败: %v", err) + } + + return mysqlDB, nil +} + +// GetMySQL 获取 MySQL 连接实例 +func GetMySQL() *gorm.DB { + if mysqlDB == nil { + db, err := InitMySQL() + if err != nil { + return nil + } + return db + } + return mysqlDB +} diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go new file mode 100644 index 0000000..0ec4777 --- /dev/null +++ b/internal/database/sqlite.go @@ -0,0 +1,102 @@ +package database + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "sync" + + "ssq-desk/internal/storage/models" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + _ "modernc.org/sqlite" // 使用纯 Go 的 SQLite 驱动(不需要 CGO) +) + +var ( + sqliteDB *gorm.DB + sqliteOnce sync.Once +) + +// InitSQLite 初始化 SQLite 连接 +func InitSQLite() (*gorm.DB, error) { + var err error + sqliteOnce.Do(func() { + // 获取应用数据目录 + homeDir, err2 := os.UserHomeDir() + if err2 != nil { + err = fmt.Errorf("获取用户目录失败: %v", err2) + return + } + + // 创建数据目录 + dataDir := filepath.Join(homeDir, ".ssq-desk") + if err2 := os.MkdirAll(dataDir, 0755); err2 != nil { + err = fmt.Errorf("创建数据目录失败: %v", err2) + return + } + + // SQLite 数据库文件路径 + dbPath := filepath.Join(dataDir, "ssq.db") + + // 直接使用 database/sql 打开连接,确保使用 modernc.org/sqlite(纯 Go,不需要 CGO) + sqlDB, err2 := sql.Open("sqlite", dbPath) + if err2 != nil { + err = fmt.Errorf("SQLite 打开连接失败: %v", err2) + sqliteDB = nil + return + } + + // 测试连接 + if err2 = sqlDB.Ping(); err2 != nil { + err = fmt.Errorf("SQLite 连接测试失败: %v", err2) + sqlDB.Close() + sqliteDB = nil + return + } + + // 使用已打开的 database/sql 连接创建 GORM 实例 + // 使用 sqlite.Dialector 并指定连接 + sqliteDB, err2 = gorm.Open(sqlite.Dialector{Conn: sqlDB}, &gorm.Config{}) + if err2 != nil { + err = fmt.Errorf("SQLite GORM 初始化失败: %v", err2) + sqlDB.Close() + sqliteDB = nil + return + } + + // 自动迁移表结构(如果表已存在但结构不对,AutoMigrate 会尝试修改) + // 如果表结构完全不匹配,可能需要手动删除旧表 + err2 = sqliteDB.AutoMigrate( + &models.SsqHistory{}, + &models.Authorization{}, + &models.Version{}, + ) + if err2 != nil { + err = fmt.Errorf("SQLite 表迁移失败: %v", err2) + sqliteDB = nil + return + } + }) + + if err != nil { + return nil, err + } + + return sqliteDB, nil +} + +// GetSQLite 获取 SQLite 连接实例 +// 如果连接未初始化或初始化失败,返回 nil +func GetSQLite() *gorm.DB { + if sqliteDB == nil { + db, err := InitSQLite() + if err != nil { + // 初始化失败,返回 nil + return nil + } + sqliteDB = db + } + return sqliteDB +} diff --git a/internal/module/auth_module.go b/internal/module/auth_module.go new file mode 100644 index 0000000..8043b9c --- /dev/null +++ b/internal/module/auth_module.go @@ -0,0 +1,62 @@ +package module + +import ( + "context" + "ssq-desk/internal/api" +) + +// AuthModule 授权码模块 +type AuthModule struct { + BaseModule + authAPI *api.AuthAPI +} + +// NewAuthModule 创建授权码模块 +func NewAuthModule() (*AuthModule, error) { + // 延迟初始化,等到 Init() 方法调用时再创建 API(此时数据库已初始化) + return &AuthModule{ + BaseModule: BaseModule{ + name: "auth", + api: nil, // 延迟初始化 + }, + authAPI: nil, // 延迟初始化 + }, nil +} + +// Init 初始化模块 +func (m *AuthModule) Init(ctx context.Context) error { + if m.authAPI == nil { + authAPI, err := api.NewAuthAPI() + if err != nil { + return err + } + m.authAPI = authAPI + m.api = authAPI + } + return nil +} + +// Start 启动模块(检查授权状态) +func (m *AuthModule) Start(ctx context.Context) error { + if m.authAPI == nil { + return m.Init(ctx) + } + + status, err := m.authAPI.GetAuthStatus() + if err == nil { + if data, ok := status["data"].(map[string]interface{}); ok && data != nil { + if isActivated, ok := data["is_activated"].(bool); ok && !isActivated { + println("授权未激活,部分功能可能受限") + } else { + println("授权验证通过") + } + } + } + + return nil +} + +// AuthAPI 返回 Auth API(类型安全) +func (m *AuthModule) AuthAPI() *api.AuthAPI { + return m.authAPI +} diff --git a/internal/module/manager.go b/internal/module/manager.go new file mode 100644 index 0000000..0e41362 --- /dev/null +++ b/internal/module/manager.go @@ -0,0 +1,119 @@ +package module + +import ( + "context" + "fmt" + "sync" +) + +// Manager 模块管理器 +type Manager struct { + modules map[string]Module + mu sync.RWMutex +} + +// NewManager 创建模块管理器 +func NewManager() *Manager { + return &Manager{ + modules: make(map[string]Module), + } +} + +// Register 注册模块 +func (m *Manager) Register(module Module) error { + if module == nil { + return fmt.Errorf("模块不能为空") + } + + name := module.Name() + if name == "" { + return fmt.Errorf("模块名称不能为空") + } + + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.modules[name]; exists { + return fmt.Errorf("模块 %s 已存在", name) + } + + m.modules[name] = module + return nil +} + +// Get 获取模块 +func (m *Manager) Get(name string) (Module, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + + module, exists := m.modules[name] + return module, exists +} + +// GetAll 获取所有模块 +func (m *Manager) GetAll() []Module { + m.mu.RLock() + defer m.mu.RUnlock() + + modules := make([]Module, 0, len(m.modules)) + for _, module := range m.modules { + modules = append(modules, module) + } + return modules +} + +// InitAll 初始化所有模块 +func (m *Manager) InitAll(ctx context.Context) error { + m.mu.RLock() + defer m.mu.RUnlock() + + for name, module := range m.modules { + if err := module.Init(ctx); err != nil { + return fmt.Errorf("初始化模块 %s 失败: %v", name, err) + } + } + + return nil +} + +// StartAll 启动所有模块 +func (m *Manager) StartAll(ctx context.Context) error { + m.mu.RLock() + defer m.mu.RUnlock() + + for name, module := range m.modules { + if err := module.Start(ctx); err != nil { + return fmt.Errorf("启动模块 %s 失败: %v", name, err) + } + } + + return nil +} + +// StopAll 停止所有模块 +func (m *Manager) StopAll(ctx context.Context) error { + m.mu.RLock() + defer m.mu.RUnlock() + + for name, module := range m.modules { + if err := module.Stop(ctx); err != nil { + return fmt.Errorf("停止模块 %s 失败: %v", name, err) + } + } + + return nil +} + +// GetAPIs 获取所有模块的 API +func (m *Manager) GetAPIs() []interface{} { + m.mu.RLock() + defer m.mu.RUnlock() + + apis := make([]interface{}, 0, len(m.modules)) + for _, module := range m.modules { + if api := module.GetAPI(); api != nil { + apis = append(apis, api) + } + } + return apis +} diff --git a/internal/module/module.go b/internal/module/module.go new file mode 100644 index 0000000..a31f68a --- /dev/null +++ b/internal/module/module.go @@ -0,0 +1,52 @@ +package module + +import "context" + +// Module 模块接口 +type Module interface { + // Name 返回模块名称 + Name() string + + // Init 初始化模块 + Init(ctx context.Context) error + + // Start 启动模块 + Start(ctx context.Context) error + + // Stop 停止模块 + Stop(ctx context.Context) error + + // GetAPI 获取模块的 API 接口(供前端调用) + GetAPI() interface{} +} + +// BaseModule 基础模块实现 +type BaseModule struct { + name string + api interface{} +} + +// Name 返回模块名称 +func (m *BaseModule) Name() string { + return m.name +} + +// GetAPI 获取模块的 API 接口 +func (m *BaseModule) GetAPI() interface{} { + return m.api +} + +// Init 初始化模块(默认实现) +func (m *BaseModule) Init(ctx context.Context) error { + return nil +} + +// Start 启动模块(默认实现) +func (m *BaseModule) Start(ctx context.Context) error { + return nil +} + +// Stop 停止模块(默认实现) +func (m *BaseModule) Stop(ctx context.Context) error { + return nil +} diff --git a/internal/module/ssq_module.go b/internal/module/ssq_module.go new file mode 100644 index 0000000..985cdb2 --- /dev/null +++ b/internal/module/ssq_module.go @@ -0,0 +1,42 @@ +package module + +import ( + "context" + "ssq-desk/internal/api" +) + +// SsqModule 双色球查询模块 +type SsqModule struct { + BaseModule + ssqAPI *api.SsqAPI +} + +// NewSsqModule 创建双色球查询模块 +func NewSsqModule() (*SsqModule, error) { + // 延迟初始化,等到 Init() 方法调用时再创建 API(此时数据库已初始化) + return &SsqModule{ + BaseModule: BaseModule{ + name: "ssq", + api: nil, // 延迟初始化 + }, + ssqAPI: nil, // 延迟初始化 + }, nil +} + +// Init 初始化模块 +func (m *SsqModule) Init(ctx context.Context) error { + if m.ssqAPI == nil { + ssqAPI, err := api.NewSsqAPI() + if err != nil { + return err + } + m.ssqAPI = ssqAPI + m.api = ssqAPI + } + return nil +} + +// SsqAPI 返回 SSQ API(类型安全) +func (m *SsqModule) SsqAPI() *api.SsqAPI { + return m.ssqAPI +} diff --git a/internal/module/update_module.go b/internal/module/update_module.go new file mode 100644 index 0000000..d76d23c --- /dev/null +++ b/internal/module/update_module.go @@ -0,0 +1,94 @@ +package module + +import ( + "context" + "ssq-desk/internal/api" + "ssq-desk/internal/service" +) + +// UpdateModule 版本更新模块 +type UpdateModule struct { + BaseModule + updateAPI *api.UpdateAPI + checkURL string // 版本检查接口 URL +} + +// NewUpdateModule 创建版本更新模块 +func NewUpdateModule() (*UpdateModule, error) { + // 从配置文件读取检查 URL + config, err := service.LoadUpdateConfig() + if err != nil { + // 配置加载失败,使用默认值 + config = &service.UpdateConfig{} + } + + checkURL := config.CheckURL + if checkURL == "" { + // 如果配置中没有,使用默认地址 + checkURL = "https://img.1216.top/ssq/last-version.json" + } + + updateAPI, err := api.NewUpdateAPI(checkURL) + if err != nil { + return nil, err + } + + return &UpdateModule{ + BaseModule: BaseModule{ + name: "update", + api: updateAPI, + }, + updateAPI: updateAPI, + checkURL: checkURL, + }, nil +} + +// Init 初始化模块 +func (m *UpdateModule) Init(ctx context.Context) error { + if m.updateAPI == nil { + updateAPI, err := api.NewUpdateAPI(m.checkURL) + if err != nil { + return err + } + m.updateAPI = updateAPI + m.api = updateAPI + } + // 设置 context 以便推送事件 + if m.updateAPI != nil { + m.updateAPI.SetContext(ctx) + } + return nil +} + +// Start 启动模块(检查更新配置,决定是否自动检查) +func (m *UpdateModule) Start(ctx context.Context) error { + if m.updateAPI == nil { + return m.Init(ctx) + } + + // 加载配置 + config, err := service.LoadUpdateConfig() + if err != nil { + // 配置加载失败不影响启动,只记录日志 + return nil + } + + // 如果启用了自动检查且满足检查条件,则检查更新 + if config.ShouldCheckUpdate() && config.CheckURL != "" { + // 异步检查更新,不阻塞启动流程 + go func() { + _, err := m.updateAPI.CheckUpdate() + if err == nil { + // 更新最后检查时间 + config.UpdateLastCheckTime() + } + }() + } + + return nil +} + +// UpdateAPI 返回 Update API(类型安全) +func (m *UpdateModule) UpdateAPI() *api.UpdateAPI { + return m.updateAPI +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go new file mode 100644 index 0000000..a9f2cf1 --- /dev/null +++ b/internal/service/auth_service.go @@ -0,0 +1,215 @@ +package service + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "os" + "runtime" + "ssq-desk/internal/database" + "ssq-desk/internal/storage/models" + "ssq-desk/internal/storage/repository" + "time" + + "gorm.io/gorm" +) + +// AuthService 授权服务 +type AuthService struct { + repo repository.AuthRepository +} + +// NewAuthService 创建授权服务 +func NewAuthService(repo repository.AuthRepository) *AuthService { + return &AuthService{repo: repo} +} + +// GetDeviceID 获取设备ID(基于硬件信息生成) +func GetDeviceID() (string, error) { + var deviceInfo string + + // 获取主机名 + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + // 获取用户目录 + homeDir, err := os.UserHomeDir() + if err != nil { + homeDir = "unknown" + } + + // 组合设备信息 + deviceInfo = fmt.Sprintf("%s-%s-%s", hostname, homeDir, runtime.GOOS) + + // 生成 MD5 作为设备ID + hash := md5.Sum([]byte(deviceInfo)) + deviceID := hex.EncodeToString(hash[:]) + + return deviceID, nil +} + +// ValidateLicenseFormat 验证授权码格式 +func ValidateLicenseFormat(licenseCode string) error { + if licenseCode == "" { + return fmt.Errorf("授权码不能为空") + } + + // 去除空格和连字符 + cleaned := "" + for _, c := range licenseCode { + if c != ' ' && c != '-' { + cleaned += string(c) + } + } + + // 格式验证:至少16位,只包含字母和数字 + if len(cleaned) < 16 { + return fmt.Errorf("授权码长度不足,至少需要16位字符") + } + + if len(cleaned) > 100 { + return fmt.Errorf("授权码长度过长,最多100位字符") + } + + // 验证字符:只允许字母和数字 + for _, c := range cleaned { + if !((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + return fmt.Errorf("授权码只能包含字母和数字") + } + } + + return nil +} + +// ValidateLicenseFromRemote 从远程数据库验证授权码 +func ValidateLicenseFromRemote(licenseCode string) error { + // 获取 MySQL 连接 + mysqlDB := database.GetMySQL() + if mysqlDB == nil { + return fmt.Errorf("无法连接远程数据库,无法验证授权码") + } + + // 清理授权码(去除空格和连字符),与格式验证保持一致 + cleaned := "" + for _, c := range licenseCode { + if c != ' ' && c != '-' { + cleaned += string(c) + } + } + + // 查询授权码是否存在且有效(支持原始格式和清理后格式) + var auth models.Authorization + err := mysqlDB.Where("(license_code = ? OR license_code = ?) AND status = ?", licenseCode, cleaned, 1).First(&auth).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("授权码无效或不存在") + } + return fmt.Errorf("验证授权码时发生错误: %v", err) + } + + // 检查是否过期 + if auth.ExpiresAt != nil && auth.ExpiresAt.Before(time.Now()) { + return fmt.Errorf("授权码已过期") + } + + return nil +} + +// ValidateLicense 验证授权码 +func (s *AuthService) ValidateLicense(licenseCode string) error { + // 格式验证 + if err := ValidateLicenseFormat(licenseCode); err != nil { + return err + } + + // 从远程数据库验证授权码有效性 + if err := ValidateLicenseFromRemote(licenseCode); err != nil { + return err + } + + // 获取设备ID + deviceID, err := GetDeviceID() + if err != nil { + return fmt.Errorf("获取设备ID失败: %v", err) + } + + // 保存授权信息到本地 + auth := &models.Authorization{ + LicenseCode: licenseCode, + DeviceID: deviceID, + ActivatedAt: time.Now(), + Status: 1, + } + + // 检查是否已存在 + existing, err := s.repo.GetByLicenseCode(licenseCode) + if err == nil && existing != nil { + // 更新现有授权 + existing.DeviceID = deviceID + existing.ActivatedAt = time.Now() + existing.Status = 1 + return s.repo.Update(existing) + } + + // 创建新授权 + return s.repo.Create(auth) +} + +// CheckAuthStatus 检查授权状态 +func (s *AuthService) CheckAuthStatus() (*AuthStatus, error) { + deviceID, err := GetDeviceID() + if err != nil { + return nil, fmt.Errorf("获取设备ID失败: %v", err) + } + + auth, err := s.repo.GetByDeviceID(deviceID) + if err != nil { + if err == gorm.ErrRecordNotFound { + return &AuthStatus{ + IsActivated: false, + Message: "未激活", + }, nil + } + return nil, err + } + + // 检查状态 + if auth.Status != 1 { + return &AuthStatus{ + IsActivated: false, + Message: "授权已失效", + }, nil + } + + // 检查过期时间 + if auth.ExpiresAt != nil && auth.ExpiresAt.Before(time.Now()) { + return &AuthStatus{ + IsActivated: false, + Message: "授权已过期", + }, nil + } + + return &AuthStatus{ + IsActivated: true, + LicenseCode: auth.LicenseCode, + ActivatedAt: auth.ActivatedAt, + ExpiresAt: auth.ExpiresAt, + Message: "已激活", + }, nil +} + +// AuthStatus 授权状态 +type AuthStatus struct { + IsActivated bool `json:"is_activated"` + LicenseCode string `json:"license_code,omitempty"` + ActivatedAt time.Time `json:"activated_at,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + Message string `json:"message"` +} + +// GetAuthInfo 获取授权信息 +func (s *AuthService) GetAuthInfo() (*AuthStatus, error) { + return s.CheckAuthStatus() +} diff --git a/internal/service/backup_service.go b/internal/service/backup_service.go new file mode 100644 index 0000000..30bd1e1 --- /dev/null +++ b/internal/service/backup_service.go @@ -0,0 +1,287 @@ +package service + +import ( + "archive/zip" + "encoding/json" + "fmt" + "os" + "path/filepath" + "ssq-desk/internal/database" + "time" +) + +// BackupService 数据备份服务 +type BackupService struct{} + +// NewBackupService 创建备份服务 +func NewBackupService() *BackupService { + return &BackupService{} +} + +// BackupResult 备份结果 +type BackupResult struct { + BackupPath string `json:"backup_path"` + FileName string `json:"file_name"` + FileSize int64 `json:"file_size"` + CreatedAt string `json:"created_at"` +} + +// Backup 备份 SQLite 数据库 +func (s *BackupService) Backup() (*BackupResult, error) { + // 获取 SQLite 数据库路径 + appDataDir, err := os.UserConfigDir() + if err != nil { + return nil, fmt.Errorf("获取用户配置目录失败: %v", err) + } + + dbPath := filepath.Join(appDataDir, "ssq-desk", "data", "ssq.db") + + // 检查数据库文件是否存在 + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + return nil, fmt.Errorf("数据库文件不存在: %s", dbPath) + } + + // 创建备份目录 + backupDir := filepath.Join(appDataDir, "ssq-desk", "backups") + if err := os.MkdirAll(backupDir, 0755); err != nil { + return nil, fmt.Errorf("创建备份目录失败: %v", err) + } + + // 生成备份文件名(带时间戳) + timestamp := time.Now().Format("20060102-150405") + backupFileName := fmt.Sprintf("ssq-backup-%s.zip", timestamp) + backupPath := filepath.Join(backupDir, backupFileName) + + // 创建 ZIP 文件 + zipFile, err := os.Create(backupPath) + if err != nil { + return nil, fmt.Errorf("创建备份文件失败: %v", err) + } + defer zipFile.Close() + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // 添加数据库文件到 ZIP + dbFile, err := os.Open(dbPath) + if err != nil { + return nil, fmt.Errorf("打开数据库文件失败: %v", err) + } + defer dbFile.Close() + + dbInfo, err := dbFile.Stat() + if err != nil { + return nil, fmt.Errorf("获取数据库文件信息失败: %v", err) + } + + dbHeader, err := zip.FileInfoHeader(dbInfo) + if err != nil { + return nil, fmt.Errorf("创建 ZIP 文件头失败: %v", err) + } + dbHeader.Name = "ssq.db" + dbHeader.Method = zip.Deflate + + dbWriter, err := zipWriter.CreateHeader(dbHeader) + if err != nil { + return nil, fmt.Errorf("创建 ZIP 写入器失败: %v", err) + } + + if _, err := dbWriter.Write([]byte{}); err != nil { + return nil, fmt.Errorf("写入 ZIP 文件失败: %v", err) + } + + // 重新写入数据库内容 + if _, err := dbFile.Seek(0, 0); err != nil { + return nil, fmt.Errorf("重置文件指针失败: %v", err) + } + + buffer := make([]byte, 1024*1024) // 1MB buffer + for { + n, err := dbFile.Read(buffer) + if n > 0 { + if _, err := dbWriter.Write(buffer[:n]); err != nil { + return nil, fmt.Errorf("写入数据库内容失败: %v", err) + } + } + if err != nil { + break + } + } + + // 添加元数据文件 + metaData := map[string]interface{}{ + "backup_time": time.Now().Format("2006-01-02 15:04:05"), + "version": "1.0", + } + + metaWriter, err := zipWriter.Create("metadata.json") + if err != nil { + return nil, fmt.Errorf("创建元数据文件失败: %v", err) + } + + metaJSON, err := json.Marshal(metaData) + if err != nil { + return nil, fmt.Errorf("序列化元数据失败: %v", err) + } + + if _, err := metaWriter.Write(metaJSON); err != nil { + return nil, fmt.Errorf("写入元数据失败: %v", err) + } + + // 获取备份文件大小 + fileInfo, err := zipFile.Stat() + if err != nil { + return nil, fmt.Errorf("获取备份文件信息失败: %v", err) + } + + return &BackupResult{ + BackupPath: backupPath, + FileName: backupFileName, + FileSize: fileInfo.Size(), + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + }, nil +} + +// Restore 恢复数据 +func (s *BackupService) Restore(backupPath string) error { + // 检查备份文件是否存在 + if _, err := os.Stat(backupPath); os.IsNotExist(err) { + return fmt.Errorf("备份文件不存在: %s", backupPath) + } + + // 打开 ZIP 文件 + zipReader, err := zip.OpenReader(backupPath) + if err != nil { + return fmt.Errorf("打开备份文件失败: %v", err) + } + defer zipReader.Close() + + // 获取 SQLite 数据库路径 + appDataDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("获取用户配置目录失败: %v", err) + } + + dataDir := filepath.Join(appDataDir, "ssq-desk", "data") + if err := os.MkdirAll(dataDir, 0755); err != nil { + return fmt.Errorf("创建数据目录失败: %v", err) + } + + dbPath := filepath.Join(dataDir, "ssq.db") + + // 备份当前数据库(如果存在) + if _, err := os.Stat(dbPath); err == nil { + backupName := fmt.Sprintf("ssq.db.bak.%s", time.Now().Format("20060102-150405")) + backupPath := filepath.Join(dataDir, backupName) + if err := copyFile(dbPath, backupPath); err != nil { + return fmt.Errorf("备份当前数据库失败: %v", err) + } + } + + // 查找数据库文件 + var dbFile *zip.File + for _, file := range zipReader.File { + if file.Name == "ssq.db" { + dbFile = file + break + } + } + + if dbFile == nil { + return fmt.Errorf("备份文件中未找到数据库文件") + } + + // 解压数据库文件 + rc, err := dbFile.Open() + if err != nil { + return fmt.Errorf("打开数据库文件失败: %v", err) + } + defer rc.Close() + + // 创建新的数据库文件 + newDBFile, err := os.Create(dbPath) + if err != nil { + return fmt.Errorf("创建数据库文件失败: %v", err) + } + defer newDBFile.Close() + + // 复制数据 + buffer := make([]byte, 1024*1024) // 1MB buffer + for { + n, err := rc.Read(buffer) + if n > 0 { + if _, err := newDBFile.Write(buffer[:n]); err != nil { + return fmt.Errorf("写入数据库文件失败: %v", err) + } + } + if err != nil { + break + } + } + + // 重新初始化数据库连接 + _, err = database.InitSQLite() + if err != nil { + return fmt.Errorf("重新初始化数据库失败: %v", err) + } + + return nil +} + +// ListBackups 列出所有备份文件 +func (s *BackupService) ListBackups() ([]BackupResult, error) { + appDataDir, err := os.UserConfigDir() + if err != nil { + return nil, fmt.Errorf("获取用户配置目录失败: %v", err) + } + + backupDir := filepath.Join(appDataDir, "ssq-desk", "backups") + + // 检查备份目录是否存在 + if _, err := os.Stat(backupDir); os.IsNotExist(err) { + return []BackupResult{}, nil + } + + files, err := os.ReadDir(backupDir) + if err != nil { + return nil, fmt.Errorf("读取备份目录失败: %v", err) + } + + var backups []BackupResult + for _, file := range files { + if filepath.Ext(file.Name()) == ".zip" { + filePath := filepath.Join(backupDir, file.Name()) + fileInfo, err := file.Info() + if err != nil { + continue + } + + backups = append(backups, BackupResult{ + BackupPath: filePath, + FileName: file.Name(), + FileSize: fileInfo.Size(), + CreatedAt: fileInfo.ModTime().Format("2006-01-02 15:04:05"), + }) + } + } + + return backups, nil +} + +// copyFile 复制文件 +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = destFile.ReadFrom(sourceFile) + return err +} diff --git a/internal/service/package_service.go b/internal/service/package_service.go new file mode 100644 index 0000000..6c2b2bb --- /dev/null +++ b/internal/service/package_service.go @@ -0,0 +1,281 @@ +package service + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "ssq-desk/internal/storage/repository" + "time" +) + +// PackageService 离线数据包服务 +type PackageService struct { + sqliteRepo repository.SsqRepository +} + +// NewPackageService 创建数据包服务 +func NewPackageService() (*PackageService, error) { + repo, err := repository.NewSQLiteSsqRepository() + if err != nil { + return nil, err + } + + return &PackageService{ + sqliteRepo: repo, + }, nil +} + +// PackageInfo 数据包信息 +type PackageInfo struct { + Version string `json:"version"` // 数据包版本 + TotalCount int `json:"total_count"` // 数据总数 + LatestIssue string `json:"latest_issue"` // 最新期号 + PackageSize int64 `json:"package_size"` // 包大小 + DownloadURL string `json:"download_url"` // 下载地址 + ReleaseDate string `json:"release_date"` // 发布日期 + CheckSum string `json:"checksum"` // 校验和 +} + +// PackageDownloadResult 数据包下载结果 +type PackageDownloadResult struct { + FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` + Duration string `json:"duration"` // 下载耗时 +} + +// ImportResult 导入结果 +type ImportResult struct { + ImportedCount int `json:"imported_count"` // 导入数量 + UpdatedCount int `json:"updated_count"` // 更新数量 + ErrorCount int `json:"error_count"` // 错误数量 + Duration string `json:"duration"` // 导入耗时 +} + +// DownloadPackage 下载数据包 +func (s *PackageService) DownloadPackage(downloadURL string, progressCallback func(int64, int64)) (*PackageDownloadResult, error) { + startTime := time.Now() + + // 创建下载目录 + appDataDir, err := os.UserConfigDir() + if err != nil { + return nil, fmt.Errorf("获取用户配置目录失败: %v", err) + } + + downloadDir := filepath.Join(appDataDir, "ssq-desk", "packages") + if err := os.MkdirAll(downloadDir, 0755); err != nil { + return nil, fmt.Errorf("创建下载目录失败: %v", err) + } + + // 生成文件名 + filename := filepath.Base(downloadURL) + if filename == "" || filename == "." { + filename = fmt.Sprintf("ssq-data-%s.zip", time.Now().Format("20060102-150405")) + } + filePath := filepath.Join(downloadDir, filename) + + // 创建文件 + out, err := os.Create(filePath) + if err != nil { + return nil, fmt.Errorf("创建文件失败: %v", err) + } + defer out.Close() + + // 发起 HTTP 请求 + resp, err := http.Get(downloadURL) + if err != nil { + return nil, fmt.Errorf("下载失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("下载失败: HTTP %d", resp.StatusCode) + } + + // 获取文件大小 + totalSize := resp.ContentLength + + // 复制数据并显示进度 + var written int64 + buffer := make([]byte, 32*1024) // 32KB buffer + + for { + nr, er := resp.Body.Read(buffer) + if nr > 0 { + nw, ew := out.Write(buffer[0:nr]) + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + ew = fmt.Errorf("无效写入结果") + } + } + written += int64(nw) + if ew != nil { + err = ew + break + } + if nr != nw { + err = io.ErrShortWrite + break + } + + // 调用进度回调 + if progressCallback != nil { + progressCallback(written, totalSize) + } + } + if er != nil { + if er != io.EOF { + err = er + } + break + } + } + + if err != nil { + os.Remove(filePath) + return nil, fmt.Errorf("写入文件失败: %v", err) + } + + // 获取文件信息 + fileInfo, err := out.Stat() + if err != nil { + return nil, fmt.Errorf("获取文件信息失败: %v", err) + } + + duration := time.Since(startTime).String() + + return &PackageDownloadResult{ + FilePath: filePath, + FileSize: fileInfo.Size(), + Duration: duration, + }, nil +} + +// ImportPackage 导入数据包 +func (s *PackageService) ImportPackage(packagePath string) (*ImportResult, error) { + startTime := time.Now() + result := &ImportResult{} + + // 检查文件是否存在 + if _, err := os.Stat(packagePath); os.IsNotExist(err) { + return nil, fmt.Errorf("数据包文件不存在: %s", packagePath) + } + + // 打开 ZIP 文件 + zipReader, err := zip.OpenReader(packagePath) + if err != nil { + return nil, fmt.Errorf("打开数据包失败: %v", err) + } + defer zipReader.Close() + + // 查找数据文件(JSON 格式) + var dataFile *zip.File + for _, file := range zipReader.File { + if filepath.Ext(file.Name) == ".json" { + dataFile = file + break + } + } + + if dataFile == nil { + return nil, fmt.Errorf("数据包中未找到 JSON 数据文件") + } + + // 读取数据文件 + rc, err := dataFile.Open() + if err != nil { + return nil, fmt.Errorf("打开数据文件失败: %v", err) + } + defer rc.Close() + + // 解析 JSON + var histories []map[string]interface{} + decoder := json.NewDecoder(rc) + if err := decoder.Decode(&histories); err != nil { + return nil, fmt.Errorf("解析数据文件失败: %v", err) + } + + // 导入数据 + // TODO: 实际导入逻辑需要根据数据包格式实现 + // 这里只是框架代码,需要将 JSON 数据转换为 SsqHistory 并插入数据库 + for range histories { + // 转换为 SsqHistory 结构(这里需要根据实际数据格式转换) + // 简化处理:直接创建记录 + // 实际应用中需要根据数据包格式解析 + result.ImportedCount++ + } + + // TODO: 实际导入逻辑需要根据数据包格式实现 + // 这里只是框架代码 + + result.Duration = time.Since(startTime).String() + return result, nil +} + +// CheckPackageUpdate 检查数据包更新 +func (s *PackageService) CheckPackageUpdate(remoteURL string) (*PackageInfo, error) { + // 发起 HTTP 请求获取数据包信息 + resp, err := http.Get(remoteURL) + if err != nil { + return nil, fmt.Errorf("获取数据包信息失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("获取数据包信息失败: HTTP %d", resp.StatusCode) + } + + // 解析 JSON + var info PackageInfo + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&info); err != nil { + return nil, fmt.Errorf("解析数据包信息失败: %v", err) + } + + // 获取本地最新期号 + localLatestIssue, err := s.sqliteRepo.GetLatestIssue() + if err != nil { + return nil, fmt.Errorf("获取本地最新期号失败: %v", err) + } + + // 比较是否需要更新 + // 如果远程最新期号大于本地,则需要更新 + if info.LatestIssue > localLatestIssue { + return &info, nil + } + + return nil, nil // 不需要更新 +} + +// ListLocalPackages 列出本地数据包 +func (s *PackageService) ListLocalPackages() ([]string, error) { + appDataDir, err := os.UserConfigDir() + if err != nil { + return nil, fmt.Errorf("获取用户配置目录失败: %v", err) + } + + packageDir := filepath.Join(appDataDir, "ssq-desk", "packages") + + // 检查目录是否存在 + if _, err := os.Stat(packageDir); os.IsNotExist(err) { + return []string{}, nil + } + + files, err := os.ReadDir(packageDir) + if err != nil { + return nil, fmt.Errorf("读取数据包目录失败: %v", err) + } + + var packages []string + for _, file := range files { + if filepath.Ext(file.Name()) == ".zip" { + packages = append(packages, filepath.Join(packageDir, file.Name())) + } + } + + return packages, nil +} diff --git a/internal/service/query_service.go b/internal/service/query_service.go new file mode 100644 index 0000000..1774218 --- /dev/null +++ b/internal/service/query_service.go @@ -0,0 +1,162 @@ +package service + +import ( + "fmt" + "ssq-desk/internal/storage/models" + "ssq-desk/internal/storage/repository" +) + +// QueryService 查询服务 +type QueryService struct { + repo repository.SsqRepository +} + +// NewQueryService 创建查询服务 +func NewQueryService(repo repository.SsqRepository) *QueryService { + return &QueryService{repo: repo} +} + +// QueryRequest 查询请求 +type QueryRequest struct { + RedBalls []int `json:"red_balls"` // 红球列表(最多6个) + BlueBall int `json:"blue_ball"` // 蓝球(0表示不限制) + BlueBallRange []int `json:"blue_ball_range"` // 蓝球筛选范围 +} + +// QueryResult 查询结果 +type QueryResult struct { + Total int64 `json:"total"` // 总记录数 + Summary []MatchSummary `json:"summary"` // 分类统计 + Details []models.SsqHistory `json:"details"` // 详细记录 +} + +// MatchSummary 匹配统计 +type MatchSummary struct { + Type string `json:"type"` // 匹配类型:如 "6红1蓝" + Count int `json:"count"` // 匹配数量 + Histories []models.SsqHistory `json:"histories"` // 匹配的记录 +} + +// Query 执行查询 +func (s *QueryService) Query(req QueryRequest) (*QueryResult, error) { + // 验证输入 + if err := s.validateRequest(req); err != nil { + return nil, err + } + + // 查询数据 + histories, err := s.repo.FindByRedAndBlue(req.RedBalls, req.BlueBall, req.BlueBallRange) + if err != nil { + return nil, fmt.Errorf("查询失败: %v", err) + } + + // 处理结果:分类统计 + summary := s.calculateSummary(histories, req.RedBalls, req.BlueBall) + + return &QueryResult{ + Total: int64(len(histories)), + Summary: summary, + Details: histories, + }, nil +} + +// validateRequest 验证请求参数 +func (s *QueryService) validateRequest(req QueryRequest) error { + // 验证红球 + if len(req.RedBalls) > 6 { + return fmt.Errorf("红球数量不能超过6个") + } + for _, ball := range req.RedBalls { + if ball < 1 || ball > 33 { + return fmt.Errorf("红球号码必须在1-33之间: %d", ball) + } + } + + // 验证蓝球 + if req.BlueBall > 0 { + if req.BlueBall < 1 || req.BlueBall > 16 { + return fmt.Errorf("蓝球号码必须在1-16之间: %d", req.BlueBall) + } + } + + // 验证蓝球范围 + for _, ball := range req.BlueBallRange { + if ball < 1 || ball > 16 { + return fmt.Errorf("蓝球筛选范围必须在1-16之间: %d", ball) + } + } + + return nil +} + +// calculateSummary 计算分类统计 +func (s *QueryService) calculateSummary(histories []models.SsqHistory, redBalls []int, blueBall int) []MatchSummary { + if len(redBalls) == 0 { + return []MatchSummary{} + } + + // 创建红球集合,用于快速查找 + redBallSet := make(map[int]bool) + for _, ball := range redBalls { + redBallSet[ball] = true + } + + // 初始化统计 + typeCounts := make(map[string][]models.SsqHistory) + + // 遍历每条记录,计算匹配度 + for _, history := range histories { + // 统计匹配的红球数量 + matchedRedCount := 0 + if redBallSet[history.RedBall1] { + matchedRedCount++ + } + if redBallSet[history.RedBall2] { + matchedRedCount++ + } + if redBallSet[history.RedBall3] { + matchedRedCount++ + } + if redBallSet[history.RedBall4] { + matchedRedCount++ + } + if redBallSet[history.RedBall5] { + matchedRedCount++ + } + if redBallSet[history.RedBall6] { + matchedRedCount++ + } + + // 判断蓝球是否匹配 + blueMatched := false + if blueBall > 0 { + blueMatched = history.BlueBall == blueBall + } else { + blueMatched = true // 未指定蓝球时,视为匹配 + } + + // 生成类型键 + typeKey := fmt.Sprintf("%d红", matchedRedCount) + if blueMatched { + typeKey += "1蓝" + } + + typeCounts[typeKey] = append(typeCounts[typeKey], history) + } + + // 转换为结果格式,按匹配度排序 + summary := make([]MatchSummary, 0) + types := []string{"6红1蓝", "6红", "5红1蓝", "5红", "4红1蓝", "4红", "3红1蓝", "3红", "2红1蓝", "2红", "1红1蓝", "1红", "0红1蓝", "0红"} + + for _, t := range types { + if histories, ok := typeCounts[t]; ok { + summary = append(summary, MatchSummary{ + Type: t, + Count: len(histories), + Histories: histories, + }) + } + } + + return summary +} diff --git a/internal/service/sync_service.go b/internal/service/sync_service.go new file mode 100644 index 0000000..e5f4c1e --- /dev/null +++ b/internal/service/sync_service.go @@ -0,0 +1,148 @@ +package service + +import ( + "fmt" + "ssq-desk/internal/storage/models" + "ssq-desk/internal/storage/repository" +) + +// SyncService 数据同步服务 +type SyncService struct { + mysqlRepo repository.SsqRepository + sqliteRepo repository.SsqRepository +} + +// NewSyncService 创建同步服务 +func NewSyncService(mysqlRepo, sqliteRepo repository.SsqRepository) *SyncService { + return &SyncService{ + mysqlRepo: mysqlRepo, + sqliteRepo: sqliteRepo, + } +} + +// SyncResult 同步结果 +type SyncResult struct { + TotalCount int `json:"total_count"` // 远程数据总数 + SyncedCount int `json:"synced_count"` // 已同步数量 + NewCount int `json:"new_count"` // 新增数量 + UpdatedCount int `json:"updated_count"` // 更新数量 + ErrorCount int `json:"error_count"` // 错误数量 + LatestIssue string `json:"latest_issue"` // 最新期号 +} + +// Sync 执行数据同步(增量同步) +func (s *SyncService) Sync() (*SyncResult, error) { + result := &SyncResult{} + + // 获取本地最新期号 + localLatestIssue, err := s.sqliteRepo.GetLatestIssue() + if err != nil { + return nil, fmt.Errorf("获取本地最新期号失败: %v", err) + } + + // 获取远程所有数据 + remoteHistories, err := s.mysqlRepo.FindAll() + if err != nil { + return nil, fmt.Errorf("获取远程数据失败: %v", err) + } + + result.TotalCount = len(remoteHistories) + + if len(remoteHistories) == 0 { + return result, nil + } + + // 确定最新期号 + if len(remoteHistories) > 0 { + result.LatestIssue = remoteHistories[0].IssueNumber + } + + // 增量同步:只同步本地没有的数据 + if localLatestIssue == "" { + // 首次同步,全量同步 + for _, history := range remoteHistories { + if err := s.sqliteRepo.Create(&history); err != nil { + result.ErrorCount++ + continue + } + result.NewCount++ + result.SyncedCount++ + } + } else { + // 增量同步:同步期号大于本地最新期号的数据 + for _, history := range remoteHistories { + // 如果期号小于等于本地最新期号,跳过 + if history.IssueNumber <= localLatestIssue { + continue + } + + // 检查本地是否已存在(基于期号) + localHistory, err := s.sqliteRepo.FindByIssue(history.IssueNumber) + if err == nil && localHistory != nil { + // 已存在,检查是否需要更新 + if s.needUpdate(localHistory, &history) { + // 更新逻辑(目前使用创建,如需更新可扩展) + result.UpdatedCount++ + } + continue + } + + // 新数据,插入 + if err := s.sqliteRepo.Create(&history); err != nil { + result.ErrorCount++ + continue + } + result.NewCount++ + result.SyncedCount++ + } + } + + return result, nil +} + +// needUpdate 判断是否需要更新 +func (s *SyncService) needUpdate(local, remote *models.SsqHistory) bool { + // 简单比较:如果任何字段不同,则认为需要更新 + return local.RedBall1 != remote.RedBall1 || + local.RedBall2 != remote.RedBall2 || + local.RedBall3 != remote.RedBall3 || + local.RedBall4 != remote.RedBall4 || + local.RedBall5 != remote.RedBall5 || + local.RedBall6 != remote.RedBall6 || + local.BlueBall != remote.BlueBall +} + +// GetSyncStatus 获取同步状态 +func (s *SyncService) GetSyncStatus() (map[string]interface{}, error) { + // 获取本地统计 + localCount, err := s.sqliteRepo.Count() + if err != nil { + return nil, err + } + + localLatestIssue, err := s.sqliteRepo.GetLatestIssue() + if err != nil { + return nil, err + } + + // 获取远程统计 + remoteCount, err := s.mysqlRepo.Count() + if err != nil { + // 远程连接失败不影响本地状态 + remoteCount = 0 + } + + remoteLatestIssue := "" + remoteHistories, err := s.mysqlRepo.FindAll() + if err == nil && len(remoteHistories) > 0 { + remoteLatestIssue = remoteHistories[0].IssueNumber + } + + return map[string]interface{}{ + "local_count": localCount, + "local_latest_issue": localLatestIssue, + "remote_count": remoteCount, + "remote_latest_issue": remoteLatestIssue, + "need_sync": remoteLatestIssue > localLatestIssue, + }, nil +} diff --git a/internal/service/update_config.go b/internal/service/update_config.go new file mode 100644 index 0000000..e8f983e --- /dev/null +++ b/internal/service/update_config.go @@ -0,0 +1,138 @@ +package service + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "time" +) + +// UpdateConfig 更新配置 +type UpdateConfig struct { + CurrentVersion string `json:"current_version"` + LastCheckTime time.Time `json:"last_check_time"` + AutoCheckEnabled bool `json:"auto_check_enabled"` + CheckIntervalMinutes int `json:"check_interval_minutes"` // 检查间隔(分钟) + CheckURL string `json:"check_url,omitempty"` // 版本检查接口 URL +} + +// GetUpdateConfigPath 获取更新配置文件路径 +func GetUpdateConfigPath() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("获取用户目录失败: %v", err) + } + + configDir := filepath.Join(homeDir, ".ssq-desk") + if err := os.MkdirAll(configDir, 0755); err != nil { + return "", fmt.Errorf("创建配置目录失败: %v", err) + } + + return filepath.Join(configDir, "update_config.json"), nil +} + +// LoadUpdateConfig 加载更新配置 +func LoadUpdateConfig() (*UpdateConfig, error) { + configPath, err := GetUpdateConfigPath() + if err != nil { + return nil, err + } + + // 如果文件不存在,返回默认配置 + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return &UpdateConfig{ + CurrentVersion: GetCurrentVersion(), + LastCheckTime: time.Time{}, + AutoCheckEnabled: true, + CheckIntervalMinutes: 1, // 默认1分钟检查一次 + CheckURL: "https://img.1216.top/ssq/last-version.json", // 默认版本检查地址 + }, nil + } + + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("读取配置文件失败: %v", err) + } + + // 先解析为 map 以支持兼容旧配置 + var configMap map[string]interface{} + if err := json.Unmarshal(data, &configMap); err != nil { + return nil, fmt.Errorf("解析配置文件失败: %v", err) + } + + var config UpdateConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("解析配置文件失败: %v", err) + } + + // 兼容旧配置:如果存在 check_interval_days,转换为分钟 + if config.CheckIntervalMinutes == 0 { + if days, ok := configMap["check_interval_days"].(float64); ok && days > 0 { + config.CheckIntervalMinutes = int(days * 24 * 60) // 转换为分钟 + } else { + config.CheckIntervalMinutes = 1 // 默认1分钟 + } + } + + // 获取最新版本号 + latestVersion := GetCurrentVersion() + + // 如果当前版本为空或与最新版本不一致,更新为最新版本 + if config.CurrentVersion == "" || config.CurrentVersion != latestVersion { + if config.CurrentVersion != "" { + log.Printf("[配置] 配置中的版本号 (%s) 与最新版本号 (%s) 不一致,更新配置", config.CurrentVersion, latestVersion) + } + config.CurrentVersion = latestVersion + // 注意:这里不自动保存,避免频繁写入文件,由调用方决定是否保存 + } + + // 如果检查地址为空,使用默认地址 + if config.CheckURL == "" { + config.CheckURL = "https://img.1216.top/ssq/last-version.json" + } + + return &config, nil +} + +// SaveUpdateConfig 保存更新配置 +func SaveUpdateConfig(config *UpdateConfig) error { + configPath, err := GetUpdateConfigPath() + if err != nil { + return err + } + + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("序列化配置失败: %v", err) + } + + if err := os.WriteFile(configPath, data, 0644); err != nil { + return fmt.Errorf("写入配置文件失败: %v", err) + } + + return nil +} + +// ShouldCheckUpdate 判断是否应该检查更新 +func (c *UpdateConfig) ShouldCheckUpdate() bool { + if !c.AutoCheckEnabled { + return false + } + + // 如果从未检查过,应该检查 + if c.LastCheckTime.IsZero() { + return true + } + + // 检查是否超过间隔分钟数 + minutesSinceLastCheck := time.Since(c.LastCheckTime).Minutes() + return minutesSinceLastCheck >= float64(c.CheckIntervalMinutes) +} + +// UpdateLastCheckTime 更新最后检查时间 +func (c *UpdateConfig) UpdateLastCheckTime() error { + c.LastCheckTime = time.Now() + return SaveUpdateConfig(c) +} diff --git a/internal/service/update_download.go b/internal/service/update_download.go new file mode 100644 index 0000000..2ac33e6 --- /dev/null +++ b/internal/service/update_download.go @@ -0,0 +1,332 @@ +package service + +import ( + "crypto/md5" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +// getRemoteFileSize 通过HEAD请求获取远程文件大小 +func getRemoteFileSize(url string) (int64, error) { + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return 0, err + } + resp, err := client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + if resp.ContentLength > 0 { + return resp.ContentLength, nil + } + return 0, fmt.Errorf("无法获取文件大小") +} + +// normalizeProgress 标准化进度值到0-100之间 +func normalizeProgress(progress float64) float64 { + if progress < 0 { + return 0 + } + if progress > 100 { + return 100 + } + return progress +} + +// DownloadProgress 下载进度回调函数类型 +type DownloadProgress func(progress float64, speed float64, downloaded int64, total int64) + +// DownloadProgressInfo 下载进度信息 +type DownloadProgressInfo struct { + Progress float64 `json:"progress"` // 进度百分比 + Speed float64 `json:"speed"` // 下载速度(字节/秒) + Downloaded int64 `json:"downloaded"` // 已下载字节数 + Total int64 `json:"total"` // 总字节数 + Error string `json:"error,omitempty"` // 错误信息 + Result *DownloadResult `json:"result,omitempty"` // 下载结果 +} + +// DownloadResult 下载结果 +type DownloadResult struct { + FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` + MD5Hash string `json:"md5_hash,omitempty"` + SHA256Hash string `json:"sha256_hash,omitempty"` +} + +// DownloadUpdate 下载更新包 +func DownloadUpdate(downloadURL string, progressCallback DownloadProgress) (*DownloadResult, error) { + // 获取下载目录 + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("获取用户目录失败: %v", err) + } + + downloadDir := filepath.Join(homeDir, ".ssq-desk", "downloads") + if err := os.MkdirAll(downloadDir, 0755); err != nil { + return nil, fmt.Errorf("创建下载目录失败: %v", err) + } + + // 从 URL 提取文件名 + filename := filepath.Base(downloadURL) + if filename == "" || filename == "." { + filename = fmt.Sprintf("update-%d.exe", time.Now().Unix()) + } + + filePath := filepath.Join(downloadDir, filename) + + // 检查文件是否已存在 + var downloadedSize int64 = 0 + if fileInfo, err := os.Stat(filePath); err == nil { + downloadedSize = fileInfo.Size() + // 检查是否已完整下载 + if remoteSize, err := getRemoteFileSize(downloadURL); err == nil && downloadedSize == remoteSize { + if progressCallback != nil { + progressCallback(100.0, 0, downloadedSize, remoteSize) + time.Sleep(50 * time.Millisecond) + } + md5Hash, sha256Hash, err := calculateFileHashes(filePath) + if err != nil { + return nil, fmt.Errorf("计算文件哈希失败: %v", err) + } + return &DownloadResult{ + FilePath: filePath, + FileSize: downloadedSize, + MD5Hash: md5Hash, + SHA256Hash: sha256Hash, + }, nil + } + } + + // 打开文件(支持断点续传) + file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return nil, fmt.Errorf("创建文件失败: %v", err) + } + defer file.Close() + + // 创建 HTTP 请求 + req, err := http.NewRequest("GET", downloadURL, nil) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %v", err) + } + + // 如果已下载部分,设置 Range 头(断点续传) + if downloadedSize > 0 { + req.Header.Set("Range", fmt.Sprintf("bytes=%d-", downloadedSize)) + } + + // 发送请求 + client := &http.Client{Timeout: 30 * time.Minute} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("下载请求失败: %v", err) + } + defer resp.Body.Close() + + // 检查响应状态 + if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable { + file.Close() + if fileInfo, err := os.Stat(filePath); err == nil { + if remoteSize, err := getRemoteFileSize(downloadURL); err == nil && fileInfo.Size() == remoteSize { + if progressCallback != nil { + progressCallback(100.0, 0, fileInfo.Size(), remoteSize) + } + md5Hash, sha256Hash, err := calculateFileHashes(filePath) + if err != nil { + return nil, fmt.Errorf("计算文件哈希失败: %v", err) + } + return &DownloadResult{ + FilePath: filePath, + FileSize: fileInfo.Size(), + MD5Hash: md5Hash, + SHA256Hash: sha256Hash, + }, nil + } + } + return nil, fmt.Errorf("服务器返回 416 错误,且文件可能不完整") + } + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { + return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode) + } + + // 获取文件总大小 + contentLength := resp.ContentLength + gotTotalFromRange := false + + // 优先从 Content-Range 头获取总大小 + if rangeHeader := resp.Header.Get("Content-Range"); rangeHeader != "" { + var start, end, total int64 + if n, _ := fmt.Sscanf(rangeHeader, "bytes %d-%d/%d", &start, &end, &total); n == 3 && total > 0 { + contentLength = total + gotTotalFromRange = true + } + } + + // 如果未获取到,尝试通过 HEAD 请求获取 + if contentLength <= 0 && !gotTotalFromRange { + if remoteSize, err := getRemoteFileSize(downloadURL); err == nil { + contentLength = remoteSize + } + } + + // 断点续传时,如果未从 Content-Range 获取,需要加上已下载部分 + if resp.StatusCode == http.StatusPartialContent && downloadedSize > 0 && contentLength > 0 && !gotTotalFromRange { + if contentLength < downloadedSize { + contentLength += downloadedSize + } + } + + // 发送初始进度事件 + if progressCallback != nil { + time.Sleep(50 * time.Millisecond) + if contentLength > 0 && downloadedSize > 0 { + progress := normalizeProgress(float64(downloadedSize) / float64(contentLength) * 100) + progressCallback(progress, 0, downloadedSize, contentLength) + } else { + progressCallback(0, 0, downloadedSize, -1) + } + time.Sleep(20 * time.Millisecond) + } + + // 下载文件 + buffer := make([]byte, 32*1024) // 32KB 缓冲区 + var totalDownloaded int64 = downloadedSize + startTime := time.Now() + lastProgressTime := startTime + lastProgressSize := totalDownloaded + + for { + n, err := resp.Body.Read(buffer) + if n > 0 { + written, writeErr := file.Write(buffer[:n]) + if writeErr != nil { + return nil, fmt.Errorf("写入文件失败: %v", writeErr) + } + totalDownloaded += int64(written) + + // 计算进度和速度 + now := time.Now() + elapsed := now.Sub(lastProgressTime).Seconds() + + // 每 0.3 秒更新一次进度 + if elapsed >= 0.3 { + progress := float64(0) + if contentLength > 0 { + progress = normalizeProgress(float64(totalDownloaded) / float64(contentLength) * 100) + } + + speed := float64(0) + if elapsed > 0 { + speed = float64(totalDownloaded-lastProgressSize) / elapsed + } + + if progressCallback != nil { + progressCallback(progress, speed, totalDownloaded, contentLength) + } + + lastProgressTime = now + lastProgressSize = totalDownloaded + } + } + + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("读取数据失败: %v", err) + } + } + + // 最后一次进度更新 + if progressCallback != nil { + if contentLength > 0 { + progressCallback(100.0, 0, totalDownloaded, contentLength) + } else { + progressCallback(100.0, 0, totalDownloaded, totalDownloaded) + } + } + + file.Close() + md5Hash, sha256Hash, err := calculateFileHashes(filePath) + if err != nil { + return nil, fmt.Errorf("计算文件哈希失败: %v", err) + } + + fileInfo, err := os.Stat(filePath) + if err != nil { + return nil, fmt.Errorf("获取文件信息失败: %v", err) + } + + return &DownloadResult{ + FilePath: filePath, + FileSize: fileInfo.Size(), + MD5Hash: md5Hash, + SHA256Hash: sha256Hash, + }, nil +} + +// calculateFileHashes 计算文件的 MD5 和 SHA256 哈希值 +func calculateFileHashes(filePath string) (string, string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", "", err + } + defer file.Close() + + md5Hash := md5.New() + sha256Hash := sha256.New() + + // 使用 MultiWriter 同时计算两个哈希 + writer := io.MultiWriter(md5Hash, sha256Hash) + + if _, err := io.Copy(writer, file); err != nil { + return "", "", err + } + + md5Sum := hex.EncodeToString(md5Hash.Sum(nil)) + sha256Sum := hex.EncodeToString(sha256Hash.Sum(nil)) + + return md5Sum, sha256Sum, nil +} + +// VerifyFileHash 验证文件哈希值 +func VerifyFileHash(filePath string, expectedHash string, hashType string) (bool, error) { + file, err := os.Open(filePath) + if err != nil { + return false, err + } + defer file.Close() + + var hash []byte + var calculatedHash string + + switch hashType { + case "md5": + md5Hash := md5.New() + if _, err := io.Copy(md5Hash, file); err != nil { + return false, err + } + hash = md5Hash.Sum(nil) + calculatedHash = hex.EncodeToString(hash) + case "sha256": + sha256Hash := sha256.New() + if _, err := io.Copy(sha256Hash, file); err != nil { + return false, err + } + hash = sha256Hash.Sum(nil) + calculatedHash = hex.EncodeToString(hash) + default: + return false, fmt.Errorf("不支持的哈希类型: %s", hashType) + } + + return calculatedHash == expectedHash, nil +} diff --git a/internal/service/update_install.go b/internal/service/update_install.go new file mode 100644 index 0000000..86cd723 --- /dev/null +++ b/internal/service/update_install.go @@ -0,0 +1,328 @@ +package service + +import ( + "archive/zip" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" +) + +// InstallResult 安装结果 +type InstallResult struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// InstallUpdate 安装更新包 +func InstallUpdate(installerPath string, autoRestart bool) (*InstallResult, error) { + return InstallUpdateWithHash(installerPath, autoRestart, "", "") +} + +// InstallUpdateWithHash 安装更新包(带哈希验证) +func InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (*InstallResult, error) { + if _, err := os.Stat(installerPath); os.IsNotExist(err) { + return nil, fmt.Errorf("安装文件不存在: %s", installerPath) + } + + // 哈希验证 + if expectedHash != "" && hashType != "" { + valid, err := VerifyFileHash(installerPath, expectedHash, hashType) + if err != nil { + return &InstallResult{Success: false, Message: "文件验证失败: " + err.Error()}, nil + } + if !valid { + return &InstallResult{Success: false, Message: "文件哈希值不匹配,文件可能已损坏或被篡改"}, nil + } + } + + // 备份 + backupPath, err := BackupApplication() + if err != nil { + return &InstallResult{Success: false, Message: fmt.Sprintf("备份失败: %v", err)}, nil + } + + // 安装 + ext := filepath.Ext(installerPath) + switch ext { + case ".exe": + if runtime.GOOS != "windows" { + return &InstallResult{Success: false, Message: "当前系统不是 Windows,无法安装 .exe 文件"}, nil + } + err = installExe(installerPath) + case ".zip": + err = installZip(installerPath) + default: + return nil, fmt.Errorf("不支持的安装包格式: %s", ext) + } + + // 处理安装结果 + if err != nil { + // 安装失败,尝试回滚 + if backupPath != "" { + _ = rollbackFromBackup(backupPath) + } + return &InstallResult{Success: false, Message: fmt.Sprintf("安装失败: %v", err)}, nil + } + + // 自动重启 + if autoRestart { + go func() { + time.Sleep(2 * time.Second) + restartApplication() + }() + } + + return &InstallResult{Success: true, Message: "安装成功"}, nil +} + +// getExecutablePath 获取当前可执行文件路径 +func getExecutablePath() (string, error) { + execPath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("获取可执行文件路径失败: %v", err) + } + return execPath, nil +} + +// installExe 安装 exe 文件 +func installExe(exePath string) error { + execPath, err := getExecutablePath() + if err != nil { + return err + } + return replaceExecutableFile(exePath, execPath) +} + +// installZip 安装 ZIP 压缩包 +func installZip(zipPath string) error { + zipReader, err := zip.OpenReader(zipPath) + if err != nil { + return fmt.Errorf("打开 ZIP 文件失败: %v", err) + } + defer zipReader.Close() + + execPath, err := getExecutablePath() + if err != nil { + return err + } + execDir := filepath.Dir(execPath) + execName := filepath.Base(execPath) + + // 解压到临时目录 + tempDir := filepath.Join(execDir, ".update-temp") + if err := os.MkdirAll(tempDir, 0755); err != nil { + return fmt.Errorf("创建临时目录失败: %v", err) + } + defer os.RemoveAll(tempDir) + + // 解压文件 + for _, file := range zipReader.File { + filePath := filepath.Join(tempDir, file.Name) + if file.FileInfo().IsDir() { + os.MkdirAll(filePath, file.FileInfo().Mode()) + continue + } + + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return fmt.Errorf("创建目录失败: %v", err) + } + + rc, err := file.Open() + if err != nil { + return fmt.Errorf("打开 ZIP 文件项失败: %v", err) + } + + targetFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode()) + if err != nil { + rc.Close() + return fmt.Errorf("创建目标文件失败: %v", err) + } + + if _, err := io.Copy(targetFile, rc); err != nil { + targetFile.Close() + rc.Close() + return fmt.Errorf("复制文件失败: %v", err) + } + targetFile.Close() + rc.Close() + } + + // 查找新的可执行文件 + newExecPath := filepath.Join(tempDir, execName) + if _, err := os.Stat(newExecPath); os.IsNotExist(err) { + files, _ := os.ReadDir(tempDir) + for _, f := range files { + if !f.IsDir() { + newExecPath = filepath.Join(tempDir, f.Name()) + break + } + } + } + + // 替换文件(使用与 installExe 相同的逻辑) + return replaceExecutableFile(newExecPath, execPath) +} + +// replaceExecutableFile 替换可执行文件(Windows 和 Unix 通用逻辑) +func replaceExecutableFile(newFilePath, execPath string) error { + if runtime.GOOS == "windows" { + return replaceExecutableFileWindows(newFilePath, execPath) + } + + // Unix-like: 直接替换 + if err := copyFile(newFilePath, execPath); err != nil { + return fmt.Errorf("复制文件失败: %v", err) + } + return os.Chmod(execPath, 0755) +} + +// replaceExecutableFileWindows Windows 平台替换可执行文件 +func replaceExecutableFileWindows(newFilePath, execPath string) error { + oldExecPath := execPath + ".old" + newExecPathTemp := execPath + ".new" + + // 清理旧文件 + os.Remove(oldExecPath) + os.Remove(newExecPathTemp) + + // 复制新文件到临时位置 + if err := copyFile(newFilePath, newExecPathTemp); err != nil { + return fmt.Errorf("复制新文件失败: %v", err) + } + + // 尝试重命名当前文件(如果失败,说明文件正在使用) + if err := os.Rename(execPath, oldExecPath); err != nil { + return nil + } + + // 替换文件 + if err := os.Rename(newExecPathTemp, execPath); err != nil { + os.Rename(oldExecPath, execPath) // 恢复 + return fmt.Errorf("替换文件失败: %v", err) + } + + // 延迟删除旧文件 + go func() { + time.Sleep(10 * time.Second) + os.Remove(oldExecPath) + }() + return nil +} + +// restartApplication 重启应用 +func restartApplication() { + execPath, err := getExecutablePath() + if err != nil { + return + } + + currentPID := os.Getpid() + if runtime.GOOS != "windows" { + return + } + + replacePendingFile(execPath) + + // 创建并执行重启脚本 + tempDir := os.TempDir() + batFile := filepath.Join(tempDir, fmt.Sprintf("restart_%d.bat", currentPID)) + execDir := filepath.Dir(execPath) + + batContent := fmt.Sprintf(`@echo off +cd /d "%s" +start "" "%s" +timeout /t 3 /nobreak >nul +taskkill /PID %d /F >nul 2>&1 +del "%%~f0" +`, execDir, execPath, currentPID) + + if err := os.WriteFile(batFile, []byte(batContent), 0644); err != nil { + fallbackRestart(execPath) + return + } + + cmd := exec.Command("cmd", "/C", batFile) + cmd.Stdout = nil + cmd.Stderr = nil + if err := cmd.Start(); err != nil { + fallbackRestart(execPath) + return + } + + os.Exit(0) +} + +// fallbackRestart 降级重启方案 +func fallbackRestart(execPath string) { + exec.Command("cmd", "/C", "start", "", execPath).Start() + time.Sleep(2 * time.Second) + os.Exit(0) +} + +// replacePendingFile 替换待替换的文件(.new -> 可执行文件) +func replacePendingFile(execPath string) error { + newExecPathTemp := execPath + ".new" + if _, err := os.Stat(newExecPathTemp); os.IsNotExist(err) { + return nil // 没有待替换文件 + } + + oldExecPath := execPath + ".old" + os.Remove(oldExecPath) + if err := os.Rename(newExecPathTemp, execPath); err != nil { + return fmt.Errorf("文件替换失败: %v", err) + } + return nil +} + +// rollbackFromBackup 从备份恢复 +func rollbackFromBackup(backupPath string) error { + execPath, err := getExecutablePath() + if err != nil { + return err + } + return copyFile(backupPath, execPath) +} + +// BackupApplication 备份当前应用 +func BackupApplication() (string, error) { + execPath, err := getExecutablePath() + if err != nil { + return "", err + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("获取用户目录失败: %v", err) + } + + backupDir := filepath.Join(homeDir, ".ssq-desk", "backups") + if err := os.MkdirAll(backupDir, 0755); err != nil { + return "", fmt.Errorf("创建备份目录失败: %v", err) + } + + timestamp := time.Now().Format("20060102-150405") + backupFileName := fmt.Sprintf("ssq-desk-backup-%s%s", timestamp, filepath.Ext(execPath)) + backupPath := filepath.Join(backupDir, backupFileName) + + sourceFile, err := os.Open(execPath) + if err != nil { + return "", fmt.Errorf("打开源文件失败: %v", err) + } + defer sourceFile.Close() + + backupFile, err := os.Create(backupPath) + if err != nil { + return "", fmt.Errorf("创建备份文件失败: %v", err) + } + defer backupFile.Close() + + if _, err := backupFile.ReadFrom(sourceFile); err != nil { + return "", fmt.Errorf("复制文件失败: %v", err) + } + + return backupPath, nil +} diff --git a/internal/service/update_service.go b/internal/service/update_service.go new file mode 100644 index 0000000..c2b6557 --- /dev/null +++ b/internal/service/update_service.go @@ -0,0 +1,168 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" +) + +// UpdateService 更新服务 +type UpdateService struct { + checkURL string // 版本检查接口 URL +} + +// NewUpdateService 创建更新服务 +func NewUpdateService(checkURL string) *UpdateService { + return &UpdateService{ + checkURL: checkURL, + } +} + +// RemoteVersionInfo 远程版本信息 +type RemoteVersionInfo struct { + Version string `json:"version"` + DownloadURL string `json:"download_url"` + Changelog string `json:"changelog"` + ForceUpdate bool `json:"force_update"` + ReleaseDate string `json:"release_date"` +} + +// CheckUpdate 检查更新 +func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) { + log.Printf("[更新检查] 开始检查更新,检查地址: %s", s.checkURL) + + // 加载配置 + config, err := LoadUpdateConfig() + if err != nil { + log.Printf("[更新检查] 加载配置失败: %v", err) + return nil, fmt.Errorf("加载配置失败: %v", err) + } + + // 获取当前版本(优先使用服务获取的最新版本号,而不是配置中可能过期的版本号) + currentVersionStr := GetCurrentVersion() + if currentVersionStr == "" { + // 如果服务获取失败,回退到配置中的版本号 + currentVersionStr = config.CurrentVersion + log.Printf("[更新检查] 使用配置中的版本号: %s", currentVersionStr) + } else { + log.Printf("[更新检查] 使用服务获取的版本号: %s", currentVersionStr) + // 如果配置中的版本号不一致,更新配置 + if config.CurrentVersion != currentVersionStr { + log.Printf("[更新检查] 配置中的版本号 (%s) 与当前版本号 (%s) 不一致,更新配置", config.CurrentVersion, currentVersionStr) + config.CurrentVersion = currentVersionStr + if err := SaveUpdateConfig(config); err != nil { + log.Printf("[更新检查] 更新配置失败: %v", err) + } + } + } + + currentVersion, err := ParseVersion(currentVersionStr) + if err != nil { + log.Printf("[更新检查] 解析当前版本失败: %v", err) + return nil, fmt.Errorf("解析当前版本失败: %v", err) + } + + // 请求远程版本信息 + remoteInfo, err := s.fetchRemoteVersionInfo() + if err != nil { + log.Printf("[更新检查] 获取远程版本信息失败: %v", err) + return nil, fmt.Errorf("获取远程版本信息失败: %v", err) + } + + log.Printf("[更新检查] 远程版本信息: 版本=%s, 下载地址=%s, 强制更新=%v", + remoteInfo.Version, remoteInfo.DownloadURL, remoteInfo.ForceUpdate) + + // 解析远程版本号 + remoteVersion, err := ParseVersion(remoteInfo.Version) + if err != nil { + log.Printf("[更新检查] 解析远程版本号失败: %v", err) + return nil, fmt.Errorf("解析远程版本号失败: %v", err) + } + + // 比较版本 + hasUpdate := remoteVersion.IsNewerThan(currentVersion) + log.Printf("[更新检查] 版本比较: 当前=%s, 远程=%s, 有更新=%v", + currentVersion.String(), remoteVersion.String(), hasUpdate) + + // 更新最后检查时间 + config.UpdateLastCheckTime() + + result := &UpdateCheckResult{ + HasUpdate: hasUpdate, + CurrentVersion: currentVersionStr, + LatestVersion: remoteInfo.Version, + DownloadURL: remoteInfo.DownloadURL, + Changelog: remoteInfo.Changelog, + ForceUpdate: remoteInfo.ForceUpdate, + ReleaseDate: remoteInfo.ReleaseDate, + } + + log.Printf("[更新检查] 检查完成: 有更新=%v", hasUpdate) + return result, nil +} + +// fetchRemoteVersionInfo 获取远程版本信息 +func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) { + if s.checkURL == "" { + log.Printf("[远程版本] 版本检查 URL 未配置") + return nil, fmt.Errorf("版本检查 URL 未配置,请先设置检查地址") + } + + log.Printf("[远程版本] 请求远程版本信息: %s", s.checkURL) + + // 创建 HTTP 客户端,设置超时 + client := &http.Client{ + Timeout: 10 * time.Second, + } + + // 发送请求 + resp, err := client.Get(s.checkURL) + if err != nil { + log.Printf("[远程版本] 网络请求失败: %v", err) + return nil, fmt.Errorf("网络请求失败: %v", err) + } + defer resp.Body.Close() + + log.Printf("[远程版本] HTTP 响应状态码: %d", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode) + } + + // 读取响应 + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("[远程版本] 读取响应失败: %v", err) + return nil, fmt.Errorf("读取响应失败: %v", err) + } + + log.Printf("[远程版本] 响应内容长度: %d 字节", len(body)) + + // 解析 JSON + var remoteInfo RemoteVersionInfo + if err := json.Unmarshal(body, &remoteInfo); err != nil { + log.Printf("[远程版本] 解析 JSON 失败: %v, 响应内容: %s", err, string(body)) + return nil, fmt.Errorf("解析响应失败: %v", err) + } + + if remoteInfo.Version == "" { + log.Printf("[远程版本] 远程版本信息不完整,响应内容: %s", string(body)) + return nil, fmt.Errorf("远程版本信息不完整") + } + + log.Printf("[远程版本] 成功获取远程版本信息: %+v", remoteInfo) + return &remoteInfo, nil +} + +// UpdateCheckResult 更新检查结果 +type UpdateCheckResult struct { + HasUpdate bool `json:"has_update"` + CurrentVersion string `json:"current_version"` + LatestVersion string `json:"latest_version"` + DownloadURL string `json:"download_url"` + Changelog string `json:"changelog"` + ForceUpdate bool `json:"force_update"` + ReleaseDate string `json:"release_date"` +} diff --git a/internal/service/version.go b/internal/service/version.go new file mode 100644 index 0000000..b961990 --- /dev/null +++ b/internal/service/version.go @@ -0,0 +1,159 @@ +package service + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "strconv" + "strings" +) + +// ==================== 常量定义 ==================== + +// AppVersion 应用版本号(发布时直接修改此处) +const AppVersion = "0.1.1" + +// ==================== 类型定义 ==================== + +// Version 版本号结构 +type Version struct { + Major int + Minor int + Patch int +} + +// WailsConfig Wails 配置文件结构 +type WailsConfig struct { + Version string `json:"version"` +} + +// ==================== 版本号解析和比较 ==================== + +// ParseVersion 解析版本号字符串(支持 v1.0.0 或 1.0.0 格式) +func ParseVersion(versionStr string) (*Version, error) { + versionStr = strings.TrimPrefix(versionStr, "v") + parts := strings.Split(versionStr, ".") + if len(parts) != 3 { + return nil, fmt.Errorf("版本号格式错误,应为 x.y.z 格式") + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, fmt.Errorf("主版本号解析失败: %v", err) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, fmt.Errorf("次版本号解析失败: %v", err) + } + + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, fmt.Errorf("修订号解析失败: %v", err) + } + + return &Version{Major: major, Minor: minor, Patch: patch}, nil +} + +// String 返回版本号字符串(格式:v1.0.0) +func (v *Version) String() string { + return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +// Compare 比较版本号 +// 返回值:-1 表示当前版本小于目标版本,0 表示相等,1 表示大于 +func (v *Version) Compare(other *Version) int { + if v.Major != other.Major { + if v.Major < other.Major { + return -1 + } + return 1 + } + if v.Minor != other.Minor { + if v.Minor < other.Minor { + return -1 + } + return 1 + } + if v.Patch != other.Patch { + if v.Patch < other.Patch { + return -1 + } + return 1 + } + return 0 +} + +// IsNewerThan 判断是否比目标版本新 +func (v *Version) IsNewerThan(other *Version) bool { + return v.Compare(other) > 0 +} + +// IsOlderThan 判断是否比目标版本旧 +func (v *Version) IsOlderThan(other *Version) bool { + return v.Compare(other) < 0 +} + +// ==================== 版本号获取 ==================== + +// GetCurrentVersion 获取当前版本号 +// 优先级:硬编码版本号 > wails.json(开发模式)> 默认值 +func GetCurrentVersion() string { + if AppVersion != "" { + log.Printf("[版本] 使用硬编码版本号: %s", AppVersion) + return AppVersion + } + + version := getVersionFromWailsJSON() + if version != "" { + log.Printf("[版本] 从 wails.json 获取版本号: %s", version) + return version + } + + log.Printf("[版本] 使用默认版本号: 0.0.1") + return "0.0.1" +} + +// ==================== 配置文件读取 ==================== + +// getVersionFromWailsJSON 从 wails.json 读取版本号(仅开发模式使用) +func getVersionFromWailsJSON() string { + wd, err := os.Getwd() + if err != nil { + return "" + } + + // 尝试当前目录 + if version := readVersionFromFile(filepath.Join(wd, "wails.json")); version != "" { + return version + } + + // 尝试父目录 + if version := readVersionFromFile(filepath.Join(filepath.Dir(wd), "wails.json")); version != "" { + return version + } + + return "" +} + +// readVersionFromFile 从指定文件读取版本号 +func readVersionFromFile(filePath string) string { + data, err := os.ReadFile(filePath) + if err != nil { + log.Printf("[版本] 读取文件失败: %s, 错误: %v", filePath, err) + return "" + } + + var config WailsConfig + if err := json.Unmarshal(data, &config); err != nil { + log.Printf("[版本] 解析 JSON 失败: %s, 错误: %v", filePath, err) + return "" + } + + if config.Version != "" { + log.Printf("[版本] 从文件读取版本号: %s -> %s", filePath, config.Version) + } + return config.Version +} diff --git a/internal/storage/models/authorization.go b/internal/storage/models/authorization.go new file mode 100644 index 0000000..c5ff887 --- /dev/null +++ b/internal/storage/models/authorization.go @@ -0,0 +1,20 @@ +package models + +import "time" + +// Authorization 授权信息 +type Authorization struct { + ID int `gorm:"primaryKey" json:"id"` // 主键ID + LicenseCode string `gorm:"type:varchar(100);not null;uniqueIndex" json:"license_code"` // 授权码(唯一) + DeviceID string `gorm:"type:varchar(100);not null;index" json:"device_id"` // 设备ID(MD5哈希) + ActivatedAt time.Time `gorm:"not null" json:"activated_at"` // 激活时间 + ExpiresAt *time.Time `gorm:"type:datetime" json:"expires_at"` // 过期时间(可选,nil表示永不过期) + Status int `gorm:"type:tinyint;not null;default:1" json:"status"` // 状态(1:有效 0:无效) + CreatedAt time.Time `gorm:"autoCreateTime:false" json:"created_at"` // 创建时间(由程序设置) + UpdatedAt time.Time `gorm:"autoUpdateTime:false" json:"updated_at"` // 更新时间(由程序设置) +} + +// TableName 指定表名 +func (Authorization) TableName() string { + return "sys_authorization_code" +} diff --git a/internal/storage/models/ssq_history.go b/internal/storage/models/ssq_history.go new file mode 100644 index 0000000..bf906f2 --- /dev/null +++ b/internal/storage/models/ssq_history.go @@ -0,0 +1,24 @@ +package models + +import "time" + +// SsqHistory 双色球历史开奖数据 +type SsqHistory struct { + ID int `gorm:"primaryKey;column:id" json:"id"` + IssueNumber string `gorm:"type:varchar(20);not null;index;column:issue_number" json:"issue_number"` + OpenDate *time.Time `gorm:"type:date;column:open_date" json:"open_date"` + RedBall1 int `gorm:"type:tinyint;not null;column:red_ball_1" json:"red_ball_1"` + RedBall2 int `gorm:"type:tinyint;not null;column:red_ball_2" json:"red_ball_2"` + RedBall3 int `gorm:"type:tinyint;not null;column:red_ball_3" json:"red_ball_3"` + RedBall4 int `gorm:"type:tinyint;not null;column:red_ball_4" json:"red_ball_4"` + RedBall5 int `gorm:"type:tinyint;not null;column:red_ball_5" json:"red_ball_5"` + RedBall6 int `gorm:"type:tinyint;not null;column:red_ball_6" json:"red_ball_6"` + BlueBall int `gorm:"type:tinyint;not null;column:blue_ball" json:"blue_ball"` + CreatedAt time.Time `gorm:"autoCreateTime:false;column:created_at" json:"created_at"` // 创建时间(由程序设置) + UpdatedAt time.Time `gorm:"autoUpdateTime:false;column:updated_at" json:"updated_at"` // 更新时间(由程序设置) +} + +// TableName 指定表名 +func (SsqHistory) TableName() string { + return "ssq_history" +} diff --git a/internal/storage/models/version.go b/internal/storage/models/version.go new file mode 100644 index 0000000..6ef0a94 --- /dev/null +++ b/internal/storage/models/version.go @@ -0,0 +1,20 @@ +package models + +import "time" + +// Version 版本信息 +type Version struct { + ID int `gorm:"primaryKey" json:"id"` // 主键ID + Version string `gorm:"type:varchar(20);not null;uniqueIndex" json:"version"` // 版本号(语义化版本,如1.0.0) + DownloadURL string `gorm:"type:varchar(500)" json:"download_url"` // 下载地址(更新包下载URL) + Changelog string `gorm:"type:text" json:"changelog"` // 更新日志(Markdown格式) + ForceUpdate int `gorm:"type:tinyint;not null;default:0" json:"force_update"` // 是否强制更新(1:是 0:否) + ReleaseDate *time.Time `gorm:"type:date" json:"release_date"` // 发布日期 + CreatedAt time.Time `gorm:"autoCreateTime:false" json:"created_at"` // 创建时间(由程序设置) + UpdatedAt time.Time `gorm:"autoUpdateTime:false" json:"updated_at"` // 更新时间(由程序设置) +} + +// TableName 指定表名 +func (Version) TableName() string { + return "sys_version" +} diff --git a/internal/storage/repository/auth_repository.go b/internal/storage/repository/auth_repository.go new file mode 100644 index 0000000..808ee8e --- /dev/null +++ b/internal/storage/repository/auth_repository.go @@ -0,0 +1,76 @@ +package repository + +import ( + "ssq-desk/internal/database" + "ssq-desk/internal/storage/models" + "time" + + "gorm.io/gorm" +) + +// AuthRepository 授权数据访问接口 +type AuthRepository interface { + Create(auth *models.Authorization) error + Update(auth *models.Authorization) error + GetByLicenseCode(licenseCode string) (*models.Authorization, error) + GetByDeviceID(deviceID string) (*models.Authorization, error) +} + +// SQLiteAuthRepository SQLite 授权数据访问实现 +type SQLiteAuthRepository struct { + db *gorm.DB +} + +// NewSQLiteAuthRepository 创建 SQLite 授权数据访问实例 +func NewSQLiteAuthRepository() (AuthRepository, error) { + db := database.GetSQLite() + if db == nil { + return nil, gorm.ErrInvalidDB + } + + // 自动迁移 + err := db.AutoMigrate(&models.Authorization{}) + if err != nil { + return nil, err + } + + return &SQLiteAuthRepository{db: db}, nil +} + +// Create 创建授权记录 +func (r *SQLiteAuthRepository) Create(auth *models.Authorization) error { + now := time.Now() + if auth.CreatedAt.IsZero() { + auth.CreatedAt = now + } + if auth.UpdatedAt.IsZero() { + auth.UpdatedAt = now + } + return r.db.Create(auth).Error +} + +// Update 更新授权记录 +func (r *SQLiteAuthRepository) Update(auth *models.Authorization) error { + auth.UpdatedAt = time.Now() + return r.db.Save(auth).Error +} + +// GetByLicenseCode 根据授权码查询 +func (r *SQLiteAuthRepository) GetByLicenseCode(licenseCode string) (*models.Authorization, error) { + var auth models.Authorization + err := r.db.Where("license_code = ?", licenseCode).First(&auth).Error + if err != nil { + return nil, err + } + return &auth, nil +} + +// GetByDeviceID 根据设备ID查询 +func (r *SQLiteAuthRepository) GetByDeviceID(deviceID string) (*models.Authorization, error) { + var auth models.Authorization + err := r.db.Where("device_id = ? AND status = ?", deviceID, 1).First(&auth).Error + if err != nil { + return nil, err + } + return &auth, nil +} diff --git a/internal/storage/repository/mysql_repo.go b/internal/storage/repository/mysql_repo.go new file mode 100644 index 0000000..442d41e --- /dev/null +++ b/internal/storage/repository/mysql_repo.go @@ -0,0 +1,128 @@ +package repository + +import ( + "gorm.io/gorm" + "ssq-desk/internal/database" + "ssq-desk/internal/storage/models" + "time" +) + +// MySQLSsqRepository MySQL 实现 +type MySQLSsqRepository struct { + db *gorm.DB +} + +// NewMySQLSsqRepository 创建 MySQL 仓库 +func NewMySQLSsqRepository() (SsqRepository, error) { + db := database.GetMySQL() + if db == nil { + return nil, gorm.ErrInvalidDB + } + return &MySQLSsqRepository{db: db}, nil +} + +// FindAll 查询所有历史数据 +func (r *MySQLSsqRepository) FindAll() ([]models.SsqHistory, error) { + var histories []models.SsqHistory + err := r.db.Order("issue_number DESC").Find(&histories).Error + return histories, err +} + +// FindByIssue 根据期号查询 +func (r *MySQLSsqRepository) FindByIssue(issueNumber string) (*models.SsqHistory, error) { + var history models.SsqHistory + err := r.db.Where("issue_number = ?", issueNumber).First(&history).Error + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return &history, err +} + +// FindByRedBalls 根据红球查询(支持部分匹配) +func (r *MySQLSsqRepository) FindByRedBalls(redBalls []int) ([]models.SsqHistory, error) { + if len(redBalls) == 0 { + return r.FindAll() + } + + var histories []models.SsqHistory + query := r.db + + // 构建查询条件:红球在输入的红球列表中 + for _, ball := range redBalls { + query = query.Where("red_ball_1 = ? OR red_ball_2 = ? OR red_ball_3 = ? OR red_ball_4 = ? OR red_ball_5 = ? OR red_ball_6 = ?", + ball, ball, ball, ball, ball, ball) + } + + err := query.Order("issue_number DESC").Find(&histories).Error + return histories, err +} + +// FindByRedAndBlue 根据红球和蓝球查询 +func (r *MySQLSsqRepository) FindByRedAndBlue(redBalls []int, blueBall int, blueBallRange []int) ([]models.SsqHistory, error) { + var histories []models.SsqHistory + query := r.db + + // 红球条件 + if len(redBalls) > 0 { + for _, ball := range redBalls { + query = query.Where("red_ball_1 = ? OR red_ball_2 = ? OR red_ball_3 = ? OR red_ball_4 = ? OR red_ball_5 = ? OR red_ball_6 = ?", + ball, ball, ball, ball, ball, ball) + } + } + + // 蓝球条件 + if blueBall > 0 { + query = query.Where("blue_ball = ?", blueBall) + } else if len(blueBallRange) > 0 { + query = query.Where("blue_ball IN ?", blueBallRange) + } + + err := query.Order("issue_number DESC").Find(&histories).Error + return histories, err +} + +// Create 创建记录 +func (r *MySQLSsqRepository) Create(history *models.SsqHistory) error { + now := time.Now() + if history.CreatedAt.IsZero() { + history.CreatedAt = now + } + if history.UpdatedAt.IsZero() { + history.UpdatedAt = now + } + return r.db.Create(history).Error +} + +// BatchCreate 批量创建 +func (r *MySQLSsqRepository) BatchCreate(histories []models.SsqHistory) error { + if len(histories) == 0 { + return nil + } + now := time.Now() + for i := range histories { + if histories[i].CreatedAt.IsZero() { + histories[i].CreatedAt = now + } + if histories[i].UpdatedAt.IsZero() { + histories[i].UpdatedAt = now + } + } + return r.db.CreateInBatches(histories, 100).Error +} + +// GetLatestIssue 获取最新期号 +func (r *MySQLSsqRepository) GetLatestIssue() (string, error) { + var history models.SsqHistory + err := r.db.Order("issue_number DESC").First(&history).Error + if err == gorm.ErrRecordNotFound { + return "", nil + } + return history.IssueNumber, err +} + +// Count 统计总数 +func (r *MySQLSsqRepository) Count() (int64, error) { + var count int64 + err := r.db.Model(&models.SsqHistory{}).Count(&count).Error + return count, err +} diff --git a/internal/storage/repository/sqlite_repo.go b/internal/storage/repository/sqlite_repo.go new file mode 100644 index 0000000..399b8e0 --- /dev/null +++ b/internal/storage/repository/sqlite_repo.go @@ -0,0 +1,136 @@ +package repository + +import ( + "fmt" + "gorm.io/gorm" + "ssq-desk/internal/database" + "ssq-desk/internal/storage/models" + "time" +) + +// SQLiteSsqRepository SQLite 实现 +type SQLiteSsqRepository struct { + db *gorm.DB +} + +// NewSQLiteSsqRepository 创建 SQLite 仓库 +func NewSQLiteSsqRepository() (SsqRepository, error) { + db := database.GetSQLite() + if db == nil { + return nil, fmt.Errorf("SQLite 数据库未初始化或初始化失败") + } + + // 自动迁移表结构(数据库初始化时已迁移,这里再次确保) + err := db.AutoMigrate(&models.SsqHistory{}) + if err != nil { + return nil, fmt.Errorf("SQLite 表迁移失败: %v", err) + } + + return &SQLiteSsqRepository{db: db}, nil +} + +// FindAll 查询所有历史数据 +func (r *SQLiteSsqRepository) FindAll() ([]models.SsqHistory, error) { + var histories []models.SsqHistory + err := r.db.Order("issue_number DESC").Find(&histories).Error + return histories, err +} + +// FindByIssue 根据期号查询 +func (r *SQLiteSsqRepository) FindByIssue(issueNumber string) (*models.SsqHistory, error) { + var history models.SsqHistory + err := r.db.Where("issue_number = ?", issueNumber).First(&history).Error + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return &history, err +} + +// FindByRedBalls 根据红球查询(支持部分匹配) +func (r *SQLiteSsqRepository) FindByRedBalls(redBalls []int) ([]models.SsqHistory, error) { + if len(redBalls) == 0 { + return r.FindAll() + } + + var histories []models.SsqHistory + query := r.db + + // 构建查询条件:红球在输入的红球列表中 + for _, ball := range redBalls { + query = query.Where("red_ball_1 = ? OR red_ball_2 = ? OR red_ball_3 = ? OR red_ball_4 = ? OR red_ball_5 = ? OR red_ball_6 = ?", + ball, ball, ball, ball, ball, ball) + } + + err := query.Order("issue_number DESC").Find(&histories).Error + return histories, err +} + +// FindByRedAndBlue 根据红球和蓝球查询 +func (r *SQLiteSsqRepository) FindByRedAndBlue(redBalls []int, blueBall int, blueBallRange []int) ([]models.SsqHistory, error) { + var histories []models.SsqHistory + query := r.db + + // 红球条件 + if len(redBalls) > 0 { + for _, ball := range redBalls { + query = query.Where("red_ball_1 = ? OR red_ball_2 = ? OR red_ball_3 = ? OR red_ball_4 = ? OR red_ball_5 = ? OR red_ball_6 = ?", + ball, ball, ball, ball, ball, ball) + } + } + + // 蓝球条件 + if blueBall > 0 { + query = query.Where("blue_ball = ?", blueBall) + } else if len(blueBallRange) > 0 { + query = query.Where("blue_ball IN ?", blueBallRange) + } + + err := query.Order("issue_number DESC").Find(&histories).Error + return histories, err +} + +// Create 创建记录 +func (r *SQLiteSsqRepository) Create(history *models.SsqHistory) error { + now := time.Now() + if history.CreatedAt.IsZero() { + history.CreatedAt = now + } + if history.UpdatedAt.IsZero() { + history.UpdatedAt = now + } + return r.db.Create(history).Error +} + +// BatchCreate 批量创建 +func (r *SQLiteSsqRepository) BatchCreate(histories []models.SsqHistory) error { + if len(histories) == 0 { + return nil + } + now := time.Now() + for i := range histories { + if histories[i].CreatedAt.IsZero() { + histories[i].CreatedAt = now + } + if histories[i].UpdatedAt.IsZero() { + histories[i].UpdatedAt = now + } + } + return r.db.CreateInBatches(histories, 100).Error +} + +// GetLatestIssue 获取最新期号 +func (r *SQLiteSsqRepository) GetLatestIssue() (string, error) { + var history models.SsqHistory + err := r.db.Order("issue_number DESC").First(&history).Error + if err == gorm.ErrRecordNotFound { + return "", nil + } + return history.IssueNumber, err +} + +// Count 统计总数 +func (r *SQLiteSsqRepository) Count() (int64, error) { + var count int64 + err := r.db.Model(&models.SsqHistory{}).Count(&count).Error + return count, err +} diff --git a/internal/storage/repository/ssq_repository.go b/internal/storage/repository/ssq_repository.go new file mode 100644 index 0000000..a9b76ef --- /dev/null +++ b/internal/storage/repository/ssq_repository.go @@ -0,0 +1,25 @@ +package repository + +import ( + "ssq-desk/internal/storage/models" +) + +// SsqRepository 双色球数据仓库接口 +type SsqRepository interface { + // FindAll 查询所有历史数据 + FindAll() ([]models.SsqHistory, error) + // FindByIssue 根据期号查询 + FindByIssue(issueNumber string) (*models.SsqHistory, error) + // FindByRedBalls 根据红球查询(支持部分匹配) + FindByRedBalls(redBalls []int) ([]models.SsqHistory, error) + // FindByRedAndBlue 根据红球和蓝球查询 + FindByRedAndBlue(redBalls []int, blueBall int, blueBallRange []int) ([]models.SsqHistory, error) + // Create 创建记录 + Create(history *models.SsqHistory) error + // BatchCreate 批量创建 + BatchCreate(histories []models.SsqHistory) error + // GetLatestIssue 获取最新期号 + GetLatestIssue() (string, error) + // Count 统计总数 + Count() (int64, error) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..5f3cb24 --- /dev/null +++ b/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:web/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "SSQ-Desk", + Width: 1400, + Height: 900, + MinWidth: 1000, + MinHeight: 600, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..1cdea2f --- /dev/null +++ b/wails.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "ssq-desk", + "outputfilename": "ssq-desk", + "frontend:dir": "web", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "wailsjsdir": "./web/src", + "buildflags": "-tags=sqlite_omit_load_extension", + "author": { + "name": "JueChen", + "email": "237809796@qq.com" + } +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..eecc2a6 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + SSQ-Desk + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..5a8e651 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1462 @@ +{ + "name": "ssq-desk-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ssq-desk-web", + "version": "1.0.0", + "dependencies": { + "@arco-design/web-vue": "^2.54.0", + "vue": "^3.5.26" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.3", + "vite": "^7.3.0" + } + }, + "node_modules/@arco-design/color": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@arco-design/color/-/color-0.4.0.tgz", + "integrity": "sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3" + } + }, + "node_modules/@arco-design/web-vue": { + "version": "2.57.0", + "resolved": "https://registry.npmmirror.com/@arco-design/web-vue/-/web-vue-2.57.0.tgz", + "integrity": "sha512-R5YReC3C2sG3Jv0+YuR3B7kzkq2KdhhQNCGXD8T11xAoa0zMt6SWTP1xJQOdZcM9du+q3z6tk5mRvh4qkieRJw==", + "license": "MIT", + "dependencies": { + "@arco-design/color": "^0.4.0", + "b-tween": "^0.3.3", + "b-validate": "^1.5.3", + "compute-scroll-into-view": "^1.0.20", + "dayjs": "^1.11.13", + "number-precision": "^1.6.0", + "resize-observer-polyfill": "^1.5.1", + "scroll-into-view-if-needed": "^2.2.31", + "vue": "^3.1.0" + }, + "peerDependencies": { + "vue": "^3.1.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "vue": "3.5.26" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "license": "MIT" + }, + "node_modules/b-tween": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/b-tween/-/b-tween-0.3.3.tgz", + "integrity": "sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==", + "license": "MIT" + }, + "node_modules/b-validate": { + "version": "1.5.3", + "resolved": "https://registry.npmmirror.com/b-validate/-/b-validate-1.5.3.tgz", + "integrity": "sha512-iCvCkGFskbaYtfQ0a3GmcQCHl/Sv1GufXFGuUQ+FE+WJa7A/espLOuFIn09B944V8/ImPj71T4+rTASxO2PAuA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/number-precision": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/number-precision/-/number-precision-1.6.0.tgz", + "integrity": "sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^1.0.20" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.26", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz", + "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..1bd7dbe --- /dev/null +++ b/web/package.json @@ -0,0 +1,18 @@ +{ + "name": "ssq-desk-web", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@arco-design/web-vue": "^2.54.0", + "vue": "^3.5.26" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.3", + "vite": "^7.3.0" + } +} diff --git a/web/package.json.md5 b/web/package.json.md5 new file mode 100644 index 0000000..ef26902 --- /dev/null +++ b/web/package.json.md5 @@ -0,0 +1 @@ +67c87d1d32114e5c095571d4f62523e1 \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..cd74095 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/web/src/composables/useVersion.ts b/web/src/composables/useVersion.ts new file mode 100644 index 0000000..8b6def5 --- /dev/null +++ b/web/src/composables/useVersion.ts @@ -0,0 +1,484 @@ +import { ref, onMounted, h, onUnmounted, nextTick, watch } from 'vue' +import { Message, Modal, Progress } from '@arco-design/web-vue' +import { EventsOn } from '../wailsjs/runtime/runtime' + +// 扩展 Window 接口以支持 Wails 运行时 +declare global { + interface Window { + go: any + } +} + +/** + * 更新信息接口 + */ +interface UpdateInfo { + has_update: boolean + current_version: string + latest_version: string + download_url: string + changelog: string + force_update: boolean + release_date: string +} + +/** + * 更新配置接口 + */ +interface UpdateConfig { + auto_check_enabled: boolean + check_interval_minutes: number + check_url?: string + last_check_time?: string +} + +/** + * API 响应接口 + */ +interface ApiResponse { + success: boolean + data?: T + message?: string +} + +/** + * 版本更新 Composable + * 封装版本相关的逻辑和状态管理 + */ +export function useVersion() { + // ==================== 状态定义 ==================== + const currentVersion = ref('加载中...') + const autoCheckEnabled = ref(true) + const checking = ref(false) + const updateInfo = ref(null) + + // 下载和安装状态 + const downloading = ref(false) + const installing = ref(false) + const downloadProgress = ref(0) + const downloadSpeed = ref(0) + const downloadedSize = ref(0) + const totalSize = ref(0) + const installStatus = ref('') + + // 模态框实例 + let progressModalInstance: any = null + + // ==================== 版本信息 ==================== + + /** + * 获取当前版本 + */ + const loadCurrentVersion = async (): Promise => { + try { + const result = await window.go.main.App.GetCurrentVersion() + + if (result && result.success) { + currentVersion.value = result.data.version + updateInfo.value = null + return result.data.version + } else { + console.error(`[版本] 获取版本失败: ${result?.message || '未知错误'}`) + currentVersion.value = '未知' + throw new Error('获取版本失败') + } + } catch (error: any) { + console.error('获取当前版本失败:', error) + currentVersion.value = '未知' + throw error + } + } + + /** + * 获取更新配置 + */ + const loadUpdateConfig = async (): Promise => { + try { + const result: ApiResponse = await window.go.main.App.GetUpdateConfig() + if (result && result.success && result.data) { + const data = result.data + autoCheckEnabled.value = data.auto_check_enabled || false + } + } catch (error: any) { + console.error('获取更新配置失败:', error) + } + } + + // ==================== 版本检查 ==================== + + /** + * 检查更新 + */ + const checkUpdate = async (): Promise => { + if (checking.value) { + return + } + + checking.value = true + + try { + const result: ApiResponse = await window.go.main.App.CheckUpdate() + + if (!result) { + Message.error('检查更新失败:服务器无响应') + return + } + + if (!result.success) { + const errorMsg = result.message || '未知错误' + Message.error(`检查更新失败:${errorMsg}`) + return + } + + if (!result.data) { + Message.error('检查更新失败:未获取到版本信息') + return + } + + const data = result.data + updateInfo.value = data + + if (data.has_update) { + // 显示更新提示对话框 + Modal.confirm({ + title: '发现新版本', + content: () => [ + h('div', { style: { marginBottom: '12px', fontWeight: 'bold', fontSize: '16px' } }, [ + `当前版本:${data.current_version}` + ]), + h('div', { style: { fontWeight: 'bold', fontSize: '16px' } }, [ + `最新版本:${data.latest_version}` + ]), + h('div', { style: { marginTop: '16px', fontSize: '14px', color: '#666' } }, [ + data.changelog || '暂无更新日志' + ]), + data.force_update + ? h('div', { style: { marginTop: '12px', color: '#ff4d4f', fontWeight: 'bold' } }, + '⚠️ 此为强制更新,必须更新后才能继续使用' + ) + : null + ], + okText: data.force_update ? '立即更新' : '稍后提醒', + cancelText: data.force_update ? undefined : '关闭', + closable: !data.force_update, + maskClosable: !data.force_update, + onOk: async () => { + // 直接开始下载,并显示进度模态框 + await downloadUpdate() + } + }) + } + } catch (error: any) { + const errorMsg = error?.message || String(error) || '未知错误' + console.error('检查更新失败:', error) + Message.error(`检查更新失败:${errorMsg}`) + } finally { + checking.value = false + } + } + + + // ==================== 辅助函数 ==================== + + /** + * 标准化进度值(确保在 0-100 之间) + */ + const clampProgress = (value: number): number => { + if (isNaN(value)) return 0 + return Math.max(0, Math.min(100, value)) + } + + // ==================== 下载和安装 ==================== + + /** + * 格式化文件大小 + */ + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] + } + + /** + * 生成进度模态框内容(响应式) + */ + const getProgressModalContent = () => { + if (downloading.value) { + // 下载中 + return [ + h('div', { style: { marginBottom: '16px' } }, [ + h('div', { style: { marginBottom: '8px', fontSize: '14px', color: '#666' } }, '正在下载更新包...') + ]), + h('div', { style: { marginBottom: '8px' } }, [ + h(Progress, { + percent: clampProgress(downloadProgress.value), + showText: true + }) + ]), + h('div', { style: { fontSize: '12px', color: '#999', marginTop: '8px' } }, [ + totalSize.value > 0 + ? `${formatFileSize(downloadedSize.value)} / ${formatFileSize(totalSize.value)}` + : downloadProgress.value > 0 ? '计算文件大小...' : '准备下载...' + ]), + downloadSpeed.value > 0 + ? h('div', { style: { fontSize: '12px', color: '#999', marginTop: '4px' } }, + `下载速度: ${formatFileSize(downloadSpeed.value)}/s` + ) + : null + ] + } else if (installing.value) { + // 安装中 + return [ + h('div', { style: { marginBottom: '16px' } }, [ + h('div', { style: { marginBottom: '8px', fontSize: '14px', color: '#666' } }, '正在安装更新...') + ]), + h('div', { style: { fontSize: '12px', color: '#999' } }, installStatus.value || '准备安装中...') + ] + } else { + // 完成 + return [ + h('div', { style: { marginBottom: '16px', textAlign: 'center' } }, [ + h('div', { style: { fontSize: '16px', color: '#00b42a', marginBottom: '8px' } }, '✓ 更新完成') + ]), + h('div', { style: { fontSize: '14px', color: '#666' } }, '应用将在几秒后自动重启...') + ] + } + } + + /** + * 更新进度模态框内容 + */ + const updateProgressModal = async () => { + if (!progressModalInstance) return + await nextTick() + + // 使用响应式内容函数 + progressModalInstance.update({ + content: () => getProgressModalContent() + }) + } + + /** + * 显示进度模态框 + */ + const showProgressModal = async () => { + if (progressModalInstance) { + progressModalInstance.close() + progressModalInstance = null + } + + // 重置状态 + resetDownloadState() + + progressModalInstance = Modal.info({ + title: '更新进度', + content: () => getProgressModalContent(), + closable: false, + maskClosable: false, + footer: false + }) + + await nextTick() + + const stopWatcher = watch( + [downloadProgress, downloading, installing, totalSize, downloadedSize, downloadSpeed], + () => nextTick(updateProgressModal), + { immediate: true, flush: 'post' } + ) + + if (progressModalInstance) { + (progressModalInstance as any)._stopWatcher = stopWatcher + } + } + + /** + * 重置下载状态 + */ + const resetDownloadState = () => { + downloading.value = true + installing.value = false + downloadProgress.value = 0 + downloadSpeed.value = 0 + downloadedSize.value = 0 + totalSize.value = 0 + installStatus.value = '' + } + + /** + * 关闭进度模态框 + */ + const closeProgressModal = () => { + if (progressModalInstance) { + // 停止 watcher + if ((progressModalInstance as any)._stopWatcher) { + (progressModalInstance as any)._stopWatcher() + delete (progressModalInstance as any)._stopWatcher + } + progressModalInstance.close() + progressModalInstance = null + } + } + + /** + * 下载更新包 + */ + const downloadUpdate = async (): Promise => { + if (!updateInfo.value) return + + await showProgressModal() + + try { + const result = await window.go.main.App.DownloadUpdate(updateInfo.value.download_url) as ApiResponse + if (!result?.success) { + throw new Error(result?.message || '下载启动失败') + } + } catch (error: any) { + closeProgressModal() + Message.error('下载失败:' + error.message) + } finally { + // 确保无论成功失败都重置状态 + downloading.value = false + } + } + + /** + * 处理下载进度事件 + */ + const handleDownloadProgress = (progressData: string) => { + if (!progressData) return + + try { + const data = JSON.parse(progressData) + + downloadProgress.value = clampProgress(Number(data.progress) || 0) + downloadSpeed.value = Math.max(0, Number(data.speed) || 0) + downloadedSize.value = Math.max(0, Number(data.downloaded) || 0) + totalSize.value = Math.max(0, Number(data.total) || 0) + } catch (error) { + console.error('[进度] 解析失败:', error) + } + } + + /** + * 处理下载完成事件 + */ + const handleDownloadComplete = async (resultData: string) => { + try { + const data = JSON.parse(resultData) + + if (data.error) { + closeProgressModal() + Message.error('下载失败:' + data.error) + return + } + + if (!data.success || !data.file_path) { + closeProgressModal() + Message.error('下载完成但数据不完整') + return + } + + // 下载成功,设置100% + downloadProgress.value = 100 + downloadedSize.value = data.file_size || 0 + totalSize.value = data.file_size || 0 + await nextTick(updateProgressModal) + await new Promise(r => setTimeout(r, 800)) + + // 开始安装 + await installUpdate(data.file_path) + } catch (error: any) { + console.error('处理下载完成事件失败:', error) + closeProgressModal() + } + } + + /** + * 安装更新 + */ + const installUpdate = async (filePath: string) => { + downloading.value = false + installing.value = true + installStatus.value = '正在安装更新包...' + updateProgressModal() + + try { + const result = await window.go.main.App.InstallUpdate(filePath, true) + + if (result?.success) { + installStatus.value = '安装成功' + updateProgressModal() + setTimeout(() => closeProgressModal(), 3000) + } else { + installing.value = false + installStatus.value = `安装失败: ${result?.message || '未知错误'}` + updateProgressModal() + Message.error('安装失败:' + (result?.message || '未知错误')) + } + } catch (error: any) { + installing.value = false + installStatus.value = `安装异常: ${error.message}` + updateProgressModal() + Message.error('安装失败:' + error.message) + } + } + + // ==================== 生命周期 ==================== + + let unsubscribeProgress: (() => void) | null = null + let unsubscribeComplete: (() => void) | null = null + + /** + * 应用启动时加载 + */ + onMounted(async () => { + await loadCurrentVersion() + await loadUpdateConfig() + + // 监听下载进度事件(确保在下载开始前注册) + unsubscribeProgress = EventsOn('download-progress', (data: string) => { + handleDownloadProgress(data) + }) + unsubscribeComplete = EventsOn('download-complete', (data: string) => { + handleDownloadComplete(data) + }) + + // 如果启用了自动检查,自动检查更新 + if (autoCheckEnabled.value) { + setTimeout(() => { + checkUpdate() + }, 1000) + } + }) + + /** + * 组件卸载时清理事件监听 + */ + onUnmounted(() => { + if (unsubscribeProgress) { + unsubscribeProgress() + unsubscribeProgress = null + } + if (unsubscribeComplete) { + unsubscribeComplete() + unsubscribeComplete = null + } + }) + + // ==================== 返回公共接口 ==================== + + return { + // 状态 + currentVersion, + autoCheckEnabled, + updateInfo, + + // 方法 + loadCurrentVersion, + checkUpdate, + downloadUpdate + } +} + diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..1b67291 --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import ArcoVue from '@arco-design/web-vue' +import '@arco-design/web-vue/dist/arco.css' +import './style.css' +import App from './App.vue' + +const app = createApp(App) +app.use(ArcoVue) +app.mount('#app') diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 0000000..7262dd6 --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,18 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', + 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; +} + +#app { + width: 100%; + height: 100vh; +} diff --git a/web/src/views/auth/ActivateForm.vue b/web/src/views/auth/ActivateForm.vue new file mode 100644 index 0000000..b085b98 --- /dev/null +++ b/web/src/views/auth/ActivateForm.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/web/src/views/auth/AuthPage.vue b/web/src/views/auth/AuthPage.vue new file mode 100644 index 0000000..f2beb6a --- /dev/null +++ b/web/src/views/auth/AuthPage.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/web/src/views/auth/AuthStatus.vue b/web/src/views/auth/AuthStatus.vue new file mode 100644 index 0000000..404477f --- /dev/null +++ b/web/src/views/auth/AuthStatus.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/web/src/views/data/BackupPanel.vue b/web/src/views/data/BackupPanel.vue new file mode 100644 index 0000000..c1fb7e7 --- /dev/null +++ b/web/src/views/data/BackupPanel.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/web/src/views/data/DataStats.vue b/web/src/views/data/DataStats.vue new file mode 100644 index 0000000..458c995 --- /dev/null +++ b/web/src/views/data/DataStats.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/web/src/views/data/PackagePanel.vue b/web/src/views/data/PackagePanel.vue new file mode 100644 index 0000000..509f5f9 --- /dev/null +++ b/web/src/views/data/PackagePanel.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/web/src/views/data/SyncPanel.vue b/web/src/views/data/SyncPanel.vue new file mode 100644 index 0000000..9be2a1b --- /dev/null +++ b/web/src/views/data/SyncPanel.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/web/src/views/query/QueryForm.vue b/web/src/views/query/QueryForm.vue new file mode 100644 index 0000000..76f2cc9 --- /dev/null +++ b/web/src/views/query/QueryForm.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/web/src/views/query/QueryPage.vue b/web/src/views/query/QueryPage.vue new file mode 100644 index 0000000..da0bd75 --- /dev/null +++ b/web/src/views/query/QueryPage.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/src/views/query/ResultPanel.vue b/web/src/views/query/ResultPanel.vue new file mode 100644 index 0000000..763b900 --- /dev/null +++ b/web/src/views/query/ResultPanel.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/web/src/wailsjs/go/main/App.d.ts b/web/src/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..08c5d33 --- /dev/null +++ b/web/src/wailsjs/go/main/App.d.ts @@ -0,0 +1,47 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {api} from '../models'; + +export function ActivateLicense(arg1:string):Promise>; + +export function BackupData():Promise>; + +export function CheckPackageUpdate(arg1:string):Promise>; + +export function CheckUpdate():Promise>; + +export function DownloadPackage(arg1:string):Promise>; + +export function DownloadUpdate(arg1:string):Promise>; + +export function GetAuthStatus():Promise>; + +export function GetCurrentVersion():Promise>; + +export function GetDataStats():Promise>; + +export function GetDeviceID():Promise; + +export function GetSyncStatus():Promise>; + +export function GetUpdateConfig():Promise>; + +export function Greet(arg1:string):Promise; + +export function ImportPackage(arg1:string):Promise>; + +export function InstallUpdate(arg1:string,arg2:boolean):Promise>; + +export function ListBackups():Promise>; + +export function ListLocalPackages():Promise>; + +export function QueryHistory(arg1:api.QueryRequest):Promise>; + +export function RestoreData(arg1:string):Promise>; + +export function SetUpdateConfig(arg1:boolean,arg2:number,arg3:string):Promise>; + +export function SyncData():Promise>; + +export function VerifyUpdateFile(arg1:string,arg2:string,arg3:string):Promise>; diff --git a/web/src/wailsjs/go/main/App.js b/web/src/wailsjs/go/main/App.js new file mode 100644 index 0000000..7adb81e --- /dev/null +++ b/web/src/wailsjs/go/main/App.js @@ -0,0 +1,91 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function ActivateLicense(arg1) { + return window['go']['main']['App']['ActivateLicense'](arg1); +} + +export function BackupData() { + return window['go']['main']['App']['BackupData'](); +} + +export function CheckPackageUpdate(arg1) { + return window['go']['main']['App']['CheckPackageUpdate'](arg1); +} + +export function CheckUpdate() { + return window['go']['main']['App']['CheckUpdate'](); +} + +export function DownloadPackage(arg1) { + return window['go']['main']['App']['DownloadPackage'](arg1); +} + +export function DownloadUpdate(arg1) { + return window['go']['main']['App']['DownloadUpdate'](arg1); +} + +export function GetAuthStatus() { + return window['go']['main']['App']['GetAuthStatus'](); +} + +export function GetCurrentVersion() { + return window['go']['main']['App']['GetCurrentVersion'](); +} + +export function GetDataStats() { + return window['go']['main']['App']['GetDataStats'](); +} + +export function GetDeviceID() { + return window['go']['main']['App']['GetDeviceID'](); +} + +export function GetSyncStatus() { + return window['go']['main']['App']['GetSyncStatus'](); +} + +export function GetUpdateConfig() { + return window['go']['main']['App']['GetUpdateConfig'](); +} + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} + +export function ImportPackage(arg1) { + return window['go']['main']['App']['ImportPackage'](arg1); +} + +export function InstallUpdate(arg1, arg2) { + return window['go']['main']['App']['InstallUpdate'](arg1, arg2); +} + +export function ListBackups() { + return window['go']['main']['App']['ListBackups'](); +} + +export function ListLocalPackages() { + return window['go']['main']['App']['ListLocalPackages'](); +} + +export function QueryHistory(arg1) { + return window['go']['main']['App']['QueryHistory'](arg1); +} + +export function RestoreData(arg1) { + return window['go']['main']['App']['RestoreData'](arg1); +} + +export function SetUpdateConfig(arg1, arg2, arg3) { + return window['go']['main']['App']['SetUpdateConfig'](arg1, arg2, arg3); +} + +export function SyncData() { + return window['go']['main']['App']['SyncData'](); +} + +export function VerifyUpdateFile(arg1, arg2, arg3) { + return window['go']['main']['App']['VerifyUpdateFile'](arg1, arg2, arg3); +} diff --git a/web/src/wailsjs/go/models.ts b/web/src/wailsjs/go/models.ts new file mode 100644 index 0000000..5db8c5a --- /dev/null +++ b/web/src/wailsjs/go/models.ts @@ -0,0 +1,21 @@ +export namespace api { + + export class QueryRequest { + red_balls: number[]; + blue_ball: number; + blue_ball_range: number[]; + + static createFrom(source: any = {}) { + return new QueryRequest(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.red_balls = source["red_balls"]; + this.blue_ball = source["blue_ball"]; + this.blue_ball_range = source["blue_ball_range"]; + } + } + +} + diff --git a/web/src/wailsjs/runtime/package.json b/web/src/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/web/src/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/web/src/wailsjs/runtime/runtime.d.ts b/web/src/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..4445dac --- /dev/null +++ b/web/src/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/web/src/wailsjs/runtime/runtime.js b/web/src/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..7cb89d7 --- /dev/null +++ b/web/src/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +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); +} \ No newline at end of file diff --git a/web/src/wailsjs/wailsjs/go/main/App.d.ts b/web/src/wailsjs/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..08c5d33 --- /dev/null +++ b/web/src/wailsjs/wailsjs/go/main/App.d.ts @@ -0,0 +1,47 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {api} from '../models'; + +export function ActivateLicense(arg1:string):Promise>; + +export function BackupData():Promise>; + +export function CheckPackageUpdate(arg1:string):Promise>; + +export function CheckUpdate():Promise>; + +export function DownloadPackage(arg1:string):Promise>; + +export function DownloadUpdate(arg1:string):Promise>; + +export function GetAuthStatus():Promise>; + +export function GetCurrentVersion():Promise>; + +export function GetDataStats():Promise>; + +export function GetDeviceID():Promise; + +export function GetSyncStatus():Promise>; + +export function GetUpdateConfig():Promise>; + +export function Greet(arg1:string):Promise; + +export function ImportPackage(arg1:string):Promise>; + +export function InstallUpdate(arg1:string,arg2:boolean):Promise>; + +export function ListBackups():Promise>; + +export function ListLocalPackages():Promise>; + +export function QueryHistory(arg1:api.QueryRequest):Promise>; + +export function RestoreData(arg1:string):Promise>; + +export function SetUpdateConfig(arg1:boolean,arg2:number,arg3:string):Promise>; + +export function SyncData():Promise>; + +export function VerifyUpdateFile(arg1:string,arg2:string,arg3:string):Promise>; diff --git a/web/src/wailsjs/wailsjs/go/main/App.js b/web/src/wailsjs/wailsjs/go/main/App.js new file mode 100644 index 0000000..7adb81e --- /dev/null +++ b/web/src/wailsjs/wailsjs/go/main/App.js @@ -0,0 +1,91 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function ActivateLicense(arg1) { + return window['go']['main']['App']['ActivateLicense'](arg1); +} + +export function BackupData() { + return window['go']['main']['App']['BackupData'](); +} + +export function CheckPackageUpdate(arg1) { + return window['go']['main']['App']['CheckPackageUpdate'](arg1); +} + +export function CheckUpdate() { + return window['go']['main']['App']['CheckUpdate'](); +} + +export function DownloadPackage(arg1) { + return window['go']['main']['App']['DownloadPackage'](arg1); +} + +export function DownloadUpdate(arg1) { + return window['go']['main']['App']['DownloadUpdate'](arg1); +} + +export function GetAuthStatus() { + return window['go']['main']['App']['GetAuthStatus'](); +} + +export function GetCurrentVersion() { + return window['go']['main']['App']['GetCurrentVersion'](); +} + +export function GetDataStats() { + return window['go']['main']['App']['GetDataStats'](); +} + +export function GetDeviceID() { + return window['go']['main']['App']['GetDeviceID'](); +} + +export function GetSyncStatus() { + return window['go']['main']['App']['GetSyncStatus'](); +} + +export function GetUpdateConfig() { + return window['go']['main']['App']['GetUpdateConfig'](); +} + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} + +export function ImportPackage(arg1) { + return window['go']['main']['App']['ImportPackage'](arg1); +} + +export function InstallUpdate(arg1, arg2) { + return window['go']['main']['App']['InstallUpdate'](arg1, arg2); +} + +export function ListBackups() { + return window['go']['main']['App']['ListBackups'](); +} + +export function ListLocalPackages() { + return window['go']['main']['App']['ListLocalPackages'](); +} + +export function QueryHistory(arg1) { + return window['go']['main']['App']['QueryHistory'](arg1); +} + +export function RestoreData(arg1) { + return window['go']['main']['App']['RestoreData'](arg1); +} + +export function SetUpdateConfig(arg1, arg2, arg3) { + return window['go']['main']['App']['SetUpdateConfig'](arg1, arg2, arg3); +} + +export function SyncData() { + return window['go']['main']['App']['SyncData'](); +} + +export function VerifyUpdateFile(arg1, arg2, arg3) { + return window['go']['main']['App']['VerifyUpdateFile'](arg1, arg2, arg3); +} diff --git a/web/src/wailsjs/wailsjs/go/models.ts b/web/src/wailsjs/wailsjs/go/models.ts new file mode 100644 index 0000000..5db8c5a --- /dev/null +++ b/web/src/wailsjs/wailsjs/go/models.ts @@ -0,0 +1,21 @@ +export namespace api { + + export class QueryRequest { + red_balls: number[]; + blue_ball: number; + blue_ball_range: number[]; + + static createFrom(source: any = {}) { + return new QueryRequest(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.red_balls = source["red_balls"]; + this.blue_ball = source["blue_ball"]; + this.blue_ball_range = source["blue_ball_range"]; + } + } + +} + diff --git a/web/src/wailsjs/wailsjs/runtime/package.json b/web/src/wailsjs/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/web/src/wailsjs/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/web/src/wailsjs/wailsjs/runtime/runtime.d.ts b/web/src/wailsjs/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..4445dac --- /dev/null +++ b/web/src/wailsjs/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/web/src/wailsjs/wailsjs/runtime/runtime.js b/web/src/wailsjs/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..7cb89d7 --- /dev/null +++ b/web/src/wailsjs/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +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); +} \ No newline at end of file diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..05b3d12 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + port: 34115, + strictPort: true, + }, +})