yeg
2026-09-06 3b39a1c00f3318da6a88767d63d1a0abb731da84
修复对比报表切换与字段快照回显

按报表编号重新加载详情并保留显式清空字段,完整替换图表配置以避免曲线颜色合并错误。补充全局搜索切换、后退和窄屏图表回归验证。
3个文件已修改
84 ■■■■■ 已修改文件
src/router/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ReportCompareForm.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/compare-save-and-reopen.spec.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts
@@ -171,7 +171,7 @@
          path: 'report-compare/:id/edit',
          name: 'report-compare-edit',
          component: () => import('@/views/ReportCompareForm.vue'),
          meta: { title: '编辑对比报表' },
          meta: { title: '对比报表详情' },
        },
        {
          path: 'report-formation',
src/views/ReportCompareForm.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { toTestDisplayValue } from '@/utils/testDataUnits'
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { use } from 'echarts/core'
@@ -140,7 +140,11 @@
  if (!detailFieldValues[field]) detailFieldValues[field] = {}
  detailTests.value.forEach(item => {
    const key = detailKey(item)
    if (detailFieldValues[field][key] === undefined) detailFieldValues[field][key] = ''
    // 未填写才读取当前预览或保存快照的业务值,显式清空不被后续预览覆盖。
    if (detailFieldValues[field][key] === undefined) {
      const source = sourceItems.value.find(value => compareSelectionKey(value.source.selection) === key)
      detailFieldValues[field][key] = source?.businessFields.find(value => value.fieldName === field)?.fieldValue ?? ''
    }
  })
}
@@ -173,34 +177,52 @@
}
detailFields.value.forEach(ensureDetailFieldValues)
onMounted(async () => {
watch(editingId, async (id, _previous, onCleanup) => {
  let cancelled = false
  onCleanup(() => { cancelled = true })
  editingRecord.value = null
  selectedTests.value = []
  sourceItems.value = []
  reportName.value = ''
  description.value = ''
  experimentName.value = ''
  batteryFields.value = ['额定容量', '电流', '内阻']
  specialFields.value = ['温升']
  seriesColors.value = ['#3978f6', '#7d42d8', '#f08a3e']
  metricDefinitions.forEach(metric => { metricVisible[metric.key] = true })
  Object.keys(detailFieldValues).forEach(field => delete detailFieldValues[field])
  loadError.value = ''
  detailFields.value.forEach(ensureDetailFieldValues)
  previewError.value = ''
  canCreate.value = false
  reportLoading.value = true
  try {
    if (editingId.value) {
      editingRecord.value = await getCompareReport(editingId.value)
    if (id) {
      const record = await getCompareReport(id)
      if (cancelled) return
      editingRecord.value = record
      loadEditingReport()
    } else {
      const info = await getUserInfo()
      if (cancelled) return
      canCreate.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:add'))
      if (!canCreate.value) throw new Error('无新增对比报表权限')
      await loadCandidates()
    }
  } catch (error) { loadError.value = error instanceof Error ? error.message : '报表加载失败' }
  finally { reportLoading.value = false }
})
  } catch (error) { if (!cancelled) loadError.value = error instanceof Error ? error.message : '报表加载失败' }
  finally { if (!cancelled) reportLoading.value = false }
}, { immediate: true })
async function refreshPreview() {
  const selections = selectedTests.value.map(item => item.selection)
  previewLoading.value = true
  previewError.value = ''
  sourceItems.value = []
  try {
    sourceItems.value = await previewCompareReport(selectedTests.value.map(item => item.selection))
    const items = await previewCompareReport(selections)
    if (readonly.value) return
    sourceItems.value = items
    detailFields.value.forEach(ensureDetailFieldValues)
    sourceItems.value.forEach(item => item.businessFields.forEach(field => {
      const key = compareSelectionKey(item.source.selection)
      if (batteryFields.value.includes(field.fieldName) && !detailFieldValues[field.fieldName]?.[key])
        detailFieldValues[field.fieldName][key] = field.fieldValue ?? ''
    }))
  } catch (error) { previewError.value = error instanceof Error ? error.message : '曲线预览失败' }
  finally { previewLoading.value = false }
}
@@ -237,7 +259,6 @@
  }
  selectedTests.value.push(toSelectedTest(candidate))
  if (!seriesColors.value[selectedTests.value.length - 1]) seriesColors.value.push('#109854')
  detailFields.value.forEach(ensureDetailFieldValues)
  testDialogVisible.value = false
  await refreshPreview()
}
@@ -445,7 +466,8 @@
    <section class="metrics-panel">
      <div v-if="!readonly" class="metric-switches"><label v-for="metric in metricDefinitions" :key="metric.key"><span>{{ metric.title }}</span><el-switch v-model="metricVisible[metric.key]" inline-prompt active-text="显" inactive-text="隐" /></label></div>
      <div v-if="chartSeries.length" class="chart-legend-bar"><strong>对比图例</strong><div v-for="(item, index) in chartSeries" :key="detailKey(item)" class="chart-legend-item"><el-color-picker v-if="!readonly" class="legend-color-picker" :model-value="seriesColors[index]" :show-alpha="false" popper-class="legend-color-popper" :aria-label="`修改${item.name}颜色`" @change="updateSeriesColor(index, $event)" /><i v-else class="series-swatch" :style="{ background: seriesColors[index] }" /><span>{{ item.name }} · {{ item.batteryNo }} · 循环{{ item.cycle }} · {{ item.process }}</span></div></div>
      <div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong></header><VChart class="metric-chart" :option="chartOption(metric)" autoresize /></article></div>
      <!-- 报表切换和比对项移除时完整替换配置,避免图表库将颜色数组缩短误判为组件合并。 -->
      <div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong></header><VChart class="metric-chart" :option="chartOption(metric)" :update-options="{ notMerge: true }" autoresize /></article></div>
    </section>
    <footer class="form-actions"><button v-if="readonly" class="primary-button" type="button" @click="router.push('/report-compare')">返回列表</button><button v-else-if="canCreate" class="primary-button save-button" type="button" :disabled="saving || previewLoading || !!previewError" @click="saveReport"><el-icon><Check /></el-icon>{{ saving ? '保存中' : '保存报表' }}</button></footer>
