<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>
|