This commit is contained in:
贾同学
2025-10-16 20:01:57 +08:00
commit 4768ef2d26
79 changed files with 3358 additions and 0 deletions

14
frontend/.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
# https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@@ -0,0 +1,4 @@
VITE_TITLE="NPQS9100-自动检测平台工具箱"
VITE_RSA_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB"
VITE_RSA_PRIVATE_KEY="MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo="

3
frontend/.env.production Normal file
View File

@@ -0,0 +1,3 @@
VITE_TITLE="NPQS9100-自动检测平台工具箱"
VITE_RSA_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB"
VITE_RSA_PRIVATE_KEY="MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo="

6
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json

105
frontend/index.html Normal file
View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" />
<title></title>
<!-- 优化vue渲染未完成之前先加一个css动画 -->
<style>
#loadingPage {
background-color: #dedede;
font-size: 12px;
}
.base {
height: 9em;
left: 50%;
margin: -7.5em;
padding: 3em;
position: absolute;
top: 50%;
width: 9em;
transform: rotateX(45deg) rotateZ(45deg);
transform-style: preserve-3d;
}
.cube,
.cube:after,
.cube:before {
content: '';
float: left;
height: 3em;
position: absolute;
width: 3em;
}
/* Top */
.cube {
background-color: #06cf68;
position: relative;
transform: translateZ(3em);
transform-style: preserve-3d;
transition: .25s;
box-shadow: 13em 13em 1.5em rgba(0, 0, 0, 0.1);
animation: anim 1s infinite;
}
.cube:after {
background-color: #05a151;
transform: rotateX(-90deg) translateY(3em);
transform-origin: 100% 100%;
}
.cube:before {
background-color: #026934;
transform: rotateY(90deg) translateX(3em);
transform-origin: 100% 0;
}
.cube:nth-child(1) {
animation-delay: 0.05s;
}
.cube:nth-child(2) {
animation-delay: 0.1s;
}
.cube:nth-child(3) {
animation-delay: 0.15s;
}
.cube:nth-child(4) {
animation-delay: 0.2s;
}
.cube:nth-child(5) {
animation-delay: 0.25s;
}
.cube:nth-child(6) {
animation-delay: 0.3s;
}
.cube:nth-child(7) {
animation-delay: 0.35s;
}
.cube:nth-child(8) {
animation-delay: 0.4s;
}
.cube:nth-child(9) {
animation-delay: 0.45s;
}
@keyframes anim {
50% {
transform: translateZ(0.5em);
}
}
</style>
</head>
<body>
<div id="loadingPage">
<div class='base'>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
</div>
</div>
<div id="app"></div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

38
frontend/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "ee",
"version": "4.0.0",
"scripts": {
"dev": "vite --host --port 8080",
"serve": "vite --host --port 8080",
"build-staging": "vite build --mode staging",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons-vue": "^6.1.0",
"ant-design-vue": "^3.2.20",
"axios": "^1.12.2",
"jsencrypt": "^3.5.4",
"node-forge": "^1.3.1",
"pinia": "^3.0.3",
"socket.io-client": "^4.8.1",
"store2": "^2.14.4",
"vue": "^3.5.22",
"vue-router": "^4.6.2",
"vuex": "^4.1.0"
},
"devDependencies": {
"@types/node": "^20.16.0",
"@types/node-forge": "^1.3.14",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/compiler-sfc": "^3.5.22",
"less": "^4.4.2",
"less-loader": "^12.3.0",
"postcss": "^8.5.6",
"postcss-pxtorem": "^6.1.0",
"terser": "^5.44.0",
"typescript": "^5.9.3",
"vite": "^6.4.0",
"vite-plugin-compression": "^0.5.1"
}
}

15
frontend/src/App.vue Normal file
View 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
View File

@@ -0,0 +1,84 @@
/**
* 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',
},
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
}

View 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View 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); // 浮层阴影

View 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

View 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;

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

View File

@@ -0,0 +1,5 @@
import AppSider from '@/layouts/AppSider.vue'
export {
AppSider
}

30
frontend/src/main.ts Normal file
View 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')

View 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

View 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
View 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
View File

0
frontend/src/types/shim.d.ts vendored Normal file
View File

7
frontend/src/types/source.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
// 声明一个模块,防止引入文件时报错
declare module '*.json';
declare module '*.png';
declare module '*.jpg';
declare module '*.scss';
declare module '*.ts';
declare module '*.js';

View 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' },
]

View 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
View 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
}

View 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;
}
}

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

35
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"jsx": "preserve",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"resolveJsonModule": true,
// 定义一个变量就必须给它一个初始值
"strictPropertyInitialization": false,
// 允许使用obj[key]访问对象属性
//"suppressImplicitAnyIndexErrors": true,
"allowJs": true,
"sourceMap": true,
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
],
}
},
"include":["src/**/*.ts", "src/**/*.vue", "src/**/*.tsx", "src/**/*.d.ts", "*.d.ts"],
"exclude": [
"node_modules",
"dist"
]
}

54
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,54 @@
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import viteCompression from 'vite-plugin-compression';
import path from 'path';
export default defineConfig((mode) => {
return {
// Project plugins
plugins: [
vue(),
viteCompression({
verbose: true,
disable: false,
threshold: 1025,
algorithm: 'gzip',
ext: '.gz',
}),
],
// Base configuration
base: './',
publicDir: 'public',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
less: {
modifyVars: {
'@border-color-base': '#dce3e8',
},
javascriptEnabled: true,
},
},
},
build: {
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 4096,
cssCodeSplit: true,
brotliSize: false,
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
// Remove console and debugger in production
drop_console: false,
drop_debugger: true,
},
},
},
};
});