前后端接口对接
This commit is contained in:
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" />
|
||||
Reference in New Issue
Block a user