yeg
2026-08-26 9f48020dfd3e29d1c0049a3da9df3203c40bc264
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import ConsoleChartPanel from '@/components/ConsoleChartPanel.vue'
import ConsoleProjectChartPanel from '@/components/ConsoleProjectChartPanel.vue'
import ConsoleDataListPanel from '@/components/ConsoleDataListPanel.vue'
import { useTests } from '@/stores/tests'
 
type ConsoleState = '检测中' | '暂停' | '已结束'
type ConsoleTab = 'realtime' | 'step-chart' | 'project-chart' | 'data-list'
 
const route = useRoute()
const router = useRouter()
const { records: testRecords, updateTestStatus } = useTests()
const testId = computed(() => Number(route.params.id) || 1024)
const testProfiles: Record<number, { name: string; owner: string; purpose: string; startedAt: string; batteryGroup: string; batteryCount: number; abnormal: boolean }> = {
  1024: { name: '高温循环性能测试', owner: '张工', purpose: '验证高温环境循环寿命', startedAt: '2026-07-10 14:40:00', batteryGroup: '高温循环 A 组', batteryCount: 8, abnormal: false },
  1023: { name: '电芯容量一致性测试', owner: '李工', purpose: '筛查批次容量离散度', startedAt: '2026-07-10 11:30:00', batteryGroup: '容量一致性 A 组', batteryCount: 6, abnormal: true },
}
const currentTest = computed(() => testRecords.value.find(item => item.id === testId.value))
const profile = computed(() => {
  const record = currentTest.value
  const fallback = testProfiles[testId.value]
  return {
    name: record?.name || fallback?.name || `测试任务 #${testId.value}`,
    owner: record?.owner || fallback?.owner || 'Admin',
    purpose: record?.purpose || fallback?.purpose || '电池性能检测',
    startedAt: record && record.startedAt !== '-' ? `${record.startedAt}:00` : fallback?.startedAt || record?.createdAt || '2026-08-07 09:00:00',
    batteryGroup: record?.batteryGroup || fallback?.batteryGroup || '演示电池组',
    batteryCount: record?.batteryCount || fallback?.batteryCount || 4,
    batteryNos: record?.batteryNo.split('、').filter(Boolean) || [],
    abnormal: record?.status === '检测中(异常)' || fallback?.abnormal || false,
  }
})
const initialStatus = currentTest.value?.status
const state = ref<ConsoleState>(initialStatus === '检测完成' || route.query.state === 'ended' ? '已结束' : initialStatus === '暂停' ? '暂停' : '检测中')
const elapsedSeconds = ref(42 * 60 + 25)
const stepDialogVisible = ref(false)
const activeTab = ref<ConsoleTab>(route.query.tab === 'data-list' ? 'data-list' : 'realtime')
const consoleTabs: Array<{ key: ConsoleTab; label: string }> = [
  { key: 'realtime', label: '实时数据' },
  { key: 'step-chart', label: '工步图表' },
  { key: 'project-chart', label: '项目图表' },
  { key: 'data-list', label: '数据列表' },
]
 
const timer = window.setInterval(() => { if (state.value === '检测中') elapsedSeconds.value += 1 }, 1000)
onBeforeUnmount(() => window.clearInterval(timer))
 
const elapsedText = computed(() => {
  const hours = Math.floor(elapsedSeconds.value / 3600)
  const minutes = Math.floor((elapsedSeconds.value % 3600) / 60)
  const seconds = elapsedSeconds.value % 60
  return [hours, minutes, seconds].map(value => String(value).padStart(2, '0')).join(':')
})
 
const cells = computed(() => Array.from({ length: profile.value.batteryCount }, (_, index) => ({
  no: profile.value.batteryNos[index] || `SP26-${214 + index}`,
  cabinet: `检测柜 A-${String(Math.floor(index / 4) + 1).padStart(2, '0')}`,
  channel: `通道 ${String(index + 1).padStart(2, '0')}`,
  voltage: (1.2952 + (index % 4) * 0.0011).toFixed(4),
  temperature: (32.6 + (index % 5) * 0.15).toFixed(1),
  positive: (1.2939 + (index % 4) * 0.001).toFixed(4),
  negative: (1.2946 + (index % 4) * 0.0009).toFixed(4),
  capacity: (1495.8 + (index % 6) * 0.48).toFixed(1),
  current: (-8170.6 + (index % 5) * 1.7).toFixed(1),
})))
 
