yeg
2026-09-08 497d3bbcbd26df3e417b6c0f735d9e3c1ab111bc
恢复化成详情表单并保留工步设置
3个文件已修改
43 ■■■■■ 已修改文件
src/api/formationReports.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/FormationReportForm.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/formation-source-details.spec.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/formationReports.ts
@@ -1,9 +1,9 @@
import { request, type ApiResponse } from './request'
import type { PersistedTestRecord } from './tests'
import type { PersistedTestRecord, TestConsoleDetail } from './tests'
import type { ReportMetricKey } from '@/utils/reportMeasurementChart'
export interface FormationSelection { testId: number; testBatteryId: number }
export interface FormationCandidate { selection: FormationSelection; name: string; type: string; groupId: number; batteryGroup: string; batteryNo: string; consoleDetail?: { experimentName?: string; batteryFields: string[]; specialFields: string[]; fieldValues: Record<string, Record<string, string>> } }
export interface FormationCandidate { selection: FormationSelection; name: string; type: string; groupId: number; batteryGroup: string; batteryNo: string; consoleDetail?: TestConsoleDetail | null }
export interface FormationField { key: string; scope: 'detail' | 'cycle'; kind: 'battery' | 'metric' | 'manual'; displayName: string; unit: string; fieldDefinitionId?: number | null }
export interface FormationChargeStep { stepNo: number; name: string; stepType: string; endVoltage: number | null; direction?: 'CHARGE' | 'DISCHARGE' | 'UNKNOWN' }
export interface FormationItem {
src/views/FormationReportForm.vue
@@ -14,6 +14,7 @@
import type { ReportMetricKey } from '@/utils/reportMeasurementChart'
import { openFormationSavedReportPrint } from '@/utils/formationSavedReportPrint'
import FormationFieldControls from '@/components/FormationFieldControls.vue'
import { formationSourceKey, formationSourceFields, formationDetailValue } from '@/utils/formationSourceDetails'
import FormationStepCurves from '@/components/FormationStepCurves.vue'
import { formationChargeSteps, formationCycleKey } from '@/utils/formationStepChart'
import type { FormationSettings, FormationStepSelection } from '@/api/formationReports'
@@ -50,6 +51,13 @@
const cycleNos = ref<number[]>([])
const fields = ref<FormationField[]>(structuredClone(defaultFormationFields))
const values = ref<Record<string, string>>({})
const sourceDetailOverrides = ref<NonNullable<FormationSettings['sourceDetailOverrides']>>({})
const sourceFields = computed(() => formationSourceFields(reportItems.value.map(item => item.source), sourceDetailOverrides.value))
function setDetail(source: FormationCandidate, value: string, field?: typeof sourceFields.value[number]) {
  const key = formationSourceKey(source)
  const current = sourceDetailOverrides.value[key] ?? {}
  sourceDetailOverrides.value[key] = field ? { ...current, fieldValues: { ...current.fieldValues, [field.key]: value } } : { ...current, experimentName: value }
}
const chargeStepSelections = ref<Record<string, number>>({})
const metrics = ref<ReportMetricKey[]>(formationCurveMetrics.map(metric => metric.key))
const seriesColors = ref<Record<string, string>>({})
@@ -58,7 +66,7 @@
const curveStepSelections = ref<Record<string, FormationStepSelection>>({})
const hiddenSeriesKeys = ref<string[]>([])
const actualSteps = computed(() => !saved.value || saved.value.curveMode === 'actual-steps')
const stepSettings = computed<FormationSettings>(() => ({ fields: fields.value, values: values.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value, manualChargeSteps: manualChargeSteps.value, curveStepSelections: curveStepSelections.value, hiddenSeriesKeys: hiddenSeriesKeys.value, activeCycle: activeCycle.value }))
const stepSettings = computed<FormationSettings>(() => ({ fields: fields.value, values: values.value, sourceDetailOverrides: sourceDetailOverrides.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value, manualChargeSteps: manualChargeSteps.value, curveStepSelections: curveStepSelections.value, hiddenSeriesKeys: hiddenSeriesKeys.value, activeCycle: activeCycle.value }))
function changeStepSettings(settings: Partial<FormationSettings>) {
  if (settings.curveStepSelections) curveStepSelections.value = settings.curveStepSelections
  if (settings.hiddenSeriesKeys) hiddenSeriesKeys.value = settings.hiddenSeriesKeys
@@ -95,11 +103,12 @@
}
function chargeKey(item: FormationItem, cycle: number) { return `${item.source.selection.testId}|${item.source.selection.testBatteryId}|${cycle}` }
function updateSeriesColor(item: FormationItem, color: string | null) { if (color) seriesColors.value[formationSeriesKey(item)] = color }
function formContent() { return JSON.stringify({ name: reportName.value, description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } }) }
function formContent() { return JSON.stringify({ name: reportName.value, description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, sourceDetailOverrides: sourceDetailOverrides.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } }) }
function restore(record: SavedFormationReport) {
  saved.value = record; reportName.value = record.name; description.value = record.description || ''
  items.value = record.items; cycleNos.value = record.cycleNos
  fields.value = record.settings.fields; values.value = record.settings.values; chargeStepSelections.value = record.settings.chargeStepSelections || {}
  sourceDetailOverrides.value = record.settings.sourceDetailOverrides ?? {}
  metrics.value = record.settings.metrics ?? formationCurveMetrics.map(metric => metric.key)
  seriesColors.value = record.settings.seriesColors ?? {}
  manualChargeSteps.value = record.settings.manualChargeSteps ?? {}; curveStepSelections.value = record.settings.curveStepSelections ?? {}
@@ -111,6 +120,7 @@
    (field.scope === 'detail' ? [0] : item.cycleNos).map(cycle => formationValueKey(item, cycle, field)))))
  values.value = Object.fromEntries(Object.entries(values.value).filter(([key]) => allowed.has(key)))
  const sourceKeys = new Set(items.value.flatMap(item => [formationSeriesKey(item), ...Object.entries(item.stepCurves ?? {}).flatMap(([cycle, steps]) => Object.keys(steps).map(step => `${formationSeriesKey(item)}|${cycle}|${step}`))]))
  sourceDetailOverrides.value = Object.fromEntries(Object.entries(sourceDetailOverrides.value).filter(([key]) => sourceKeys.has(key)))
  hiddenSeriesKeys.value = hiddenSeriesKeys.value.filter(key => sourceKeys.has(key))
  const cycleKeys = new Set(items.value.flatMap(item => item.cycleNos.map(cycle => formationCycleKey(item, cycle))))
  manualChargeSteps.value = Object.fromEntries(Object.entries(manualChargeSteps.value).filter(([key]) => cycleKeys.has(key)))
