<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>
|