yeg
2026-09-05 fe08695ed46eb0834a0a0cf94ec70c77c3b27da4
补齐控制台初次加载恢复与窄屏展示

首次配置请求失败后重试,补充权限拒绝和服务不可用验证,调整实时来源列和窄屏子页控件避免截断。
3个文件已修改
59 ■■■■■ 已修改文件
src/components/ConsoleDataListPanel.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestConsole.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/test-ticket11-browser.mjs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ConsoleDataListPanel.vue
@@ -361,7 +361,7 @@
)
</script>
<template>
  <section class="console-data-panel">
  <section class="console-data-panel" :class="activeTab">
    <header class="section-heading">
      <div><i></i><strong>数据列表</strong><span>TEST DATA</span></div>
      <nav class="data-tabs" aria-label="数据列表子页">
@@ -429,7 +429,7 @@
            <th>记录序号</th>
            <th>设备时间</th>
            <th>服务器接收时间</th>
            <th>来源</th>
            <th class="source-column">来源</th>
            <th>温度(℃)</th>
            <th>正电位(V)</th>
            <th>负电位(V)</th>
@@ -487,7 +487,7 @@
              <td>{{ row.sequenceId }}</td>
              <td>{{ row.recordedAt }}</td>
              <td>{{ row.serverReceivedAt }}</td>
              <td :title="row.sourceIdentity">{{ row.sourceType === 'COLLECTOR' ? `采集器 ${row.collectorCode}` : row.sourceType === 'SNAPSHOT' ? '检测柜 + 采集器' : '检测柜' }}<small>{{ row.sourceIdentity }}</small><small v-if="row.sourceType === 'SNAPSHOT'">采集器 {{ row.collectorCode }} · {{ row.collectorDeviceTime }}</small><small v-if="row.sourceType === 'COLLECTOR'">{{ row.channelCode }}</small><small v-if="row.extensionMeasurements !== '-'">{{ row.extensionMeasurements }}</small></td>
              <td class="source-column" :title="row.sourceIdentity">{{ row.sourceType === 'COLLECTOR' ? `采集器 ${row.collectorCode}` : row.sourceType === 'SNAPSHOT' ? '检测柜 + 采集器' : '检测柜' }}<small>{{ row.sourceIdentity }}</small><small v-if="row.sourceType === 'SNAPSHOT'">采集器 {{ row.collectorCode }} · {{ row.collectorDeviceTime }}</small><small v-if="row.sourceType === 'COLLECTOR'">{{ row.channelCode }}</small><small v-if="row.extensionMeasurements !== '-'">{{ row.extensionMeasurements }}</small></td>
              <td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].temperature" size="small" /><span v-else>{{ row.temperature }}</span></td>
              <td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].positivePotential" size="small" /><span v-else>{{ row.positivePotential }}</span></td>
              <td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].negativePotential" size="small" /><span v-else>{{ row.negativePotential }}</span></td>
