组件封装
This commit is contained in:
29
components.d.ts
vendored
29
components.d.ts
vendored
@@ -11,11 +11,13 @@ declare module 'vue' {
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||
Actions: typeof import('./src/components/actions/index.vue')['default']
|
||||
ActionButton: typeof import('./src/components/common/ActionButton/ActionButton.vue')['default']
|
||||
ActionButtons: typeof import('./src/components/common/ActionButtons/index.vue')['default']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
@@ -27,19 +29,23 @@ declare module 'vue' {
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
DetectingDevice: typeof import('./src/components/detecting-device/index.vue')['default']
|
||||
ExecutionResult: typeof import('./src/components/common/ExecutionResult/ExecutionResult.vue')['default']
|
||||
CameraModal: typeof import('./src/components/CameraModal/index.vue')['default']
|
||||
CameraStatus: typeof import('./src/components/CameraStatus/index.vue')['default']
|
||||
ExecutionResult: typeof import('./src/components/common/ExecutionResult/index.vue')['default']
|
||||
ILucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||
ILucideCamera: typeof import('~icons/lucide/camera')['default']
|
||||
ILucideClock: typeof import('~icons/lucide/clock')['default']
|
||||
ILucideCpu: typeof import('~icons/lucide/cpu')['default']
|
||||
ILucideDatabase: typeof import('~icons/lucide/database')['default']
|
||||
ILucideEdit: typeof import('~icons/lucide/edit')['default']
|
||||
ILucideFileText: typeof import('~icons/lucide/file-text')['default']
|
||||
ILucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
ILucideLink: typeof import('~icons/lucide/link')['default']
|
||||
ILucideLinkOff: typeof import('~icons/lucide/link-off')['default']
|
||||
ILucideLinOff: typeof import('~icons/lucide/lin-off')['default']
|
||||
ILucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||
ILucideLock: typeof import('~icons/lucide/lock')['default']
|
||||
ILucideLogIn: typeof import('~icons/lucide/log-in')['default']
|
||||
ILucideLoOff: typeof import('~icons/lucide/lo-off')['default']
|
||||
ILucideLucideDatabase: typeof import('~icons/lucide/lucide-database')['default']
|
||||
ILucideMonitor: typeof import('~icons/lucide/monitor')['default']
|
||||
@@ -47,17 +53,30 @@ declare module 'vue' {
|
||||
ILucidePlug: typeof import('~icons/lucide/plug')['default']
|
||||
ILucidePlugOff: typeof import('~icons/lucide/plug-off')['default']
|
||||
ILucideRadio: typeof import('~icons/lucide/radio')['default']
|
||||
ILucideRefreshCw: typeof import('~icons/lucide/refresh-cw')['default']
|
||||
ILucideSave: typeof import('~icons/lucide/save')['default']
|
||||
ILucideShieldCheck: typeof import('~icons/lucide/shield-check')['default']
|
||||
ILucideSquarePen: typeof import('~icons/lucide/square-pen')['default']
|
||||
ILucideTrash2: typeof import('~icons/lucide/trash2')['default']
|
||||
ILucideUnlink: typeof import('~icons/lucide/unlink')['default']
|
||||
ILucideUnlinkOff: typeof import('~icons/lucide/unlink-off')['default']
|
||||
ILucideUser: typeof import('~icons/lucide/user')['default']
|
||||
ILucideWifi: typeof import('~icons/lucide/wifi')['default']
|
||||
ILucideX: typeof import('~icons/lucide/x')['default']
|
||||
ILucideZap: typeof import('~icons/lucide/zap')['default']
|
||||
LoginModal: typeof import('./src/components/common/Modal/LoginModal.vue')['default']
|
||||
PackageAutomatically: typeof import('./src/components/package-automatically/index.vue')['default']
|
||||
MesModal: typeof import('./src/components/MesModal/index.vue')['default']
|
||||
MesStatus: typeof import('./src/components/MesStatus/index.vue')['default']
|
||||
NetworkModal: typeof import('./src/components/NetworkModal/index.vue')['default']
|
||||
NetworkStatus: typeof import('./src/components/NetworkStatus/index.vue')['default']
|
||||
PlcModal: typeof import('./src/components/PlcModal/index.vue')['default']
|
||||
PlcStatus: typeof import('./src/components/PlcStatus/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Settings: typeof import('./src/components/Settings/index.vue')['default']
|
||||
SettingsModal: typeof import('./src/components/SettingsModal/index.vue')['default']
|
||||
SSELogs: typeof import('./src/components/SSELogs/index.vue')['default']
|
||||
SseModal: typeof import('./src/components/SseModal/index.vue')['default']
|
||||
SseStatus: typeof import('./src/components/SseStatus/index.vue')['default']
|
||||
XxxModal: typeof import('./src/components/xxxModal/index.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
27
package-lock.json
generated
27
package-lock.json
generated
@@ -10,7 +10,6 @@
|
||||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.10.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pinia": "^3.0.3",
|
||||
"unplugin-vue-components": "^28.7.0",
|
||||
"vue": "^3.5.13",
|
||||
@@ -18,9 +17,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.66",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^24.0.3",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"js-cookie": "^3.0.5",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"typescript": "~5.8.3",
|
||||
"unplugin-icons": "^22.2.0",
|
||||
@@ -925,6 +928,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/event-source-polyfill": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz",
|
||||
"integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/js-cookie": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
||||
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.3.tgz",
|
||||
@@ -1600,6 +1617,13 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/event-source-polyfill": {
|
||||
"version": "1.0.31",
|
||||
"resolved": "https://registry.npmmirror.com/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz",
|
||||
"integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz",
|
||||
@@ -1902,6 +1926,7 @@
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.10.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pinia": "^3.0.3",
|
||||
"unplugin-vue-components": "^28.7.0",
|
||||
"vue": "^3.5.13",
|
||||
@@ -19,9 +18,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.66",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^24.0.3",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"js-cookie": "^3.0.5",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"typescript": "~5.8.3",
|
||||
"unplugin-icons": "^22.2.0",
|
||||
|
||||
BIN
public/bg.jpg
Normal file
BIN
public/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1004 KiB |
39
src/api/detect/index.ts
Normal file
39
src/api/detect/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import request from '../request';
|
||||
import type {
|
||||
CheckOrderNumber,
|
||||
ProcessInfo,
|
||||
} from './model';
|
||||
|
||||
// 验证工单、产品编码关系是否正确
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// 查询 L1 数据
|
||||
export function listL1DataApi() {
|
||||
return request({
|
||||
url: '/jinghua/l1Data/list',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 查询 L4 数据
|
||||
export function listL4DataApi() {
|
||||
return request({
|
||||
url: '/jinghua/l4Data/list',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// 验证工单-物料
|
||||
export interface CheckOrderNumber {
|
||||
workOrderCode: string;
|
||||
materialCode: string;
|
||||
productCode: string;
|
||||
}
|
||||
|
||||
// 加工信息
|
||||
@@ -1,5 +1,5 @@
|
||||
import request from '../request';
|
||||
import { type LmsWorkMode } from '../types';
|
||||
import type { LmsWorkMode } from './model';
|
||||
|
||||
// 获取 LMS 工作模式
|
||||
export const fetchLmsWorkMode = () => {
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from './user';
|
||||
export * from './laser';
|
||||
export * from './station';
|
||||
export * from './check';
|
||||
export * from './system';
|
||||
@@ -1,32 +0,0 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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'
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { Modal, notification } from 'ant-design-vue';
|
||||
import router from '@/router';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
interface ErrCodeMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const errCodeMap: ErrCodeMap = {
|
||||
'401': '认证失败,无法访问系统资源',
|
||||
const errCodeMap: { [key: string]: string } = {
|
||||
'403': '当前操作没有权限',
|
||||
'404': '访问资源不存在',
|
||||
'default': '系统未知错误,请反馈给管理员'
|
||||
@@ -23,8 +20,8 @@ service.interceptors.request.use(
|
||||
config => {
|
||||
config.headers = config.headers || {};
|
||||
config.headers['Accept-Language'] = 'zh-CN';
|
||||
// 可选:添加token
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
@@ -38,21 +35,28 @@ service.interceptors.request.use(
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
// 统一处理后端自定义结构 { code, msg, data }
|
||||
const res = response.data;
|
||||
if (res.code === 200) {
|
||||
// 成功,返回data字段
|
||||
return res.data;
|
||||
console.log(response)
|
||||
// 未设置状态码则默认成功状态
|
||||
const code = response.data.code || 200;
|
||||
|
||||
if (code === 200) {
|
||||
return response.data ?? response;
|
||||
} else if (code === 401) {
|
||||
Modal.error({
|
||||
title: '系统提示',
|
||||
content: '登录状态已过期,请重新登录',
|
||||
onOk: () => {
|
||||
router.push('/login')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 优先使用本地错误码映射
|
||||
const codeStr = String(res.code);
|
||||
const errMsg = errCodeMap[codeStr] || res.msg || errCodeMap['default'];
|
||||
const codeStr = String(code);
|
||||
const errMsg = errCodeMap[codeStr] || response.data.msg || errCodeMap['default'];
|
||||
notification.error({
|
||||
message: '请求错误',
|
||||
description: errMsg,
|
||||
});
|
||||
// 可在此处对401等特殊code做处理(如跳转登录)
|
||||
return Promise.reject(res);
|
||||
return Promise.reject(response.data);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
|
||||
19
src/api/system/index.ts
Normal file
19
src/api/system/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import request from '../request'
|
||||
import type { LoginInfo } from './model';
|
||||
|
||||
// 用户登录
|
||||
export function login(data: LoginInfo) {
|
||||
return request({
|
||||
url: '/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
export function getCaptcha() {
|
||||
return request({
|
||||
url: '/captchaImage',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
6
src/api/system/model.d.ts
vendored
Normal file
6
src/api/system/model.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface LoginInfo {
|
||||
username: string
|
||||
password: string
|
||||
uuid: string
|
||||
code: string
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './laser';
|
||||
export * from './station';
|
||||
export * from './check';
|
||||
export * from './system';
|
||||
@@ -1,42 +0,0 @@
|
||||
// 获取 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
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// 工序过站
|
||||
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,4 +1,110 @@
|
||||
$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;
|
||||
|
||||
|
||||
$primary-color: #4a90e2;
|
||||
$primary-light: #5ba0f2;
|
||||
$primary-dark: #3a7bd5;
|
||||
$success-color: #52c41a;
|
||||
$success-light: #73d13d;
|
||||
$success-bg: rgba(82, 196, 26, 0.2);
|
||||
$success-bg-hover: rgba(82, 196, 26, 0.4);
|
||||
$warning-color: #faad14;
|
||||
$warning-light: #ffc53d;
|
||||
$warning-bg: rgba(250, 173, 20, 0.2);
|
||||
$warning-bg-hover: rgba(250, 173, 20, 0.4);
|
||||
$error-color: #ff4d4f;
|
||||
$error-light: #ff7875;
|
||||
$error-bg: rgba(255, 77, 79, 0.2);
|
||||
$error-bg-hover: rgba(255, 77, 79, 0.4);
|
||||
|
||||
$white: #ffffff;
|
||||
$text-size: 14px;
|
||||
$text-light: #b8d4f0;
|
||||
$text-dark: #333333;
|
||||
$text-gray: #cccccc;
|
||||
$text-success: #b7eb8f;
|
||||
$text-warning: #ffd666;
|
||||
$text-error: #ffccc7;
|
||||
|
||||
$bg-primary: rgba(74, 144, 226, 0.2);
|
||||
$bg-primary-hover: rgba(74, 144, 226, 0.4);
|
||||
$bg-overlay: rgba(255, 255, 255, 0.1);
|
||||
$bg-overlay-hover: rgba(255, 255, 255, 0.2);
|
||||
$bg-light: rgba(255, 255, 255, 0.1);
|
||||
$bg-light-hover: rgba(255, 255, 255, 0.2);
|
||||
$bg-dark: rgba(0, 0, 0, 0.3);
|
||||
$bg-input: rgba(255, 255, 255, 0.1);
|
||||
$bg-input-focus: rgba(255, 255, 255, 0.15);
|
||||
|
||||
$border-primary: 1px solid $primary-color;
|
||||
$border-light: 1px solid rgba(255, 255, 255, 0.3);
|
||||
$border-light-hover: 1px solid rgba(255, 255, 255, 0.5);
|
||||
$border-transparent: 1px solid transparent;
|
||||
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 12px;
|
||||
$spacing-lg: 15px;
|
||||
$spacing-xl: 20px;
|
||||
$spacing-xxl: 30px;
|
||||
|
||||
$border-radius: 4px;
|
||||
$border-radius-lg: 8px;
|
||||
|
||||
$transition: all 0.3s ease;
|
||||
$font-family: 'Microsoft YaHei', sans-serif;
|
||||
|
||||
// 混合器定义
|
||||
@mixin button-base {
|
||||
border-radius: $border-radius;
|
||||
cursor: pointer;
|
||||
transition: $transition;
|
||||
border: $border-transparent;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
@mixin button-hover($bg-color, $border-color, $text-color) {
|
||||
&:hover {
|
||||
background: $bg-color;
|
||||
border-color: $border-color;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin status-button($bg, $bg-hover, $border, $border-hover, $text, $text-hover) {
|
||||
background: $bg;
|
||||
border-color: $border;
|
||||
color: $text;
|
||||
|
||||
&:hover {
|
||||
background: $bg-hover;
|
||||
border-color: $border-hover;
|
||||
color: $text-hover;
|
||||
box-shadow: 0 0 12px rgba($border, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@mixin transition-ease {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@mixin icon-base($size) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
}
|
||||
108
src/components/CameraStatus/index.vue
Normal file
108
src/components/CameraStatus/index.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="status-item status-button" @click="show">
|
||||
<i-lucide-camera class="status-icon" />
|
||||
<span class="status-label">摄像头状态:</span>
|
||||
<span class="status-value error">{{ cameraStatus }}</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="dialogVisible" :title="modalTitle" :footer="null">
|
||||
<a-row :gutter="[0, 12]">
|
||||
<template v-for="(item, index) in modalItems" :key="index">
|
||||
<a-col :span="8" class="modal-label">{{ item.label }}</a-col>
|
||||
<a-col :span="16" class="modal-value">{{ item.value }}</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useDialog } from '@/utils/useDialog';
|
||||
|
||||
// useDialog管理弹窗状态
|
||||
const { visible: dialogVisible, show, hide } = useDialog();
|
||||
|
||||
const cameraStatus = ref('异常');
|
||||
const modalTitle = '摄像头状态';
|
||||
const modalItems = ref([
|
||||
{ label: '当前状态', value: cameraStatus.value },
|
||||
{ label: '摄像头型号', value: '待赋值' },
|
||||
{ label: 'IP地址', value: '待赋值' },
|
||||
{ label: '分辨率', value: '待赋值' },
|
||||
{ label: '错误信息', value: '待赋值' },
|
||||
{ label: '最后在线', value: '待赋值' }
|
||||
]);
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/_variables.scss';
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.status-button {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #8395B6;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
color: #8395B6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-size: $text-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
109
src/components/MesStatus/index.vue
Normal file
109
src/components/MesStatus/index.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<!-- Status Item -->
|
||||
<div class="status-item status-button" @click="show">
|
||||
<i-lucide-database class="status-icon" />
|
||||
<span class="status-label">MES 登录状态:</span>
|
||||
<span class="status-value success">{{ mesStatus }}</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="dialogVisible" :title="modalTitle" :footer="null">
|
||||
<a-row :gutter="[0, 12]">
|
||||
<template v-for="(item, index) in modalItems" :key="index">
|
||||
<a-col :span="8" class="modal-label">{{ item.label }}</a-col>
|
||||
<a-col :span="16" class="modal-value">{{ item.value }}</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useDialog } from '@/utils/useDialog';
|
||||
|
||||
// useDialog管理弹窗状态
|
||||
const { visible: dialogVisible, show, hide } = useDialog();
|
||||
|
||||
// Modal配置数据
|
||||
const mesStatus = ref('已登录');
|
||||
const modalTitle = ref('MES 登录状态');
|
||||
const modalItems = ref([
|
||||
{ label: 'MES 服务器', value: '192.168.1.100:8080'},
|
||||
{ label: '登录状态', value: mesStatus.value},
|
||||
{ label: '用户名', value: 'admin'},
|
||||
{ label: '连接时间', value: '2024-01-15 09:30:25'},
|
||||
{ label: '最后活动', value: '2024-01-15 14:25:30'}
|
||||
]);
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/_variables.scss';
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.status-button {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #8395B6;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
color: #8395B6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-size: $text-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
121
src/components/NetworkStatus/index.vue
Normal file
121
src/components/NetworkStatus/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="status-item status-button" @click="show">
|
||||
<i-lucide-wifi class="status-icon" />
|
||||
<span class="status-label">网络通讯状态:</span>
|
||||
<span class="status-value success">{{ networkStatus }}</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="dialogVisible" :title="modalTitle" :footer="null">
|
||||
<a-row :gutter="[0, 12]">
|
||||
<template v-for="(item, index) in modalItems" :key="index">
|
||||
<a-col :span="8" class="modal-label">{{ item.label }}</a-col>
|
||||
<a-col :span="16" class="modal-value">{{ item.value }}</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useDialog } from '@/utils/useDialog';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
networkStatus: {
|
||||
type: String,
|
||||
default: '正常'
|
||||
},
|
||||
developingData: {
|
||||
type: String,
|
||||
default: '待赋值'
|
||||
}
|
||||
});
|
||||
|
||||
// useDialog管理弹窗状态
|
||||
const { visible: dialogVisible, show, hide } = useDialog();
|
||||
|
||||
// Modal配置数据
|
||||
const networkStatus = ref(props.networkStatus);
|
||||
const modalTitle = ref('网络通讯状态');
|
||||
const modalItems = ref([
|
||||
{ label: '网络状态', value: networkStatus.value },
|
||||
{ label: 'IP地址', value: '192.168.1.100' },
|
||||
{ label: '子网掩码', value: '255.255.255.0' },
|
||||
{ label: '网关', value: '192.168.1.1' },
|
||||
{ label: 'DNS服务器', value: '8.8.8.8' },
|
||||
{ label: '开发数据', value: props.developingData }
|
||||
]);
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/_variables.scss';
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.status-button {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #8395B6;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
color: #8395B6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-size: $text-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
109
src/components/PlcStatus/index.vue
Normal file
109
src/components/PlcStatus/index.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<!-- Status Item -->
|
||||
<div class="status-item status-button" @click="show">
|
||||
<i-lucide-zap class="status-icon" />
|
||||
<span class="status-label">PLC 通讯状态:</span>
|
||||
<span class="status-value success">{{ plcStatus }}</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="dialogVisible" :title="modalTitle" :footer="null">
|
||||
<a-row :gutter="[0, 12]">
|
||||
<template v-for="(item, index) in modalItems" :key="index">
|
||||
<a-col :span="8" class="modal-label">{{ item.label }}</a-col>
|
||||
<a-col :span="16" class="modal-value">{{ item.value }}</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useDialog } from '@/utils/useDialog';
|
||||
|
||||
// useDialog管理弹窗状态
|
||||
const { visible: dialogVisible, show, hide } = useDialog();
|
||||
|
||||
const plcStatus = ref('正常');
|
||||
const modalTitle = ref('PLC 通讯状态');
|
||||
const modalItems = ref([
|
||||
{ label: 'PLC状态', value: plcStatus.value },
|
||||
{ label: 'PLC型号', value: 'Siemens S7-1200' },
|
||||
{ label: 'IP地址', value: '192.168.1.10' },
|
||||
{ label: '通讯协议', value: 'Modbus TCP' },
|
||||
{ label: '连接时间', value: '2024-01-15 08:30:00' },
|
||||
{ label: '开发数据', value: '' }
|
||||
]);
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/_variables.scss';
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.status-button {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #8395B6;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
color: #8395B6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-size: $text-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
327
src/components/SSELogs/index.vue
Normal file
327
src/components/SSELogs/index.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<div class="sse-logs" ref="sseLogsRef">
|
||||
<div class="sse-status-info">
|
||||
<div class="status-item">
|
||||
<span class="label">当前状态</span>
|
||||
<span class="value" :class="sseStatusClass">{{ sseStatusText }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">客户端ID</span>
|
||||
<span class="value">{{ sseClientId }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">服务器地址</span>
|
||||
<span class="value">{{ sseConnectTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sse-logs-content" ref="logContainer" @mouseenter="showClearButton = true" @mouseleave="showClearButton = false">
|
||||
<div v-for="(log, index) in internalLogs" :key="index" class="log-item" :class="getLogClass(log)">
|
||||
{{ log }}
|
||||
</div>
|
||||
|
||||
<div v-if="internalLogs.length === 0" class="empty-logs">
|
||||
<i-lucide-radio class="empty-icon" />
|
||||
<span>暂无SSE连接日志</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
// 定义组件属性
|
||||
interface Props {
|
||||
logs?: string[];
|
||||
sseStatusText?: string;
|
||||
sseStatusClass?: string;
|
||||
sseClientId?: string;
|
||||
sseConnectTime?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
logs: () => [],
|
||||
sseStatusText: '未连接',
|
||||
sseStatusClass: 'disconnected',
|
||||
sseClientId: '未分配',
|
||||
sseConnectTime: '未连接'
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
clear: [];
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const showClearButton = ref(false);
|
||||
const logContainer = ref<HTMLElement>();
|
||||
const sseLogsRef = ref<HTMLElement>();
|
||||
const internalLogs = ref<string[]>([...props.logs]);
|
||||
|
||||
// 监听外部logs变化
|
||||
watch(() => props.logs, (newLogs) => {
|
||||
internalLogs.value = [...newLogs];
|
||||
|
||||
scrollToBottom();
|
||||
}, { deep: true });
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (logContainer.value) {
|
||||
nextTick(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取日志样式类
|
||||
const getLogClass = (log: string) => {
|
||||
if (log.includes('错误') || log.includes('失败') || log.includes('Error')) {
|
||||
return 'error';
|
||||
}
|
||||
if (log.includes('警告') || log.includes('Warning')) {
|
||||
return 'warning';
|
||||
}
|
||||
if (log.includes('连接') || log.includes('成功') || log.includes('Success')) {
|
||||
return 'success';
|
||||
}
|
||||
return 'info';
|
||||
};
|
||||
|
||||
// 处理清空按钮点击
|
||||
const handleClearClick = () => {
|
||||
internalLogs.value = [];
|
||||
emit('clear');
|
||||
resetClickState();
|
||||
};
|
||||
|
||||
// 重置点击状态
|
||||
const resetClickState = () => {
|
||||
showClearButton.value = false;
|
||||
};
|
||||
|
||||
// 全局点击监听
|
||||
const handleGlobalClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (sseLogsRef.value && !sseLogsRef.value.contains(target)) {
|
||||
resetClickState();
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时添加全局监听
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
// 组件卸载时移除全局监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
scrollToBottom
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 变量定义
|
||||
$primary-color: #4a90e2;
|
||||
$primary-light: #5ba0f2;
|
||||
$success-color: #52c41a;
|
||||
$warning-color: #faad14;
|
||||
$error-color: #ff4d4f;
|
||||
$bg-dark: #283D62;
|
||||
$bg-light: rgba(255, 255, 255, 0.1);
|
||||
$bg-light-hover: rgba(255, 255, 255, 0.2);
|
||||
$border-light: rgba(255, 255, 255, 0.3);
|
||||
$text-white: #ffffff;
|
||||
$text-gray: #cccccc;
|
||||
$text-dark: #333333;
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 12px;
|
||||
$spacing-lg: 16px;
|
||||
$border-radius: 6px;
|
||||
|
||||
// 混合器
|
||||
@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;
|
||||
}
|
||||
|
||||
.sse-logs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.sse-status-info {
|
||||
background: $bg-light;
|
||||
border: 1px solid $border-light;
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: $spacing-md;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.status-item {
|
||||
@include flex-align-center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $text-dark;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $text-dark;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
&.connected {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.connecting {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sse-logs-content {
|
||||
flex: 1;
|
||||
background: $bg-dark;
|
||||
border: 1px solid $border-light;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacing-md;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
min-height: 200px;
|
||||
max-height: 200px;
|
||||
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
right: $spacing-sm;
|
||||
top: $spacing-sm;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@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;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $bg-light-hover;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
@include icon-base(18px);
|
||||
color: $error-color;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
}
|
||||
|
||||
.log-item {
|
||||
color: $text-white;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 3px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
@include flex-center;
|
||||
flex-direction: column;
|
||||
height: calc(200px - 2 * $spacing-md - 2px);
|
||||
color: $text-gray;
|
||||
opacity: 0.6;
|
||||
font-size: 13px;
|
||||
|
||||
.empty-icon {
|
||||
@include icon-base(32px);
|
||||
margin-bottom: $spacing-sm;
|
||||
color: $text-gray;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
&::-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
76
src/components/SettingsModal/index.vue
Normal file
76
src/components/SettingsModal/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="right-info" @click="show">
|
||||
<span class="time-info">{{ currentTime }}</span>
|
||||
<span class="date-info">{{ currentDate }}</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="dialogVisible" :title="modalTitle" :footer="null">
|
||||
<a-row :gutter="[0, 12]">
|
||||
<template v-for="(item, index) in modalItems" :key="index">
|
||||
<a-col :span="8" class="modal-label">{{ item.label }}</a-col>
|
||||
<a-col :span="16" class="modal-value">{{ item.value }}</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useDialog } from '@/utils/useDialog';
|
||||
import { useRealTime } from '@/utils/dateUtils';
|
||||
|
||||
// 实时时间
|
||||
const { currentTime } = useRealTime('HH:mm:ss');
|
||||
const currentDate = ref('');
|
||||
|
||||
const now = new Date();
|
||||
currentDate.value = `${now.getFullYear()}年${String(now.getMonth() + 1).padStart(2, '0')}月${String(now.getDate()).padStart(2, '0')}日`;
|
||||
|
||||
|
||||
const { visible: dialogVisible, show, hide } = useDialog();
|
||||
|
||||
const modalTitle = '系统设置';
|
||||
const modalItems = ref([
|
||||
{ label: '系统版本', value: 'v1.0.0' },
|
||||
{ label: '运行环境', value: '生产环境' },
|
||||
{ label: '数据库状态', value: '正常' },
|
||||
{ label: '运行时长', value: '24小时30分钟' },
|
||||
{ label: '内存使用', value: '2.1GB / 8GB' }
|
||||
]);
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/_variables.scss';
|
||||
|
||||
.modal-label {
|
||||
font-size: $text-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.right-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
padding: 0 $spacing-md;
|
||||
@include button-base;
|
||||
@include button-hover($bg-overlay, rgba(255, 255, 255, 0.2), $white);
|
||||
|
||||
.time-info {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.date-info {
|
||||
font-size: 12px;
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
360
src/components/SseStatus/index.vue
Normal file
360
src/components/SseStatus/index.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="status-item status-button" @click="show">
|
||||
<i-lucide-radio class="status-icon" />
|
||||
<span class="status-label">SSE 连接状态:</span>
|
||||
<span class="status-value" :class="sseStatusClass">{{ sseStatus }}</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="dialogVisible" :title="modalTitle" :footer="null" width="600px">
|
||||
<a-row :gutter="[0, 12]">
|
||||
<template v-for="(item, index) in modalItems" :key="index">
|
||||
<a-col :span="8" class="modal-label">{{ item.label }}</a-col>
|
||||
<a-col :span="16" class="modal-value"
|
||||
:class="item.label === '当前状态' ? sseStatusClass : null">
|
||||
{{ item.value }}
|
||||
</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
<div class="sse-controls">
|
||||
<a-button type="primary" @click="connect" v-if="!isConnected"
|
||||
:loading="isConnecting" :disabled="isInCooldown">
|
||||
{{ isConnecting ? '连接中...' : (isInCooldown ? `冷却中 (${cooldownRemaining}s)` : '连接') }}
|
||||
</a-button>
|
||||
<a-button danger @click="sseDisconnect" v-else>
|
||||
断开连接
|
||||
</a-button>
|
||||
<a-button @click="handleClearSseLogs" class="clear-btn">
|
||||
<i-lucide-trash-2 />
|
||||
清空日志
|
||||
</a-button>
|
||||
<a-button @click="test" class="clear-btn">
|
||||
<i-lucide-trash-2 />
|
||||
测试
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="sse-logs" ref="logContainer">
|
||||
<div v-for="(log, index) in logs" :key="index" class="log-item" :class="log.type">
|
||||
{{ log.message }}
|
||||
</div>
|
||||
|
||||
<div v-if="logs.length === 0" class="empty-logs">
|
||||
<i-lucide-radio class="empty-icon" />
|
||||
<span>暂无SSE连接日志</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import { useDialog } from '@/utils/useDialog';
|
||||
import { useSSE } from '@/utils/useSSE';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
onL1Event: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onL4Event: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onMesEvent: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onSseMessage: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
// useDialog管理弹窗状态
|
||||
const { visible: dialogVisible, show, hide } = useDialog();
|
||||
|
||||
// 冷却时间管理
|
||||
const isInCooldown = ref(false);
|
||||
const cooldownRemaining = ref(0);
|
||||
let cooldownTimer: number | null = null;
|
||||
|
||||
// 内部日志管理
|
||||
const logContainer = ref();
|
||||
function test() {
|
||||
addSseLog('测试日志');
|
||||
}
|
||||
// 使用SSE工具函数
|
||||
const {
|
||||
clientId,
|
||||
serverUrl,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
sseStatusText,
|
||||
sseStatusClass,
|
||||
logs,
|
||||
connect,
|
||||
disconnect,
|
||||
} = useSSE({
|
||||
onMessage: (data) => {
|
||||
addSseLog(data);
|
||||
// 调用父组件传入的消息处理函数
|
||||
if (props.onSseMessage) {
|
||||
props.onSseMessage(data);
|
||||
}
|
||||
},
|
||||
onL1Event: (data) => {
|
||||
if (props.onL1Event) {
|
||||
props.onL1Event(data);
|
||||
}
|
||||
},
|
||||
onL4Event: (data) => {
|
||||
if (props.onL4Event) {
|
||||
props.onL4Event(data);
|
||||
}
|
||||
},
|
||||
onMesEvent: (data) => {
|
||||
if (props.onMesEvent) {
|
||||
props.onMesEvent(data);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('SSE 连接错误:', error);
|
||||
addSseLog('连接错误', 'error');
|
||||
},
|
||||
onConnect: () => {
|
||||
addSseLog('已连接到服务器', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
const sseStatus = ref(sseStatusText.value);
|
||||
const modalTitle = ref('SSE 连接状态');
|
||||
const modalItems = ref([
|
||||
{ label: '当前状态', value: sseStatus.value },
|
||||
{ label: '客户端ID', value: clientId || 'hmi-main-client' },
|
||||
{ label: '服务器地址', value: serverUrl.value }
|
||||
]);
|
||||
|
||||
// 启动连接冷却时间
|
||||
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();
|
||||
};
|
||||
|
||||
// 包装断开函数,添加冷却逻辑
|
||||
const sseDisconnect = () => {
|
||||
disconnect();
|
||||
startCooldown();
|
||||
};
|
||||
|
||||
// 添加 SSE 日志函数
|
||||
const addSseLog = (message: string, type: 'success' | 'error' | 'log' = 'log') => {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const logEntry = `${timestamp} - ${message}`;
|
||||
logs.value.push({ message: logEntry, type });
|
||||
|
||||
return logEntry;
|
||||
};
|
||||
|
||||
// 处理清空日志
|
||||
const handleClearSseLogs = () => {
|
||||
logs.value = [];
|
||||
};
|
||||
|
||||
// 监听日志变化,自动滚动
|
||||
watch(() => logs.value.length, () => {
|
||||
const { scrollTop = 0, scrollHeight = 0, clientHeight = 0 } = logContainer.value;
|
||||
const isUserAtBottom = scrollTop + clientHeight >= scrollHeight - 10;
|
||||
nextTick(() => {
|
||||
if (logContainer.value && isUserAtBottom) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 暴露方法和数据
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
logs
|
||||
});
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
connect();
|
||||
});
|
||||
|
||||
// 清理
|
||||
onBeforeUnmount(() => {
|
||||
if (cooldownTimer) {
|
||||
clearTimeout(cooldownTimer);
|
||||
cooldownTimer = null;
|
||||
}
|
||||
disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/_variables.scss';
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.status-button {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #8395B6;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
color: #8395B6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-size: $text-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sse-controls {
|
||||
margin: $spacing-md 0;
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
@include transition-ease;
|
||||
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sse-logs {
|
||||
flex: 1;
|
||||
background-color: #283D63;
|
||||
border: 1px solid $border-light;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacing-md;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
min-height: 200px;
|
||||
max-height: 200px;
|
||||
|
||||
.log-item {
|
||||
color: $white;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 3px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
@include flex-center;
|
||||
flex-direction: column;
|
||||
height: calc(200px - 2 * $spacing-md - 2px);
|
||||
color: $text-gray;
|
||||
opacity: 0.6;
|
||||
font-size: 13px;
|
||||
|
||||
.empty-icon {
|
||||
@include icon-base(32px);
|
||||
margin-bottom: $spacing-sm;
|
||||
color: $text-gray;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
&::-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
394
src/components/common/ActionButtons/index.vue
Normal file
394
src/components/common/ActionButtons/index.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div class="bottom-actions">
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-control left" v-if="showPagination">
|
||||
<div class="pagination-btn-wrapper">
|
||||
<a-button :disabled="currentPage === 1" @click="goToPrevPage" class="pagination-btn prev-btn">
|
||||
<template #icon>
|
||||
<LeftOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<div class="page-tooltip" v-if="currentPage > 1">
|
||||
{{ currentPage - 1 }} / {{ totalPages }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons-container">
|
||||
<a-row :gutter="16" justify="center">
|
||||
<a-col v-for="button in currentPageButtons" :key="button.label">
|
||||
<a-button :type="button.type" size="large" @click="executeButtonAction(button.handler)"
|
||||
:class="['action-button', getButtonStatusClass(button)]">
|
||||
<component :is="getButtonStatusIcon(button)" v-if="getButtonStatusIcon(button)"
|
||||
class="status-icon-btn" />
|
||||
{{ button.label }}
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<div class="pagination-control right" v-if="showPagination">
|
||||
<div class="pagination-btn-wrapper">
|
||||
<a-button :disabled="currentPage === totalPages" @click="goToNextPage" class="pagination-btn next-btn">
|
||||
<template #icon>
|
||||
<RightOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<div class="page-tooltip" v-if="currentPage < totalPages">
|
||||
{{ currentPage + 1 }} / {{ totalPages }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
type ButtonType = 'primary' | 'default' | 'dashed' | 'text' | 'link';
|
||||
|
||||
interface ActionButton {
|
||||
label: string;
|
||||
handler: string;
|
||||
type: ButtonType;
|
||||
status?: 'running' | 'paused' | 'idle';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
buttons: ActionButton[];
|
||||
buttonsPerPage?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'execute', handlerName: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
buttonsPerPage: 6
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 底部按钮分页控制
|
||||
const currentPage = ref(1);
|
||||
const buttonsPerPage = ref(props.buttonsPerPage);
|
||||
|
||||
// 获取按钮状态样式类
|
||||
const getButtonStatusClass = (button: ActionButton) => {
|
||||
const statusClasses = {
|
||||
running: 'status-running',
|
||||
paused: 'status-paused',
|
||||
idle: 'status-idle'
|
||||
};
|
||||
return statusClasses[button.status || 'idle'];
|
||||
};
|
||||
|
||||
// 获取按钮状态图标
|
||||
const getButtonStatusIcon = (button: ActionButton) => {
|
||||
const statusIcons = {
|
||||
running: 'i-play-circle',
|
||||
paused: 'i-pause-circle',
|
||||
idle: ''
|
||||
};
|
||||
return statusIcons[button.status || 'idle'];
|
||||
};
|
||||
|
||||
// 计算当前页显示的按钮
|
||||
const currentPageButtons = computed(() => {
|
||||
const start = (currentPage.value - 1) * buttonsPerPage.value;
|
||||
const end = start + buttonsPerPage.value;
|
||||
return props.buttons.slice(start, end);
|
||||
});
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(props.buttons.length / buttonsPerPage.value);
|
||||
});
|
||||
|
||||
// 是否显示分页控制
|
||||
const showPagination = computed(() => {
|
||||
return totalPages.value > 1;
|
||||
});
|
||||
|
||||
// 分页控制函数
|
||||
const goToPrevPage = () => {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const goToNextPage = () => {
|
||||
if (currentPage.value < totalPages.value) {
|
||||
currentPage.value++;
|
||||
}
|
||||
};
|
||||
|
||||
// 执行按钮操作
|
||||
const executeButtonAction = (handlerName: string) => {
|
||||
emit('execute', handlerName);
|
||||
};
|
||||
|
||||
// 响应式调整按钮数量
|
||||
const updateButtonsPerPage = () => {
|
||||
nextTick(() => {
|
||||
const buttonsContainer = document.querySelector('.buttons-container') as HTMLElement;
|
||||
if (!buttonsContainer) {
|
||||
buttonsPerPage.value = props.buttonsPerPage; // 默认值
|
||||
return;
|
||||
}
|
||||
|
||||
const containerWidth = buttonsContainer.offsetWidth;
|
||||
const buttonWidth = 100; // 单个操作按钮最小宽度
|
||||
const buttonGap = 16; // 按钮间距
|
||||
|
||||
// 计算可以放置的按钮数量
|
||||
const maxButtons = Math.floor((containerWidth + buttonGap) / (buttonWidth + buttonGap));
|
||||
|
||||
// 设置最小和最大按钮数量
|
||||
buttonsPerPage.value = Math.max(3, maxButtons);
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 初始化按钮数量
|
||||
updateButtonsPerPage();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', updateButtonsPerPage);
|
||||
});
|
||||
|
||||
// 清理事件监听
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateButtonsPerPage);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 变量定义
|
||||
$primary-color: #4a90e2;
|
||||
$primary-light: #5ba0f2;
|
||||
$success-color: #52c41a;
|
||||
$success-light: #73d13d;
|
||||
$success-bg: rgba(82, 196, 26, 0.1);
|
||||
$success-bg-hover: rgba(82, 196, 26, 0.2);
|
||||
$warning-color: #faad14;
|
||||
$warning-light: #ffc53d;
|
||||
$warning-bg: rgba(250, 173, 20, 0.1);
|
||||
$warning-bg-hover: rgba(250, 173, 20, 0.2);
|
||||
$error-color: #ff4d4f;
|
||||
$error-light: #ff7875;
|
||||
$bg-dark: rgba(0, 0, 0, 0.4);
|
||||
$bg-overlay: rgba(255, 255, 255, 0.1);
|
||||
$bg-overlay-hover: rgba(255, 255, 255, 0.2);
|
||||
$text-light: #cccccc;
|
||||
$text-success: #52c41a;
|
||||
$text-warning: #faad14;
|
||||
$white: #ffffff;
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 12px;
|
||||
$spacing-lg: 16px;
|
||||
$border-radius: 6px;
|
||||
$border-radius-lg: 8px;
|
||||
$transition: all 0.3s ease;
|
||||
|
||||
// 混合器
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin button-base {
|
||||
border-radius: $border-radius;
|
||||
font-weight: 500;
|
||||
transition: $transition;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部操作按钮区域 */
|
||||
.bottom-actions {
|
||||
height: 80px;
|
||||
background: $bg-dark;
|
||||
border-top: 2px solid $primary-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
@include flex-center;
|
||||
width: 100%;
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
|
||||
.pagination-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-btn-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
width: 35px;
|
||||
height: 50px;
|
||||
border-radius: $border-radius-lg;
|
||||
@include flex-center;
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
border: 1px solid $primary-color;
|
||||
color: $text-light;
|
||||
transition: $transition;
|
||||
|
||||
&:hover {
|
||||
background: rgba(74, 144, 226, 0.2);
|
||||
border-color: $primary-light;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $bg-overlay;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.page-tooltip {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: $white;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $border-radius;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1000;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn-wrapper:hover .page-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
flex: 1;
|
||||
@include flex-center;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
min-width: 100px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
@include button-base;
|
||||
}
|
||||
|
||||
/* 按钮状态样式 */
|
||||
.action-button {
|
||||
&.status-running {
|
||||
background: $success-bg !important;
|
||||
border-color: $success-color !important;
|
||||
color: $text-success !important;
|
||||
box-shadow: 0 0 8px rgba(82, 196, 26, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: $success-bg-hover !important;
|
||||
border-color: $success-light !important;
|
||||
color: $white !important;
|
||||
box-shadow: 0 0 12px rgba(82, 196, 26, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.status-paused {
|
||||
background: $warning-bg !important;
|
||||
border-color: $warning-color !important;
|
||||
color: $text-warning !important;
|
||||
box-shadow: 0 0 8px rgba(250, 173, 20, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: $warning-bg-hover !important;
|
||||
border-color: $warning-light !important;
|
||||
color: $white !important;
|
||||
box-shadow: 0 0 12px rgba(250, 173, 20, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.status-idle {
|
||||
background: $bg-overlay;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: $text-light;
|
||||
|
||||
&:hover {
|
||||
background: $bg-overlay-hover;
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon-btn {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1366px) {
|
||||
.bottom-actions .ant-btn {
|
||||
min-width: 90px;
|
||||
height: 36px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.bottom-actions {
|
||||
height: 70px;
|
||||
|
||||
.ant-row {
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
min-width: 80px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="execution-result" ref="executionResultRef">
|
||||
<div class="execution-result">
|
||||
<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="log-container" ref="logContainer">
|
||||
<div
|
||||
class="clear-button"
|
||||
:class="{ 'visible': showClearButton }"
|
||||
@click.stop="handleClearClick"
|
||||
title="清空执行结果"
|
||||
v-if="internalLogs.length !== 0"
|
||||
>
|
||||
<i-lucide-trash-2 class="clear-icon" />
|
||||
</div>
|
||||
<div v-for="(log, index) in internalLogs" :key="index" class="log-item">
|
||||
<div v-for="(log, index) in internalLogs" :key="index" class="log-item" :class="{'error': log.includes('错误'), 'warning': log.includes('警告')}">
|
||||
{{ log }}
|
||||
</div>
|
||||
<div v-if="internalLogs.length === 0" class="empty-logs">
|
||||
@@ -25,19 +25,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import type { ExecutionResultProps } from './types';
|
||||
|
||||
// 定义组件属性
|
||||
interface Props {
|
||||
title?: string;
|
||||
logs?: string[];
|
||||
autoScroll?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<ExecutionResultProps>(), {
|
||||
title: '执行结果',
|
||||
logs: () => [],
|
||||
autoScroll: true
|
||||
logs: () => []
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
@@ -46,18 +39,13 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
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 });
|
||||
|
||||
// 滚动到底部
|
||||
@@ -73,53 +61,16 @@ const scrollToBottom = () => {
|
||||
|
||||
// 处理清空按钮点击
|
||||
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;
|
||||
$warning-color: #faad14;
|
||||
$error-color: #ff4d4f;
|
||||
$bg-dark: rgba(0, 0, 0, 0.3);
|
||||
$bg-light: rgba(255, 255, 255, 0.1);
|
||||
@@ -225,12 +176,17 @@ $border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
position: relative;
|
||||
|
||||
&:hover .clear-button {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
right: $spacing-sm;
|
||||
top: $spacing-sm;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@include flex-center;
|
||||
background: $bg-light;
|
||||
border-radius: $border-radius;
|
||||
@@ -242,19 +198,12 @@ $border-radius: 4px;
|
||||
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);
|
||||
@include icon-base(18px);
|
||||
color: $error-color;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
@@ -266,6 +215,14 @@ $border-radius: 4px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
@@ -1,7 +1,7 @@
|
||||
// ExecutionResult组件相关类型定义
|
||||
export interface ExecutionResultProps {
|
||||
title?: string;
|
||||
logs: string[];
|
||||
logs?: string[];
|
||||
}
|
||||
|
||||
// 日志项类型
|
||||
@@ -1,74 +0,0 @@
|
||||
<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>
|
||||
37
src/locales/zh-CN/L4Data.json
Normal file
37
src/locales/zh-CN/L4Data.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "主键",
|
||||
"processInfoId": "加工信息ID",
|
||||
"pltNo": "PLT No",
|
||||
"processF1": "加工F1",
|
||||
"processF2": "加工F2",
|
||||
"goodProductF1": "良品F1",
|
||||
"goodProductF2": "良品F2",
|
||||
"electricalResult": "电气检测结果",
|
||||
"engraveResult": "印字检测结果",
|
||||
"qrCode": "二维码",
|
||||
"qrCodeLevel": "二维码等级",
|
||||
"pressure15Riveting": "压力1_5#_铆接",
|
||||
"height15Riveting": "高度1_5#_铆接",
|
||||
"pressure25Magnet1": "压力2_5#_磁石1",
|
||||
"height25Magnet1": "高度2_5#_磁石1",
|
||||
"pressure36Magnet2": "压力3_6#_磁石2",
|
||||
"height36Magnet2": "高度3_6#_磁石2",
|
||||
"torque47AxisInsert": "扭矩4_7#_轴旋入",
|
||||
"height47AxisInsert": "高度4_7#_轴旋入",
|
||||
"pressure58LowerCase": "压力5_8#_下壳装入",
|
||||
"height58LowerCase": "高度5_8#_下壳装入",
|
||||
"pressure69UpperCase": "压力6_9#_上壳装入",
|
||||
"height69UpperCase": "高度6_9#_上壳装入",
|
||||
"height79HeightCheck": "高度7_9#_高度检测",
|
||||
"pressure79Laser": "压力7_9#_激光",
|
||||
"height89Laser": "高度8_9#_激光",
|
||||
"value19DcrUpper": "数値1_9#_DCR(上)",
|
||||
"value29DcrLower": "数値2_9#_DCR(下)",
|
||||
"value39LcrUpperLs": "数値3_9#_LCR(上)LS",
|
||||
"value49LcrLowerQ": "数値4_9#_LCR(下)Q",
|
||||
"value59LcrLowerLs": "数値5_9#_LCR(下)LS",
|
||||
"value69LcrLowerQ": "数値6_9#_LCR(下)Q",
|
||||
"value79IrR": "数値7_9#_IR R",
|
||||
"value89IrI": "数値8_9#_IR I",
|
||||
"createTime": "创建时间"
|
||||
}
|
||||
@@ -3,23 +3,18 @@ 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: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login.vue')
|
||||
},
|
||||
{
|
||||
path: '/sse-test',
|
||||
name: 'SSETest',
|
||||
component: () => import('@/views/sse/sse-test.vue')
|
||||
path: '/package-station',
|
||||
name: 'PackageStation',
|
||||
component: () => import('@/views/package-station/index.vue')
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
15
src/utils/auth.ts
Normal file
15
src/utils/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const TokenKey = 'Admin-Token'
|
||||
|
||||
export function getToken() {
|
||||
return Cookies.get(TokenKey)
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
return Cookies.set(TokenKey, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return Cookies.remove(TokenKey)
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* 时间格式化工具函数
|
||||
*/
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
@@ -40,8 +38,6 @@ export function getCurrentTime(format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||
return formatDateTime(new Date(), format);
|
||||
}
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
/**
|
||||
* 获取实时时间(用于显示)
|
||||
* @param format 格式字符串
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { generateUUID } from './uuidUtils';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
const defaultServerUrl = 'http://192.168.1.100:18081/sse';
|
||||
const defaultServerUrl = 'http://192.168.1.38:18081/sse';
|
||||
|
||||
interface EventData {
|
||||
timestamp: string;
|
||||
message: string;
|
||||
data: any;
|
||||
code: 200 | 201 | 300 | 400 | 500;
|
||||
}
|
||||
|
||||
interface logItem {
|
||||
message: string;
|
||||
type?: "success" | "error" | "log";
|
||||
}
|
||||
|
||||
export interface SSEOptions {
|
||||
/** SSE服务器地址 */
|
||||
serverUrl?: string;
|
||||
/** 自动连接 */
|
||||
autoConnect?: boolean;
|
||||
/** 连接超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 是否使用凭证 */
|
||||
withCredentials?: boolean;
|
||||
/** 客户端ID,不传则自动生成 */
|
||||
clientId?: string;
|
||||
/** 消息处理函数 */
|
||||
@@ -20,10 +30,12 @@ export interface SSEOptions {
|
||||
onConnect?: () => void;
|
||||
/** 连接错误回调 */
|
||||
onError?: (error: Event | Error) => void;
|
||||
/** 是否在组件挂载时自动连接 */
|
||||
connectOnMount?: boolean;
|
||||
/** 是否在组件卸载时自动断开连接 */
|
||||
disconnectOnUnmount?: boolean;
|
||||
/** L1事件处理回调 */
|
||||
onL1Event?: (data: EventData) => void;
|
||||
/** L4事件处理回调 */
|
||||
onL4Event?: (data: EventData) => void;
|
||||
/** MES事件处理回调 */
|
||||
onMesEvent?: (data: EventData) => void;
|
||||
}
|
||||
|
||||
function addSSEListener(eventSource: EventSource, type: string, handler: (data: any, event: MessageEvent) => void) {
|
||||
@@ -42,32 +54,16 @@ function addSSEListener(eventSource: EventSource, type: string, handler: (data:
|
||||
* @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 logs = ref<logItem[]>([]);
|
||||
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); // 剩余冷却时间
|
||||
const clientId = ref(options.clientId || generateUUID());
|
||||
const serverUrl = ref(options.serverUrl || defaultServerUrl);
|
||||
|
||||
// 存储EventSource实例
|
||||
let eventSource: EventSource | null = null;
|
||||
let connectionTimeout: number | null = null;
|
||||
let cooldownTimer: number | null = null;
|
||||
|
||||
// 计算属性
|
||||
const sseStatusText = computed(() => {
|
||||
@@ -82,32 +78,16 @@ export function useSSE(options: SSEOptions) {
|
||||
return 'disconnected error';
|
||||
});
|
||||
|
||||
// 构建包含clientId的连接URL
|
||||
const getConnectUrl = () => {
|
||||
return `${serverUrl.value}/connect`;
|
||||
};
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message: string) => {
|
||||
const addLog = (message: string, type: 'success' | 'error' | 'log' = 'log') => {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const logEntry = `${timestamp} - ${message}`;
|
||||
logs.value.push(logEntry);
|
||||
logs.value.push({ message: logEntry, type });
|
||||
return logEntry;
|
||||
};
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = () => {
|
||||
logs.value = [];
|
||||
};
|
||||
|
||||
// 连接SSE
|
||||
const connect = () => {
|
||||
// 检查是否在冷却期间
|
||||
if (isInCooldown.value) {
|
||||
addLog(`连接冷却中,请等待 ${cooldownRemaining.value} 秒后重试`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 先断开可能存在的连接
|
||||
if (eventSource) {
|
||||
disconnect();
|
||||
@@ -121,7 +101,7 @@ export function useSSE(options: SSEOptions) {
|
||||
throw new Error('SSE服务器地址不能为空');
|
||||
}
|
||||
|
||||
const connectUrl = getConnectUrl();
|
||||
const connectUrl = `${serverUrl.value}/connect`;
|
||||
addLog(`开始连接到 ${connectUrl}...`);
|
||||
|
||||
// 设置连接超时
|
||||
@@ -131,7 +111,7 @@ export function useSSE(options: SSEOptions) {
|
||||
|
||||
connectionTimeout = window.setTimeout(() => {
|
||||
if (isConnecting.value) {
|
||||
addLog('连接超时,请检查服务器地址或网络连接');
|
||||
addLog('连接超时,请检查服务器地址或网络连接', 'error');
|
||||
isConnecting.value = false;
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
@@ -139,15 +119,19 @@ export function useSSE(options: SSEOptions) {
|
||||
}
|
||||
|
||||
// 调用错误回调
|
||||
if (mergedOptions.onError) {
|
||||
mergedOptions.onError(new Error('连接超时'));
|
||||
if (options.onError) {
|
||||
options.onError(new Error('连接超时'));
|
||||
}
|
||||
}
|
||||
}, mergedOptions.timeout || 10000); // 默认10秒超时
|
||||
}, options.timeout || 10000); // 默认10秒超时
|
||||
|
||||
// 创建EventSource
|
||||
const eventSourceOptions = { withCredentials: mergedOptions.withCredentials || false };
|
||||
eventSource = new EventSource(connectUrl, eventSourceOptions);
|
||||
const eventSourceOptions = {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${getToken()}`,
|
||||
}
|
||||
};
|
||||
eventSource = new EventSourcePolyfill(connectUrl, eventSourceOptions);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
if (connectionTimeout) {
|
||||
@@ -157,11 +141,11 @@ export function useSSE(options: SSEOptions) {
|
||||
|
||||
isConnected.value = true;
|
||||
isConnecting.value = false;
|
||||
addLog('SSE 连接已建立');
|
||||
addLog('SSE 连接已建立', 'success');
|
||||
|
||||
// 调用连接成功回调
|
||||
if (mergedOptions.onConnect) {
|
||||
mergedOptions.onConnect();
|
||||
if (options.onConnect) {
|
||||
options.onConnect();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,14 +154,29 @@ export function useSSE(options: SSEOptions) {
|
||||
console.log(event)
|
||||
|
||||
// 调用消息处理回调
|
||||
if (mergedOptions.onMessage) {
|
||||
mergedOptions.onMessage(event.data);
|
||||
if (options.onMessage) {
|
||||
options.onMessage(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
// 改为以下格式
|
||||
// 添加事件监听器
|
||||
addSSEListener(eventSource, 'L1_EVENT', (data) => {
|
||||
console.log('收到 L1_EVENT:', data);
|
||||
if (options.onL1Event) {
|
||||
options.onL1Event(data);
|
||||
}
|
||||
});
|
||||
addSSEListener(eventSource, 'L4_EVENT', (data) => {
|
||||
console.log('收到 L4_EVENT:', data);
|
||||
if (options.onL4Event) {
|
||||
options.onL4Event(data);
|
||||
}
|
||||
});
|
||||
addSSEListener(eventSource, 'MES_EVENT', (data) => {
|
||||
console.log('收到 MES_EVENT:', data);
|
||||
if (options.onMesEvent) {
|
||||
options.onMesEvent(data);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
@@ -186,7 +185,7 @@ export function useSSE(options: SSEOptions) {
|
||||
connectionTimeout = null;
|
||||
}
|
||||
|
||||
addLog('SSE 连接错误,请检查服务器地址或网络连接');
|
||||
addLog('SSE 连接错误,请检查服务器地址或网络连接', 'error');
|
||||
isConnected.value = false;
|
||||
isConnecting.value = false;
|
||||
|
||||
@@ -196,17 +195,17 @@ export function useSSE(options: SSEOptions) {
|
||||
}
|
||||
|
||||
// 调用错误回调
|
||||
if (mergedOptions.onError) {
|
||||
mergedOptions.onError(err);
|
||||
if (options.onError) {
|
||||
options.onError(err);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
addLog(`连接失败: ${error}`);
|
||||
addLog(`连接失败: ${error}`, 'error');
|
||||
isConnecting.value = false;
|
||||
|
||||
// 调用错误回调
|
||||
if (mergedOptions.onError && error instanceof Error) {
|
||||
mergedOptions.onError(error);
|
||||
if (options.onError && error instanceof Error) {
|
||||
options.onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -223,89 +222,11 @@ export function useSSE(options: SSEOptions) {
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
addLog('已断开连接');
|
||||
addLog('已断开连接', 'log');
|
||||
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,
|
||||
@@ -315,13 +236,8 @@ export function useSSE(options: SSEOptions) {
|
||||
serverUrl,
|
||||
sseStatusText,
|
||||
sseStatusClass,
|
||||
isInCooldown,
|
||||
cooldownRemaining,
|
||||
connect,
|
||||
disconnect,
|
||||
addLog, // SSE内部日志记录
|
||||
clearLogs,
|
||||
cleanup,
|
||||
processMessage // 消息分类处理
|
||||
addLog
|
||||
};
|
||||
}
|
||||
0
src/views/L1-data-list/index.vue
Normal file
0
src/views/L1-data-list/index.vue
Normal file
0
src/views/L4-data-list/index.vue
Normal file
0
src/views/L4-data-list/index.vue
Normal file
975
src/views/index.vue
Normal file
975
src/views/index.vue
Normal file
@@ -0,0 +1,975 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, reactive, nextTick } from 'vue';
|
||||
import { useRealTime } from '@/utils/dateUtils';
|
||||
import { checkOrderNumberApi, addProcessInfoApi } from '@/api/detect';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
// 检测设备表单规则
|
||||
const rules: Record<string, Rule[]> = {
|
||||
workOrderCode: [{ required: true, message: '请输入工单号', trigger: 'change' }],
|
||||
productCode: [{ required: true, message: '请输入产品编码', trigger: 'change' }],
|
||||
employeeCode: [{ required: true, message: '请输入员工号', trigger: 'change' }],
|
||||
processName: [{ required: true, message: '请输入工序名称', trigger: 'change' }],
|
||||
resourceName: [{ required: true, message: '请输入资源名称', trigger: 'change' }],
|
||||
equipmentCode: [{ required: true, message: '请输入设备编码', trigger: 'change' }],
|
||||
fixtureCode: [{ required: true, message: '请输入治具编码', trigger: 'change' }],
|
||||
};
|
||||
|
||||
// 包装设备表单规则
|
||||
const packageRules: Record<string, Rule[]> = {
|
||||
customerSelection: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
productModel: [{ required: true, message: '请输入产品型号', trigger: 'change' }],
|
||||
employeeCode: [{ required: true, message: '请输入员工号', trigger: 'change' }],
|
||||
processName: [{ required: true, message: '请输入工序名称', trigger: 'change' }],
|
||||
resourceName: [{ required: true, message: '请输入资源名称', trigger: 'change' }],
|
||||
equipmentCode: [{ required: true, message: '请输入设备编码', trigger: 'change' }],
|
||||
fixtureCode: [{ required: true, message: '请输入治具编码', trigger: 'change' }],
|
||||
packingResult: [{ required: true, message: '请选择装箱结果', trigger: 'change' }],
|
||||
};
|
||||
|
||||
// 实时时间
|
||||
const { currentTime } = useRealTime('HH:mm:ss');
|
||||
const currentDate = ref('');
|
||||
|
||||
// 可编辑表单状态管理
|
||||
const isDetectingEditMode = ref(false);
|
||||
const isPackageEditMode = ref(false);
|
||||
|
||||
// 初始副本存储
|
||||
let detectFormBackup: any = null;
|
||||
let packageFormBackup: any = null;
|
||||
|
||||
// 表单引用
|
||||
const detectFormRef = ref();
|
||||
const packageFormRef = ref();
|
||||
|
||||
// 执行结果日志
|
||||
const executionLogs = ref([
|
||||
'14:25:32 - 开始检测流程 SN-A538-09-2023-00018',
|
||||
'14:25:33 - 读取产品参数完成',
|
||||
'14:25:36 - 尺寸检测: 通过 (0.15mm)',
|
||||
'14:25:36 - 电压检测: 通过 (3.25V)',
|
||||
'14:25:37 - 精度检测: 通过 (98.7%)',
|
||||
'14:25:38 - 检测结果: 合格'
|
||||
]);
|
||||
|
||||
const packageLogs = ref([
|
||||
'14:25:45 - 包装流程: SN-A538-09-2023-00018',
|
||||
'14:25:46 - 读取产品标签完成',
|
||||
'14:25:49 - 包装材料准备完成',
|
||||
'14:25:52 - 产品包装完成',
|
||||
'14:25:53 - 更新库存: 已包装 (18/25)',
|
||||
'14:25:54 - 包装结果: 正常'
|
||||
]);
|
||||
|
||||
// 滚动到检测设备日志底部的函数
|
||||
const scrollToExecutionBottom = () => {
|
||||
const logsContainer = document.querySelector('.execution-logs-content');
|
||||
if (logsContainer) {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// 底部操作按钮配置
|
||||
const actionButtons = ref([
|
||||
{ label: '工序过站', handler: 'handleProcessPass', type: 'primary' as const, status: 'idle' as const },
|
||||
{ label: '复检序号检查', handler: 'handleRecheckSequence', type: 'default' as const, status: 'idle' as const },
|
||||
{ label: '扫码', handler: 'handleScanCode', type: 'default' as const, status: 'idle' as const },
|
||||
{ label: '序号检查', handler: 'handleSequenceCheck', type: 'default' as const, status: 'idle' as const },
|
||||
{ label: '包装过站', handler: 'handlePackagePass', type: 'primary' as const, status: 'idle' as const }
|
||||
]);
|
||||
|
||||
// 执行按钮操作
|
||||
const executeButtonAction = (handlerName: string) => {
|
||||
const handlers: { [key: string]: () => void } = {
|
||||
handleOrderCheck,
|
||||
handleProcessPass,
|
||||
handleRecheckSequence,
|
||||
handleSequenceCheck,
|
||||
handlePackagePass
|
||||
};
|
||||
|
||||
const handler = handlers[handlerName];
|
||||
if (handler) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
// 日志记录工具函数
|
||||
const addDetectionLog = (message: string) => {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const logEntry = `${timestamp} - ${message}`;
|
||||
executionLogs.value.push(logEntry);
|
||||
|
||||
// 自动滚动到底部
|
||||
nextTick(() => {
|
||||
scrollToExecutionBottom();
|
||||
});
|
||||
};
|
||||
|
||||
const addPackageLog = (message: string) => {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const logEntry = `${timestamp} - ${message}`;
|
||||
packageLogs.value.push(logEntry);
|
||||
|
||||
// 自动滚动到底部
|
||||
nextTick(() => {
|
||||
const logContainers = document.querySelectorAll('.log-container');
|
||||
logContainers.forEach(container => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 清空检测设备执行日志
|
||||
const clearExecutionLogs = () => {
|
||||
executionLogs.value = [];
|
||||
};
|
||||
|
||||
// 清空自动包装执行日志
|
||||
const clearPackageLogs = () => {
|
||||
packageLogs.value = [];
|
||||
};
|
||||
|
||||
// 单号检查
|
||||
const handleOrderCheck = () => {
|
||||
checkOrderNumberApi({
|
||||
workOrderCode: detectForm.workOrderCode,
|
||||
productCode: detectForm.productCode
|
||||
}).then(res => {
|
||||
if (res) {
|
||||
addDetectionLog('单号检查成功');
|
||||
} else {
|
||||
addDetectionLog('单号检查失败');
|
||||
}
|
||||
})
|
||||
addDetectionLog('开始执行单号检查操作');
|
||||
console.log('单号检查');
|
||||
};
|
||||
|
||||
// 工序过站
|
||||
const handleProcessPass = () => {
|
||||
addDetectionLog('开始执行工序过站操作');
|
||||
console.log('工序过站');
|
||||
};
|
||||
|
||||
// 复检序号检查
|
||||
const handleRecheckSequence = () => {
|
||||
addPackageLog('开始执行复检序号检查操作');
|
||||
console.log('复检序号检查');
|
||||
};
|
||||
|
||||
// 序号检查
|
||||
const handleSequenceCheck = () => {
|
||||
addPackageLog('开始执行序号检查操作');
|
||||
console.log('序号检查');
|
||||
};
|
||||
|
||||
// 包装过站
|
||||
const handlePackagePass = () => {
|
||||
addPackageLog('开始执行包装过站操作');
|
||||
console.log('包装过站');
|
||||
};
|
||||
|
||||
// SSE事件处理函数
|
||||
const handleL1Event = (data: any) => {
|
||||
// L1_EVENT放到自动包装日志区
|
||||
addPackageLog(`L1事件: ${JSON.stringify(data)}`);
|
||||
};
|
||||
|
||||
const handleL4Event = (data: any) => {
|
||||
// L4_EVENT放到检测设备日志区
|
||||
addDetectionLog(`L4事件: ${JSON.stringify(data)}`);
|
||||
if(data.code === 201) {
|
||||
detectForm.confirmSequence = data.data
|
||||
}
|
||||
};
|
||||
|
||||
const handleMESEvent = (data: any) => {
|
||||
// MES_EVENT放到检测设备日志区
|
||||
addDetectionLog(`MES事件: ${JSON.stringify(data)}`);
|
||||
};
|
||||
|
||||
const handleSseMessage = (data: string) => {
|
||||
// 处理SSE原始消息(如果需要的话)
|
||||
console.log('收到SSE消息:', data);
|
||||
};
|
||||
|
||||
// Status handlers are now managed by individual modal components
|
||||
|
||||
// 检测设备表单数据
|
||||
const detectForm = reactive({
|
||||
workOrderCode: 'SKT202507220001',
|
||||
productCode: 'JH1008611',
|
||||
employeeCode: 'ZDXTEST',
|
||||
processName: 'T-自动组装测试',
|
||||
resourceName: 'T-A自动组装测试',
|
||||
equipmentCode: 'JH.02.101.13-219',
|
||||
fixtureCode: 'JH0001',
|
||||
confirmSequence: '',
|
||||
detectionData: '',
|
||||
sequenceCount: 1
|
||||
});
|
||||
|
||||
// 包装设备表单数据
|
||||
const packageForm = reactive({
|
||||
customerSelection: '上海电子科技有限公司',
|
||||
productModel: 'A538-09-高精度传感器',
|
||||
employeeCode: 'OP-7853',
|
||||
processName: '自动包装',
|
||||
resourceName: '包装台2号',
|
||||
equipmentCode: 'EQ-PK-002',
|
||||
fixtureCode: 'FX-PK-002-B',
|
||||
packingResult: '正常',
|
||||
sequenceNumber: 'SN-A538-09-2023-00018',
|
||||
passStationCount: 18,
|
||||
fullBoxCount: 25,
|
||||
passStationSequence: 'SN-A538-09-2023-00018'
|
||||
});
|
||||
|
||||
// 编辑模式切换
|
||||
const toggleDetectingEditMode = () => {
|
||||
if (!isDetectingEditMode.value) {
|
||||
// 进入编辑模式,保存初始副本
|
||||
detectFormBackup = JSON.parse(JSON.stringify(detectForm));
|
||||
}
|
||||
isDetectingEditMode.value = !isDetectingEditMode.value;
|
||||
};
|
||||
|
||||
const togglePackageEditMode = () => {
|
||||
if (!isPackageEditMode.value) {
|
||||
// 进入编辑模式,保存初始副本
|
||||
packageFormBackup = JSON.parse(JSON.stringify(packageForm));
|
||||
}
|
||||
isPackageEditMode.value = !isPackageEditMode.value;
|
||||
};
|
||||
|
||||
// 保存检测设备编辑
|
||||
const saveDetectingEdit = () => {
|
||||
detectFormRef.value.validate().then(() => {
|
||||
console.log('保存检测设备数据:', detectForm);
|
||||
isDetectingEditMode.value = false;
|
||||
// 保存成功,清除备份
|
||||
detectFormBackup = null;
|
||||
|
||||
addProcessInfoApi({
|
||||
workOrderCode: detectForm.workOrderCode,
|
||||
productCode: detectForm.productCode,
|
||||
employeeCode: detectForm.employeeCode,
|
||||
processName: detectForm.processName,
|
||||
resourceName: detectForm.resourceName,
|
||||
equipmentCode: detectForm.equipmentCode,
|
||||
fixtureCode: detectForm.fixtureCode
|
||||
}).then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
}).catch(() => {
|
||||
console.log('检测设备表单验证失败');
|
||||
});
|
||||
};
|
||||
|
||||
// 保存包装设备编辑
|
||||
const savePackageEdit = () => {
|
||||
packageFormRef.value.validate().then(() => {
|
||||
console.log('保存包装设备数据:', packageForm);
|
||||
isPackageEditMode.value = false;
|
||||
// 保存成功,清除备份
|
||||
packageFormBackup = null;
|
||||
}).catch(() => {
|
||||
console.log('包装设备表单验证失败');
|
||||
});
|
||||
};
|
||||
|
||||
// 取消检测设备编辑
|
||||
const cancelDetectingEdit = () => {
|
||||
isDetectingEditMode.value = false;
|
||||
if (detectFormBackup) {
|
||||
// 恢复到初始副本
|
||||
Object.assign(detectForm, detectFormBackup);
|
||||
detectFormBackup = null;
|
||||
}
|
||||
// 清除表单验证状态
|
||||
detectFormRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// 取消包装设备编辑
|
||||
const cancelPackageEdit = () => {
|
||||
isPackageEditMode.value = false;
|
||||
if (packageFormBackup) {
|
||||
// 恢复到初始副本
|
||||
Object.assign(packageForm, packageFormBackup);
|
||||
packageFormBackup = null;
|
||||
}
|
||||
// 清除表单验证状态
|
||||
packageFormRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 设置当前日期
|
||||
const now = new Date();
|
||||
currentDate.value = `${now.getFullYear()}年${String(now.getMonth() + 1).padStart(2, '0')}月${String(now.getDate()).padStart(2, '0')}日`;
|
||||
|
||||
// SSE连接逻辑已迁移到SseStatus组件中
|
||||
});
|
||||
|
||||
// 清理事件监听
|
||||
onBeforeUnmount(() => {
|
||||
// SSE断开连接逻辑已迁移到SseStatus组件中
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hmi-container">
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="header-status">
|
||||
<div class="left-info">
|
||||
<span class="app-title">
|
||||
<i-lucide-cpu class="app-icon" /> 工业控制系统 HMI
|
||||
</span>
|
||||
</div>
|
||||
<div class="center-status">
|
||||
<!-- MES状态 -->
|
||||
<MesStatus />
|
||||
|
||||
<!-- 网络通讯状态 -->
|
||||
<NetworkStatus />
|
||||
|
||||
<!-- PLC通讯状态 -->
|
||||
<PlcStatus />
|
||||
|
||||
<!-- 摄像头状态 -->
|
||||
<CameraStatus />
|
||||
|
||||
<!-- SSE 日志 -->
|
||||
<SseStatus
|
||||
:on-l1-event="handleL1Event"
|
||||
:on-l4-event="handleL4Event"
|
||||
:on-mes-event="handleMESEvent"
|
||||
:on-sse-message="handleSseMessage"
|
||||
/>
|
||||
</div>
|
||||
<SettingsModal />
|
||||
<!-- <div class="right-info" @click="showSettings">
|
||||
<span class="time-info">{{ currentTime }}</span>
|
||||
<span class="date-info">{{ currentDate }}</span>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 检测设备区域 -->
|
||||
<div class="device-section detecting-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
<i-lucide-monitor class="title-icon" />
|
||||
检测设备
|
||||
</h3>
|
||||
<div class="edit-controls" v-if="!isDetectingEditMode">
|
||||
<a-button size="small" @click="toggleDetectingEditMode" class="edit-btn">
|
||||
<i-lucide-edit class="btn-icon" />
|
||||
编辑
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="edit-controls" v-else>
|
||||
<a-button size="small" type="primary" @click="saveDetectingEdit" class="save-btn">
|
||||
<i-lucide-save class="btn-icon" />
|
||||
保存
|
||||
</a-button>
|
||||
<a-button size="small" @click="cancelDetectingEdit" class="cancel-btn">
|
||||
<i-lucide-x class="btn-icon" />
|
||||
取消
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-form :model="detectForm" :rules="rules" class="device-info" ref="detectFormRef" :colon="false"
|
||||
hideRequiredMark :labelCol="{ span: 4 }" :wrapperCol="{ span: 20 }">
|
||||
<a-row :gutter="32">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="工单号" name="workOrderCode">
|
||||
<a-input-group compact>
|
||||
<a-input v-model:value="detectForm.workOrderCode" class="edit-input"
|
||||
:disabled="!isDetectingEditMode" />
|
||||
<a-button type="primary" @click="handleOrderCheck">单号检查</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="产品编码" name="productCode">
|
||||
<a-input v-model:value="detectForm.productCode" class="edit-input"
|
||||
:disabled="!isDetectingEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="员工工号" name="employeeCode">
|
||||
<a-input v-model:value="detectForm.employeeCode" class="edit-input"
|
||||
:disabled="!isDetectingEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="工序名称" name="processName">
|
||||
<a-input v-model:value="detectForm.processName" class="edit-input"
|
||||
:disabled="!isDetectingEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="资源名称" name="resourceName">
|
||||
<a-input v-model:value="detectForm.resourceName" class="edit-input"
|
||||
:disabled="!isDetectingEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备编码" name="equipmentCode">
|
||||
<a-input v-model:value="detectForm.equipmentCode" class="edit-input"
|
||||
:disabled="!isDetectingEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="治具编码" name="fixtureCode">
|
||||
<a-input v-model:value="detectForm.fixtureCode" class="edit-input"
|
||||
:disabled="!isDetectingEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="序号数量">
|
||||
<a-input v-model:value="detectForm.sequenceCount" class="edit-input" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="确认序号" name="confirmSequence" :labelCol="{ span: 2 }" :wrapperCol="{ span: 22 }">
|
||||
<a-input-group compact>
|
||||
<a-input v-model:value="detectForm.confirmSequence" class="edit-input" disabled />
|
||||
<a-button type="primary" @click="handleSequenceCheck">序号检查</a-button>
|
||||
<a-button type="primary" @click="handleRecheckSequence" style="border-left: 1px solid #8395B6;">复检序号检查</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="检测数据" name="fixtureCode" :labelCol="{ span: 2 }" :wrapperCol="{ span: 22 }">
|
||||
<a-input v-model:value="detectForm.detectionData" class="edit-input"
|
||||
disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<ExecutionResult title="执行结果" :logs="executionLogs" @clear="clearExecutionLogs" />
|
||||
</div>
|
||||
|
||||
<!-- 自动包装区域 -->
|
||||
<div class="device-section package-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
<i-lucide-package class="title-icon" />
|
||||
自动包装
|
||||
</h3>
|
||||
<div class="edit-controls" v-if="!isPackageEditMode">
|
||||
<a-button size="small" @click="togglePackageEditMode" class="edit-btn">
|
||||
<i-lucide-square-pen class="btn-icon" />
|
||||
编辑
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="edit-controls" v-else>
|
||||
<a-button size="small" type="primary" @click="savePackageEdit" class="save-btn">
|
||||
<i-lucide-save class="btn-icon" />
|
||||
保存
|
||||
</a-button>
|
||||
<a-button size="small" @click="cancelPackageEdit" class="cancel-btn">
|
||||
<i-lucide-x class="btn-icon" />
|
||||
取消
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-form :model="packageForm" :rules="packageRules" class="device-info" ref="packageFormRef" :colon="false"
|
||||
hideRequiredMark :labelCol="{ span: 4 }" :wrapperCol="{ span: 20 }">
|
||||
<a-row :gutter="32">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="客户选择" name="customerSelection">
|
||||
<a-input v-model:value="packageForm.customerSelection" class="edit-input"
|
||||
:disabled="!isPackageEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="产品型号" name="productModel">
|
||||
<a-input v-model:value="packageForm.productModel" class="edit-input"
|
||||
:disabled="!isPackageEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="工号" name="employeeCode">
|
||||
<a-input v-model:value="packageForm.employeeCode" class="edit-input"
|
||||
:disabled="!isPackageEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="工序名称" name="processName">
|
||||
<a-input v-model:value="packageForm.processName" class="edit-input"
|
||||
:disabled="!isPackageEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="资源名称" name="resourceName">
|
||||
<a-input v-model:value="packageForm.resourceName" class="edit-input"
|
||||
:disabled="!isPackageEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备编码" name="equipmentCode">
|
||||
<a-input v-model:value="packageForm.equipmentCode" class="edit-input"
|
||||
:disabled="!isPackageEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="治具编码" name="fixtureCode">
|
||||
<a-input v-model:value="packageForm.fixtureCode" class="edit-input"
|
||||
:disabled="!isPackageEditMode" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="装箱结果" name="packingResult">
|
||||
<a-select v-model:value="packageForm.packingResult" class="edit-input"
|
||||
:disabled="!isPackageEditMode">
|
||||
<a-select-option value="正常">正常</a-select-option>
|
||||
<a-select-option value="异常">异常</a-select-option>
|
||||
<a-select-option value="待检">待检</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="序号" name="fixtureCode">
|
||||
<a-input v-model:value="packageForm.sequenceNumber" class="edit-input" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过站数量" name="fixtureCode">
|
||||
<a-input v-model:value="packageForm.passStationCount" class="edit-input" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="满箱数量" name="fixtureCode">
|
||||
<a-input v-model:value="packageForm.fullBoxCount" class="edit-input" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过站序号" name="fixtureCode">
|
||||
<a-input v-model:value="packageForm.passStationSequence" class="edit-input" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<ExecutionResult title="执行结果" :logs="packageLogs" @clear="clearPackageLogs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮区域 -->
|
||||
<!-- <ActionButtons :buttons="actionButtons" @execute="executeButtonAction" /> -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/_variables.scss';
|
||||
|
||||
.hmi-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: $white;
|
||||
font-family: $font-family;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
font-size: clamp(14px, 1.4vw, 18px);
|
||||
|
||||
/* 响应式字体大小 */
|
||||
@media (max-width: 1024px) {
|
||||
font-size: clamp(12px, 1.7vw, 16px);
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) and (max-width: 1366px) {
|
||||
font-size: clamp(14px, 1.5vw, 17px);
|
||||
}
|
||||
|
||||
@media (min-width: 1367px) {
|
||||
font-size: clamp(16px, 1.3vw, 20px);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式字体大小的具体元素
|
||||
@media (max-width: 1024px) {
|
||||
.app-title {
|
||||
font-size: clamp(16px, 2.8vw, 20px) !important;
|
||||
}
|
||||
|
||||
.status-label,
|
||||
.status-value {
|
||||
font-size: clamp(12px, 1.4vw, 14px) !important;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: clamp(16px, 2.0vw, 18px) !important;
|
||||
}
|
||||
|
||||
.info-item .label,
|
||||
.info-item .value {
|
||||
font-size: clamp(12px, 1.4vw, 14px) !important;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
font-size: clamp(12px, 1.4vw, 14px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) and (max-width: 1366px) {
|
||||
.app-title {
|
||||
font-size: clamp(18px, 2.4vw, 20px) !important;
|
||||
}
|
||||
|
||||
.status-label,
|
||||
.status-value {
|
||||
font-size: clamp(13px, 1.3vw, 15px) !important;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: clamp(17px, 1.8vw, 19px) !important;
|
||||
}
|
||||
|
||||
.info-item .label,
|
||||
.info-item .value {
|
||||
font-size: clamp(13px, 1.3vw, 15px) !important;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
font-size: clamp(14px, 1.3vw, 16px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1367px) {
|
||||
.app-title {
|
||||
font-size: clamp(20px, 2.0vw, 22px) !important;
|
||||
}
|
||||
|
||||
.status-label,
|
||||
.status-value {
|
||||
font-size: clamp(14px, 1.2vw, 16px) !important;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: clamp(18px, 1.6vw, 20px) !important;
|
||||
}
|
||||
|
||||
.info-item .label,
|
||||
.info-item .value {
|
||||
font-size: clamp(14px, 1.2vw, 16px) !important;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
font-size: clamp(15px, 1.2vw, 17px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部状态栏 */
|
||||
.header-status {
|
||||
height: 50px;
|
||||
background: $bg-dark;
|
||||
border-bottom: 2px solid $primary-color;
|
||||
@include flex-between;
|
||||
padding: 0 $spacing-lg;
|
||||
flex-shrink: 0;
|
||||
|
||||
.left-info {
|
||||
.app-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.center-status {
|
||||
display: flex;
|
||||
gap: $spacing-xxl;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-button {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
@include button-base;
|
||||
@include button-hover($bg-overlay, rgba(255, 255, 255, 0.2), $white);
|
||||
}
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-lg;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.device-section {
|
||||
flex: 1;
|
||||
background: $bg-overlay;
|
||||
border: 2px solid $primary-color;
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-lg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: $spacing-md;
|
||||
border-bottom: 1px solid $primary-color;
|
||||
padding-bottom: 6px;
|
||||
@include flex-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
margin: 0;
|
||||
color: $white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
filter: drop-shadow(0 2px 4px rgba(74, 144, 226, 0.3));
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $primary-color;
|
||||
margin-right: $spacing-sm;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 6px;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: $spacing-xs;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: rotation 2s infinite linear;
|
||||
/* 添加旋转动画 */
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
/* 从 0 度开始 */
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
/* 旋转到 360 度 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 编辑控制按钮 */
|
||||
.edit-controls {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
@include status-button($bg-primary, $bg-primary-hover, $primary-color, $primary-light, $text-light, $white);
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
@include status-button($success-bg, $success-bg-hover, $success-color, $success-light, $text-success, $white);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
@include status-button($error-bg, $error-bg-hover, $error-color, $error-light, $text-error, $white);
|
||||
}
|
||||
|
||||
/* 编辑输入框样式 */
|
||||
.edit-input {
|
||||
background: $bg-input !important;
|
||||
border: $border-light !important;
|
||||
color: $white !important;
|
||||
width: 100%;
|
||||
|
||||
&.bn {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $bg-input-focus !important;
|
||||
border-color: #fff !important;
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2) !important;
|
||||
}
|
||||
|
||||
&.ant-input-disabled {
|
||||
color: $text-light !important;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent !important;
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
&.ant-select.ant-select-in-form-item {
|
||||
border: none !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-select.ant-select-disabled) {
|
||||
.ant-select-selection-item {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border: $border-light !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: #bdbdbd;
|
||||
}
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
color: $text-light;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
min-height: 20px;
|
||||
|
||||
|
||||
.value {
|
||||
color: $white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.success {
|
||||
color: $success-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* SSE日志对话框样式 */
|
||||
.sse-logs-dialog {
|
||||
:deep(.ant-modal-content) {
|
||||
background: rgba(30, 30, 45, 0.95);
|
||||
border: 1px solid $primary-color;
|
||||
}
|
||||
|
||||
.sse-controls {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.sse-control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1600px) {
|
||||
.main-content {
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
.device-section {
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
.label {
|
||||
min-width: 70px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.center-status {
|
||||
gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.status-label,
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.device-section {
|
||||
padding: $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.device-section {
|
||||
flex: 1;
|
||||
height: calc(50vh - 100px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog样式已迁移到各个子组件中 */
|
||||
|
||||
:deep(.ant-input-group.ant-input-group-compact) {
|
||||
display: flex;
|
||||
|
||||
.ant-btn[disabled] {
|
||||
border-color: #8396B7;
|
||||
color: #b4b4b4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
527
src/views/login.vue
Normal file
527
src/views/login.vue
Normal file
@@ -0,0 +1,527 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { login, getCaptcha } from '@/api/system'
|
||||
import type { LoginInfo } from '@/api/system/model'
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import { setToken } from '@/utils/auth';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<LoginInfo>({
|
||||
username: '',
|
||||
password: '',
|
||||
uuid: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
// 验证码图片
|
||||
const captchaImg = ref('')
|
||||
const loading = ref(false)
|
||||
const captchaLoading = ref(false)
|
||||
|
||||
// 表单验证规则
|
||||
const rules: Record<string, Rule[]> = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'change' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'change' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
const refreshCaptcha = async () => {
|
||||
try {
|
||||
captchaLoading.value = true
|
||||
await getCaptcha().then((res: any) => {
|
||||
if (res.captchaOnOff) {
|
||||
captchaImg.value = "data:image/gif;base64," + res.img
|
||||
}
|
||||
formData.uuid = res.uuid
|
||||
formData.code = '' // 清空验证码输入
|
||||
})
|
||||
} catch (error) {
|
||||
message.error('获取验证码失败')
|
||||
console.error('获取验证码失败:', error)
|
||||
} finally {
|
||||
captchaLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await login(formData).then(async (res: any) => {
|
||||
if (res.code === 200) {
|
||||
message.success('登录成功')
|
||||
setToken(res.token)
|
||||
// 可以在这里保存用户信息到store
|
||||
// 跳转到主页
|
||||
router.push('/')
|
||||
} else {
|
||||
message.error(res.msg || '登录失败')
|
||||
// 登录失败后重新获取验证码
|
||||
await refreshCaptcha()
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
message.error(error.message || error.msg || '登录失败,请检查网络连接')
|
||||
console.error('登录失败:', error)
|
||||
// 登录失败后重新获取验证码
|
||||
await refreshCaptcha()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取验证码
|
||||
refreshCaptcha()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<!-- 背景图 -->
|
||||
<div class="login-background"></div>
|
||||
|
||||
<!-- 登录框 -->
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h2 class="login-title">
|
||||
<i-lucide-cpu class="title-icon" />
|
||||
工业控制系统 HMI
|
||||
</h2>
|
||||
<p class="login-subtitle">请登录您的账户</p>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
@finish="handleLogin"
|
||||
class="login-form"
|
||||
layout="vertical"
|
||||
>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item name="username" class="form-item">
|
||||
<a-input
|
||||
v-model:value="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
class="login-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<i-lucide-user class="input-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<a-form-item name="password" class="form-item">
|
||||
<a-input-password
|
||||
v-model:value="formData.password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
class="login-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<i-lucide-lock class="input-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<a-form-item name="code" class="form-item">
|
||||
<div class="captcha-container">
|
||||
<a-input
|
||||
v-model:value="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
size="large"
|
||||
class="login-input captcha-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<i-lucide-shield-check class="input-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="captcha-image-container" @click="refreshCaptcha">
|
||||
<img
|
||||
v-if="captchaImg"
|
||||
:src="captchaImg"
|
||||
alt="验证码"
|
||||
class="captcha-image"
|
||||
/>
|
||||
<div v-else class="captcha-loading">
|
||||
<i-lucide-loader class="loading-icon" />
|
||||
</div>
|
||||
<div class="captcha-refresh-hint">
|
||||
<i-lucide-refresh-cw class="refresh-icon" />
|
||||
点击刷新
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<a-form-item class="form-item">
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
block
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 变量定义
|
||||
$primary-color: #4a90e2;
|
||||
$primary-light: #5ba0f2;
|
||||
$primary-dark: #3a7bd5;
|
||||
$success-color: #52c41a;
|
||||
$warning-color: #faad14;
|
||||
$error-color: #ff4d4f;
|
||||
$white: #ffffff;
|
||||
$text-light: #b8d4f0;
|
||||
$text-dark: #333333;
|
||||
$bg-overlay: rgba(255, 255, 255, 0.1);
|
||||
$bg-glass: rgba(255, 255, 255, 0.15);
|
||||
$border-light: rgba(255, 255, 255, 0.3);
|
||||
$shadow-light: rgba(0, 0, 0, 0.1);
|
||||
$shadow-medium: rgba(0, 0, 0, 0.2);
|
||||
$border-radius: 8px;
|
||||
$border-radius-lg: 12px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 16px;
|
||||
$spacing-lg: 24px;
|
||||
$spacing-xl: 32px;
|
||||
$transition: all 0.3s ease;
|
||||
|
||||
.login-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('/bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
filter: blur(2px);
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(74, 144, 226, 0.3) 0%,
|
||||
rgba(58, 123, 213, 0.4) 50%,
|
||||
rgba(91, 160, 242, 0.3) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.login-box {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 420px;
|
||||
padding: $spacing-xl;
|
||||
background: $bg-glass;
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid $border-light;
|
||||
border-radius: $border-radius-lg;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
0 4px 16px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
.login-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-sm;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: $white;
|
||||
margin: 0 0 $spacing-sm 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.title-icon {
|
||||
font-size: 28px;
|
||||
color: $primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
color: $text-light;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.form-item {
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-input {
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: $border-radius;
|
||||
transition: $transition;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: $primary-light;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
:deep(.ant-input) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: $white;
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input-password-icon) {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-container {
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
align-items: stretch;
|
||||
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.captcha-image-container {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 48px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: $transition;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-light;
|
||||
|
||||
.captcha-refresh-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.captcha-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
.loading-icon {
|
||||
color: $white;
|
||||
font-size: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-refresh-hint {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $white;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: $transition;
|
||||
|
||||
.refresh-icon {
|
||||
font-size: 16px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
|
||||
border: none;
|
||||
border-radius: $border-radius;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: $transition;
|
||||
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, $primary-light 0%, $primary-color 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
margin-right: $spacing-sm;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-loading-icon) {
|
||||
margin-right: $spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证错误样式
|
||||
:deep(.ant-form-item-has-error) {
|
||||
.login-input {
|
||||
border-color: $error-color;
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-image-container {
|
||||
border-color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain-error) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.login-box {
|
||||
width: 90%;
|
||||
max-width: 380px;
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
.captcha-container {
|
||||
flex-direction: column;
|
||||
|
||||
.captcha-image-container {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-box {
|
||||
padding: $spacing-md;
|
||||
}
|
||||
|
||||
.login-header .login-title {
|
||||
font-size: 20px;
|
||||
|
||||
.title-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
684
src/views/package-station/index.vue
Normal file
684
src/views/package-station/index.vue
Normal file
@@ -0,0 +1,684 @@
|
||||
<template>
|
||||
<div class="package-station-container">
|
||||
<!-- 顶部控制面板 -->
|
||||
<a-form class="control-panel">
|
||||
<!-- 左侧:箱号 -->
|
||||
<div class="control-left">
|
||||
<label class="control-label">箱号</label>
|
||||
<div class="control-input-group">
|
||||
<a-input
|
||||
v-model:value="boxNumber"
|
||||
placeholder="请输入箱号"
|
||||
class="control-input"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="applyBoxNumber"
|
||||
class="apply-btn"
|
||||
>
|
||||
申请箱号
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:包装规格、本箱累计 -->
|
||||
<div class="control-center">
|
||||
<div class="control-row">
|
||||
<div class="control-item">
|
||||
<label class="control-label">包装规格</label>
|
||||
<div class="control-input-group">
|
||||
<a-input
|
||||
v-model:value="packageSpec"
|
||||
placeholder="请输入包装规格"
|
||||
class="control-input"
|
||||
suffix="个"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="updateSpec"
|
||||
class="update-btn"
|
||||
>
|
||||
更新
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<label class="control-label">本箱累计</label>
|
||||
<div class="total-display">{{ totalCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:打印箱签、提交装箱 -->
|
||||
<div class="control-right">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="printBoxLabel"
|
||||
:disabled="!boxNumber"
|
||||
class="action-btn print-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<PrinterOutlined />
|
||||
</template>
|
||||
打印箱签
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="submitPackage"
|
||||
:disabled="!boxNumber || scannedItems.length === 0"
|
||||
class="action-btn submit-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<BoxPlotOutlined />
|
||||
</template>
|
||||
提交装箱
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
<!-- 已扫描件号表格 -->
|
||||
<div class="table-section">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="scannedItems"
|
||||
:pagination="pagination"
|
||||
:scroll="{ y: 500 }"
|
||||
bordered
|
||||
class="scanned-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button
|
||||
type="link"
|
||||
danger
|
||||
@click="deleteItem(record.key)"
|
||||
class="delete-btn"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { PrinterOutlined, BoxPlotOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 响应式数据
|
||||
const boxNumber = ref<string>('');
|
||||
const packageSpec = ref<string>('175');
|
||||
const scannedItems = ref<any[]>([
|
||||
{ key: '1', itemCode: 'GK-2022-0001', specification: '175', quantity: 2, operationTime: '2023-07-15 14:32:45', operator: '张工' },
|
||||
{ key: '2', itemCode: 'GK-2022-0002', specification: '175', quantity: 1, operationTime: '2023-07-15 14:35:12', operator: '张工' },
|
||||
{ key: '3', itemCode: 'GK-2022-0015', specification: '175', quantity: 3, operationTime: '2023-07-15 14:38:27', operator: '张工' },
|
||||
{ key: '4', itemCode: 'GK-2022-0023', specification: '175', quantity: 1, operationTime: '2023-07-15 14:41:53', operator: '李工' },
|
||||
{ key: '5', itemCode: 'GK-2022-0037', specification: '175', quantity: 2, operationTime: '2023-07-15 14:45:18', operator: '李工' },
|
||||
{ key: '6', itemCode: 'GK-2022-0042', specification: '175', quantity: 1, operationTime: '2023-07-15 14:48:36', operator: '王工' },
|
||||
{ key: '7', itemCode: 'GK-2022-0055', specification: '175', quantity: 2, operationTime: '2023-07-15 14:52:09', operator: '王工' },
|
||||
{ key: '8', itemCode: 'GK-2022-0078', specification: '175', quantity: 3, operationTime: '2023-07-15 14:56:24', operator: '赵工' },
|
||||
]);
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => scannedItems.value.length);
|
||||
|
||||
// 分页配置
|
||||
const pagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: scannedItems.value.length,
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false,
|
||||
showTotal: () => null,
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '件号',
|
||||
dataIndex: 'itemCode',
|
||||
key: 'itemCode',
|
||||
width: 200,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '规格',
|
||||
dataIndex: 'specification',
|
||||
key: 'specification',
|
||||
width: 180,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'quantity',
|
||||
key: 'quantity',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'operationTime',
|
||||
key: 'operationTime',
|
||||
width: 180,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 80,
|
||||
fixed: 'right' as const,
|
||||
align: 'center' as const,
|
||||
},
|
||||
];
|
||||
|
||||
// 方法
|
||||
const applyBoxNumber = () => {
|
||||
// 生成箱号逻辑
|
||||
const timestamp = new Date().getTime();
|
||||
boxNumber.value = `BOX${timestamp}`;
|
||||
};
|
||||
|
||||
const printBoxLabel = () => {
|
||||
console.log('打印箱签:', boxNumber.value);
|
||||
// 这里可以添加打印逻辑
|
||||
};
|
||||
|
||||
const updateSpec = () => {
|
||||
console.log('更新包装规格:', packageSpec.value);
|
||||
// 这里可以添加更新规格的逻辑
|
||||
};
|
||||
|
||||
const submitPackage = () => {
|
||||
console.log('提交装箱:', {
|
||||
boxNumber: boxNumber.value,
|
||||
packageSpec: packageSpec.value,
|
||||
totalCount: totalCount.value,
|
||||
items: scannedItems.value
|
||||
});
|
||||
// 这里可以添加提交逻辑
|
||||
};
|
||||
|
||||
const deleteItem = (key: string) => {
|
||||
scannedItems.value = scannedItems.value.filter(item => item.key !== key);
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.package-station-container {
|
||||
height: 100vh;
|
||||
background-color: #1E293B;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.control-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 40px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 2px solid #475569;
|
||||
|
||||
// 左侧:箱号
|
||||
.control-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.control-label {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.control-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.control-input {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
|
||||
:deep(.ant-input) {
|
||||
background-color: #fff;
|
||||
border-color: #d1d5db;
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #64748b;
|
||||
border-color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 中间:包装规格、本箱累计
|
||||
.control-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
|
||||
.control-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.control-label {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.control-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.control-input {
|
||||
flex: 1;
|
||||
max-width: 150px;
|
||||
|
||||
:deep(.ant-input) {
|
||||
background-color: #fff;
|
||||
border-color: #d1d5db;
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
background-color: #8b5cf6;
|
||||
border-color: #8b5cf6;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #7c3aed;
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #64748b;
|
||||
border-color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.total-display {
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
background-color: #475569;
|
||||
border: 2px solid #64748b;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧:打印箱签、提交装箱
|
||||
.control-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
|
||||
&.print-btn {
|
||||
background-color: #10b981;
|
||||
border-color: #10b981;
|
||||
|
||||
&:hover {
|
||||
background-color: #059669;
|
||||
border-color: #059669;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #64748b;
|
||||
border-color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
&.submit-btn {
|
||||
background-color: #f59e0b;
|
||||
border-color: #f59e0b;
|
||||
|
||||
&:hover {
|
||||
background-color: #d97706;
|
||||
border-color: #d97706;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #64748b;
|
||||
border-color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.scanned-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.ant-table) {
|
||||
background-color: transparent;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-table-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-table-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background-color: #475569;
|
||||
border-color: #64748b;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 16px 12px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
background-color: #475569;
|
||||
border-color: #64748b;
|
||||
color: #e2e8f0;
|
||||
font-size: 15px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:nth-child(even) > td {
|
||||
background-color: #4a5568;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background-color: #64748b !important;
|
||||
}
|
||||
|
||||
.ant-table-pagination {
|
||||
background-color: #475569;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
border-top: 1px solid #64748b;
|
||||
flex-shrink: 0;
|
||||
|
||||
.ant-pagination-item {
|
||||
background-color: #475569;
|
||||
border-color: #64748b;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: #64748b;
|
||||
}
|
||||
|
||||
&.ant-pagination-item-active {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-prev,
|
||||
.ant-pagination-next {
|
||||
background-color: #475569;
|
||||
border-color: #64748b;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: #ef4444;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1200px) {
|
||||
.package-station-container {
|
||||
.control-panel {
|
||||
gap: 20px;
|
||||
|
||||
.control-left,
|
||||
.control-center,
|
||||
.control-right {
|
||||
.control-label {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.control-input-group {
|
||||
.control-input {
|
||||
:deep(.ant-input) {
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-btn,
|
||||
.update-btn {
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.total-display {
|
||||
width: 130px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-center {
|
||||
.control-row {
|
||||
gap: 20px;
|
||||
|
||||
.control-item {
|
||||
.control-input-group {
|
||||
.control-input {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.total-display {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-right {
|
||||
gap: 10px;
|
||||
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
.scanned-table {
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
font-size: 14px;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.package-station-container {
|
||||
padding: 10px;
|
||||
|
||||
.control-panel {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
.control-left,
|
||||
.control-center,
|
||||
.control-right {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.control-center {
|
||||
.control-row {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
.control-item {
|
||||
.control-input-group {
|
||||
.control-input {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.total-display {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -36,7 +36,7 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://192.168.1.38:18081',
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user