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 @@
-[](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+**
-- 🏆 码云最有价值开源项目
- 
+前端页面提供以下能力:
-## 📚 文档
-- 快速体验:[教程文档](https://www.kaka996.com/)
- 
+- 录入设备申请码
+- 选择需要激活的模块
+- 生成设备激活码
+- 复制激活码
+- 填写备注
-## 📦 特性
-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. 激活记录管理
- 
+激活记录页面支持:
-- 🚍 macOS平台
- 
+- 按 `macAddress` 查询
+- 按模块组合查询
+- 查看历史激活记录
+- 复制历史记录中的激活码
+- 将新生成的激活记录保存到本地数据库
-- 🚔 linux平台 - 国产UOS、Deepin
- 
+### 3. 本地数据库存储
-- 🚔 linux平台 - ubuntu
- 
+项目不依赖后端,激活记录直接写入本地 SQLite。
-### 🚐 2. vue、react、angular、web 转换成桌面软件
-- 🚙 vue-ant-design(本地)
+当前数据库文件名:
- 
+- `pqs9100-tool.db`
-- 🚙 禅道项目管理(web项目地址)
+当前表:
- 
+- `activate_record`
-### 🚂 3. 游戏(h5相关技术开发)
-- 🚊 忍者100层
+主要字段:
- 
+- `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
-
-
+### Electron 主进程
-### 🐸 云盘
+- `electron/main.ts`
+ - 应用启动入口
+ - 注册生命周期与 preload
-- FM Cloud
-
-
-
+- `electron/preload/index.ts`
+ - 启动时初始化托盘
+ - 初始化安全服务
+ - 初始化自动更新服务
+ - 初始化本地数据库
-### 🐸 IM
+- `electron/preload/lifecycle.ts`
+ - 控制应用 ready、窗口 ready、关闭前等生命周期行为
+ - 启动后自动设置主窗口大小和位置
-- Cede IM
-
-
-
+### 数据库相关
-### 🐸 壁纸
+- `electron/service/database/basedb.ts`
+ - SQLite 基础封装
+ - 负责数据库文件路径和连接初始化
-- warpar
-
+- `electron/service/database/activateRecord.ts`
+ - `activate_record` 表初始化
+ - 保存激活记录
+ - 查询激活记录
+ - 删除记录等数据库操作
-### 🐸 英雄联盟助手
+- `electron/controller/activateRecord.ts`
+ - 暴露激活记录相关 IPC 接口
-- Serendlplty
-
+### 前端相关
-### 🐸 更多
+- `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 @@
-[](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+**
-- 🏆 码云最有价值开源项目
- 
-
-## 📚 文档
-- 快速体验:[教程文档](https://www.kaka996.com/)
- 
-
-## 📦 特性
-1. 🍄 跨平台:一套代码,可以打包成windows版、Mac版、Linux版、国产UOS、Deepin、麒麟等
-2. 🌹 架构:单业务进程/模块化/多任务(进程,线程,渲染进程),让开发大型项目变的简单。
-3. 🌱 简单高效:只需学习 js 语言
-4. 🌴 前端独立:理论上支持任何前端技术,如:vue、react、html等等
-5. 🍁 工程化:可以用前端、服务端的开发思维,来编写桌面软件
-6. 🌷 高性能:事件驱动、非阻塞式IO
-7. 🌰 功能丰富:配置、通信、插件、数据库、升级、打包、工具... 应有尽有
-8. 💐 安全:支持字节码加密、压缩混淆加密
-9. 🌻 功能demo:桌面软件常见功能,框架集成或提供demo
-
-## ✈️ 使用场景
-
-### 1. 🚀 常规桌面软件
-- 🚖 windows平台
-
- 
-
-- 🚍 macOS平台
- 
-
-- 🚔 linux平台 - 国产UOS、Deepin
- 
-
-- 🚔 linux平台 - ubuntu
- 
-
-### 🚐 2. vue、react、angular、web 转换成桌面软件
-- 🚙 vue-ant-design(本地)
-
- 
-
-- 🚙 禅道项目管理(web项目地址)
-
- 
-
-### 🚂 3. 游戏(h5相关技术开发)
-- 🚊 忍者100层
-
- 
-
-
-## 📒 开始使用
-
-- ✒️ [安装文档](https://www.kaka996.com/pages/e64ff6/)
-
-## 项目案例
-- 🐟 框架已经应用于医疗、学校、政务、股票交易、ERP、娱乐、视频、企业等领域客户端
-
-### 🐸 远控
-
-- RQ Center
-
-
-
-### 🐸 云盘
-
-- FM Cloud
-
-
-
-
-### 🐸 IM
-
-- Cede IM
-
-
-
-
-### 🐸 壁纸
-
-- warpar
-
-
-### 🐸 英雄联盟助手
-
-- Serendlplty
-
-
-### 🐸 更多
-
-- [更多案例](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 @@
-
-
-
-
-
+
+
+
RSA 密钥配置
+
仅在需要手动调整密钥时使用。
+
-
-
-
-
+
+
+
+
-
- 设备激活码
-
-
-
-
-
- 备注
-
-
-
-
+
+
-
-
-
- 生成激活码
-
+
+
+
+ 模拟式模块
+ 用于模拟式相关功能授权
+
+
+
-
- 复制激活码
-
-
-
-
+
+
+ 数字式模块
+ 用于数字式相关功能授权
+
+
+
+
+
+
+ 比对式模块
+ 用于比对式相关功能授权
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
- 设备激活
-
-
+
+
+
+
+
+ 设备激活
+
+
+
+
+
+ 备份数据
+
+
+
+
+
+ 导入备份
+
+
+
+
+
+ 清空数据
+
+
+
-
+
{{ m === 'simulate' ? '模拟式模块' : m === 'digital' ? '数字式模块' : '比对式模块' }}
-
-
-
-
- 复制激活码
-
+
+
+
+
+
+ 复制激活码
+
+
+
+
+
+
+ 删除
+
+
+
-
+
设备激活
@@ -79,12 +140,11 @@
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",