前后端接口对接
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