// 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 (
| 名次 |
运营人员 |
App |
负责区域 |
区间充值
|
当月充值
|
{rows.map((row, index) => {
const item = row.item;
return (
|
|
|
|
{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 (
| 名次 |
运营人员 |
App |
区域 |
区间充值
|
当月充值
|
{rows.map((row, index) => {
const item = row.item;
return (
|
|
|
|
{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}
);
}