@@ -135,7 +145,7 @@
  saving.value = true
  try {
    pruneValues()
    const content = { name: reportName.value.trim(), description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } }
    const content = { name: reportName.value.trim(), description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, sourceDetailOverrides: sourceDetailOverrides.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } }
    const result = editingId.value && saved.value
      ? await editFormationReport(editingId.value, { ...content, baseVersionNo: saved.value.versionNo, changeSummary: changeSummary.value.trim() })
      : await createFormationReport({ ...content, selections: items.value.map(item => item.source.selection) })
@@ -219,6 +229,7 @@
  loading.value = true; loadError.value = ''
  previewSequence++; previewLoading.value = false; previewError.value = ''
  saved.value = null; items.value = []; cycleNos.value = []; values.value = {}; chargeStepSelections.value = {}; selectedKeys.value = []
  sourceDetailOverrides.value = {}
  try {
    const permissions = (await getUserInfo()).permissions || []
    const has = (action: string) => permissions.includes('*:*:*') || permissions.includes(`system:formation-report:${action}`)
@@ -262,7 +273,10 @@
          <div class="report-data-table-wrap"><table class="report-data-table" :style="tableStyle">
            <thead><tr><th>测试</th><th v-for="column in reportColumns" :key="column.id" :colspan="column.items.length">{{ column.name }}</th></tr>
              <tr><th>电池组 / 电池号</th><th v-for="item in reportItems" :key="selectionKey(item.source)">{{ item.source.batteryGroup }} / {{ item.source.batteryNo }}</th></tr></thead>
            <tbody><tr v-for="field in detailFields" :key="field.key"><td>{{ formationFieldLabel(field) }}</td><td v-for="item in reportItems" :key="selectionKey(item.source)">
            <tbody>
              <tr><td>实验名称</td><td v-for="item in reportItems" :key="selectionKey(item.source)"><el-input :model-value="formationDetailValue(item.source, sourceDetailOverrides)" :disabled="readonly" :aria-label="`${item.source.name} ${item.source.batteryNo} 实验名称`" @update:model-value="setDetail(item.source, $event)" /></td></tr>
              <tr v-for="field in sourceFields" :key="`console:${field.key}`"><td>{{ field.name }}({{ field.category === 'battery' ? '电池字段' : '特殊字段' }})</td><td v-for="item in reportItems" :key="selectionKey(item.source)"><el-input :model-value="formationDetailValue(item.source, sourceDetailOverrides, field)" :disabled="readonly" :aria-label="`${item.source.name} ${item.source.batteryNo} 详情 ${field.name}`" @update:model-value="setDetail(item.source, $event, field)" /></td></tr>
              <tr v-for="field in detailFields" :key="field.key"><td>{{ formationFieldLabel(field) }}</td><td v-for="item in reportItems" :key="selectionKey(item.source)">
              <el-input :model-value="inputValue(item, 0, field)" @update:model-value="setValue(item, 0, field, $event)" :disabled="readonly" :aria-label="`${item.source.name} ${item.source.batteryNo} ${field.displayName}`" :placeholder="field.kind === 'manual' ? '人工填写' : '指标缺失'" size="small" />
            </td></tr></tbody>
          </table></div>
tests/e2e/formation-source-details.spec.ts
@@ -83,6 +83,16 @@
  expect(first.settings.sourceDetailOverrides['9501|9501'].fieldValues['battery|人工字段']).toBe('报表覆盖')
  expect(first.settings.sourceDetailOverrides['9502|9503'].experimentName).toBe('')
  expect((await api(page, 'test/9501')).consoleDetail.fieldValues.人工字段.同名电池).toBe('甲值')
  for (const sourceDetailOverrides of [
    { '9506|9507': { experimentName: '伪造来源' } },
    { '9501|9501|1|1': { experimentName: '曲线范围不是详情身份' } },
    { '9501|9501': { fieldValues: { voltage: '不能冒充设备字段' } } },
  ]) {
    await api(page, `formation-report/${id}/versions`, 'POST', {
      name: first.name, description: '', baseVersionNo: 1, changeSummary: '拒绝伪造详情', settings: { ...first.settings, sourceDetailOverrides },
    }, 500)
  }
  expect((await api(page, `formation-report/${id}`)).versionNo).toBe(1)
  const fields = first.settings.fields
  const template = await api(page, 'formation-report/templates', 'POST', { name: '纯结构模板', fields })
  expect(template).toBeGreaterThan(0)
@@ -96,10 +106,15 @@
  await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 详情 人工字段', exact: true })).toHaveValue('报表覆盖')
  await page.getByRole('textbox', { name: '化成详情甲 同名电池 实验名称', exact: true }).fill('报表实验甲')
  await page.getByPlaceholder('请输入修改说明').fill('详情独立编辑')
  const versionResponse = page.waitForResponse(response => response.url().endsWith(`/formation-report/${id}/versions`) && response.request().method() === 'POST')
  await page.getByRole('button', { name: '保存新版本', exact: true }).click()
  const versionBody = await (await versionResponse).json()
  expect(versionBody.code, versionBody.msg).toBe(200)
  expect(versionBody.data.versionNo).toBe(2)
  await expect(page).toHaveURL(/mode=view$/)
  await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 实验名称', exact: true })).toHaveValue('报表实验甲')
  const second = await api(page, `formation-report/${id}`)
  expect(second.versionNo).toBeGreaterThanOrEqual(1)
  expect(second.versionNo).toBe(2)
  expect(second.items).toEqual(first.items)
  await api(page, `formation-report/${id}/versions`, 'POST', { name: first.name, description: '', baseVersionNo: 1, changeSummary: '陈旧', settings: first.settings }, 500)
  await api(page, 'test/9501/console-detail', 'PUT', { ...detailA, experimentName: '最新来源实验' })