前后端接口对接
This commit is contained in:
2
.env.development
Normal file
2
.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
# 开发环境
|
||||
VITE_APP_BASE_API = '/api'
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
63
components.d.ts
vendored
Normal file
63
components.d.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||
Actions: typeof import('./src/components/actions/index.vue')['default']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
DetectingDevice: typeof import('./src/components/detecting-device/index.vue')['default']
|
||||
ExecutionResult: typeof import('./src/components/common/ExecutionResult/ExecutionResult.vue')['default']
|
||||
ILucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||
ILucideCamera: typeof import('~icons/lucide/camera')['default']
|
||||
ILucideClock: typeof import('~icons/lucide/clock')['default']
|
||||
ILucideCpu: typeof import('~icons/lucide/cpu')['default']
|
||||
ILucideDatabase: typeof import('~icons/lucide/database')['default']
|
||||
ILucideEdit: typeof import('~icons/lucide/edit')['default']
|
||||
ILucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
ILucideLink: typeof import('~icons/lucide/link')['default']
|
||||
ILucideLinkOff: typeof import('~icons/lucide/link-off')['default']
|
||||
ILucideLinOff: typeof import('~icons/lucide/lin-off')['default']
|
||||
ILucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||
ILucideLoOff: typeof import('~icons/lucide/lo-off')['default']
|
||||
ILucideLucideDatabase: typeof import('~icons/lucide/lucide-database')['default']
|
||||
ILucideMonitor: typeof import('~icons/lucide/monitor')['default']
|
||||
ILucidePackage: typeof import('~icons/lucide/package')['default']
|
||||
ILucidePlug: typeof import('~icons/lucide/plug')['default']
|
||||
ILucidePlugOff: typeof import('~icons/lucide/plug-off')['default']
|
||||
ILucideRadio: typeof import('~icons/lucide/radio')['default']
|
||||
ILucideSave: typeof import('~icons/lucide/save')['default']
|
||||
ILucideSquarePen: typeof import('~icons/lucide/square-pen')['default']
|
||||
ILucideTrash2: typeof import('~icons/lucide/trash2')['default']
|
||||
ILucideUnlink: typeof import('~icons/lucide/unlink')['default']
|
||||
ILucideUnlinkOff: typeof import('~icons/lucide/unlink-off')['default']
|
||||
ILucideWifi: typeof import('~icons/lucide/wifi')['default']
|
||||
ILucideX: typeof import('~icons/lucide/x')['default']
|
||||
ILucideZap: typeof import('~icons/lucide/zap')['default']
|
||||
LoginModal: typeof import('./src/components/common/Modal/LoginModal.vue')['default']
|
||||
PackageAutomatically: typeof import('./src/components/package-automatically/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
4
global.d.ts
vendored
Normal file
4
global.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// declare module '@/*';
|
||||
declare module '@/components/*';
|
||||
declare module '@/views/*';
|
||||
declare module '@/api/*';
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>精华 HMI 自动采集设备</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
3075
package-lock.json
generated
Normal file
3075
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "rd_mes_front_hmi",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.10.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pinia": "^3.0.3",
|
||||
"unplugin-vue-components": "^28.7.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.66",
|
||||
"@types/node": "^24.0.3",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"typescript": "~5.8.3",
|
||||
"unplugin-icons": "^22.2.0",
|
||||
"vite": "^6.3.5",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
9
src/App.vue
Normal file
9
src/App.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<a-config-provider :locale="zhCN">
|
||||
<router-view />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||
</script>
|
||||
23
src/api/modules/check.ts
Normal file
23
src/api/modules/check.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import request from '../request.ts';
|
||||
import {
|
||||
type CheckOrderNumber,
|
||||
type ProcessInfo,
|
||||
} from '../types';
|
||||
|
||||
// 验证工单、产品编码关系是否正确
|
||||
export function checkOrderNumberApi(data: CheckOrderNumber) {
|
||||
return request({
|
||||
url: '/jinghua/mes/work-order-material/verify',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 新增加工信息
|
||||
export function addProcessInfoApi(data: ProcessInfo) {
|
||||
return request({
|
||||
url: '/jinghua/processInfo',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
5
src/api/modules/index.ts
Normal file
5
src/api/modules/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './user';
|
||||
export * from './laser';
|
||||
export * from './station';
|
||||
export * from './check';
|
||||
export * from './system';
|
||||
32
src/api/modules/laser.ts
Normal file
32
src/api/modules/laser.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import request from '../request.ts';
|
||||
import {
|
||||
type LaserCarvingOrderSN,
|
||||
type LaserResult
|
||||
} from '../types';
|
||||
|
||||
// 根据工单、产品编码、拼版数量获取 SN 号码信息
|
||||
export function fetchSNApi(data: LaserCarvingOrderSN) {
|
||||
return request({
|
||||
url: '/laser/carving/getOrderSN',
|
||||
method: 'get',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 测试项目结果上传
|
||||
export function uploadTestResultApi(data: string) {
|
||||
return request({
|
||||
url: '/laser/carving/uploadResult',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 根据工单号、产品编码、接收镭雕结果,更改 SN 状态
|
||||
export function updateSNStatusApi(data: LaserResult) {
|
||||
return request({
|
||||
url: '/laser/carving/updateSNStatus',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
19
src/api/modules/station.ts
Normal file
19
src/api/modules/station.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import request from '../request.ts';
|
||||
|
||||
// 工序过站
|
||||
export function operationStationApi(data: string) {
|
||||
return request({
|
||||
url: '/operationStation',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 包装过站
|
||||
export function packageStationApi(data: string) {
|
||||
return request({
|
||||
url: '/packageStation',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
18
src/api/modules/system.ts
Normal file
18
src/api/modules/system.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import request from '../request';
|
||||
import { type LmsWorkMode } from '../types';
|
||||
|
||||
// 获取 LMS 工作模式
|
||||
export const fetchLmsWorkMode = () => {
|
||||
return request({
|
||||
url: '/jinghua/mes/work-mode',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 更新 LMS 工作模式
|
||||
export const updateLmsWorkMode = (workMode: LmsWorkMode) => {
|
||||
return request({
|
||||
url: `/jinghua/mes/work-mode/${workMode}`,
|
||||
method: 'put',
|
||||
})
|
||||
}
|
||||
41
src/api/modules/user.ts
Normal file
41
src/api/modules/user.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import request from '../request'
|
||||
|
||||
export interface LoginInfo {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
export const login = (data: LoginInfo) => {
|
||||
return request({
|
||||
url: '/user/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export const getUserInfo = (data: any) => {
|
||||
return request({
|
||||
url: '/user/info',
|
||||
method: 'get',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
export const updateUser = (id: string, data: any) => {
|
||||
return request({
|
||||
url: `/user/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (id: string) => {
|
||||
return request({
|
||||
url: `/user/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
68
src/api/request.ts
Normal file
68
src/api/request.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import axios from 'axios';
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
interface ErrCodeMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const errCodeMap: ErrCodeMap = {
|
||||
'401': '认证失败,无法访问系统资源',
|
||||
'403': '当前操作没有权限',
|
||||
'404': '访问资源不存在',
|
||||
'default': '系统未知错误,请反馈给管理员'
|
||||
};
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API as string,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
config.headers = config.headers || {};
|
||||
config.headers['Accept-Language'] = 'zh-CN';
|
||||
// 可选:添加token
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
// 统一处理后端自定义结构 { code, msg, data }
|
||||
const res = response.data;
|
||||
if (res.code === 200) {
|
||||
// 成功,返回data字段
|
||||
return res.data;
|
||||
} else {
|
||||
// 优先使用本地错误码映射
|
||||
const codeStr = String(res.code);
|
||||
const errMsg = errCodeMap[codeStr] || res.msg || errCodeMap['default'];
|
||||
notification.error({
|
||||
message: '请求错误',
|
||||
description: errMsg,
|
||||
});
|
||||
// 可在此处对401等特殊code做处理(如跳转登录)
|
||||
return Promise.reject(res);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
// 网络/服务器错误统一处理
|
||||
notification.error({
|
||||
message: '网络错误',
|
||||
description: error.message || '请求失败',
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default service;
|
||||
16
src/api/types/check.ts
Normal file
16
src/api/types/check.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// 验证工单-物料
|
||||
export interface CheckOrderNumber {
|
||||
workOrderCode: string;
|
||||
materialCode: string;
|
||||
}
|
||||
|
||||
// 加工信息
|
||||
export interface ProcessInfo {
|
||||
workOrderCode: string;
|
||||
productCode: string;
|
||||
employeeCode: string;
|
||||
processName: string;
|
||||
resourceName: string;
|
||||
equipmentCode: string;
|
||||
fixtureCode: string;
|
||||
}
|
||||
4
src/api/types/index.ts
Normal file
4
src/api/types/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './laser';
|
||||
export * from './station';
|
||||
export * from './check';
|
||||
export * from './system';
|
||||
42
src/api/types/laser.ts
Normal file
42
src/api/types/laser.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// 获取 SN 号所需参数
|
||||
export interface LaserCarvingOrderSN {
|
||||
orderNumber: string;
|
||||
itemCode: string;
|
||||
qty: number;
|
||||
}
|
||||
|
||||
// 测试项目上传所需参数
|
||||
export interface commandStringItem {
|
||||
number: '04';
|
||||
// 工号
|
||||
empNo: string;
|
||||
// SN 号
|
||||
snNo: string;
|
||||
// 工序名称
|
||||
operationName: string;
|
||||
// 资源名称
|
||||
resName: string;
|
||||
// 设备编码
|
||||
machineNo: string;
|
||||
// 工治具编码
|
||||
fixtureCode: string;
|
||||
// 测试开始时间
|
||||
testStartTime: string;
|
||||
// 检测项(检测项名称:结果值)
|
||||
testItem1: string;
|
||||
testItem2?: string;
|
||||
testItem3?: string;
|
||||
testItem4?: string;
|
||||
testItem5?: string;
|
||||
testItem6?: string;
|
||||
testItem7?: string;
|
||||
// 测试次数
|
||||
testTimes: number;
|
||||
}
|
||||
|
||||
// 上传镭雕结果所需参数,result 代表镭雕结果,例如:{"SN001":"OK}
|
||||
export interface LaserResult {
|
||||
orderNumber: string;
|
||||
itemCode: string;
|
||||
result: any
|
||||
}
|
||||
39
src/api/types/station.ts
Normal file
39
src/api/types/station.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// 工序过站
|
||||
export interface operationCommandStringItem {
|
||||
number: '05';
|
||||
// 工号
|
||||
empNo: string;
|
||||
// SN 号
|
||||
snNo: string;
|
||||
// 工序名称
|
||||
operationName: string;
|
||||
// 资源名称
|
||||
resName: string;
|
||||
// 设备编码
|
||||
machineNo: string | null;
|
||||
// 工治具编码
|
||||
fixtureCode: string | null;
|
||||
// 测试结果
|
||||
testResult: 'OK' | 'NG';
|
||||
// 不良原因
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// 包装过站
|
||||
export interface packageCommandStringItem {
|
||||
number: '070';
|
||||
// 工号
|
||||
empNo: string;
|
||||
// SN 号
|
||||
snNo: string | string[];
|
||||
// 工序名称
|
||||
operationName: string;
|
||||
// 资源名称
|
||||
resName: string;
|
||||
// 设备编码
|
||||
machineNo: string | null;
|
||||
// 工治具编码
|
||||
fixtureCode: string | null;
|
||||
// 装箱结果
|
||||
packageResult: 'OK';
|
||||
}
|
||||
1
src/api/types/system.ts
Normal file
1
src/api/types/system.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type LmsWorkMode = 0 | 1
|
||||
153
src/assets/styles/_ant-design.scss
Normal file
153
src/assets/styles/_ant-design.scss
Normal file
@@ -0,0 +1,153 @@
|
||||
@use './variables' as *;
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
height: 5vh;
|
||||
min-height: 5vh;
|
||||
|
||||
&.title {
|
||||
height: 6vh;
|
||||
min-height: 6vh;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: $title-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.logList-row {
|
||||
flex: 1 1 auto; // 修改
|
||||
min-height: 0; // 新增
|
||||
|
||||
.logList-col {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logList {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ant-col {
|
||||
border-right: 2px solid #fff;
|
||||
border-bottom: 2px solid #fff;
|
||||
font-size: $content-font-size;
|
||||
|
||||
&.option {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px 3px 3px;
|
||||
}
|
||||
|
||||
&.subtitle,
|
||||
&.text,
|
||||
&.label {
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
line-height: 5vh;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.logList-col {
|
||||
padding: 2px 2px 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn,
|
||||
.ant-btn.ant-btn-sm {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
background-color: #d6d6d6;
|
||||
color: #000;
|
||||
font-size: $content-font-size;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
border: 1px solid #000;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
font-size: 1vw;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(.9);
|
||||
transition: 100ms;
|
||||
background-color: #252525;
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏空列表
|
||||
:deep(.ant-list-empty-text) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 列表
|
||||
.ant-list {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-right: 0 !important;
|
||||
|
||||
.ant-list-item {
|
||||
padding: 4px 12px;
|
||||
color: #fff;
|
||||
font-size: $log-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉框
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
|
||||
:deep(.ant-select-selector) {
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: unset;
|
||||
}
|
||||
:deep(.ant-select-selection-search-input) {
|
||||
height: 100% !important;
|
||||
}
|
||||
:deep(.ant-select-selection-item) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: $content-font-size;
|
||||
color: #adff2f;
|
||||
}
|
||||
:deep(.ant-select-selection-placeholder) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: $content-font-size;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
:deep(.ant-select-suffix svg) {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
14
src/assets/styles/_base.scss
Normal file
14
src/assets/styles/_base.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
/* 整个滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
// display: none;
|
||||
/* 对应纵向滚动条的宽度 */
|
||||
width: 10px;
|
||||
/* 对应横向滚动条的宽度 */
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* 滚动条上的滚动滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #8B8B8B;
|
||||
border-radius: 32px;
|
||||
}
|
||||
4
src/assets/styles/_variables.scss
Normal file
4
src/assets/styles/_variables.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
$title-font-size: clamp(1.5rem, 2vw, 2.4rem);
|
||||
$content-font-size: clamp(1rem, 1.5vw, 1.2rem);
|
||||
$button-font-size: clamp(1rem, 1.5vw, 1.5rem);
|
||||
$log-font-size: 1.1rem;
|
||||
2
src/assets/styles/index.scss
Normal file
2
src/assets/styles/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@forward './base';
|
||||
@forward './variables';
|
||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
322
src/components/common/ExecutionResult/ExecutionResult.vue
Normal file
322
src/components/common/ExecutionResult/ExecutionResult.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="execution-result" ref="executionResultRef">
|
||||
<h4 class="section-title">
|
||||
<i-lucide-activity class="title-icon" />
|
||||
{{ title }}
|
||||
</h4>
|
||||
<div class="log-container" ref="logContainer" @mouseenter="showClearButton = true" @mouseleave="showClearButton = false">
|
||||
<div
|
||||
class="clear-button"
|
||||
:class="{ 'visible': showClearButton }"
|
||||
@click.stop="handleClearClick"
|
||||
title="清空执行结果"
|
||||
>
|
||||
<i-lucide-trash-2 class="clear-icon" />
|
||||
</div>
|
||||
<div v-for="(log, index) in internalLogs" :key="index" class="log-item">
|
||||
{{ log }}
|
||||
</div>
|
||||
<div v-if="internalLogs.length === 0" class="empty-logs">
|
||||
<i-lucide-inbox class="empty-icon" />
|
||||
<span>暂无执行结果</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
// 定义组件属性
|
||||
interface Props {
|
||||
title?: string;
|
||||
logs?: string[];
|
||||
autoScroll?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '执行结果',
|
||||
logs: () => [],
|
||||
autoScroll: true
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
clear: [];
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const showClearButton = ref(false);
|
||||
const logContainer = ref<HTMLElement>();
|
||||
const executionResultRef = ref<HTMLElement>();
|
||||
const internalLogs = ref<string[]>([...props.logs]);
|
||||
|
||||
// 监听外部logs变化
|
||||
watch(() => props.logs, (newLogs) => {
|
||||
internalLogs.value = [...newLogs];
|
||||
|
||||
if (props.autoScroll) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (logContainer.value) {
|
||||
setTimeout(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理清空按钮点击
|
||||
const handleClearClick = () => {
|
||||
// 单次点击即清空日志
|
||||
internalLogs.value = [];
|
||||
emit('clear');
|
||||
resetClickState();
|
||||
};
|
||||
|
||||
// 重置点击状态
|
||||
const resetClickState = () => {
|
||||
showClearButton.value = false;
|
||||
};
|
||||
|
||||
// 全局点击监听,点击其他地方重置状态
|
||||
const handleGlobalClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// 如果点击的不是当前组件内的清空按钮,则重置状态
|
||||
if (executionResultRef.value && !executionResultRef.value.contains(target)) {
|
||||
resetClickState();
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时添加全局监听
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
|
||||
// 初始滚动到底部
|
||||
if (props.autoScroll) {
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时移除全局监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
scrollToBottom
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 变量定义
|
||||
$primary-color: #4a90e2;
|
||||
$primary-light: #5ba0f2;
|
||||
$success-color: #52c41a;
|
||||
$error-color: #ff4d4f;
|
||||
$bg-dark: rgba(0, 0, 0, 0.3);
|
||||
$bg-light: rgba(255, 255, 255, 0.1);
|
||||
$bg-light-hover: rgba(255, 255, 255, 0.2);
|
||||
$border-light: rgba(255, 255, 255, 0.2);
|
||||
$text-white: #ffffff;
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 10px;
|
||||
$border-radius: 4px;
|
||||
|
||||
// 混合器
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin flex-align-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@mixin icon-base($size) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
}
|
||||
|
||||
@mixin transition-ease {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.execution-result {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.section-title {
|
||||
@include flex-align-center;
|
||||
gap: $spacing-sm;
|
||||
margin: 0 0 $spacing-md 0;
|
||||
color: $text-white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
border-bottom: 1px solid $primary-color;
|
||||
padding-bottom: $spacing-sm;
|
||||
position: relative;
|
||||
|
||||
.title-icon {
|
||||
@include icon-base(20px);
|
||||
filter: drop-shadow(0 2px 4px rgba(74, 144, 226, 0.3));
|
||||
}
|
||||
|
||||
.sse-indicator {
|
||||
@include flex-center;
|
||||
@include icon-base(16px);
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
|
||||
.indicator-icon {
|
||||
@include icon-base(16px);
|
||||
color: $error-color;
|
||||
opacity: 0.5;
|
||||
@include transition-ease;
|
||||
}
|
||||
|
||||
&.connected .indicator-icon {
|
||||
color: $success-color;
|
||||
opacity: 1;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: $error-color;
|
||||
border-radius: 50%;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
opacity: 0.5;
|
||||
@include transition-ease;
|
||||
}
|
||||
|
||||
&.connected::after {
|
||||
background-color: $success-color;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-container {
|
||||
flex: 1;
|
||||
background: $bg-dark;
|
||||
border: 1px solid $primary-color;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacing-md;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
position: relative;
|
||||
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
right: $spacing-sm;
|
||||
top: $spacing-sm;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@include flex-center;
|
||||
background: $bg-light;
|
||||
border-radius: $border-radius;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
@include transition-ease;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid $border-light;
|
||||
z-index: 10;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $bg-light-hover;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
@include icon-base(14px);
|
||||
color: $error-color;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
}
|
||||
|
||||
.log-item {
|
||||
color: $success-color;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
@include flex-center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 80px;
|
||||
color: $text-white;
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
|
||||
.empty-icon {
|
||||
@include icon-base(24px);
|
||||
margin-bottom: $spacing-sm;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $bg-light;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $primary-color;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: $primary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
74
src/components/common/Modal/LoginModal.vue
Normal file
74
src/components/common/Modal/LoginModal.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="登录"
|
||||
ok-text="登录"
|
||||
cancel-text="取消"
|
||||
@close="handleClose"
|
||||
@cancel="handleClose"
|
||||
@ok="handleLogin"
|
||||
>
|
||||
<a-form :value="loginForm"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 18 }">
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model:value="loginForm.username" />
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model:value="loginForm.password" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits([
|
||||
'ok',
|
||||
'close',
|
||||
'cancel',
|
||||
'update:modelValue',
|
||||
]);
|
||||
|
||||
const handleLogin = () => {
|
||||
console.log('loginForm: ', loginForm);
|
||||
console.log('ok')
|
||||
handleClose();
|
||||
}
|
||||
|
||||
const visible = ref(props.modelValue);
|
||||
// 关闭模态框
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
emit('update:modelValue', false);
|
||||
};
|
||||
|
||||
// 监听外部变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal;
|
||||
});
|
||||
|
||||
// 监听内部变化
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal !== props.modelValue) {
|
||||
emit('update:modelValue', newVal);
|
||||
}
|
||||
});
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ant-modal-header {
|
||||
margin-bottom: 30px !important;
|
||||
}
|
||||
</style>
|
||||
20
src/components/common/types.ts
Normal file
20
src/components/common/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ExecutionResult组件相关类型定义
|
||||
export interface ExecutionResultProps {
|
||||
title?: string;
|
||||
logs: string[];
|
||||
}
|
||||
|
||||
// 日志项类型
|
||||
export interface LogItem {
|
||||
timestamp?: string;
|
||||
message: string;
|
||||
level?: 'info' | 'success' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
// 扩展的ExecutionResult属性(支持更复杂的日志格式)
|
||||
export interface ExtendedExecutionResultProps {
|
||||
title?: string;
|
||||
logs: LogItem[];
|
||||
maxHeight?: string;
|
||||
showTimestamp?: boolean;
|
||||
}
|
||||
17
src/main.ts
Normal file
17
src/main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createApp } from "vue";
|
||||
// import Antd from "ant-design-vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
// Pinia 状态管理
|
||||
import { createPinia } from "pinia";
|
||||
const pinia = createPinia();
|
||||
|
||||
// Vue Router
|
||||
import router from "./router";
|
||||
|
||||
// 样式文件
|
||||
import "ant-design-vue/dist/reset.css";
|
||||
import "./assets/styles/index.scss";
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(pinia).use(router).mount("#app");
|
||||
31
src/router/index.ts
Normal file
31
src/router/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'IndexV2',
|
||||
component: () => import('@/views/index_v2.vue')
|
||||
},
|
||||
{
|
||||
path: '/index',
|
||||
name: 'Index',
|
||||
component: () => import('@/views/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/package-station',
|
||||
name: 'PackageStation',
|
||||
component: () => import('@/views/PackageStation.vue')
|
||||
},
|
||||
{
|
||||
path: '/sse-test',
|
||||
name: 'SSETest',
|
||||
component: () => import('@/views/sse/sse-test.vue')
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
export default router;
|
||||
30
src/store/user.ts
Normal file
30
src/store/user.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface UserInfo {
|
||||
username: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('token') || '',
|
||||
userInfo: null as UserInfo | null,
|
||||
}),
|
||||
getters: {
|
||||
isLogin: (state) => !!state.token,
|
||||
},
|
||||
actions: {
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
localStorage.setItem('token', token);
|
||||
},
|
||||
setUserInfo(userInfo: UserInfo) {
|
||||
this.userInfo = userInfo;
|
||||
},
|
||||
reset() {
|
||||
this.token = '';
|
||||
this.userInfo = null;
|
||||
localStorage.removeItem('token');
|
||||
},
|
||||
},
|
||||
});
|
||||
75
src/utils/dateUtils.ts
Normal file
75
src/utils/dateUtils.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 时间格式化工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param date 日期对象或时间戳
|
||||
* @param format 格式字符串,默认 'YYYY-MM-DD HH:mm:ss'
|
||||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export function formatDateTime(date: Date | number | string, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||
const d = new Date(date);
|
||||
|
||||
if (isNaN(d.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间字符串
|
||||
* @param format 格式字符串
|
||||
* @returns 当前时间字符串
|
||||
*/
|
||||
export function getCurrentTime(format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||
return formatDateTime(new Date(), format);
|
||||
}
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
/**
|
||||
* 获取实时时间(用于显示)
|
||||
* @param format 格式字符串
|
||||
* @returns 响应式时间字符串
|
||||
*/
|
||||
export function useRealTime(format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
const currentTime = ref(getCurrentTime(format));
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const startTimer = () => {
|
||||
timer = setInterval(() => {
|
||||
currentTime.value = getCurrentTime(format);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const stopTimer = () => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(startTimer);
|
||||
onBeforeUnmount(stopTimer);
|
||||
|
||||
return {
|
||||
currentTime,
|
||||
startTimer,
|
||||
stopTimer
|
||||
};
|
||||
}
|
||||
33
src/utils/useDialog.ts
Normal file
33
src/utils/useDialog.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Dialog控制Hook
|
||||
* @returns visible(显隐状态)、show(显示)、hide(隐藏)、toggle(切换)
|
||||
*/
|
||||
export function useDialog(initialVisible:boolean = false): {
|
||||
visible: import('vue').Ref<boolean>,
|
||||
show: () => void,
|
||||
hide: () => void,
|
||||
toggle: () => void
|
||||
} {
|
||||
const visible = ref(initialVisible);
|
||||
|
||||
const show = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
visible.value = !visible.value;
|
||||
};
|
||||
|
||||
return {
|
||||
visible,
|
||||
show,
|
||||
hide,
|
||||
toggle
|
||||
};
|
||||
}
|
||||
41
src/utils/useLoading.ts
Normal file
41
src/utils/useLoading.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Loading控制Hook
|
||||
* @returns loading(加载状态)、startLoading(开始加载)、stopLoading(停止加载)、withLoading(包装异步函数)
|
||||
*/
|
||||
export function useLoading(initialLoading = false) {
|
||||
const loading = ref(initialLoading);
|
||||
|
||||
const startLoading = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
const stopLoading = () => {
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 包装异步函数,自动控制loading状态
|
||||
* @param fn 异步函数
|
||||
* @returns 包装后的函数
|
||||
*/
|
||||
const withLoading = <T extends (...args: any[]) => Promise<any>>(fn: T): T => {
|
||||
return (async (...args: any[]) => {
|
||||
startLoading();
|
||||
try {
|
||||
const result = await fn(...args);
|
||||
return result;
|
||||
} finally {
|
||||
stopLoading();
|
||||
}
|
||||
}) as T;
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
startLoading,
|
||||
stopLoading,
|
||||
withLoading
|
||||
};
|
||||
}
|
||||
327
src/utils/useSSE.ts
Normal file
327
src/utils/useSSE.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { generateUUID } from './uuidUtils';
|
||||
|
||||
const defaultServerUrl = 'http://192.168.1.100:18081/sse';
|
||||
|
||||
export interface SSEOptions {
|
||||
/** SSE服务器地址 */
|
||||
serverUrl?: string;
|
||||
/** 自动连接 */
|
||||
autoConnect?: boolean;
|
||||
/** 连接超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 是否使用凭证 */
|
||||
withCredentials?: boolean;
|
||||
/** 客户端ID,不传则自动生成 */
|
||||
clientId?: string;
|
||||
/** 消息处理函数 */
|
||||
onMessage?: (data: string) => void;
|
||||
/** 连接成功回调 */
|
||||
onConnect?: () => void;
|
||||
/** 连接错误回调 */
|
||||
onError?: (error: Event | Error) => void;
|
||||
/** 是否在组件挂载时自动连接 */
|
||||
connectOnMount?: boolean;
|
||||
/** 是否在组件卸载时自动断开连接 */
|
||||
disconnectOnUnmount?: boolean;
|
||||
}
|
||||
|
||||
function addSSEListener(eventSource: EventSource, type: string, handler: (data: any, event: MessageEvent) => void) {
|
||||
eventSource.addEventListener(type, (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
handler(data, event);
|
||||
} catch (err) {
|
||||
console.error('解析 SSE 数据失败:', err, event.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE连接管理工具
|
||||
* @param options SSE连接选项
|
||||
*/
|
||||
export function useSSE(options: SSEOptions) {
|
||||
// 默认选项
|
||||
const defaultOptions: SSEOptions = {
|
||||
serverUrl: defaultServerUrl,
|
||||
autoConnect: true,
|
||||
timeout: 10000,
|
||||
withCredentials: false,
|
||||
connectOnMount: true,
|
||||
disconnectOnUnmount: true,
|
||||
clientId: generateUUID()
|
||||
};
|
||||
|
||||
// 合并选项
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
// 状态变量
|
||||
const logs = ref<string[]>([]);
|
||||
const isConnecting = ref(false);
|
||||
const isConnected = ref(false);
|
||||
const clientId = ref(mergedOptions.clientId || generateUUID());
|
||||
const serverUrl = ref(mergedOptions.serverUrl);
|
||||
const isInCooldown = ref(false); // 连接冷却状态
|
||||
const cooldownRemaining = ref(0); // 剩余冷却时间
|
||||
|
||||
// 存储EventSource实例
|
||||
let eventSource: EventSource | null = null;
|
||||
let connectionTimeout: number | null = null;
|
||||
let cooldownTimer: number | null = null;
|
||||
|
||||
// 计算属性
|
||||
const sseStatusText = computed(() => {
|
||||
if (isConnecting.value) return '连接中';
|
||||
if (isConnected.value) return '已连接';
|
||||
return '未连接';
|
||||
});
|
||||
|
||||
const sseStatusClass = computed(() => {
|
||||
if (isConnecting.value) return 'connecting warning';
|
||||
if (isConnected.value) return 'connected success';
|
||||
return 'disconnected error';
|
||||
});
|
||||
|
||||
// 构建包含clientId的连接URL
|
||||
const getConnectUrl = () => {
|
||||
return `${serverUrl.value}/connect`;
|
||||
};
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message: string) => {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const logEntry = `${timestamp} - ${message}`;
|
||||
logs.value.push(logEntry);
|
||||
return logEntry;
|
||||
};
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = () => {
|
||||
logs.value = [];
|
||||
};
|
||||
|
||||
// 连接SSE
|
||||
const connect = () => {
|
||||
// 检查是否在冷却期间
|
||||
if (isInCooldown.value) {
|
||||
addLog(`连接冷却中,请等待 ${cooldownRemaining.value} 秒后重试`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 先断开可能存在的连接
|
||||
if (eventSource) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
isConnecting.value = true;
|
||||
|
||||
try {
|
||||
// 确保URL不为空
|
||||
if (!serverUrl.value) {
|
||||
throw new Error('SSE服务器地址不能为空');
|
||||
}
|
||||
|
||||
const connectUrl = getConnectUrl();
|
||||
addLog(`开始连接到 ${connectUrl}...`);
|
||||
|
||||
// 设置连接超时
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
}
|
||||
|
||||
connectionTimeout = window.setTimeout(() => {
|
||||
if (isConnecting.value) {
|
||||
addLog('连接超时,请检查服务器地址或网络连接');
|
||||
isConnecting.value = false;
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
// 调用错误回调
|
||||
if (mergedOptions.onError) {
|
||||
mergedOptions.onError(new Error('连接超时'));
|
||||
}
|
||||
}
|
||||
}, mergedOptions.timeout || 10000); // 默认10秒超时
|
||||
|
||||
// 创建EventSource
|
||||
const eventSourceOptions = { withCredentials: mergedOptions.withCredentials || false };
|
||||
eventSource = new EventSource(connectUrl, eventSourceOptions);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = null;
|
||||
}
|
||||
|
||||
isConnected.value = true;
|
||||
isConnecting.value = false;
|
||||
addLog('SSE 连接已建立');
|
||||
|
||||
// 调用连接成功回调
|
||||
if (mergedOptions.onConnect) {
|
||||
mergedOptions.onConnect();
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
addLog(`收到消息: ${JSON.parse(event.data).message}`);
|
||||
console.log(event)
|
||||
|
||||
// 调用消息处理回调
|
||||
if (mergedOptions.onMessage) {
|
||||
mergedOptions.onMessage(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
// 改为以下格式
|
||||
addSSEListener(eventSource, 'L4_EVENT', (data) => {
|
||||
console.log('收到 L4_EVENT:', data);
|
||||
});
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = null;
|
||||
}
|
||||
|
||||
addLog('SSE 连接错误,请检查服务器地址或网络连接');
|
||||
isConnected.value = false;
|
||||
isConnecting.value = false;
|
||||
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
// 调用错误回调
|
||||
if (mergedOptions.onError) {
|
||||
mergedOptions.onError(err);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
addLog(`连接失败: ${error}`);
|
||||
isConnecting.value = false;
|
||||
|
||||
// 调用错误回调
|
||||
if (mergedOptions.onError && error instanceof Error) {
|
||||
mergedOptions.onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 断开连接
|
||||
const disconnect = () => {
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = null;
|
||||
}
|
||||
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
addLog('已断开连接');
|
||||
isConnected.value = false;
|
||||
|
||||
// 启动5秒冷却时间
|
||||
startCooldown();
|
||||
};
|
||||
|
||||
// 启动连接冷却时间
|
||||
const startCooldown = () => {
|
||||
isInCooldown.value = true;
|
||||
cooldownRemaining.value = 5;
|
||||
|
||||
const updateCooldown = () => {
|
||||
if (cooldownRemaining.value > 0) {
|
||||
cooldownRemaining.value--;
|
||||
cooldownTimer = window.setTimeout(updateCooldown, 1000);
|
||||
} else {
|
||||
isInCooldown.value = false;
|
||||
cooldownTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
updateCooldown();
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
if (mergedOptions.connectOnMount && mergedOptions.autoConnect) {
|
||||
connect();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (mergedOptions.disconnectOnUnmount) {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 清理函数(兼容旧版本)
|
||||
const cleanup = () => {
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = null;
|
||||
}
|
||||
|
||||
if (cooldownTimer) {
|
||||
clearTimeout(cooldownTimer);
|
||||
cooldownTimer = null;
|
||||
}
|
||||
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
isConnected.value = false;
|
||||
isConnecting.value = false;
|
||||
isInCooldown.value = false;
|
||||
cooldownRemaining.value = 0;
|
||||
};
|
||||
|
||||
// 消息分类处理函数
|
||||
const processMessage = (data: string) => {
|
||||
try {
|
||||
// 尝试解析JSON消息
|
||||
const message = JSON.parse(data);
|
||||
// 根据消息类型分类
|
||||
return {
|
||||
type: message.type || 'system',
|
||||
content: message.content || data,
|
||||
raw: data,
|
||||
timestamp: new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
};
|
||||
} catch (error) {
|
||||
// 非JSON格式消息,视为系统消息
|
||||
return {
|
||||
type: 'system',
|
||||
content: data,
|
||||
raw: data,
|
||||
timestamp: new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
logs,
|
||||
isConnecting,
|
||||
isConnected,
|
||||
clientId,
|
||||
serverUrl,
|
||||
sseStatusText,
|
||||
sseStatusClass,
|
||||
isInCooldown,
|
||||
cooldownRemaining,
|
||||
connect,
|
||||
disconnect,
|
||||
addLog, // SSE内部日志记录
|
||||
clearLogs,
|
||||
cleanup,
|
||||
processMessage // 消息分类处理
|
||||
};
|
||||
}
|
||||
11
src/utils/uuidUtils.ts
Normal file
11
src/utils/uuidUtils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 生成一个随机的UUID v4
|
||||
* @returns 生成的UUID字符串
|
||||
*/
|
||||
export function generateUUID(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
17
tsconfig.app.json
Normal file
17
tsconfig.app.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"composite": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
}
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"composite": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
45
vite.config.ts
Normal file
45
vite.config.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import Components from "unplugin-vue-components/vite"; // 按需组件自动导入
|
||||
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import Icons from 'unplugin-icons/vite';
|
||||
import IconsResolver from 'unplugin-icons/resolver';
|
||||
import path from 'path';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
Components({
|
||||
dts: true, //生成components.d.ts 全局定义文件
|
||||
resolvers: [
|
||||
AntDesignVueResolver({ //对使用到的全局ant design vue组件进行类型导入
|
||||
importStyle: false, // 不动态引入css,这个不强求
|
||||
}),
|
||||
// 自动导入 lucide 图标
|
||||
IconsResolver({
|
||||
prefix: 'i', // 比如用 <i-a-arrow-down />
|
||||
enabledCollections: ['lucide']
|
||||
})
|
||||
],
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.tsx$/], //包含的文件类型
|
||||
}),
|
||||
Icons({
|
||||
autoInstall: true, // 没安装的图标库会自动下载
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user