调整界面
调整脚本 增加功能:备份、恢复、清空
This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dir:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
402
README.md
402
README.md
@@ -1,221 +1,277 @@
|
||||
[](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+**
|
||||
- 🏆 码云最有价值开源项目
|
||||

|
||||
前端页面提供以下能力:
|
||||
|
||||
## 📚 文档
|
||||
- 快速体验:[教程文档](https://www.kaka996.com/)
|
||||

|
||||
- 录入设备申请码
|
||||
- 选择需要激活的模块
|
||||
- 生成设备激活码
|
||||
- 复制激活码
|
||||
- 填写备注
|
||||
|
||||
## 📦 特性
|
||||
1. 🍄 跨平台:一套代码,可以打包成windows版、Mac版、Linux版、国产UOS、Deepin、麒麟等
|
||||
2. 🌹 架构:单业务进程/模块化/多任务(进程,线程,渲染进程),让开发大型项目变的简单。
|
||||
3. 🌱 简单高效:只需学习 js 语言
|
||||
4. 🌴 前端独立:理论上支持任何前端技术,如:vue、react、html等等
|
||||
5. 🍁 工程化:可以用前端、服务端的开发思维,来编写桌面软件
|
||||
6. 🌷 高性能:事件驱动、非阻塞式IO
|
||||
7. 🌰 功能丰富:配置、通信、插件、数据库、升级、打包、工具... 应有尽有
|
||||
8. 💐 安全:支持字节码加密、压缩混淆加密
|
||||
9. 🌻 功能demo:桌面软件常见功能,框架集成或提供demo
|
||||
当前支持的模块有:
|
||||
|
||||
## ✈️ 使用场景
|
||||
- `simulate`
|
||||
- `digital`
|
||||
- `contrast`
|
||||
|
||||
### 1. 🚀 常规桌面软件
|
||||
- 🚖 windows平台
|
||||
### 2. 激活记录管理
|
||||
|
||||

|
||||
激活记录页面支持:
|
||||
|
||||
- 🚍 macOS平台
|
||||

|
||||
- 按 `macAddress` 查询
|
||||
- 按模块组合查询
|
||||
- 查看历史激活记录
|
||||
- 复制历史记录中的激活码
|
||||
- 将新生成的激活记录保存到本地数据库
|
||||
|
||||
- 🚔 linux平台 - 国产UOS、Deepin
|
||||

|
||||
### 3. 本地数据库存储
|
||||
|
||||
- 🚔 linux平台 - ubuntu
|
||||

|
||||
项目不依赖后端,激活记录直接写入本地 SQLite。
|
||||
|
||||
### 🚐 2. vue、react、angular、web 转换成桌面软件
|
||||
- 🚙 vue-ant-design(本地)
|
||||
当前数据库文件名:
|
||||
|
||||

|
||||
- `pqs9100-tool.db`
|
||||
|
||||
- 🚙 禅道项目管理(web项目地址)
|
||||
当前表:
|
||||
|
||||

|
||||
- `activate_record`
|
||||
|
||||
### 🚂 3. 游戏(h5相关技术开发)
|
||||
- 🚊 忍者100层
|
||||
主要字段:
|
||||
|
||||

|
||||
- `id`
|
||||
- `macAddress`
|
||||
- `applicationCode`
|
||||
- `module`
|
||||
- `activationCode`
|
||||
- `createTime`
|
||||
- `remark`
|
||||
|
||||
## 业务流程
|
||||
|
||||
## 📒 开始使用
|
||||
整体流程如下:
|
||||
|
||||
- ✒️ [安装文档](https://www.kaka996.com/pages/e64ff6/)
|
||||
1. 用户在前端输入设备申请码。
|
||||
2. 前端使用 RSA 私钥解密申请码,解析出设备信息。
|
||||
3. 用户选择需要激活的模块。
|
||||
4. 前端组装激活码明文后,再用 RSA 公钥加密生成激活码。
|
||||
5. 用户点击保存后,前端通过 IPC 调用主进程。
|
||||
6. 主进程将记录写入本地 SQLite 数据库。
|
||||
7. 列表页面通过 IPC 查询本地数据库并展示历史记录。
|
||||
|
||||
## 项目案例
|
||||
- 🐟 框架已经应用于医疗、学校、政务、股票交易、ERP、娱乐、视频、企业等领域客户端
|
||||
## 核心代码说明
|
||||
|
||||
### 🐸 远控
|
||||
### Electron 主进程
|
||||
|
||||
- RQ Center
|
||||

|
||||

|
||||
- `electron/main.ts`
|
||||
- 应用启动入口
|
||||
- 注册生命周期与 preload
|
||||
|
||||
### 🐸 云盘
|
||||
- `electron/preload/index.ts`
|
||||
- 启动时初始化托盘
|
||||
- 初始化安全服务
|
||||
- 初始化自动更新服务
|
||||
- 初始化本地数据库
|
||||
|
||||
- FM Cloud
|
||||

|
||||

|
||||

|
||||
- `electron/preload/lifecycle.ts`
|
||||
- 控制应用 ready、窗口 ready、关闭前等生命周期行为
|
||||
- 启动后自动设置主窗口大小和位置
|
||||
|
||||
### 🐸 IM
|
||||
### 数据库相关
|
||||
|
||||
- Cede IM
|
||||

|
||||

|
||||

|
||||
- `electron/service/database/basedb.ts`
|
||||
- SQLite 基础封装
|
||||
- 负责数据库文件路径和连接初始化
|
||||
|
||||
### 🐸 壁纸
|
||||
- `electron/service/database/activateRecord.ts`
|
||||
- `activate_record` 表初始化
|
||||
- 保存激活记录
|
||||
- 查询激活记录
|
||||
- 删除记录等数据库操作
|
||||
|
||||
- warpar
|
||||

|
||||
- `electron/controller/activateRecord.ts`
|
||||
- 暴露激活记录相关 IPC 接口
|
||||
|
||||
### 🐸 英雄联盟助手
|
||||
### 前端相关
|
||||
|
||||
- Serendlplty
|
||||

|
||||
- `frontend/src/main.ts`
|
||||
- Vue 应用入口
|
||||
- 注册 Ant Design Vue 和全局组件
|
||||
|
||||
### 🐸 更多
|
||||
- `frontend/src/router/routerMap.ts`
|
||||
- 当前仅注册一个业务页面:`/activate`
|
||||
|
||||
- [更多案例](https://www.kaka996.com/pages/eadf46/)
|
||||
- `frontend/src/layouts/AppSider.vue`
|
||||
- 左侧菜单布局
|
||||
- 当前只包含“设备激活”菜单
|
||||
|
||||
## 💬 交流
|
||||
1. [讨论](https://www.kaka996.com/pages/c2720e/)
|
||||
- `frontend/src/views/activate/index.vue`
|
||||
- 激活记录查询页面
|
||||
- 激活记录展示、查询、复制、保存入口
|
||||
|
||||
## 📌 关于pr
|
||||
请前往[GitHub项目](https://github.com/dromara/electron-egg)提pr(避免代码同步后,pr被覆盖掉),感谢!
|
||||
- `frontend/src/views/activate/ActiveForm.vue`
|
||||
- 激活码生成表单
|
||||
- 处理申请码解析、模块选择、激活码生成
|
||||
|
||||
地址:https://github.com/dromara/electron-egg
|
||||
- `frontend/src/utils/rsa.ts`
|
||||
- RSA 加解密封装
|
||||
|
||||
## 📔 框架核心包 ee-core
|
||||
ee-core:[https://github.com/wallace5303/ee-core](https://github.com/wallace5303/ee-core)
|
||||
- `frontend/src/utils/ipcRenderer.ts`
|
||||
- 渲染进程 IPC 调用封装
|
||||
|
||||
## 📚 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>
|
||||
当前前端只有一条实际业务路由:
|
||||
|
||||
- `/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` 模板本身
|
||||
- 如果后续继续迭代业务,建议优先维护本文件,保证项目文档与实际实现一致
|
||||
|
||||
221
README.zh-CN.md
221
README.zh-CN.md
@@ -1,221 +0,0 @@
|
||||
[](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+**
|
||||
- 🏆 码云最有价值开源项目
|
||||

|
||||
|
||||
## 📚 文档
|
||||
- 快速体验:[教程文档](https://www.kaka996.com/)
|
||||

|
||||
|
||||
## 📦 特性
|
||||
1. 🍄 跨平台:一套代码,可以打包成windows版、Mac版、Linux版、国产UOS、Deepin、麒麟等
|
||||
2. 🌹 架构:单业务进程/模块化/多任务(进程,线程,渲染进程),让开发大型项目变的简单。
|
||||
3. 🌱 简单高效:只需学习 js 语言
|
||||
4. 🌴 前端独立:理论上支持任何前端技术,如:vue、react、html等等
|
||||
5. 🍁 工程化:可以用前端、服务端的开发思维,来编写桌面软件
|
||||
6. 🌷 高性能:事件驱动、非阻塞式IO
|
||||
7. 🌰 功能丰富:配置、通信、插件、数据库、升级、打包、工具... 应有尽有
|
||||
8. 💐 安全:支持字节码加密、压缩混淆加密
|
||||
9. 🌻 功能demo:桌面软件常见功能,框架集成或提供demo
|
||||
|
||||
## ✈️ 使用场景
|
||||
|
||||
### 1. 🚀 常规桌面软件
|
||||
- 🚖 windows平台
|
||||
|
||||

|
||||
|
||||
- 🚍 macOS平台
|
||||

|
||||
|
||||
- 🚔 linux平台 - 国产UOS、Deepin
|
||||

|
||||
|
||||
- 🚔 linux平台 - ubuntu
|
||||

|
||||
|
||||
### 🚐 2. vue、react、angular、web 转换成桌面软件
|
||||
- 🚙 vue-ant-design(本地)
|
||||
|
||||

|
||||
|
||||
- 🚙 禅道项目管理(web项目地址)
|
||||
|
||||

|
||||
|
||||
### 🚂 3. 游戏(h5相关技术开发)
|
||||
- 🚊 忍者100层
|
||||
|
||||

|
||||
|
||||
|
||||
## 📒 开始使用
|
||||
|
||||
- ✒️ [安装文档](https://www.kaka996.com/pages/e64ff6/)
|
||||
|
||||
## 项目案例
|
||||
- 🐟 框架已经应用于医疗、学校、政务、股票交易、ERP、娱乐、视频、企业等领域客户端
|
||||
|
||||
### 🐸 远控
|
||||
|
||||
- RQ Center
|
||||

|
||||

|
||||
|
||||
### 🐸 云盘
|
||||
|
||||
- FM Cloud
|
||||

|
||||

|
||||

|
||||
|
||||
### 🐸 IM
|
||||
|
||||
- Cede IM
|
||||

|
||||

|
||||

|
||||
|
||||
### 🐸 壁纸
|
||||
|
||||
- warpar
|
||||

|
||||
|
||||
### 🐸 英雄联盟助手
|
||||
|
||||
- Serendlplty
|
||||

|
||||
|
||||
### 🐸 更多
|
||||
|
||||
- [更多案例](https://www.kaka996.com/pages/eadf46/)
|
||||
|
||||
## 💬 交流
|
||||
1. [讨论](https://www.kaka996.com/pages/c2720e/)
|
||||
|
||||
## 📌 关于pr
|
||||
请前往[GitHub项目](https://github.com/dromara/electron-egg)提pr(避免代码同步后,pr被覆盖掉),感谢!
|
||||
|
||||
地址:https://github.com/dromara/electron-egg
|
||||
|
||||
## 📔 框架核心包 ee-core
|
||||
ee-core:[https://github.com/wallace5303/ee-core](https://github.com/wallace5303/ee-core)
|
||||
|
||||
## 📚 Dromara 成员项目
|
||||
|
||||
<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
129
build-portable.bat
Normal 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
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
226
docs/current-page.md
Normal file
226
docs/current-page.md
Normal 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. 如有需要,可手动备份当前数据,或在清空后通过备份文件恢复。
|
||||
|
||||
## 页面价值总结
|
||||
|
||||
当前页面虽然只有一个,但已经覆盖完整核心业务流程:
|
||||
|
||||
- 录入申请方
|
||||
- 输入申请码
|
||||
- 选择激活模块
|
||||
- 生成激活码
|
||||
- 保存激活记录
|
||||
- 查询历史记录
|
||||
- 复制历史激活码
|
||||
- 删除历史记录
|
||||
- 备份本地数据
|
||||
- 导入备份恢复数据
|
||||
|
||||
因此,这个页面本质上是一个“激活工具 + 本地记录管理工具”的组合界面。
|
||||
@@ -1,13 +1,16 @@
|
||||
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}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -17,3 +41,4 @@ app.register("preload", preload);
|
||||
|
||||
// Run
|
||||
app.run();
|
||||
writeRuntimeLog('app bootstrap end');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
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);
|
||||
);`;
|
||||
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 : '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,116 +1,139 @@
|
||||
<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-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公钥内容"
|
||||
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)"
|
||||
>
|
||||
<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私钥">
|
||||
<div class="field-block">
|
||||
<label>RSA 私钥</label>
|
||||
<a-textarea
|
||||
v-model:value="rsaKeys.privateKey"
|
||||
:readonly="readonly"
|
||||
:rows="5"
|
||||
placeholder="RSA私钥内容"
|
||||
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-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
|
||||
<section class="activation-section">
|
||||
<div class="section-title">
|
||||
<h3>激活模块</h3>
|
||||
<p>至少选择一个模块。</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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="请输入申请方"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-divider orientation="left" orientation-margin="0px">备注</a-divider>
|
||||
<a-form-item>
|
||||
</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"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-space>
|
||||
<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"
|
||||
@@ -119,26 +142,38 @@
|
||||
>
|
||||
生成激活码
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
:disabled="!activationCode"
|
||||
@click="copyToClipboard(activationCode)"
|
||||
>
|
||||
复制激活码
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</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 { 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:'',
|
||||
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 密钥')
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
.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-form-item-label) {
|
||||
font-weight: bold;
|
||||
:deep(.ant-btn) {
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 18px;
|
||||
@media (max-width: 640px) {
|
||||
.module-grid,
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
.field-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.field-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-actions :deep(.ant-btn) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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">
|
||||
<a-space style="margin-bottom: 10px;" wrap>
|
||||
<a-button 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-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-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,13 +140,12 @@
|
||||
</template>
|
||||
|
||||
<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 ActiveForm from "@/views/activate/ActiveForm.vue";
|
||||
import { ipc } from "@/utils/ipcRenderer";
|
||||
import { ipcApiRoute } from "@/api";
|
||||
|
||||
|
||||
const searchFormRef = ref<FormInstance>();
|
||||
const activateFormRef = ref();
|
||||
const searchForm = ref({
|
||||
@@ -98,11 +158,16 @@ 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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user