yeg
2026-09-09 d67f933e50c7b5bf8c90e7d7db0000721ce796cc
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
import type { FormationItem, FormationSettings } from '@/api/formationReports'
import type { PersistedTestRecord } from '@/api/tests'
import { formationSeriesKey, formationSeriesColor } from './formationCycleChart'
import { measurementChartOption, type ReportMetricKey } from './reportMeasurementChart'
 
export const formationStepGroups = [{ key: 'charge', title: '充电' }, { key: 'discharge', title: '放电' }] as const
export type FormationStepGroup = typeof formationStepGroups[number]['key']
export function formationCycleKey(item: FormationItem, cycle: number) { return formationSeriesKey(item) + '|' + cycle }
 
/** 当前循环图例事件只更新其中出现的来源,保留其他循环独有电池的显示设置。 */
export function formationHiddenSeries(hidden: string[], selected: Record<string, boolean>) {
  const next = new Set(hidden)
  for (const [key, shown] of Object.entries(selected)) shown ? next.delete(key) : next.add(key)
  return [...next]
}
 
/** 化成只使用可靠识别的实际工步,逻辑合并与人工范围均不参与。 */
export function formationChargeSteps(item: FormationItem, cycle: number, _manual?: Record<string, number[]>) {
  return (item.chargeSteps?.[cycle] ?? []).filter(step => step.direction === 'CHARGE').sort((a,b)=>a.stepNo-b.stepNo)
}
export function formationSelectedSteps(item: FormationItem, cycle: number, group: FormationStepGroup, _settings?: Pick<FormationSettings, 'curveStepSelections' | 'manualChargeSteps'>) {
  return (item.chargeSteps?.[cycle] ?? []).filter(step => step.direction === (group === 'charge' ? 'CHARGE' : 'DISCHARGE')).map(step=>step.stepNo)
}
function emptyPoint(row: PersistedTestRecord): PersistedTestRecord {
  return {...row, voltage:null,current:null,temperature:null,positivePotential:null,negativePotential:null}
}
export function formationStepSeries(items: FormationItem[], cycle: number, group: FormationStepGroup, settings: FormationSettings) {
  return items.filter(item=>item.cycleNos.includes(cycle)).map(item => {
    const index = items.indexOf(item), key = formationSeriesKey(item)
    const selected = new Set(formationSelectedSteps(item,cycle,group))
    const allSteps = [...(item.chargeSteps?.[cycle] ?? [])].sort((a,b)=>a.stepNo-b.stepNo)
    const all = [...(item.curves?.[cycle]?.records ?? [])].sort((a,b)=>Number(a.deviceTime ?? a.collectorDeviceTime)-Number(b.deviceTime ?? b.collectorDeviceTime))
    const chosen = all.filter(row=>selected.has(row.stepNo))
    const times = chosen.flatMap(row=>[row.deviceTime,row.collectorDeviceTime]).filter((time): time is number=>time!=null).map(Number)
    const zeroTime = times.length ? Math.min(...times) : null
    const endTime = times.length ? Math.max(...times) : null
    const records: PersistedTestRecord[] = []
    let previousStep: number | undefined
    for (const row of all) {
      const times = [row.deviceTime,row.collectorDeviceTime].filter(time=>time!=null).map(Number)
      if (zeroTime==null || endTime==null || !times.some(time=>time>=zeroTime && time<=endTime)) continue
      if (selected.has(row.stepNo)) {
        // 即使静置没有实时记录,也用空值绘图分隔符断开两段;不修改冻结记录。
        if (previousStep!=null && row.stepNo!==previousStep && allSteps.some(step=>step.stepNo>previousStep! && step.stepNo<row.stepNo && !selected.has(step.stepNo))) records.push(emptyPoint(row))
        records.push(row); previousStep=row.stepNo
      } else records.push(emptyPoint(row))
    }
    return {key,name:item.source.name+'-'+item.source.batteryNo,zeroTime,records,hidden:settings.hiddenSeriesKeys?.includes(key)??false,color:formationSeriesColor(item,index,settings.seriesColors??{})}
  })
}
/** 一电池一方向一条曲线,以该方向最早有效设备时间归零,保留实际间隔。 */
export function formationStepChartOption(metric: { key: ReportMetricKey; unit: string }, items: FormationItem[], cycle: number, group: FormationStepGroup, settings: FormationSettings) {
  const series = formationStepSeries(items,cycle,group,settings)
  const option = measurementChartOption(metric,series,series.map(item=>item.color))
  // ECharts 按名称控制图例;内部使用来源键,显示文字仍保持简短,避免同名来源串联显隐。
  const labels = Object.fromEntries(series.map(item=>[item.key,item.name]))
  return {...option,tooltip:{trigger:'axis',renderMode:'richText',formatter:(entries:Array<{seriesName:string;value:[number,number|null]}>)=>entries.filter(entry=>entry.value[1]!=null).map(entry=>`${labels[entry.seriesName]??entry.seriesName}: ${entry.value[1]} ${metric.unit}`).join('\n')},legend:{...option.legend,formatter:(key:string)=>labels[key]??key,selected:Object.fromEntries(series.map(item=>[item.key,!item.hidden]))},series:option.series.map((line,index)=>({...line,name:series[index]!.key}))}
}