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.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 || '—' }}
+
+
+
+ 开始时间
+ {{ item.startDate || '—' }}
+
+
+ 结束时间
+ {{ item.endDate || '—' }}
+
+
+ 区域
+ {{ item.area || '—' }}
+
+
+ 设备
+ {{ item.seton || '—' }}
+
+
+ 原因
+
+
+
@@ -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