wangrong
2026-09-24 bf9400d4198094194439214b5e90036b2f040052
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
export type ScreenDataMode = 'live' | 'demo' | 'mixed';
 
/** KPI 分组:组内任一指标有真实值,则整组采用 live(含合法 0 值) */
export const KPI_GROUPS: Record<string, string[]> = {
    'kpis.weight': [
        'todayWeightKg',
        'periodWeightTon',
        'totalWeightTon',
        'recyclableTon',
        'bulkTon',
        'elecTon',
        'fabricTon',
        'resourceRate',
        'periodValue'
    ],
    'kpis.carbon': ['periodCarbonTon', 'yearCarbonTon', 'monthCarbonTon', 'totalCarbonTon', 'greenActions'],
    'kpis.participation': [
        'totalParticipants',
        'totalPoints',
        'newParticipants',
        'participateRate',
        'pointsIssued',
        'pointsConsumed',
        'pointsBalance'
    ],
    'kpis.operation': [
        'sorterOnline',
        'sorterTotal',
        'vehicleOnline',
        'vehicleTotal',
        'siteOnline',
        'siteTotal',
        'todayOrders',
        'todayAppointments',
        'pendingAppointments',
        'alerts',
        'healthAlerts'
    ]
};
 
export const SECTION_LABELS: Record<string, string> = {
    axis: '时间轴',
    'kpis.weight': '回收与资源化指标',
    'kpis.carbon': '碳减排指标',
    'kpis.participation': '参与与碳积分指标',
    'kpis.operation': '运营监控指标',
    wasteMix: '垃圾分类结构',
    recycleItems: '可回收物品类',
    trend: '回收趋势',
    yearCarbon: '年度月度碳减排',
    deptRank: '院系排行',
    classRank: '班级排行',
    majorRank: '专业排行',
    pointFlow: '碳积分流转',
    participateTrend: '参与人数趋势',
    deliverTrend: '投递次数趋势',
    ordersByType: '订单类型趋势',
    appointmentFlow: '预约订单漏斗',
    sorterStatus: '分拣员状态',
    vehicles: '车辆运行',
    sites: '分拣点吞吐'
};
 
export class FieldTracker {
    readonly demo = new Set<string>();
    readonly live = new Set<string>();
 
    markDemo(field: string) {
        this.demo.add(field);
        this.live.delete(field);
    }
 
    markLive(field: string) {
        this.live.add(field);
        this.demo.delete(field);
    }
 
    listDemo() {
        return [...this.demo];
    }
 
    listLive() {
        return [...this.live];
    }
}
 
export function groupHasLive(keys: string[], live: Record<string, number>) {
    return keys.some(k => Number(live[k]) > 0);
}
 
export function resolveDataMode(
    live: unknown,
    tracker: FieldTracker
): ScreenDataMode {
    if (!live) return 'demo';
    if (tracker.demo.size === 0) return 'live';
    if (tracker.live.size === 0) return 'demo';
    return 'mixed';
}
 
function kpiGroupOf(field: string) {
    if (!field.startsWith('kpis.')) return null;
    const key = field.slice(5);
    for (const [group, keys] of Object.entries(KPI_GROUPS)) {
        if (keys.includes(key)) return group;
    }
    return null;
}
 
function fieldBelongsToSection(field: string, section: string) {
    if (field === section || field.startsWith(`${section}.`)) return true;
    if (section.startsWith('kpis.')) {
        return kpiGroupOf(field) === section;
    }
    return false;
}
 
/** 将字段级 demo 标记折叠为可读区块列表 */
export function groupDemoFields(fields: string[]) {
    const sections = new Set<string>();
    for (const field of fields) {
        const group = kpiGroupOf(field);
        if (group) {
            sections.add(group);
            continue;
        }
        if (field.startsWith('pointFlow.')) {
            sections.add('pointFlow');
            continue;
        }
        sections.add(field.split('.')[0]);
    }
    return [...sections]
        .sort()
        .map(section => ({
            section,
            label: SECTION_LABELS[section] || section
        }));
}
 
export function isDemoSection(
    section: string,
    demoFields: string[],
    dataMode: ScreenDataMode
) {
    if (dataMode === 'live') return false;
    if (dataMode === 'demo') return true;
    return demoFields.some(field => fieldBelongsToSection(field, section));
}
 
export function isDemoField(field: string, demoFields: string[], dataMode: ScreenDataMode) {
    if (dataMode === 'live') return false;
    if (dataMode === 'demo') return true;
    if (demoFields.includes(field)) return true;
    const group = kpiGroupOf(field);
    if (!group) return false;
    return demoFields.some(item => fieldBelongsToSection(item, group));
}