调整界面

调整脚本
增加功能:备份、恢复、清空
This commit is contained in:
2026-04-03 14:05:18 +08:00
parent 4d0ce274e0
commit 51d607d970
15 changed files with 1494 additions and 679 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(dir:*)"
],
"deny": [],
"ask": []
}
}

404
README.md
View File

@@ -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
<div align=center> ## 项目简介
<h3>🎉🎉🎉 ElectronEgg V4 已发布! 🎉🎉🎉</h3>
</div>
<br>
<div align=center> 这是一个基于 Electron 的桌面端小工具,用于离线生成和管理设备激活码。
<img src="./public/images/example/logo.png" width="150" height="150" />
</div>
<div align=center> 项目定位:
<h3><strong>一个入门简单、跨平台、企业级桌面软件开发框架</strong></h3>
</div>
<br>
<!-- ## 🌏 [English](https://www.yuque.com/u34495/ee-doc) | [中文](https://www.kaka996.com/) --> - 面向 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. 🚀 常规桌面软件 ### 2. 激活记录管理
- 🚖 windows平台
![](./public/images/example/ee-win-home.png) 激活记录页面支持:
- 🚍 macOS平台 - `macAddress` 查询
![](./public/images/example/ee-mac-home.png) - 按模块组合查询
- 查看历史激活记录
- 复制历史记录中的激活码
- 将新生成的激活记录保存到本地数据库
- 🚔 linux平台 - 国产UOS、Deepin ### 3. 本地数据库存储
![](./public/images/example/uos-home.png)
- 🚔 linux平台 - ubuntu 项目不依赖后端,激活记录直接写入本地 SQLite。
![](./public/images/example/ubuntu-db.png)
### 🚐 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/) 1. 用户在前端输入设备申请码。
2. 前端使用 RSA 私钥解密申请码,解析出设备信息。
## 项目案例 3. 用户选择需要激活的模块。
- 🐟 框架已经应用于医疗、学校、政务、股票交易、ERP、娱乐、视频、企业等领域客户端 4. 前端组装激活码明文后,再用 RSA 公钥加密生成激活码。
5. 用户点击保存后,前端通过 IPC 调用主进程。
6. 主进程将记录写入本地 SQLite 数据库。
7. 列表页面通过 IPC 查询本地数据库并展示历史记录。
### 🐸 远控 ## 核心代码说明
- RQ Center ### Electron 主进程
![](./public/images/example/rq-1.png)
![](./public/images/example/rq-2.png)
### 🐸 云盘 - `electron/main.ts`
- 应用启动入口
- 注册生命周期与 preload
- FM Cloud - `electron/preload/index.ts`
![](./public/images/example/fm-p2.png) - 启动时初始化托盘
![](./public/images/example/fm-p1.png) - 初始化安全服务
![](./public/images/example/fm-p4.png) - 初始化自动更新服务
- 初始化本地数据库
### 🐸 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 - `electron/service/database/activateRecord.ts`
![](./public/images/example/aw-3.png) - `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`
## 💬 交流 - `frontend/src/layouts/AppSider.vue`
1. [讨论](https://www.kaka996.com/pages/c2720e/) - 左侧菜单布局
- 当前只包含“设备激活”菜单
## 📌 关于pr - `frontend/src/views/activate/index.vue`
请前往[GitHub项目](https://github.com/dromara/electron-egg)提pr避免代码同步后pr被覆盖掉感谢 - 激活记录查询页面
- 激活记录展示、查询、复制、保存入口
地址https://github.com/dromara/electron-egg - `frontend/src/views/activate/ActiveForm.vue`
- 激活码生成表单
- 处理申请码解析、模块选择、激活码生成
## 📔 框架核心包 ee-core - `frontend/src/utils/rsa.ts`
ee-core[https://github.com/wallace5303/ee-core](https://github.com/wallace5303/ee-core) - RSA 加解密封装
## 📚 Dromara 成员项目 - `frontend/src/utils/ipcRenderer.ts`
- 渲染进程 IPC 调用封装
<p align="center"> ## 路由与界面
<a href="https://gitee.com/dromara/TLog" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/tlog2.png" title="一个轻量级的分布式日志标记追踪神器10分钟即可接入自动对日志打标签完成微服务的链路追踪" width="15%"> 当前前端只有一条实际业务路由:
</a>
<a href="https://gitee.com/dromara/liteFlow" target="_blank"> - `/activate` 设备激活页面
<img src="https://oss.dev33.cn/sa-token/link/liteflow.png" title="轻量,快速,稳定,可编排的组件式流程引擎" width="15%">
</a> 左侧菜单当前也只有一个入口:
<a href="https://hutool.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/hutool.jpg" title="小而全的Java工具类库使Java拥有函数式语言般的优雅让Java语言也可以“甜甜的”。" width="15%"> - 设备激活
</a>
<a href="https://sa-token.dev33.cn/" target="_blank"> 因此,整个系统目前可以理解为“单页面桌面工具”,所有主要功能都集中在这一个界面中完成。
<img src="https://oss.dev33.cn/sa-token/link/sa-token.png" title="一个轻量级 java 权限认证框架,让鉴权变得简单、优雅!" width="15%">
</a> 当前页面的详细说明已单独整理到:
<a href="https://gitee.com/dromara/hmily" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/hmily.png" title="高性能一站式分布式事务解决方案。" width="15%"> - `docs/current-page.md`
</a>
<a href="https://gitee.com/dromara/Raincat" target="_blank"> ## 配置说明
<img src="https://oss.dev33.cn/sa-token/link/raincat.png" title="强一致性分布式事务解决方案。" width="15%">
</a> ### 根目录脚本
</p>
<p align="center"> 常用命令:
<a href="https://gitee.com/dromara/myth" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/myth.png" title="可靠消息分布式事务解决方案。" width="15%"> - `npm run dev` 启动 Electron + 前端开发环境
</a> - `npm run dev-frontend` 仅启动前端
<a href="https://cubic.jiagoujishu.com/" target="_blank"> - `npm run dev-electron` 仅启动 Electron
<img src="https://oss.dev33.cn/sa-token/link/cubic.png" title="一站式问题定位平台以agent的方式无侵入接入应用完整集成arthas功能模块致力于应用级监控帮助开发人员快速定位问题" width="15%"> - `npm run build` 构建前端、Electron 并执行加密
</a> - `npm run build-w` 构建 Windows 安装包
<a href="https://maxkey.top/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/maxkey.png" title="业界领先的身份管理和认证产品" width="15%"> ### 前端环境变量
</a>
<a href="http://forest.dtflyx.com/" target="_blank"> 位于:
<img src="https://oss.dev33.cn/sa-token/link/forest-logo.png" title="Forest能够帮助您使用更简单的方式编写Java的HTTP客户端" width="15%">
</a> - `frontend/.env.development`
<a href="https://jpom.io/" target="_blank"> - `frontend/.env.production`
<img src="https://oss.dev33.cn/sa-token/link/jpom.png" title="一款简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件" width="15%">
</a> 当前已使用的变量主要有:
<a href="https://su.usthe.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/sureness.png" title="面向 REST API 的高性能认证鉴权框架" width="15%"> - `VITE_TITLE`
</a> - `VITE_RSA_PUBLIC_KEY`
</p> - `VITE_RSA_PRIVATE_KEY`
<p align="center"> - `VITE_RSA_CAN_EDIT`
<a href="https://easy-es.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/easy-es2.png" title="傻瓜级ElasticSearch搜索引擎ORM框架" width="15%"> ## 数据存储说明
</a>
<a href="https://gitee.com/dromara/northstar" target="_blank"> 数据库由主进程在应用启动时初始化。
<img src="https://oss.dev33.cn/sa-token/link/northstar_logo.png" title="Northstar盈富量化交易平台" width="15%">
</a> 数据库初始化入口:
<a href="https://hertzbeat.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/hertzbeat_brand.jpg" title="易用友好的云监控系统" width="15%"> - `electron/preload/index.ts`
</a>
<a href="https://plugins.sheng90.wang/fast-request/" target="_blank"> 数据库基础路径由 `ee-core` 提供的数据目录决定,实际数据库文件保存在应用数据目录下的 `db` 子目录中。
<img src="https://oss.dev33.cn/sa-token/link/fast-request.gif" title="Idea 版 Postman为简化调试API而生" width="15%">
</a> ## 当前项目特征总结
<a href="https://www.jeesuite.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/mendmix.png" title="开源分布式云原生架构一站式解决方案" width="15%"> 从当前代码看,项目有以下特点:
</a>
<a href="https://gitee.com/dromara/koalas-rpc" target="_blank"> - 业务功能集中,当前只有激活工具这一个主要模块
<img src="https://oss.dev33.cn/sa-token/link/koalas-rpc2.png" title="企业生产级百亿日PV高可用可拓展的RPC框架。" width="15%"> - 没有后端依赖,所有数据均在本地处理和存储
</a> - 前端负责激活码的生成逻辑
</p> - 主进程负责数据库初始化与数据持久化
<p align="center"> - 项目仍保留部分 `electron-egg` 模板能力和示例代码
<a href="https://async.sizegang.cn/" target="_blank"> - 现有目录中存在一些当前业务未直接使用的模板模块,可在后续按需要继续裁剪
<img src="https://oss.dev33.cn/sa-token/link/gobrs-async.png" title="配置极简功能强大的异步任务动态编排框架" width="15%">
</a> ## 适合后续演进的方向
<a href="https://dynamictp.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/dynamic-tp.png" title="基于配置中心的轻量级动态可监控线程池" width="15%"> 结合当前项目现状,后续工作更适合围绕以下方向展开:
</a>
<a href="https://www.x-easypdf.cn" target="_blank"> - 梳理并精简模板残留代码
<img src="https://oss.dev33.cn/sa-token/link/x-easypdf.png" title="一个用搭积木的方式构建pdf的框架基于pdfbox" width="15%"> - 完善激活记录管理能力
</a> - 增加导入、导出、删除、编辑等本地数据操作
<a href="http://dromara.gitee.io/image-combiner" target="_blank"> - 统一页面文案、编码和可维护性
<img src="https://oss.dev33.cn/sa-token/link/image-combiner.png" title="一个专门用于图片合成的工具,没有很复杂的功能,简单实用,却不失强大" width="15%"> - 补充更明确的项目文档与交付说明
</a>
<a href="https://www.herodotus.cn/" target="_blank"> ## 运行说明
<img src="https://oss.dev33.cn/sa-token/link/dante-cloud2.png" title="Dante-Cloud 是一款企业级微服务架构和服务能力开发平台。" width="15%">
</a> ### 开发环境
<a href="https://dromara.org/zh/projects/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/dromara.png" title="让每一位开源爱好者,体会到开源的快乐。" width="15%"> ```bash
</a> npm install
</p> npm run dev
```
### 构建
```bash
npm run build
```
Windows 安装包构建:
```bash
npm run build-w
```
## 补充说明
- 原仓库中的 `README.zh-CN.md` 更偏向上游模板说明
- 当前 `README.md` 旨在描述本项目自身,而不是 `electron-egg` 模板本身
- 如果后续继续迭代业务,建议优先维护本文件,保证项目文档与实际实现一致

View File

@@ -1,221 +0,0 @@
[![star](https://gitee.com/dromara/electron-egg/badge/star.svg?theme=gvp)](https://gitee.com/dromara/electron-egg/stargazers)
<div align=center>
<h3>🎉🎉🎉 ElectronEgg V4 已发布! 🎉🎉🎉</h3>
</div>
<br>
<div align=center>
<img src="./public/images/example/logo.png" width="150" height="150" />
</div>
<div align=center>
<h3><strong>一个入门简单、跨平台、企业级桌面软件开发框架</strong></h3>
</div>
<br>
<!-- ## 🌏 [English](https://www.yuque.com/u34495/ee-doc) | [中文](https://www.kaka996.com/) -->
## 📋 介绍
> 框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、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 成员项目
<p align="center">
<a href="https://gitee.com/dromara/TLog" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/tlog2.png" title="一个轻量级的分布式日志标记追踪神器10分钟即可接入自动对日志打标签完成微服务的链路追踪" width="15%">
</a>
<a href="https://gitee.com/dromara/liteFlow" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/liteflow.png" title="轻量,快速,稳定,可编排的组件式流程引擎" width="15%">
</a>
<a href="https://hutool.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/hutool.jpg" title="小而全的Java工具类库使Java拥有函数式语言般的优雅让Java语言也可以“甜甜的”。" width="15%">
</a>
<a href="https://sa-token.dev33.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/sa-token.png" title="一个轻量级 java 权限认证框架,让鉴权变得简单、优雅!" width="15%">
</a>
<a href="https://gitee.com/dromara/hmily" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/hmily.png" title="高性能一站式分布式事务解决方案。" width="15%">
</a>
<a href="https://gitee.com/dromara/Raincat" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/raincat.png" title="强一致性分布式事务解决方案。" width="15%">
</a>
</p>
<p align="center">
<a href="https://gitee.com/dromara/myth" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/myth.png" title="可靠消息分布式事务解决方案。" width="15%">
</a>
<a href="https://cubic.jiagoujishu.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/cubic.png" title="一站式问题定位平台以agent的方式无侵入接入应用完整集成arthas功能模块致力于应用级监控帮助开发人员快速定位问题" width="15%">
</a>
<a href="https://maxkey.top/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/maxkey.png" title="业界领先的身份管理和认证产品" width="15%">
</a>
<a href="http://forest.dtflyx.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/forest-logo.png" title="Forest能够帮助您使用更简单的方式编写Java的HTTP客户端" width="15%">
</a>
<a href="https://jpom.io/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/jpom.png" title="一款简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件" width="15%">
</a>
<a href="https://su.usthe.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/sureness.png" title="面向 REST API 的高性能认证鉴权框架" width="15%">
</a>
</p>
<p align="center">
<a href="https://easy-es.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/easy-es2.png" title="傻瓜级ElasticSearch搜索引擎ORM框架" width="15%">
</a>
<a href="https://gitee.com/dromara/northstar" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/northstar_logo.png" title="Northstar盈富量化交易平台" width="15%">
</a>
<a href="https://hertzbeat.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/hertzbeat_brand.jpg" title="易用友好的云监控系统" width="15%">
</a>
<a href="https://plugins.sheng90.wang/fast-request/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/fast-request.gif" title="Idea 版 Postman为简化调试API而生" width="15%">
</a>
<a href="https://www.jeesuite.com/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/mendmix.png" title="开源分布式云原生架构一站式解决方案" width="15%">
</a>
<a href="https://gitee.com/dromara/koalas-rpc" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/koalas-rpc2.png" title="企业生产级百亿日PV高可用可拓展的RPC框架。" width="15%">
</a>
</p>
<p align="center">
<a href="https://async.sizegang.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/gobrs-async.png" title="配置极简功能强大的异步任务动态编排框架" width="15%">
</a>
<a href="https://dynamictp.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/dynamic-tp.png" title="基于配置中心的轻量级动态可监控线程池" width="15%">
</a>
<a href="https://www.x-easypdf.cn" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/x-easypdf.png" title="一个用搭积木的方式构建pdf的框架基于pdfbox" width="15%">
</a>
<a href="http://dromara.gitee.io/image-combiner" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/image-combiner.png" title="一个专门用于图片合成的工具,没有很复杂的功能,简单实用,却不失强大" width="15%">
</a>
<a href="https://www.herodotus.cn/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/dante-cloud2.png" title="Dante-Cloud 是一款企业级微服务架构和服务能力开发平台。" width="15%">
</a>
<a href="https://dromara.org/zh/projects/" target="_blank">
<img src="https://oss.dev33.cn/sa-token/link/dromara.png" title="让每一位开源爱好者,体会到开源的快乐。" width="15%">
</a>
</p>

129
build-portable.bat Normal file
View File

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

View File

@@ -47,6 +47,11 @@ module.exports = {
win_e: { win_e: {
args: ['--config=./cmd/builder.json', '-w=portable', '--x64'], args: ['--config=./cmd/builder.json', '-w=portable', '--x64'],
}, },
win_dir: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder.json', '-w=dir', '--x64'],
},
win_7z: { win_7z: {
args: ['--config=./cmd/builder.json', '-w=7z', '--x64'], args: ['--config=./cmd/builder.json', '-w=7z', '--x64'],
}, },
@@ -196,4 +201,4 @@ module.exports = {
stdio: "inherit", // ignore stdio: "inherit", // ignore
}, },
}, },
}; };

