yeg
2026-09-09 90e122881e224c98f697ada317fe1963716bf725
修复辅助测量修订显示并仅为编辑展示字段变化

字段展示名单遗漏温度、正电位和负电位,导致已保存的编辑变化被过滤。补齐对应名称,导入和删除不再渲染字段变化区,保留原有操作摘要与审计事实。

构建和6项相关测试通过;真实修订摘要浏览器回放验证前后值、零空值和单位,以及各8000条导入删除记录不展示字段变化。规范与需求审查均无发现。
1个文件已添加
1个文件已修改
70 ■■■■■ 已修改文件
src/views/DataRevisionDetail.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/test-data-revision-detail-browser.mjs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/DataRevisionDetail.vue
@@ -6,11 +6,12 @@
const route = useRoute(); const router = useRouter(); const record = ref<DataRevisionAction | null>(null); const loading = ref(true); const loadError = ref('')
const dataTypeLabels = revisionDataTypeLabels; const operationLabels = revisionOperationLabels
// 后端修订摘要保留 A/Ah/Wh,仅展示时换算,不改写已保存的修订事实。
const fieldLabels: Record<string, string> = { voltage: '电压(V)', current: '电流(mA)', capacity: '容量(mAh)', energy: '能量(mWh)', stepDuration: '工步时间(s)', startVoltage: '起始电压(V)', endVoltage: '终止电压(V)', chargeMedianVoltage: '充电中值电压(V)', dischargeMedianVoltage: '放电中值电压(V)', chargePeakVoltage: '充电峰值电压(V)', dischargeValleyVoltage: '放电谷值电压(V)', chargeCapacity: '充电容量(mAh)', dischargeCapacity: '放电容量(mAh)', efficiency: '效率(%)', chargeEnergy: '充电能量(mWh)', dischargeEnergy: '放电能量(mWh)' }
const fieldLabels: Record<string, string> = { temperature: '温度(℃)', positivePotential: '正电位(V)', negativePotential: '负电位(V)', voltage: '电压(V)', current: '电流(mA)', capacity: '容量(mAh)', energy: '能量(mWh)', stepDuration: '工步时间(s)', startVoltage: '起始电压(V)', endVoltage: '终止电压(V)', chargeMedianVoltage: '充电中值电压(V)', dischargeMedianVoltage: '放电中值电压(V)', chargePeakVoltage: '充电峰值电压(V)', dischargeValleyVoltage: '放电谷值电压(V)', chargeCapacity: '充电容量(mAh)', dischargeCapacity: '放电容量(mAh)', efficiency: '效率(%)', chargeEnergy: '充电能量(mWh)', dischargeEnergy: '放电能量(mWh)' }
function parseArray(value: string | null) { if (!value) return [] as Array<Record<string, unknown> | string>; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : [] } catch { return [] as Array<Record<string, unknown> | string> } }
// 字段变化只在编辑详情读取,导入和删除不解析或渲染逐条变化,审计原文仍由后端保存。
const identities = computed(() => parseArray(record.value?.identitySummary || null).map(item => String(item))); const changeRows = computed(() => parseArray(record.value?.changesSummary || null).map((item, index) => { if (!item || typeof item !== 'object') return { identity: identities.value[index] || '-', changes: [] as Array<{ field: string; before: unknown; after: unknown }> }; const row = item as Record<string, unknown>; const before = row.before && typeof row.before === 'object' ? row.before as Record<string, unknown> : {}; const after = row.after && typeof row.after === 'object' ? row.after as Record<string, unknown> : row; const changes = Object.keys(after).filter(key => before[key] !== after[key] && fieldLabels[key]).map(key => ({ field: fieldLabels[key], before: formatTestDisplayValue(key, before[key]), after: formatTestDisplayValue(key, after[key]) })); return { identity: String(row.identity || row.recordNo || identities.value[index] || '-'), changes } }))
const formatTime = formatRevisionTime; const formatScope = formatRevisionScope; function goBack() { router.push('/data-revision') }; function returnToTest() { if (record.value) router.push({ path: `/test-list/${record.value.testId}/console`, query: { tab: 'data-list', dataType: record.value.dataType, ...(record.value.testReceptionStatus === 'ENDED' ? { state: 'ended' } : {}) } }) }
onMounted(async () => { try { record.value = await getDataRevisionAction(Number(route.params.id)) } catch (error) { loadError.value = error instanceof Error ? error.message : '数据修订行为加载失败' } finally { loading.value = false } })
</script>
<template><div class="revision-detail-page"><div class="page-tools"><button class="back-button" type="button" @click="goBack"><el-icon><ArrowLeft /></el-icon>返回数据修订记录</button><span>DATA REVISION ACTION</span></div><section v-loading="loading" class="detail-panel"><template v-if="record"><header class="detail-toolbar"><div class="heading-group"><i></i><div><strong>修订行为 #{{ record.actionId }}</strong><span>一次用户提交 · 只读审计记录</span></div></div><span class="operation-tag" :class="record.operationType.toLowerCase()">{{ operationLabels[record.operationType] }}</span></header><section class="summary-section"><div class="summary-grid"><div><span>测试</span><strong>{{ record.testName }} (#{{ record.testId }})</strong></div><div><span>数据类型</span><strong>{{ dataTypeLabels[record.dataType] }}</strong></div><div><span>操作范围</span><strong>{{ formatScope(record.scopeSummary) }}</strong></div><div><span>涉及记录</span><strong>{{ identities.length }} 条</strong></div><div><span>操作人</span><strong>{{ record.operatorName }}</strong></div><div><span>操作时间</span><strong>{{ formatTime(record.createdAtUtc) }}</strong></div></div><div class="reason-line"><span>操作原因</span><p>{{ record.reason }}</p></div><button class="primary-button return-button" type="button" @click="returnToTest">回到对应测试数据列表</button></section><section class="data-section"><div class="section-title"><i></i><strong>记录编号</strong><span>RECORD NUMBERS</span></div><div class="identity-list"><code v-for="identity in identities" :key="identity">{{ identity }}</code><span v-if="!identities.length">暂无记录编号</span></div></section><section class="data-section"><div class="section-title"><i></i><strong>字段变化摘要</strong><span>CHANGE SUMMARY</span></div><div v-if="record.operationType === 'DELETE'" class="deleted-notice"><el-icon><Delete /></el-icon><div><strong>数据已物理删除</strong><p>本页仅保留本次删除行为的范围、身份和原因摘要,不恢复或展示已删除数据。</p></div></div><div v-else-if="changeRows.length" class="change-list"><article v-for="row in changeRows" :key="row.identity"><header>{{ row.identity }}</header><dl v-if="row.changes.length"><div v-for="change in row.changes" :key="change.field"><dt>{{ change.field }}</dt><dd><span>{{ change.before ?? '-' }}</span><el-icon><Right /></el-icon><strong>{{ change.after ?? '-' }}</strong></dd></div></dl><p v-else>本次导入包含该记录,字段值已按提交内容保存。</p></article></div><div v-else class="empty-summary">无字段变化摘要</div></section></template><div v-else-if="!loading" class="not-found"><el-empty :description="loadError || '未找到该数据修订行为'" /><button class="secondary-button" type="button" @click="goBack">返回记录列表</button></div></section></div></template>
<style scoped>.revision-detail-page{width:100%}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;cursor:pointer}.detail-panel{min-height:460px;background:#fff;border:1px solid #d8e1e9}.detail-toolbar{height:54px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;background:var(--surface-soft);border-bottom:1px solid var(--line)}.heading-group{display:flex;align-items:center;gap:9px}.heading-group>i,.section-title i{width:3px;height:18px;background:#2b78a3}.heading-group strong,.heading-group span{display:block}.heading-group strong{color:var(--ink);font-size:15px}.heading-group span{margin-top:2px;color:#81909d;font-size:10px}.operation-tag{padding:4px 9px;border:1px solid}.operation-tag.edit{color:#176fcb;background:#edf6ff;border-color:#b8d9fb}.operation-tag.import{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.operation-tag.delete{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.summary-section,.data-section{padding:20px 22px}.data-section{border-top:1px solid var(--line)}.summary-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px 28px}.summary-grid>div{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px}.summary-grid span,.reason-line>span{color:#7b8794;font-size:12px}.summary-grid strong{min-width:0;color:#34414d;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.reason-line{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px;margin:18px 0;padding-top:16px;border-top:1px dashed #dce5ed}.reason-line p{color:#34414d;font-size:13px}.primary-button,.secondary-button{height:34px;padding:0 13px;border:1px solid transparent;border-radius:3px;cursor:pointer}.primary-button{color:#fff;background:var(--blue)}.return-button{height:30px;padding:0 10px;font-size:12px}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.section-title{display:flex;align-items:center;gap:8px;margin-bottom:14px}.section-title i{height:15px}.section-title strong{color:#26333f;font-size:14px}.section-title span{color:#8a98a5;font-size:10px}.identity-list{display:flex;flex-wrap:wrap;gap:6px}.identity-list code{padding:3px 7px;border:1px solid #d8e3ec;background:#f6f8fa;color:#405261;font-size:11px;line-height:18px}.identity-list>span,.empty-summary{color:#8795a3;font-size:12px}.change-list{display:grid;gap:12px}.change-list article{border:1px solid #d8e3ec}.change-list article>header{padding:9px 12px;background:#f6f9fc;color:#405261;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px}.change-list dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;background:#e2e9ef}.change-list dl>div{padding:10px 12px;background:#fff}.change-list dt{margin-bottom:6px;color:#71808d;font-size:11px}.change-list dd{display:flex;align-items:center;gap:8px;font-size:12px}.change-list dd span{color:#8795a3;text-decoration:line-through}.change-list dd strong{color:#176fcb}.change-list article>p{padding:12px;color:#52606d;font-size:12px}.deleted-notice{display:flex;align-items:center;gap:14px;padding:18px;border:1px solid #efd0d0;background:#fff7f7;color:#b34f4f}.deleted-notice>.el-icon{font-size:24px}.deleted-notice p{margin-top:5px;color:#785c5c;font-size:12px}.not-found{min-height:420px;display:flex;align-items:center;justify-content:center;flex-direction:column}@media(max-width:900px){.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.change-list dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.summary-section,.data-section{padding:16px 12px}.summary-grid,.change-list dl{grid-template-columns:1fr}.reason-line{grid-template-columns:1fr}}</style>
<template><div class="revision-detail-page"><div class="page-tools"><button class="back-button" type="button" @click="goBack"><el-icon><ArrowLeft /></el-icon>返回数据修订记录</button><span>DATA REVISION ACTION</span></div><section v-loading="loading" class="detail-panel"><template v-if="record"><header class="detail-toolbar"><div class="heading-group"><i></i><div><strong>修订行为 #{{ record.actionId }}</strong><span>一次用户提交 · 只读审计记录</span></div></div><span class="operation-tag" :class="record.operationType.toLowerCase()">{{ operationLabels[record.operationType] }}</span></header><section class="summary-section"><div class="summary-grid"><div><span>测试</span><strong>{{ record.testName }} (#{{ record.testId }})</strong></div><div><span>数据类型</span><strong>{{ dataTypeLabels[record.dataType] }}</strong></div><div><span>操作范围</span><strong>{{ formatScope(record.scopeSummary) }}</strong></div><div><span>涉及记录</span><strong>{{ identities.length }} 条</strong></div><div><span>操作人</span><strong>{{ record.operatorName }}</strong></div><div><span>操作时间</span><strong>{{ formatTime(record.createdAtUtc) }}</strong></div></div><div class="reason-line"><span>操作原因</span><p>{{ record.reason }}</p></div><button class="primary-button return-button" type="button" @click="returnToTest">回到对应测试数据列表</button></section><section class="data-section"><div class="section-title"><i></i><strong>记录编号</strong><span>RECORD NUMBERS</span></div><div class="identity-list"><code v-for="identity in identities" :key="identity">{{ identity }}</code><span v-if="!identities.length">暂无记录编号</span></div></section><section v-if="record.operationType === 'EDIT'" class="data-section"><div class="section-title"><i></i><strong>字段变化摘要</strong><span>CHANGE SUMMARY</span></div><div v-if="changeRows.length" class="change-list"><article v-for="row in changeRows" :key="row.identity"><header>{{ row.identity }}</header><dl v-if="row.changes.length"><div v-for="change in row.changes" :key="change.field"><dt>{{ change.field }}</dt><dd><span>{{ change.before ?? '-' }}</span><el-icon><Right /></el-icon><strong>{{ change.after ?? '-' }}</strong></dd></div></dl><p v-else>无字段变化摘要</p></article></div><div v-else class="empty-summary">无字段变化摘要</div></section></template><div v-else-if="!loading" class="not-found"><el-empty :description="loadError || '未找到该数据修订行为'" /><button class="secondary-button" type="button" @click="goBack">返回记录列表</button></div></section></div></template>
<style scoped>.revision-detail-page{width:100%}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;cursor:pointer}.detail-panel{min-height:460px;background:#fff;border:1px solid #d8e1e9}.detail-toolbar{height:54px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;background:var(--surface-soft);border-bottom:1px solid var(--line)}.heading-group{display:flex;align-items:center;gap:9px}.heading-group>i,.section-title i{width:3px;height:18px;background:#2b78a3}.heading-group strong,.heading-group span{display:block}.heading-group strong{color:var(--ink);font-size:15px}.heading-group span{margin-top:2px;color:#81909d;font-size:10px}.operation-tag{padding:4px 9px;border:1px solid}.operation-tag.edit{color:#176fcb;background:#edf6ff;border-color:#b8d9fb}.operation-tag.import{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.operation-tag.delete{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.summary-section,.data-section{padding:20px 22px}.data-section{border-top:1px solid var(--line)}.summary-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px 28px}.summary-grid>div{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px}.summary-grid span,.reason-line>span{color:#7b8794;font-size:12px}.summary-grid strong{min-width:0;color:#34414d;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.reason-line{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px;margin:18px 0;padding-top:16px;border-top:1px dashed #dce5ed}.reason-line p{color:#34414d;font-size:13px}.primary-button,.secondary-button{height:34px;padding:0 13px;border:1px solid transparent;border-radius:3px;cursor:pointer}.primary-button{color:#fff;background:var(--blue)}.return-button{height:30px;padding:0 10px;font-size:12px}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.section-title{display:flex;align-items:center;gap:8px;margin-bottom:14px}.section-title i{height:15px}.section-title strong{color:#26333f;font-size:14px}.section-title span{color:#8a98a5;font-size:10px}.identity-list{display:flex;flex-wrap:wrap;gap:6px}.identity-list code{padding:3px 7px;border:1px solid #d8e3ec;background:#f6f8fa;color:#405261;font-size:11px;line-height:18px}.identity-list>span,.empty-summary{color:#8795a3;font-size:12px}.change-list{display:grid;gap:12px}.change-list article{border:1px solid #d8e3ec}.change-list article>header{padding:9px 12px;background:#f6f9fc;color:#405261;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px}.change-list dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;background:#e2e9ef}.change-list dl>div{padding:10px 12px;background:#fff}.change-list dt{margin-bottom:6px;color:#71808d;font-size:11px}.change-list dd{display:flex;align-items:center;gap:8px;font-size:12px}.change-list dd span{color:#8795a3;text-decoration:line-through}.change-list dd strong{color:#176fcb}.change-list article>p{padding:12px;color:#52606d;font-size:12px}.not-found{min-height:420px;display:flex;align-items:center;justify-content:center;flex-direction:column}@media(max-width:900px){.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.change-list dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.summary-section,.data-section{padding:16px 12px}.summary-grid,.change-list dl{grid-template-columns:1fr}.reason-line{grid-template-columns:1fr}}</style>
tests/test-data-revision-detail-browser.mjs
New file
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { chromium, expect } from '@playwright/test'
// #19 的只读修订摘要回放;页面加载真实构建产物,API 使用替身,不启动服务或写数据库。
const capturedChanges = [
  { identity: 'realtime:53519', before: { energy: 16.3385, current: -8.1659, voltage: 0.8999, capacity: 13.6507, temperature: 25.5, negativePotential: -0.105, positivePotential: 1.505 }, after: { temperature: '25.5001' } },
  { identity: 'realtime:53518', before: { energy: 16.3034, current: -8.1674, voltage: 0.9095, capacity: 13.6119, temperature: 25.4, negativePotential: -0.104, positivePotential: 1.504 }, after: { negativePotential: '-0.1041' } },
]
const origin = 'http://revision-detail.test'
const browser = await chromium.launch({ channel: 'chrome', headless: true })
try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
  const errors = []
  page.on('pageerror', error => errors.push(error.message))
  await page.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'browser-test'))
  let operationType = 'EDIT'
  let changes = process.env.MINIMAL_REPRO ? [{ identity: 'realtime:53519', before: { temperature: 25.5 }, after: { temperature: '25.5001' } }] : capturedChanges
  await page.route(`${origin}/**`, async route => {
    const path = new URL(route.request().url()).pathname
    if (path.startsWith('/assets/')) return route.fulfill({ body: await readFile(`dist${path}`), contentType: path.endsWith('.css') ? 'text/css' : 'text/javascript' })
    if (path.endsWith('/system/data-revision/19')) return route.fulfill({ json: { code: 200, data: {
      actionId: 19, testId: 4, testName: '修订回放测试', dataType: 'REALTIME', operationType,
      identitySummary: JSON.stringify(changes.map(row => row.identity)), changesSummary: JSON.stringify(changes),
      scopeSummary: '修改测量值', reason: '核对修订显示', operatorName: '测试操作员', createdAtUtc: 1788924699166,
    } } })
    if (path.endsWith('/getInfo')) return route.fulfill({ json: { code: 200, user: { userName: 'test' }, roles: ['admin'] } })
    if (path.includes('/system/')) return route.fulfill({ json: { code: 200, data: [], rows: [] } })
    return route.fulfill({ body: await readFile('dist/index.html'), contentType: 'text/html' })
  })
  await page.goto(`${origin}/data-revision/19`)
  await expect(page.locator('.change-list')).toContainText('温度(℃)')
  await expect(page.locator('.change-list')).toContainText('25.5000')
  await expect(page.locator('.change-list')).toContainText('25.5001')
  if (!process.env.MINIMAL_REPRO) {
    await expect(page.locator('.change-list')).toContainText('负电位(V)')
    await expect(page.locator('.change-list')).toContainText('-0.1040')
    await expect(page.locator('.change-list')).toContainText('-0.1041')
    await expect(page.locator('.change-list dt')).toHaveCount(2)
    await expect(page.locator('.change-list')).not.toContainText('本次导入')
    changes = [{ identity: 'realtime:1', before: { positivePotential: null, current: -8.1651, capacity: 0, temperature: 25 }, after: { positivePotential: '0', current: '-8', capacity: null, temperature: 25 } }]
    await page.reload()
    await expect(page.locator('.change-list')).toContainText('正电位(V)')
    await expect(page.locator('.change-list')).toContainText('-8165.1000')
    await expect(page.locator('.change-list')).toContainText('-8000.0000')
    await expect(page.locator('.change-list')).toContainText('0.0000')
    await expect(page.locator('.change-list dt')).toHaveCount(3)
    await page.setViewportSize({ width: 390, height: 844 })
    await expect(page.locator('.app-main')).toHaveCSS('margin-left', '0px')
    await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth > innerWidth)).toBe(false)
    for (const operation of ['IMPORT', 'DELETE']) {
      operationType = operation
      changes = Array.from({ length: 8000 }, (_, index) => ({ identity: `realtime:${index}`, voltage: 1.5 }))
      await page.reload()
      await expect(page.locator('.operation-tag')).toHaveText(operation === 'IMPORT' ? '导入' : '删除')
      await expect(page.getByText('字段变化摘要', { exact: true })).toHaveCount(0)
      await expect(page.locator('.change-list')).toHaveCount(0)
      await expect(page.locator('.summary-section')).toContainText('8000 条')
    }
  }
  assert.deepEqual(errors, [])
  console.log('通过:编辑字段前后值、毫单位、零与空值正确;导入和删除不展示字段变化。')
} finally { await browser.close() }