yeg
2026-09-08 cfd4511378aefa04d4e06058cee628b6edd8e9c1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useBatteryGroupFields } from '@/stores/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 batteryOptions = computed(() => {
  const enabled = managedFields.value.filter(field => field.status === '启用').map(field => field.name)
  return [...new Set([...(enabled.length ? enabled : ['额定容量', '电流', '内阻', '质量', '材料批次']),
    ...fields.value.filter(field => field.category === 'battery').map(field => field.name)])]
})
const specialTemplates: Record<string, string[]> = {
  循环寿命模板: ['容量保持率', '能量效率', '循环次数', '内阻变化'],
  热特性模板: ['最高温度', '温差', '温升速率'],
}
const templateVisible = ref(false)
const templateMode = ref<'battery' | 'special'>('battery')
const selectedBatteryFields = ref<string[]>([])
const selectedSpecialTemplate = ref('循环寿命模板')
const locked = computed(() => props.readonly || props.disabled || !props.columns.length)
 
function updateValue(column: ReportDetailColumn, field: typeof fields.value[number], value: string) {
  if (locked.value) return
  const next = snapshotReportDetails(props.columns, details.value)
  const detail = next[column.key]!
  const names = detail[`${field.category}Fields`]!
  if (!names.includes(field.name)) names.push(field.name)
  detail.fieldValues![field.key] = value
  emit('update:modelValue', next)
}
 
async function addField() {
  if (locked.value) return
  try {
    const { value } = await ElMessageBox.prompt('请输入要添加的字段名称', '添加项', {
      confirmButtonText: '添加', cancelButtonText: '取消', inputPlaceholder: '例如:库仑效率',
      inputValidator: value => Boolean(value?.trim()) && value.trim().length <= 100 && !/[\r\n]/.test(value) || '请输入不超过100字的字段名称',
    })
    if (locked.value) return
    const name = value.trim()
    if (fields.value.some(field => field.name === name)) { ElMessage.warning('该字段已经存在'); return }
    const next = snapshotReportDetails(props.columns, details.value)
    for (const detail of Object.values(next)) { detail.specialFields!.push(name); detail.fieldValues![`special|${name}`] = '' }
    emit('update:modelValue', next)
  } catch { /* 用户取消添加。 */ }
}
 
function removeField(name: string) {
  if (locked.value) return
  const next = snapshotReportDetails(props.columns, details.value)
  for (const detail of Object.values(next)) {
    detail.specialFields = detail.specialFields!.filter(field => field !== name)
    delete detail.fieldValues![`special|${name}`]
  }
  emit('update:modelValue', next)
}
 
function openTemplate(mode: 'battery' | 'special') {
  templateMode.value = mode
  selectedBatteryFields.value = fields.value.filter(field => field.category === 'battery').map(field => field.name)
  templateVisible.value = true
}
 
function applyTemplate() {
  if (locked.value) return
  const next = snapshotReportDetails(props.columns, details.value)
  for (const detail of Object.values(next)) {
    if (templateMode.value === 'battery') {
      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))
  }
  // 去除模板已取消的格,避免保存后恢复;仍选字段的值不会被模板覆盖。
  emit('update:modelValue', snapshotReportDetails(props.columns, next))
  templateVisible.value = false
  ElMessage.success('字段模板已应用')
}
</script>
 
<template>
  <section class="detail-panel report-test-detail">
    <header class="panel-heading"><div><strong>测试详情</strong><el-icon><ArrowUp /></el-icon></div></header>
    <div class="field-table-wrap">
      <table class="field-table" :class="{ 'with-actions': !readonly }" :style="{ minWidth: `${Math.max(900, columns.length * 220 + 214)}px` }">
        <thead><tr><th>字段</th><th v-for="column in columns" :key="column.key">{{ column.batteryNo }}</th><th v-if="!readonly">操作</th></tr></thead>
        <tbody>
          <tr class="detail-meta-row"><td>测试</td><td v-for="column in columns" :key="column.key">{{ column.testName }}</td><td v-if="!readonly">-</td></tr>
          <tr class="detail-meta-row"><td>电池组</td><td v-for="column in columns" :key="column.key">{{ column.batteryGroup }}</td><td v-if="!readonly">-</td></tr>
          <tr class="sample-row"><td>电池号</td><td v-for="column in columns" :key="column.key">{{ column.batteryNo }}</td><td v-if="!readonly">-</td></tr>
          <tr v-for="field in fields" :key="field.key" class="source-detail-row" :data-field="field.key"><td>{{ field.name }}</td>
            <td v-for="column in columns" :key="column.key"><el-input :model-value="details[column.key]?.fieldValues?.[field.key] ?? ''" :readonly="readonly" :disabled="disabled" maxlength="500" size="small" :placeholder="details[column.key]?.fieldValues?.[field.key] === undefined ? '缺失' : '请输入'" :aria-label="`${column.testName} ${column.batteryNo} ${field.name}`" @update:model-value="updateValue(column, field, $event)" /></td>
            <td v-if="!readonly"><button v-if="field.category === 'special'" class="delete-link" type="button" :disabled="locked" @click="removeField(field.name)">删除</button><span v-else class="field-lock">-</span></td>
          </tr>
          <tr v-if="!fields.length"><td :colspan="columns.length + (readonly ? 1 : 2)" class="empty-cell">暂无字段,请先选择电池组模板或添加特殊字段</td></tr>
        </tbody>
      </table>
    </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>
      <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>
    </el-dialog>
  </section>
</template>
 
<style scoped>
.detail-panel{min-width:0;border:1px solid #d8e1e9;background:#fff;margin-bottom:16px;padding-bottom:18px}.panel-heading{min-height:48px;display:flex;align-items:center;padding:0 24px;border-bottom:1px solid var(--line)}.panel-heading>div{display:flex;align-items:center;gap:9px;color:#26333f;font-size:13px}.panel-heading>div::before{content:'';width:3px;height:16px;background:#2b78a3}.field-table-wrap{width:100%;max-width:100%;overflow-x:auto;padding:18px 24px 0}.field-table{width:100%;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:13px}.field-table th,.field-table td{height:42px;padding:0 12px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.field-table th{color:#fff;background:#3c6497;font-size:12px;font-weight:600}.field-table th:first-child,.field-table td:first-child{width:140px;text-align:left}.field-table.with-actions th:last-child,.field-table.with-actions td:last-child{width:74px}.field-table tr:hover{background:#f7fafc}.detail-meta-row td,.sample-row td{background:#fbfdff}.detail-meta-row td:not(:first-child){font-weight:500;color:#26333f}.field-table :deep(.el-input__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.field-lock,.empty-cell{color:#8795a3}.empty-cell{height:72px!important}.delete-link{border:0;color:#d15b5b;background:transparent;font-size:12px;cursor:pointer}.detail-actions{display:flex;flex-wrap:wrap;gap:10px;padding:16px 24px 0}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid var(--line-strong);border-radius:3px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;background:#fff;color:#52606d}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}button:disabled{opacity:.55;cursor:not-allowed}.template-field-options{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto;padding:14px;border:1px solid #d8e1e9;background:#f8fafc}.template-field-options :deep(.el-checkbox),.template-field-options :deep(.el-radio){height:auto;min-height:34px;margin-right:0}.report-test-detail :deep(.el-dialog){max-width:calc(100vw - 28px)}@media(max-width:680px){.panel-heading,.field-table-wrap,.detail-actions{padding-left:12px;padding-right:12px}}
</style>