init
This commit is contained in:
15
frontend/src/App.vue
Normal file
15
frontend/src/App.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
const loadingElement = document.getElementById('loadingPage');
|
||||
if (loadingElement) {
|
||||
(loadingElement as HTMLElement).remove();
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
<style lang="less"></style>
|
||||
84
frontend/src/api/index.ts
Normal file
84
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
/**
|
||||
* Definition of communication channel between main process and rendering process
|
||||
* format:controller/filename/method
|
||||
* Definition of communication channels between main process and rendering process
|
||||
*/
|
||||
const ipcApiRoute = {
|
||||
example: {
|
||||
test: 'controller/example/test',
|
||||
},
|
||||
framework: {
|
||||
checkForUpdater: 'controller/framework/checkForUpdater',
|
||||
downloadApp: 'controller/framework/downloadApp',
|
||||
jsondbOperation: 'controller/framework/jsondbOperation',
|
||||
sqlitedbOperation: 'controller/framework/sqlitedbOperation',
|
||||
uploadFile: 'controller/framework/uploadFile',
|
||||
checkHttpServer: 'controller/framework/checkHttpServer',
|
||||
doHttpRequest: 'controller/framework/doHttpRequest',
|
||||
doSocketRequest: 'controller/framework/doSocketRequest',
|
||||
ipcInvokeMsg: 'controller/framework/ipcInvokeMsg',
|
||||
ipcSendSyncMsg: 'controller/framework/ipcSendSyncMsg',
|
||||
ipcSendMsg: 'controller/framework/ipcSendMsg',
|
||||
startJavaServer: 'controller/framework/startJavaServer',
|
||||
closeJavaServer: 'controller/framework/closeJavaServer',
|
||||
someJob: 'controller/framework/someJob',
|
||||
timerJobProgress: 'controller/framework/timerJobProgress',
|
||||
createPool: 'controller/framework/createPool',
|
||||
createPoolNotice: 'controller/framework/createPoolNotice',
|
||||
someJobByPool: 'controller/framework/someJobByPool',
|
||||
hello: 'controller/framework/hello',
|
||||
openSoftware: 'controller/framework/openSoftware',
|
||||
},
|
||||
|
||||
// os
|
||||
os: {
|
||||
messageShow: 'controller/os/messageShow',
|
||||
messageShowConfirm: 'controller/os/messageShowConfirm',
|
||||
selectFolder: 'controller/os/selectFolder',
|
||||
selectPic: 'controller/os/selectPic',
|
||||
openDirectory: 'controller/os/openDirectory',
|
||||
loadViewContent: 'controller/os/loadViewContent',
|
||||
removeViewContent: 'controller/os/removeViewContent',
|
||||
createWindow: 'controller/os/createWindow',
|
||||
getWCid: 'controller/os/getWCid',
|
||||
sendNotification: 'controller/os/sendNotification',
|
||||
initPowerMonitor: 'controller/os/initPowerMonitor',
|
||||
getScreen: 'controller/os/getScreen',
|
||||
autoLaunch: 'controller/os/autoLaunch',
|
||||
setTheme: 'controller/os/setTheme',
|
||||
getTheme: 'controller/os/getTheme',
|
||||
window1ToWindow2: 'controller/os/window1ToWindow2',
|
||||
window2ToWindow1: 'controller/os/window2ToWindow1',
|
||||
},
|
||||
|
||||
// effect
|
||||
effect: {
|
||||
selectFile: 'controller/effect/selectFile',
|
||||
loginWindow: 'controller/effect/loginWindow',
|
||||
restoreWindow: 'controller/effect/restoreWindow',
|
||||
},
|
||||
|
||||
// cross
|
||||
cross: {
|
||||
crossInfo: 'controller/cross/info',
|
||||
getCrossUrl: 'controller/cross/getUrl',
|
||||
killCrossServer: 'controller/cross/killServer',
|
||||
createCrossServer: 'controller/cross/createServer',
|
||||
requestApi: 'controller/cross/requestApi',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize Channel
|
||||
* Format: Custom (recommended to add a prefix)
|
||||
*/
|
||||
const specialIpcRoute = {
|
||||
appUpdater: 'custom/app/updater', // updater channel
|
||||
}
|
||||
|
||||
export {
|
||||
ipcApiRoute,
|
||||
specialIpcRoute
|
||||
}
|
||||
|
||||
15
frontend/src/assets/global.less
Normal file
15
frontend/src/assets/global.less
Normal file
@@ -0,0 +1,15 @@
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 滚动条 */
|
||||
::-webkit-scrollbar{width:8px;height:4px}
|
||||
::-webkit-scrollbar-button{width:10px;height:0}
|
||||
::-webkit-scrollbar-track{background:0 0}
|
||||
::-webkit-scrollbar-thumb{background: #ecf3fb; border-radius: 4px;-webkit-transition:.3s;transition:.3s}
|
||||
::-webkit-scrollbar-thumb:hover{background-color:#1890ff}
|
||||
::-webkit-scrollbar-thumb:active{background-color:#1890ff}
|
||||
BIN
frontend/src/assets/login.png
Normal file
BIN
frontend/src/assets/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
17
frontend/src/assets/theme.less
Normal file
17
frontend/src/assets/theme.less
Normal file
@@ -0,0 +1,17 @@
|
||||
@import 'ant-design-vue/dist/antd.less';
|
||||
|
||||
// 可自定义主题颜色
|
||||
//@primary-color: #07C160; // 全局主色
|
||||
@link-color: #1890ff; // 链接色
|
||||
@success-color: #52c41a; // 成功色
|
||||
@warning-color: #faad14; // 警告色
|
||||
@error-color: #f5222d; // 错误色
|
||||
@font-size-base: 14px; // 主字号
|
||||
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
|
||||
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
|
||||
@text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
|
||||
@disabled-color: rgba(0, 0, 0, 0.25); // 失效色
|
||||
@border-radius-base: 4px; // 组件/浮层圆角
|
||||
@border-color-base: #dce3e8; // 边框色
|
||||
@box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影
|
||||
|
||||
22
frontend/src/components/global/iconFont.ts
Normal file
22
frontend/src/components/global/iconFont.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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
|
||||
19
frontend/src/components/global/index.ts
Normal file
19
frontend/src/components/global/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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 });
|
||||
|
||||
// Create a map of component names to their default exports
|
||||
const map: { [key: string]: any } = {};
|
||||
Object.keys(modules).forEach(file => {
|
||||
const moduleName = file.replace('./', '').replace('.vue', '');
|
||||
map[moduleName] = modules[file].default;
|
||||
});
|
||||
|
||||
// Combine the dynamically imported components with the iconFont component
|
||||
const globalComponents = {
|
||||
...map,
|
||||
iconFont,
|
||||
};
|
||||
|
||||
export default globalComponents;
|
||||
112
frontend/src/layouts/AppSider.vue
Normal file
112
frontend/src/layouts/AppSider.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<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
|
||||
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="{ marginLeft: '200px' }">
|
||||
<a-layout-content class="layout-content">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {FileProtectOutlined} from "@ant-design/icons-vue"; // 定义菜单项的类型
|
||||
|
||||
// 定义菜单项的类型
|
||||
interface MenuItem {
|
||||
icon: any;
|
||||
title: string;
|
||||
pageName: string;
|
||||
}
|
||||
|
||||
// 定义菜单的类型
|
||||
interface Menu {
|
||||
[key: string]: MenuItem;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const collapsed = ref<boolean>(false);
|
||||
const current = ref<string>('menu_1');
|
||||
const menu = ref<Menu>({
|
||||
'menu_1': {
|
||||
icon: FileProtectOutlined,
|
||||
title: '设备激活',
|
||||
pageName: 'Activate'
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
menuHandle(null);
|
||||
});
|
||||
|
||||
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});
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// 嵌套
|
||||
#app-layout-sider {
|
||||
height: 100%;
|
||||
.logo {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding:10px;
|
||||
user-select: none;
|
||||
}
|
||||
.pic-logo {
|
||||
height: 32px;
|
||||
margin: 10px;
|
||||
}
|
||||
.layout-sider {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
overflow: auto;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.menu-item {
|
||||
.ant-menu-item {
|
||||
background-color: #fff;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
.layout-content {
|
||||
width: 96%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
frontend/src/layouts/index.ts
Normal file
5
frontend/src/layouts/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import AppSider from '@/layouts/AppSider.vue'
|
||||
|
||||
export {
|
||||
AppSider
|
||||
}
|
||||
30
frontend/src/main.ts
Normal file
30
frontend/src/main.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as AntIcon from '@ant-design/icons-vue';
|
||||
import Antd from 'ant-design-vue';
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import './assets/global.less';
|
||||
import './assets/theme.less';
|
||||
import components from './components/global';
|
||||
import Router from './router/index';
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// components
|
||||
type ComponentsType = typeof components;
|
||||
for (const componentName in components) {
|
||||
if (Object.prototype.hasOwnProperty.call(components, componentName)) {
|
||||
const component = components[componentName as keyof ComponentsType];
|
||||
app.component(componentName, component);
|
||||
}
|
||||
}
|
||||
|
||||
// icon
|
||||
const whiteList = ['createFromIconfontCN', 'getTwoToneColor', 'setTwoToneColor', 'default']
|
||||
const iconKeys = Object.keys(AntIcon) as Array<keyof typeof AntIcon>;
|
||||
iconKeys.forEach(key => {
|
||||
if (!whiteList.includes(key as typeof whiteList[number])) {
|
||||
app.component(key.toString(), AntIcon[key]);
|
||||
}
|
||||
});
|
||||
|
||||
app.use(Antd).use(Router).mount('#app')
|
||||
9
frontend/src/router/index.ts
Normal file
9
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import routerMap from './routerMap'
|
||||
|
||||
const Router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: routerMap as RouteRecordRaw[],
|
||||
})
|
||||
|
||||
export default Router
|
||||
21
frontend/src/router/routerMap.ts
Normal file
21
frontend/src/router/routerMap.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 基础路由
|
||||
* @type { *[] }
|
||||
*/
|
||||
|
||||
const constantRouterMap = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/AppSider.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/activate',
|
||||
name: 'Activate',
|
||||
component: () => import('@/views/activate/index.vue'),
|
||||
props: { id: 'activate' }
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
export default constantRouterMap
|
||||
12
frontend/src/types/env.d.ts
vendored
Normal file
12
frontend/src/types/env.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
// declare global {
|
||||
// interface Window {
|
||||
// electron?: any;
|
||||
// }
|
||||
// }
|
||||
0
frontend/src/types/pinia.d.ts
vendored
Normal file
0
frontend/src/types/pinia.d.ts
vendored
Normal file
0
frontend/src/types/shim.d.ts
vendored
Normal file
0
frontend/src/types/shim.d.ts
vendored
Normal file
7
frontend/src/types/source.d.ts
vendored
Normal file
7
frontend/src/types/source.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// 声明一个模块,防止引入文件时报错
|
||||
declare module '*.json';
|
||||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
declare module '*.scss';
|
||||
declare module '*.ts';
|
||||
declare module '*.js';
|
||||
27
frontend/src/utils/iconList.ts
Normal file
27
frontend/src/utils/iconList.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export default [
|
||||
{ name: '对话框', type: 'icon-duihuakuang' },
|
||||
{ name: '闹钟', type: 'icon-naozhong' },
|
||||
{ name: '笑脸', type: 'icon-xiaolian' },
|
||||
{ name: 'ok', type: 'icon-ok' },
|
||||
{ name: '风车', type: 'icon-fengche' },
|
||||
{ name: '汗颜', type: 'icon-hanyan' },
|
||||
{ name: '相机', type: 'icon-xiangji' },
|
||||
{ name: '礼物', type: 'icon-liwu' },
|
||||
{ name: '礼花', type: 'icon-lihua' },
|
||||
{ name: '扭蛋', type: 'icon-niudan' },
|
||||
{ name: '流星', type: 'icon-liuxing' },
|
||||
{ name: '风筝', type: 'icon-fengzheng' },
|
||||
{ name: '蛋糕', type: 'icon-dangao' },
|
||||
{ name: '泡泡', type: 'icon-paopao' },
|
||||
{ name: '购物', type: 'icon-gouwu' },
|
||||
{ name: '饮料', type: 'icon-yinliao' },
|
||||
{ name: '云彩', type: 'icon-yuncai' },
|
||||
{ name: '彩铅', type: 'icon-caiqian' },
|
||||
{ name: '纸飞机', type: 'icon-zhifeiji' },
|
||||
{ name: '点赞', type: 'icon-dianzan' },
|
||||
{ name: '煎蛋', type: 'icon-jiandan' },
|
||||
{ name: '小熊', type: 'icon-xiaoxiong' },
|
||||
{ name: '花', type: 'icon-hua' },
|
||||
{ name: '眼睛', type: 'icon-yanjing' },
|
||||
]
|
||||
|
||||
39
frontend/src/utils/ipcRenderer.ts
Normal file
39
frontend/src/utils/ipcRenderer.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
electron?: any;
|
||||
}
|
||||
}
|
||||
|
||||
const Renderer = (window.require && window.require('electron')) || window.electron || {};
|
||||
|
||||
/**
|
||||
* ipc
|
||||
* 官方api说明:https://www.electronjs.org/zh/docs/latest/api/ipc-renderer
|
||||
*
|
||||
* 属性/方法
|
||||
* ipc.invoke(channel, param) - 发送异步消息(invoke/handle 模型)
|
||||
* ipc.sendSync(channel, param) - 发送同步消息(send/on 模型)
|
||||
* ipc.on(channel, listener) - 监听 channel, 当新消息到达,调用 listener
|
||||
* ipc.once(channel, listener) - 添加一次性 listener 函数
|
||||
* ipc.removeListener(channel, listener) - 为特定的 channel 从监听队列中删除特定的 listener 监听者
|
||||
* ipc.removeAllListeners(channel) - 移除所有的监听器,当指定 channel 时只移除与其相关的所有监听器
|
||||
* ipc.send(channel, ...args) - 通过channel向主进程发送异步消息
|
||||
* ipc.postMessage(channel, message, [transfer]) - 发送消息到主进程
|
||||
* ipc.sendTo(webContentsId, channel, ...args) - 通过 channel 发送消息到带有 webContentsId 的窗口
|
||||
* ipc.sendToHost(channel, ...args) - 消息会被发送到 host 页面上的 <webview> 元素
|
||||
*/
|
||||
|
||||
/**
|
||||
* ipc
|
||||
*/
|
||||
const ipc = Renderer.ipcRenderer || undefined;
|
||||
|
||||
/**
|
||||
* 是否为EE环境
|
||||
*/
|
||||
const isEE = ipc ? true : false;
|
||||
|
||||
export {
|
||||
Renderer, ipc, isEE
|
||||
};
|
||||
|
||||
47
frontend/src/utils/rsa.ts
Normal file
47
frontend/src/utils/rsa.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import JSEncrypt from 'jsencrypt'
|
||||
|
||||
// 获取 RSA 公钥
|
||||
const publicKey = import.meta.env.VITE_RSA_PUBLIC_KEY
|
||||
|
||||
// 获取 RSA 私钥
|
||||
const privateKey = import.meta.env.VITE_RSA_PRIVATE_KEY
|
||||
|
||||
// RSA加密
|
||||
const encrypt = (data: string): string => {
|
||||
try {
|
||||
const encrypt = new JSEncrypt()
|
||||
encrypt.setPublicKey(publicKey)
|
||||
const encrypted = encrypt.encrypt(data)
|
||||
if (encrypted) {
|
||||
return encrypted
|
||||
} else {
|
||||
throw new Error('加密失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加密失败:', error)
|
||||
throw new Error('RSA加密失败')
|
||||
}
|
||||
}
|
||||
|
||||
// RSA解密
|
||||
const decrypt = (encryptedData: string): string => {
|
||||
try {
|
||||
const decrypt = new JSEncrypt()
|
||||
decrypt.setPrivateKey(privateKey)
|
||||
const decrypted = decrypt.decrypt(encryptedData)
|
||||
if (decrypted) {
|
||||
return decrypted as string
|
||||
} else {
|
||||
throw new Error('解密失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解密失败:', error)
|
||||
throw new Error('RSA解密失败')
|
||||
}
|
||||
}
|
||||
export default {
|
||||
encrypt,
|
||||
decrypt,
|
||||
publicKey,
|
||||
privateKey
|
||||
}
|
||||
56
frontend/src/views/activate/index.ts
Normal file
56
frontend/src/views/activate/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
export namespace Activate {
|
||||
|
||||
export interface ApplicationModule {
|
||||
/**
|
||||
* 是否申请 1是 0否
|
||||
*/
|
||||
apply: number;
|
||||
|
||||
}
|
||||
|
||||
export interface ActivateModule extends ApplicationModule {
|
||||
/**
|
||||
* 是否永久 1是 0否
|
||||
*/
|
||||
permanently: number;
|
||||
}
|
||||
|
||||
export interface ApplicationCodePlaintext {
|
||||
|
||||
/**
|
||||
* 模拟式模块
|
||||
*/
|
||||
simulate: ApplicationModule;
|
||||
|
||||
/**
|
||||
* 数字式模块
|
||||
*/
|
||||
digital: ApplicationModule;
|
||||
|
||||
/**
|
||||
* 比对式模块
|
||||
*/
|
||||
contrast: ApplicationModule;
|
||||
}
|
||||
export interface ActivationCodePlaintext {
|
||||
|
||||
/**
|
||||
* 模拟式模块
|
||||
*/
|
||||
simulate: ActivateModule;
|
||||
|
||||
/**
|
||||
* 数字式模块
|
||||
*/
|
||||
digital: ActivateModule;
|
||||
|
||||
/**
|
||||
* 比对式模块
|
||||
*/
|
||||
contrast: ActivateModule;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
205
frontend/src/views/activate/index.vue
Normal file
205
frontend/src/views/activate/index.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="activation-page">
|
||||
<a-card title="RSA密钥配置" style="margin-bottom: 20px;">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-alert
|
||||
message="注意:请妥善保管私钥,不要泄露给他人"
|
||||
type="warning"
|
||||
show-icon
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item label="RSA公钥">
|
||||
<a-textarea
|
||||
v-model:value="rsaKeys.publicKey"
|
||||
:rows="5"
|
||||
readonly
|
||||
placeholder="RSA公钥内容"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
@click="copyToClipboard(rsaKeys.publicKey)"
|
||||
:disabled="!rsaKeys.publicKey"
|
||||
style="margin-top: 8px;"
|
||||
>
|
||||
复制公钥
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item label="RSA私钥">
|
||||
<a-textarea
|
||||
v-model:value="rsaKeys.privateKey"
|
||||
:rows="5"
|
||||
readonly
|
||||
placeholder="RSA私钥内容"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
@click="copyToClipboard(rsaKeys.privateKey)"
|
||||
:disabled="!rsaKeys.privateKey"
|
||||
style="margin-top: 8px;"
|
||||
>
|
||||
复制私钥
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<a-card title="设备激活">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-divider orientation="left" orientation-margin="0px">设备申请码</a-divider>
|
||||
<a-form-item>
|
||||
<a-textarea
|
||||
v-model:value="activationForm.requestCode"
|
||||
:rows="3"
|
||||
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="activationForm.activationCode"
|
||||
:rows="3"
|
||||
placeholder="生成的激活码将显示在这里"
|
||||
readonly
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="generateActivationCode"
|
||||
:loading="generating"
|
||||
:disabled="!activationForm.requestCode.trim()"
|
||||
>
|
||||
生成激活码
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
@click="copyToClipboard(activationForm.activationCode)"
|
||||
:disabled="!activationForm.activationCode"
|
||||
>
|
||||
复制激活码
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {message} from 'ant-design-vue'
|
||||
import rsa from '@/utils/rsa'
|
||||
import {Activate} from "@/views/activate/index";
|
||||
|
||||
const rsaKeys = ref({
|
||||
publicKey: rsa.publicKey,
|
||||
privateKey: rsa.privateKey
|
||||
})
|
||||
|
||||
const activationForm = ref({
|
||||
requestCode: '',
|
||||
activationCode: ''
|
||||
})
|
||||
|
||||
const generating = ref(false)
|
||||
|
||||
|
||||
// 生成激活码
|
||||
const generateActivationCode = () => {
|
||||
if (!activationForm.value.requestCode.trim()) {
|
||||
message.error('请输入设备申请码')
|
||||
return
|
||||
}
|
||||
generating.value = true
|
||||
let activationCodePlaintext
|
||||
try {
|
||||
const plaintext = rsa.decrypt(activationForm.value.requestCode)
|
||||
activationCodePlaintext = JSON.parse(plaintext) as Activate.ActivationCodePlaintext
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (!activationCodePlaintext) {
|
||||
generating.value = false
|
||||
message.error('无效的设备申请码')
|
||||
return
|
||||
}
|
||||
const contrast = activationCodePlaintext.contrast
|
||||
const digital = activationCodePlaintext.digital
|
||||
const simulate = activationCodePlaintext.simulate
|
||||
if (!contrast && !digital && !simulate) {
|
||||
generating.value = false
|
||||
message.error('无效的设备申请码')
|
||||
return
|
||||
}
|
||||
if (contrast.apply === 1) {
|
||||
activationCodePlaintext.contrast.permanently = 1
|
||||
}
|
||||
if (digital.apply === 1) {
|
||||
activationCodePlaintext.digital.permanently = 1
|
||||
}
|
||||
if (simulate.apply === 1) {
|
||||
activationCodePlaintext.simulate.permanently = 1
|
||||
}
|
||||
const data = JSON.stringify(activationCodePlaintext)
|
||||
try {
|
||||
setTimeout(() => {
|
||||
activationForm.value.activationCode = 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('复制失败')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.activation-page {
|
||||
:deep(textarea) {
|
||||
font-family: consolas, monospace;
|
||||
resize: none;
|
||||
}
|
||||
:deep(.ant-form-item-label) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user