// TeamKpiView:运营中心视图。后端已按当前登录用户的数据范围裁剪,页面只展示可见运营负责范围内的充值数据。 // 金额字段为 USD 分,缺失值显示 "--",不在前端把缺失口径当作 0。 import { useMemo, useState } from "react"; import { useSocialBi } from "../SocialBiApp.jsx"; import { appColor } from "../metrics.js"; import { formatCount, formatMoneyMinor, isBlank } from "../format.js"; import "./team-kpi-view.css"; const DIMENSIONS = [ { key: "person", label: "按人员×区域" }, { key: "operatorApp", label: "按人员×App" }, { key: "team", label: "按部门" } ]; function addNullable(sum, value) { if (isBlank(value)) { return sum; } return (sum ?? 0) + Number(value); } function numberOr(value, fallback) { return isBlank(value) ? fallback : Number(value); } function compareByMonthRecharge(left, right) { return numberOr(right.monthMinor, -1) - numberOr(left.monthMinor, -1); } function operatorLabelOf(item) { return item.operator_name || item.operator_account || `#${item.operator_user_id}`; } function regionLabelOf(item) { return item.region_name || item.region_code || "全部区域"; } function operatorCountOf(items) { return new Set(items.map((item) => item.operator_user_id).filter((id) => !isBlank(id))).size; } export function TeamKpiView() { const { appCodes, isLoading, kpi } = useSocialBi(); const [dimension, setDimension] = useState("person"); const items = useMemo(() => kpi?.items || [], [kpi]); const personRows = useMemo(() => { const rows = items.map((item, index) => ({ item, key: `${item.operator_user_id}-${item.app_code}-${item.region_id}-${index}`, monthMinor: item.month_recharge_usd_minor })); rows.sort(compareByMonthRecharge); return rows; }, [items]); const operatorAppRows = useMemo(() => { const rows = (kpi?.operator_app_rows || []).map((item, index) => ({ item, key: `${item.operator_user_id}-${item.app_code}-${index}`, monthMinor: item.month_recharge_usd_minor })); rows.sort(compareByMonthRecharge); return rows; }, [kpi]); const teamRows = useMemo(() => { const groups = new Map(); items.forEach((item) => { const teamName = item.team || "未分组"; let group = groups.get(teamName); if (!group) { group = { members: new Set(), monthMinor: null, rangeMinor: null, team: teamName }; groups.set(teamName, group); } group.members.add(item.operator_user_id); group.rangeMinor = addNullable(group.rangeMinor, item.range_recharge_usd_minor); group.monthMinor = addNullable(group.monthMinor, item.month_recharge_usd_minor); }); const rows = [...groups.values()].map((group) => ({ memberCount: group.members.size, monthMinor: group.monthMinor, rangeMinor: group.rangeMinor, team: group.team })); rows.sort(compareByMonthRecharge); return rows; }, [items]); if (!items.length) { return isLoading ? ( ) : (
暂无充值数据 先在用户管理为运营人员分配 App/区域数据范围
); } const summary = kpi.summary || {}; const operatorCount = isBlank(summary.operator_count) ? operatorCountOf(items) : summary.operator_count; return (
充值排行 {kpi.period_month} · {formatCount(operatorCount)} 名运营
{DIMENSIONS.map((option) => ( ))}
{dimension === "person" ? ( ) : dimension === "operatorApp" ? ( ) : ( )}
); } function KpiSkeleton() { return (
{[0, 1, 2, 3, 4, 5].map((index) => ( ))}
); } function RankBadge({ rank }) { const medal = rank === 1 ? " is-gold" : rank === 2 ? " is-silver" : rank === 3 ? " is-bronze" : ""; return {rank}; } function OperatorAppTable({ appCodes, rows }) { return ( {rows.map((row, index) => { const item = row.item; return ( ); })}
名次 运营人员 App 负责区域 区间充值 当月充值
{item.whole_app_scope ? "全部区域" : item.region_names || `${formatCount(item.region_count)} 个区域`} {formatMoneyMinor(item.range_recharge_usd_minor)} {formatMoneyMinor(item.month_recharge_usd_minor)}
); } function PersonTable({ appCodes, rows }) { return ( {rows.map((row, index) => { const item = row.item; return ( ); })}
名次 运营人员 App 区域 区间充值 当月充值
{regionLabelOf(item)} {formatMoneyMinor(item.range_recharge_usd_minor)} {formatMoneyMinor(item.month_recharge_usd_minor)}
); } function TeamTable({ rows }) { return ( {rows.map((row, index) => ( ))}
名次 部门 人数 区间充值 当月充值
{row.team} {formatCount(row.memberCount)} {formatMoneyMinor(row.rangeMinor)} {formatMoneyMinor(row.monthMinor)}
); } function OperatorCell({ item }) { return (
{operatorLabelOf(item)} {item.data_error ? ( 数据异常 ) : null} {[item.operator_account, item.team || "未分组"].filter(Boolean).join(" · ")}
); } function AppCell({ appCodes, item }) { return ( {item.app_name || item.app_code} ); }