@@ -848,6 +848,22 @@
  font-family: 'Cascadia Mono', Consolas, monospace;
  font-size: 11px;
}
.source-column {
  width: 280px;
  white-space: normal;
  overflow-wrap: anywhere;
}
.console-data-panel.realtime table {
  min-width: 2100px;
}
.console-data-panel.realtime th:nth-child(8),
.console-data-panel.realtime th:nth-child(9) {
  width: 185px;
}
.source-column small {
  display: block;
  line-height: 1.6;
}
.empty-cell {
  height: 150px;
  color: #8795a3;
@@ -869,6 +885,20 @@
  --el-pagination-hover-color: var(--blue);
}
@media (max-width: 900px) {
  .section-heading {
    height: auto;
    min-height: 90px;
    flex-wrap: wrap;
    gap: 8px;
    padding-top: 10px;
    padding-bottom: 10px;
  }
  .data-tabs {
    position: static;
    order: 3;
    flex-basis: 100%;
    transform: none;
  }
  .filter-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
src/views/TestConsole.vue
@@ -107,6 +107,7 @@
  if (disposed || reconnectTimer !== null) return
  reconnectTimer = window.setTimeout(async () => {
    reconnectTimer = null
    if (!backendDetail.value) { await loadConsole(); return }
    try { applyLiveState(await getTestLiveState(testId.value)); if (!disposed) connectLiveStream() }
    catch (error) { handleLiveError(error instanceof Error ? error : new Error('实时状态加载失败')) }
  }, 2000)
@@ -142,7 +143,7 @@
  scheduleLiveReconnect()
}
onMounted(async () => {
async function loadConsole() {
  try {
    const detail = await getTestDetail(testId.value)
    if (disposed) return
@@ -174,9 +175,10 @@
    if (activeTab.value === 'step-chart') await loadRealtimeRecords()
    if (activeTab.value === 'project-chart') await loadProjectChartData()
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '测试状态加载失败')
    handleLiveError(error instanceof Error ? error : new Error('测试状态加载失败'))
  }
})
}
onMounted(loadConsole)
onBeforeUnmount(() => { disposed = true; closeLiveStream?.(); if (reconnectTimer !== null) window.clearTimeout(reconnectTimer) })
async function loadRealtimeRecords() {
tests/test-ticket11-browser.mjs
@@ -19,12 +19,16 @@
const live = { ...detail, updatedAt: time, sources: [], exceptions: [], testPlan: { currentCycleNo: 2, currentStepNo: 3, plannedCycleCount: 5, rows: [{ rowType: 'STEP', stepNo: 3, cyclePosition: 1, stepName: '设备计划放电', durationSeconds: 60, voltageV: 3.2, executionStatus: '进行中' }] } }
let liveReads = 0
let unavailable = false
let detailUnavailable = false
let forbidden = false
const requests = []
await context.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'ticket11-token'))
await page.route('http://ticket11.test/**', async route => {
  const url = new URL(route.request().url())
  if (url.pathname.startsWith('/dev-api/')) {
    requests.push(url.pathname)
    if (detailUnavailable && url.pathname.endsWith('/system/test/42')) return route.fulfill({ status: 503, json: { code: 503, msg: '测试配置暂时不可用' } })
    if (forbidden && /\/live(?:-state)?$/.test(url.pathname)) return route.fulfill({ status: 403, json: { code: 403, msg: '实时状态权限不足' } })
    if (unavailable && /\/live(?:-state)?$/.test(url.pathname)) return route.fulfill({ status: 503, json: { code: 503, msg: '后端暂时不可用' } })
    let result = detail
    if (url.pathname.endsWith('/live-state')) { liveReads++; result = live }
@@ -57,6 +61,8 @@
  assert.match(await page.locator('body').innerText(), /HUMIDITY.*41.*%/)
  assert.match(await page.locator('body').innerText(), /S-04/)
  assert.match(await page.locator('body').innerText(), /adapter-a:1:2:3:4/)
  await mkdir(new URL('../test-results/ticket11/', import.meta.url), { recursive: true })
  await page.screenshot({ path: new URL('../test-results/ticket11/source-desktop.png', import.meta.url).pathname.replace(/^\/(\w:)/, '$1'), fullPage: true })
  await panel.getByRole('button', { name: '编辑', exact: true }).click()
  const dialog = page.getByRole('dialog')
  await dialog.locator('label').filter({ hasText: '电压(V)' }).locator('input').fill('4.1256')
@@ -105,6 +111,7 @@
  await original.waitFor({ state: 'detached' })
  await page.reload()
  await page.getByText('IMPORT-REALTIME', { exact: true }).waitFor()
  await expect(panel.getByRole('button', { name: '实时', exact: true })).toHaveAttribute('aria-current', 'page')
  assert.equal(await page.getByText('RT-1', { exact: true }).count(), 0)
  await page.getByRole('button', { name: '工步', exact: true }).click()
  await page.getByText('STEP-1', { exact: true }).waitFor()
@@ -135,6 +142,14 @@
  await page.reload()
  await expect(page.locator('.reception-state')).toHaveText('暂停接收')
  await expect(page.locator('.metric-list .voltage')).toHaveText('-')
  forbidden = true
  await expect(page.locator('.live-error')).toContainText('权限不足', { timeout: 10000 })
  forbidden = false
  detailUnavailable = true
  await page.reload()
  await expect(page.locator('.live-error')).toContainText('测试配置暂时不可用')
  detailUnavailable = false
  await expect(page.locator('.reception-state')).toHaveText('暂停接收', { timeout: 10000 })
  assert.ok(requests.every(url => !url.includes('simulator') && !url.includes('overrides')))
  assert.deepEqual(errors, [])
  await mkdir(new URL('../test-results/ticket11/', import.meta.url), { recursive: true })