const steps = [
  { no: 1, name: '恒流充电', time: '-', voltage: '0.9000 V', state: '已完成' },
  { no: 2, name: '搁置', time: '00:05:00', voltage: '-', state: '已完成' },
  { no: 3, name: '恒流充电', time: '00:00:00', voltage: '2.4000 V', state: '进行中' },
  { no: 4, name: '搁置', time: '00:05:00', voltage: '-', state: '待执行' },
  { no: 5, name: '恒流放电', time: '-', voltage: '0.9000 V', state: '待执行' },
  { no: 6, name: '循环', time: '起始工步 2', voltage: '循环次数 32', state: '待执行' },
  { no: 7, name: '结束', time: '-', voltage: '-', state: '待执行' },
]
 
const parameters = [
  ['测试名称', profile.value.name], ['创建者', '陈师傅'], ['负责人', profile.value.owner], ['测试项目', profile.value.purpose],
  ['创建时间', '2026-07-10 14:32:00'], ['上限电压', '2.500 V'], ['下限电压', '0.600 V'], ['上限电流', '8167.4 mA'],
  ['下限电流', '-8167.4 mA'], ['记录条件', '60000 ms / 0.000 V / 0.0 mA'], ['电压量程', '5 V'], ['电流量程', '+12000 / -12000 mA'],
  ['测试目的', profile.value.purpose], ['备注', '自动采集,异常时通知负责人'],
]
 
const channels = computed(() => cells.value.map((cell, index) => ({ no: cell.no, cabinet: cell.cabinet, cabinetState: index === 2 && profile.value.abnormal ? '异常' : '在线', channel: cell.channel, collector: `采集器 COL-${String(Math.floor(index / 4) + 1).padStart(2, '0')}`, collectorState: '在线', temperature: `温度通道 ${index + 1}`, positive: `正电位通道 ${index + 1}`, negative: `负电位通道 ${index + 1}` })))
 
function togglePause() {
  if (state.value === '已结束') return
  state.value = state.value === '暂停' ? '检测中' : '暂停'
  updateTestStatus(testId.value, state.value === '暂停' ? '暂停' : '检测中')
  ElMessage.success(state.value === '暂停' ? '测试已暂停' : '测试已继续')
}
 
