From 8e3a26448a671e11fbe5a8ec6bf82689876630fd Mon Sep 17 00:00:00 2001 From: 86156 <823267011@qq.com> Date: Thu, 15 Jan 2026 20:18:37 +0800 Subject: [PATCH] =?UTF-8?q?app=E6=9B=B4=E6=96=B0=E5=AF=B9l2=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/hand-factory/api/business/dashboard.js | 12 +- apps/hand-factory/api/business/report.js | 12 +- apps/hand-factory/api/business/stoppage.js | 7 +- apps/hand-factory/api/login.js | 46 + apps/hand-factory/api/pocket/deviceEnum.js | 7 +- .../api/pocket/deviceFieldMeta.js | 7 +- .../hand-factory/api/pocket/deviceSnapshot.js | 12 +- .../hand-factory/components/lines/acidity.vue | 2 +- apps/hand-factory/components/lines/zinc1.vue | 1077 ++++++++++++----- apps/hand-factory/store/modules/user.js | 91 +- apps/hand-factory/utils/auth.js | 14 + apps/hand-factory/utils/request.js | 4 +- apps/hand-factory/utils/socketMeasure.js | 74 +- apps/hand-factory/utils/zinc1Request.js | 70 ++ 14 files changed, 1072 insertions(+), 363 deletions(-) create mode 100644 apps/hand-factory/utils/zinc1Request.js diff --git a/apps/hand-factory/api/business/dashboard.js b/apps/hand-factory/api/business/dashboard.js index d009059..b5c9724 100644 --- a/apps/hand-factory/api/business/dashboard.js +++ b/apps/hand-factory/api/business/dashboard.js @@ -1,20 +1,18 @@ -import request from '@/utils/request' +import zinc1Request from '@/utils/zinc1Request' // 获取当前生产中的计划信息 export function getCurrentPlan() { - return request({ + return zinc1Request({ url: '/api/business/dashboard/currentPlan', - method: 'get', - baseUrl: 'http://140.143.206.120:10082/prod-api' + method: 'get' }) } // 获取当前生产卷的关键工艺参数 export function getCurrentProcess() { - return request({ + return zinc1Request({ url: '/api/business/dashboard/currentProcess', - method: 'get', - baseUrl: 'http://140.143.206.120:10082/prod-api' + method: 'get' }) } diff --git a/apps/hand-factory/api/business/report.js b/apps/hand-factory/api/business/report.js index 7d188b0..51d4005 100644 --- a/apps/hand-factory/api/business/report.js +++ b/apps/hand-factory/api/business/report.js @@ -1,22 +1,20 @@ -import request from '@/utils/request' +import zinc1Request from '@/utils/zinc1Request' // 生产实绩汇总 export function getReportSummary(params) { - return request({ + return zinc1Request({ url: '/api/report/summary', method: 'get', - params, - baseUrl: 'http://140.143.206.120:10082/prod-api' + params }) } // 生产实绩明细 export function getReportDetails(params) { - return request({ + return zinc1Request({ url: '/api/report/details', method: 'get', - params, - baseUrl: 'http://140.143.206.120:10082/prod-api' + params }) } diff --git a/apps/hand-factory/api/business/stoppage.js b/apps/hand-factory/api/business/stoppage.js index 9c0331f..ade1f83 100644 --- a/apps/hand-factory/api/business/stoppage.js +++ b/apps/hand-factory/api/business/stoppage.js @@ -1,12 +1,11 @@ -import request from '@/utils/request' +import zinc1Request from '@/utils/zinc1Request' // 停机记录列表 export function listStoppage(data) { - return request({ + return zinc1Request({ url: '/api/stoppage/list', method: 'post', - data, - baseUrl: 'http://140.143.206.120:10082/prod-api' + data }) } diff --git a/apps/hand-factory/api/login.js b/apps/hand-factory/api/login.js index 555e578..33f587a 100644 --- a/apps/hand-factory/api/login.js +++ b/apps/hand-factory/api/login.js @@ -1,4 +1,6 @@ import request from '@/utils/request' +import errorCode from '@/utils/errorCode' +import { toast, tansParams } from '@/utils/common' // 登录方法 export function login(username, password, code, uuid) { @@ -18,6 +20,50 @@ export function login(username, password, code, uuid) { }) } +// Zinc1系统登录方法(直接使用uni.request,因为登录接口不需要token,完全静默处理) +export function loginZinc1(username, password, code, uuid) { + const data = { + username, + password, + code, + uuid + } + + const baseUrl = 'http://140.143.206.120:10082/prod-api' + const timeout = 10000 + + return new Promise((resolve, reject) => { + uni.request({ + method: 'post', + timeout: timeout, + url: baseUrl + '/login', + data: data, + header: { + 'Content-Type': 'application/json' + }, + dataType: 'json' + }).then(response => { + let [error, res] = response + if (error) { + // 静默失败,不显示任何提示 + reject('Zinc1系统连接异常') + return + } + const code = res.data.code || 200 + if (code === 200 && res.data && res.data.token) { + // 只有成功时才resolve + resolve(res.data) + } else { + // 其他情况静默失败 + reject('Zinc1登录失败') + } + }).catch(error => { + // 静默失败,不显示任何提示 + reject(error) + }) + }) +} + // 注册方法 export function register(data) { return request({ diff --git a/apps/hand-factory/api/pocket/deviceEnum.js b/apps/hand-factory/api/pocket/deviceEnum.js index 6b52c18..1c6ba41 100644 --- a/apps/hand-factory/api/pocket/deviceEnum.js +++ b/apps/hand-factory/api/pocket/deviceEnum.js @@ -1,10 +1,9 @@ -import request from '@/utils/request' +import zinc1Request from '@/utils/zinc1Request' export function listDeviceEnumAll() { - return request({ + return zinc1Request({ url: '/api/deviceEnum/all', - method: 'get', - baseUrl: 'http://140.143.206.120:10082/prod-api' + method: 'get' }) } diff --git a/apps/hand-factory/api/pocket/deviceFieldMeta.js b/apps/hand-factory/api/pocket/deviceFieldMeta.js index b59c298..dfc60d7 100644 --- a/apps/hand-factory/api/pocket/deviceFieldMeta.js +++ b/apps/hand-factory/api/pocket/deviceFieldMeta.js @@ -1,10 +1,9 @@ -import request from '@/utils/request' +import zinc1Request from '@/utils/zinc1Request' export function getDeviceFieldMetaAll() { - return request({ + return zinc1Request({ url: '/api/deviceFieldMeta/all', - method: 'get', - baseUrl: 'http://140.143.206.120:10082/prod-api' + method: 'get' }) } diff --git a/apps/hand-factory/api/pocket/deviceSnapshot.js b/apps/hand-factory/api/pocket/deviceSnapshot.js index d87940e..2d8a6c7 100644 --- a/apps/hand-factory/api/pocket/deviceSnapshot.js +++ b/apps/hand-factory/api/pocket/deviceSnapshot.js @@ -1,22 +1,20 @@ -import request from '@/utils/request' +import zinc1Request from '@/utils/zinc1Request' // 获取最新N条设备快照 export function listDeviceSnapshotLatest(params) { - return request({ + return zinc1Request({ url: '/api/deviceSnapshot/latest', method: 'get', - params, - baseUrl: 'http://140.143.206.120:10082/prod-api' + params }) } // 按时间范围查询设备快照 export function listDeviceSnapshotRange(params) { - return request({ + return zinc1Request({ url: '/api/deviceSnapshot/range', method: 'get', - params, - baseUrl: 'http://140.143.206.120:10082/prod-api' + params }) } diff --git a/apps/hand-factory/components/lines/acidity.vue b/apps/hand-factory/components/lines/acidity.vue index ee4af6c..27a6382 100644 --- a/apps/hand-factory/components/lines/acidity.vue +++ b/apps/hand-factory/components/lines/acidity.vue @@ -404,7 +404,7 @@ export default { gridColor: "#e4e7ed", showTitle: true, fontSize: 10, - data: [{ min: 0, title: "温度(°C)" }] + data: [{ min: 0, title: "m/min" }] }, extra: { line: { diff --git a/apps/hand-factory/components/lines/zinc1.vue b/apps/hand-factory/components/lines/zinc1.vue index 4d82438..38f575c 100644 --- a/apps/hand-factory/components/lines/zinc1.vue +++ b/apps/hand-factory/components/lines/zinc1.vue @@ -28,12 +28,12 @@ - Socket + 网络状态 {{ isConnected ? '已连接' : '未连接' }} - 更新时间 + 更新 {{ lastUpdateTime }} @@ -41,21 +41,40 @@ 设备数 {{ deviceDefs.length }} - - - 折线图 - - {{ isChartPaused ? '已暂停刷新' : '实时刷新中' }} - + + + + + 带钢状态 + + + + 带钢位置 + {{ getRealtimeValueBySource('ENTRY', 'stripLocation') }} + m + + + + + {{ stripLocationProgress.toFixed(1) }}% + + + + + 带钢速度 + {{ getRealtimeValueBySource('ENTRY', 'stripSpeed') }} + m/min + + + + + - 入口段(实时) + 入口段 {{ it.label || getFieldLabel(it.field) }} @@ -69,21 +88,21 @@ - 退火炉段(实时) + 退火炉段 - - {{ it.label || getFieldLabel(it.field) }} - {{ getRealtimeValueBySource('FURNACE', it.field) }} - {{ it.unit || getFieldUnit(it.field) }} + + {{ it.label || getFieldLabel(it.actualField) }} + {{ getRealtimeValueBySource('FURNACE', it.actualField) }} + {{ it.unit || getFieldUnit(it.actualField) }} + + 设定: {{ getRealtimeValueBySource('FURNACE', it.setField) }} + - - - - 后处理/涂层段(实时) + 后处理/涂层段 {{ it.label || getFieldLabel(it.field) }} @@ -97,7 +116,7 @@ - 出口段(实时) + 出口段 {{ it.label || getFieldLabel(it.field) }} @@ -113,33 +132,7 @@ - - 当前生产情况 - - - - 当前钢卷 - 钢种 - 规格 (厚×宽) - 计划长度 - 计划重量 - - - {{ currentPlan.exitMatId || '—' }} - {{ currentPlan.steelGrade || '—' }} - - {{ formatNum(currentPlan.exitThickness) }} × {{ formatNum(currentPlan.exitWidth) }} - - {{ formatNum(currentPlan.exitLength) }} - {{ formatNum(currentPlan.exitWeight) }} - - - - 暂无当前生产计划数据。 - - - - + 生产实绩汇总 @@ -168,28 +161,39 @@ 生产实绩明细 - - - 成品卷号 - 原料卷号 - 班/组 - 规格 (厚×宽) - 长度/重量 - + - {{ item.exitMatId }} - {{ item.entryMatId }} - {{ item.groupNo || '—' }}/{{ item.shiftNo || '—' }} - - {{ formatNum(item.exitThickness) }} × {{ formatNum(item.exitWidth) }} - - - {{ formatNum(item.exitLength) }} m / {{ formatNum(item.actualWeight) }} t - + + 成品卷号:{{ item.exitMatId }} + + + + 原料卷号 + {{ item.entryMatId || '—' }} + + + 班/组 + {{ item.groupNo || '—' }} / {{ item.shiftNo || '—' }} + + + 规格 + + {{ formatNum(item.exitThickness) }} × {{ formatNum(item.exitWidth) }} + + + + 长度 + {{ formatNum(item.exitLength) }} m + + + 重量 + {{ formatNum(item.actualWeight) }} t + + @@ -210,26 +214,38 @@ - - - 开始时间 - 结束时间 - 持续时间 - 区域/设备 - 原因 - + - {{ item.startDate }} - {{ item.endDate }} - {{ item.duration }} - - {{ item.area || '—' }}/{{ item.seton || '—' }} - - {{ item.remark || '—' }} + + 停机记录 #{{ index + 1 }} + {{ formatDuration(item.duration) }} + + + + 开始时间 + {{ item.startDate || '—' }} + + + 结束时间 + {{ item.endDate || '—' }} + + + 区域 + {{ item.area || '—' }} + + + 设备 + {{ item.seton || '—' }} + + + 原因 + {{ item.remark }} + + @@ -262,8 +278,10 @@ export default { isRefreshing: false, isConnected: false, lastUpdateTime: '—', - // 折线图实时刷新开关(避免查看某个点时被新数据刷掉) - isChartPaused: false, + // 记录起始时间,用于计算运行时长 + startTime: null, + // socket数据是否已补充到折线图(仅补充一次) + socketDataAppended: false, // socket最新消息 latestMeasure: null, @@ -274,15 +292,8 @@ export default { // 字段元数据:fieldName -> {label, unit, description} fieldMeta: {}, - // device_field_trend: deviceCode -> { fieldName -> trendDTO } - fieldTrendMap: {}, - - // 懒加载:已订阅的设备列表 - subscribedDeviceCodes: [], - // 实时数据卡片定义(按酸轧页组织,不按设备分块) entryMetrics: [ - { field: 'entryCoilId', label: '入口卷号', unit: '' }, { field: 'stripLocation', label: '带钢位置', unit: 'm' }, { field: 'stripSpeed', label: '带钢速度', unit: 'm/min' }, { field: 'tensionPorBr1', label: '入口张力 POR-BR1', unit: 'daN' }, @@ -290,10 +301,20 @@ export default { { field: 'tensionBr2Br3', label: '张力 BR2-BR3', unit: 'daN' } ], furnaceMetrics: [ - { field: 'phFurnaceTemperatureActual', label: 'PH炉温', unit: '℃' }, - { field: 'rtf1FurnaceTemperatureActual', label: 'RTF1炉温', unit: '℃' }, - { field: 'potTemperature', label: '锌锅温度', unit: '℃' }, - { field: 'potPower', label: '锌锅功率', unit: '' } + { actualField: 'jcf1FurnaceTemperatureActual', setField: 'jcf1FurnaceTemperatureSet', label: 'JCF1炉温', unit: '℃' }, + { actualField: 'jcf2FurnaceTemperatureActual', setField: 'jcf2FurnaceTemperatureSet', label: 'JCF2炉温', unit: '℃' }, + { actualField: 'lbzFurnaceTemperatureActual', setField: 'lbzFurnaceTemperatureSet', label: 'LBZ炉温', unit: '℃' }, + { actualField: 'lthFurnaceTemperatureActual', setField: 'lthFurnaceTemperatureSet', label: 'LTH炉温', unit: '℃' }, + { actualField: 'nof1FurnaceTemperatureActual', setField: 'nof1FurnaceTemperatureSet', label: 'NOF1炉温', unit: '℃' }, + { actualField: 'nof2FurnaceTemperatureActual', setField: 'nof2FurnaceTemperatureSet', label: 'NOF2炉温', unit: '℃' }, + { actualField: 'nof3FurnaceTemperatureActual', setField: 'nof3FurnaceTemperatureSet', label: 'NOF3炉温', unit: '℃' }, + { actualField: 'nof4FurnaceTemperatureActual', setField: 'nof4FurnaceTemperatureSet', label: 'NOF4炉温', unit: '℃' }, + { actualField: 'nof5FurnaceTemperatureActual', setField: 'nof5FurnaceTemperatureSet', label: 'NOF5炉温', unit: '℃' }, + { actualField: 'phFurnaceTemperatureActual', setField: null, label: 'PH炉温', unit: '℃' }, + { actualField: 'rtf1FurnaceTemperatureActual', setField: 'rtf1FurnaceTemperatureSet', label: 'RTF1炉温', unit: '℃' }, + { actualField: 'rtf2FurnaceTemperatureActual', setField: 'rtf2FurnaceTemperatureSet', label: 'RTF2炉温', unit: '℃' }, + { actualField: 'sfFurnaceTemperatureActual', setField: 'sfFurnaceTemperatureSet', label: 'SF炉温', unit: '℃' }, + { actualField: 'tdsFurnaceTemperatureActual', setField: 'tdsFurnaceTemperatureSet', label: 'TDS炉温', unit: '℃' }, ], coatMetrics: [ { field: 'avrCoatingWeightTop', label: '上层平均涂层重量', unit: 'g/m²' }, @@ -302,6 +323,24 @@ export default { { field: 'stripSpeedTmExit', label: 'TM出口速度', unit: 'm/min' } ], exitMetrics: [ + { field: 'alkaliConcentration', label: '碱液浓度', unit: '' }, + { field: 'alkaliTemperature', label: '碱液温度', unit: '℃' }, + { field: 'celCapacity', label: '电池容量', unit: '' }, + { field: 'celLength', label: '电池长度', unit: 'm' }, + { field: 'cleaningCurrent', label: '清洗电流', unit: 'A' }, + { field: 'cleaningVoltage', label: '清洗电压', unit: 'V' }, + { field: 'dryingTemperature', label: '干燥温度', unit: '℃' }, + { field: 'entryCoilId', label: '入口卷号', unit: '' }, + { field: 'hotAirFlow', label: '热风流量', unit: '' }, + { field: 'hotAirPressure', label: '热风压力', unit: '' }, + { field: 'payOffReelNumber', label: '放卷卷号', unit: '' }, + { field: 'rinseConductivity', label: '冲洗电导率', unit: '' }, + { field: 'rinseTemperature', label: '冲洗温度', unit: '℃' }, + { field: 'stripLocation', label: '带钢位置', unit: 'm' }, + { field: 'stripSpeed', label: '带钢速度', unit: 'm/min' }, + { field: 'tensionBr1Br2', label: '张力 BR1-BR2', unit: 'daN' }, + { field: 'tensionBr2Br3', label: '张力 BR2-BR3', unit: 'daN' }, + { field: 'tensionPorBr1', label: '入口张力 POR-BR1', unit: 'daN' }, { field: 'tensionBr8Br9', label: '张力 BR8-BR9', unit: 'daN' }, { field: 'tensionBr9Tr', label: '张力 BR9-TR', unit: 'daN' }, { field: 'speedExitSection', label: '出口速度', unit: 'm/min' }, @@ -310,11 +349,26 @@ export default { // 前端历史缓存(打开页面即可出趋势) chartMaxPoints: 60, + // 带钢速度折线图最大点数(单独配置) + stripSpeedMaxPoints: 30, chartSeries: { entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] }, - furnace: { time: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], potTemperature: [] }, + furnace: { + time: [], + jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [], + lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [], + nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [], + nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [], + nof5FurnaceTemperatureActual: [], + phFurnaceTemperatureActual: [], + rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [], + sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [], + potTemperature: [] + }, coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] }, - exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] } + exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] }, + // 带钢速度实时折线图(持续刷新) + stripSpeed: { time: [], stripSpeed: [] } }, // 生产统计(实绩) @@ -329,6 +383,9 @@ export default { stoppageLoading: false, stoppageMonth: '', // yyyy-MM + // 组件销毁标志,用于防止异步操作在组件销毁后更新数据 + isDestroyed: false, + lineChartOpts: { color: ['#0066cc', '#409eff', '#66b1ff', '#a0cfff', '#d9ecff', '#ecf5ff'], padding: [15, 15, 0, 15], @@ -347,41 +404,95 @@ export default { }, extra: { line: { type: 'curve', width: 2, activeType: 'hollow' } } }, + // 带钢速度折线图配置(Y轴固定为100-150,完全禁用动画) + stripSpeedChartOpts: { + color: ['#0066cc'], + padding: [15, 15, 0, 15], + enableScroll: false, + legend: { show: true, position: 'top', fontSize: 10, lineHeight: 14, itemGap: 6 }, + dataLabel: false, + dataPointShape: false, + animation: false, // 禁用动画,直接显示到值上 + animationDuration: 0, // 动画时长为0,确保无动画效果 + xAxis: { disableGrid: true, rotateLabel: true, itemCount: 5, labelCount: 5, fontSize: 10 }, + yAxis: { + gridType: 'dash', + dashLength: 4, + gridColor: '#e4e7ed', + showTitle: true, + fontSize: 10, + data: [{ + title: '速度(m/min)', + min: 80, + max: 150 + }] + }, + extra: { line: { type: 'curve', width: 2, activeType: 'hollow' } } + }, - socketClient: null, - fieldTrendSocket: null, - fieldTrendSubscribed: false + socketClient: null } }, mounted() { + if (this.isDestroyed) return + // 记录起始时间 + this.startTime = new Date() this.initSocket() - Promise.all([this.loadDeviceDefs(), this.loadFieldMeta(), this.loadHistoryForCharts()]).then(() => {}) + Promise.all([this.loadDeviceDefs(), this.loadFieldMeta(), this.loadHistoryForCharts()]).then(() => { + // Promise完成后检查组件是否已销毁 + if (this.isDestroyed) return + }).catch(() => { + // 错误处理中也要检查 + if (this.isDestroyed) return + }) this.updateLastTime() - - // 周期性从后端快照刷新折线图数据(降低刷新频率,避免频繁打断用户查看) - // 这里用 60 秒刷一次,后端快照是每 5 分钟一条,折线图变化会比较平滑 - this._historyTimer = setInterval(() => { - if (!this.isChartPaused) { - this.loadHistoryForCharts() - } - }, 60 * 1000) }, computed: { stoppageMonthLabel() { + if (this.isDestroyed) return '选择月份' if (!this.stoppageMonth) return '选择月份' const parts = (this.stoppageMonth || '').split('-') if (parts.length < 2) return this.stoppageMonth const [y, m] = parts return `${y}年${m}月` + }, + // 带钢位置进度(位置/3000) + stripLocationProgress() { + if (this.isDestroyed) return 0 + const m = this.latestMeasure || {} + const entry = m.appMeasureEntryMessage || {} + const location = entry.stripLocation !== null && entry.stripLocation !== undefined + ? parseFloat(entry.stripLocation) || 0 + : 0 + const maxLocation = 3000 + const progress = (location / maxLocation) * 100 + return Math.min(Math.max(progress, 0), 100) + }, + // 带钢速度折线图数据(使用计算属性,确保数据引用稳定) + stripSpeedChartData() { + if (this.isDestroyed) return { categories: [], series: [] } + const g = this.chartSeries.stripSpeed + if (!g) return { categories: [], series: [] } + // 返回新对象,但保持数组引用稳定,避免触发重新初始化 + const timeArray = g.time || [] + const speedArray = g.stripSpeed || [] + return { + categories: timeArray, + series: [{ + name: '带钢速度', + data: speedArray + }] + } } }, watch: { currentTab(newVal) { + if (this.isDestroyed) return if (newVal === 2) { this.loadReportData() } else if (newVal === 3) { @@ -398,55 +509,89 @@ export default { }, beforeDestroy() { - if (this.socketClient) this.socketClient.close() - if (this.fieldTrendSocket) this.fieldTrendSocket.close() - if (this._historyTimer) { - clearInterval(this._historyTimer) - this._historyTimer = null + // 先设置销毁标志,防止异步回调访问已销毁的组件 + this.isDestroyed = true + + // 设置闭包中的销毁标志(必须在关闭socket之前,且要立即执行) + // 这会取消所有待执行的异步更新 + if (this._clearSocketFlag) { + try { + this._clearSocketFlag() + } catch (e) { + // ignore + } + // 清空引用,防止再次调用 + this._clearSocketFlag = null + } + + // 清理定时器 + if (this._scrollTimer) { + clearTimeout(this._scrollTimer) + this._scrollTimer = null + } + + // 关闭socket连接并清空回调(必须立即执行) + if (this.socketClient) { + try { + // 先清空回调,防止关闭过程中触发回调 + if (this.socketClient.clearCallbacks) { + this.socketClient.clearCallbacks() + } + // 立即关闭socket + this.socketClient.close() + } catch (e) { + // ignore + } + // 立即清空引用 + this.socketClient = null + } + + // 清空所有响应式数据引用,防止后续访问 + try { + this.latestMeasure = null + this.chartSeries = null + } catch (e) { + // ignore } }, methods: { async loadDeviceDefs() { const res = await listDeviceEnumAll() - // 打印响应,排查是否拿到 data 数组(避免页面空白) - try { - console.log(res) - } catch (e) { - // ignore - } + if (this.isDestroyed) return this.deviceDefs = (res && res.data) || [] - - }, async loadFieldMeta() { const res = await getDeviceFieldMetaAll() - // 打印响应,排查字段元数据是否加载成功 - try { - const keys = res && res.data && typeof res.data === 'object' ? Object.keys(res.data).length : -1 - console.log('[resp]/api/deviceFieldMeta/all:', { - code: res && res.code, - msg: res && res.msg, - dataType: res && res.data ? Object.prototype.toString.call(res.data) : 'null', - keyCount: keys - }) - } catch (e) { - // ignore - } + if (this.isDestroyed) return this.fieldMeta = (res && res.data) || {} }, - // 加载历史快照,填充折线图的“历史部分” + // 加载历史快照,填充折线图的"历史部分" async loadHistoryForCharts() { try { // 使用设备快照作为折线图唯一数据源,避免每条 socket 消息都触发重绘 // 每次加载前先清空本地缓存,防止重复累加 + // 注意:stripSpeed 数据不清空,保持实时更新连续性 this.chartSeries = { entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] }, - furnace: { time: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], potTemperature: [] }, + furnace: { + time: [], + jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [], + lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [], + nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [], + nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [], + nof5FurnaceTemperatureActual: [], + phFurnaceTemperatureActual: [], + rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [], + sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [], + potTemperature: [] + }, coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] }, - exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] } + exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] }, + // stripSpeed 保持现有数据,不清空,确保实时更新连续性 + stripSpeed: this.chartSeries.stripSpeed || { time: [], stripSpeed: [] } } const limit = this.chartMaxPoints @@ -456,7 +601,17 @@ export default { entry: { deviceCode: 'POR1', fields: ['stripSpeed', 'tensionPorBr1', 'tensionBr1Br2', 'tensionBr2Br3'] }, furnace: { deviceCode: 'FUR2', - fields: ['phFurnaceTemperatureActual', 'rtf1FurnaceTemperatureActual', 'potTemperature'] + fields: [ + 'jcf1FurnaceTemperatureActual', 'jcf2FurnaceTemperatureActual', + 'lbzFurnaceTemperatureActual', 'lthFurnaceTemperatureActual', + 'nof1FurnaceTemperatureActual', 'nof2FurnaceTemperatureActual', + 'nof3FurnaceTemperatureActual', 'nof4FurnaceTemperatureActual', + 'nof5FurnaceTemperatureActual', + 'phFurnaceTemperatureActual', + 'rtf1FurnaceTemperatureActual', 'rtf2FurnaceTemperatureActual', + 'sfFurnaceTemperatureActual', 'tdsFurnaceTemperatureActual', + 'potTemperature' + ] }, coat: { deviceCode: 'COAT', @@ -470,10 +625,12 @@ export default { tasks.push( listDeviceSnapshotLatest({ limit, deviceCode: cfg.deviceCode }) .then((res) => { + if (this.isDestroyed) return const rows = (res && res.data) || [] // 按时间升序 const list = rows.slice().reverse() list.forEach((row) => { + if (this.isDestroyed) return const t = (row.createTime || '').slice(11, 19) // HH:mm:ss let snap = {} try { @@ -499,77 +656,147 @@ export default { }, initSocket() { + // 使用闭包保存组件引用和销毁标志,避免直接访问 this + const vm = this + let isDestroyed = false + // 保存所有待执行的异步操作ID,用于取消 + const pendingUpdates = [] + // 实时测量数据 WebSocket this.socketClient = createMeasureSocket({ type: 'track_measure', onOpen: () => { - console.log('初始化socket成功') - this.isConnected = true + if (isDestroyed) return + try { + vm.isConnected = true + } catch (e) { + // 组件已销毁,忽略错误 + } }, onClose: () => { - this.isConnected = false + if (isDestroyed) return + try { + vm.isConnected = false + } catch (e) { + // 组件已销毁,忽略错误 + } }, onError: () => { - this.isConnected = false + if (isDestroyed) return + try { + vm.isConnected = false + } catch (e) { + // 组件已销毁,忽略错误 + } }, onMessage: (data) => { + // 在函数最开始就检查,避免任何后续操作 + if (isDestroyed) return + // 双重检查:如果组件实例不存在,直接返回 + if (!vm) { + isDestroyed = true + return + } + + // 使用 try-catch 包裹整个函数,防止任何错误 try { - // 兼容字符串 / 已解析对象两种情况,并打日志方便排查 + // 兼容字符串 / 已解析对象两种情况 let payload = null if (typeof data === 'string') { payload = JSON.parse(data) } else if (data && typeof data === 'object') { - // uni 有时会包一层 { data: 'xxx' } if (typeof data.data === 'string') { payload = JSON.parse(data.data) } else { payload = data } } - if (!payload) return - this.latestMeasure = payload - // 实时卡片仍然使用最新测量值,折线图改为走后端快照,避免高频刷新 - this.updateLastTime() + + // 再次检查,防止在解析过程中组件被销毁 + if (!payload || isDestroyed || !vm) return + + // 使用 requestAnimationFrame 延迟更新 + // 如果组件已销毁,requestAnimationFrame 回调不会执行 + const updateFrame = () => { + // 多重检查:确保组件仍然存在 + if (isDestroyed || !vm) return + + // 检查 Vue 实例状态 - 使用更安全的方式 + try { + // 检查组件是否真的还存在且可用 + if (!vm || typeof vm !== 'object') { + isDestroyed = true + return + } + // 尝试访问 Vue 实例的内部属性来验证 + if (vm._isDestroyed === true || vm._isBeingDestroyed === true) { + isDestroyed = true + return + } + } catch (e) { + isDestroyed = true + return + } + + // 安全地更新数据 - 使用 Object.defineProperty 或直接赋值 + try { + // 直接赋值,但确保组件存在 + if (vm && !vm._isDestroyed && !vm._isBeingDestroyed) { + vm.latestMeasure = payload + if (!vm.socketDataAppended) { + vm.appendChartPoint(payload) + vm.socketDataAppended = true + } + vm.appendStripSpeedPoint(payload) + vm.updateLastTime() + } + } catch (e) { + // 如果更新失败,设置销毁标志 + isDestroyed = true + } + } + + // 使用 requestAnimationFrame 或 setTimeout,并保存ID以便取消 + let frameId = null + if (typeof requestAnimationFrame !== 'undefined') { + frameId = requestAnimationFrame(updateFrame) + pendingUpdates.push(frameId) + } else { + frameId = setTimeout(updateFrame, 0) + pendingUpdates.push(frameId) + } } catch (e) { - console.error('解析 track_measure 数据失败:', e, data) + // 解析错误或其他错误,静默处理 + if (!isDestroyed) { + try { + console.error('解析 track_measure 数据失败:', e) + } catch (err) { + // 忽略日志错误 + } + } } } }) + // 保存清空标志的函数引用 + this._clearSocketFlag = () => { + isDestroyed = true + // 取消所有待执行的异步更新 + pendingUpdates.forEach(id => { + try { + if (typeof cancelAnimationFrame !== 'undefined') { + cancelAnimationFrame(id) + } else { + clearTimeout(id) + } + } catch (e) { + // ignore + } + }) + pendingUpdates.length = 0 + } + this.socketClient.connect() - - // 点位趋势 WebSocket(懒加载订阅) - this.fieldTrendSocket = createMeasureSocket({ - type: 'device_field_trend', - onOpen: () => { - console.log('点位趋势 WebSocket 连接成功') - // 初次连接后先不订阅,等滚动/展开触发 - this.fieldTrendSubscribed = true - this.sendFieldTrendSubscribe([]) - }, - onClose: () => { - console.log('点位趋势 WebSocket 连接关闭') - this.fieldTrendSubscribed = false - }, - onError: () => { - console.error('点位趋势 WebSocket 连接错误') - this.fieldTrendSubscribed = false - }, - onMessage: (data) => { - try { - const trend = JSON.parse(data) - if (!trend || !trend.deviceCode || !trend.fieldName) return - if (!this.fieldTrendMap[trend.deviceCode]) { - this.$set(this.fieldTrendMap, trend.deviceCode, {}) - } - this.$set(this.fieldTrendMap[trend.deviceCode], trend.fieldName, trend) - } catch (e) { - console.error('解析点位趋势数据失败:', e) - } - } - }) - - this.fieldTrendSocket.connect() }, // 加载生产统计 / 实绩数据 @@ -580,32 +807,84 @@ export default { // 当前计划 & 当前工艺 try { const planRes = await getCurrentPlan() + if (this.isDestroyed) return this.currentPlan = (planRes && planRes.data) || null } catch (e) { + if (this.isDestroyed) return this.currentPlan = null } try { const procRes = await getCurrentProcess() + if (this.isDestroyed) return this.currentProcess = (procRes && procRes.data) || null } catch (e) { + if (this.isDestroyed) return this.currentProcess = null } // 实绩汇总 & 明细:不传筛选条件,后端按默认时间范围处理 + let summaryData = null + let detailsData = [] + try { const sumRes = await getReportSummary({}) - this.reportSummary = sumRes || null + if (this.isDestroyed) return + // 处理可能的多种返回结构:直接数据、{data: {...}}、{rows: [...], total: ...} + if (sumRes) { + if (sumRes.data && typeof sumRes.data === 'object' && !Array.isArray(sumRes.data)) { + summaryData = sumRes.data + } else { + summaryData = sumRes + } + } } catch (e) { - this.reportSummary = null + if (this.isDestroyed) return + console.error('加载实绩汇总失败:', e) } + try { const detRes = await getReportDetails({}) - this.reportDetails = (detRes && Array.isArray(detRes) ? detRes : []) || [] + if (this.isDestroyed) return + // 处理可能的多种返回结构 + if (detRes) { + if (Array.isArray(detRes)) { + detailsData = detRes + } else if (detRes.data && Array.isArray(detRes.data)) { + detailsData = detRes.data + } else if (detRes.rows && Array.isArray(detRes.rows)) { + detailsData = detRes.rows + } + } } catch (e) { - this.reportDetails = [] + if (this.isDestroyed) return + console.error('加载实绩明细失败:', e) } + + // 统一处理汇总数据:修正coilCount + if (this.isDestroyed) return + + if (summaryData) { + // 如果coilCount不存在或看起来像是页数(通常页数很小),尝试从其他字段获取 + if (summaryData.total !== undefined && + (summaryData.coilCount === undefined || + (summaryData.coilCount < 10 && summaryData.total > summaryData.coilCount))) { + // 优先使用total字段 + summaryData.coilCount = summaryData.total + } else if (detailsData.length > 0 && + (summaryData.coilCount === undefined || summaryData.coilCount < detailsData.length)) { + // 如果total不存在,使用明细数组长度作为总数 + summaryData.coilCount = detailsData.length + } + this.reportSummary = summaryData + } else { + this.reportSummary = null + } + + this.reportDetails = detailsData } finally { - this.reportLoading = false + if (!this.isDestroyed) { + this.reportLoading = false + } } }, @@ -627,11 +906,15 @@ export default { endDate = `${last.getFullYear()}-${pad(last.getMonth() + 1)}-${pad(last.getDate())}` } const res = await listStoppage({ startDate, endDate }) + if (this.isDestroyed) return this.stoppageList = (res && res.data) || [] } catch (e) { + if (this.isDestroyed) return this.stoppageList = [] } finally { - this.stoppageLoading = false + if (!this.isDestroyed) { + this.stoppageLoading = false + } } }, @@ -642,94 +925,22 @@ export default { this.loadStoppageData() }, - // 滚动事件:节流触发订阅更新 - onScroll() { - if (this._scrollTimer) return - this._scrollTimer = setTimeout(() => { - this._scrollTimer = null - this.updateVisibleDeviceSubscriptions() - }, 200) - }, - - // 计算当前可视区域内的设备,并向后端发送订阅(懒加载) - updateVisibleDeviceSubscriptions() { - const list = this.deviceDefs || [] - if (!list.length) return - - // 使用 selectorQuery 获取每个设备块的位置 - const query = uni.createSelectorQuery().in(this) - list.forEach((d) => { - query.select('#device-' + d.deviceCode).boundingClientRect() - }) - query.selectViewport().scrollOffset() - - query.exec((res) => { - // res: [rect1, rect2, ..., viewport] - if (!res || res.length < 2) return - const viewport = res[res.length - 1] || {} - const scrollTop = viewport.scrollTop || 0 - const windowHeight = viewport.windowHeight || 0 - - // 预加载阈值:提前 0.5 屏开始订阅 - const preload = windowHeight * 0.5 - const bottom = scrollTop + windowHeight + preload - - // 找到“已经进入/即将进入”的最大索引 - let maxIdx = -1 - for (let i = 0; i < list.length; i++) { - const rect = res[i] - if (!rect) continue - const top = rect.top + scrollTop - if (top <= bottom) { - maxIdx = i - } - } - - // 订阅规则:当前可视(含预加载)位置的设备及其以前全部设备 - const nextList = maxIdx >= 0 ? list.slice(0, maxIdx + 1).map((d) => d.deviceCode) : [] - - if (JSON.stringify(nextList) === JSON.stringify(this.subscribedDeviceCodes || [])) return - this.subscribedDeviceCodes = nextList - this.sendFieldTrendSubscribe(nextList) - }) - }, - - - getFieldLabel(field) { + if (this.isDestroyed) return field || '' const meta = this.fieldMeta[field] return (meta && meta.label) || field }, getFieldUnit(field) { + if (this.isDestroyed) return '' const meta = this.fieldMeta[field] return (meta && meta.unit) || '' }, - sendFieldTrendSubscribe(deviceCodes) { - if (!this.fieldTrendSocket || !this.fieldTrendSubscribed) return - this.fieldTrendSocket.send({ - lazy: true, - deviceCodes: deviceCodes || [] - }) - }, - - // 折线图数据转换(仅结构转换,不做计算) - toLineChart(trend, fieldName) { - if (!trend) return { categories: [], series: [] } - return { - categories: trend.categories || [], - series: [ - { - name: this.getFieldLabel(fieldName), - data: trend.data || [] - } - ] - } - }, // 根据 sourceType + 字段名取实时值(不按设备分块) getRealtimeValueBySource(sourceType, field) { + if (this.isDestroyed) return '—' const m = this.latestMeasure || {} let msg = {} switch (sourceType) { @@ -755,6 +966,7 @@ export default { // 追加一帧到前端历史缓存(用于统计折线图) appendChartPoint(payload) { + if (this.isDestroyed) return const now = new Date() const hh = String(now.getHours()).padStart(2, '0') const mm = String(now.getMinutes()).padStart(2, '0') @@ -774,8 +986,20 @@ export default { }) this.pushSeries('furnace', t, { + jcf1FurnaceTemperatureActual: furnace.jcf1FurnaceTemperatureActual, + jcf2FurnaceTemperatureActual: furnace.jcf2FurnaceTemperatureActual, + lbzFurnaceTemperatureActual: furnace.lbzFurnaceTemperatureActual, + lthFurnaceTemperatureActual: furnace.lthFurnaceTemperatureActual, + nof1FurnaceTemperatureActual: furnace.nof1FurnaceTemperatureActual, + nof2FurnaceTemperatureActual: furnace.nof2FurnaceTemperatureActual, + nof3FurnaceTemperatureActual: furnace.nof3FurnaceTemperatureActual, + nof4FurnaceTemperatureActual: furnace.nof4FurnaceTemperatureActual, + nof5FurnaceTemperatureActual: furnace.nof5FurnaceTemperatureActual, phFurnaceTemperatureActual: furnace.phFurnaceTemperatureActual, rtf1FurnaceTemperatureActual: furnace.rtf1FurnaceTemperatureActual, + rtf2FurnaceTemperatureActual: furnace.rtf2FurnaceTemperatureActual, + sfFurnaceTemperatureActual: furnace.sfFurnaceTemperatureActual, + tdsFurnaceTemperatureActual: furnace.tdsFurnaceTemperatureActual, potTemperature: furnace.potTemperature }) @@ -794,8 +1018,14 @@ export default { }, pushSeries(group, time, values) { + if (this.isDestroyed) return const g = this.chartSeries[group] if (!g) return + + // 带钢速度折线图使用单独的最大点数限制 + const maxPoints = group === 'stripSpeed' ? this.stripSpeedMaxPoints : this.chartMaxPoints + + // 直接修改数组内容,保持数组引用不变,避免触发图表重新初始化 g.time.push(time) Object.keys(values).forEach((k) => { if (!g[k]) return @@ -803,16 +1033,21 @@ export default { const num = raw === null || raw === undefined || raw === '' ? null : Number(raw) g[k].push(Number.isFinite(num) ? num : null) }) - if (g.time.length > this.chartMaxPoints) { + + // 如果超过最大点数,去掉最早的数据 + if (g.time.length > maxPoints) { g.time.shift() Object.keys(values).forEach((k) => { - if (g[k] && g[k].length > this.chartMaxPoints) g[k].shift() + if (g[k] && g[k].length > maxPoints) { + g[k].shift() + } }) } }, // 生成多序列折线图数据 toGroupLineChart(group) { + if (this.isDestroyed) return { categories: [], series: [] } const g = this.chartSeries[group] if (!g) return { categories: [], series: [] } const seriesKeys = Object.keys(g).filter((k) => k !== 'time') @@ -825,9 +1060,22 @@ export default { } }, - // 切换折线图实时刷新状态 - toggleChartPause() { - this.isChartPaused = !this.isChartPaused + // 追加带钢速度点到折线图(持续刷新) + appendStripSpeedPoint(payload) { + if (this.isDestroyed) return + const entry = payload.appMeasureEntryMessage || {} + if (entry && entry.stripSpeed !== null && entry.stripSpeed !== undefined) { + const now = new Date() + const hh = String(now.getHours()).padStart(2, '0') + const mm = String(now.getMinutes()).padStart(2, '0') + const ss = String(now.getSeconds()).padStart(2, '0') + const t = `${hh}:${mm}:${ss}` + + const speed = Number(entry.stripSpeed) + if (Number.isFinite(speed)) { + this.pushSeries('stripSpeed', t, { stripSpeed: speed }) + } + } }, formatNum(v) { @@ -846,22 +1094,41 @@ export default { this.isRefreshing = true try { await Promise.all([this.loadDeviceDefs(), this.loadFieldMeta()]) - // 刷新后更新订阅(按滚动懒加载逻辑) - this.$nextTick(() => { - this.updateVisibleDeviceSubscriptions() - }) this.updateLastTime() } finally { - this.isRefreshing = false + if (!this.isDestroyed) { + this.isRefreshing = false + } } }, updateLastTime() { + + + // 如果没有起始时间,使用当前时间作为起始时间 + if (!this.startTime) { + this.startTime = new Date() + } + const now = new Date() - const hour = String(now.getHours()).padStart(2, '0') - const minute = String(now.getMinutes()).padStart(2, '0') - const second = String(now.getSeconds()).padStart(2, '0') - this.lastUpdateTime = `${hour}:${minute}:${second}` + const diff = now - this.startTime // 时间差(毫秒) + + // 计算天数、小时数、分钟数 + const days = Math.floor(diff / (1000 * 60 * 60 * 24)) + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) + + // 格式化显示 + let timeStr = '' + if (days > 0) { + timeStr += `${days}天` + } + if (hours > 0 || days > 0) { + timeStr += `${hours}小时` + } + timeStr += `${minutes}分钟` + + this.lastUpdateTime = timeStr || '0分钟' }, formatTime(str) { @@ -872,6 +1139,30 @@ export default { return `${h}:${m}` }, + // 格式化持续时间:将分钟数转换为"X天X小时X分钟"格式 + formatDuration(minutes) { + if (minutes === null || minutes === undefined || minutes === '') return '—' + const totalMinutes = Math.floor(Number(minutes)) + if (isNaN(totalMinutes) || totalMinutes < 0) return '—' + + const days = Math.floor(totalMinutes / (24 * 60)) + const hours = Math.floor((totalMinutes % (24 * 60)) / 60) + const mins = Math.floor(totalMinutes % 60) + + let result = '' + if (days > 0) { + result += `${days}天` + } + if (hours > 0 || days > 0) { + result += `${hours}小时` + } + if (mins > 0 || result === '') { + result += `${mins}分钟` + } + + return result || '0分钟' + }, + formatValue(v) { if (v === null || v === undefined || v === '') return '—' const n = Number(v) @@ -1124,6 +1415,71 @@ export default { border-left: 4rpx solid #0066cc; } +.strip-status-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16rpx; + margin-bottom: 20rpx; +} + +.strip-status-card { + background: #fff; + border: 1rpx solid #e4e7ed; + border-radius: 8rpx; + padding: 24rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.strip-status-label { + font-size: 24rpx; + color: #909399; + margin-bottom: 12rpx; +} + +.strip-status-value { + font-size: 48rpx; + font-weight: 600; + color: #0066cc; + line-height: 1; + margin-bottom: 8rpx; +} + +.strip-status-unit { + font-size: 20rpx; + color: #909399; + margin-bottom: 16rpx; +} + +.progress-container { + width: 100%; + margin-top: 8rpx; +} + +.progress-bar { + width: 100%; + height: 8rpx; + background: #e4e7ed; + border-radius: 4rpx; + overflow: hidden; + margin-bottom: 8rpx; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #0066cc 0%, #409eff 100%); + border-radius: 4rpx; + transition: width 0.3s ease; +} + +.progress-text { + font-size: 20rpx; + color: #606266; + text-align: center; + display: block; +} + .device-card { background: #fff; border: 1rpx solid #e4e7ed; @@ -1202,6 +1558,13 @@ export default { color: #909399; } +.metric-set-value { + display: block; + font-size: 20rpx; + color: #909399; + margin-top: 4rpx; +} + .device-actions { display: flex; justify-content: flex-end; @@ -1410,4 +1773,154 @@ export default { font-weight: 600; color: #303133; } + +.detail-cards { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.detail-card { + background: #fff; + border: 1rpx solid #e4e7ed; + border-radius: 12rpx; + overflow: hidden; + transition: all 0.3s ease; +} + +.detail-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 12rpx rgba(0, 102, 204, 0.1); +} + +.detail-card-header { + background: linear-gradient(135deg, #0066cc 0%, #409eff 100%); + padding: 20rpx 24rpx; + border-bottom: 1rpx solid #e4e7ed; +} + +.detail-card-title { + font-size: 28rpx; + font-weight: 600; + color: #fff; +} + +.detail-card-body { + padding: 24rpx; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.detail-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12rpx 0; + border-bottom: 1rpx solid #f5f7fa; +} + +.detail-row:last-child { + border-bottom: none; +} + +.detail-label { + font-size: 24rpx; + color: #909399; + font-weight: 400; + min-width: 120rpx; +} + +.detail-value { + font-size: 26rpx; + color: #303133; + font-weight: 500; + text-align: right; + flex: 1; +} + +.stoppage-cards { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.stoppage-card { + background: #fff; + border: 1rpx solid #e4e7ed; + border-radius: 12rpx; + overflow: hidden; + transition: all 0.3s ease; +} + +.stoppage-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 12rpx rgba(245, 108, 108, 0.1); +} + +.stoppage-card-header { + background: linear-gradient(135deg, #f56c6c 0%, #ff7875 100%); + padding: 20rpx 24rpx; + border-bottom: 1rpx solid #e4e7ed; + display: flex; + justify-content: space-between; + align-items: center; +} + +.stoppage-card-title { + font-size: 28rpx; + font-weight: 600; + color: #fff; +} + +.stoppage-card-duration { + font-size: 26rpx; + font-weight: 600; + color: #fff; + background: rgba(255, 255, 255, 0.2); + padding: 6rpx 16rpx; + border-radius: 20rpx; +} + +.stoppage-card-body { + padding: 24rpx; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.stoppage-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 12rpx 0; + border-bottom: 1rpx solid #f5f7fa; +} + +.stoppage-row:last-child { + border-bottom: none; +} + +.stoppage-label { + font-size: 24rpx; + color: #909399; + font-weight: 400; + min-width: 120rpx; +} + +.stoppage-value { + font-size: 26rpx; + color: #303133; + font-weight: 500; + text-align: right; + flex: 1; +} + +.stoppage-remark { + color: #606266; + font-weight: 400; + word-break: break-all; + white-space: normal; + text-align: left; +} diff --git a/apps/hand-factory/store/modules/user.js b/apps/hand-factory/store/modules/user.js index 7ddc10b..e3d5eec 100644 --- a/apps/hand-factory/store/modules/user.js +++ b/apps/hand-factory/store/modules/user.js @@ -7,13 +7,17 @@ import { } from "@/utils/validate" import { login, + loginZinc1, logout, getInfo } from '@/api/login' import { getToken, setToken, - removeToken + removeToken, + getZinc1Token, + setZinc1Token, + removeZinc1Token } from '@/utils/auth' import defAva from '@/static/images/avatar.png' @@ -56,25 +60,41 @@ const user = { }, actions: { - // 登录 - Login({ - commit - }, userInfo) { - const username = userInfo.username.trim() - const password = userInfo.password - const code = userInfo.code - const uuid = userInfo.uuid - return new Promise((resolve, reject) => { - login(username, password, code, uuid).then(res => { - console.log('token', res) - setToken(res.data.token) - commit('SET_TOKEN', res.data.token) - resolve() - }).catch(error => { - reject(error) + // 登录(主系统登录成功即算登录成功,Zinc1系统静默登录) + Login({ + commit + }, userInfo) { + const username = userInfo.username.trim() + const password = userInfo.password + const code = userInfo.code + const uuid = userInfo.uuid + return new Promise((resolve, reject) => { + // 先执行主系统登录 + login(username, password, code, uuid).then(mainRes => { + // 主系统登录成功,立即保存token并resolve + if (mainRes && mainRes.data && mainRes.data.token) { + setToken(mainRes.data.token) + commit('SET_TOKEN', mainRes.data.token) + } + + // 主系统登录成功后,静默执行Zinc1登录(不等待结果,不显示任何错误) + loginZinc1(username, password, code, uuid).then(zinc1Res => { + // Zinc1登录成功,静默保存token + if (zinc1Res && zinc1Res.data && zinc1Res.data.token) { + setZinc1Token(zinc1Res.data.token) + } + }).catch(() => { + // Zinc1登录失败,完全静默处理,不做任何提示 }) + + // 主系统登录成功即返回 + resolve() + }).catch(error => { + // 只有主系统登录失败才reject + reject(error) }) - }, + }) + }, // 获取用户信息 GetInfo({ @@ -106,24 +126,25 @@ const user = { }) }, - // 退出系统 - LogOut({ - commit, - state - }) { - return new Promise((resolve, reject) => { - logout(state.token).then(() => { - commit('SET_TOKEN', '') - commit('SET_ROLES', []) - commit('SET_PERMISSIONS', []) - removeToken() - storage.clean() - resolve() - }).catch(error => { - reject(error) - }) + // 退出系统 + LogOut({ + commit, + state + }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + commit('SET_TOKEN', '') + commit('SET_ROLES', []) + commit('SET_PERMISSIONS', []) + removeToken() + removeZinc1Token() // 同时清除Zinc1 token + storage.clean() + resolve() + }).catch(error => { + reject(error) }) - } + }) + } } } diff --git a/apps/hand-factory/utils/auth.js b/apps/hand-factory/utils/auth.js index 9a7cc04..8653edb 100644 --- a/apps/hand-factory/utils/auth.js +++ b/apps/hand-factory/utils/auth.js @@ -1,4 +1,5 @@ const TokenKey = 'App-Token' +const Zinc1TokenKey = 'App-Zinc1-Token' export function getToken() { return uni.getStorageSync(TokenKey) @@ -11,3 +12,16 @@ export function setToken(token) { export function removeToken() { return uni.removeStorageSync(TokenKey) } + +// Zinc1系统的token管理 +export function getZinc1Token() { + return uni.getStorageSync(Zinc1TokenKey) +} + +export function setZinc1Token(token) { + return uni.setStorageSync(Zinc1TokenKey, token) +} + +export function removeZinc1Token() { + return uni.removeStorageSync(Zinc1TokenKey) +} diff --git a/apps/hand-factory/utils/request.js b/apps/hand-factory/utils/request.js index 672c52f..62a3529 100644 --- a/apps/hand-factory/utils/request.js +++ b/apps/hand-factory/utils/request.js @@ -20,9 +20,7 @@ const request = config => { url = url.slice(0, -1) config.url = url } - - console.log('请求参数[' + config.method + config.url + ']', config) - + return new Promise((resolve, reject) => { uni.request({ method: config.method || 'get', diff --git a/apps/hand-factory/utils/socketMeasure.js b/apps/hand-factory/utils/socketMeasure.js index d3f2a09..d16bfd4 100644 --- a/apps/hand-factory/utils/socketMeasure.js +++ b/apps/hand-factory/utils/socketMeasure.js @@ -13,6 +13,13 @@ export function createMeasureSocket({ } = {}) { let socket = null let manualClose = false + // 保存回调函数的引用,允许清空 + let callbacks = { + onOpen, + onClose, + onError, + onMessage + } const wsBase = (config.wsUrl || config.baseUrl || '').replace(/^http/, 'ws') const url = `${wsBase}/websocket?type=${type}` @@ -30,12 +37,12 @@ export function createMeasureSocket({ }, fail(err) { console.error('connectSocket 调用失败:', err) - onError && onError(err) + callbacks.onError && callbacks.onError(err) } }) } catch (err) { console.error('connectSocket 执行异常(可能环境不支持 WebSocket):', err) - onError && onError(err) + callbacks.onError && callbacks.onError(err) return } @@ -47,24 +54,52 @@ export function createMeasureSocket({ // 正确的事件注册方式:socketTask.onOpen / onMessage / onError / onClose socket.onOpen((res) => { console.log('WebSocket 已打开', res) - onOpen && onOpen(res) + // 检查回调是否已被清空 + if (callbacks && callbacks.onOpen) { + try { + callbacks.onOpen(res) + } catch (e) { + // 忽略回调错误 + } + } }) socket.onMessage((evt) => { // H5 为 evt.data,小程序为 evt.data,统一兼容 const data = evt && (evt.data || evt) - onMessage && onMessage(data) + // 检查回调是否已被清空 + if (callbacks && callbacks.onMessage) { + try { + callbacks.onMessage(data) + } catch (e) { + // 忽略回调错误,可能是组件已销毁 + } + } }) socket.onError((err) => { console.error('WebSocket 发生错误:', err) - onError && onError(err) + // 检查回调是否已被清空 + if (callbacks && callbacks.onError) { + try { + callbacks.onError(err) + } catch (e) { + // 忽略回调错误 + } + } }) socket.onClose((evt) => { console.log('WebSocket 已关闭', evt) - onClose && onClose(evt) - if (!manualClose) { + // 检查回调是否已被清空 + if (callbacks && callbacks.onClose) { + try { + callbacks.onClose(evt) + } catch (e) { + // 忽略回调错误 + } + } + if (!manualClose && callbacks) { setTimeout(connect, 3000) } }) @@ -72,8 +107,19 @@ export function createMeasureSocket({ function close() { manualClose = true + // 清空所有回调,防止在关闭过程中或关闭后触发回调 + callbacks = { + onOpen: null, + onClose: null, + onError: null, + onMessage: null + } if (socket) { - socket.close(1000, 'client close') + try { + socket.close(1000, 'client close') + } catch (e) { + // ignore + } socket = null } } @@ -93,6 +139,16 @@ export function createMeasureSocket({ }) } - return { connect, close, send } + // 清空回调函数的方法 + function clearCallbacks() { + callbacks = { + onOpen: null, + onClose: null, + onError: null, + onMessage: null + } + } + + return { connect, close, send, clearCallbacks } } diff --git a/apps/hand-factory/utils/zinc1Request.js b/apps/hand-factory/utils/zinc1Request.js new file mode 100644 index 0000000..4b0e654 --- /dev/null +++ b/apps/hand-factory/utils/zinc1Request.js @@ -0,0 +1,70 @@ +import store from '@/store' +import config from '@/config' +import { getZinc1Token } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { toast, showConfirm, tansParams } from '@/utils/common' + +let timeout = 10000 +// 固定使用 zinc1 的 baseUrl +const baseUrl = 'http://140.143.206.120:10082/prod-api' + +const zinc1Request = config => { + // 是否需要设置 token + const isToken = (config.header || {}).isToken === false + config.header = config.header || {} + // 使用Zinc1系统的token + if (getZinc1Token() && !isToken) { + config.header['Authorization'] = 'Bearer ' + getZinc1Token() + } + // get请求映射params参数 + if (config.params) { + let url = config.url + '?' + tansParams(config.params) + url = url.slice(0, -1) + config.url = url + } + + return new Promise((resolve, reject) => { + uni.request({ + method: config.method || 'get', + timeout: config.timeout || timeout, + url: baseUrl + config.url, + data: config.data, + header: config.header, + dataType: 'json' + }).then(response => { + let [error, res] = response + if (error) { + toast('后端接口连接异常') + reject('后端接口连接异常') + return + } + const code = res.data.code || 200 + const msg = errorCode[code] || res.data.msg || errorCode['default'] + if (code === 401) { + // Zinc1系统是可选的,如果返回401,静默处理,不显示任何提示 + reject('Zinc1系统登录状态已过期') + } else if (code === 500) { + toast(msg) + reject('500') + } else if (code !== 200) { + toast(msg) + reject(code) + } + resolve(res.data) + }) + .catch(error => { + let { message } = error + if (message === 'Network Error') { + message = '后端接口连接异常' + } else if (message.includes('timeout')) { + message = '系统接口请求超时' + } else if (message.includes('Request failed with status code')) { + message = '系统接口' + message.substr(message.length - 3) + '异常' + } + toast(message) + reject(error) + }) + }) +} + +export default zinc1Request