Files
ln-bi/src/modules/assets/faults/analysis-model.ts
T
shishengliang 4214dca4af
ci/woodpecker/push/woodpecker Pipeline was successful
feat(faults): add severity dimension and detail column
2026-10-24 13:37:09 +08:00

52 lines
3.8 KiB
TypeScript

import type { AssetAnalysisDimension } from '../../../shared/assets/analytics';
import type { FaultAnalysisRecord } from '../../../shared/assets/faults';
import { DIMENSION_LABELS, getAvailableDimensions } from '../analytics-model';
export type FaultAnalysisDimension = AssetAnalysisDimension | 'handler' | 'plate' | 'faultStatus' | 'level';
export type FaultAnalysisFilters = Partial<Record<FaultAnalysisDimension, string[]>>;
export const FAULT_ANALYSIS_DIMENSIONS: FaultAnalysisDimension[] = getAvailableDimensions('assets').flatMap(d => d === 'type' ? [d, 'handler', 'plate', 'faultStatus', 'level'] : [d]);
export const FAULT_ANALYSIS_LABELS = { ...DIMENSION_LABELS, handler: '运维处置人', plate: '车牌', faultStatus: '故障工单状态', level: '故障程度' };
export const FAULT_CLOSURE_LABELS: Record<string, string> = { closed: '已闭环', unclosed: '未闭环' };
export interface FaultAnalysisGroup {
key: string; values: Partial<Record<FaultAnalysisDimension, string>>;
count: number; share: number; recordIds: string[];
}
export function faultDimensionValues(row: FaultAnalysisRecord, dimension: FaultAnalysisDimension): string[] {
if (dimension === 'plate') return [row.plate?.trim() || '未关联车辆'];
if (dimension === 'faultStatus') return [row.status === '已归档' ? 'closed' : 'unclosed'];
if (dimension === 'level') return [row.level?.trim() || '未标注'];
return dimension === 'handler' ? [...new Set(row.handlers.length ? row.handlers.map(h => h.value) : ['unassigned'])] : [row.cell[dimension]];
}
export function filterFaultRecords(rows: FaultAnalysisRecord[], filters: FaultAnalysisFilters = {}): FaultAnalysisRecord[] {
return [...new Map(rows.filter(row => FAULT_ANALYSIS_DIMENSIONS.every(d => !filters[d]?.length || faultDimensionValues(row, d).some(value => filters[d]!.includes(value)))).map(row => [row.id, row])).values()];
}
/** Expand only the grouped handler dimension; totals and each group remain distinct fault IDs. */
export function analyzeFaultRecords(rows: FaultAnalysisRecord[], dimensions: FaultAnalysisDimension[], filters: FaultAnalysisFilters = {}) {
const selected = filterFaultRecords(rows, filters);
const groups = new Map<string, { values: FaultAnalysisGroup['values']; ids: Set<string> }>();
const validDimensions = [...new Set(dimensions.filter(d => FAULT_ANALYSIS_DIMENSIONS.includes(d)))];
for (const row of selected) {
let combinations: [FaultAnalysisDimension, string][][] = [[]];
for (const d of validDimensions) {
const values = faultDimensionValues(row, d).filter(value => !filters[d]?.length || filters[d]!.includes(value));
combinations = combinations.flatMap(parts => values.map(value => [...parts, [d, value] as [FaultAnalysisDimension, string]]));
}
for (const entries of combinations) {
const key = JSON.stringify(entries);
const group = groups.get(key) || { values: Object.fromEntries(entries), ids: new Set<string>() };
group.ids.add(row.id); groups.set(key, group);
}
}
return { total: selected.length, rows: [...groups].map(([key, group]): FaultAnalysisGroup => ({ key, values: group.values,
count: group.ids.size, share: selected.length ? group.ids.size / selected.length * 100 : 0, recordIds: [...group.ids] })) };
}
/** Multi-handler child rows overlap: parent subtotals must use the union, never sum children. */
export function faultParentTotals(rows: FaultAnalysisGroup[], dimensions: FaultAnalysisDimension[]): number[][] {
const totals = new Map<string, Set<string>>();
const keys = rows.map(row => dimensions.slice(0, -1).map((_, depth) => JSON.stringify(dimensions.slice(0, depth + 1).map(d => row.values[d]))));
rows.forEach((row, i) => keys[i].forEach(key => {
const ids = totals.get(key) || new Set<string>(); row.recordIds.forEach(id => ids.add(id)); totals.set(key, ids);
}));
return keys.map(row => row.map(key => totals.get(key)!.size));
}