226
docs/current-page.md Normal file
View File

@@ -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. 如有需要,可手动备份当前数据,或在清空后通过备份文件恢复。
## 页面价值总结
当前页面虽然只有一个,但已经覆盖完整核心业务流程:
- 录入申请方
- 输入申请码
- 选择激活模块
- 生成激活码
- 保存激活记录
- 查询历史记录
- 复制历史激活码
- 删除历史记录
- 备份本地数据
- 导入备份恢复数据
因此,这个页面本质上是一个“激活工具 + 本地记录管理工具”的组合界面。

View File

@@ -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 { class ActivateRecordController {
async list(args: { macAddress: string, modules: string[] }): Promise<any> { async list(args: { macAddress: string, modules: string[] }): Promise<any> {
const {macAddress, modules} = args const { macAddress, modules } = args;
return activateRecordService.list(macAddress, modules) return activateRecordService.list(macAddress, modules);
} }
async save(args: { async save(args: {
applicant: string,
macAddress: string, macAddress: string,
applicationCode: string, applicationCode: string,
modules: string[], modules: string[],
@@ -15,8 +18,76 @@ class ActivateRecordController {
createTime: string, createTime: string,
remark: string remark: string
}): Promise<any> { }): Promise<any> {
const { modules} = args const { modules } = args;
return activateRecordService.save({...args, module : modules.join(',')}) return activateRecordService.save({ ...args, module: modules.join(',') });
}
async removeById(args: { id: number }): Promise<any> {
const { id } = args;
return activateRecordService.removeById(id);
}
async clear(): Promise<any> {
return activateRecordService.clear();
}
async backup(): Promise<any> {
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<any> {
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}`;
} }
} }

View File

@@ -1,6 +1,30 @@
import fs from 'fs';
import path from 'path';
import { ElectronEgg } from 'ee-core'; import { ElectronEgg } from 'ee-core';
import { Lifecycle } from './preload/lifecycle'; import { Lifecycle } from './preload/lifecycle';
import { preload } from './preload'; 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 // New app
const app = new ElectronEgg(); const app = new ElectronEgg();
@@ -16,4 +40,5 @@ app.register("before-close", life.beforeClose);
app.register("preload", preload); app.register("preload", preload);
// Run // Run
app.run(); app.run();
writeRuntimeLog('app bootstrap end');

View File

@@ -2,6 +2,19 @@ import { app as electronApp, screen } from 'electron';
import { logger } from 'ee-core/log'; import { logger } from 'ee-core/log';
import { getConfig } from 'ee-core/config'; import { getConfig } from 'ee-core/config';
import { getMainWindow } from 'ee-core/electron'; 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 { class Lifecycle {
/** /**
@@ -36,6 +49,16 @@ class Lifecycle {
const win = getMainWindow(); 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 // 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, // 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 // and calculate the coordinates of the upper left corner when the window is centered
@@ -67,4 +90,4 @@ class Lifecycle {
} }
Lifecycle.toString = () => '[class Lifecycle]'; Lifecycle.toString = () => '[class Lifecycle]';
export { Lifecycle }; export { Lifecycle };

View File

@@ -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数据存储 * sqlite data storage
* @class
*/ */
class ActivateRecordService extends BasedbService { class ActivateRecordService extends BasedbService {
tableName: string; tableName: string;
@@ -10,75 +20,89 @@ class ActivateRecordService extends BasedbService {
constructor() { constructor() {
const options = { const options = {
dbname: 'pqs9100-tool.db', dbname: 'pqs9100-tool.db',
} };
super(options); super(options);
this.tableName = 'activate_record'; this.tableName = 'activate_record';
} }
/* /**
* 初始化表 * Initialize table and perform lightweight schema migration.
*/ */
init(): void { init(): void {
this._init(); this._init();
// 检查表是否存在
const masterStmt = this.db.prepare('SELECT * FROM sqlite_master WHERE type=? AND name = ?'); 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) { if (!tableExists) {
// 创建表 const createTableSql =
const create_table_sql =
`CREATE TABLE ${this.tableName} `CREATE TABLE ${this.tableName}
( (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
applicant CHAR(100) NULL,
macAddress CHAR(50) NOT NULL, macAddress CHAR(50) NOT NULL,
applicationCode CHAR(2000) NOT NULL, applicationCode CHAR(2000) NOT NULL,
module CHAR(200) NOT NULL, module CHAR(200) NOT NULL,
activationCode CHAR(2000) NOT NULL, activationCode CHAR(2000) NOT NULL,
createTime CHAR(32) NOT NULL, createTime CHAR(32) NOT NULL,
remark CHAR(120) NULL remark CHAR(120) NULL
);` );`;
this.db.exec(create_table_sql); 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: { async save(data: ActivateRecordItem) {
macAddress: string; const insert = this.db.prepare(
applicationCode: string; `INSERT INTO ${this.tableName} (applicant, macAddress, applicationCode, module, activationCode, createTime, remark)
module: string; VALUES (@applicant, @macAddress, @applicationCode, @module, @activationCode, @createTime, @remark)`
activationCode: string; );
createTime: string; insert.run({
remark: string; ...data,
}) { applicant: data.applicant || '',
const insert = this.db.prepare(`INSERT INTO ${this.tableName} (macAddress, applicationCode, module, activationCode, createTime, remark) remark: data.remark || ''
VALUES (@macAddress, @applicationCode, @module, @activationCode, @createTime, @remark)`); });
insert.run(data);
return true; return true;
} }
/* /**
* 删 data * Delete one record by id.
*/ */
async removeById(name: string = ''): Promise<boolean> { async removeById(id: number): Promise<boolean> {
const remove = this.db.prepare(`DELETE const remove = this.db.prepare(`DELETE
FROM ${this.tableName} FROM ${this.tableName}
WHERE id = ?`); WHERE id = ?`);
remove.run(name); remove.run(id);
return true; return true;
} }
/* /**
* 查list data (sqlite) * Clear all records.
*/
async clear(): Promise<boolean> {
const clearStmt = this.db.prepare(`DELETE FROM ${this.tableName}`);
clearStmt.run();
return true;
}
/**
* Query record list.
*/ */
async list(macAddress: string = '', modules: string[] = []): Promise<any[]> { async list(macAddress: string = '', modules: string[] = []): Promise<any[]> {
let condition = '' let condition = '';
if (macAddress) { if (macAddress) {
condition += ` AND macAddress = '${macAddress}'` condition += ` AND macAddress = '${macAddress}'`;
} }
if (modules.length > 0) { 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})`; condition += ` AND (${moduleConditions})`;
} }
const select = this.db.prepare(`SELECT * const select = this.db.prepare(`SELECT *
@@ -88,35 +112,88 @@ class ActivateRecordService extends BasedbService {
return select.all(); return select.all();
} }
/* /**
* all Test data (sqlite) * Export all records to a JSON backup file.
*/
async backup(filePath: string): Promise<number> {
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<number> {
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<any[]> { async getAllTestDataSqlite(): Promise<any[]> {
const selectAllUser = this.db.prepare(`SELECT * const selectAllUser = this.db.prepare(`SELECT *
FROM ${this.tableName} `); FROM ${this.tableName}
const allUser = selectAllUser.all(); ORDER BY id DESC`);
return allUser; return selectAllUser.all();
} }
/* /**
* get data dir (sqlite) * Get data directory.
*/ */
async getDataDir(): Promise<string> { async getDataDir(): Promise<string> {
const dir = this.storage.getDbDir(); return this.storage.getDbDir();
return dir;
} }
/* /**
* set custom data dir (sqlite) * Set custom data directory.
*/ */
async setCustomDataDir(dir: string): Promise<void> { async setCustomDataDir(dir: string): Promise<void> {
if (dir.length == 0) { if (dir.length === 0) {
return; return;
} }
this.changeDataDir(dir); this.changeDataDir(dir);
this.init(); 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 : '',
};
} }
} }

View File

@@ -15,6 +15,7 @@
"jsencrypt": "^3.5.4", "jsencrypt": "^3.5.4",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"pqs9100_tool": "file:..",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"store2": "^2.14.4", "store2": "^2.14.4",
"vue": "^3.5.22", "vue": "^3.5.22",

View File

@@ -7,6 +7,10 @@ const ipcApiRoute = {
activateRecord: { activateRecord: {
list: 'controller/activateRecord/list', list: 'controller/activateRecord/list',
save: 'controller/activateRecord/save', save: 'controller/activateRecord/save',
removeById: 'controller/activateRecord/removeById',
clear: 'controller/activateRecord/clear',
backup: 'controller/activateRecord/backup',
importBackup: 'controller/activateRecord/importBackup',
}, },
framework: { framework: {
checkForUpdater: 'controller/framework/checkForUpdater', checkForUpdater: 'controller/framework/checkForUpdater',

View File

@@ -1,144 +1,179 @@
<template> <template>
<div class="activation-page"> <div class="activation-page">
<a-card v-if="hiddenKeys" style="margin-bottom: 10px;" title="RSA密钥配置"> <section v-if="hiddenKeys" class="activation-section">
<a-row :gutter="16"> <div class="section-title">
<a-col :span="24"> <h3>RSA 密钥配置</h3>
<a-alert <p>仅在需要手动调整密钥时使用</p>
message="注意:请妥善保管私钥,不要泄露给他人" </div>
show-icon
style="margin-bottom: 16px;"
type="warning"
/>
</a-col>
<a-col :span="12"> <a-alert
<a-form-item label="RSA公钥"> message="请妥善保管私钥,不要泄露给他人。"
<a-textarea show-icon
v-model:value="rsaKeys.publicKey" type="warning"
:readonly="readonly" class="security-alert"
:rows="5" />
placeholder="RSA公钥内容"
@blur="readonly = true" <div class="form-grid">
@focus="readonly = false" <div class="field-block">
/> <label>RSA 公钥</label>
<a-button <a-textarea
:disabled="!rsaKeys.publicKey" v-model:value="rsaKeys.publicKey"
ghost :readonly="readonly"
size="small" :rows="5"
style="margin-top: 8px;" placeholder="请输入 RSA 公钥"
type="primary" @blur="readonly = true"
@click="copyToClipboard(rsaKeys.publicKey)" @focus="readonly = false"
> />
<div class="field-actions field-actions--single">
<a-button :disabled="!rsaKeys.publicKey" @click="copyToClipboard(rsaKeys.publicKey)">
复制公钥 复制公钥
</a-button> </a-button>
</a-form-item> </div>
</a-col> </div>
<a-col :span="12"> <div class="field-block">
<a-form-item label="RSA私钥"> <label>RSA 私钥</label>
<a-textarea <a-textarea
v-model:value="rsaKeys.privateKey" v-model:value="rsaKeys.privateKey"
:readonly="readonly" :readonly="readonly"
:rows="5" :rows="5"
placeholder="RSA私钥内容" placeholder="请输入 RSA 私钥"
@blur="readonly = true" @blur="readonly = true"
@focus="readonly = false" @focus="readonly = false"
/> />
<a-button <div class="field-actions field-actions--single">
:disabled="!rsaKeys.privateKey" <a-button :disabled="!rsaKeys.privateKey" @click="copyToClipboard(rsaKeys.privateKey)">
ghost
size="small"
style="margin-top: 8px;"
type="primary"
@click="copyToClipboard(rsaKeys.privateKey)"
>
复制私钥 复制私钥
</a-button> </a-button>
</a-form-item> </div>
</a-col> </div>
</a-row> </div>
</a-card> </section>
<a-row :gutter="16">
<a-col :span="24">
<a-divider orientation="left" orientation-margin="0px">激活模块</a-divider>
<a-form :label-col="labelCol" layout="inline">
<a-form-item label="模拟式模块">
<a-switch v-model:checked="activationForm.simulate.permanently"/>
</a-form-item>
<a-form-item label="数字式模块">
<a-switch v-model:checked="activationForm.digital.permanently"/>
</a-form-item>
<a-form-item label="比对式模块">
<a-switch v-model:checked="activationForm.contrast.permanently"/>
</a-form-item>
</a-form>
</a-col>
<a-col :span="24">
<a-divider orientation="left" orientation-margin="0px">设备申请码</a-divider>
<a-form-item>
<a-textarea
v-model:value="activationForm.applicationCode"
:rows="3"
allow-clear
placeholder="请输入设备申请码"
/>
</a-form-item>
</a-col>
<a-col :span="24"> <section class="activation-section">
<a-divider orientation="left" orientation-margin="0px">设备激活码</a-divider> <div class="section-title">
<a-form-item> <h3>激活模块</h3>
<a-textarea <p>至少选择一个模块</p>
v-model:value="activationCode" </div>
:rows="3"
placeholder="生成的激活码将显示在这里"
readonly
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-divider orientation="left" orientation-margin="0px">备注</a-divider>
<a-form-item>
<a-textarea
v-model:value="activationForm.remark"
allow-clear
placeholder="添加备注内容"
maxlength="120"
show-count
/>
</a-form-item>
</a-col>
<a-col :span="24"> <div class="module-grid">
<a-space> <div class="module-item" :class="{ active: activationForm.simulate.permanently }">
<a-button <div class="module-copy">
:disabled="!activationForm.applicationCode.trim()" <strong>模拟式模块</strong>
:loading="generating" <span>用于模拟式相关功能授权</span>
type="primary" </div>
@click="generateActivationCode" <a-switch v-model:checked="activationForm.simulate.permanently" />
> </div>
生成激活码
</a-button>
<a-button <div class="module-item" :class="{ active: activationForm.digital.permanently }">
:disabled="!activationCode" <div class="module-copy">
@click="copyToClipboard(activationCode)" <strong>数字式模块</strong>
> <span>用于数字式相关功能授权</span>
复制激活码 </div>
</a-button> <a-switch v-model:checked="activationForm.digital.permanently" />
</a-space> </div>
</a-col>
</a-row> <div class="module-item" :class="{ active: activationForm.contrast.permanently }">
<div class="module-copy">
<strong>比对式模块</strong>
<span>用于比对式相关功能授权</span>
</div>
<a-switch v-model:checked="activationForm.contrast.permanently" />
</div>
</div>
</section>
<section class="activation-section">
<div class="section-title">
<h3>激活信息</h3>
</div>
<div class="form-rows">
<div class="form-row form-row--compact">
<div class="field-block">
<label>申请方</label>
<p class="field-tip">用于标记本次激活记录归属</p>
<a-input
v-model:value="activationForm.applicant"
allow-clear
placeholder="请输入申请方"
/>
</div>
<div class="field-block">
<label>备注</label>
<p class="field-tip">记录额外说明例如客户或交付备注</p>
<a-textarea
v-model:value="activationForm.remark"
allow-clear
placeholder="添加备注内容"
maxlength="120"
show-count
:rows="4"
/>
</div>
</div>
<div class="form-row">
<div class="field-block field-block--code">
<label>设备申请码</label>
<p class="field-tip">粘贴设备侧生成的申请码系统会先解密校验</p>
<a-textarea
v-model:value="activationForm.applicationCode"
:rows="8"
allow-clear
class="code-textarea"
placeholder="请输入设备申请码"
/>
</div>
<div class="field-block field-block--code">
<div class="field-head">
<div>
<label>设备激活码</label>
<p class="field-tip">生成后可直接复制</p>
</div>
<div class="field-actions">
<a-button
:disabled="!activationForm.applicationCode.trim()"
:loading="generating"
type="primary"
@click="generateActivationCode"
>
生成激活码
</a-button>
<a-button
:disabled="!activationCode"
@click="copyToClipboard(activationCode)"
>
复制激活码
</a-button>
</div>
</div>
<a-textarea
v-model:value="activationCode"
:rows="8"
class="code-textarea"
placeholder="生成的激活码将显示在这里"
readonly
/>
</div>
</div>
</div>
</section>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {ref} from 'vue' import { ref, watch } from 'vue'
import {message} from 'ant-design-vue' import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import rsa from '@/utils/rsa' import rsa from '@/utils/rsa'
import {Activate} from "@/views/activate/index"; import { Activate } from '@/views/activate/index'
import dayjs from "dayjs";
const RSA_CAN_EDIT = import.meta.env.VITE_RSA_CAN_EDIT === 'true' const RSA_CAN_EDIT = import.meta.env.VITE_RSA_CAN_EDIT === 'true'
const rsaKeys = ref({ const rsaKeys = ref({
publicKey: rsa.publicKey, publicKey: rsa.publicKey,
privateKey: rsa.privateKey privateKey: rsa.privateKey
@@ -146,93 +181,92 @@ const rsaKeys = ref({
const hiddenKeys = ref(RSA_CAN_EDIT) const hiddenKeys = ref(RSA_CAN_EDIT)
const readonly = ref(true) const readonly = ref(true)
const activationForm = ref({ const activationForm = ref({
applicant: '',
applicationCode: '', applicationCode: '',
simulate: {permanently: false}, simulate: { permanently: false },
digital: {permanently: false}, digital: { permanently: false },
contrast: {permanently: false}, contrast: { permanently: false },
remark:'', remark: ''
}) })
const activationCode = ref('') const activationCode = ref('')
const macAddress = ref('') const macAddress = ref('')
const generating = ref(false) const generating = ref(false)
const labelCol = {style: {width: '120px'}}
// 生成激活码 const clearGeneratedResult = () => {
activationCode.value = ''
macAddress.value = ''
}
watch(
() => [
activationForm.value.applicationCode,
activationForm.value.simulate.permanently,
activationForm.value.digital.permanently,
activationForm.value.contrast.permanently
],
clearGeneratedResult
)
const generateActivationCode = () => { const generateActivationCode = () => {
if (!rsaKeys.value.publicKey || !rsaKeys.value.privateKey) { if (!rsaKeys.value.publicKey || !rsaKeys.value.privateKey) {
message.error('请先配置RSA密钥') message.error('请先配置 RSA 密钥')
return return
} }
if (!activationForm.value.applicationCode.trim()) { if (!activationForm.value.applicationCode.trim()) {
message.error('请输入设备申请码') message.error('请输入设备申请码')
return return
} }
let simulate = activationForm.value.simulate as Activate.ActivateModule
let digital = activationForm.value.digital as Activate.ActivateModule const simulate = activationForm.value.simulate as Activate.ActivateModule
let contrast = activationForm.value.contrast as Activate.ActivateModule const digital = activationForm.value.digital as Activate.ActivateModule
const contrast = activationForm.value.contrast as Activate.ActivateModule
if (!simulate.permanently && !digital.permanently && !contrast.permanently) { if (!simulate.permanently && !digital.permanently && !contrast.permanently) {
message.error('请至少选择一种激活模块') message.error('请至少选择一种激活模块')
return return
} }
generating.value = true generating.value = true
let applicationCodePlaintext let applicationCodePlaintext
try { try {
const applicationCode = rsa.decrypt(activationForm.value.applicationCode) const applicationCode = rsa.decrypt(activationForm.value.applicationCode)
applicationCodePlaintext = JSON.parse(applicationCode) applicationCodePlaintext = JSON.parse(applicationCode)
} catch (e) { } catch (error) {
console.error(e) console.error(error)
} }
if (!applicationCodePlaintext) {
if (!applicationCodePlaintext?.macAddress) {
generating.value = false generating.value = false
message.error('无效的设备申请码') message.error('无效的设备申请码')
return return
} }
if (!applicationCodePlaintext.macAddress) {
message.error('无效的设备申请码') const activationCodePlaintext: Activate.ActivationCodePlaintext = {
return macAddress: applicationCodePlaintext.macAddress,
simulate: { permanently: simulate.permanently ? 1 : 0 },
digital: { permanently: digital.permanently ? 1 : 0 },
contrast: { permanently: contrast.permanently ? 1 : 0 }
} }
let activationCodePlaintext: Activate.ActivationCodePlaintext = {
macAddress: '',
simulate: {permanently: 0},
digital: {permanently: 0},
contrast: {permanently: 0}
}
activationCodePlaintext.macAddress = applicationCodePlaintext.macAddress
macAddress.value = applicationCodePlaintext.macAddress macAddress.value = applicationCodePlaintext.macAddress
if (simulate.permanently) {
activationCodePlaintext.simulate = {
permanently: 1
}
}
if (digital.permanently) {
activationCodePlaintext.digital = {
permanently: 1
}
}
if (contrast.permanently) {
activationCodePlaintext.contrast = {
permanently: 1
}
}
const data = JSON.stringify(activationCodePlaintext)
// message.info(data)
try { try {
setTimeout(() => { setTimeout(() => {
activationCode.value = rsa.encrypt(data) activationCode.value = rsa.encrypt(JSON.stringify(activationCodePlaintext))
generating.value = false generating.value = false
}, 1000) }, 1000)
} catch (error) {
} catch (e) { console.error(error)
console.error(e)
generating.value = false generating.value = false
message.error('生成激活码失败') message.error('生成激活码失败')
} }
} }
// 复制到剪贴板
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
if (!text) { if (!text) {
message.warning('没有内容可复制') message.warning('没有可复制的内容')
return return
} }
@@ -247,10 +281,12 @@ const getData = () => {
if (!activationCode.value) { if (!activationCode.value) {
return return
} }
let simulate = activationForm.value.simulate as Activate.ActivateModule
let digital = activationForm.value.digital as Activate.ActivateModule const simulate = activationForm.value.simulate as Activate.ActivateModule
let contrast = activationForm.value.contrast as Activate.ActivateModule const digital = activationForm.value.digital as Activate.ActivateModule
let modules = [] const contrast = activationForm.value.contrast as Activate.ActivateModule
const modules: string[] = []
if (simulate.permanently) { if (simulate.permanently) {
modules.push('simulate') modules.push('simulate')
} }
@@ -260,40 +296,230 @@ const getData = () => {
if (contrast.permanently) { if (contrast.permanently) {
modules.push('contrast') modules.push('contrast')
} }
return { return {
applicant: activationForm.value.applicant.trim(),
macAddress: macAddress.value, macAddress: macAddress.value,
applicationCode: activationForm.value.applicationCode, applicationCode: activationForm.value.applicationCode,
modules: modules, modules,
activationCode: activationCode.value, activationCode: activationCode.value,
createTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'), createTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
remark: activationForm.value.remark remark: activationForm.value.remark
} }
} }
defineExpose( {getData})
defineExpose({ getData })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.activation-page { .activation-page {
text-align: center; display: flex;
height: 100%; flex-direction: column;
background-color: #fff; gap: 12px;
border-radius: 8px; }
:deep(textarea) { .activation-section {
font-family: consolas, monospace; background: #ffffff;
resize: none; border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 16px;
}
.section-title {
margin-bottom: 12px;
}
.section-title h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #1f2329;
}
.section-title p {
margin: 4px 0 0;
font-size: 12px;
color: #8c8c8c;
}
.security-alert {
margin-bottom: 12px;
}
.module-grid,
.form-grid,
.form-row {
display: grid;
gap: 12px;
}
.module-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.form-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
.form-rows {
display: flex;
flex-direction: column;
gap: 12px;
}
.form-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
.form-row--compact .field-block {
min-height: 0;
}
.module-item,
.field-block {
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.module-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 14px 16px;
}
.module-item.active {
border-color: #91caff;
background: #f7fbff;
}
.module-copy {
display: flex;
flex-direction: column;
gap: 4px;
}
.module-copy strong {
font-size: 14px;
font-weight: 600;
color: #1f2329;
}
.module-copy span {
font-size: 12px;
color: #8c8c8c;
}
.field-block {
align-self: start;
padding: 14px;
}
.field-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
}
.field-block label {
display: block;
margin-bottom: 6px;
font-size: 14px;
font-weight: 600;
color: #1f2329;
}
.field-tip {
margin: 0 0 8px;
font-size: 12px;
line-height: 1.5;
color: #8c8c8c;
}
.field-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.field-actions--single {
margin-top: 10px;
}
.field-block--code :deep(.ant-input-textarea textarea) {
min-height: 180px;
font-family: "Consolas", "Courier New", monospace;
}
:deep(.ant-input),
:deep(.ant-input-affix-wrapper),
:deep(.ant-input-textarea textarea) {
border-radius: 10px;
border-color: #d9d9d9;
box-shadow: none;
}
:deep(input.ant-input),
:deep(.ant-input-affix-wrapper) {
min-height: 40px;
}
:deep(.ant-input-affix-wrapper) {
display: flex;
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
:deep(.ant-input-affix-wrapper input.ant-input) {
min-height: auto;
padding: 0;
border: 0;
box-shadow: none;
background: transparent;
}
:deep(.ant-input:hover),
:deep(.ant-input:focus),
:deep(.ant-input-affix-wrapper:hover),
:deep(.ant-input-affix-wrapper-focused),
:deep(.ant-input-textarea textarea:hover),
:deep(.ant-input-textarea textarea:focus) {
border-color: #4096ff;
box-shadow: none;
}
:deep(.ant-input-textarea textarea) {
resize: none;
}
:deep(.ant-btn) {
height: 36px;
border-radius: 10px;
}
@media (max-width: 640px) {
.module-grid,
.form-row {
grid-template-columns: 1fr;
} }
:deep(.ant-form-item-label) { .field-head {
font-weight: bold; flex-direction: column;
} }
:deep(.ant-card-body) { .field-actions {
padding: 18px; width: 100%;
} }
:deep(.ant-card) { .field-actions :deep(.ant-btn) {
border-radius: 8px; flex: 1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
} }
</style> </style>

View File

@@ -3,12 +3,23 @@
<a-card style="margin-bottom: 5px" title="激活记录"> <a-card style="margin-bottom: 5px" title="激活记录">
<a-form ref="searchFormRef" :model="searchForm" layout="inline"> <a-form ref="searchFormRef" :model="searchForm" layout="inline">
<a-form-item label="mac地址" name="macAddress"> <a-form-item label="mac地址" name="macAddress">
<a-input v-model:value="searchForm.macAddress" allow-clear autocomplete="off" placeholder="请输入mac地址" <a-input
style="width: 200px"/> v-model:value="searchForm.macAddress"
allow-clear
autocomplete="off"
placeholder="请输入mac地址"
style="width: 200px"
/>
</a-form-item> </a-form-item>
<a-form-item label="激活模块" name="modules"> <a-form-item label="激活模块" name="modules">
<a-select v-model:value="searchForm.modules" :max-tag-count="1" allow-clear mode="multiple" <a-select
placeholder="全部" style="width: 200px"> v-model:value="searchForm.modules"
:max-tag-count="1"
allow-clear
mode="multiple"
placeholder="全部"
style="width: 200px"
>
<a-select-option value="simulate">模拟式模块</a-select-option> <a-select-option value="simulate">模拟式模块</a-select-option>
<a-select-option value="digital">数字式模块</a-select-option> <a-select-option value="digital">数字式模块</a-select-option>
<a-select-option value="contrast">比对式模块</a-select-option> <a-select-option value="contrast">比对式模块</a-select-option>
@@ -33,37 +44,87 @@
</a-form> </a-form>
</a-card> </a-card>
<a-card> <a-card>
<a-button style="margin-bottom: 10px;" type="primary" @click="visible = true"> <a-space style="margin-bottom: 10px;" wrap>
<template #icon> <a-button type="primary" @click="visible = true">
<file-protect-outlined/> <template #icon>
</template> <file-protect-outlined/>
设备激活 </template>
</a-button> 设备激活
<a-table :columns="columns" :dataSource="dataSource" :loading="loading" :pagination="pagination" bordered </a-button>
size="small"> <a-button @click="backupData">
<template #icon>
<download-outlined/>
</template>
备份数据
</a-button>
<a-button @click="confirmImportBackup">
<template #icon>
<upload-outlined/>
</template>
导入备份
</a-button>
<a-button danger @click="confirmClearData">
<template #icon>
<delete-outlined/>
</template>
清空数据
</a-button>
</a-space>
<a-table
:columns="columns"
:dataSource="dataSource"
:loading="loading"
:pagination="pagination"
:rowKey="(record: any) => record.id"
bordered
size="small"
>
<template #emptyText> <template #emptyText>
<a-empty description="暂无记录"/> <a-empty description="暂无记录"/>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'module'"> <template v-if="column.key === 'module'">
<a-tag color="green" v-for="m in record.module.split(',')"> <a-tag color="green" v-for="m in record.module.split(',')" :key="m">
{{ m === 'simulate' ? '模拟式模块' : m === 'digital' ? '数字式模块' : '比对式模块' }} {{ m === 'simulate' ? '模拟式模块' : m === 'digital' ? '数字式模块' : '比对式模块' }}
</a-tag> </a-tag>
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-button type="primary" size="small" @click="copyToClipboard(record.activationCode)"> <a-space>
<template #icon> <a-button type="primary" size="small" @click="copyToClipboard(record.activationCode)">
<copy-outlined /> <template #icon>
</template> <copy-outlined />
复制激活码 </template>
</a-button> 复制激活码
</a-button>
<a-popconfirm
title="确认删除这条记录吗?"
ok-text="删除"
cancel-text="取消"
@confirm="removeRecord(record.id)"
>
<a-button danger size="small">
<template #icon>
<delete-outlined />
</template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template> </template>
</template> </template>
</a-table> </a-table>
</a-card> </a-card>
</div> </div>
<a-modal v-model:visible="visible" :keyboard="false" :mask-closable="false" centered destroy-on-close width="70%"> <a-modal
v-model:visible="visible"
:keyboard="false"
:mask-closable="false"
centered
destroy-on-close
width="940px"
wrapClassName="apple-activation-modal"
>
<template #title> <template #title>
<file-protect-outlined/> <file-protect-outlined/>
设备激活 设备激活
@@ -79,12 +140,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {FormInstance, message} from "ant-design-vue"; import { FormInstance, Modal, message } from "ant-design-vue";
import {onMounted, ref} from "vue"; import { onMounted, ref } from "vue";
import ActiveForm from "@/views/activate/ActiveForm.vue"; import ActiveForm from "@/views/activate/ActiveForm.vue";
import {ipc} from "@/utils/ipcRenderer"; import { ipc } from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api"; import { ipcApiRoute } from "@/api";
const searchFormRef = ref<FormInstance>(); const searchFormRef = ref<FormInstance>();
const activateFormRef = ref(); const activateFormRef = ref();
@@ -94,15 +154,20 @@ const searchForm = ref({
}) })
const loading = ref(false) const loading = ref(false)
const visible = ref(false) const visible = ref(false)
const dataSource = ref<any []>([]) const dataSource = ref<any[]>([])
const pagination = ref({ const pagination = ref({
total: 0, total: 0,
pageSize: 10, pageSize: 10,
// current: 1,
showTotal: (total: number) => `${total}` showTotal: (total: number) => `${total}`
}) })
const columns = [ const columns = [
{
title: '申请方',
dataIndex: 'applicant',
key: 'applicant',
align: 'center',
width: 120,
},
{ {
title: 'mac地址', title: 'mac地址',
dataIndex: 'macAddress', dataIndex: 'macAddress',
@@ -116,14 +181,14 @@ const columns = [
key: 'applicationCode', key: 'applicationCode',
align: 'center', align: 'center',
ellipsis: true, ellipsis: true,
width: 100, width: 120,
}, },
{ {
title: '激活模块', title: '激活模块',
dataIndex: 'module', dataIndex: 'module',
key: 'module', key: 'module',
align: 'center', align: 'center',
width: 250, width: 220,
}, },
{ {
title: '激活码', title: '激活码',
@@ -131,7 +196,7 @@ const columns = [
key: 'activationCode', key: 'activationCode',
align: 'center', align: 'center',
ellipsis: true, ellipsis: true,
width: 100, width: 120,
}, },
{ {
title: '生成时间', title: '生成时间',
@@ -145,13 +210,13 @@ const columns = [
dataIndex: 'remark', dataIndex: 'remark',
key: 'remark', key: 'remark',
align: 'center', align: 'center',
width: 100, width: 120,
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center', align: 'center',
width: 120, width: 220,
}, },
] ]
@@ -163,6 +228,7 @@ const resetForm = () => {
onMounted(() => { onMounted(() => {
resetForm() resetForm()
}) })
const query = () => { const query = () => {
loading.value = true loading.value = true
const params: any = { const params: any = {
@@ -171,7 +237,6 @@ const query = () => {
} }
setTimeout(() => { setTimeout(() => {
ipc.invoke(ipcApiRoute.activateRecord.list, params).then((res: any[]) => { ipc.invoke(ipcApiRoute.activateRecord.list, params).then((res: any[]) => {
console.log(res)
dataSource.value = res dataSource.value = res
pagination.value.total = dataSource.value.length pagination.value.total = dataSource.value.length
pagination.value.current = 1 pagination.value.current = 1
@@ -180,8 +245,9 @@ const query = () => {
console.error(err) console.error(err)
loading.value = false loading.value = false
}) })
}, 500) }, 300)
} }
const save = () => { const save = () => {
const data = activateFormRef.value.getData() const data = activateFormRef.value.getData()
if (!data) { if (!data) {
@@ -196,9 +262,79 @@ const save = () => {
console.error(err) console.error(err)
message.error('保存失败') message.error('保存失败')
}) })
} }
// 复制到剪贴板
const removeRecord = (id: number) => {
ipc.invoke(ipcApiRoute.activateRecord.removeById, { id }).then(() => {
message.success('删除成功')
query()
}).catch((err: any) => {
console.error(err)
message.error('删除失败')
})
}
const confirmClearData = () => {
Modal.confirm({
title: '确认清空全部数据吗?',
content: '清空后当前本地激活记录将被删除,请先确认是否需要备份。',
okText: '清空',
cancelText: '取消',
okButtonProps: {
danger: true
},
onOk: () => clearData()
})
}
const clearData = () => {
ipc.invoke(ipcApiRoute.activateRecord.clear).then(() => {
message.success('已清空本地数据')
query()
}).catch((err: any) => {
console.error(err)
message.error('清空失败')
})
}
const backupData = () => {
ipc.invoke(ipcApiRoute.activateRecord.backup).then((res: any) => {
if (res?.canceled) {
return
}
message.success(`备份成功,共导出 ${res.count} 条记录`)
}).catch((err: any) => {
console.error(err)
message.error('备份失败')
})
}
const confirmImportBackup = () => {
Modal.confirm({
title: '确认导入备份吗?',
content: '导入备份会先清空当前本地数据,再恢复备份内容。',
okText: '导入',
cancelText: '取消',
okButtonProps: {
danger: true
},
onOk: () => importBackup()
})
}
const importBackup = () => {
ipc.invoke(ipcApiRoute.activateRecord.importBackup).then((res: any) => {
if (res?.canceled) {
return
}
message.success(`导入成功,共恢复 ${res.count} 条记录`)
query()
}).catch((err: any) => {
console.error(err)
message.error('导入失败,请检查备份文件格式')
})
}
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
if (!text) { if (!text) {
message.warning('没有内容可复制') message.warning('没有内容可复制')
@@ -213,5 +349,52 @@ const copyToClipboard = (text: string) => {
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
:deep(.apple-activation-modal .ant-modal-content) {
border-radius: 30px;
overflow: hidden;
border: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 28px 60px rgba(15, 23, 42, 0.22);
background:
linear-gradient(180deg, rgba(250, 250, 252, 0.98), rgba(243, 245, 247, 0.98));
}
:deep(.apple-activation-modal .ant-modal-header) {
padding: 18px 24px 14px;
background: transparent;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
}
:deep(.apple-activation-modal .ant-modal-title) {
font-size: 18px;
font-weight: 650;
color: #111827;
}
:deep(.apple-activation-modal .ant-modal-body) {
padding: 12px 14px 8px;
background: transparent;
max-height: 76vh;
overflow-y: auto;
}
:deep(.apple-activation-modal .ant-modal-footer) {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 20px 18px;
border-top: 1px solid rgba(15, 23, 42, 0.06);
background: transparent;
}
:deep(.apple-activation-modal .ant-btn) {
min-width: 88px;
height: 38px;
border-radius: 12px;
}
:deep(.apple-activation-modal .ant-btn-primary) {
border: none;
background: linear-gradient(180deg, #0a84ff, #0071e3);
box-shadow: 0 10px 24px rgba(0, 113, 227, 0.22);
}
</style> </style>

View File

@@ -16,6 +16,7 @@
"re-sqlite": "electron-rebuild -f -w better-sqlite3", "re-sqlite": "electron-rebuild -f -w better-sqlite3",
"build-w": "ee-bin build --cmds=win64", "build-w": "ee-bin build --cmds=win64",
"build-we": "ee-bin build --cmds=win_e", "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": "ee-bin build --cmds=mac",
"build-m-arm64": "ee-bin build --cmds=mac_arm64", "build-m-arm64": "ee-bin build --cmds=mac_arm64",
"build-l": "ee-bin build --cmds=linux", "build-l": "ee-bin build --cmds=linux",