From 46654ac23f78e6abf58e683fe8b8b6aa37fc4cc5 Mon Sep 17 00:00:00 2001 From: zhx Date: Mon, 6 Jul 2026 11:47:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=BB=E6=8E=89kpi=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/admin-openapi.json | 64 -- databi/src/DatabiApp.test.jsx | 18 +- databi/src/api.js | 21 - databi/src/social/useSocialBiData.js | 2 +- databi/src/social/views/TeamKpiView.jsx | 705 +++------------------- databi/src/social/views/team-kpi-view.css | 358 +---------- src/app/permissions.ts | 2 - src/shared/api/generated/endpoints.ts | 16 - src/shared/api/generated/schema.d.ts | 68 --- 9 files changed, 80 insertions(+), 1174 deletions(-) diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index bd5b8ac..75ccf57 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -3287,70 +3287,6 @@ "x-permissions": ["overview:view"] } }, - "/admin/databi/social/kpi-targets": { - "get": { - "operationId": "listSocialBiKpiTargets", - "x-permission": "overview:view", - "parameters": [ - { "in": "query", "name": "period_month", "schema": { "type": "string" } }, - { "in": "query", "name": "user_id", "schema": { "type": "integer", "format": "int64" } } - ], - "responses": { - "200": { - "$ref": "#/components/responses/StatisticsObjectResponse" - } - }, - "x-permissions": ["overview:view"] - }, - "put": { - "operationId": "replaceSocialBiKpiTargets", - "x-permission": "databi-kpi:manage", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "userId": { "type": "integer", "format": "int64" }, - "appCode": { "type": "string" }, - "regionId": { "type": "integer", "format": "int64" }, - "periodMonth": { "type": "string" }, - "targetUsdMinor": { "type": "integer", "format": "int64" }, - "dailyTargetUsdMinor": { "type": "integer", "format": "int64" } - } - } - }, - "appItems": { - "type": "array", - "items": { - "type": "object", - "properties": { - "appCode": { "type": "string" }, - "periodMonth": { "type": "string" }, - "targetUsdMinor": { "type": "integer", "format": "int64" }, - "dailyTargetUsdMinor": { "type": "integer", "format": "int64" } - } - } - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/StatisticsObjectResponse" - } - }, - "x-permissions": ["databi-kpi:manage"] - } - }, "/admin/finance/scope": { "get": { "operationId": "getFinanceScope", diff --git a/databi/src/DatabiApp.test.jsx b/databi/src/DatabiApp.test.jsx index e31223a..bf8e318 100644 --- a/databi/src/DatabiApp.test.jsx +++ b/databi/src/DatabiApp.test.jsx @@ -16,10 +16,8 @@ vi.mock("./api.js", () => ({ fetchSocialBiFilterOptions: vi.fn(), fetchSocialBiFunnel: vi.fn(), fetchSocialBiKpi: vi.fn(), - fetchSocialBiKpiTargets: vi.fn(), fetchSocialBiMaster: vi.fn(), fetchSocialBiOverview: vi.fn(), - replaceSocialBiKpiTargets: vi.fn(), fetchSelfGameStatisticsOverview: vi.fn(), fetchStatisticsOverview: vi.fn(), getCurrentAppCode: vi.fn(() => "lalu"), @@ -54,7 +52,7 @@ beforeEach(() => { access: { all: true, scopes: [] }, apps: [], operators: [], - permissions: { kpi_manage: false, kpi_view_all: true }, + permissions: {}, regions: [] }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] }); @@ -267,7 +265,7 @@ test("routes databi social page to Social BI and loads real overview rows", asyn operators: [ { account: "omar", name: "Omar", scopes: [{ app_code: "aslan", region_id: 11 }], team: "中东运营部", team_id: 2, user_id: 9 } ], - permissions: { kpi_manage: true, kpi_view_all: true }, + permissions: {}, regions: [{ app_code: "aslan", countries: ["SA"], region_code: "MENA", region_id: 11, region_name: "中东大区" }] }); fetchSocialBiOverview.mockResolvedValue({ @@ -302,10 +300,7 @@ test("routes databi social page to Social BI and loads real overview rows", asyn { app_code: "aslan", app_name: "Aslan", - daily_target_usd_minor: 0, - month_attainment_rate: 0.9, month_recharge_usd_minor: 90_000, - month_target_usd_minor: 100_000, operator_account: "omar", operator_name: "Omar", operator_user_id: 9, @@ -318,7 +313,7 @@ test("routes databi social page to Social BI and loads real overview rows", asyn } ], period_month: "2026-06", - summary: { month_recharge_usd_minor: 90_000, month_target_usd_minor: 100_000, operator_count: 1, range_recharge_usd_minor: 5000 } + summary: { month_recharge_usd_minor: 90_000, operator_count: 1, range_recharge_usd_minor: 5000 } }); render(); @@ -356,14 +351,13 @@ test("routes databi social page to Social BI and loads real overview rows", asyn expect(screen.getAllByText("中东大区").length).toBeGreaterThan(0); expect(screen.getAllByText("$50").length).toBeGreaterThan(0); - // 侧栏切换:运营中心 —— 运营人员 Omar 上榜、达成率 90.0%、kpi_manage 时有配置目标入口。 + // 侧栏切换:运营中心 —— 运营人员 Omar 上榜,只展示负责范围内的充值数据。 await act(async () => { within(viewTabs).getByRole("tab", { name: "运营中心" }).click(); }); expect(within(viewTabs).getByRole("tab", { name: "运营中心" })).toHaveAttribute("aria-selected", "true"); expect(screen.getAllByText("Omar").length).toBeGreaterThan(0); - expect(screen.getAllByText("90.0%").length).toBeGreaterThan(0); - expect(screen.getByRole("button", { name: "配置目标" })).toBeTruthy(); + expect(screen.getAllByText("$900").length).toBeGreaterThan(0); }); test("renders social BI funnel view and limits funnel apps to supported apps", async () => { @@ -378,7 +372,7 @@ test("renders social BI funnel view and limits funnel apps to supported apps", a { app_code: "yumi", app_name: "Yumi", kind: "legacy" } ], operators: [], - permissions: { kpi_manage: false, kpi_view_all: true }, + permissions: {}, regions: [] }); fetchSocialBiFunnel.mockResolvedValue({ diff --git a/databi/src/api.js b/databi/src/api.js index ac77c4d..611535f 100644 --- a/databi/src/api.js +++ b/databi/src/api.js @@ -223,27 +223,6 @@ export async function fetchSocialBiKpi({ appCodes, endMs, operatorUserId, period return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiKpi), { query }); } -export async function fetchSocialBiKpiTargets({ periodMonth, userId } = {}) { - const query = {}; - if (periodMonth) { - query.period_month = periodMonth; - } - if (userId) { - query.user_id = String(userId); - } - return fetchDatabiData(apiEndpointPath(API_OPERATIONS.listSocialBiKpiTargets), { query }); -} - -// payload 支持 {items, appItems}:items=人级目标(regionId=0 表示该运营的整 App 目标), -// appItems=App 级总目标(不分运营);传数组时兼容旧调用视为 items。 -export async function replaceSocialBiKpiTargets(payload) { - const body = Array.isArray(payload) ? { items: payload } : payload; - return fetchDatabiData(apiEndpointPath(API_OPERATIONS.replaceSocialBiKpiTargets), { - body, - method: "PUT" - }); -} - async function fetchDatabiData(path, { appCode, body, method, query } = {}) { const headers = requestHeaders(appCode); const init = { diff --git a/databi/src/social/useSocialBiData.js b/databi/src/social/useSocialBiData.js index e0cbd5d..bd3cf19 100644 --- a/databi/src/social/useSocialBiData.js +++ b/databi/src/social/useSocialBiData.js @@ -112,7 +112,7 @@ export function useSocialBiData(filters) { }); } else { setKpi(null); - nextErrors.push(`充值目标数据加载失败: ${kpiResult.reason?.message || "未知错误"}`); + nextErrors.push(`运营充值数据加载失败: ${kpiResult.reason?.message || "未知错误"}`); } setErrors(nextErrors); setIsLoading(false); diff --git a/databi/src/social/views/TeamKpiView.jsx b/databi/src/social/views/TeamKpiView.jsx index 5bfc525..73c8c45 100644 --- a/databi/src/social/views/TeamKpiView.jsx +++ b/databi/src/social/views/TeamKpiView.jsx @@ -1,59 +1,18 @@ -// TeamKpiView:运营中心视图。kpi_view_all 用户看团队排行,普通运营看"我的充值"。 -// 数据契约见 SocialBiApp.jsx 的 useSocialBi():金额为 USD 分、比例为 0-1 小数、缺失为 null(显示 "--")。 +// TeamKpiView:运营中心视图。后端已按当前登录用户的数据范围裁剪,页面只展示可见运营负责范围内的充值数据。 +// 金额字段为 USD 分,缺失值显示 "--",不在前端把缺失口径当作 0。 import { useMemo, useState } from "react"; -import Button from "@mui/material/Button"; import { useSocialBi } from "../SocialBiApp.jsx"; -import { replaceSocialBiKpiTargets } from "../../api.js"; import { appColor } from "../metrics.js"; -import { formatCount, formatMoneyMinor, formatRatioPercent, isBlank } from "../format.js"; -import { rangeLabel } from "../state.js"; +import { formatCount, formatMoneyMinor, isBlank } from "../format.js"; import "./team-kpi-view.css"; -const DAY_MS = 86400000; - const DIMENSIONS = [ { key: "person", label: "按人员×区域" }, { key: "operatorApp", label: "按人员×App" }, { key: "team", label: "按部门" } ]; -const SORTS = [ - { key: "rate", label: "按达成率" }, - { key: "month", label: "按当月充值" } -]; - -// 剩余天数:当月结束时间与当前时间的差值向上取整,最小 0。 -function remainingDaysOf(monthEndMs) { - if (isBlank(monthEndMs)) { - return 0; - } - return Math.max(0, Math.ceil((Number(monthEndMs) - Date.now()) / DAY_MS)); -} - -// 缺口(USD 分):月目标 - 当月充值;无目标或当月充值缺失时返回 null(不可当 0 计算)。 -function gapMinorOf(targetMinor, monthMinor) { - if (isBlank(targetMinor) || Number(targetMinor) <= 0 || isBlank(monthMinor)) { - return null; - } - return Number(targetMinor) - Number(monthMinor); -} - -// 行内"需日均":缺口 / 剩余天数;已达成显示"已达成",算不出来显示 "--"。 -function dailyNeedText(gapMinor, remainingDays) { - if (gapMinor === null) { - return "--"; - } - if (gapMinor <= 0) { - return "已达成"; - } - if (!remainingDays) { - return "--"; - } - return formatMoneyMinor(Math.round(gapMinor / remainingDays)); -} - -// 可空求和:全部缺失保持 null,避免把缺失口径当 0。 function addNullable(sum, value) { if (isBlank(value)) { return sum; @@ -65,14 +24,7 @@ function numberOr(value, fallback) { return isBlank(value) ? fallback : Number(value); } -// 排序:按达成率(缺失排最后),并以当月充值作为次级排序;或直接按当月充值。 -function compareRows(left, right, sortKey) { - if (sortKey === "rate") { - const diff = numberOr(right.rate, -1) - numberOr(left.rate, -1); - if (diff) { - return diff; - } - } +function compareByMonthRecharge(left, right) { return numberOr(right.monthMinor, -1) - numberOr(left.monthMinor, -1); } @@ -84,52 +36,35 @@ function regionLabelOf(item) { return item.region_name || item.region_code || "全部区域"; } -function usdInputText(minor) { - if (isBlank(minor)) { - return "0"; - } - return String(Number(minor) / 100); -} - -// 美元输入 → USD 分;空串视为 0(清除目标),NaN/负数返回 null 表示非法。 -function usdInputToMinor(text) { - const numeric = Number(String(text).trim()); - if (!Number.isFinite(numeric) || numeric < 0) { - return null; - } - return Math.round(numeric * 100); +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, master, range, refresh } = useSocialBi(); + const { appCodes, isLoading, kpi } = useSocialBi(); const [dimension, setDimension] = useState("person"); - const [sortKey, setSortKey] = useState("rate"); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); const items = useMemo(() => kpi?.items || [], [kpi]); - const appRows = useMemo(() => kpi?.app_rows || [], [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, - rate: item.month_attainment_rate + monthMinor: item.month_recharge_usd_minor })); - rows.sort((left, right) => compareRows(left, right, sortKey)); + rows.sort(compareByMonthRecharge); return rows; - }, [items, sortKey]); + }, [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, - rate: item.month_attainment_rate + monthMinor: item.month_recharge_usd_minor })); - rows.sort((left, right) => compareRows(left, right, sortKey)); + rows.sort(compareByMonthRecharge); return rows; - }, [kpi, sortKey]); + }, [kpi]); const teamRows = useMemo(() => { const groups = new Map(); @@ -137,28 +72,27 @@ export function TeamKpiView() { const teamName = item.team || "未分组"; let group = groups.get(teamName); if (!group) { - group = { members: new Set(), monthMinor: null, rangeMinor: null, targetMinor: null, team: teamName }; + 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); - group.targetMinor = addNullable(group.targetMinor, item.month_target_usd_minor); }); const rows = [...groups.values()].map((group) => ({ memberCount: group.members.size, monthMinor: group.monthMinor, rangeMinor: group.rangeMinor, - rate: group.targetMinor && group.monthMinor !== null ? group.monthMinor / group.targetMinor : null, - targetMinor: group.targetMinor, team: group.team })); - rows.sort((left, right) => compareRows(left, right, sortKey)); + rows.sort(compareByMonthRecharge); return rows; - }, [items, sortKey]); + }, [items]); if (!items.length) { - return isLoading ? : ( + return isLoading ? ( + + ) : (
暂无充值数据 @@ -169,79 +103,15 @@ export function TeamKpiView() { } const summary = kpi.summary || {}; - const canViewAll = master?.permissions?.kpi_view_all !== false; - const canManage = Boolean(master?.permissions?.kpi_manage); - const remainingDays = remainingDaysOf(kpi.month_end_ms); - - const heroRate = summary.month_attainment_rate; - const attainText = isBlank(heroRate) ? "未设目标" : formatRatioPercent(heroRate); - const ringPct = isBlank(heroRate) ? 0 : Math.max(0, Math.min(100, Number(heroRate) * 100)); - const heroGap = gapMinorOf(summary.month_target_usd_minor, summary.month_recharge_usd_minor); - const heroGapText = heroGap === null ? "--" : heroGap <= 0 ? "已达成" : formatMoneyMinor(heroGap); - const heroDailyNeed = - heroGap !== null && heroGap > 0 && remainingDays > 0 ? formatMoneyMinor(Math.round(heroGap / remainingDays)) : "--"; + const operatorCount = isBlank(summary.operator_count) ? operatorCountOf(items) : summary.operator_count; return (
-
-
-
- {canViewAll ? "团队充值" : "我的充值"} - 本月充值目标进度 · {kpi.period_month} - {!canViewAll ? 仅展示我负责的范围 : null} -
-
-
-
- {attainText} - 本月达成率 -
-
-
-
-
当月充值
-
{formatMoneyMinor(summary.month_recharge_usd_minor)}
-
-
-
月目标
-
{formatMoneyMinor(summary.month_target_usd_minor)}
-
-
-
缺口
-
{heroGapText}
-
-
-
-
-
- 剩余天数 - {remainingDays} 天 - {kpi.period_month} -
-
- 需日均 - {heroDailyNeed} - 达成月目标每日还需充值 -
-
- 区间充值 - {formatMoneyMinor(summary.range_recharge_usd_minor)} - {rangeLabel(range)} -
-
- - {appRows.length ? : null} -
充值排行 - {kpi.period_month} · {formatCount(summary.operator_count)} 名运营 + {kpi.period_month} · {formatCount(operatorCount)} 名运营
@@ -258,49 +128,18 @@ export function TeamKpiView() { ))}
-
- {SORTS.map((option) => ( - - ))} -
- {canManage ? ( - - ) : null}
{dimension === "person" ? ( - + ) : dimension === "operatorApp" ? ( - + ) : ( )}
- - {isDrawerOpen ? ( - setIsDrawerOpen(false)} - onSaved={() => { - setIsDrawerOpen(false); - refresh(); - }} - /> - ) : null}
); } @@ -308,26 +147,9 @@ export function TeamKpiView() { function KpiSkeleton() { return (
-
-
- -
- - - -
-
- {[0, 1, 2].map((index) => ( -
- - - -
- ))} -
- {[0, 1, 2, 3, 4].map((index) => ( + {[0, 1, 2, 3, 4, 5].map((index) => ( ))}
@@ -341,64 +163,7 @@ function RankBadge({ rank }) { return {rank}; } -// 达成率单元格:底槽 + 按达成率填充(超 100% 封顶),数字显示真实值;null 显示"未设目标"。 -function AttainmentBar({ rate }) { - if (isBlank(rate)) { - return 未设目标; - } - const numeric = Number(rate); - const pct = Math.max(0, Math.min(100, numeric * 100)); - return ( - - - ); -} - -// App 总目标进度条:不分运营的全员共同目标(kpi.app_rows)。 -function AppTargetStrip({ appCodes, appRows, remainingDays }) { - return ( -
- {appRows.map((row) => { - const gap = gapMinorOf(row.month_target_usd_minor, row.month_recharge_usd_minor); - return ( -
-
- - - {row.app_name || row.app_code} - - App 总目标 - {row.data_error ? ( - - 数据异常 - - ) : null} -
-
- {formatMoneyMinor(row.month_recharge_usd_minor)} - / 目标 {row.month_target_usd_minor > 0 ? formatMoneyMinor(row.month_target_usd_minor) : "未设"} -
- - - {[ - `${formatCount(row.operator_count)} 名运营`, - `区间充值 ${formatMoneyMinor(row.range_recharge_usd_minor)}`, - `需日均 ${dailyNeedText(gap, remainingDays)}` - ].join(" · ")} - -
- ); - })} -
- ); -} - -// 人员×App 小计表:该运营在此 App 下全部负责区域的合计与其 App 级目标。 -function OperatorAppTable({ appCodes, remainingDays, rows }) { +function OperatorAppTable({ appCodes, rows }) { return ( @@ -408,60 +173,34 @@ function OperatorAppTable({ appCodes, remainingDays, rows }) { - - - {rows.map((row, index) => { const item = row.item; - const gap = gapMinorOf(item.month_target_usd_minor, item.month_recharge_usd_minor); return ( - + - - - ); })} @@ -470,7 +209,7 @@ function OperatorAppTable({ appCodes, remainingDays, rows }) { ); } -function PersonTable({ appCodes, remainingDays, rows }) { +function PersonTable({ appCodes, rows }) { return (
App 负责区域 - 区间充值 + + 区间充值 + - 当月充值 - - App 月目标 - - 达成率 - - 需日均 + + 当月充值 +
-
- - {operatorLabelOf(item)} - {item.data_error ? ( - - 数据异常 - - ) : null} - - {[item.operator_account, item.team || "未分组"].filter(Boolean).join(" · ")} -
+
- - - {item.app_name || item.app_code} - - - {item.whole_app_scope ? "全部区域" : item.region_names || `${formatCount(item.region_count)} 个区域`} + {item.whole_app_scope ? "全部区域" : item.region_names || `${formatCount(item.region_count)} 个区域`} {formatMoneyMinor(item.range_recharge_usd_minor)} {formatMoneyMinor(item.month_recharge_usd_minor)}{formatMoneyMinor(item.month_target_usd_minor)} - - {dailyNeedText(gap, remainingDays)}
@@ -480,58 +219,34 @@ function PersonTable({ appCodes, remainingDays, rows }) { - - - {rows.map((row, index) => { const item = row.item; - const gap = gapMinorOf(item.month_target_usd_minor, item.month_recharge_usd_minor); return ( - - - ); })} @@ -548,19 +263,19 @@ function TeamTable({ rows }) { - - @@ -574,10 +289,6 @@ function TeamTable({ rows }) { - - ))} @@ -585,299 +296,27 @@ function TeamTable({ rows }) { ); } -function draftInvalid(draft) { - return usdInputToMinor(draft.monthText) === null || usdInputToMinor(draft.dailyText) === null; -} - -function TargetInputCell({ draft, label, onChange }) { +function OperatorCell({ item }) { return ( - - ); -} - -// 配置目标抽屉:三级目标—— -// ① App 总目标(不分运营的全员共同目标) ② 运营×App 总目标(该运营在此 App 的总盘子) -// ③ 运营×App×区域 明细目标。月/日目标同时填 0 表示清除该条。 -function KpiTargetDrawer({ appCodes, kpi, onClose, onSaved }) { - const [appDrafts, setAppDrafts] = useState(() => - (kpi.app_rows || []).map((row) => ({ - appCode: row.app_code, - appName: row.app_name || row.app_code, - dailyText: usdInputText(row.daily_target_usd_minor), - monthText: usdInputText(row.month_target_usd_minor) - })) - ); - // 整 App 范围(region 0)的运营在明细区已有"全部区域"行(同一存储键),此处排除避免同键两处编辑。 - const [groupDrafts, setGroupDrafts] = useState(() => - (kpi.operator_app_rows || []) - .filter((row) => !row.whole_app_scope) - .map((row) => ({ - appCode: row.app_code, - appName: row.app_name || row.app_code, - dailyText: usdInputText(row.daily_target_usd_minor), - monthText: usdInputText(row.month_target_usd_minor), - operatorAccount: row.operator_account || "", - operatorLabel: row.operator_name || row.operator_account || `#${row.operator_user_id}`, - regionNames: row.region_names || "", - userId: row.operator_user_id - })) - ); - const [regionDrafts, setRegionDrafts] = useState(() => - (kpi.items || []).map((item) => ({ - appCode: item.app_code, - appName: item.app_name || item.app_code, - dailyText: usdInputText(item.daily_target_usd_minor), - monthText: usdInputText(item.month_target_usd_minor), - operatorAccount: item.operator_account || "", - operatorLabel: operatorLabelOf(item), - regionId: Number(item.region_id ?? 0), - regionName: regionLabelOf(item), - userId: item.operator_user_id - })) - ); - const [isSaving, setIsSaving] = useState(false); - const [saveError, setSaveError] = useState(""); - - const hasInvalid = [...appDrafts, ...groupDrafts, ...regionDrafts].some(draftInvalid); - const hasRows = appDrafts.length > 0 || groupDrafts.length > 0 || regionDrafts.length > 0; - - const updateAt = (setDrafts) => (index, key, value) => { - setDrafts((current) => current.map((draft, draftIndex) => (draftIndex === index ? { ...draft, [key]: value } : draft))); - }; - const updateAppDraft = updateAt(setAppDrafts); - const updateGroupDraft = updateAt(setGroupDrafts); - const updateRegionDraft = updateAt(setRegionDrafts); - - const handleSave = async () => { - const items = [ - ...groupDrafts.map((draft) => ({ - appCode: draft.appCode, - dailyTargetUsdMinor: usdInputToMinor(draft.dailyText), - periodMonth: kpi.period_month, - regionId: 0, - targetUsdMinor: usdInputToMinor(draft.monthText), - userId: draft.userId - })), - ...regionDrafts.map((draft) => ({ - appCode: draft.appCode, - dailyTargetUsdMinor: usdInputToMinor(draft.dailyText), - periodMonth: kpi.period_month, - regionId: draft.regionId, - targetUsdMinor: usdInputToMinor(draft.monthText), - userId: draft.userId - })) - ]; - const appItems = appDrafts.map((draft) => ({ - appCode: draft.appCode, - dailyTargetUsdMinor: usdInputToMinor(draft.dailyText), - periodMonth: kpi.period_month, - targetUsdMinor: usdInputToMinor(draft.monthText) - })); - setIsSaving(true); - setSaveError(""); - try { - await replaceSocialBiKpiTargets({ appItems, items }); - onSaved(); - } catch (error) { - setSaveError(error?.message || "保存失败,请稍后重试"); - setIsSaving(false); - } - }; - - return ( -
-
App 区域 - 区间充值 + + 区间充值 + - 当月充值 - - 月目标 - - 达成率 - - 需日均 + + 当月充值 +
-
- - {operatorLabelOf(item)} - {item.data_error ? ( - - 数据异常 - - ) : null} - - {[item.operator_account, item.team || "未分组"].filter(Boolean).join(" · ")} -
+
- - - {item.app_name || item.app_code} - + {regionLabelOf(item)} {formatMoneyMinor(item.range_recharge_usd_minor)} {formatMoneyMinor(item.month_recharge_usd_minor)}{formatMoneyMinor(item.month_target_usd_minor)} - - {dailyNeedText(gap, remainingDays)}
名次 部门 - 人数 + + 人数 + - 区间充值 + + 区间充值 + - 当月充值 - - 月目标 - - 达成率 + + 当月充值 +
{formatCount(row.memberCount)} {formatMoneyMinor(row.rangeMinor)} {formatMoneyMinor(row.monthMinor)}{formatMoneyMinor(row.targetMinor)} - -
- - - - - - - - - {appDrafts.map((draft, index) => ( - - - - - - ))} - -
App当月目标 ($)当日目标 ($)
- - - {draft.appName} - - - updateAppDraft(index, "monthText", event.target.value)} - /> - - updateAppDraft(index, "dailyText", event.target.value)} - /> -
-
-
- ) : null} - {groupDrafts.length ? ( -
-

运营 × App 总目标 该运营在此 App 全部负责区域的总盘子

-
- - - - - - - - - - - - {groupDrafts.map((draft, index) => ( - - - - - - - - ))} - -
运营人员App负责区域当月目标 ($)当日目标 ($)
-
- - {draft.operatorLabel} - - {draft.operatorAccount ? {draft.operatorAccount} : null} -
-
- - - {draft.appName} - - {draft.regionNames} - updateGroupDraft(index, "monthText", event.target.value)} - /> - - updateGroupDraft(index, "dailyText", event.target.value)} - /> -
-
-
- ) : null} - {regionDrafts.length ? ( -
-

运营 × App × 区域 明细目标

-
- - - - - - - - - - - - {regionDrafts.map((draft, index) => ( - - - - - - - - ))} - -
运营人员App区域当月目标 ($)当日目标 ($)
-
- - {draft.operatorLabel} - - {draft.operatorAccount ? {draft.operatorAccount} : null} -
-
- - - {draft.appName} - - {draft.regionName} - updateRegionDraft(index, "monthText", event.target.value)} - /> - - updateRegionDraft(index, "dailyText", event.target.value)} - /> -
-
-
- ) : null} - - ) : ( -
- 暂无可配置的目标行 - 先在用户管理为运营人员分配 App/区域数据范围 -
- )} - -
- {saveError || hasInvalid ? ( - - {saveError || "目标金额需为非负数字"} - - ) : null} - - -
- +
+ + {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} + + ); +} diff --git a/databi/src/social/views/team-kpi-view.css b/databi/src/social/views/team-kpi-view.css index 9f54af1..3859ef0 100644 --- a/databi/src/social/views/team-kpi-view.css +++ b/databi/src/social/views/team-kpi-view.css @@ -1,5 +1,4 @@ -/* 人员绩效视图(TeamKpiView)专属样式:仅消费 social-v2.css 的 token,类名前缀 sbi-kpi-。 - 金银铜名次徽标与图表网格色 #eef3f9 为本视图的功能色,其余颜色一律走 token。 */ +/* 运营中心充值排行视图专属样式:仅消费 social-v2.css 的 token,类名前缀 sbi-kpi-。 */ .sbi-kpi-view { display: flex; @@ -7,121 +6,6 @@ gap: 16px; } -/* ---------- Hero:1 大卡(环形进度)+ 3 小卡 ---------- */ - -.sbi-kpi-hero { - display: grid; - grid-template-columns: minmax(0, 1.8fr) repeat(3, minmax(0, 1fr)); - gap: 16px; -} - -.sbi-kpi-hero-body { - display: flex; - align-items: center; - gap: 28px; - padding: 18px; -} - -.sbi-kpi-ring { - --sbi-kpi-pct: 0; - display: grid; - flex: none; - width: 138px; - height: 138px; - place-items: center; - border-radius: 50%; - background: conic-gradient(var(--sbi-accent) calc(var(--sbi-kpi-pct) * 1%), #eef3f9 0); -} - -.sbi-kpi-ring-hole { - display: grid; - gap: 2px; - width: 104px; - height: 104px; - place-content: center; - place-items: center; - border-radius: 50%; - background: var(--sbi-card); -} - -.sbi-kpi-ring-hole strong { - font-size: 22px; - font-variant-numeric: tabular-nums; -} - -.sbi-kpi-ring-hole strong.is-muted { - color: var(--sbi-text-3); - font-size: 14px; - font-weight: 700; -} - -.sbi-kpi-ring-hole small { - color: var(--sbi-text-3); - font-size: 11px; -} - -.sbi-kpi-hero-stats { - display: grid; - flex: 1; - gap: 11px; - min-width: 0; - margin: 0; -} - -.sbi-kpi-hero-stats > div { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 12px; - padding-bottom: 9px; - border-bottom: 1px dashed var(--sbi-border); -} - -.sbi-kpi-hero-stats > div:last-child { - padding-bottom: 0; - border-bottom: 0; -} - -.sbi-kpi-hero-stats dt { - color: var(--sbi-text-2); - font-size: 12.5px; -} - -.sbi-kpi-hero-stats dd { - margin: 0; - font-size: 17px; - font-weight: 720; - font-variant-numeric: tabular-nums; -} - -.sbi-kpi-mini { - display: flex; - flex-direction: column; - justify-content: center; - gap: 5px; - padding: 16px 18px; -} - -.sbi-kpi-mini-label { - align-self: flex-start; - color: var(--sbi-text-2); - font-size: 12.5px; - font-weight: 640; -} - -.sbi-kpi-mini-value { - font-size: 22px; - font-weight: 760; - font-variant-numeric: tabular-nums; -} - -.sbi-kpi-mini-note { - color: var(--sbi-text-3); - font-size: 11.5px; -} - -/* ---------- 排行榜 ---------- */ - .sbi-kpi-board-scroll { max-height: 620px; margin-top: 12px; @@ -185,248 +69,8 @@ border-radius: 50%; } -.sbi-kpi-attain { - display: inline-flex; - align-items: center; - gap: 8px; -} - -.sbi-kpi-bar { - display: inline-block; - width: 96px; - height: 6px; - overflow: hidden; - border-radius: 999px; - background: #eef3f9; -} - -.sbi-kpi-bar-fill { - display: block; - height: 100%; - border-radius: 999px; - background: var(--sbi-accent-grad); -} - -.sbi-kpi-bar-fill.is-done { - background: var(--sbi-up); -} - -.sbi-kpi-attain-value { - min-width: 52px; - text-align: right; - font-variant-numeric: tabular-nums; -} - -.sbi-kpi-no-target { - color: var(--sbi-text-3); - font-size: 12px; -} - -/* ---------- 骨架 ---------- */ - -.sbi-kpi-hero-loading { - display: flex; - align-items: center; - gap: 28px; - padding: 18px; -} - -.sbi-kpi-skeleton-ring { - flex: none; - width: 138px; - height: 138px; - border-radius: 50%; -} - -.sbi-kpi-skeleton-lines { - display: grid; - flex: 1; - gap: 14px; -} - .sbi-kpi-table-skeleton { display: grid; gap: 14px; padding: 18px; } - -/* ---------- 配置目标抽屉 ---------- */ - -.sbi-kpi-drawer-mask { - position: fixed; - inset: 0; - z-index: 60; - background: rgba(16, 27, 45, 0.42); -} - -.sbi-kpi-drawer { - position: absolute; - top: 0; - right: 0; - bottom: 0; - display: flex; - flex-direction: column; - width: min(760px, 94vw); - border-left: 1px solid var(--sbi-border); - background: var(--sbi-card); - box-shadow: var(--sbi-shadow); - animation: sbi-kpi-slide-in 180ms ease; -} - -@keyframes sbi-kpi-slide-in { - from { - transform: translateX(28px); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -.sbi-kpi-drawer-header { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 16px 20px; - border-bottom: 1px solid var(--sbi-border); -} - -.sbi-kpi-drawer-title { - display: grid; - gap: 3px; -} - -.sbi-kpi-drawer-title strong { - font-size: 15px; -} - -.sbi-kpi-drawer-title small { - color: var(--sbi-text-3); - font-size: 12px; -} - -.sbi-kpi-drawer-close { - display: grid; - width: 30px; - height: 30px; - place-items: center; - margin-left: auto; - border: 1px solid var(--sbi-border); - border-radius: 8px; - background: var(--sbi-card); - color: var(--sbi-text-2); - font-size: 16px; - line-height: 1; - cursor: pointer; -} - -.sbi-kpi-drawer-close:hover { - border-color: var(--sbi-accent); - color: var(--sbi-accent); -} - -.sbi-kpi-drawer-body { - flex: 1; - overflow: auto; - padding: 0 20px; -} - -.sbi-kpi-drawer-footer { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 10px; - padding: 14px 20px; - border-top: 1px solid var(--sbi-border); -} - -.sbi-kpi-drawer-error { - margin-right: auto; - color: var(--sbi-down); - font-size: 12.5px; -} - -.sbi-kpi-input { - width: 112px; - height: 30px; - padding: 0 8px; - border: 1px solid var(--sbi-border); - border-radius: 7px; - background: var(--sbi-card); - color: var(--sbi-text); - font-size: 13px; - text-align: right; - font-variant-numeric: tabular-nums; -} - -.sbi-kpi-input:focus { - border-color: var(--sbi-accent); - outline: none; - box-shadow: 0 0 0 3px rgba(37, 87, 214, 0.12); -} - -.sbi-kpi-input.is-invalid { - border-color: var(--sbi-down); -} - -/* App 总目标进度条(全员共同目标) */ - -.sbi-kpi-app-strip { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 12px; -} - -.sbi-kpi-app-card { - display: flex; - flex-direction: column; - gap: 8px; - padding: 14px 18px; -} - -.sbi-kpi-app-card-head { - display: flex; - align-items: center; - gap: 8px; -} - -.sbi-kpi-app-card-head strong { - font-size: 14px; -} - -.sbi-kpi-app-card-amounts strong { - font-size: 20px; - font-variant-numeric: tabular-nums; -} - -.sbi-kpi-app-card-amounts small { - color: var(--sbi-text-3); - font-size: 12px; -} - -.sbi-kpi-app-card-note { - color: var(--sbi-text-3); - font-size: 11.5px; -} - -/* 目标抽屉分区 */ - -.sbi-kpi-drawer-section { - margin-bottom: 18px; -} - -.sbi-kpi-drawer-section > h4 { - display: flex; - align-items: baseline; - gap: 8px; - margin: 0 0 8px; - color: var(--sbi-text); - font-size: 13.5px; -} - -.sbi-kpi-drawer-section > h4 > small { - color: var(--sbi-text-3); - font-size: 12px; - font-weight: 500; -} diff --git a/src/app/permissions.ts b/src/app/permissions.ts index bc86dce..8f70dee 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -42,8 +42,6 @@ export const PERMISSIONS = { coinSellerStockCredit: "coin-seller:stock-credit", coinSellerExchangeRate: "coin-seller:exchange-rate", hostWithdrawalView: "host-withdrawal:view", - databiKpiManage: "databi-kpi:manage", - databiKpiViewAll: "databi-kpi:view-all", financeView: "finance:view", financeApplicationCreate: "finance-application:create", financeApplicationAudit: "finance-application:audit", diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 889eb78..1432da2 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -223,7 +223,6 @@ export const API_OPERATIONS = { listRoomTurnoverRewardSettlements: "listRoomTurnoverRewardSettlements", listSelfGames: "listSelfGames", listSevenDayCheckInClaims: "listSevenDayCheckInClaims", - listSocialBiKpiTargets: "listSocialBiKpiTargets", listSplashScreens: "listSplashScreens", listSystemMenus: "listSystemMenus", listTaskDefinitions: "listTaskDefinitions", @@ -256,7 +255,6 @@ export const API_OPERATIONS = { replaceRegionCountries: "replaceRegionCountries", replaceRoleDataScopes: "replaceRoleDataScopes", replaceRolePermissions: "replaceRolePermissions", - replaceSocialBiKpiTargets: "replaceSocialBiKpiTargets", replaceUserFinanceScopes: "replaceUserFinanceScopes", resetUserPassword: "resetUserPassword", retryRedPacketRefund: "retryRedPacketRefund", @@ -1813,13 +1811,6 @@ export const API_ENDPOINTS: Record = { permission: "seven-day-checkin:view", permissions: ["seven-day-checkin:view"] }, - listSocialBiKpiTargets: { - method: "GET", - operationId: API_OPERATIONS.listSocialBiKpiTargets, - path: "/v1/admin/databi/social/kpi-targets", - permission: "overview:view", - permissions: ["overview:view"] - }, listSplashScreens: { method: "GET", operationId: API_OPERATIONS.listSplashScreens, @@ -2034,13 +2025,6 @@ export const API_ENDPOINTS: Record = { permission: "role:permission", permissions: ["role:permission","role:manage"] }, - replaceSocialBiKpiTargets: { - method: "PUT", - operationId: API_OPERATIONS.replaceSocialBiKpiTargets, - path: "/v1/admin/databi/social/kpi-targets", - permission: "databi-kpi:manage", - permissions: ["databi-kpi:manage"] - }, replaceUserFinanceScopes: { method: "PUT", operationId: API_OPERATIONS.replaceUserFinanceScopes, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 0319813..f66045b 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -2100,22 +2100,6 @@ export interface paths { patch?: never; trace?: never; }; - "/admin/databi/social/kpi-targets": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["listSocialBiKpiTargets"]; - put: operations["replaceSocialBiKpiTargets"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/admin/finance/scope": { parameters: { query?: never; @@ -7284,58 +7268,6 @@ export interface operations { 200: components["responses"]["StatisticsObjectResponse"]; }; }; - listSocialBiKpiTargets: { - parameters: { - query?: { - period_month?: string; - user_id?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: components["responses"]["StatisticsObjectResponse"]; - }; - }; - replaceSocialBiKpiTargets: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - items?: { - /** Format: int64 */ - userId?: number; - appCode?: string; - /** Format: int64 */ - regionId?: number; - periodMonth?: string; - /** Format: int64 */ - targetUsdMinor?: number; - /** Format: int64 */ - dailyTargetUsdMinor?: number; - }[]; - appItems?: { - appCode?: string; - periodMonth?: string; - /** Format: int64 */ - targetUsdMinor?: number; - /** Format: int64 */ - dailyTargetUsdMinor?: number; - }[]; - }; - }; - }; - responses: { - 200: components["responses"]["StatisticsObjectResponse"]; - }; - }; getFinanceScope: { parameters: { query?: never;