yeg
2026-09-06 62ef8d70cbf9e18fa0d3458afa8622ae9ac91946
src/views/ReportCompareForm.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { toTestDisplayValue } from '@/utils/testDataUnits'
import { computed, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { use } from 'echarts/core'
@@ -10,6 +10,7 @@
import VChart from 'vue-echarts'
import { getCurrentUser } from '@/utils/auth'
import { useCompareReports, type CompareMetricKey, type CompareReportPayload, type CompareTestSelection } from '@/stores/compareReports'
import { getPersistedTestRecords, getTestDetail, getTestList, type PersistedTestRecord } from '@/api/tests'
import { useBatteryGroupFields } from '@/stores/batteryGroupFields'
import { compareMetricDefinitions as metricDefinitions, openCompareReportPrint, type CompareMetricDefinition } from '@/utils/compareReportPrint'
@@ -36,9 +37,11 @@
const reportName = ref('')
const experimentName = ref('XXX实验')
const batteryGroup = ref('电池组1')
const selectedTests = ref<CompareTestSelection[]>([
  { id: 1, name: '第一次测试', type: '化成', batteryGroup: '电池组1', batteryNo: 'SP26-3', process: '恒流充电', cycle: 3 },
])
const selectedTests = ref<CompareTestSelection[]>([])
const candidateTests = ref<CompareTestSelection[]>([])
const sourceRecords = reactive<Record<number, PersistedTestRecord[]>>({})
const candidatesLoading = ref(false)
const candidatesError = ref('')
const testDialogVisible = ref(false)
const templateDialogVisible = ref(false)
const selectedTemplateKey = ref('cycle')
@@ -67,13 +70,6 @@
})())
const selectedBatteryTemplateName = ref('标准容量样品')
const selectedBatteryTemplateFieldIds = ref<number[]>([])
const candidateTests: CompareTestSelection[] = [
  { id: 2, name: '第二次测试', type: '化成', batteryGroup: '电池组1', batteryNo: 'SP26-4', process: '恒流充电', cycle: 3 },
  { id: 3, name: '高温循环测试', type: '循环', batteryGroup: '电池组2', batteryNo: 'SP26-5', process: '恒流放电', cycle: 50 },
  { id: 4, name: '低温充放电测试', type: '性能', batteryGroup: '电池组2', batteryNo: 'SP26-6', process: '恒流充放电', cycle: 10 },
  { id: 5, name: '倍率放电测试', type: '倍率', batteryGroup: '电池组3', batteryNo: 'SP26-7', process: '脉冲放电', cycle: 8 },
]
const specialTemplateStorageKey = 'battery_detection_compare_special_templates_v2'
const specialTemplateSelections = reactive<Record<string, string[]>>((() => {
@@ -116,7 +112,7 @@
const batteryTemplateFieldOptions = computed(() => [...managedGroupFields.value].sort((a, b) => b.createdAt.localeCompare(a.createdAt)))
const enabledBatteryTemplateFields = computed(() => batteryTemplateFieldOptions.value.filter(field => field.status === '启用'))
const detailFields = computed(() => [...batteryFields.value, ...specialFields.value])
const selectedCandidate = computed(() => candidateTests.find(item => item.id === selectedCandidateId.value) || null)
const selectedCandidate = computed(() => candidateTests.value.find(item => item.id === selectedCandidateId.value) || null)
const detailTests = computed(() => {
  const seen = new Set<string>()
  return selectedTests.value.filter(item => {
@@ -126,10 +122,39 @@
    return true
  })
})
const chartSeries = computed(() => detailTests.value.slice(0, 3))
const batteryChoices = computed(() => [...new Set([selectedCandidate.value?.batteryNo || '', 'SP26-1', 'SP26-2', 'SP26-3', 'SP26-4', 'SP26-5'])].filter(Boolean))
const cycleChoices = [1, 2, 3, 5, 10, 20, 50]
const processChoices = ['1-恒流充电', '2-恒流放电', '3-静置', '4-脉冲放电']
const chartSeries = computed(() => detailTests.value)
const batteryChoices = computed(() => [...new Set((sourceRecords[selectedCandidateId.value || 0] || []).map(item => item.batteryNo).filter(Boolean))])
const cycleChoices = computed(() => [...new Set((sourceRecords[selectedCandidateId.value || 0] || []).map(item => Number(item.cycleNo)).filter(Number.isFinite))].sort((a, b) => a - b))
const processChoices = computed(() => [...new Map((sourceRecords[selectedCandidateId.value || 0] || []).map(item => [`${item.stepNo}-${item.stepName}`, `${item.stepNo}-${item.stepName}`])).values()])
async function loadCandidates() {
  candidatesLoading.value = true
  candidatesError.value = ''
  try {
    const list = await getTestList({ pageNum: 1, pageSize: 200, status: 'COMPLETED' })
    const completed = list.rows.filter(item => item.status === '检测完成')
    const loaded = await Promise.all(completed.map(async item => {
      const [detail, rows] = await Promise.all([getTestDetail(item.id), getPersistedTestRecords(item.id)])
      sourceRecords[item.id] = rows
      const first = rows[0]
      return first ? { id: item.id, name: item.name, type: item.type, batteryGroup: detail.batteries[0]?.groupName || item.batteryGroup, batteryNo: first.batteryNo, process: `${first.stepNo}-${first.stepName}`, cycle: first.cycleNo } : null
    }))
    candidateTests.value = loaded.filter(Boolean) as CompareTestSelection[]
  } catch (error) {
    candidatesError.value = error instanceof Error ? error.message : '真实测试来源加载失败'
  } finally {
    candidatesLoading.value = false
  }
}
function recordsFor(item: CompareTestSelection) {
  return (sourceRecords[item.id] || []).filter(row => row.batteryNo === item.batteryNo && Number(row.cycleNo) === Number(item.cycle) && `${row.stepNo}-${row.stepName}` === item.process)
}
function metricValues(key: CompareMetricKey) {
  const field: Record<CompareMetricKey, keyof PersistedTestRecord> = { voltage: 'voltage', current: 'current', positiveVoltage: 'positivePotential', negativeVoltage: 'negativePotential', temperature: 'temperature' }
  return chartSeries.value.map(item => recordsFor(item).map(row => row[field[key]]).filter((value): value is number => value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value))).map(Number))
}
function detailKey(item: CompareTestSelection) {
  return `${item.id}|${item.batteryGroup || batteryGroup.value}|${item.batteryNo}`
@@ -181,6 +206,7 @@
detailFields.value.forEach(ensureDetailFieldValues)
loadEditingReport()
onMounted(() => { void loadCandidates() })
function openAddTest() {
  selectedCandidateId.value = null
@@ -361,7 +387,7 @@
function chartOption(metric: CompareMetricDefinition) {
    const names = chartSeries.value.map(item => `${item.name} · ${item.batteryNo}`)
    const colors = seriesColors.value
    const visibleSeries = metric.values.slice(0, Math.max(1, names.length)).map(series => series.map(value => toTestDisplayValue(metric.key, value)))
    const visibleSeries = metricValues(metric.key).map(series => series.map(value => toTestDisplayValue(metric.key, value)))
    const visibleValues = visibleSeries.flat()
    const maxDataLength = Math.max(1, ...visibleSeries.map(series => series.length))
    const timeLabels = Array.from({ length: maxDataLength }, (_, index) => {
@@ -369,13 +395,15 @@
      const minutes = String(index % 60).padStart(2, '0')
      return `${hours}:${minutes}`
    })
    const dataMax = Math.max(...visibleValues)
  const axisMax = metric.unit === '℃' ? Math.ceil(dataMax / 10) * 10 : Math.ceil(dataMax * 10) / 10
    const dataMin = Math.min(0, ...visibleValues)
    const dataMax = Math.max(0, ...visibleValues)
    const range = Math.max(dataMax - dataMin, 1)
  const axisMax = Math.max(metric.unit === '℃' ? Math.ceil(dataMax / 10) * 10 : Math.ceil(dataMax * 10) / 10, 1)
  const createYAxis = (position: 'left' | 'right') => ({
    type: 'value',
    name: metric.unit,
    position,
    min: 0,
    min: dataMin - range * 0.05,
    max: axisMax,
    nameTextStyle: { color: '#8795a3', fontSize: 10 },
    axisLine: { show: true, lineStyle: { color: '#cbd8e4' } },
@@ -391,7 +419,7 @@
    legend: { top: 4, right: 6, icon: 'roundRect', itemWidth: 14, itemHeight: 8, textStyle: { fontSize: 11, color: '#52606d' } },
      xAxis: { type: 'category', boundaryGap: false, name: '时间/h:mm', nameLocation: 'middle', nameGap: 28, data: timeLabels, axisLine: { lineStyle: { color: '#cbd8e4' } }, axisLabel: { color: '#8795a3', fontSize: 10, interval: 'auto' }, nameTextStyle: { color: '#26333f', fontSize: 11, fontWeight: 600 } },
    yAxis: [createYAxis('left'), createYAxis('right')],
    series: names.map((name, index) => ({ name, type: 'line', smooth: true, showSymbol: false, lineStyle: { width: 2 }, data: visibleSeries[index] || visibleSeries[0] })),
    series: names.map((name, index) => ({ name, type: 'line', smooth: false, showSymbol: false, lineStyle: { width: 2 }, data: visibleSeries[index] || [] })),
  }
}
@@ -522,10 +550,11 @@
    <el-dialog v-model="testDialogVisible" class="test-dialog" title="添加对比" width="540px" align-center destroy-on-close>
      <div class="compare-select-form">
        <label><span>选择测试</span><el-select v-model="selectedCandidateId" filterable clearable placeholder="输入测试名称或ID搜索" @change="syncDialogDefaults"><el-option v-for="item in candidateTests" :key="item.id" :label="`${item.name}(${item.id})`" :value="item.id" /></el-select></label>
        <label><span>选择测试</span><el-select v-model="selectedCandidateId" filterable clearable :loading="candidatesLoading" placeholder="输入测试名称或ID搜索" @change="syncDialogDefaults"><el-option v-for="item in candidateTests" :key="item.id" :label="`${item.name}(${item.id})`" :value="item.id" /></el-select></label>
        <label><span>选择电池</span><el-select v-model="selectedBatteryNo" placeholder="请选择" :disabled="!selectedCandidateId"><el-option v-for="item in batteryChoices" :key="item" :label="item" :value="item" /></el-select></label>
        <label><span>选择循环</span><el-select v-model="selectedCycle" placeholder="请选择" :disabled="!selectedCandidateId"><el-option v-for="item in cycleChoices" :key="item" :label="`${item}`" :value="item" /></el-select></label>
        <label><span>选择工步</span><div class="control-stack"><el-select v-model="selectedProcess" placeholder="请选择" :disabled="!selectedCandidateId"><el-option v-for="item in processChoices" :key="item" :label="item" :value="item" /></el-select><small class="select-hint">工步显示为:1-恒流充电、21-恒流充电</small></div></label>
        <label><span>选择工步</span><div class="control-stack"><el-select v-model="selectedProcess" placeholder="请选择" :disabled="!selectedCandidateId"><el-option v-for="item in processChoices" :key="item" :label="item" :value="item" /></el-select><small class="select-hint">仅显示所选检测完成测试中已保存的实际工步</small></div></label>
        <p v-if="candidatesError" class="select-error">{{ candidatesError }}</p><p v-else-if="!candidatesLoading && !candidateTests.length" class="select-empty">暂无检测完成且有已保存数据的测试</p>
      </div>
      <template #footer><div class="dialog-footer-left"><button class="primary-button" type="button" @click="addSelectedTest"><el-icon><Check /></el-icon>添加</button></div></template>
    </el-dialog>