二级后端添加数据快照修正。前端添加数字孪生
This commit is contained in:
@@ -3,7 +3,8 @@ import request from '@/utils/request'
|
|||||||
export function listDeviceEnumAll() {
|
export function listDeviceEnumAll() {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/deviceEnum/all',
|
url: '/api/deviceEnum/all',
|
||||||
method: 'get'
|
method: 'get',
|
||||||
|
baseUrl: 'http://140.143.206.120:10082/prod-api'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import request from '@/utils/request'
|
|||||||
export function getDeviceFieldMetaAll() {
|
export function getDeviceFieldMetaAll() {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/deviceFieldMeta/all',
|
url: '/api/deviceFieldMeta/all',
|
||||||
method: 'get'
|
method: 'get',
|
||||||
|
baseUrl: 'http://140.143.206.120:10082/prod-api'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export function listDeviceSnapshotLatest(params) {
|
|||||||
return request({
|
return request({
|
||||||
url: '/api/deviceSnapshot/latest',
|
url: '/api/deviceSnapshot/latest',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params,
|
||||||
|
baseUrl: 'http://140.143.206.120:10082/prod-api'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +15,8 @@ export function listDeviceSnapshotRange(params) {
|
|||||||
return request({
|
return request({
|
||||||
url: '/api/deviceSnapshot/range',
|
url: '/api/deviceSnapshot/range',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params,
|
||||||
|
baseUrl: 'http://140.143.206.120:10082/prod-api'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,12 @@
|
|||||||
<text class="refresh-icon" :class="{ rotating: isRefreshing }">⟳</text>
|
<text class="refresh-icon" :class="{ rotating: isRefreshing }">⟳</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<scroll-view scroll-y class="scroll-container" v-if="currentTab === 1" @scroll="onScroll">
|
<!-- Tab1:实时监控 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="scroll-container"
|
||||||
|
v-if="currentTab === 1"
|
||||||
|
>
|
||||||
<!-- 顶部状态栏 -->
|
<!-- 顶部状态栏 -->
|
||||||
<view class="status-bar">
|
<view class="status-bar">
|
||||||
<view class="status-item">
|
<view class="status-item">
|
||||||
@@ -36,50 +41,213 @@
|
|||||||
<text class="status-label">设备数</text>
|
<text class="status-label">设备数</text>
|
||||||
<text class="status-value">{{ deviceDefs.length }}</text>
|
<text class="status-value">{{ deviceDefs.length }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="status-divider"></view>
|
||||||
|
<view class="status-item status-toggle" @click="toggleChartPause">
|
||||||
|
<text class="status-label">折线图</text>
|
||||||
|
<text
|
||||||
|
class="status-value status-time"
|
||||||
|
:class="isChartPaused ? 'status-异常' : 'status-通畅'"
|
||||||
|
>
|
||||||
|
{{ isChartPaused ? '已暂停刷新' : '实时刷新中' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 实时数据卡片 + 统计图(按酸轧页风格,不按设备分块) -->
|
||||||
|
<view class="section">
|
||||||
<view v-for="dev in deviceDefs" :key="'device_' + dev.deviceCode" :id="'device-' + dev.deviceCode" class="device-section">
|
<view class="section-title">入口段(实时)</view>
|
||||||
<view class="section-title">{{ dev.desc }}({{ dev.deviceCode }})</view>
|
<view class="metrics-grid-3">
|
||||||
|
<view class="metric-box" v-for="it in entryMetrics" :key="'entry_' + it.field">
|
||||||
<view v-if="fieldTrendMap[dev.deviceCode]">
|
<text class="metric-name">{{ it.label || getFieldLabel(it.field) }}</text>
|
||||||
<view v-for="fieldName in (dev.paramFields || [])" :key="dev.deviceCode + '_' + fieldName" class="field-chart-container">
|
<text class="metric-value">{{ getRealtimeValueBySource('ENTRY', it.field) }}</text>
|
||||||
<view v-if="fieldTrendMap[dev.deviceCode][fieldName]">
|
<text class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</text>
|
||||||
<view class="chart-header">
|
</view>
|
||||||
<text class="chart-title">{{ getFieldLabel(fieldName) }}</text>
|
|
||||||
<text class="chart-unit">{{ getFieldUnit(fieldName) }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="chart-box">
|
<view class="chart-box">
|
||||||
<qiun-data-charts type="line" :chartData="toLineChart(fieldTrendMap[dev.deviceCode][fieldName], fieldName)" :opts="lineChartOpts" />
|
<qiun-data-charts type="line" :chartData="toGroupLineChart('entry')" :opts="lineChartOpts" />
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="stats-grid">
|
|
||||||
<view class="stat-item"><text class="stat-label">AVG</text><text class="stat-value">{{ formatNum(fieldTrendMap[dev.deviceCode][fieldName].avg) }}</text></view>
|
|
||||||
<view class="stat-item"><text class="stat-label">MAX</text><text class="stat-value">{{ formatNum(fieldTrendMap[dev.deviceCode][fieldName].max) }}</text></view>
|
|
||||||
<view class="stat-item"><text class="stat-label">MIN</text><text class="stat-value">{{ formatNum(fieldTrendMap[dev.deviceCode][fieldName].min) }}</text></view>
|
|
||||||
<view class="stat-item"><text class="stat-label">LAST</text><text class="stat-value">{{ formatNum(fieldTrendMap[dev.deviceCode][fieldName].last) }}</text></view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="placeholder-box">
|
<view class="section">
|
||||||
<text class="placeholder-text">等待 {{ getFieldLabel(fieldName) }} 数据...</text>
|
<view class="section-title">退火炉段(实时)</view>
|
||||||
|
<view class="metrics-grid-3">
|
||||||
|
<view class="metric-box" v-for="it in furnaceMetrics" :key="'furnace_' + it.field">
|
||||||
|
<text class="metric-name">{{ it.label || getFieldLabel(it.field) }}</text>
|
||||||
|
<text class="metric-value">{{ getRealtimeValueBySource('FURNACE', it.field) }}</text>
|
||||||
|
<text class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="chart-box">
|
||||||
|
<qiun-data-charts type="line" :chartData="toGroupLineChart('furnace')" :opts="lineChartOpts" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="placeholder-box">
|
<view class="section">
|
||||||
<text class="placeholder-text">等待 {{ dev.desc }} 数据...(滚动到此处加载)</text>
|
<view class="section-title">后处理/涂层段(实时)</view>
|
||||||
|
<view class="metrics-grid-3">
|
||||||
|
<view class="metric-box" v-for="it in coatMetrics" :key="'coat_' + it.field">
|
||||||
|
<text class="metric-name">{{ it.label || getFieldLabel(it.field) }}</text>
|
||||||
|
<text class="metric-value">{{ getRealtimeValueBySource('COAT', it.field) }}</text>
|
||||||
|
<text class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-box">
|
||||||
|
<qiun-data-charts type="line" :chartData="toGroupLineChart('coat')" :opts="lineChartOpts" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">出口段(实时)</view>
|
||||||
|
<view class="metrics-grid-3">
|
||||||
|
<view class="metric-box" v-for="it in exitMetrics" :key="'exit_' + it.field">
|
||||||
|
<text class="metric-name">{{ it.label || getFieldLabel(it.field) }}</text>
|
||||||
|
<text class="metric-value">{{ getRealtimeValueBySource('EXIT', it.field) }}</text>
|
||||||
|
<text class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-box">
|
||||||
|
<qiun-data-charts type="line" :chartData="toGroupLineChart('exit')" :opts="lineChartOpts" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 生产统计 / 实绩 -->
|
||||||
|
<scroll-view scroll-y class="scroll-container" v-if="currentTab === 2">
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">当前生产情况</view>
|
||||||
|
<view class="chart-box" v-if="currentPlan">
|
||||||
|
<view class="stats-table">
|
||||||
|
<view class="stats-header">
|
||||||
|
<text class="stats-col">当前钢卷</text>
|
||||||
|
<text class="stats-col">钢种</text>
|
||||||
|
<text class="stats-col">规格 (厚×宽)</text>
|
||||||
|
<text class="stats-col">计划长度</text>
|
||||||
|
<text class="stats-col">计划重量</text>
|
||||||
|
</view>
|
||||||
|
<view class="stats-row">
|
||||||
|
<text class="stats-col">{{ currentPlan.exitMatId || '—' }}</text>
|
||||||
|
<text class="stats-col">{{ currentPlan.steelGrade || '—' }}</text>
|
||||||
|
<text class="stats-col">
|
||||||
|
{{ formatNum(currentPlan.exitThickness) }} × {{ formatNum(currentPlan.exitWidth) }}
|
||||||
|
</text>
|
||||||
|
<text class="stats-col">{{ formatNum(currentPlan.exitLength) }}</text>
|
||||||
|
<text class="stats-col">{{ formatNum(currentPlan.exitWeight) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="history-tip" v-if="!currentPlan">
|
||||||
|
<text>暂无当前生产计划数据。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">生产实绩汇总</view>
|
||||||
|
<view class="overview-grid" v-if="reportSummary">
|
||||||
|
<view class="overview-card">
|
||||||
|
<text class="overview-label">钢卷总数</text>
|
||||||
|
<text class="overview-value">{{ reportSummary.coilCount || 0 }}</text>
|
||||||
|
<text class="overview-unit">卷</text>
|
||||||
|
</view>
|
||||||
|
<view class="overview-card">
|
||||||
|
<text class="overview-label">总实际重量</text>
|
||||||
|
<text class="overview-value">{{ formatNum(reportSummary.totalActualWeight) }}</text>
|
||||||
|
<text class="overview-unit">t</text>
|
||||||
|
</view>
|
||||||
|
<view class="overview-card">
|
||||||
|
<text class="overview-label">成材率</text>
|
||||||
|
<text class="overview-value">
|
||||||
|
{{ reportSummary.yieldRate != null ? (reportSummary.yieldRate * 100).toFixed(1) : '—' }}
|
||||||
|
</text>
|
||||||
|
<text class="overview-unit">%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-box" v-else>
|
||||||
|
<text class="placeholder-text">暂无实绩汇总数据。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">生产实绩明细</view>
|
||||||
|
<view class="stats-table" v-if="reportDetails && reportDetails.length">
|
||||||
|
<view class="stats-header">
|
||||||
|
<text class="stats-col">成品卷号</text>
|
||||||
|
<text class="stats-col">原料卷号</text>
|
||||||
|
<text class="stats-col">班/组</text>
|
||||||
|
<text class="stats-col">规格 (厚×宽)</text>
|
||||||
|
<text class="stats-col">长度/重量</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="stats-row"
|
||||||
|
v-for="item in reportDetails"
|
||||||
|
:key="item.exitMatId + '_' + item.onlineTime"
|
||||||
|
>
|
||||||
|
<text class="stats-col">{{ item.exitMatId }}</text>
|
||||||
|
<text class="stats-col">{{ item.entryMatId }}</text>
|
||||||
|
<text class="stats-col">{{ item.groupNo || '—' }}/{{ item.shiftNo || '—' }}</text>
|
||||||
|
<text class="stats-col">
|
||||||
|
{{ formatNum(item.exitThickness) }} × {{ formatNum(item.exitWidth) }}
|
||||||
|
</text>
|
||||||
|
<text class="stats-col">
|
||||||
|
{{ formatNum(item.exitLength) }} m / {{ formatNum(item.actualWeight) }} t
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-box" v-else>
|
||||||
|
<text class="placeholder-text">暂无生产明细记录。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 停机统计 / 停机记录 -->
|
||||||
|
<scroll-view scroll-y class="scroll-container" v-if="currentTab === 3">
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">停机记录</view>
|
||||||
|
<!-- 月份选择器 -->
|
||||||
|
<view class="stoppage-filter">
|
||||||
|
<picker mode="date" fields="month" :value="stoppageMonth" @change="onStoppageMonthChange">
|
||||||
|
<view class="stoppage-month-btn">
|
||||||
|
<text class="stoppage-month-text">{{ stoppageMonthLabel }}</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="stats-table" v-if="stoppageList && stoppageList.length">
|
||||||
|
<view class="stats-header">
|
||||||
|
<text class="stats-col">开始时间</text>
|
||||||
|
<text class="stats-col">结束时间</text>
|
||||||
|
<text class="stats-col">持续时间</text>
|
||||||
|
<text class="stats-col">区域/设备</text>
|
||||||
|
<text class="stats-col">原因</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="stats-row"
|
||||||
|
v-for="item in stoppageList"
|
||||||
|
:key="item.stopid"
|
||||||
|
>
|
||||||
|
<text class="stats-col">{{ item.startDate }}</text>
|
||||||
|
<text class="stats-col">{{ item.endDate }}</text>
|
||||||
|
<text class="stats-col">{{ item.duration }}</text>
|
||||||
|
<text class="stats-col">
|
||||||
|
{{ item.area || '—' }}/{{ item.seton || '—' }}
|
||||||
|
</text>
|
||||||
|
<text class="stats-col">{{ item.remark || '—' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-box" v-else>
|
||||||
|
<text class="placeholder-text">暂无停机记录。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listDeviceEnumAll } from '@/api/pocket/deviceEnum'
|
import { listDeviceEnumAll } from '@/api/pocket/deviceEnum'
|
||||||
import { getDeviceFieldMetaAll } from '@/api/pocket/deviceFieldMeta'
|
import { getDeviceFieldMetaAll } from '@/api/pocket/deviceFieldMeta'
|
||||||
|
import { listDeviceSnapshotLatest } from '@/api/pocket/deviceSnapshot'
|
||||||
|
import { getCurrentPlan, getCurrentProcess } from '@/api/business/dashboard'
|
||||||
|
import { getReportSummary, getReportDetails } from '@/api/business/report'
|
||||||
|
import { listStoppage } from '@/api/business/stoppage'
|
||||||
import { createMeasureSocket } from '@/utils/socketMeasure'
|
import { createMeasureSocket } from '@/utils/socketMeasure'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -87,11 +255,15 @@ export default {
|
|||||||
return {
|
return {
|
||||||
currentTab: 1,
|
currentTab: 1,
|
||||||
tabData: [
|
tabData: [
|
||||||
{ text: '实时监控', value: 1 }
|
{ text: '实时监控', value: 1 },
|
||||||
|
{ text: '生产统计', value: 2 },
|
||||||
|
{ text: '停机统计', value: 3 }
|
||||||
],
|
],
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
lastUpdateTime: '—',
|
lastUpdateTime: '—',
|
||||||
|
// 折线图实时刷新开关(避免查看某个点时被新数据刷掉)
|
||||||
|
isChartPaused: false,
|
||||||
|
|
||||||
// socket最新消息
|
// socket最新消息
|
||||||
latestMeasure: null,
|
latestMeasure: null,
|
||||||
@@ -108,6 +280,55 @@ export default {
|
|||||||
// 懒加载:已订阅的设备列表
|
// 懒加载:已订阅的设备列表
|
||||||
subscribedDeviceCodes: [],
|
subscribedDeviceCodes: [],
|
||||||
|
|
||||||
|
// 实时数据卡片定义(按酸轧页组织,不按设备分块)
|
||||||
|
entryMetrics: [
|
||||||
|
{ field: 'entryCoilId', label: '入口卷号', unit: '' },
|
||||||
|
{ field: 'stripLocation', label: '带钢位置', unit: 'm' },
|
||||||
|
{ field: 'stripSpeed', label: '带钢速度', unit: 'm/min' },
|
||||||
|
{ field: 'tensionPorBr1', label: '入口张力 POR-BR1', unit: 'daN' },
|
||||||
|
{ field: 'tensionBr1Br2', label: '张力 BR1-BR2', unit: 'daN' },
|
||||||
|
{ 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: '' }
|
||||||
|
],
|
||||||
|
coatMetrics: [
|
||||||
|
{ field: 'avrCoatingWeightTop', label: '上层平均涂层重量', unit: 'g/m²' },
|
||||||
|
{ field: 'avrCoatingWeightBottom', label: '下层平均涂层重量', unit: 'g/m²' },
|
||||||
|
{ field: 'airKnifePressure', label: '气刀压力', unit: '' },
|
||||||
|
{ field: 'stripSpeedTmExit', label: 'TM出口速度', unit: 'm/min' }
|
||||||
|
],
|
||||||
|
exitMetrics: [
|
||||||
|
{ field: 'tensionBr8Br9', label: '张力 BR8-BR9', unit: 'daN' },
|
||||||
|
{ field: 'tensionBr9Tr', label: '张力 BR9-TR', unit: 'daN' },
|
||||||
|
{ field: 'speedExitSection', label: '出口速度', unit: 'm/min' },
|
||||||
|
{ field: 'coilLength', label: '钢卷长度', unit: 'm' }
|
||||||
|
],
|
||||||
|
|
||||||
|
// 前端历史缓存(打开页面即可出趋势)
|
||||||
|
chartMaxPoints: 60,
|
||||||
|
chartSeries: {
|
||||||
|
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
||||||
|
furnace: { time: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], potTemperature: [] },
|
||||||
|
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
||||||
|
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生产统计(实绩)
|
||||||
|
currentPlan: null,
|
||||||
|
currentProcess: null,
|
||||||
|
reportSummary: null,
|
||||||
|
reportDetails: [],
|
||||||
|
reportLoading: false,
|
||||||
|
|
||||||
|
// 停机记录
|
||||||
|
stoppageList: [],
|
||||||
|
stoppageLoading: false,
|
||||||
|
stoppageMonth: '', // yyyy-MM
|
||||||
|
|
||||||
lineChartOpts: {
|
lineChartOpts: {
|
||||||
color: ['#0066cc', '#409eff', '#66b1ff', '#a0cfff', '#d9ecff', '#ecf5ff'],
|
color: ['#0066cc', '#409eff', '#66b1ff', '#a0cfff', '#d9ecff', '#ecf5ff'],
|
||||||
padding: [15, 15, 0, 15],
|
padding: [15, 15, 0, 15],
|
||||||
@@ -129,32 +350,71 @@ export default {
|
|||||||
|
|
||||||
socketClient: null,
|
socketClient: null,
|
||||||
fieldTrendSocket: null,
|
fieldTrendSocket: null,
|
||||||
fieldTrendSubscribed: false,
|
fieldTrendSubscribed: false
|
||||||
subscribedDeviceCodes: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initSocket()
|
this.initSocket()
|
||||||
|
|
||||||
Promise.all([this.loadDeviceDefs(), this.loadFieldMeta()]).then(() => {
|
Promise.all([this.loadDeviceDefs(), this.loadFieldMeta(), this.loadHistoryForCharts()]).then(() => {})
|
||||||
// 页面初始化完成后,默认订阅首屏设备(懒加载)
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateVisibleDeviceSubscriptions()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.updateLastTime()
|
this.updateLastTime()
|
||||||
|
|
||||||
|
// 周期性从后端快照刷新折线图数据(降低刷新频率,避免频繁打断用户查看)
|
||||||
|
// 这里用 60 秒刷一次,后端快照是每 5 分钟一条,折线图变化会比较平滑
|
||||||
|
this._historyTimer = setInterval(() => {
|
||||||
|
if (!this.isChartPaused) {
|
||||||
|
this.loadHistoryForCharts()
|
||||||
|
}
|
||||||
|
}, 60 * 1000)
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
stoppageMonthLabel() {
|
||||||
|
if (!this.stoppageMonth) return '选择月份'
|
||||||
|
const parts = (this.stoppageMonth || '').split('-')
|
||||||
|
if (parts.length < 2) return this.stoppageMonth
|
||||||
|
const [y, m] = parts
|
||||||
|
return `${y}年${m}月`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
currentTab(newVal) {
|
||||||
|
if (newVal === 2) {
|
||||||
|
this.loadReportData()
|
||||||
|
} else if (newVal === 3) {
|
||||||
|
// 默认当前月
|
||||||
|
if (!this.stoppageMonth) {
|
||||||
|
const now = new Date()
|
||||||
|
const y = now.getFullYear()
|
||||||
|
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
this.stoppageMonth = `${y}-${m}`
|
||||||
|
}
|
||||||
|
this.loadStoppageData()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.socketClient) this.socketClient.close()
|
if (this.socketClient) this.socketClient.close()
|
||||||
if (this.fieldTrendSocket) this.fieldTrendSocket.close()
|
if (this.fieldTrendSocket) this.fieldTrendSocket.close()
|
||||||
|
if (this._historyTimer) {
|
||||||
|
clearInterval(this._historyTimer)
|
||||||
|
this._historyTimer = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async loadDeviceDefs() {
|
async loadDeviceDefs() {
|
||||||
const res = await listDeviceEnumAll()
|
const res = await listDeviceEnumAll()
|
||||||
|
// 打印响应,排查是否拿到 data 数组(避免页面空白)
|
||||||
|
try {
|
||||||
|
console.log(res)
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
this.deviceDefs = (res && res.data) || []
|
this.deviceDefs = (res && res.data) || []
|
||||||
|
|
||||||
|
|
||||||
@@ -162,9 +422,82 @@ export default {
|
|||||||
|
|
||||||
async loadFieldMeta() {
|
async loadFieldMeta() {
|
||||||
const res = await getDeviceFieldMetaAll()
|
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
|
||||||
|
}
|
||||||
this.fieldMeta = (res && res.data) || {}
|
this.fieldMeta = (res && res.data) || {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 加载历史快照,填充折线图的“历史部分”
|
||||||
|
async loadHistoryForCharts() {
|
||||||
|
try {
|
||||||
|
// 使用设备快照作为折线图唯一数据源,避免每条 socket 消息都触发重绘
|
||||||
|
// 每次加载前先清空本地缓存,防止重复累加
|
||||||
|
this.chartSeries = {
|
||||||
|
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
||||||
|
furnace: { time: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], potTemperature: [] },
|
||||||
|
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
||||||
|
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = this.chartMaxPoints
|
||||||
|
const tasks = []
|
||||||
|
|
||||||
|
const deviceMap = {
|
||||||
|
entry: { deviceCode: 'POR1', fields: ['stripSpeed', 'tensionPorBr1', 'tensionBr1Br2', 'tensionBr2Br3'] },
|
||||||
|
furnace: {
|
||||||
|
deviceCode: 'FUR2',
|
||||||
|
fields: ['phFurnaceTemperatureActual', 'rtf1FurnaceTemperatureActual', 'potTemperature']
|
||||||
|
},
|
||||||
|
coat: {
|
||||||
|
deviceCode: 'COAT',
|
||||||
|
fields: ['avrCoatingWeightTop', 'avrCoatingWeightBottom', 'airKnifePressure', 'stripSpeedTmExit']
|
||||||
|
},
|
||||||
|
exit: { deviceCode: 'TR', fields: ['tensionBr8Br9', 'tensionBr9Tr', 'speedExitSection'] }
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(deviceMap).forEach((group) => {
|
||||||
|
const cfg = deviceMap[group]
|
||||||
|
tasks.push(
|
||||||
|
listDeviceSnapshotLatest({ limit, deviceCode: cfg.deviceCode })
|
||||||
|
.then((res) => {
|
||||||
|
const rows = (res && res.data) || []
|
||||||
|
// 按时间升序
|
||||||
|
const list = rows.slice().reverse()
|
||||||
|
list.forEach((row) => {
|
||||||
|
const t = (row.createTime || '').slice(11, 19) // HH:mm:ss
|
||||||
|
let snap = {}
|
||||||
|
try {
|
||||||
|
snap = row.snapshotData ? JSON.parse(row.snapshotData) : {}
|
||||||
|
} catch (e) {
|
||||||
|
snap = {}
|
||||||
|
}
|
||||||
|
const values = {}
|
||||||
|
cfg.fields.forEach((f) => {
|
||||||
|
values[f] = snap[f]
|
||||||
|
})
|
||||||
|
this.pushSeries(group, t, values)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(tasks)
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
initSocket() {
|
initSocket() {
|
||||||
// 实时测量数据 WebSocket
|
// 实时测量数据 WebSocket
|
||||||
this.socketClient = createMeasureSocket({
|
this.socketClient = createMeasureSocket({
|
||||||
@@ -181,10 +514,24 @@ export default {
|
|||||||
},
|
},
|
||||||
onMessage: (data) => {
|
onMessage: (data) => {
|
||||||
try {
|
try {
|
||||||
this.latestMeasure = JSON.parse(data)
|
// 兼容字符串 / 已解析对象两种情况,并打日志方便排查
|
||||||
|
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()
|
this.updateLastTime()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
console.error('解析 track_measure 数据失败:', e, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -225,6 +572,76 @@ export default {
|
|||||||
this.fieldTrendSocket.connect()
|
this.fieldTrendSocket.connect()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 加载生产统计 / 实绩数据
|
||||||
|
async loadReportData() {
|
||||||
|
if (this.reportLoading) return
|
||||||
|
this.reportLoading = true
|
||||||
|
try {
|
||||||
|
// 当前计划 & 当前工艺
|
||||||
|
try {
|
||||||
|
const planRes = await getCurrentPlan()
|
||||||
|
this.currentPlan = (planRes && planRes.data) || null
|
||||||
|
} catch (e) {
|
||||||
|
this.currentPlan = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const procRes = await getCurrentProcess()
|
||||||
|
this.currentProcess = (procRes && procRes.data) || null
|
||||||
|
} catch (e) {
|
||||||
|
this.currentProcess = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实绩汇总 & 明细:不传筛选条件,后端按默认时间范围处理
|
||||||
|
try {
|
||||||
|
const sumRes = await getReportSummary({})
|
||||||
|
this.reportSummary = sumRes || null
|
||||||
|
} catch (e) {
|
||||||
|
this.reportSummary = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const detRes = await getReportDetails({})
|
||||||
|
this.reportDetails = (detRes && Array.isArray(detRes) ? detRes : []) || []
|
||||||
|
} catch (e) {
|
||||||
|
this.reportDetails = []
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.reportLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载停机记录
|
||||||
|
async loadStoppageData() {
|
||||||
|
if (this.stoppageLoading) return
|
||||||
|
this.stoppageLoading = true
|
||||||
|
try {
|
||||||
|
// 计算当月起止日期(yyyy-MM-dd)
|
||||||
|
const month = this.stoppageMonth
|
||||||
|
let startDate = ''
|
||||||
|
let endDate = ''
|
||||||
|
if (month) {
|
||||||
|
const [y, m] = month.split('-').map((s) => Number(s))
|
||||||
|
const first = new Date(y, m - 1, 1)
|
||||||
|
const last = new Date(y, m, 0)
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
startDate = `${first.getFullYear()}-${pad(first.getMonth() + 1)}-${pad(first.getDate())}`
|
||||||
|
endDate = `${last.getFullYear()}-${pad(last.getMonth() + 1)}-${pad(last.getDate())}`
|
||||||
|
}
|
||||||
|
const res = await listStoppage({ startDate, endDate })
|
||||||
|
this.stoppageList = (res && res.data) || []
|
||||||
|
} catch (e) {
|
||||||
|
this.stoppageList = []
|
||||||
|
} finally {
|
||||||
|
this.stoppageLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onStoppageMonthChange(e) {
|
||||||
|
// e.detail.value 为 yyyy-MM-dd,这里截取前 7 位当作月份
|
||||||
|
const v = e.detail && e.detail.value
|
||||||
|
this.stoppageMonth = v ? v.slice(0, 7) : ''
|
||||||
|
this.loadStoppageData()
|
||||||
|
},
|
||||||
|
|
||||||
// 滚动事件:节流触发订阅更新
|
// 滚动事件:节流触发订阅更新
|
||||||
onScroll() {
|
onScroll() {
|
||||||
if (this._scrollTimer) return
|
if (this._scrollTimer) return
|
||||||
@@ -311,34 +728,116 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 根据 sourceType + 字段名取实时值(不按设备分块)
|
||||||
|
getRealtimeValueBySource(sourceType, field) {
|
||||||
|
const m = this.latestMeasure || {}
|
||||||
|
let msg = {}
|
||||||
|
switch (sourceType) {
|
||||||
|
case 'ENTRY':
|
||||||
|
msg = m.appMeasureEntryMessage || {}
|
||||||
|
break
|
||||||
|
case 'FURNACE':
|
||||||
|
msg = m.appMeasureFurnaceMessage || {}
|
||||||
|
break
|
||||||
|
case 'COAT':
|
||||||
|
msg = m.appMeasureCoatMessage || {}
|
||||||
|
break
|
||||||
|
case 'EXIT':
|
||||||
|
msg = m.appMeasureExitMessage || {}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
msg = {}
|
||||||
|
}
|
||||||
|
const v = msg ? msg[field] : null
|
||||||
|
if (typeof v === 'string') return v || '—'
|
||||||
|
return this.formatValue(v)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 追加一帧到前端历史缓存(用于统计折线图)
|
||||||
|
appendChartPoint(payload) {
|
||||||
|
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 entry = payload.appMeasureEntryMessage || {}
|
||||||
|
const furnace = payload.appMeasureFurnaceMessage || {}
|
||||||
|
const coat = payload.appMeasureCoatMessage || {}
|
||||||
|
const exit = payload.appMeasureExitMessage || {}
|
||||||
|
|
||||||
|
this.pushSeries('entry', t, {
|
||||||
|
stripSpeed: entry.stripSpeed,
|
||||||
|
tensionPorBr1: entry.tensionPorBr1,
|
||||||
|
tensionBr1Br2: entry.tensionBr1Br2,
|
||||||
|
tensionBr2Br3: entry.tensionBr2Br3
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pushSeries('furnace', t, {
|
||||||
|
phFurnaceTemperatureActual: furnace.phFurnaceTemperatureActual,
|
||||||
|
rtf1FurnaceTemperatureActual: furnace.rtf1FurnaceTemperatureActual,
|
||||||
|
potTemperature: furnace.potTemperature
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pushSeries('coat', t, {
|
||||||
|
avrCoatingWeightTop: coat.avrCoatingWeightTop,
|
||||||
|
avrCoatingWeightBottom: coat.avrCoatingWeightBottom,
|
||||||
|
airKnifePressure: coat.airKnifePressure,
|
||||||
|
stripSpeedTmExit: coat.stripSpeedTmExit
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pushSeries('exit', t, {
|
||||||
|
tensionBr8Br9: exit.tensionBr8Br9,
|
||||||
|
tensionBr9Tr: exit.tensionBr9Tr,
|
||||||
|
speedExitSection: exit.speedExitSection
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
pushSeries(group, time, values) {
|
||||||
|
const g = this.chartSeries[group]
|
||||||
|
if (!g) return
|
||||||
|
g.time.push(time)
|
||||||
|
Object.keys(values).forEach((k) => {
|
||||||
|
if (!g[k]) return
|
||||||
|
const raw = values[k]
|
||||||
|
const num = raw === null || raw === undefined || raw === '' ? null : Number(raw)
|
||||||
|
g[k].push(Number.isFinite(num) ? num : null)
|
||||||
|
})
|
||||||
|
if (g.time.length > this.chartMaxPoints) {
|
||||||
|
g.time.shift()
|
||||||
|
Object.keys(values).forEach((k) => {
|
||||||
|
if (g[k] && g[k].length > this.chartMaxPoints) g[k].shift()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生成多序列折线图数据
|
||||||
|
toGroupLineChart(group) {
|
||||||
|
const g = this.chartSeries[group]
|
||||||
|
if (!g) return { categories: [], series: [] }
|
||||||
|
const seriesKeys = Object.keys(g).filter((k) => k !== 'time')
|
||||||
|
return {
|
||||||
|
categories: g.time || [],
|
||||||
|
series: seriesKeys.map((k) => ({
|
||||||
|
name: this.getFieldLabel(k),
|
||||||
|
data: g[k] || []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换折线图实时刷新状态
|
||||||
|
toggleChartPause() {
|
||||||
|
this.isChartPaused = !this.isChartPaused
|
||||||
|
},
|
||||||
|
|
||||||
formatNum(v) {
|
formatNum(v) {
|
||||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return '—'
|
if (v === null || v === undefined || Number.isNaN(Number(v))) return '—'
|
||||||
return Number(v).toFixed(2)
|
return Number(v).toFixed(2)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 根据 DeviceEnum.sourceType 决定取哪个 message
|
// 不再按设备分块展示:pickSourceMsg/getRealtimeFieldValue 已弃用
|
||||||
pickSourceMsg(dev) {
|
|
||||||
const m = this.latestMeasure || {}
|
|
||||||
if (!dev || !dev.sourceType) return {}
|
|
||||||
switch (dev.sourceType) {
|
|
||||||
case 'ENTRY':
|
|
||||||
return m.appMeasureEntryMessage || {}
|
|
||||||
case 'FURNACE':
|
|
||||||
return m.appMeasureFurnaceMessage || {}
|
|
||||||
case 'COAT':
|
|
||||||
return m.appMeasureCoatMessage || {}
|
|
||||||
case 'EXIT':
|
|
||||||
return m.appMeasureExitMessage || {}
|
|
||||||
default:
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getRealtimeFieldValue(dev, field) {
|
// 快捷导航(已移除)
|
||||||
const msg = this.pickSourceMsg(dev)
|
|
||||||
const v = msg ? msg[field] : null
|
|
||||||
return this.formatValue(v)
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -457,6 +956,86 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 快速导航菜单(固定在左下角,模仿 acidity.vue) */
|
||||||
|
.nav-menu-fixed {
|
||||||
|
position: fixed;
|
||||||
|
left: 32rpx;
|
||||||
|
bottom: 120rpx;
|
||||||
|
z-index: 998;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
background: #409eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(64, 158, 255, 0.4);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
background: #fff;
|
||||||
|
border: 2rpx solid #409eff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(64, 158, 255, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f0f9ff;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
@@ -471,6 +1050,19 @@ export default {
|
|||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder-box {
|
||||||
|
background: #fff;
|
||||||
|
border: 1rpx dashed #dcdfe6;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 28rpx 20rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -696,27 +1288,25 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overview-card {
|
.overview-card {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #fff;
|
||||||
border-radius: 12rpx;
|
border-radius: 12rpx;
|
||||||
padding: 32rpx 24rpx;
|
padding: 32rpx 24rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
|
border: 1rpx solid #e4e7ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-card:nth-child(2) {
|
.overview-card:nth-child(2) {
|
||||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
background: #fff;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(245, 87, 108, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-card:nth-child(3) {
|
.overview-card:nth-child(3) {
|
||||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
background: #fff;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(79, 172, 254, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-label {
|
.overview-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: #909399;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,17 +1314,35 @@ export default {
|
|||||||
display: block;
|
display: block;
|
||||||
font-size: 48rpx;
|
font-size: 48rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #303133;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-unit {
|
.overview-unit {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: #909399;
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stoppage-filter {
|
||||||
|
margin: 0 0 20rpx 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stoppage-month-btn {
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
border: 1rpx solid #dcdfe6;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stoppage-month-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
.status-distribution {
|
.status-distribution {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
|||||||
@@ -20,34 +20,54 @@ export function createMeasureSocket({
|
|||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
manualClose = false
|
manualClose = false
|
||||||
// socket = new WebSocket(url)
|
|
||||||
|
// 使用 uni.connectSocket 建立连接(部分运行环境可能不支持 WebSocket,会直接抛异常)
|
||||||
|
try {
|
||||||
socket = uni.connectSocket({
|
socket = uni.connectSocket({
|
||||||
url,
|
url,
|
||||||
success() {
|
success() {
|
||||||
console.log('连接成功')
|
console.log('connectSocket 调用成功,等待 onOpen 回调')
|
||||||
}
|
},
|
||||||
})
|
fail(err) {
|
||||||
|
console.error('connectSocket 调用失败:', err)
|
||||||
console.log(socket)
|
|
||||||
socket.onOpen = () => {
|
|
||||||
console.log('连接成功')
|
|
||||||
onOpen && onOpen()
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.onMessage = (evt) => {
|
|
||||||
onMessage && onMessage(evt.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.onError = (err) => {
|
|
||||||
onError && onError(err)
|
onError && onError(err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('connectSocket 执行异常(可能环境不支持 WebSocket):', err)
|
||||||
|
onError && onError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
socket.onClose = (evt) => {
|
if (!socket) {
|
||||||
|
console.error('connectSocket 未返回有效 socketTask')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正确的事件注册方式:socketTask.onOpen / onMessage / onError / onClose
|
||||||
|
socket.onOpen((res) => {
|
||||||
|
console.log('WebSocket 已打开', res)
|
||||||
|
onOpen && onOpen(res)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.onMessage((evt) => {
|
||||||
|
// H5 为 evt.data,小程序为 evt.data,统一兼容
|
||||||
|
const data = evt && (evt.data || evt)
|
||||||
|
onMessage && onMessage(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.onError((err) => {
|
||||||
|
console.error('WebSocket 发生错误:', err)
|
||||||
|
onError && onError(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.onClose((evt) => {
|
||||||
|
console.log('WebSocket 已关闭', evt)
|
||||||
onClose && onClose(evt)
|
onClose && onClose(evt)
|
||||||
if (!manualClose) {
|
if (!manualClose) {
|
||||||
setTimeout(connect, 3000)
|
setTimeout(connect, 3000)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -59,11 +79,18 @@ export function createMeasureSocket({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function send(data) {
|
function send(data) {
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (!socket) {
|
||||||
socket.send(JSON.stringify(data));
|
console.error('WebSocket 未初始化,无法发送消息')
|
||||||
} else {
|
return
|
||||||
console.error('WebSocket is not open. Cannot send message.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uni-app 的 socketTask.send 使用对象形式
|
||||||
|
socket.send({
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
fail(err) {
|
||||||
|
console.error('发送 WebSocket 消息失败:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { connect, close, send }
|
return { connect, close, send }
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"screenfull": "5.0.2",
|
"screenfull": "5.0.2",
|
||||||
"sortablejs": "1.10.2",
|
"sortablejs": "1.10.2",
|
||||||
"splitpanes": "2.4.1",
|
"splitpanes": "2.4.1",
|
||||||
|
"three": "^0.158.0",
|
||||||
"vue": "2.6.12",
|
"vue": "2.6.12",
|
||||||
"vue-count-to": "1.0.13",
|
"vue-count-to": "1.0.13",
|
||||||
"vue-cropper": "0.5.5",
|
"vue-cropper": "0.5.5",
|
||||||
|
|||||||
BIN
apps/l2/public/models/光整机.glb
Normal file
BIN
apps/l2/public/models/光整机.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/剪切机.glb
Normal file
BIN
apps/l2/public/models/剪切机.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/卷取机.glb
Normal file
BIN
apps/l2/public/models/卷取机.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/夹送矫直机.glb
Normal file
BIN
apps/l2/public/models/夹送矫直机.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/开卷机.glb
Normal file
BIN
apps/l2/public/models/开卷机.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/拉轿机.glb
Normal file
BIN
apps/l2/public/models/拉轿机.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/清洗段.glb
Normal file
BIN
apps/l2/public/models/清洗段.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/炉火段.glb
Normal file
BIN
apps/l2/public/models/炉火段.glb
Normal file
Binary file not shown.
BIN
apps/l2/public/models/锌锅.glb
Normal file
BIN
apps/l2/public/models/锌锅.glb
Normal file
Binary file not shown.
278
apps/l2/src/config/productionLine.js
Normal file
278
apps/l2/src/config/productionLine.js
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
// 产线配置数据(替代后端 API)
|
||||||
|
// 轨道节点配置说明:
|
||||||
|
// trackNodes: 轨道节点数组,按顺序连接
|
||||||
|
// 每个节点包含:
|
||||||
|
// id: 节点唯一标识
|
||||||
|
// name: 节点名称(可选,用于显示)
|
||||||
|
// position: { x, y, z } 节点位置坐标
|
||||||
|
// width: 轨道宽度(可选,默认1.2)
|
||||||
|
// waypoints: 中间路径点数组(可选),用于定义从该节点到下一个节点的中间转弯点
|
||||||
|
// 每个waypoint包含:{ x, y, z } 位置坐标
|
||||||
|
// 示例:waypoints: [{ x: 10, y: 0.5, z: 5 }, { x: 15, y: 0.5, z: 5 }]
|
||||||
|
// 轨道会依次经过:节点 -> waypoint1 -> waypoint2 -> ... -> 下一个节点
|
||||||
|
export const productionLineConfig = {
|
||||||
|
name: "产线展示",
|
||||||
|
description: "基于节点的轨道系统,可手动调节每个节点的位置和高度。",
|
||||||
|
// 运动扁钢(压钢带)参数
|
||||||
|
steelFlow: {
|
||||||
|
count: 4, // 扁钢数量(同时在轨道上运动的条数)
|
||||||
|
length: 8, // 每条扁钢长度(沿轨道方向)
|
||||||
|
widthRatio: 0.85, // 扁钢宽度=轨道宽度*比例
|
||||||
|
thickness: 0.10, // 扁钢厚度
|
||||||
|
speed: 6 // 运动速度(数值越大越快)
|
||||||
|
},
|
||||||
|
// 轨道节点配置 - 按顺序连接,轨道会平滑通过这些节点
|
||||||
|
// 可以在节点之间添加 waypoints 来定义中间转弯点
|
||||||
|
trackNodes: [
|
||||||
|
{ id: "node_1", name: "起点", position: { x: 5.0, y: 5.0, z: 0.0 }, width: 1.8 },
|
||||||
|
{ id: "node_2", name: "1#开卷机后", position: { x: 23.0, y: 5.2, z: 0.0 }, width: 1.2 },
|
||||||
|
{ id: "node_3", name: "2#开卷机前", position: { x: 28.0, y: 5.2, z: 0.0 }, width: 1.2 },
|
||||||
|
{ id: "node_4", name: "2#开卷机后", position: { x: 42.0, y: 5.2, z: 0.0 }, width: 1.2 },
|
||||||
|
{
|
||||||
|
id: "node_5",
|
||||||
|
name: "夹送矫直前",
|
||||||
|
position: { x: 46, y: 6.0, z: 0.0 },
|
||||||
|
width: 1.2,
|
||||||
|
// 示例:从上一个节点到当前节点,经过多个转弯点
|
||||||
|
// waypoints: [
|
||||||
|
// { x: -13.0, y: 3.0, z: 0.0 }, // 第一个转弯点
|
||||||
|
// { x: -12.5, y: 4.5, z: 0.0 } // 第二个转弯点
|
||||||
|
// ]
|
||||||
|
},
|
||||||
|
{ id: "node_6", name: "夹送矫直后", position: { x:55.0, y: 5.5, z: 0.0 }, width: 1.2 },
|
||||||
|
{
|
||||||
|
id: "node_7",
|
||||||
|
name: "清洗段前",
|
||||||
|
position: { x: 64, y: 2, z: 1.6 },
|
||||||
|
width: 1.2,
|
||||||
|
// 从清洗段前到清洗段后的中间转弯点
|
||||||
|
// 轨道会依次经过:node_7 -> waypoint1 -> waypoint2 -> ... -> node_8
|
||||||
|
waypoints: [
|
||||||
|
// 在这里添加转弯点,例如:
|
||||||
|
{ x: 70, y: 2.0, z: 0.0 }, // 转弯点1
|
||||||
|
{ x: 72, y: 2.0, z: 13.0 }, // 转弯点2
|
||||||
|
// { x: 90, y: 0.6, z: 12.0 } // 转弯点3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "node_8",
|
||||||
|
name: "清洗段后",
|
||||||
|
position: { x: 92, y: 0.6, z: 13.6 },
|
||||||
|
width: 1.2,
|
||||||
|
// 示例:清洗段内部可能有多次转弯
|
||||||
|
waypoints: [
|
||||||
|
// { x: -2.0, y: 0.6, z: 5.0 }, // 转弯点1
|
||||||
|
// { x: 0.0, y: 0.6, z: 9.0 }, // 转弯点2
|
||||||
|
// { x: 1.0, y: 0.6, z: 11.5 } // 转弯点3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ id: "node_9", name: "炉火段前", position: { x:92, y:3, z: 2.2 }, width: 1.2 },
|
||||||
|
{ id: "node_10", name: "炉火段后", position: { x: 92, y: 3, z: 2.2 }, width: 1.2,
|
||||||
|
waypoints: [
|
||||||
|
{ x: 92.0, y:5, z: 0.0 }, // 转弯点1
|
||||||
|
{ x: 92.0, y: 5, z: -10.0 }, // 转弯点2
|
||||||
|
// { x: 1.0, y: 0.6, z: 11.5 } // 转弯点3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ id: "node_11", name: "锌锅前", position: { x: 109, y: 3, z: -10.0 }, width: 1.2 },
|
||||||
|
{ id: "node_12", name: "锌锅后", position: { x: 109, y:7, z: 4.0 }, width: 1.2,
|
||||||
|
waypoints: [
|
||||||
|
{ x: 109.0, y: 5, z: 13.0 }, // 转弯点1
|
||||||
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
{ id: "node_13", name: "光整机前",position: { x: 127.0, y: 5, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_14", name: "光整机后", position: { x: 130.0, y: 5, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_15", name: "拉矫机前", position: { x: 134.0, y: 7, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_16", name: "拉矫机后", position: { x: 142.0, y: 7, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_17", name: "剪切机前", position: { x:148.0, y: 5, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_18", name: "剪切机后", position: { x: 158.0, y: 5, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_19", name: "卷取机前", position: { x: 160.0, y: 7, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_20", name: "终点", position: { x: 165.0, y: 7, z: 15.0 }, width: 1.2 },
|
||||||
|
{ id: "node_21", name: "结束", position: { x: 170.0, y: 2, z: 15.0 }, width: 1.2 },
|
||||||
|
],
|
||||||
|
// 设备配置(独立于轨道)
|
||||||
|
models: [
|
||||||
|
// 入口段(示例:开卷机可复用两次)
|
||||||
|
{
|
||||||
|
id: "unjcoiler_1",
|
||||||
|
name: "1#开卷机",
|
||||||
|
deviceCode: "POR1",
|
||||||
|
file: "/models/开卷机.glb",
|
||||||
|
position: { x: -24.0, y: 0.0, z: 0.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut:2,
|
||||||
|
leadIn: 0,
|
||||||
|
entryOffset: { y: 0 },
|
||||||
|
exitOffset: { y: 1.2 },
|
||||||
|
conveyor: { enabled: true, yOffset: 5.5, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unjcoiler_2",
|
||||||
|
name: "2#开卷机",
|
||||||
|
deviceCode: "POR2",
|
||||||
|
file: "/models/开卷机.glb",
|
||||||
|
position: { x: -18.0, y: 0.0, z: 0.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut: 6,
|
||||||
|
leadIn: 1,
|
||||||
|
entryOffset: { y: 0.2 },
|
||||||
|
exitOffset: { y: 0.2 },
|
||||||
|
conveyor: { enabled: true, yOffset: 5.5, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 夹送矫直
|
||||||
|
{
|
||||||
|
id: "pinch_leveler",
|
||||||
|
name: "夹送矫直机",
|
||||||
|
// 暂无对应的实时设备编码,可后续按需补充
|
||||||
|
file: "/models/夹送矫直机.glb",
|
||||||
|
position: { x: -12.0, y: 0.0, z: 0.0 },
|
||||||
|
rotation: { x: 0.0, y: 3.1415926, z: 0.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut: 0,
|
||||||
|
leadIn: 2.0,
|
||||||
|
entryOffset: { y: 0.25 },
|
||||||
|
exitOffset: { y: 0.25 },
|
||||||
|
rampLength: 0,
|
||||||
|
conveyor: { enabled: true, yOffset: 6, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清洗/炉火
|
||||||
|
{
|
||||||
|
id: "clean",
|
||||||
|
name: "清洗段",
|
||||||
|
deviceCode: "CLEAN",
|
||||||
|
file: "/models/清洗段.glb",
|
||||||
|
position: { x: -6, y: 0.0, z: 5.6 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut: 8.0,
|
||||||
|
leadIn: 0,
|
||||||
|
entryOffset: { y: 0, z:-4},
|
||||||
|
exitOffset: { y: 0, z: 8 },
|
||||||
|
rampLength: 4.0,
|
||||||
|
conveyor: { enabled: false, yOffset: 0.6, zOffset: 0, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "furnace",
|
||||||
|
name: "炉火段",
|
||||||
|
deviceCode: "FUR1",
|
||||||
|
file: "/models/炉火段.glb",
|
||||||
|
position: { x: 0.0, y: 0.0, z: 2.2 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut: 2.0,
|
||||||
|
leadIn: 2.0,
|
||||||
|
entryOffset: { y: 0.25 },
|
||||||
|
exitOffset: { y: 0.25 },
|
||||||
|
rampLength: 3.5,
|
||||||
|
conveyor: { enabled: true, yOffset: 0.25, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 锌锅/后处理
|
||||||
|
{
|
||||||
|
id: "pot",
|
||||||
|
name: "锌锅",
|
||||||
|
deviceCode: "POT",
|
||||||
|
file: "/models/锌锅.glb",
|
||||||
|
position: { x: 6.0, y: 0.0, z: 0.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+z",
|
||||||
|
leadOut: 1.8,
|
||||||
|
leadIn: 2.2,
|
||||||
|
entryOffset: { y: 0.35 },
|
||||||
|
exitOffset: { y: 0.35, x: -0.6 },
|
||||||
|
rampLength: 3.0,
|
||||||
|
conveyor: { enabled: true, yOffset: 0.35, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skinpass",
|
||||||
|
name: "光整机",
|
||||||
|
deviceCode: "TM",
|
||||||
|
file: "/models/光整机.glb",
|
||||||
|
position: { x: 6.0, y: 0.0, z: 18.0 },
|
||||||
|
rotation: { x: 0.0, y: Math.PI / 2, z: 0.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-z",
|
||||||
|
exitDir: "+z",
|
||||||
|
leadOut: 2.2,
|
||||||
|
leadIn: 2.2,
|
||||||
|
entryOffset: { y: 0.25 },
|
||||||
|
exitOffset: { y: 0.25 },
|
||||||
|
rampLength: 3.5,
|
||||||
|
conveyor: { enabled: true, yOffset: 0.25, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bridle",
|
||||||
|
name: "拉矫机",
|
||||||
|
deviceCode: "TL",
|
||||||
|
file: "/models/拉轿机.glb",
|
||||||
|
rotation: { x: 0.0, y: Math.PI / 2, z: 0.0 },
|
||||||
|
position: { x: 8.0, y: 0.0, z: 15.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-z",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut: 2.0,
|
||||||
|
leadIn: 2.0,
|
||||||
|
entryOffset: { y: 0.25 },
|
||||||
|
exitOffset: { y: 0.25 },
|
||||||
|
rampLength: 3.0,
|
||||||
|
conveyor: { enabled: true, yOffset: 0.25, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 剪切/卷取
|
||||||
|
{
|
||||||
|
id: "shear",
|
||||||
|
name: "剪切机",
|
||||||
|
deviceCode: "EXC",
|
||||||
|
file: "/models/剪切机.glb",
|
||||||
|
position: { x: 16.0, y: 0.0, z: 15.0 },
|
||||||
|
rotation: { x: 0.0, y: Math.PI / 2, z: 0.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut: 2.0,
|
||||||
|
leadIn: 2.0,
|
||||||
|
entryOffset: { y: 0.3 },
|
||||||
|
exitOffset: { y: 0.3 },
|
||||||
|
rampLength: 3.0,
|
||||||
|
conveyor: { enabled: true, yOffset: 0.3, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "coiler",
|
||||||
|
name: "卷取机",
|
||||||
|
deviceCode: "TR",
|
||||||
|
file: "/models/卷取机.glb",
|
||||||
|
position: { x: 24.0, y: 0.0, z: 22.0 },
|
||||||
|
rotation: { x: 0.0, y: Math.PI / 2, z: 0.0 },
|
||||||
|
io: {
|
||||||
|
entryDir: "-x",
|
||||||
|
exitDir: "+x",
|
||||||
|
leadOut: 2.0,
|
||||||
|
leadIn: 2.0,
|
||||||
|
entryOffset: { y: 0.3 },
|
||||||
|
exitOffset: { y: 0.3 },
|
||||||
|
conveyor: { enabled: true, yOffset: 0.3, width: 1.2 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ $theme-text-gray: #a1a6af;
|
|||||||
// 仅维护组件自身样式,无定位属性
|
// 仅维护组件自身样式,无定位属性
|
||||||
.current-time {
|
.current-time {
|
||||||
padding: 1.2vw 1.8vw;
|
padding: 1.2vw 1.8vw;
|
||||||
background: white;
|
background: rgba(255, 255, 255, 0.4);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
border: 1px solid rgba(167, 172, 180, 0.3);
|
border: 1px solid rgba(167, 172, 180, 0.3);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="industrial-dashboard">
|
<div class="industrial-dashboard">
|
||||||
<!-- Header: 系统标题和当前时间 -->
|
<!-- 顶部:产线三维展示(时间显示放入3D组件内部) -->
|
||||||
<div class="dashboard-header">
|
<div class="production-line-section">
|
||||||
<div class="header-left">
|
<div class="production-line-view">
|
||||||
<h1 class="system-title">生产监控大屏</h1>
|
<ProductionLine />
|
||||||
<p class="system-subtitle">实时生产数据与设备状态</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<CurrentTime />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,9 +176,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CurrentTime from "./components/CurrentTime.vue";
|
|
||||||
import HomeMain from "./components/HomeMain.vue";
|
import HomeMain from "./components/HomeMain.vue";
|
||||||
import MiniTable from "./components/MiniTable.vue";
|
import MiniTable from "./components/MiniTable.vue";
|
||||||
|
import ProductionLine from "./l2/productLine/ProductLine.vue";
|
||||||
// 引入日志API / 生产相关API
|
// 引入日志API / 生产相关API
|
||||||
import { getLogDataPage } from "@/api/l2/log";
|
import { getLogDataPage } from "@/api/l2/log";
|
||||||
import { getRollHistorytList } from '@/api/l2/roller'
|
import { getRollHistorytList } from '@/api/l2/roller'
|
||||||
@@ -192,7 +188,7 @@ import { getCurrentProducingPlan, getCurrentProcessParams } from "@/api/business
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Index",
|
name: "Index",
|
||||||
components: { CurrentTime, HomeMain, MiniTable, TrackMeasure },
|
components: { HomeMain, MiniTable, TrackMeasure, ProductionLine },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// KPI指标数据
|
// KPI指标数据
|
||||||
@@ -431,30 +427,16 @@ $theme-text-gray: #c9cdcf;
|
|||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 仪表板头部 */
|
/* 顶部产线展示区 */
|
||||||
.dashboard-header {
|
.production-line-section {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 20px;
|
|
||||||
background: #ffffff; // 简洁白色背景,避免低级渐变色
|
.production-line-view {
|
||||||
|
background: #ffffff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0;
|
||||||
.header-left {
|
overflow: hidden;
|
||||||
.system-title {
|
|
||||||
color: #000;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-subtitle {
|
|
||||||
color: #000;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1390
apps/l2/src/views/l2/productLine/ProductLine.vue
Normal file
1390
apps/l2/src/views/l2/productLine/ProductLine.vue
Normal file
File diff suppressed because it is too large
Load Diff
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -83,6 +83,9 @@ importers:
|
|||||||
splitpanes:
|
splitpanes:
|
||||||
specifier: 2.4.1
|
specifier: 2.4.1
|
||||||
version: 2.4.1
|
version: 2.4.1
|
||||||
|
three:
|
||||||
|
specifier: ^0.158.0
|
||||||
|
version: 0.158.0
|
||||||
vue:
|
vue:
|
||||||
specifier: 2.6.12
|
specifier: 2.6.12
|
||||||
version: 2.6.12
|
version: 2.6.12
|
||||||
@@ -14235,6 +14238,10 @@ packages:
|
|||||||
webpack: 4.47.0
|
webpack: 4.47.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/three@0.158.0:
|
||||||
|
resolution: {integrity: sha512-TALj4EOpdDPF1henk2Q+s17K61uEAAWQ7TJB68nr7FKxqwyDr3msOt5IWdbGm4TaWKjrtWS8DJJWe9JnvsWOhQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/three@0.180.0:
|
/three@0.180.0:
|
||||||
resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==}
|
resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
Reference in New Issue
Block a user