add:添加sqlite保存激活记录

This commit is contained in:
贾同学
2025-10-24 15:10:23 +08:00
parent 005b5137cf
commit ba143138d1
15 changed files with 758 additions and 313 deletions

View File

@@ -8,10 +8,10 @@ const config: () => AppConfig = () => {
singleLock: true,
windowsOption: {
title: 'PQS9100工具箱', // 软件标题
width: 980, // 软件窗口宽度
height: 650, // 软件窗口高度
minWidth: 800, // 软件窗口最小宽度
minHeight: 650, // 软件窗口最小高度
width: 1366, // 软件窗口宽度
height: 768, // 软件窗口高度
minWidth: 1366, // 软件窗口最小宽度
minHeight: 768, // 软件窗口最小高度
autoHideMenuBar: true, // 默认不显示菜单栏,
webPreferences: {
webSecurity: true,

View File

@@ -0,0 +1,26 @@
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)
}
async save(args: {
macAddress: string,
applicationCode: string,
modules: string[],
activationCode: string,
createTime: string,
remark: string
}): Promise<any> {
const { modules} = args
return activateRecordService.save({...args, module : modules.join(',')})
}
}
ActivateRecordController.toString = () => '[class ActivateRecordController]';
export default ActivateRecordController;

View File

@@ -1,9 +1,9 @@
import { logger } from 'ee-core/log';
import { isChildJob, exit } from 'ee-core/ps';
import { childMessage } from 'ee-core/message';
import { welcome } from './hello';
import { UserService } from '../../service/job/user';
import { sqlitedbService } from '../../service/database/sqlitedb';
import {logger} from 'ee-core/log';
import {exit, isChildJob} from 'ee-core/ps';
import {childMessage} from 'ee-core/message';
import {welcome} from './hello';
import {UserService} from '../../service/job/user';
import {sqlitedbService} from '../../service/database/activateRecord';
/**
* example - TimerJob

View File

@@ -6,6 +6,7 @@ import {logger} from 'ee-core/log';
import {trayService} from '../service/os/tray';
import {securityService} from '../service/os/security';
import {autoUpdaterService} from '../service/os/auto_updater';
import {activateRecordService} from '../service/database/activateRecord';
function preload(): void {
// Example feature module, optional to use and modify
@@ -13,6 +14,8 @@ function preload(): void {
trayService.create();
securityService.create();
autoUpdaterService.create();
activateRecordService.init();
}
/**

View File

@@ -0,0 +1,129 @@
import {BasedbService} from './basedb';
/**
* sqlite数据存储
* @class
*/
class ActivateRecordService extends BasedbService {
tableName: string;
constructor() {
const options = {
dbname: 'pqs9100-tool.db',
}
super(options);
this.tableName = 'activate_record';
}
/*
* 初始化表
*/
init(): void {
this._init();
// 检查表是否存在
const masterStmt = this.db.prepare('SELECT * FROM sqlite_master WHERE type=? AND name = ?');
let tableExists = masterStmt.get('table', this.tableName);
if (!tableExists) {
// 创建表
const create_table_sql =
`CREATE TABLE ${this.tableName}
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
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);
}
}
/*
* 增 data (sqlite)
*/
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);
return true;
}
/*
* 删 data
*/
async removeById(name: string = ''): Promise<boolean> {
const remove = this.db.prepare(`DELETE
FROM ${this.tableName}
WHERE id = ?`);
remove.run(name);
return true;
}
/*
* 查list data (sqlite)
*/
async list(macAddress: string = '', modules: string[] = []): Promise<any[]> {
let condition = ''
if (macAddress) {
condition += ` AND macAddress = '${macAddress}'`
}
if (modules.length > 0) {
const moduleConditions = modules.map(module => `module LIKE '%${module}%'`).join(' OR ');
condition += ` AND (${moduleConditions})`;
}
const select = this.db.prepare(`SELECT *
FROM ${this.tableName}
WHERE 1 = 1 ${condition}
order by id desc`);
return select.all();
}
/*
* all Test data (sqlite)
*/
async getAllTestDataSqlite(): Promise<any[]> {
const selectAllUser = this.db.prepare(`SELECT *
FROM ${this.tableName} `);
const allUser = selectAllUser.all();
return allUser;
}
/*
* get data dir (sqlite)
*/
async getDataDir(): Promise<string> {
const dir = this.storage.getDbDir();
return dir;
}
/*
* set custom data dir (sqlite)
*/
async setCustomDataDir(dir: string): Promise<void> {
if (dir.length == 0) {
return;
}
this.changeDataDir(dir);
this.init();
return;
}
}
ActivateRecordService.toString = () => '[class ActivateRecordService]';
const activateRecordService = new ActivateRecordService();
export {
ActivateRecordService,
activateRecordService
};

View File

@@ -0,0 +1,52 @@
import {type Database, SqliteStorage} from 'ee-core/storage';
import {getDataDir} from 'ee-core/ps';
import path from 'path';
/**
* sqlite数据存储
* @class
*/
class BasedbService {
dbname: string;
db!: Database;
storage!: SqliteStorage;
constructor(options: { dbname: string }) {
const { dbname } = options;
this.dbname = dbname;
}
/*
* 初始化
*/
protected _init(): void {
// 定义数据文件
const dbFile = path.join(getDataDir(), "db", this.dbname);
const sqliteOptions = {
timeout: 6000,
verbose: console.log
}
this.storage = new SqliteStorage(dbFile, sqliteOptions);
this.db = this.storage.db;
}
/*
* change data dir (sqlite)
*/
changeDataDir(dir: string): void {
// the absolute path of the db file
const dbFile = path.join(dir, this.dbname);
const sqliteOptions = {
timeout: 6000,
verbose: console.log
}
this.storage = new SqliteStorage(dbFile, sqliteOptions);
this.db = this.storage.db;
}
}
BasedbService.toString = () => '[class BasedbService]';
export {
BasedbService,
}

View File

@@ -1,12 +1,12 @@
/**
* Definition of communication channel between main process and rendering process
* formatcontroller/filename/method
* Definition of communication channels between main process and rendering process
*/
const ipcApiRoute = {
example: {
test: 'controller/example/test',
activateRecord: {
list: 'controller/activateRecord/list',
save: 'controller/activateRecord/save',
},
framework: {
checkForUpdater: 'controller/framework/checkForUpdater',
@@ -28,7 +28,7 @@ const ipcApiRoute = {
createPoolNotice: 'controller/framework/createPoolNotice',
someJobByPool: 'controller/framework/someJobByPool',
hello: 'controller/framework/hello',
openSoftware: 'controller/framework/openSoftware',
openSoftware: 'controller/framework/openSoftware',
},
// os
@@ -78,7 +78,7 @@ const specialIpcRoute = {
}
export {
ipcApiRoute,
ipcApiRoute,
specialIpcRoute
}

View File

@@ -2,7 +2,6 @@
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
width: 100vw;
height: 100vh;

View File

@@ -1,22 +0,0 @@
import { createFromIconfontCN } from '@ant-design/icons-vue'
import { h, VNode } from 'vue'
const IconFont = createFromIconfontCN({
scriptUrl: 'https://at.alicdn.com/t/font_2456157_4ovzopz659q.js',
extraCommonProps: {
type: 'icon-fengche',
style: {
fontSize: '18px',
},
},
})
interface Props {
type?: string;
}
const DynamicIconFont = (props: Props): VNode => {
return h(IconFont, { type: props.type || 'icon-fengche' })
}
export default DynamicIconFont

View File

@@ -1,5 +1,3 @@
import iconFont from './iconFont';
// Use import.meta.globEager to dynamically import all .vue files in the directory
const modules: { [key: string]: { default: any } } = import.meta.glob('./*.vue', { eager: true });
@@ -12,8 +10,7 @@ Object.keys(modules).forEach(file => {
// Combine the dynamically imported components with the iconFont component
const globalComponents = {
...map,
iconFont,
...map
};
export default globalComponents;
export default globalComponents;

View File

@@ -1,34 +1,34 @@
<template>
<a-layout has-sider id="app-layout-sider">
<a-layout-sider
v-model:collapsed="collapsed" collapsible
theme="light"
class="layout-sider"
>
<div class="logo">
<img src="@/assets/logo.png" class="pic-logo" />
<h4>PQS9100工具箱</h4>
</div>
<a-menu
<a-layout id="app-layout-sider" has-sider>
<a-layout-sider
v-model:collapsed="collapsed" class="layout-sider"
collapsible
theme="light"
mode="inline"
:selectedKeys="[current]"
@click="menuHandle"
>
<a-menu-item v-for="(menuInfo, index) in menu" :key="index">
<component :is="menuInfo.icon"></component>
<span>{{ menuInfo.title }}</span>
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout :style="contentStyles">
<a-layout-content class="layout-content">
<router-view />
</a-layout-content>
<div class="logo">
<img class="pic-logo" src="@/assets/logo.png"/>
<h4>PQS9100工具箱</h4>
</div>
<a-menu
:selectedKeys="[current]"
mode="inline"
theme="light"
@click="menuHandle"
>
<a-menu-item v-for="(menuInfo, index) in menu" :key="index">
<component :is="menuInfo.icon"></component>
<span>{{ menuInfo.title }}</span>
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout :style="contentStyles">
<a-layout-content class="layout-content">
<router-view/>
</a-layout-content>
</a-layout>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import {onMounted, ref, watch} from 'vue';
import {useRouter} from 'vue-router';
import {FileProtectOutlined} from "@ant-design/icons-vue"; // 定义菜单项的类型
@@ -61,38 +61,38 @@ onMounted(() => {
menuHandle(null);
});
watch(collapsed , (val:boolean) => {
watch(collapsed, (val: boolean) => {
if (val) {
contentStyles.value.marginLeft = '100px';
contentStyles.value.marginLeft = '80px';
} else {
contentStyles.value.marginLeft='200px';
contentStyles.value.marginLeft = '200px';
}
})
const menuHandle = (e: any): void => {
console.log('sider menu e:', e);
if (e) {
current.value = e.key;
}
console.log('sider menu current:', current.value);
const linkInfo = menu.value[current.value];
console.log('[home] load linkInfo:', linkInfo);
router.push({ name: linkInfo.pageName});
router.push({name: linkInfo.pageName});
}
</script>
<style lang="less" scoped>
// 嵌套
#app-layout-sider {
height: 100%;
.logo {
border-bottom: 1px solid #e8e8e8;
padding:10px;
padding: 10px;
user-select: none;
}
.pic-logo {
height: 32px;
margin: 10px;
}
.layout-sider {
border-top: 1px solid #e8e8e8;
border-right: 1px solid #e8e8e8;
@@ -103,6 +103,7 @@ const menuHandle = (e: any): void => {
top: 0;
bottom: 0;
}
.menu-item {
.ant-menu-item {
background-color: #fff;
@@ -111,9 +112,11 @@ const menuHandle = (e: any): void => {
padding: 0 !important;
}
}
.layout-content {
width: 96%;
margin: 0 auto;
width: 96%;
height: 100%;
margin: 0 auto;
}
}
</style>

View File

@@ -1,9 +1,6 @@
/**
* 基础路由
* @type { *[] }
*/
import {RouteRecordRaw} from 'vue-router'
const constantRouterMap = [
const routerMap: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/AppSider.vue'),
@@ -11,11 +8,10 @@ const constantRouterMap = [
{
path: '/activate',
name: 'Activate',
component: () => import('@/views/activate/index.vue'),
props: { id: 'activate' }
component: () => import('@/views/activate/index.vue')
}
]
},
]
export default constantRouterMap
export default routerMap

View File

@@ -0,0 +1,299 @@
<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>
<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-button>
</a-form-item>
</a-col>
<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)"
>
复制私钥
</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>
<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>
<a-col :span="24">
<a-space>
<a-button
:disabled="!activationForm.applicationCode.trim()"
:loading="generating"
type="primary"
@click="generateActivationCode"
>
生成激活码
</a-button>
<a-button
:disabled="!activationCode"
@click="copyToClipboard(activationCode)"
>
复制激活码
</a-button>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {message} from 'ant-design-vue'
import rsa from '@/utils/rsa'
import {Activate} from "@/views/activate/index";
import dayjs from "dayjs";
const RSA_CAN_EDIT = import.meta.env.VITE_RSA_CAN_EDIT === 'true'
const rsaKeys = ref({
publicKey: rsa.publicKey,
privateKey: rsa.privateKey
})
const hiddenKeys = ref(RSA_CAN_EDIT)
const readonly = ref(true)
const activationForm = ref({
applicationCode: '',
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 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
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)
}
if (!applicationCodePlaintext) {
generating.value = false
message.error('无效的设备申请码')
return
}
if (!applicationCodePlaintext.macAddress) {
message.error('无效的设备申请码')
return
}
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)
generating.value = false
}, 1000)
} catch (e) {
console.error(e)
generating.value = false
message.error('生成激活码失败')
}
}
// 复制到剪贴板
const copyToClipboard = (text: string) => {
if (!text) {
message.warning('没有内容可复制')
return
}
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
}).catch(() => {
message.error('复制失败')
})
}
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 = []
if (simulate.permanently) {
modules.push('simulate')
}
if (digital.permanently) {
modules.push('digital')
}
if (contrast.permanently) {
modules.push('contrast')
}
return {
macAddress: macAddress.value,
applicationCode: activationForm.value.applicationCode,
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;
:deep(textarea) {
font-family: consolas, monospace;
resize: none;
}
:deep(.ant-form-item-label) {
font-weight: bold;
}
:deep(.ant-card-body) {
padding: 18px;
}
:deep(.ant-card) {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
</style>

View File

@@ -1,221 +1,203 @@
<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>
<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-button>
</a-form-item>
</a-col>
<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)"
>
复制私钥
</a-button>
</a-form-item>
</a-col>
</a-row>
<div style="width: 100%;height: 100%">
<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-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-option value="simulate">模拟式模块</a-select-option>
<a-select-option value="digital">数字式模块</a-select-option>
<a-select-option value="contrast">比对式模块</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="query">
<template #icon>
<search-outlined/>
</template>
查询
</a-button>
</a-form-item>
<a-form-item>
<a-button @click="resetForm">
<template #icon>
<reload-outlined/>
</template>
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<a-card title="设备激活">
<a-row :gutter="16">
<a-col :span="24">
<a-divider orientation="left" orientation-margin="0px">激活模块</a-divider>
<a-form layout="inline" :label-col="labelCol">
<a-form-item label="模拟式模块">
<a-switch v-model:checked="activationForm.simulate.permanently"/>
</a-form-item>
<a-form-item label="数字式模块">
<a-switch v-model:checked="activationForm.digital.permanently"/>
</a-form-item>
<a-form-item label="比对式模块">
<a-switch v-model:checked="activationForm.contrast.permanently"/>
</a-form-item>
</a-form>
</a-col>
<a-col :span="24">
<a-divider orientation="left" orientation-margin="0px">设备申请码</a-divider>
<a-form-item>
<a-textarea
v-model:value="activationForm.applicationCode"
:rows="3"
allow-clear
placeholder="请输入设备申请码"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<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-space>
<a-button
:disabled="!activationForm.applicationCode.trim()"
:loading="generating"
type="primary"
@click="generateActivationCode"
>
生成激活码
</a-button>
<a-button
:disabled="!activationCode"
@click="copyToClipboard(activationCode)"
>
<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">
<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(',')">
{{ 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-col>
</a-row>
</template>
</template>
</a-table>
</a-card>
</div>
<a-modal v-model:visible="visible" :keyboard="false" :mask-closable="false" centered destroy-on-close width="70%">
<template #title>
<file-protect-outlined/>
设备激活
</template>
<ActiveForm ref="activateFormRef"></ActiveForm>
<template #footer>
<a-button type="primary" @click="save">
保存
</a-button>
<a-button @click="visible = false">关闭</a-button>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {message} from 'ant-design-vue'
import rsa from '@/utils/rsa'
import {Activate} from "@/views/activate/index";
import {FormInstance, 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 RSA_CAN_EDIT = import.meta.env.VITE_RSA_CAN_EDIT === 'true'
const rsaKeys = ref({
publicKey: rsa.publicKey,
privateKey: rsa.privateKey
const searchFormRef = ref<FormInstance>();
const activateFormRef = ref();
const searchForm = ref({
macAddress: '',
modules: [],
})
const hiddenKeys = ref(RSA_CAN_EDIT)
const readonly = ref(true)
const activationForm = ref({
applicationCode: '',
simulate: {permanently: false},
digital: {permanently: false},
contrast: {permanently: false},
const loading = ref(false)
const visible = ref(false)
const dataSource = ref<any []>([])
const pagination = ref({
total: 0,
pageSize: 10,
// current: 1,
showTotal: (total: number) => `${total}`
})
const activationCode = ref('')
const generating = ref(false)
const labelCol = { style: { width: '120px' } }
// 生成激活码
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
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)
}
if (!applicationCodePlaintext) {
generating.value = false
message.error('无效的设备申请码')
return
}
if (!applicationCodePlaintext.macAddress) {
message.error('无效的设备申请码')
return
}
let activationCodePlaintext: Activate.ActivationCodePlaintext = {
macAddress:'',
simulate: {permanently: 0},
digital: {permanently: 0},
contrast: {permanently: 0}
}
activationCodePlaintext.macAddress = 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)
generating.value = false
}, 1000)
} catch (e) {
console.error(e)
generating.value = false
message.error('生成激活码失败')
}
const columns = [
{
title: 'mac地址',
dataIndex: 'macAddress',
key: 'macAddress',
align: 'center',
width: 150,
},
{
title: '申请码',
dataIndex: 'applicationCode',
key: 'applicationCode',
align: 'center',
ellipsis: true,
width: 100,
},
{
title: '激活模块',
dataIndex: 'module',
key: 'module',
align: 'center',
width: 250,
},
{
title: '激活码',
dataIndex: 'activationCode',
key: 'activationCode',
align: 'center',
ellipsis: true,
width: 100,
},
{
title: '生成时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 150,
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
align: 'center',
width: 100,
},
{
title: '操作',
key: 'action',
align: 'center',
width: 120,
},
]
const resetForm = () => {
searchFormRef.value?.resetFields();
query()
}
onMounted(() => {
resetForm()
})
const query = () => {
loading.value = true
const params: any = {
macAddress: searchForm.value.macAddress,
modules: [...searchForm.value.modules],
}
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
loading.value = false
}).catch((err: any) => {
console.error(err)
loading.value = false
})
}, 500)
}
const save = () => {
const data = activateFormRef.value.getData()
if (!data) {
message.warning('请先生成激活码')
return
}
ipc.invoke(ipcApiRoute.activateRecord.save, data).then(() => {
message.success('保存成功')
visible.value = false
resetForm()
}).catch((err: any) => {
console.error(err)
message.error('保存失败')
})
}
// 复制到剪贴板
const copyToClipboard = (text: string) => {
if (!text) {
@@ -231,26 +213,5 @@ const copyToClipboard = (text: string) => {
}
</script>
<style lang="less" scoped>
.activation-page {
height: 100%;
background-color: #fff;
border-radius: 8px;
:deep(textarea) {
font-family: consolas, monospace;
resize: none;
}
:deep(.ant-form-item-label) {
font-weight: bold;
}
:deep(.ant-card-body) {
padding: 18px;
}
:deep(.ant-card) {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
</style>

View File

@@ -13,6 +13,7 @@
"build-electron": "ee-bin build --cmds=electron",
"encrypt": "ee-bin encrypt",
"icon": "ee-bin icon",
"re-sqlite": "electron-rebuild -f -w better-sqlite3",
"build-w": "ee-bin build --cmds=win64",
"build-we": "ee-bin build --cmds=win_e",
"build-m": "ee-bin build --cmds=mac",
@@ -32,6 +33,7 @@
"author": "njcn",
"devDependencies": {
"@electron/rebuild": "^3.7.1",
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^20.16.0",
"cross-env": "^7.0.3",
"debug": "^4.4.0",
@@ -42,7 +44,7 @@
"typescript": "^5.4.2"
},
"dependencies": {
"axios": "^1.7.9",
"better-sqlite3": "^11.7.0",
"dayjs": "^1.11.13",
"ee-core": "^4.1.5",
"electron-updater": "^6.3.8"