调整界面

调整脚本
增加功能:备份、恢复、清空
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>
<img src="./public/images/example/logo.png" width="150" height="150" />
</div>
这是一个基于 Electron 的桌面端小工具,用于离线生成和管理设备激活码。
<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. 🚀 常规桌面软件
- 🚖 windows平台
### 2. 激活记录管理
![](./public/images/example/ee-win-home.png)
激活记录页面支持:
- 🚍 macOS平台
![](./public/images/example/ee-mac-home.png)
- `macAddress` 查询
- 按模块组合查询
- 查看历史激活记录
- 复制历史记录中的激活码
- 将新生成的激活记录保存到本地数据库
- 🚔 linux平台 - 国产UOS、Deepin
![](./public/images/example/uos-home.png)
### 3. 本地数据库存储
- 🚔 linux平台 - ubuntu
![](./public/images/example/ubuntu-db.png)
项目不依赖后端,激活记录直接写入本地 SQLite。
### 🚐 2. vue、react、angular、web 转换成桌面软件
- 🚙 vue-ant-design本地
当前数据库文件名:
![](./public/images/example/vue-antd.png)
- `pqs9100-tool.db`
- 🚙 禅道项目管理web项目地址
当前表:
![](./public/images/example/ee-project-7.png)
- `activate_record`
### 🚂 3. 游戏h5相关技术开发
- 🚊 忍者100层
主要字段:
![](./public/images/example/ee_game_1.png)
- `id`
- `macAddress`
- `applicationCode`
- `module`
- `activationCode`
- `createTime`
- `remark`
## 业务流程
## 📒 开始使用
整体流程如下:
- ✒️ [安装文档](https://www.kaka996.com/pages/e64ff6/)
## 项目案例
- 🐟 框架已经应用于医疗、学校、政务、股票交易、ERP、娱乐、视频、企业等领域客户端
1. 用户在前端输入设备申请码。
2. 前端使用 RSA 私钥解密申请码,解析出设备信息。
3. 用户选择需要激活的模块。
4. 前端组装激活码明文后,再用 RSA 公钥加密生成激活码。
5. 用户点击保存后,前端通过 IPC 调用主进程。
6. 主进程将记录写入本地 SQLite 数据库。
7. 列表页面通过 IPC 查询本地数据库并展示历史记录。
### 🐸 远控
## 核心代码说明
- RQ Center
![](./public/images/example/rq-1.png)
![](./public/images/example/rq-2.png)
### Electron 主进程
### 🐸 云盘
- `electron/main.ts`
- 应用启动入口
- 注册生命周期与 preload
- FM Cloud
![](./public/images/example/fm-p2.png)
![](./public/images/example/fm-p1.png)
![](./public/images/example/fm-p4.png)
- `electron/preload/index.ts`
- 启动时初始化托盘
- 初始化安全服务
- 初始化自动更新服务
- 初始化本地数据库
### 🐸 IM
- `electron/preload/lifecycle.ts`
- 控制应用 ready、窗口 ready、关闭前等生命周期行为
- 启动后自动设置主窗口大小和位置
- Cede IM
![](./public/images/example/im-p1.png)
![](./public/images/example/im-p5.png)
![](./public/images/example/im-p1.png)
### 数据库相关
### 🐸 壁纸
- `electron/service/database/basedb.ts`
- SQLite 基础封装
- 负责数据库文件路径和连接初始化
- warpar
![](./public/images/example/aw-3.png)
- `electron/service/database/activateRecord.ts`
- `activate_record` 表初始化
- 保存激活记录
- 查询激活记录
- 删除记录等数据库操作
### 🐸 英雄联盟助手
- `electron/controller/activateRecord.ts`
- 暴露激活记录相关 IPC 接口
- Serendlplty
![](./public/images/example/lol-zhanji.png)
### 前端相关
### 🐸 更多
- `frontend/src/main.ts`
- Vue 应用入口
- 注册 Ant Design Vue 和全局组件
- [更多案例](https://www.kaka996.com/pages/eadf46/)
- `frontend/src/router/routerMap.ts`
- 当前仅注册一个业务页面:`/activate`
## 💬 交流
1. [讨论](https://www.kaka996.com/pages/c2720e/)
- `frontend/src/layouts/AppSider.vue`
- 左侧菜单布局
- 当前只包含“设备激活”菜单
## 📌 关于pr
请前往[GitHub项目](https://github.com/dromara/electron-egg)提pr避免代码同步后pr被覆盖掉感谢
- `frontend/src/views/activate/index.vue`
- 激活记录查询页面
- 激活记录展示、查询、复制、保存入口
地址https://github.com/dromara/electron-egg
- `frontend/src/views/activate/ActiveForm.vue`
- 激活码生成表单
- 处理申请码解析、模块选择、激活码生成
## 📔 框架核心包 ee-core
ee-core[https://github.com/wallace5303/ee-core](https://github.com/wallace5303/ee-core)
- `frontend/src/utils/rsa.ts`
- RSA 加解密封装
## 📚 Dromara 成员项目
- `frontend/src/utils/ipcRenderer.ts`
- 渲染进程 IPC 调用封装
<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>
## 路由与界面
当前前端只有一条实际业务路由:
- `/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` 模板本身
- 如果后续继续迭代业务,建议优先维护本文件,保证项目文档与实际实现一致

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: {
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
},
},
};
};

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 {
async list(args: { macAddress: string, modules: string[] }): Promise<any> {
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<any> {
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<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 { 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();
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 { 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 };
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数据存储
* @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<boolean> {
async removeById(id: number): Promise<boolean> {
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<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[]> {
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<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[]> {
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<string> {
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<void> {
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 : '',
};
}
}

View File

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

View File

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

View File

@@ -1,144 +1,179 @@
<template>
<div class="activation-page">
<a-card v-if="hiddenKeys" style="margin-bottom: 10px;" title="RSA密钥配置">
<a-row :gutter="16">
<a-col :span="24">
<a-alert
message="注意:请妥善保管私钥,不要泄露给他人"
show-icon
style="margin-bottom: 16px;"
type="warning"
/>
</a-col>
<section v-if="hiddenKeys" class="activation-section">
<div class="section-title">
<h3>RSA 密钥配置</h3>
<p>仅在需要手动调整密钥时使用</p>
</div>
<a-col :span="12">
<a-form-item label="RSA公钥">
<a-textarea
v-model:value="rsaKeys.publicKey"
:readonly="readonly"
:rows="5"
placeholder="RSA公钥内容"
@blur="readonly = true"
@focus="readonly = false"
/>
<a-button
:disabled="!rsaKeys.publicKey"
ghost
size="small"
style="margin-top: 8px;"
type="primary"
@click="copyToClipboard(rsaKeys.publicKey)"
>
<a-alert
message="请妥善保管私钥,不要泄露给他人。"
show-icon
type="warning"
class="security-alert"
/>
<div class="form-grid">
<div class="field-block">
<label>RSA 公钥</label>
<a-textarea
v-model:value="rsaKeys.publicKey"
:readonly="readonly"
:rows="5"
placeholder="请输入 RSA 公钥"
@blur="readonly = true"
@focus="readonly = false"
/>
<div class="field-actions field-actions--single">
<a-button :disabled="!rsaKeys.publicKey" @click="copyToClipboard(rsaKeys.publicKey)">
复制公钥
</a-button>
</a-form-item>
</a-col>
</div>
</div>
<a-col :span="12">
<a-form-item label="RSA私钥">
<a-textarea
v-model:value="rsaKeys.privateKey"
:readonly="readonly"
:rows="5"
placeholder="RSA私钥内容"
@blur="readonly = true"
@focus="readonly = false"
/>
<a-button
:disabled="!rsaKeys.privateKey"
ghost
size="small"
style="margin-top: 8px;"
type="primary"
@click="copyToClipboard(rsaKeys.privateKey)"
>
<div class="field-block">
<label>RSA 私钥</label>
<a-textarea
v-model:value="rsaKeys.privateKey"
:readonly="readonly"
:rows="5"
placeholder="请输入 RSA 私钥"
@blur="readonly = true"
@focus="readonly = false"
/>
<div class="field-actions field-actions--single">
<a-button :disabled="!rsaKeys.privateKey" @click="copyToClipboard(rsaKeys.privateKey)">
复制私钥
</a-button>
</a-form-item>
</a-col>
</a-row>
</a-card>
<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>
</div>
</div>
</div>
</section>
<a-col :span="24">
<a-divider orientation="left" orientation-margin="0px">设备激活码</a-divider>
<a-form-item>
<a-textarea
v-model:value="activationCode"
: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>
<section class="activation-section">
<div class="section-title">
<h3>激活模块</h3>
<p>至少选择一个模块</p>
</div>
<a-col :span="24">
<a-space>
<a-button
:disabled="!activationForm.applicationCode.trim()"
:loading="generating"
type="primary"
@click="generateActivationCode"
>
生成激活码
</a-button>
<div class="module-grid">
<div class="module-item" :class="{ active: activationForm.simulate.permanently }">
<div class="module-copy">
<strong>模拟式模块</strong>
<span>用于模拟式相关功能授权</span>
</div>
<a-switch v-model:checked="activationForm.simulate.permanently" />
</div>
<a-button
:disabled="!activationCode"
@click="copyToClipboard(activationCode)"
>
复制激活码
</a-button>
</a-space>
</a-col>
</a-row>
<div class="module-item" :class="{ active: activationForm.digital.permanently }">
<div class="module-copy">
<strong>数字式模块</strong>
<span>用于数字式相关功能授权</span>
</div>
<a-switch v-model:checked="activationForm.digital.permanently" />
</div>
<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>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {message} from 'ant-design-vue'
import { ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import rsa from '@/utils/rsa'
import {Activate} from "@/views/activate/index";
import dayjs from "dayjs";
import { Activate } from '@/views/activate/index'
const RSA_CAN_EDIT = import.meta.env.VITE_RSA_CAN_EDIT === 'true'
const rsaKeys = ref({
publicKey: rsa.publicKey,
privateKey: rsa.privateKey
@@ -146,93 +181,92 @@ const rsaKeys = ref({
const hiddenKeys = ref(RSA_CAN_EDIT)
const readonly = ref(true)
const activationForm = ref({
applicant: '',
applicationCode: '',
simulate: {permanently: false},
digital: {permanently: false},
contrast: {permanently: false},
remark:'',
simulate: { permanently: false },
digital: { permanently: false },
contrast: { permanently: false },
remark: ''
})
const activationCode = ref('')
const macAddress = ref('')
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 = () => {
if (!rsaKeys.value.publicKey || !rsaKeys.value.privateKey) {
message.error('请先配置RSA密钥')
message.error('请先配置 RSA 密钥')
return
}
if (!activationForm.value.applicationCode.trim()) {
message.error('请输入设备申请码')
return
}
let simulate = activationForm.value.simulate as Activate.ActivateModule
let digital = activationForm.value.digital as Activate.ActivateModule
let contrast = activationForm.value.contrast as Activate.ActivateModule
const simulate = activationForm.value.simulate 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) {
message.error('请至少选择一种激活模块')
return
}
generating.value = true
let applicationCodePlaintext
try {
const applicationCode = rsa.decrypt(activationForm.value.applicationCode)
applicationCodePlaintext = JSON.parse(applicationCode)
} catch (e) {
console.error(e)
} catch (error) {
console.error(error)
}
if (!applicationCodePlaintext) {
if (!applicationCodePlaintext?.macAddress) {
generating.value = false
message.error('无效的设备申请码')
return
}
if (!applicationCodePlaintext.macAddress) {
message.error('无效的设备申请码')
return
const activationCodePlaintext: Activate.ActivationCodePlaintext = {
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
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 {
setTimeout(() => {
activationCode.value = rsa.encrypt(data)
activationCode.value = rsa.encrypt(JSON.stringify(activationCodePlaintext))
generating.value = false
}, 1000)
} catch (e) {
console.error(e)
} catch (error) {
console.error(error)
generating.value = false
message.error('生成激活码失败')
}
}
// 复制到剪贴板
const copyToClipboard = (text: string) => {
if (!text) {
message.warning('没有内容可复制')
message.warning('没有可复制的内容')
return
}
@@ -247,10 +281,12 @@ const getData = () => {
if (!activationCode.value) {
return
}
let simulate = activationForm.value.simulate as Activate.ActivateModule
let digital = activationForm.value.digital as Activate.ActivateModule
let contrast = activationForm.value.contrast as Activate.ActivateModule
let modules = []
const simulate = activationForm.value.simulate as Activate.ActivateModule
const digital = activationForm.value.digital as Activate.ActivateModule
const contrast = activationForm.value.contrast as Activate.ActivateModule
const modules: string[] = []
if (simulate.permanently) {
modules.push('simulate')
}
@@ -260,40 +296,230 @@ const getData = () => {
if (contrast.permanently) {
modules.push('contrast')
}
return {
applicant: activationForm.value.applicant.trim(),
macAddress: macAddress.value,
applicationCode: activationForm.value.applicationCode,
modules: modules,
modules,
activationCode: activationCode.value,
createTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
remark: activationForm.value.remark
remark: activationForm.value.remark
}
}
defineExpose( {getData})
defineExpose({ getData })
</script>
<style lang="less" scoped>
.activation-page {
text-align: center;
height: 100%;
background-color: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
:deep(textarea) {
font-family: consolas, monospace;
resize: none;
.activation-section {
background: #ffffff;
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) {
font-weight: bold;
.field-head {
flex-direction: column;
}
:deep(.ant-card-body) {
padding: 18px;
.field-actions {
width: 100%;
}
:deep(.ant-card) {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.field-actions :deep(.ant-btn) {
flex: 1;
}
}
</style>

View File

@@ -3,12 +3,23 @@
<a-card style="margin-bottom: 5px" title="激活记录">
<a-form ref="searchFormRef" :model="searchForm" layout="inline">
<a-form-item label="mac地址" name="macAddress">
<a-input v-model:value="searchForm.macAddress" allow-clear autocomplete="off" placeholder="请输入mac地址"
style="width: 200px"/>
<a-input
v-model:value="searchForm.macAddress"
allow-clear
autocomplete="off"
placeholder="请输入mac地址"
style="width: 200px"
/>
</a-form-item>
<a-form-item label="激活模块" name="modules">
<a-select v-model:value="searchForm.modules" :max-tag-count="1" allow-clear mode="multiple"
placeholder="全部" style="width: 200px">
<a-select
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="digital">数字式模块</a-select-option>
<a-select-option value="contrast">比对式模块</a-select-option>
@@ -33,37 +44,87 @@
</a-form>
</a-card>
<a-card>
<a-button style="margin-bottom: 10px;" type="primary" @click="visible = true">
<template #icon>
<file-protect-outlined/>
</template>
设备激活
</a-button>
<a-table :columns="columns" :dataSource="dataSource" :loading="loading" :pagination="pagination" bordered
size="small">
<a-space style="margin-bottom: 10px;" wrap>
<a-button type="primary" @click="visible = true">
<template #icon>
<file-protect-outlined/>
</template>
设备激活
</a-button>
<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>
<a-empty description="暂无记录"/>
</template>
<template #bodyCell="{ column, record }">
<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' ? '数字式模块' : '比对式模块' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-button type="primary" size="small" @click="copyToClipboard(record.activationCode)">
<template #icon>
<copy-outlined />
</template>
复制激活码
</a-button>
<a-space>
<a-button type="primary" size="small" @click="copyToClipboard(record.activationCode)">
<template #icon>
<copy-outlined />
</template>
复制激活码
</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>
</a-table>
</a-card>
</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>
<file-protect-outlined/>
设备激活
@@ -79,12 +140,11 @@
</template>
<script lang="ts" setup>
import {FormInstance, message} from "ant-design-vue";
import {onMounted, ref} from "vue";
import { FormInstance, Modal, message } from "ant-design-vue";
import { onMounted, ref } from "vue";
import ActiveForm from "@/views/activate/ActiveForm.vue";
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { ipc } from "@/utils/ipcRenderer";
import { ipcApiRoute } from "@/api";
const searchFormRef = ref<FormInstance>();
const activateFormRef = ref();
@@ -94,15 +154,20 @@ const searchForm = ref({
})
const loading = ref(false)
const visible = ref(false)
const dataSource = ref<any []>([])
const dataSource = ref<any[]>([])
const pagination = ref({
total: 0,
pageSize: 10,
// current: 1,
showTotal: (total: number) => `${total}`
})
const columns = [
{
title: '申请方',
dataIndex: 'applicant',
key: 'applicant',
align: 'center',
width: 120,
},
{
title: 'mac地址',
dataIndex: 'macAddress',
@@ -116,14 +181,14 @@ const columns = [
key: 'applicationCode',
align: 'center',
ellipsis: true,
width: 100,
width: 120,
},
{
title: '激活模块',
dataIndex: 'module',
key: 'module',
align: 'center',
width: 250,
width: 220,
},
{
title: '激活码',
@@ -131,7 +196,7 @@ const columns = [
key: 'activationCode',
align: 'center',
ellipsis: true,
width: 100,
width: 120,
},
{
title: '生成时间',
@@ -145,13 +210,13 @@ const columns = [
dataIndex: 'remark',
key: 'remark',
align: 'center',
width: 100,
width: 120,
},
{
title: '操作',
key: 'action',
align: 'center',
width: 120,
width: 220,
},
]
@@ -163,6 +228,7 @@ const resetForm = () => {
onMounted(() => {
resetForm()
})
const query = () => {
loading.value = true
const params: any = {
@@ -171,7 +237,6 @@ const query = () => {
}
setTimeout(() => {
ipc.invoke(ipcApiRoute.activateRecord.list, params).then((res: any[]) => {
console.log(res)
dataSource.value = res
pagination.value.total = dataSource.value.length
pagination.value.current = 1
@@ -180,8 +245,9 @@ const query = () => {
console.error(err)
loading.value = false
})
}, 500)
}, 300)
}
const save = () => {
const data = activateFormRef.value.getData()
if (!data) {
@@ -196,9 +262,79 @@ const save = () => {
console.error(err)
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) => {
if (!text) {
message.warning('没有内容可复制')
@@ -213,5 +349,52 @@ const copyToClipboard = (text: string) => {
}
</script>
<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>

View File

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