diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9541374 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(dir:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/README.md b/README.md index 61f2410..3632a94 100644 --- a/README.md +++ b/README.md @@ -1,221 +1,277 @@ -[![star](https://gitee.com/dromara/electron-egg/badge/star.svg?theme=gvp)](https://gitee.com/dromara/electron-egg/stargazers) +# PQS-9100 Tool Client -
-

🎉🎉🎉 ElectronEgg V4 已发布! 🎉🎉🎉

-
-
+## 项目简介 -
- -
+这是一个基于 Electron 的桌面端小工具,用于离线生成和管理设备激活码。 -
-

一个入门简单、跨平台、企业级桌面软件开发框架

-
-
+项目定位: - +- 面向 C 端桌面使用场景 +- 不依赖后端服务 +- 数据保存在本地内置 SQLite 数据库 +- 当前核心业务为设备激活记录管理 -## 📋 介绍 +该项目是在 `electron-egg` 模板基础上裁剪和扩展得到,现阶段实际业务代码集中在“设备激活”模块。 -> 框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、ERP、娱乐、视频等领域客户端,请放心使用! +## 技术栈 -## 👦 谁可以使用 +- 桌面框架:Electron 31 +- Electron 应用框架:electron-egg / ee-core +- 前端框架:Vue 3 +- UI 组件库:Ant Design Vue +- 构建工具:Vite 6 +- 本地数据库:better-sqlite3 +- 日期处理:dayjs +- 加解密:jsencrypt -项目已经有 5 个交流群,覆盖`前端`、`java`、`go`、`python`、`php` 等开发者。 +## 项目结构 -无论你是前端、服务端、运维、游戏、客户端等,都可以很快入门, +```text +. +├─electron/ Electron 主进程代码 +│ ├─config/ 应用配置 +│ ├─controller/ IPC 控制器 +│ ├─preload/ 应用启动与 preload 逻辑 +│ ├─service/ 主进程服务 +│ │ ├─database/ 本地数据库封装 +│ │ └─os/ 托盘、窗口、安全、更新等系统能力 +│ └─main.ts Electron 入口 +├─frontend/ Vue 前端应用 +│ ├─src/ +│ │ ├─api/ IPC 路由定义 +│ │ ├─assets/ 静态资源与样式 +│ │ ├─layouts/ 页面布局 +│ │ ├─router/ 路由配置 +│ │ ├─utils/ 工具函数 +│ │ └─views/activate/ 激活业务页面 +│ ├─.env.development 开发环境变量 +│ ├─.env.production 生产环境变量 +│ └─vite.config.ts 前端构建配置 +├─build/ 打包资源 +├─cmd/ electron-builder 配置 +├─public/ Electron 公共资源 +├─package.json 根脚本与 Electron 依赖 +└─README.md 项目说明 +``` -## 🐶 精彩案例 +## 核心功能 -- [**点击查看**](#项目案例) +### 1. 设备激活码生成 -## 📺 特点 -- 🍩 **为什么使用?** 桌面软件(办公方向、 个人工具),仍然是未来十几年PC端需求之一,提高工作效率 -- 🍉 **简单:** 只需懂 JavaScript -- 🍑 **愿景:** 所有开发者都能学会桌面软件研发 -- 🍰 **gitee:** https://gitee.com/dromara/electron-egg **5100+** -- 🍨 **github:** https://github.com/dromara/electron-egg **1800+** -- 🏆 码云最有价值开源项目 - ![](./public/images/example/ee-zs.png) +前端页面提供以下能力: -## 📚 文档 -- 快速体验:[教程文档](https://www.kaka996.com/) - ![](./public/images/example/v3-home.png) +- 录入设备申请码 +- 选择需要激活的模块 +- 生成设备激活码 +- 复制激活码 +- 填写备注 -## 📦 特性 -1. 🍄 跨平台:一套代码,可以打包成windows版、Mac版、Linux版、国产UOS、Deepin、麒麟等 -2. 🌹 架构:单业务进程/模块化/多任务(进程,线程,渲染进程),让开发大型项目变的简单。 -3. 🌱 简单高效:只需学习 js 语言 -4. 🌴 前端独立:理论上支持任何前端技术,如:vue、react、html等等 -5. 🍁 工程化:可以用前端、服务端的开发思维,来编写桌面软件 -6. 🌷 高性能:事件驱动、非阻塞式IO -7. 🌰 功能丰富:配置、通信、插件、数据库、升级、打包、工具... 应有尽有 -8. 💐 安全:支持字节码加密、压缩混淆加密 -9. 🌻 功能demo:桌面软件常见功能,框架集成或提供demo +当前支持的模块有: -## ✈️ 使用场景 +- `simulate` +- `digital` +- `contrast` -### 1. 🚀 常规桌面软件 -- 🚖 windows平台 +### 2. 激活记录管理 - ![](./public/images/example/ee-win-home.png) +激活记录页面支持: -- 🚍 macOS平台 - ![](./public/images/example/ee-mac-home.png) +- 按 `macAddress` 查询 +- 按模块组合查询 +- 查看历史激活记录 +- 复制历史记录中的激活码 +- 将新生成的激活记录保存到本地数据库 -- 🚔 linux平台 - 国产UOS、Deepin - ![](./public/images/example/uos-home.png) +### 3. 本地数据库存储 -- 🚔 linux平台 - ubuntu - ![](./public/images/example/ubuntu-db.png) +项目不依赖后端,激活记录直接写入本地 SQLite。 -### 🚐 2. vue、react、angular、web 转换成桌面软件 -- 🚙 vue-ant-design(本地) +当前数据库文件名: - ![](./public/images/example/vue-antd.png) +- `pqs9100-tool.db` -- 🚙 禅道项目管理(web项目地址) +当前表: - ![](./public/images/example/ee-project-7.png) +- `activate_record` -### 🚂 3. 游戏(h5相关技术开发) -- 🚊 忍者100层 +主要字段: - ![](./public/images/example/ee_game_1.png) +- `id` +- `macAddress` +- `applicationCode` +- `module` +- `activationCode` +- `createTime` +- `remark` +## 业务流程 -## 📒 开始使用 +整体流程如下: -- ✒️ [安装文档](https://www.kaka996.com/pages/e64ff6/) - -## 项目案例 -- 🐟 框架已经应用于医疗、学校、政务、股票交易、ERP、娱乐、视频、企业等领域客户端 +1. 用户在前端输入设备申请码。 +2. 前端使用 RSA 私钥解密申请码,解析出设备信息。 +3. 用户选择需要激活的模块。 +4. 前端组装激活码明文后,再用 RSA 公钥加密生成激活码。 +5. 用户点击保存后,前端通过 IPC 调用主进程。 +6. 主进程将记录写入本地 SQLite 数据库。 +7. 列表页面通过 IPC 查询本地数据库并展示历史记录。 -### 🐸 远控 +## 核心代码说明 -- RQ Center -![](./public/images/example/rq-1.png) -![](./public/images/example/rq-2.png) +### Electron 主进程 -### 🐸 云盘 +- `electron/main.ts` + - 应用启动入口 + - 注册生命周期与 preload -- FM Cloud -![](./public/images/example/fm-p2.png) -![](./public/images/example/fm-p1.png) -![](./public/images/example/fm-p4.png) +- `electron/preload/index.ts` + - 启动时初始化托盘 + - 初始化安全服务 + - 初始化自动更新服务 + - 初始化本地数据库 -### 🐸 IM +- `electron/preload/lifecycle.ts` + - 控制应用 ready、窗口 ready、关闭前等生命周期行为 + - 启动后自动设置主窗口大小和位置 -- Cede IM -![](./public/images/example/im-p1.png) -![](./public/images/example/im-p5.png) -![](./public/images/example/im-p1.png) +### 数据库相关 -### 🐸 壁纸 +- `electron/service/database/basedb.ts` + - SQLite 基础封装 + - 负责数据库文件路径和连接初始化 -- warpar -![](./public/images/example/aw-3.png) +- `electron/service/database/activateRecord.ts` + - `activate_record` 表初始化 + - 保存激活记录 + - 查询激活记录 + - 删除记录等数据库操作 -### 🐸 英雄联盟助手 +- `electron/controller/activateRecord.ts` + - 暴露激活记录相关 IPC 接口 -- Serendlplty -![](./public/images/example/lol-zhanji.png) +### 前端相关 -### 🐸 更多 +- `frontend/src/main.ts` + - Vue 应用入口 + - 注册 Ant Design Vue 和全局组件 -- [更多案例](https://www.kaka996.com/pages/eadf46/) +- `frontend/src/router/routerMap.ts` + - 当前仅注册一个业务页面:`/activate` -## 💬 交流 -1. [讨论](https://www.kaka996.com/pages/c2720e/) +- `frontend/src/layouts/AppSider.vue` + - 左侧菜单布局 + - 当前只包含“设备激活”菜单 -## 📌 关于pr -请前往[GitHub项目](https://github.com/dromara/electron-egg)提pr(避免代码同步后,pr被覆盖掉),感谢! +- `frontend/src/views/activate/index.vue` + - 激活记录查询页面 + - 激活记录展示、查询、复制、保存入口 -地址:https://github.com/dromara/electron-egg +- `frontend/src/views/activate/ActiveForm.vue` + - 激活码生成表单 + - 处理申请码解析、模块选择、激活码生成 -## 📔 框架核心包 ee-core -ee-core:[https://github.com/wallace5303/ee-core](https://github.com/wallace5303/ee-core) +- `frontend/src/utils/rsa.ts` + - RSA 加解密封装 -## 📚 Dromara 成员项目 +- `frontend/src/utils/ipcRenderer.ts` + - 渲染进程 IPC 调用封装 -

- - - - - - - - - - - - - - - - - - -

-

- - - - - - - - - - - - - - - - - - -

-

- - - - - - - - - - - - - - - - - - -

-

- - - - - - - - - - - - - - - - - - -

\ No newline at end of file +## 路由与界面 + +当前前端只有一条实际业务路由: + +- `/activate` 设备激活页面 + +左侧菜单当前也只有一个入口: + +- 设备激活 + +因此,整个系统目前可以理解为“单页面桌面工具”,所有主要功能都集中在这一个界面中完成。 + +当前页面的详细说明已单独整理到: + +- `docs/current-page.md` + +## 配置说明 + +### 根目录脚本 + +常用命令: + +- `npm run dev` 启动 Electron + 前端开发环境 +- `npm run dev-frontend` 仅启动前端 +- `npm run dev-electron` 仅启动 Electron +- `npm run build` 构建前端、Electron 并执行加密 +- `npm run build-w` 构建 Windows 安装包 + +### 前端环境变量 + +位于: + +- `frontend/.env.development` +- `frontend/.env.production` + +当前已使用的变量主要有: + +- `VITE_TITLE` +- `VITE_RSA_PUBLIC_KEY` +- `VITE_RSA_PRIVATE_KEY` +- `VITE_RSA_CAN_EDIT` + +## 数据存储说明 + +数据库由主进程在应用启动时初始化。 + +数据库初始化入口: + +- `electron/preload/index.ts` + +数据库基础路径由 `ee-core` 提供的数据目录决定,实际数据库文件保存在应用数据目录下的 `db` 子目录中。 + +## 当前项目特征总结 + +从当前代码看,项目有以下特点: + +- 业务功能集中,当前只有激活工具这一个主要模块 +- 没有后端依赖,所有数据均在本地处理和存储 +- 前端负责激活码的生成逻辑 +- 主进程负责数据库初始化与数据持久化 +- 项目仍保留部分 `electron-egg` 模板能力和示例代码 +- 现有目录中存在一些当前业务未直接使用的模板模块,可在后续按需要继续裁剪 + +## 适合后续演进的方向 + +结合当前项目现状,后续工作更适合围绕以下方向展开: + +- 梳理并精简模板残留代码 +- 完善激活记录管理能力 +- 增加导入、导出、删除、编辑等本地数据操作 +- 统一页面文案、编码和可维护性 +- 补充更明确的项目文档与交付说明 + +## 运行说明 + +### 开发环境 + +```bash +npm install +npm run dev +``` + +### 构建 + +```bash +npm run build +``` + +Windows 安装包构建: + +```bash +npm run build-w +``` + +## 补充说明 + +- 原仓库中的 `README.zh-CN.md` 更偏向上游模板说明 +- 当前 `README.md` 旨在描述本项目自身,而不是 `electron-egg` 模板本身 +- 如果后续继续迭代业务,建议优先维护本文件,保证项目文档与实际实现一致 diff --git a/README.zh-CN.md b/README.zh-CN.md deleted file mode 100644 index 61f2410..0000000 --- a/README.zh-CN.md +++ /dev/null @@ -1,221 +0,0 @@ -[![star](https://gitee.com/dromara/electron-egg/badge/star.svg?theme=gvp)](https://gitee.com/dromara/electron-egg/stargazers) - -
-

🎉🎉🎉 ElectronEgg V4 已发布! 🎉🎉🎉

-
-
- -
- -
- -
-

一个入门简单、跨平台、企业级桌面软件开发框架

-
-
- - - -## 📋 介绍 - -> 框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、ERP、娱乐、视频等领域客户端,请放心使用! - -## 👦 谁可以使用 - -项目已经有 5 个交流群,覆盖`前端`、`java`、`go`、`python`、`php` 等开发者。 - -无论你是前端、服务端、运维、游戏、客户端等,都可以很快入门, - -## 🐶 精彩案例 - -- [**点击查看**](#项目案例) - -## 📺 特点 -- 🍩 **为什么使用?** 桌面软件(办公方向、 个人工具),仍然是未来十几年PC端需求之一,提高工作效率 -- 🍉 **简单:** 只需懂 JavaScript -- 🍑 **愿景:** 所有开发者都能学会桌面软件研发 -- 🍰 **gitee:** https://gitee.com/dromara/electron-egg **5100+** -- 🍨 **github:** https://github.com/dromara/electron-egg **1800+** -- 🏆 码云最有价值开源项目 - ![](./public/images/example/ee-zs.png) - -## 📚 文档 -- 快速体验:[教程文档](https://www.kaka996.com/) - ![](./public/images/example/v3-home.png) - -## 📦 特性 -1. 🍄 跨平台:一套代码,可以打包成windows版、Mac版、Linux版、国产UOS、Deepin、麒麟等 -2. 🌹 架构:单业务进程/模块化/多任务(进程,线程,渲染进程),让开发大型项目变的简单。 -3. 🌱 简单高效:只需学习 js 语言 -4. 🌴 前端独立:理论上支持任何前端技术,如:vue、react、html等等 -5. 🍁 工程化:可以用前端、服务端的开发思维,来编写桌面软件 -6. 🌷 高性能:事件驱动、非阻塞式IO -7. 🌰 功能丰富:配置、通信、插件、数据库、升级、打包、工具... 应有尽有 -8. 💐 安全:支持字节码加密、压缩混淆加密 -9. 🌻 功能demo:桌面软件常见功能,框架集成或提供demo - -## ✈️ 使用场景 - -### 1. 🚀 常规桌面软件 -- 🚖 windows平台 - - ![](./public/images/example/ee-win-home.png) - -- 🚍 macOS平台 - ![](./public/images/example/ee-mac-home.png) - -- 🚔 linux平台 - 国产UOS、Deepin - ![](./public/images/example/uos-home.png) - -- 🚔 linux平台 - ubuntu - ![](./public/images/example/ubuntu-db.png) - -### 🚐 2. vue、react、angular、web 转换成桌面软件 -- 🚙 vue-ant-design(本地) - - ![](./public/images/example/vue-antd.png) - -- 🚙 禅道项目管理(web项目地址) - - ![](./public/images/example/ee-project-7.png) - -### 🚂 3. 游戏(h5相关技术开发) -- 🚊 忍者100层 - - ![](./public/images/example/ee_game_1.png) - - -## 📒 开始使用 - -- ✒️ [安装文档](https://www.kaka996.com/pages/e64ff6/) - -## 项目案例 -- 🐟 框架已经应用于医疗、学校、政务、股票交易、ERP、娱乐、视频、企业等领域客户端 - -### 🐸 远控 - -- RQ Center -![](./public/images/example/rq-1.png) -![](./public/images/example/rq-2.png) - -### 🐸 云盘 - -- FM Cloud -![](./public/images/example/fm-p2.png) -![](./public/images/example/fm-p1.png) -![](./public/images/example/fm-p4.png) - -### 🐸 IM - -- Cede IM -![](./public/images/example/im-p1.png) -![](./public/images/example/im-p5.png) -![](./public/images/example/im-p1.png) - -### 🐸 壁纸 - -- warpar -![](./public/images/example/aw-3.png) - -### 🐸 英雄联盟助手 - -- Serendlplty -![](./public/images/example/lol-zhanji.png) - -### 🐸 更多 - -- [更多案例](https://www.kaka996.com/pages/eadf46/) - -## 💬 交流 -1. [讨论](https://www.kaka996.com/pages/c2720e/) - -## 📌 关于pr -请前往[GitHub项目](https://github.com/dromara/electron-egg)提pr(避免代码同步后,pr被覆盖掉),感谢! - -地址:https://github.com/dromara/electron-egg - -## 📔 框架核心包 ee-core -ee-core:[https://github.com/wallace5303/ee-core](https://github.com/wallace5303/ee-core) - -## 📚 Dromara 成员项目 - -

- - - - - - - - - - - - - - - - - - -

-

- - - - - - - - - - - - - - - - - - -

-

- - - - - - - - - - - - - - - - - - -

-

- - - - - - - - - - - - - - - - - - -

\ No newline at end of file diff --git a/build-portable.bat b/build-portable.bat new file mode 100644 index 0000000..bbc4a1e --- /dev/null +++ b/build-portable.bat @@ -0,0 +1,129 @@ +@echo off +setlocal enabledelayedexpansion + +cd /d "%~dp0" + +set "ROOT_DIR=%CD%" +set "OUT_DIR=%ROOT_DIR%\out" +set "SOURCE_DIR=%OUT_DIR%\win-unpacked" +set "RELEASE_DIR=%OUT_DIR%\NPQS9100_tool" +set "APP_NAME=NPQS9100_tool.exe" +set "LOG_FILE=%ROOT_DIR%\build-portable.log" + +if exist "%LOG_FILE%" del /q "%LOG_FILE%" + +call :log "========================================" +call :log "Building folder app..." +call :log "Project: %ROOT_DIR%" +call :log "Source : %SOURCE_DIR%" +call :log "Target : %RELEASE_DIR%" +call :log "========================================" + +where npm >nul 2>nul +if errorlevel 1 ( + call :fail "npm not found. Please install Node.js first." + goto :end +) + +call :run "Installing root dependencies" "npm install" +if errorlevel 1 goto :end + +call :run "Installing frontend dependencies" "npm install --prefix frontend" +if errorlevel 1 goto :end + +call :run "Building project" "npm run build" +if errorlevel 1 goto :end + +call :run "Packaging folder app" "npm run build-wd" +if errorlevel 1 goto :end + +call :log "" +call :log "[5/5] Renaming output folder..." + +if not exist "%OUT_DIR%" ( + call :fail "out folder does not exist." + goto :end +) + +if exist "%RELEASE_DIR%" ( + rd /s /q "%RELEASE_DIR%" + if errorlevel 1 ( + call :fail "failed to delete existing target folder: %RELEASE_DIR%" + goto :end +) +) + +if not exist "%SOURCE_DIR%" ( + call :fail "folder output not found: %SOURCE_DIR%" + goto :end +) + +ren "%SOURCE_DIR%" "NPQS9100_tool" +if errorlevel 1 ( + call :fail "failed to rename win-unpacked to NPQS9100_tool" + goto :end +) + +for %%F in ("%OUT_DIR%\*.exe") do ( + if exist "%%~fF" del /q "%%~fF" +) + +for %%F in ("%RELEASE_DIR%\*.exe") do ( + if /i not "%%~nxF"=="%APP_NAME%" ( + ren "%%~fF" "%APP_NAME%" + goto :renamed + ) +) + +:renamed +call :log "" +call :log "Folder app ready:" +call :log "Folder: %RELEASE_DIR%" +call :log "EXE : %RELEASE_DIR%\%APP_NAME%" +call :log "Runtime logs will be written beside the exe as:" +call :log "%RELEASE_DIR%\runtime.log" +call :log "Build log:" +call :log "%LOG_FILE%" +call :log "========================================" +goto :success + +:run +set "STEP_NAME=%~1" +set "STEP_CMD=%~2" +call :log "" +call :log "==== %STEP_NAME% ====" +call :log "CMD: %STEP_CMD%" +cmd /c "%STEP_CMD%" >> "%LOG_FILE%" 2>&1 +if errorlevel 1 ( + call :fail "%STEP_NAME% failed." + exit /b 1 +) +call :log "%STEP_NAME% completed." +exit /b 0 + +:log +if "%~1"=="" ( + echo. + >> "%LOG_FILE%" echo. + exit /b 0 +) +echo %~1 +>> "%LOG_FILE%" echo %~1 +exit /b 0 + +:fail +call :log "[ERROR] %~1" +call :log "Please check: %LOG_FILE%" +echo. +echo [ERROR] %~1 +echo See log: %LOG_FILE% + +exit /b 1 + +:success +echo. +echo Build finished successfully. +pause + +:end +endlocal diff --git a/cmd/bin.js b/cmd/bin.js index f62f4ba..57cc2ac 100644 --- a/cmd/bin.js +++ b/cmd/bin.js @@ -47,6 +47,11 @@ module.exports = { win_e: { args: ['--config=./cmd/builder.json', '-w=portable', '--x64'], }, + win_dir: { + cmd: 'electron-builder', + directory: './', + args: ['--config=./cmd/builder.json', '-w=dir', '--x64'], + }, win_7z: { args: ['--config=./cmd/builder.json', '-w=7z', '--x64'], }, @@ -196,4 +201,4 @@ module.exports = { stdio: "inherit", // ignore }, }, -}; \ No newline at end of file +}; diff --git a/docs/current-page.md b/docs/current-page.md new file mode 100644 index 0000000..543be73 --- /dev/null +++ b/docs/current-page.md @@ -0,0 +1,226 @@ +# 当前页面说明 + +## 页面定位 + +当前系统只有一个实际业务页面,即“设备激活”页面。 + +页面对应代码: + +- `frontend/src/views/activate/index.vue` +- `frontend/src/views/activate/ActiveForm.vue` + +该页面同时承担两类职责: + +- 激活码生成 +- 激活记录管理 + +也就是说,它不是单纯的录入页,而是把“生成新激活码”和“查询历史激活记录”放在一个界面中完成。 + +## 页面结构 + +从界面结构上看,当前页面可以分成 3 个区域: + +1. 激活记录查询区 +2. 激活记录列表区 +3. 设备激活弹窗区 + +## 1. 激活记录查询区 + +页面顶部是激活记录查询区,用于筛选本地数据库中的历史记录。 + +包含以下控件: + +- `macAddress` 输入框 + - 用于按设备 MAC 地址筛选记录 + +- 激活模块多选框 + - 用于按模块筛选记录 + - 支持多选 + - 当前可选模块: + - `simulate` + - `digital` + - `contrast` + +- 查询按钮 + - 按当前条件查询本地 SQLite 中的激活记录 + +- 重置按钮 + - 清空查询条件 + - 清空后重新查询全部记录 + +该区域的作用是帮助用户快速定位某台设备、某类模块,或直接查看全部历史记录。 + +## 2. 激活记录列表区 + +查询区下方是激活记录列表区,用于展示本地数据库中的激活历史。 + +当前列表字段如下: + +- `applicant` + - 申请方 + +- `macAddress` + - 设备 MAC 地址 + +- `applicationCode` + - 设备申请码 + - 长内容以省略形式展示 + +- `module` + - 当前记录对应的激活模块 + - 界面中会拆分为标签显示 + +- `activationCode` + - 最终生成的激活码 + - 长内容以省略形式展示 + +- `createTime` + - 记录生成时间 + +- `remark` + - 备注信息 + +- `action` + - 当前提供“复制激活码”和“删除”操作 + +列表区支持的功能: + +- 展示本地全部或筛选后的激活记录 +- 分页显示,当前每页 10 条 +- 无数据时显示空状态 +- 直接复制某条历史记录中的激活码 +- 删除单条历史记录 + +列表上方工具区还提供 3 个批量操作: + +- 备份数据 + - 将当前本地激活记录导出为 JSON 备份文件 + +- 导入备份 + - 选择之前导出的 JSON 备份文件 + - 导入时会先清空当前数据,再恢复备份内容 + +- 清空数据 + - 一次性删除本地全部激活记录 + +该区域主要用于“查历史”和“复用历史结果”,避免重复生成。 + +## 3. 设备激活弹窗区 + +列表区上方提供“设备激活”按钮,点击后打开激活弹窗。 + +这个弹窗是当前页面的核心操作区,用于生成新的激活码并保存到本地数据库。 + +### 3.1 RSA 密钥配置区 + +该区域是否显示,由环境变量 `VITE_RSA_CAN_EDIT` 控制。 + +如果开启,则会显示: + +- RSA 公钥文本框 +- RSA 私钥文本框 +- 复制公钥按钮 +- 复制私钥按钮 + +当前默认配置为不显示该区域。 + +### 3.2 激活模块选择区 + +用户可以通过 3 个开关选择需要激活的模块: + +- 模拟模式模块 `simulate` +- 数字模式模块 `digital` +- 对比模式模块 `contrast` + +生成激活码前,至少要选择一个模块,否则页面会提示错误。 + +### 3.3 申请方输入区 + +用户可以录入本次激活记录的申请方。 + +该字段当前用于: + +- 标识该条激活记录属于谁 +- 辅助后续在历史列表中识别记录来源 + +申请方不是生成激活码的必要条件,但会和激活记录一起保存到本地数据库。 + +### 3.4 设备申请码输入区 + +用户在这里输入或粘贴设备申请码。 + +这是生成激活码的前提条件,当前规则包括: + +- 不能为空 +- 必须能被当前 RSA 私钥正确解密 +- 解密结果中必须包含 `macAddress` + +如果申请码无效,页面会提示错误并终止生成。 + +### 3.5 设备激活码展示区 + +当模块选择和申请码校验通过后,用户可以点击“生成激活码”。 + +生成成功后: + +- 激活码显示在只读文本框中 +- 用户可以点击“复制激活码”直接复制结果 + +这里展示的是最终输出内容,也是后续保存到数据库中的核心字段。 + +### 3.6 备注区 + +用户可以输入备注信息。 + +当前限制: + +- 最长 120 个字符 + +备注会随激活记录一起保存,方便后续查询和识别用途。 + +### 3.7 弹窗底部操作区 + +弹窗底部有两个按钮: + +- 保存 + - 前提是必须已经生成激活码 + - 点击后将本次数据写入本地 SQLite + - 保存成功后关闭弹窗,并刷新主页面列表 + +- 关闭 + - 直接关闭弹窗 + - 不保存当前内容 + +## 页面操作流程 + +从用户视角,当前页面的典型使用流程如下: + +1. 进入“设备激活”页面。 +2. 点击“设备激活”按钮,打开激活弹窗。 +3. 选择一个或多个需要激活的模块。 +4. 录入申请方。 +5. 输入设备申请码。 +6. 点击“生成激活码”。 +7. 生成成功后查看或复制激活码。 +8. 如有需要,填写备注。 +9. 点击“保存”,将数据写入本地数据库。 +10. 返回主页面后,在历史列表中查看新记录。 +11. 后续如需再次使用,可直接从列表复制历史激活码。 +12. 如有需要,可手动备份当前数据,或在清空后通过备份文件恢复。 + +## 页面价值总结 + +当前页面虽然只有一个,但已经覆盖完整核心业务流程: + +- 录入申请方 +- 输入申请码 +- 选择激活模块 +- 生成激活码 +- 保存激活记录 +- 查询历史记录 +- 复制历史激活码 +- 删除历史记录 +- 备份本地数据 +- 导入备份恢复数据 + +因此,这个页面本质上是一个“激活工具 + 本地记录管理工具”的组合界面。 diff --git a/electron/controller/activateRecord.ts b/electron/controller/activateRecord.ts index 8089f44..023967a 100644 --- a/electron/controller/activateRecord.ts +++ b/electron/controller/activateRecord.ts @@ -1,13 +1,16 @@ -import {activateRecordService} from "../service/database/activateRecord"; +import path from 'path'; +import { app as electronApp, dialog } from 'electron'; +import { activateRecordService } from "../service/database/activateRecord"; class ActivateRecordController { async list(args: { macAddress: string, modules: string[] }): Promise { - const {macAddress, modules} = args - return activateRecordService.list(macAddress, modules) + const { macAddress, modules } = args; + return activateRecordService.list(macAddress, modules); } async save(args: { + applicant: string, macAddress: string, applicationCode: string, modules: string[], @@ -15,8 +18,76 @@ class ActivateRecordController { createTime: string, remark: string }): Promise { - const { modules} = args - return activateRecordService.save({...args, module : modules.join(',')}) + const { modules } = args; + return activateRecordService.save({ ...args, module: modules.join(',') }); + } + + async removeById(args: { id: number }): Promise { + const { id } = args; + return activateRecordService.removeById(id); + } + + async clear(): Promise { + return activateRecordService.clear(); + } + + async backup(): Promise { + const defaultFileName = `activate-record-backup-${this.getTimestamp()}.json`; + const filePath = dialog.showSaveDialogSync({ + title: '导出备份', + defaultPath: path.join(electronApp.getPath('documents'), defaultFileName), + filters: [ + { name: 'JSON', extensions: ['json'] } + ] + }); + + if (!filePath) { + return { + canceled: true + }; + } + + const count = await activateRecordService.backup(filePath); + return { + canceled: false, + count, + filePath + }; + } + + async importBackup(): Promise { + const filePaths = dialog.showOpenDialogSync({ + title: '导入备份', + properties: ['openFile'], + filters: [ + { name: 'JSON', extensions: ['json'] } + ] + }); + + if (!filePaths || filePaths.length === 0) { + return { + canceled: true + }; + } + + const filePath = filePaths[0]; + const count = await activateRecordService.importBackup(filePath); + return { + canceled: false, + count, + filePath + }; + } + + private getTimestamp(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = `${now.getMonth() + 1}`.padStart(2, '0'); + const day = `${now.getDate()}`.padStart(2, '0'); + const hour = `${now.getHours()}`.padStart(2, '0'); + const minute = `${now.getMinutes()}`.padStart(2, '0'); + const second = `${now.getSeconds()}`.padStart(2, '0'); + return `${year}${month}${day}-${hour}${minute}${second}`; } } diff --git a/electron/main.ts b/electron/main.ts index f3f9a91..5ea7cf3 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,6 +1,30 @@ +import fs from 'fs'; +import path from 'path'; import { ElectronEgg } from 'ee-core'; import { Lifecycle } from './preload/lifecycle'; import { preload } from './preload'; +import { app as electronApp } from 'electron'; + +function writeRuntimeLog(message: string): void { + try { + const baseDir = electronApp.isPackaged ? path.dirname(process.execPath) : process.cwd(); + const logFile = path.join(baseDir, 'runtime.log'); + const line = `[${new Date().toISOString()}] ${message}\n`; + fs.appendFileSync(logFile, line, 'utf8'); + } catch (error) { + console.error('[runtime-log] write failed:', error); + } +} + +process.on('uncaughtException', (error) => { + writeRuntimeLog(`uncaughtException: ${error?.stack || error}`); +}); + +process.on('unhandledRejection', (reason) => { + writeRuntimeLog(`unhandledRejection: ${String(reason)}`); +}); + +writeRuntimeLog('app bootstrap start'); // New app const app = new ElectronEgg(); @@ -16,4 +40,5 @@ app.register("before-close", life.beforeClose); app.register("preload", preload); // Run -app.run(); \ No newline at end of file +app.run(); +writeRuntimeLog('app bootstrap end'); diff --git a/electron/preload/lifecycle.ts b/electron/preload/lifecycle.ts index 0a22700..0a532af 100644 --- a/electron/preload/lifecycle.ts +++ b/electron/preload/lifecycle.ts @@ -2,6 +2,19 @@ import { app as electronApp, screen } from 'electron'; import { logger } from 'ee-core/log'; import { getConfig } from 'ee-core/config'; import { getMainWindow } from 'ee-core/electron'; +import fs from 'fs'; +import path from 'path'; + +function writeRuntimeLog(message: string): void { + try { + const baseDir = electronApp.isPackaged ? path.dirname(process.execPath) : process.cwd(); + const logFile = path.join(baseDir, 'runtime.log'); + const line = `[${new Date().toISOString()}] ${message}\n`; + fs.appendFileSync(logFile, line, 'utf8'); + } catch (error) { + console.error('[runtime-log] write failed:', error); + } +} class Lifecycle { /** @@ -36,6 +49,16 @@ class Lifecycle { const win = getMainWindow(); + win.webContents.on('did-finish-load', () => { + logger.info('[window] did-finish-load'); + writeRuntimeLog('window did-finish-load'); + }); + + win.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => { + logger.error('[window] did-fail-load errorCode:', errorCode, 'errorDescription:', errorDescription, 'url:', validatedURL); + writeRuntimeLog(`window did-fail-load errorCode=${errorCode} errorDescription=${errorDescription} url=${validatedURL}`); + }); + // The window is centered and scaled proportionally // Obtain the size information of the main screen, calculate the width and height of the window as a percentage of the screen, // and calculate the coordinates of the upper left corner when the window is centered @@ -67,4 +90,4 @@ class Lifecycle { } Lifecycle.toString = () => '[class Lifecycle]'; -export { Lifecycle }; \ No newline at end of file +export { Lifecycle }; diff --git a/electron/service/database/activateRecord.ts b/electron/service/database/activateRecord.ts index 341dee0..64f1baf 100644 --- a/electron/service/database/activateRecord.ts +++ b/electron/service/database/activateRecord.ts @@ -1,8 +1,18 @@ -import {BasedbService} from './basedb'; +import fs from 'fs'; +import { BasedbService } from './basedb'; + +interface ActivateRecordItem { + applicant: string; + macAddress: string; + applicationCode: string; + module: string; + activationCode: string; + createTime: string; + remark: string; +} /** - * sqlite数据存储 - * @class + * sqlite data storage */ class ActivateRecordService extends BasedbService { tableName: string; @@ -10,75 +20,89 @@ class ActivateRecordService extends BasedbService { constructor() { const options = { dbname: 'pqs9100-tool.db', - } + }; super(options); this.tableName = 'activate_record'; } - /* - * 初始化表 + /** + * Initialize table and perform lightweight schema migration. */ init(): void { this._init(); - // 检查表是否存在 const masterStmt = this.db.prepare('SELECT * FROM sqlite_master WHERE type=? AND name = ?'); - let tableExists = masterStmt.get('table', this.tableName); + const tableExists = masterStmt.get('table', this.tableName); if (!tableExists) { - // 创建表 - const create_table_sql = + const createTableSql = `CREATE TABLE ${this.tableName} ( id INTEGER PRIMARY KEY AUTOINCREMENT, + applicant CHAR(100) NULL, macAddress CHAR(50) NOT NULL, applicationCode CHAR(2000) NOT NULL, module CHAR(200) NOT NULL, activationCode CHAR(2000) NOT NULL, createTime CHAR(32) NOT NULL, - remark CHAR(120) NULL - );` - this.db.exec(create_table_sql); + remark CHAR(120) NULL + );`; + this.db.exec(createTableSql); + return; + } + + const columns = this.db.prepare(`PRAGMA table_info(${this.tableName})`).all() as Array<{ name: string }>; + const hasApplicant = columns.some((column) => column.name === 'applicant'); + if (!hasApplicant) { + this.db.exec(`ALTER TABLE ${this.tableName} ADD COLUMN applicant CHAR(100) NULL`); } } - /* - * 增 data (sqlite) + /** + * Insert one record. */ - async save(data: { - macAddress: string; - applicationCode: string; - module: string; - activationCode: string; - createTime: string; - remark: string; - }) { - const insert = this.db.prepare(`INSERT INTO ${this.tableName} (macAddress, applicationCode, module, activationCode, createTime, remark) - VALUES (@macAddress, @applicationCode, @module, @activationCode, @createTime, @remark)`); - insert.run(data); + async save(data: ActivateRecordItem) { + const insert = this.db.prepare( + `INSERT INTO ${this.tableName} (applicant, macAddress, applicationCode, module, activationCode, createTime, remark) + VALUES (@applicant, @macAddress, @applicationCode, @module, @activationCode, @createTime, @remark)` + ); + insert.run({ + ...data, + applicant: data.applicant || '', + remark: data.remark || '' + }); return true; } - /* - * 删 data + /** + * Delete one record by id. */ - async removeById(name: string = ''): Promise { + async removeById(id: number): Promise { const remove = this.db.prepare(`DELETE FROM ${this.tableName} WHERE id = ?`); - remove.run(name); + remove.run(id); return true; } - /* - * 查list data (sqlite) + /** + * Clear all records. + */ + async clear(): Promise { + const clearStmt = this.db.prepare(`DELETE FROM ${this.tableName}`); + clearStmt.run(); + return true; + } + + /** + * Query record list. */ async list(macAddress: string = '', modules: string[] = []): Promise { - let condition = '' + let condition = ''; if (macAddress) { - condition += ` AND macAddress = '${macAddress}'` + condition += ` AND macAddress = '${macAddress}'`; } if (modules.length > 0) { - const moduleConditions = modules.map(module => `module LIKE '%${module}%'`).join(' OR '); + const moduleConditions = modules.map((module) => `module LIKE '%${module}%'`).join(' OR '); condition += ` AND (${moduleConditions})`; } const select = this.db.prepare(`SELECT * @@ -88,35 +112,88 @@ class ActivateRecordService extends BasedbService { return select.all(); } - /* - * all Test data (sqlite) + /** + * Export all records to a JSON backup file. + */ + async backup(filePath: string): Promise { + const records = await this.getAllTestDataSqlite(); + const payload = { + version: 1, + tableName: this.tableName, + exportedAt: new Date().toISOString(), + records + }; + fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8'); + return records.length; + } + + /** + * Import backup and replace current records. + */ + async importBackup(filePath: string): Promise { + const raw = fs.readFileSync(filePath, 'utf8'); + const parsed = JSON.parse(raw); + const inputRecords = Array.isArray(parsed) ? parsed : parsed.records; + if (!Array.isArray(inputRecords)) { + throw new Error('Invalid backup file'); + } + + const records = inputRecords.map((item: any) => this.normalizeRecord(item)); + const clearStmt = this.db.prepare(`DELETE FROM ${this.tableName}`); + const insertStmt = this.db.prepare( + `INSERT INTO ${this.tableName} (applicant, macAddress, applicationCode, module, activationCode, createTime, remark) + VALUES (@applicant, @macAddress, @applicationCode, @module, @activationCode, @createTime, @remark)` + ); + const transaction = this.db.transaction((rows: ActivateRecordItem[]) => { + clearStmt.run(); + for (const row of rows) { + insertStmt.run(row); + } + }); + + transaction(records); + return records.length; + } + + /** + * Read all records. */ async getAllTestDataSqlite(): Promise { const selectAllUser = this.db.prepare(`SELECT * - FROM ${this.tableName} `); - const allUser = selectAllUser.all(); - return allUser; + FROM ${this.tableName} + ORDER BY id DESC`); + return selectAllUser.all(); } - /* - * get data dir (sqlite) + /** + * Get data directory. */ async getDataDir(): Promise { - const dir = this.storage.getDbDir(); - return dir; + return this.storage.getDbDir(); } - /* - * set custom data dir (sqlite) + /** + * Set custom data directory. */ async setCustomDataDir(dir: string): Promise { - if (dir.length == 0) { + if (dir.length === 0) { return; } this.changeDataDir(dir); this.init(); - return; + } + + private normalizeRecord(item: any): ActivateRecordItem { + return { + applicant: typeof item?.applicant === 'string' ? item.applicant : '', + macAddress: typeof item?.macAddress === 'string' ? item.macAddress : '', + applicationCode: typeof item?.applicationCode === 'string' ? item.applicationCode : '', + module: typeof item?.module === 'string' ? item.module : Array.isArray(item?.modules) ? item.modules.join(',') : '', + activationCode: typeof item?.activationCode === 'string' ? item.activationCode : '', + createTime: typeof item?.createTime === 'string' ? item.createTime : '', + remark: typeof item?.remark === 'string' ? item.remark : '', + }; } } diff --git a/frontend/package.json b/frontend/package.json index 0b182ab..f904b5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "jsencrypt": "^3.5.4", "node-forge": "^1.3.1", "pinia": "^3.0.3", + "pqs9100_tool": "file:..", "socket.io-client": "^4.8.1", "store2": "^2.14.4", "vue": "^3.5.22", diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1c3c315..07f79aa 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -7,6 +7,10 @@ const ipcApiRoute = { activateRecord: { list: 'controller/activateRecord/list', save: 'controller/activateRecord/save', + removeById: 'controller/activateRecord/removeById', + clear: 'controller/activateRecord/clear', + backup: 'controller/activateRecord/backup', + importBackup: 'controller/activateRecord/importBackup', }, framework: { checkForUpdater: 'controller/framework/checkForUpdater', diff --git a/frontend/src/views/activate/ActiveForm.vue b/frontend/src/views/activate/ActiveForm.vue index 179e3d7..ad9bc57 100644 --- a/frontend/src/views/activate/ActiveForm.vue +++ b/frontend/src/views/activate/ActiveForm.vue @@ -1,144 +1,179 @@ + + diff --git a/frontend/src/views/activate/index.vue b/frontend/src/views/activate/index.vue index 8fa49be..614f634 100644 --- a/frontend/src/views/activate/index.vue +++ b/frontend/src/views/activate/index.vue @@ -3,12 +3,23 @@ - + - + 模拟式模块 数字式模块 比对式模块 @@ -33,37 +44,87 @@ - - - 设备激活 - - + + + + 设备激活 + + + + 备份数据 + + + + 导入备份 + + + + 清空数据 + + + - + diff --git a/package.json b/package.json index 59b8a2d..13ade9f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "re-sqlite": "electron-rebuild -f -w better-sqlite3", "build-w": "ee-bin build --cmds=win64", "build-we": "ee-bin build --cmds=win_e", + "build-wd": "ee-bin build --cmds=win_dir", "build-m": "ee-bin build --cmds=mac", "build-m-arm64": "ee-bin build --cmds=mac_arm64", "build-l": "ee-bin build --cmds=linux",