yeg
2026-09-08 e46cafc5d359de8a66d3deac15f7911bf6007ce4
补齐详情模板失败提示并保留同名字段填写值

字段加载失败时展示错误和重试并禁止应用;同名特殊字段转为电池组字段时保留各来源的填写值及空字符串。补充三类报表浏览器回归并通过全套前端测试。
2个文件已修改
50 ■■■■■ 已修改文件
src/components/ReportTestDetail.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/report-test-detail.ui.test.mjs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ReportTestDetail.vue
@@ -1,14 +1,16 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useBatteryGroupFields } from '@/stores/batteryGroupFields'
import { getBatteryGroupFields, type BatteryGroupField } from '@/api/batteryGroupFields'
import { reportDetailFields, snapshotReportDetails, type ReportDetailColumn, type ReportTestDetails } from '@/utils/reportTestDetails'
const props = defineProps<{ columns: ReportDetailColumn[]; modelValue?: ReportTestDetails; readonly?: boolean; disabled?: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [ReportTestDetails] }>()
const details = computed(() => snapshotReportDetails(props.columns, props.modelValue))
const fields = computed(() => reportDetailFields(details.value))
const { fields: managedFields } = useBatteryGroupFields()
const managedFields = ref<BatteryGroupField[]>([])
const fieldsLoading = ref(false)
const fieldsError = ref('')
const batteryOptions = computed(() => {
  const enabled = managedFields.value.filter(field => field.status === '启用').map(field => field.name)
  return [...new Set([...(enabled.length ? enabled : ['额定容量', '电流', '内阻', '质量', '材料批次']),
@@ -23,6 +25,7 @@
const selectedBatteryFields = ref<string[]>([])
const selectedSpecialTemplate = ref('循环寿命模板')
const locked = computed(() => props.readonly || props.disabled || !props.columns.length)
const templateLocked = computed(() => locked.value || (templateMode.value === 'battery' && (fieldsLoading.value || Boolean(fieldsError.value))))
function updateValue(column: ReportDetailColumn, field: typeof fields.value[number], value: string) {
  if (locked.value) return
@@ -64,13 +67,27 @@
  templateMode.value = mode
  selectedBatteryFields.value = fields.value.filter(field => field.category === 'battery').map(field => field.name)
  templateVisible.value = true
  if (mode === 'battery') void loadBatteryFields()
}
async function loadBatteryFields() {
  fieldsLoading.value = true
  fieldsError.value = ''
  try { managedFields.value = await getBatteryGroupFields() }
  catch (error) { fieldsError.value = error instanceof Error ? error.message : '电池组字段加载失败' }
  finally { fieldsLoading.value = false }
}
function applyTemplate() {
  if (locked.value) return
  if (templateLocked.value) return
  const next = snapshotReportDetails(props.columns, details.value)
  for (const detail of Object.values(next)) {
    if (templateMode.value === 'battery') {
      // 与控制台一致:同名特殊字段转为电池组字段时,保留本列原值及明确空字符串。
      for (const name of selectedBatteryFields.value) {
        const value = detail.fieldValues![`special|${name}`]
        if (!detail.batteryFields!.includes(name) && value !== undefined) detail.fieldValues![`battery|${name}`] = value
      }
      detail.batteryFields = [...selectedBatteryFields.value]
      detail.specialFields = detail.specialFields!.filter(name => !detail.batteryFields!.includes(name))
    } else detail.specialFields = [...specialTemplates[selectedSpecialTemplate.value]!].filter(name => !detail.batteryFields!.includes(name))
@@ -102,9 +119,13 @@
    </div>
    <div v-if="!readonly" class="detail-actions"><button class="secondary-button" type="button" :disabled="locked" @click="addField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" :disabled="locked" @click="openTemplate('battery')">电池组字段模板</button><button class="secondary-button" type="button" :disabled="locked" @click="openTemplate('special')">选择特殊字段模板</button></div>
    <el-dialog v-model="templateVisible" :title="templateMode === 'battery' ? '选择电池组字段模板' : '选择特殊字段模板'" width="620px" align-center>
      <el-checkbox-group v-if="templateMode === 'battery'" v-model="selectedBatteryFields" class="template-field-options"><el-checkbox v-for="field in batteryOptions" :key="field" :value="field">{{ field }}</el-checkbox></el-checkbox-group>
      <template v-if="templateMode === 'battery'">
        <p v-if="fieldsLoading" role="status">正在加载电池组字段</p>
        <div v-else-if="fieldsError" role="alert">{{ fieldsError }}<button class="secondary-button" type="button" @click="loadBatteryFields">重试</button></div>
        <el-checkbox-group v-else v-model="selectedBatteryFields" class="template-field-options"><el-checkbox v-for="field in batteryOptions" :key="field" :value="field">{{ field }}</el-checkbox></el-checkbox-group>
      </template>
      <el-radio-group v-else v-model="selectedSpecialTemplate" class="template-field-options"><el-radio v-for="(names, name) in specialTemplates" :key="name" :value="name" border>{{ name }}({{ names.join('、') }})</el-radio></el-radio-group>
      <template #footer><button class="secondary-button" type="button" @click="templateVisible=false">取消</button><button class="primary-button" type="button" :disabled="locked" @click="applyTemplate"><el-icon><Check /></el-icon>应用模板</button></template>
      <template #footer><button class="secondary-button" type="button" @click="templateVisible=false">取消</button><button class="primary-button" type="button" :disabled="templateLocked" @click="applyTemplate"><el-icon><Check /></el-icon>应用模板</button></template>
    </el-dialog>
  </section>
</template>
tests/report-test-detail.ui.test.mjs
@@ -29,7 +29,7 @@
  for (const type of ['compare', 'formation', 'splice']) {
    const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } })
    await context.addInitScript(() => { localStorage.setItem('battery_detection_auth_token', 'ui-fixture'); window.print = () => {} })
    let saved = fixture(type), submitted, failSave = true
    let saved = fixture(type), submitted, failSave = true, failFieldLoad = true
    const requests = [], errors = []
    await context.route('http://report.test/**', async route => {
      const url = new URL(route.request().url()), path = url.pathname
@@ -37,7 +37,10 @@
        requests.push({ path, method: route.request().method() })
        let response = { code: 200, msg: '成功', rows: [], total: 0 }
        if (path.endsWith('/getInfo')) Object.assign(response, { user: { userName: 'admin', nickName: '操作员' }, roles: ['admin'], permissions: ['*:*:*'] })
        else if (path.includes('/batteryGroupField/')) response.rows = [{ fieldId: 1, fieldName: '额定容量', fieldType: 'text', status: '0' }, { fieldId: 2, fieldName: '质量', fieldType: 'text', status: '0' }]
        else if (path.includes('/batteryGroupField/')) {
          if (failFieldLoad) { failFieldLoad = false; response = { code: 500, msg: '字段加载失败' } }
          else response.rows = [{ fieldId: 1, fieldName: '额定容量', fieldType: 'text', status: '0' }, { fieldId: 2, fieldName: '质量', fieldType: 'text', status: '0' }]
        }
        else if (path.includes(`/${type}-report/7`)) {
          if (route.request().method() === 'POST') {
            if (failSave) { failSave = false; response = { code: 500, msg: '测试保存失败' } }
@@ -68,8 +71,15 @@
    await page.getByRole('button', { name: '添加', exact: true }).click()
    await row('新字段').locator('input').nth(0).fill('甲填写')
    await row('新字段').locator('input').nth(1).fill('乙填写')
    await detail.getByRole('button', { name: '添加项', exact: true }).click()
    await page.getByRole('dialog', { name: '添加项' }).getByRole('textbox').fill('质量')
    await page.getByRole('button', { name: '添加', exact: true }).click()
    await row('质量').locator('input').nth(0).fill('同名保留')
    await detail.getByRole('button', { name: '电池组字段模板', exact: true }).click()
    let dialog = page.getByRole('dialog', { name: '选择电池组字段模板' })
    await expect(dialog.getByRole('alert')).toContainText('字段加载失败')
    await expect(dialog.getByRole('button', { name: '应用模板', exact: true })).toBeDisabled()
    await dialog.getByRole('button', { name: '重试', exact: true }).click()
    await dialog.locator('.el-checkbox').filter({ hasText: '额定容量' }).click()
    await dialog.getByRole('button', { name: '取消', exact: true }).click()
    await expect(row('额定容量')).toHaveCount(1)
@@ -79,6 +89,8 @@
    await expect(dialog).not.toBeVisible()
    await expect(row('额定容量').locator('input').nth(1)).toHaveValue('报表乙')
    await expect(row('新字段').locator('input').nth(0)).toHaveValue('甲填写')
    await expect(row('质量').locator('input').nth(0)).toHaveValue('同名保留')
    await expect(row('质量').locator('input').nth(1)).toHaveValue('')
    await detail.getByRole('button', { name: '选择特殊字段模板', exact: true }).click()
    dialog = page.getByRole('dialog', { name: '选择特殊字段模板' })
    await dialog.locator('.el-radio').filter({ hasText: '热特性模板' }).click()
@@ -102,7 +114,8 @@
    await expect(row('最高温度').locator('input').nth(0)).toHaveValue('报表最高温度')
    await save.click()
    await expect.poll(() => submitted).toBeTruthy()
    assert.deepEqual(submitted.settings.sourceDetailOverrides['1|1'], { batteryFields: ['额定容量', '质量'], specialFields: ['最高温度', '温差', '温升速率'], fieldValues: { 'battery|额定容量': '', 'special|最高温度': '报表最高温度' } })
    assert.deepEqual(submitted.settings.sourceDetailOverrides['1|1'], { batteryFields: ['额定容量', '质量'], specialFields: ['最高温度', '温差', '温升速率'], fieldValues: { 'battery|额定容量': '', 'battery|质量': '同名保留', 'special|最高温度': '报表最高温度' } })
    assert.equal(submitted.settings.sourceDetailOverrides['2|2'].fieldValues['battery|质量'], '')
    assert.equal(submitted.settings.sourceDetailOverrides['2|2'].fieldValues['special|最高温度'], '')
    if (type === 'formation') assert.ok(submitted.settings.fields.every(field => field.scope === 'cycle'))
    else assert.deepEqual(submitted.settings.fieldStructure, [])