yeg
6 天以前 3721379cf11a47d36dab3c37f20766ab007cc3cd
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
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 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 : ['额定容量', '电流', '内阻', '质量', '材料批次']),
    ...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)
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
  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
  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 (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))
  }
  // 去除模板已取消的格,避免保存后恢复;仍选字段的值不会被模板覆盖。
  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>
      <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="templateLocked" @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>