tests/e2e/compare-save-and-reopen.spec.ts
@@ -43,8 +43,8 @@
function seedSources() {
  stack.executeIsolatedSql(`
    INSERT INTO t_battery_group(group_id,group_name,model) VALUES(8101,'对比验收电池组','验收型号');
    INSERT INTO t_battery_group_field(field_id,field_name,field_type) VALUES(8101,'额定容量','number');
    INSERT INTO t_battery_group_relation_field(group_id,battery_group_field_id,field_value) VALUES(8101,8101,'12.5');
    INSERT INTO t_battery_group_field(field_id,field_name,field_type) VALUES(8101,'额定容量','number'),(8102,'质量','number');
    INSERT INTO t_battery_group_relation_field(group_id,battery_group_field_id,field_value) VALUES(8101,8101,'12.5'),(8101,8102,'88');
    INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(8101,'对比验收类型');
    INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status,logic_step_mappings_json)
    VALUES(8101,'对比来源测试',1,1,8101,'隔离受控数据','COMPLETED','ENDED','[{"logicalNo":1,"logicalName":"合并充放电","startStepNo":1,"endStepNo":2}]'),
@@ -63,6 +63,10 @@
}
test('首次保存冻结真实来源,失败回滚且重启与删除来源后仍可重开', async ({ page }) => {
  const pageErrors: string[] = []
  page.on('pageerror', error => {
    if (page.url().includes('/report-compare')) pageErrors.push(error.message)
  })
  seedSources()
  await login(page)
  const sources = (await api(page, '/system/compare-report/sources')).data
@@ -123,6 +127,10 @@
    await expect(page.getByRole('button', { name: '添加比较', exact: true })).toBeEnabled()
  }
  await page.locator('.metric-switches label').filter({ hasText: '正电位' }).locator('.el-switch__core').click()
  await page.getByRole('button', { name: '电池组字段', exact: true }).click()
  await page.getByRole('dialog').getByText('质量', { exact: true }).click()
  await page.getByRole('dialog').getByRole('button', { name: '确认', exact: true }).click()
  await expect(page.locator('.field-table tbody tr').filter({ hasText: '质量' }).locator('input').first()).toHaveValue('88')
  await page.route('**/dev-api/system/compare-report', async route => {
    await route.fulfill({ json: { code: 500, msg: '验收保存失败' } })
  }, { times: 1 })
@@ -137,6 +145,7 @@
  expect(browserSaved.items).toHaveLength(3)
  expect(browserSaved.settings.metrics).not.toContain('positiveVoltage')
  expect(browserSaved.description).toBe('浏览器说明')
  expect(browserSaved.settings.detailFieldValues['质量']['8101|8101|1|1|']).toBe('88')
  await api(page, '/system/test/8101/console-data/actions', 'POST', {
    dataType: 'REALTIME', operationType: 'EDIT', scopeSummary: '首行电流修订', reason: '验证报表独立',
@@ -164,9 +173,11 @@
  await expect(page.getByPlaceholder('请输入报表名称')).toHaveValue('首版独立快照')
  await expect(page.getByPlaceholder('请输入说明')).toHaveValue('保存后保留说明')
  await expect(page.getByPlaceholder('请输入报表名称')).toHaveAttribute('readonly')
  await expect(page.locator('.field-table tbody tr').filter({ hasText: '额定容量' }).locator('input').first()).toHaveValue('12.5')
  for (const width of [1440, 390]) {
    await page.setViewportSize({ width, height: 900 })
    await page.screenshot({ path: `tests/e2e/.artifacts/compare-reopen-${width}.png`, fullPage: true })
    await expect.poll(async () => (await page.locator('.metric-chart canvas').first().boundingBox())!.width).toBeGreaterThan(200)
    await page.screenshot({ path: `tests/e2e/.artifacts/compare-reopen-${width}.png`, fullPage: true, animations: 'disabled' })
    expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
    const coloredPixels = await page.locator('.metric-chart canvas').first().evaluate((canvas: HTMLCanvasElement) => {
      const pixels = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height).data
@@ -177,6 +188,16 @@
    })
    expect(coloredPixels).toBeGreaterThan(20)
  }
  await page.setViewportSize({ width: 1440, height: 900 })
  await page.getByRole('button', { name: '页面搜索', exact: true }).click()
  await page.getByPlaceholder('搜索页面、测试名称或报表').fill('浏览器保存报表')
  await page.locator('.search-result').filter({ hasText: '浏览器保存报表' }).click()
  await expect(page.getByPlaceholder('请输入报表名称')).toHaveValue('浏览器保存报表')
  await expect(page.locator('.compare-table tbody tr')).toHaveCount(3)
  await expect(page.locator('.field-table tbody tr').filter({ hasText: '质量' }).locator('input').first()).toHaveValue('88')
  await page.goBack()
  await expect(page.getByPlaceholder('请输入报表名称')).toHaveValue('首版独立快照')
  const menus = (await api(page, '/system/menu/permissionList')).data
  const comparePage = menus.flatMap((module: any) => module.pages).find((item: any) => item.pageName === '对比报表')
@@ -207,4 +228,5 @@
    }
    if (username === 'compare_author') expect((await api(page, '/system/compare-report/sources')).data).toEqual([])
  }
  expect(pageErrors).toEqual([])
})