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

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>