async function endTest() {
  if (state.value === '已结束') return
  try {
    await ElMessageBox.confirm('结束测试后将停止设备采集,是否继续?', '确认结束测试', { confirmButtonText: '结束测试', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' })
    state.value = '已结束'
    updateTestStatus(testId.value, '检测完成')
    ElMessage.success('测试已结束')
  } catch { /* 用户取消 */ }
}
</script>
 
<template>
  <div class="console-page">
    <div class="page-tools"><button class="back-button" type="button" @click="router.push('/test-list')"><el-icon><ArrowLeft /></el-icon>返回测试列表</button><span>TEST CONSOLE · #{{ testId }}</span></div>
 
    <nav class="console-tabs" aria-label="控制台页面导航"><button v-for="tab in consoleTabs" :key="tab.key" type="button" :class="{ active: activeTab === tab.key }" @click="activeTab = tab.key">{{ tab.label }}</button></nav>
 
    <div v-show="activeTab === 'realtime'" class="tab-content">
    <section class="realtime-panel">
      <header class="realtime-header">
        <div><strong>实时数据</strong><span class="cell-count">{{ cells.length }} 个电池</span><span class="running-state" :class="state"><i></i>{{ state }}</span></div>
        <dl><div><dt>测试开始时间</dt><dd>{{ profile.startedAt }}</dd></div><div><dt>测试已进行时间</dt><dd>{{ elapsedText }}</dd></div></dl>
        <div class="test-actions"><button class="primary-button" type="button" :disabled="state === '已结束'" @click="togglePause"><el-icon><VideoPause v-if="state !== '暂停'" /><VideoPlay v-else /></el-icon>{{ state === '暂停' ? '继续测试' : '暂停测试' }}</button><button class="danger-button" type="button" :disabled="state === '已结束'" @click="endTest"><el-icon><CircleCloseFilled /></el-icon>结束测试</button></div>
      </header>
      <div class="cell-grid">
        <article v-for="cell in cells" :key="cell.no" class="cell-card">
          <div class="cell-summary"><strong>{{ cell.no }}</strong><span>{{ profile.batteryGroup }}</span><p>{{ cell.cabinet }} · {{ cell.channel }}</p><small>工步开始时间:2026/07/10 14:40:12</small><small>工步进行时间:{{ elapsedText }}</small></div>
          <dl class="metric-list"><div><dt>电压(V)</dt><dd class="voltage">{{ cell.voltage }}</dd></div><div><dt>温度(℃)</dt><dd class="temperature">{{ cell.temperature }}</dd></div><div><dt>正电位</dt><dd class="positive">{{ cell.positive }}</dd></div><div><dt>负电位</dt><dd class="negative">{{ cell.negative }}</dd></div><div><dt>容量</dt><dd class="capacity">{{ cell.capacity }}</dd></div><div><dt>电流(mA)</dt><dd class="current">{{ cell.current }}</dd></div></dl>
        </article>
      </div>
    </section>
 
    <div class="console-layout">
      <main>
        <section class="panel step-panel"><header class="panel-heading"><div><i></i><strong>项目参数</strong><span>STEP PARAMETERS</span></div><button class="primary-button compact-button" type="button" @click="stepDialogVisible=true"><el-icon><Setting /></el-icon>工步设置</button></header><div class="project-meta"><span>测试项目 <strong>{{ profile.purpose }}</strong></span><span>当前循环 <strong>循环 36 / 计划 60</strong></span></div><div class="step-table-wrap"><table><thead><tr><th>工步号</th><th>工步名称</th><th>时间(h:min:s.ms)</th><th>电压(V)</th><th>状态</th></tr></thead><tbody><tr v-for="step in steps" :key="step.no" :class="{ active: step.state === '进行中' }"><td>{{ step.no }}</td><td><strong>{{ step.name }}</strong></td><td>{{ step.time }}</td><td>{{ step.voltage }}</td><td><span class="step-state" :class="step.state">{{ step.state }}</span></td></tr></tbody></table></div></section>
        <section class="panel alarm-panel"><header class="panel-heading"><div><i></i><strong>异常警告</strong><span>ALARM</span></div></header><div v-if="profile.abnormal" class="alarm-list"><div><el-icon><WarningFilled /></el-icon><span><strong>壳体温度超过预警值</strong><small>SP26-216 · 33.1℃ · 2026-07-10 15:18:06</small></span></div><div><el-icon><WarningFilled /></el-icon><span><strong>通道电压波动异常</strong><small>通道 03 · 波动 0.12V · 2026-07-10 15:20:12</small></span></div></div><div v-else class="alarm-empty"><el-icon><CircleCheckFilled /></el-icon>暂无异常</div></section>
        <section class="panel channel-panel"><header class="panel-heading"><div><i></i><strong>通道</strong><span>CHANNEL STATUS</span></div></header><div class="channel-table-wrap"><table><thead><tr><th>电池号</th><th>测试柜</th><th>柜状态</th><th>检测通道</th><th>采集器</th><th>采集器状态</th><th>温度</th><th>正电位</th><th>负电位</th></tr></thead><tbody><tr v-for="item in channels" :key="item.no"><td><strong>{{ item.no }}</strong></td><td>{{ item.cabinet }}</td><td><span class="device-state" :class="item.cabinetState">{{ item.cabinetState }}</span></td><td>{{ item.channel }}</td><td>{{ item.collector }}</td><td><span class="device-state 在线">{{ item.collectorState }}</span></td><td>{{ item.temperature }}</td><td>{{ item.positive }}</td><td>{{ item.negative }}</td></tr></tbody></table></div></section>
      </main>
      <aside class="parameter-panel"><header class="panel-heading"><div><i></i><strong>测试参数</strong><span>TEST SETTINGS</span></div></header><dl><div v-for="item in parameters" :key="item[0]"><dt>{{ item[0] }}</dt><dd>{{ item[1] }}</dd></div></dl></aside>
    </div>
    </div>
 
    <ConsoleChartPanel v-if="activeTab === 'step-chart'" :cells="cells" :test-name="profile.name" :battery-group="profile.batteryGroup" :duration-minutes="5" />
    <ConsoleProjectChartPanel v-if="activeTab === 'project-chart'" :cells="cells" />
 
    <ConsoleDataListPanel v-if="activeTab === 'data-list'" :test-id="testId" :test-name="profile.name" :started-at="profile.startedAt" :cells="cells" :can-import="state === '已结束'" />
 
    <el-dialog v-model="stepDialogVisible" title="工步设置" width="620px" align-center><div class="step-dialog-content"><p>当前执行工步</p><strong>工步 3 · 恒流充电</strong><dl><div><dt>目标电压</dt><dd>2.4000 V</dd></div><div><dt>循环进度</dt><dd>36 / 60</dd></div><div><dt>执行时间</dt><dd>{{ elapsedText }}</dd></div></dl></div><template #footer><button class="secondary-button" type="button" @click="stepDialogVisible=false">取消</button><button class="primary-button" type="button" @click="stepDialogVisible=false; ElMessage.success('工步设置已保存')"><el-icon><Check /></el-icon>确认</button></template></el-dialog>
  </div>
</template>
 
<style scoped>
.console-page{width:100%;padding-bottom:24px}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px;font-weight:600}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;font-size:13px;cursor:pointer}.back-button:hover{color:var(--blue)}.primary-button,.secondary-button,.danger-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.danger-button{color:#fff;background:#ee6666;border-color:#ee6666}.primary-button:disabled,.danger-button:disabled{opacity:.5;cursor:not-allowed}.realtime-panel,.panel,.parameter-panel{border:1px solid #d8e1e9;background:#fff}.realtime-panel{margin-bottom:16px}.realtime-header{min-height:96px;display:grid;grid-template-columns:minmax(190px,1fr) minmax(360px,2fr) auto;align-items:center;gap:24px;padding:16px 20px;border-bottom:1px solid var(--line)}.realtime-header>div:first-child{display:flex;align-items:center;gap:12px}.realtime-header strong{font-size:15px}.cell-count{color:#71808d;font-size:11px}.running-state{display:inline-flex;align-items:center;gap:6px;padding:4px 7px;border:1px solid;font-size:11px}.running-state i{width:5px;height:5px;border-radius:50%;background:currentColor}.running-state.检测中{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.running-state.暂停{color:#b06a18;background:#fff8e9;border-color:#efd59e}.running-state.已结束{color:#7b8794;background:#f2f4f6;border-color:#d8dde2}.realtime-header dl{display:flex;gap:44px}.realtime-header dl>div{display:flex;align-items:center;gap:16px}.realtime-header dt{color:#52606d;font-size:12px}.realtime-header dd{color:#7b8794;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px;font-weight:600}.test-actions{display:flex;gap:10px}.cell-grid{display:grid;grid-template-columns:repeat(4,minmax(190px,1fr));gap:24px;padding:22px 36px}.cell-card{min-width:0;border:1px solid #bec9d3;border-radius:6px;background:#f5f7fa;overflow:hidden}.cell-summary{display:flex;flex-direction:column;gap:7px;padding:16px 18px 12px;border-bottom:1px solid #c8d1d9}.cell-summary strong{color:#1268dc;font-size:13px}.cell-summary span{color:#37688e;font-size:12px;font-weight:600}.cell-summary p,.cell-summary small{color:#71808d;font-size:10px}.metric-list{display:grid;gap:12px;padding:14px 18px 16px}.metric-list>div{display:flex;align-items:center;justify-content:space-between}.metric-list dt{color:#52606d;font-size:10px}.metric-list dd{font-size:17px;font-weight:700}.metric-list .voltage{color:#7f35c5}.metric-list .temperature{color:#bf6100}.metric-list .positive{color:#e99f9f}.metric-list .negative{color:#176ee8}.metric-list .capacity{color:#f59a2a}.metric-list .current{color:#21628c}.console-layout{display:grid;grid-template-columns:minmax(0,2.35fr) minmax(270px,1fr);gap:16px;align-items:start}.console-layout main{display:grid;gap:16px}.panel-heading{height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;border-bottom:1px solid var(--line);background:#fbfdff}.panel-heading>div{display:flex;align-items:center;gap:8px}.panel-heading>div>i{width:3px;height:16px;background:#2b78a3}.panel-heading strong{color:#26333f;font-size:14px}.panel-heading span{color:#81909d;font-size:10px}.compact-button{height:30px;font-size:12px}.project-meta{display:flex;gap:32px;padding:14px 16px 0;color:#71808d;font-size:11px}.project-meta strong{margin-left:8px;color:#31404d}.step-table-wrap,.channel-table-wrap{overflow-x:auto;padding:14px 16px 18px}table{width:100%;border-collapse:collapse;color:#52606d;font-size:12px}th,td{height:38px;padding:0 10px;border:1px solid #d5e0e9;text-align:center;white-space:nowrap}th{color:#52606d;background:#eaf4ff;font-weight:600}.step-table tr.active td{background:#eef7ff}.step-table tr.active td:first-child{border-left:3px solid #2188f5}.step-state{font-size:11px}.step-state.进行中{color:#1677dd}.step-state.已完成{color:#23865e}.step-state.待执行{color:#8795a3}.parameter-panel dl{display:grid;gap:0;padding:12px 18px 18px}.parameter-panel dl>div{display:grid;grid-template-columns:88px minmax(0,1fr);gap:10px;padding:9px 0;border-bottom:1px solid #edf1f5}.parameter-panel dl>div:last-child{border-bottom:0}.parameter-panel dt{color:#71808d;font-size:11px}.parameter-panel dd{min-width:0;color:#52606d;font-size:11px;overflow-wrap:anywhere}.alarm-empty{height:68px;display:flex;align-items:center;gap:8px;margin:14px 16px;padding:0 16px;color:#71808d;background:#f4f7fa;font-size:12px}.alarm-empty .el-icon{color:#23865e;font-size:16px}.alarm-list{display:grid;gap:8px;padding:14px 16px}.alarm-list>div{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid #f0c8a3;background:#fff7ef;color:#b35a25}.alarm-list span,.alarm-list strong,.alarm-list small{display:block}.alarm-list strong{font-size:12px}.alarm-list small{margin-top:3px;color:#8c6849;font-size:10px}.channel-table-wrap table{min-width:1120px}.device-state{display:inline-flex;padding:3px 7px;border:1px solid;font-size:10px}.device-state.在线{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.device-state.异常{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.step-dialog-content{display:grid;gap:12px}.step-dialog-content>p{color:#71808d;font-size:12px}.step-dialog-content>strong{color:#26333f}.step-dialog-content dl{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.step-dialog-content dl>div{padding:12px;border:1px solid #d8e1e9;background:#f8fafc}.step-dialog-content dt{color:#8795a3;font-size:10px}.step-dialog-content dd{margin-top:5px;color:#405261;font-size:12px}@media(max-width:1280px){.cell-grid{gap:16px;padding:20px}.console-layout{grid-template-columns:minmax(0,1fr) 280px}}@media(max-width:1050px){.cell-grid{grid-template-columns:repeat(2,minmax(210px,1fr))}.realtime-header{grid-template-columns:1fr auto}.realtime-header dl{grid-column:1/-1;grid-row:2}.console-layout{grid-template-columns:1fr}.parameter-panel{grid-row:2}}@media(max-width:680px){.realtime-header{grid-template-columns:1fr}.realtime-header dl{grid-column:auto;grid-row:auto;align-items:flex-start;flex-direction:column;gap:10px}.test-actions{justify-content:stretch}.test-actions button{flex:1}.cell-grid{grid-template-columns:1fr;padding:14px}.project-meta{flex-direction:column;gap:8px}.step-dialog-content dl{grid-template-columns:1fr}}
/* 双栏收缩边界:宽表只能在所属面板内横向滚动,不能撑开主网格。 */
.console-tabs{min-height:52px;display:flex;align-items:center;gap:10px;margin-bottom:14px;padding:0 4px;border-bottom:1px solid #d5e0e9}.console-tabs button{height:30px;min-width:88px;padding:0 16px;border:1px solid transparent;border-radius:16px;color:#52606d;background:#f5f7fa;font-size:12px;cursor:pointer;transition:.16s ease}.console-tabs button:hover{color:var(--blue);background:#edf5fd}.console-tabs button.active{color:#1677dd;background:#dff0ff;border-color:#c4e3ff;font-weight:600}.tab-content{min-width:0}.data-list-panel{min-width:0;border:1px solid #d8e1e9;background:#fff}.data-list-panel .panel-heading>small{color:#8795a3;font-size:10px}.data-table-wrap{width:100%;overflow-x:auto;padding:16px 18px 20px}.data-table-wrap table{min-width:1260px}.data-table-wrap td strong{color:#1268dc}.data-voltage{color:#7f35c5;font-weight:600}.data-temperature{color:#bf6100;font-weight:600}
.console-layout{grid-template-columns:minmax(0,2.35fr) minmax(300px,1fr);gap:24px;min-width:0}
.console-layout>main,.console-layout>.parameter-panel,.console-layout main>.panel{min-width:0;max-width:100%}
.step-table-wrap,.channel-table-wrap{width:100%;max-width:100%;min-width:0;overflow-x:auto;overflow-y:hidden}
.step-table-wrap table{min-width:720px}
.channel-table-wrap table{width:1120px;min-width:1120px;max-width:none}
.parameter-panel{position:relative;z-index:1;overflow:hidden}
@media(max-width:1280px){.console-layout{grid-template-columns:minmax(0,1fr) 300px;gap:20px}}
@media(max-width:1050px){.console-layout{grid-template-columns:minmax(0,1fr);gap:16px}.parameter-panel{grid-row:2}}
@media(max-width:600px){.console-tabs{overflow-x:auto}.console-tabs button{flex:0 0 auto}.data-table-wrap{padding:12px}}
</style>