import type { ScreenData, TimeRange } from './screen';
|
import { demoScreen } from './screen-demo';
|
import {
|
FieldTracker,
|
KPI_GROUPS,
|
groupHasLive,
|
resolveDataMode,
|
type ScreenDataMode
|
} from './screen-field-meta';
|
import { structureScreen } from './screen-structure';
|
|
export type { ScreenDataMode };
|
|
export interface ResolvedScreenData {
|
data: ScreenData;
|
mode: ScreenDataMode;
|
demoFields: string[];
|
liveFields: string[];
|
}
|
|
function sum(nums: number[]) {
|
return nums.reduce((a, b) => a + (Number(b) || 0), 0);
|
}
|
|
function hasPositive(nums: number[]) {
|
return nums.some(v => Number(v) > 0);
|
}
|
|
function hasRankData(list: any[]) {
|
return list.length > 0 && list.some(e => Number(e?.people) > 0 || Number(e?.weight) > 0);
|
}
|
|
function pickKpis(
|
live: Record<string, number> | undefined,
|
demo: Record<string, number>,
|
ctx: FieldTracker
|
) {
|
const out = { ...demo };
|
if (!live) {
|
Object.values(KPI_GROUPS)
|
.flat()
|
.forEach(key => ctx.markDemo(`kpis.${key}`));
|
return out;
|
}
|
|
for (const [group, keys] of Object.entries(KPI_GROUPS)) {
|
if (groupHasLive(keys, live)) {
|
keys.forEach(key => {
|
const v = Number(live[key]);
|
out[key] = Number.isFinite(v) ? v : demo[key];
|
ctx.markLive(`kpis.${key}`);
|
});
|
} else {
|
keys.forEach(key => ctx.markDemo(`kpis.${key}`));
|
}
|
}
|
return out;
|
}
|
|
function pickArray<T>(
|
field: string,
|
live: T[] | undefined,
|
demo: T[],
|
ctx: FieldTracker,
|
valid: (v: T[]) => boolean
|
) {
|
if (live && valid(live)) {
|
ctx.markLive(field);
|
return live;
|
}
|
ctx.markDemo(field);
|
return demo;
|
}
|
|
function pickTrend(
|
field: string,
|
live: { weight: number[]; value: number[]; carbon: number[] } | undefined,
|
demo: ScreenData['trend'],
|
axis: string[],
|
ctx: FieldTracker
|
) {
|
const valid =
|
live &&
|
live.weight?.length &&
|
(sum(live.weight) > 0 || sum(live.value) > 0 || sum(live.carbon) > 0);
|
if (valid) {
|
ctx.markLive(field);
|
return {
|
weight: alignSeries(live!.weight, axis.length, live!.weight[live!.weight.length - 1] || 0),
|
value: alignSeries(live!.value, axis.length, live!.value[live!.value.length - 1] || 0),
|
carbon: alignSeries(live!.carbon, axis.length, live!.carbon[live!.carbon.length - 1] || 0)
|
};
|
}
|
ctx.markDemo(field);
|
return alignTrend(demo, axis.length);
|
}
|
|
function alignSeries(source: number[], len: number, fill = 0) {
|
if (!source?.length) return Array.from({ length: len }, () => fill);
|
if (source.length === len) return [...source];
|
if (source.length > len) return source.slice(source.length - len);
|
return [...Array.from({ length: len - source.length }, () => fill), ...source];
|
}
|
|
function alignTrend(trend: ScreenData['trend'], len: number) {
|
return {
|
weight: alignSeries(trend.weight, len),
|
value: alignSeries(trend.value, len),
|
carbon: alignSeries(trend.carbon, len)
|
};
|
}
|
|
function pickWasteMix(
|
live: ScreenData['wasteMix'] | undefined,
|
demo: ScreenData['wasteMix'],
|
ctx: FieldTracker
|
) {
|
if (
|
live?.length &&
|
live.some(e => Number(e.weight) > 0 || Number(e.count) > 0 || Number(e.value) > 0)
|
) {
|
ctx.markLive('wasteMix');
|
return live.map((item, i) => ({
|
...demo[i],
|
...item,
|
color: item.color || demo[i]?.color
|
}));
|
}
|
ctx.markDemo('wasteMix');
|
return demo;
|
}
|
|
function pickRecycleItems(
|
live: ScreenData['recycleItems'] | undefined,
|
demo: ScreenData['recycleItems'],
|
ctx: FieldTracker
|
) {
|
if (live?.length && live.some(e => Number(e.weight) > 0 || Number(e.value) > 0 || Number(e.carbon) > 0)) {
|
ctx.markLive('recycleItems');
|
return live.map((item, i) => ({
|
...demo[i],
|
...item,
|
color: item.color || demo[i]?.color
|
}));
|
}
|
ctx.markDemo('recycleItems');
|
return demo;
|
}
|
|
function pickPointFlow(
|
live: ScreenData['pointFlow'] | undefined,
|
demo: ScreenData['pointFlow'],
|
ctx: FieldTracker
|
) {
|
const out = { ...demo };
|
if (!live) {
|
ctx.markDemo('pointFlow');
|
(['issued', 'transfer', 'donate', 'mall', 'balance'] as const).forEach(key =>
|
ctx.markDemo(`pointFlow.${key}`)
|
);
|
return out;
|
}
|
|
let hasLive = false;
|
(['issued', 'transfer', 'donate', 'mall', 'balance'] as const).forEach(key => {
|
if (Number(live[key]) > 0) {
|
out[key] = live[key];
|
ctx.markLive(`pointFlow.${key}`);
|
hasLive = true;
|
} else {
|
ctx.markDemo(`pointFlow.${key}`);
|
}
|
});
|
if (hasLive) ctx.markLive('pointFlow');
|
else ctx.markDemo('pointFlow');
|
return out;
|
}
|
|
function pickOrdersByType(
|
live: ScreenData['ordersByType'] | undefined,
|
demo: ScreenData['ordersByType'],
|
ctx: FieldTracker
|
) {
|
const keys = ['现场回收', '预约上门', '大屏投递', '活动回收'] as const;
|
const liveValid = live?.axis?.length && keys.some(k => hasPositive(live[k] || []));
|
if (liveValid) {
|
ctx.markLive('ordersByType');
|
return live!;
|
}
|
ctx.markDemo('ordersByType');
|
return demo;
|
}
|
|
function pickAppointmentFlow(
|
live: ScreenData['appointmentFlow'] | undefined,
|
demo: ScreenData['appointmentFlow'],
|
ctx: FieldTracker
|
) {
|
if (live?.length && live.some(e => Number(e.value) > 0)) {
|
ctx.markLive('appointmentFlow');
|
return live;
|
}
|
ctx.markDemo('appointmentFlow');
|
return demo;
|
}
|
|
function pickSorterStatus(
|
live: ScreenData['sorterStatus'] | undefined,
|
demo: ScreenData['sorterStatus'],
|
ctx: FieldTracker
|
) {
|
if (live?.length && sum(live.map(e => Number(e.value))) > 0) {
|
ctx.markLive('sorterStatus');
|
return live;
|
}
|
ctx.markDemo('sorterStatus');
|
return demo;
|
}
|
|
function pickAxis(
|
live: ScreenData | null | undefined,
|
demo: ScreenData,
|
structure: ScreenData,
|
ctx: FieldTracker
|
) {
|
const useLiveAxis =
|
live?.axis?.length &&
|
(sum(live.trend?.weight || []) > 0 ||
|
sum(live.participateTrend || []) > 0 ||
|
sum(live.deliverTrend || []) > 0 ||
|
live.axis.length > 1);
|
if (useLiveAxis) {
|
ctx.markLive('axis');
|
return live!.axis;
|
}
|
ctx.markDemo('axis');
|
return demo.axis.length ? demo.axis : structure.axis;
|
}
|
|
/** 结构 + 演示 + 真实数据按字段/分组合并 */
|
export function resolveScreenData(live: ScreenData | null | undefined, range: TimeRange): ResolvedScreenData {
|
const structure = structureScreen(range);
|
const demo = demoScreen(range);
|
const ctx = new FieldTracker();
|
|
const axis = pickAxis(live, demo, structure, ctx);
|
const kpis = pickKpis(live?.kpis, demo.kpis, ctx);
|
const wasteMix = pickWasteMix(live?.wasteMix, demo.wasteMix, ctx);
|
const recycleItems = pickRecycleItems(live?.recycleItems, demo.recycleItems, ctx);
|
const trend = pickTrend('trend', live?.trend, demo.trend, axis, ctx);
|
|
let yearCarbon = demo.yearCarbon;
|
if (live?.yearCarbon?.length && sum(live.yearCarbon) > 0) {
|
yearCarbon = live.yearCarbon;
|
ctx.markLive('yearCarbon');
|
} else {
|
ctx.markDemo('yearCarbon');
|
}
|
|
const deptRank = pickArray('deptRank', live?.deptRank, demo.deptRank, ctx, hasRankData);
|
const classRank = pickArray('classRank', live?.classRank, demo.classRank, ctx, hasRankData);
|
const majorRank = pickArray('majorRank', live?.majorRank, demo.majorRank, ctx, hasRankData);
|
const pointFlow = pickPointFlow(live?.pointFlow, demo.pointFlow, ctx);
|
|
let participateTrend = alignSeries(demo.participateTrend, axis.length);
|
if (live?.participateTrend?.length && sum(live.participateTrend) > 0) {
|
participateTrend = alignSeries(live.participateTrend, axis.length);
|
ctx.markLive('participateTrend');
|
} else {
|
ctx.markDemo('participateTrend');
|
}
|
|
let deliverTrend = alignSeries(demo.deliverTrend, axis.length);
|
if (live?.deliverTrend?.length && sum(live.deliverTrend) > 0) {
|
deliverTrend = alignSeries(live.deliverTrend, axis.length);
|
ctx.markLive('deliverTrend');
|
} else {
|
ctx.markDemo('deliverTrend');
|
}
|
|
const ordersByType = pickOrdersByType(live?.ordersByType, demo.ordersByType, ctx);
|
const appointmentFlow = pickAppointmentFlow(live?.appointmentFlow, demo.appointmentFlow, ctx);
|
const sorterStatus = pickSorterStatus(live?.sorterStatus, demo.sorterStatus, ctx);
|
const vehicles = pickArray('vehicles', live?.vehicles, demo.vehicles, ctx, v => v.length > 0);
|
const sites = pickArray('sites', live?.sites, demo.sites, ctx, v => v.length > 0);
|
|
const data: ScreenData = {
|
range,
|
axis,
|
kpis,
|
wasteMix,
|
recycleItems,
|
trend,
|
yearCarbon,
|
deptRank,
|
classRank,
|
majorRank,
|
pointFlow,
|
participateTrend,
|
deliverTrend,
|
ordersByType,
|
appointmentFlow,
|
sorterStatus,
|
vehicles,
|
sites,
|
unavailable: live?.unavailable || []
|
};
|
|
return {
|
data,
|
mode: resolveDataMode(live, ctx),
|
demoFields: ctx.listDemo(),
|
liveFields: ctx.listLive()
|
};
|
}
|
|
/** 接口失败或无 live 数据时的初始展示 */
|
export function initialScreenData(range: TimeRange = 'month') {
|
return resolveScreenData(null, range);
|
}
|
|
export { structureScreen, buildScreenAxis } from './screen-structure';
|
export { demoScreen } from './screen-demo';
|