前后端接口对接

This commit is contained in:
2025-09-22 17:44:31 +08:00
commit fb974f1100
44 changed files with 4801 additions and 0 deletions

2
.env.development Normal file
View File

@@ -0,0 +1,2 @@
# 开发环境
VITE_APP_BASE_API = '/api'

24
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
// declare module '@/*';
declare module '@/components/*';
declare module '@/views/*';
declare module '@/api/*';

13
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
})
}

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

@@ -0,0 +1 @@
export type LmsWorkMode = 0 | 1

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

View File

@@ -0,0 +1,14 @@
/* 整个滚动条 */
::-webkit-scrollbar {
// display: none;
/* 对应纵向滚动条的宽度 */
width: 10px;
/* 对应横向滚动条的宽度 */
height: 10px;
}
/* 滚动条上的滚动滑块 */
::-webkit-scrollbar-thumb {
background-color: #8B8B8B;
border-radius: 32px;
}

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

View File

@@ -0,0 +1,2 @@
@forward './base';
@forward './variables';

1
src/assets/vue.svg Normal file
View 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

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

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

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

@@ -0,0 +1 @@
/// <reference types="vite/client" />

17
tsconfig.app.json Normal file
View 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
View 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
View 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
View 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/, '')
}
}
}
})