yeg
2026-08-29 ce1c1ea8805604385db5f4c672fe61b9a2f18121
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, 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'
import { getTestDetail, startTestReception, type TestConfigurationDetail } from '@/api/tests'
 
type ConsoleState = '未开始接收' | '检测中' | '暂停' | '已结束'
type ReceptionState = '未开始接收' | '接收中' | '暂停接收' | '已结束接收'
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 backendDetail = ref<TestConfigurationDetail | null>(null)
const runtimeAttach = ref(false)
const receptionState = ref<ReceptionState>('未开始接收')
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 === '暂停' ? '暂停' : 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))
 
onMounted(async () => {
  try {
    const detail = await getTestDetail(testId.value)
    backendDetail.value = detail
    runtimeAttach.value = detail.runtimeAttach === true
    if (detail.receptionStatus === 'NOT_STARTED') {
      state.value = '未开始接收'
      receptionState.value = '未开始接收'
    } else if (detail.receptionStatus === 'PAUSED') {
      state.value = '暂停'
      receptionState.value = '暂停接收'
    } else if (detail.receptionStatus === 'ENDED') {
      state.value = '已结束'
      receptionState.value = '已结束接收'
    } else {
      state.value = '检测中'
      receptionState.value = '接收中'
    }
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '测试状态加载失败')
  }
})
 
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 === '已结束' || state.value === '未开始接收') return
  state.value = state.value === '暂停' ? '检测中' : '暂停'
  updateTestStatus(testId.value, state.value === '暂停' ? '暂停' : '检测中')
  ElMessage.success(state.value === '暂停' ? '测试已暂停' : '测试已继续')
}
 
async function startReception() {
  if (state.value !== '未开始接收') return
  try {
    backendDetail.value = await startTestReception(testId.value)
    runtimeAttach.value = backendDetail.value.runtimeAttach === true
    state.value = '检测中'
    receptionState.value = '接收中'
    updateTestStatus(testId.value, '检测中')
    ElMessage.success('已开始接收,未向设备发送控制命令')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '开始接收失败')
  }
}
 
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><span class="reception-state"><i></i>{{ receptionState }}</span><span v-if="runtimeAttach" class="attach-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 v-if="state === '未开始接收'" class="primary-button" type="button" @click="startReception"><el-icon><VideoPlay /></el-icon>开始接收</button><button v-else 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 === '已结束' || 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}.running-state.未开始接收{color:#7b8794;background:#f2f4f6;border-color:#d8dde2}
.reception-state{padding:4px 7px;border:1px solid #b9decf;color:#23865e;background:#edf9f3;font-size:11px}
.attach-state{padding:4px 7px;border:1px solid #bdd8f4;color:#1677dd;background:#edf6ff;font-size:11px}
/* 双栏收缩边界:宽表只能在所属面板内横向滚动,不能撑开主网格。 */
.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>