修改bi后台数据
This commit is contained in:
parent
a83907fc05
commit
68ef0dfee3
@ -3206,6 +3206,18 @@
|
|||||||
"dailyTargetUsdMinor": { "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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>HYApp Social BI</title>
|
<title>运营数据总览</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="databi-root"></div>
|
<div id="databi-root"></div>
|
||||||
|
|||||||
@ -214,9 +214,12 @@ export async function fetchSocialBiKpiTargets({ periodMonth, userId } = {}) {
|
|||||||
return fetchDatabiData(apiEndpointPath(API_OPERATIONS.listSocialBiKpiTargets), { query });
|
return fetchDatabiData(apiEndpointPath(API_OPERATIONS.listSocialBiKpiTargets), { query });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function replaceSocialBiKpiTargets(items) {
|
// 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), {
|
return fetchDatabiData(apiEndpointPath(API_OPERATIONS.replaceSocialBiKpiTargets), {
|
||||||
body: { items },
|
body,
|
||||||
method: "PUT"
|
method: "PUT"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,8 +100,7 @@ function Sidebar({ data, onViewChange, viewKey }) {
|
|||||||
<div className="sbi-brand">
|
<div className="sbi-brand">
|
||||||
<span className="sbi-brand-mark">HY</span>
|
<span className="sbi-brand-mark">HY</span>
|
||||||
<div className="sbi-brand-copy">
|
<div className="sbi-brand-copy">
|
||||||
<strong>社交 BI</strong>
|
<strong>运营数据总览</strong>
|
||||||
<small>Social Analytics</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="sbi-nav" role="tablist" aria-label="数据视图">
|
<nav className="sbi-nav" role="tablist" aria-label="数据视图">
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import "./team-kpi-view.css";
|
|||||||
const DAY_MS = 86400000;
|
const DAY_MS = 86400000;
|
||||||
|
|
||||||
const DIMENSIONS = [
|
const DIMENSIONS = [
|
||||||
{ key: "person", label: "按人员" },
|
{ key: "person", label: "按人员×区域" },
|
||||||
|
{ key: "operatorApp", label: "按人员×App" },
|
||||||
{ key: "team", label: "按部门" }
|
{ key: "team", label: "按部门" }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -106,6 +107,7 @@ export function TeamKpiView() {
|
|||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
|
||||||
const items = useMemo(() => kpi?.items || [], [kpi]);
|
const items = useMemo(() => kpi?.items || [], [kpi]);
|
||||||
|
const appRows = useMemo(() => kpi?.app_rows || [], [kpi]);
|
||||||
|
|
||||||
const personRows = useMemo(() => {
|
const personRows = useMemo(() => {
|
||||||
const rows = items.map((item, index) => ({
|
const rows = items.map((item, index) => ({
|
||||||
@ -118,6 +120,17 @@ export function TeamKpiView() {
|
|||||||
return rows;
|
return rows;
|
||||||
}, [items, sortKey]);
|
}, [items, sortKey]);
|
||||||
|
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
rows.sort((left, right) => compareRows(left, right, sortKey));
|
||||||
|
return rows;
|
||||||
|
}, [kpi, sortKey]);
|
||||||
|
|
||||||
const teamRows = useMemo(() => {
|
const teamRows = useMemo(() => {
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
@ -222,6 +235,8 @@ export function TeamKpiView() {
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{appRows.length ? <AppTargetStrip appCodes={appCodes} appRows={appRows} remainingDays={remainingDays} /> : null}
|
||||||
|
|
||||||
<section className="sbi-card sbi-kpi-board">
|
<section className="sbi-card sbi-kpi-board">
|
||||||
<div className="sbi-card-header">
|
<div className="sbi-card-header">
|
||||||
<strong>绩效排行</strong>
|
<strong>绩效排行</strong>
|
||||||
@ -267,6 +282,8 @@ export function TeamKpiView() {
|
|||||||
<div className="sbi-table-scroll sbi-kpi-board-scroll">
|
<div className="sbi-table-scroll sbi-kpi-board-scroll">
|
||||||
{dimension === "person" ? (
|
{dimension === "person" ? (
|
||||||
<PersonTable appCodes={appCodes} remainingDays={remainingDays} rows={personRows} />
|
<PersonTable appCodes={appCodes} remainingDays={remainingDays} rows={personRows} />
|
||||||
|
) : dimension === "operatorApp" ? (
|
||||||
|
<OperatorAppTable appCodes={appCodes} remainingDays={remainingDays} rows={operatorAppRows} />
|
||||||
) : (
|
) : (
|
||||||
<TeamTable rows={teamRows} />
|
<TeamTable rows={teamRows} />
|
||||||
)}
|
)}
|
||||||
@ -341,6 +358,118 @@ function AttainmentBar({ rate }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App 总目标进度条:不分运营的全员共同目标(kpi.app_rows)。
|
||||||
|
function AppTargetStrip({ appCodes, appRows, remainingDays }) {
|
||||||
|
return (
|
||||||
|
<section aria-label="App 总目标进度" className="sbi-kpi-app-strip">
|
||||||
|
{appRows.map((row) => {
|
||||||
|
const gap = gapMinorOf(row.month_target_usd_minor, row.month_recharge_usd_minor);
|
||||||
|
return (
|
||||||
|
<article className="sbi-card sbi-kpi-app-card" key={row.app_code}>
|
||||||
|
<div className="sbi-kpi-app-card-head">
|
||||||
|
<span className="sbi-kpi-app">
|
||||||
|
<span className="sbi-kpi-app-dot" style={{ background: appColor(row.app_code, appCodes) }} />
|
||||||
|
<strong>{row.app_name || row.app_code}</strong>
|
||||||
|
</span>
|
||||||
|
<span className="sbi-badge" title="不分运营的全员共同目标">App 总目标</span>
|
||||||
|
{row.data_error ? (
|
||||||
|
<span className="sbi-badge is-warn" title={row.data_error}>
|
||||||
|
数据异常
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="sbi-kpi-app-card-amounts">
|
||||||
|
<strong>{formatMoneyMinor(row.month_recharge_usd_minor)}</strong>
|
||||||
|
<small> / 目标 {row.month_target_usd_minor > 0 ? formatMoneyMinor(row.month_target_usd_minor) : "未设"}</small>
|
||||||
|
</div>
|
||||||
|
<AttainmentBar rate={row.month_attainment_rate} />
|
||||||
|
<small className="sbi-kpi-app-card-note">
|
||||||
|
{[
|
||||||
|
`${formatCount(row.operator_count)} 名运营`,
|
||||||
|
`区间充值 ${formatMoneyMinor(row.range_recharge_usd_minor)}`,
|
||||||
|
`需日均 ${dailyNeedText(gap, remainingDays)}`
|
||||||
|
].join(" · ")}
|
||||||
|
</small>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 人员×App 小计表:该运营在此 App 下全部负责区域的合计与其 App 级目标。
|
||||||
|
function OperatorAppTable({ appCodes, remainingDays, rows }) {
|
||||||
|
return (
|
||||||
|
<table className="sbi-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="is-left">名次</th>
|
||||||
|
<th className="is-left">运营人员</th>
|
||||||
|
<th className="is-left">App</th>
|
||||||
|
<th className="is-left">负责区域</th>
|
||||||
|
<th>
|
||||||
|
<span className="sbi-metric-hint" title="所选区间内该运营在此 App 负责范围的充值合计(USD)">区间充值</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span className="sbi-metric-hint" title="本自然月至今该运营在此 App 负责范围的充值合计(USD)">当月充值</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span className="sbi-metric-hint" title="该运营在此 App 的月度总目标(USD)">App 月目标</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span className="sbi-metric-hint" title="当月充值 / App 月目标">达成率</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span className="sbi-metric-hint" title="缺口 / 剩余天数">需日均</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row, index) => {
|
||||||
|
const item = row.item;
|
||||||
|
const gap = gapMinorOf(item.month_target_usd_minor, item.month_recharge_usd_minor);
|
||||||
|
return (
|
||||||
|
<tr key={row.key}>
|
||||||
|
<td className="is-left">
|
||||||
|
<RankBadge rank={index + 1} />
|
||||||
|
</td>
|
||||||
|
<td className="is-left">
|
||||||
|
<div className="sbi-kpi-person">
|
||||||
|
<span className="sbi-kpi-person-top">
|
||||||
|
<strong>{operatorLabelOf(item)}</strong>
|
||||||
|
{item.data_error ? (
|
||||||
|
<span className="sbi-badge is-warn" title={item.data_error}>
|
||||||
|
数据异常
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<small>{[item.operator_account, item.team || "未分组"].filter(Boolean).join(" · ")}</small>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-left">
|
||||||
|
<span className="sbi-kpi-app">
|
||||||
|
<span className="sbi-kpi-app-dot" style={{ background: appColor(item.app_code, appCodes) }} />
|
||||||
|
{item.app_name || item.app_code}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="is-left">
|
||||||
|
{item.whole_app_scope ? "全部区域" : item.region_names || `${formatCount(item.region_count)} 个区域`}
|
||||||
|
</td>
|
||||||
|
<td>{formatMoneyMinor(item.range_recharge_usd_minor)}</td>
|
||||||
|
<td>{formatMoneyMinor(item.month_recharge_usd_minor)}</td>
|
||||||
|
<td>{formatMoneyMinor(item.month_target_usd_minor)}</td>
|
||||||
|
<td>
|
||||||
|
<AttainmentBar rate={item.month_attainment_rate} />
|
||||||
|
</td>
|
||||||
|
<td>{dailyNeedText(gap, remainingDays)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function PersonTable({ appCodes, remainingDays, rows }) {
|
function PersonTable({ appCodes, remainingDays, rows }) {
|
||||||
return (
|
return (
|
||||||
<table className="sbi-table">
|
<table className="sbi-table">
|
||||||
@ -456,10 +585,53 @@ function TeamTable({ rows }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置目标抽屉:按 运营人员 × App × 区域 编辑当月/当日充值目标(美元),
|
function draftInvalid(draft) {
|
||||||
// 保存走 PUT /admin/databi/social/kpi-targets(replaceSocialBiKpiTargets),月/日目标填 0 表示清除。
|
return usdInputToMinor(draft.monthText) === null || usdInputToMinor(draft.dailyText) === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TargetInputCell({ draft, label, onChange }) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
aria-label={label}
|
||||||
|
className={usdInputToMinor(draft.value) === null ? "sbi-kpi-input is-invalid" : "sbi-kpi-input"}
|
||||||
|
inputMode="decimal"
|
||||||
|
min="0"
|
||||||
|
onChange={onChange}
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
value={draft.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置目标抽屉:三级目标——
|
||||||
|
// ① App 总目标(不分运营的全员共同目标) ② 运营×App 总目标(该运营在此 App 的总盘子)
|
||||||
|
// ③ 运营×App×区域 明细目标。月/日目标同时填 0 表示清除该条。
|
||||||
function KpiTargetDrawer({ appCodes, kpi, onClose, onSaved }) {
|
function KpiTargetDrawer({ appCodes, kpi, onClose, onSaved }) {
|
||||||
const [drafts, setDrafts] = useState(() =>
|
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) => ({
|
(kpi.items || []).map((item) => ({
|
||||||
appCode: item.app_code,
|
appCode: item.app_code,
|
||||||
appName: item.app_name || item.app_code,
|
appName: item.app_name || item.app_code,
|
||||||
@ -475,27 +647,45 @@ function KpiTargetDrawer({ appCodes, kpi, onClose, onSaved }) {
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState("");
|
const [saveError, setSaveError] = useState("");
|
||||||
|
|
||||||
const hasInvalid = drafts.some(
|
const hasInvalid = [...appDrafts, ...groupDrafts, ...regionDrafts].some(draftInvalid);
|
||||||
(draft) => usdInputToMinor(draft.monthText) === null || usdInputToMinor(draft.dailyText) === null
|
const hasRows = appDrafts.length > 0 || groupDrafts.length > 0 || regionDrafts.length > 0;
|
||||||
);
|
|
||||||
|
|
||||||
const updateDraft = (index, key, value) => {
|
const updateAt = (setDrafts) => (index, key, value) => {
|
||||||
setDrafts((current) => current.map((draft, draftIndex) => (draftIndex === index ? { ...draft, [key]: value } : draft)));
|
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 handleSave = async () => {
|
||||||
const payload = drafts.map((draft) => ({
|
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,
|
appCode: draft.appCode,
|
||||||
dailyTargetUsdMinor: usdInputToMinor(draft.dailyText),
|
dailyTargetUsdMinor: usdInputToMinor(draft.dailyText),
|
||||||
periodMonth: kpi.period_month,
|
periodMonth: kpi.period_month,
|
||||||
regionId: draft.regionId,
|
targetUsdMinor: usdInputToMinor(draft.monthText)
|
||||||
targetUsdMinor: usdInputToMinor(draft.monthText),
|
|
||||||
userId: draft.userId
|
|
||||||
}));
|
}));
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setSaveError("");
|
setSaveError("");
|
||||||
try {
|
try {
|
||||||
await replaceSocialBiKpiTargets(payload);
|
await replaceSocialBiKpiTargets({ appItems, items });
|
||||||
onSaved();
|
onSaved();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSaveError(error?.message || "保存失败,请稍后重试");
|
setSaveError(error?.message || "保存失败,请稍后重试");
|
||||||
@ -509,72 +699,164 @@ function KpiTargetDrawer({ appCodes, kpi, onClose, onSaved }) {
|
|||||||
<header className="sbi-kpi-drawer-header">
|
<header className="sbi-kpi-drawer-header">
|
||||||
<div className="sbi-kpi-drawer-title">
|
<div className="sbi-kpi-drawer-title">
|
||||||
<strong>配置充值 KPI 目标</strong>
|
<strong>配置充值 KPI 目标</strong>
|
||||||
<small>{kpi.period_month} · 按 运营人员 × App × 区域 · 月/日目标填 0 表示清除</small>
|
<small>{kpi.period_month} · App 总目标 / 运营×App / 运营×App×区域 三级 · 月/日目标填 0 表示清除</small>
|
||||||
</div>
|
</div>
|
||||||
<button aria-label="关闭" className="sbi-kpi-drawer-close" onClick={onClose} type="button">
|
<button aria-label="关闭" className="sbi-kpi-drawer-close" onClick={onClose} type="button">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className="sbi-kpi-drawer-body">
|
<div className="sbi-kpi-drawer-body">
|
||||||
{drafts.length ? (
|
{hasRows ? (
|
||||||
<div className="sbi-table-scroll">
|
<>
|
||||||
<table className="sbi-table">
|
{appDrafts.length ? (
|
||||||
<thead>
|
<section className="sbi-kpi-drawer-section">
|
||||||
<tr>
|
<h4>App 总目标 <small>不分运营的全员共同目标</small></h4>
|
||||||
<th className="is-left">运营人员</th>
|
<div className="sbi-table-scroll">
|
||||||
<th className="is-left">App</th>
|
<table className="sbi-table">
|
||||||
<th className="is-left">区域</th>
|
<thead>
|
||||||
<th>当月目标 ($)</th>
|
<tr>
|
||||||
<th>当日目标 ($)</th>
|
<th className="is-left">App</th>
|
||||||
</tr>
|
<th>当月目标 ($)</th>
|
||||||
</thead>
|
<th>当日目标 ($)</th>
|
||||||
<tbody>
|
</tr>
|
||||||
{drafts.map((draft, index) => (
|
</thead>
|
||||||
<tr key={`${draft.userId}-${draft.appCode}-${draft.regionId}-${index}`}>
|
<tbody>
|
||||||
<td className="is-left">
|
{appDrafts.map((draft, index) => (
|
||||||
<div className="sbi-kpi-person">
|
<tr key={draft.appCode}>
|
||||||
<span className="sbi-kpi-person-top">
|
<td className="is-left">
|
||||||
<strong>{draft.operatorLabel}</strong>
|
<span className="sbi-kpi-app">
|
||||||
</span>
|
<span className="sbi-kpi-app-dot" style={{ background: appColor(draft.appCode, appCodes) }} />
|
||||||
{draft.operatorAccount ? <small>{draft.operatorAccount}</small> : null}
|
{draft.appName}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="is-left">
|
<td>
|
||||||
<span className="sbi-kpi-app">
|
<TargetInputCell
|
||||||
<span className="sbi-kpi-app-dot" style={{ background: appColor(draft.appCode, appCodes) }} />
|
draft={{ ...draft, value: draft.monthText }}
|
||||||
{draft.appName}
|
label={`${draft.appName} App 当月总目标(美元)`}
|
||||||
</span>
|
onChange={(event) => updateAppDraft(index, "monthText", event.target.value)}
|
||||||
</td>
|
/>
|
||||||
<td className="is-left">{draft.regionName}</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<TargetInputCell
|
||||||
aria-label={`${draft.operatorLabel} ${draft.appName} 当月目标(美元)`}
|
draft={{ ...draft, value: draft.dailyText }}
|
||||||
className={usdInputToMinor(draft.monthText) === null ? "sbi-kpi-input is-invalid" : "sbi-kpi-input"}
|
label={`${draft.appName} App 当日总目标(美元)`}
|
||||||
inputMode="decimal"
|
onChange={(event) => updateAppDraft(index, "dailyText", event.target.value)}
|
||||||
min="0"
|
/>
|
||||||
onChange={(event) => updateDraft(index, "monthText", event.target.value)}
|
</td>
|
||||||
step="0.01"
|
</tr>
|
||||||
type="number"
|
))}
|
||||||
value={draft.monthText}
|
</tbody>
|
||||||
/>
|
</table>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
</section>
|
||||||
<input
|
) : null}
|
||||||
aria-label={`${draft.operatorLabel} ${draft.appName} 当日目标(美元)`}
|
{groupDrafts.length ? (
|
||||||
className={usdInputToMinor(draft.dailyText) === null ? "sbi-kpi-input is-invalid" : "sbi-kpi-input"}
|
<section className="sbi-kpi-drawer-section">
|
||||||
inputMode="decimal"
|
<h4>运营 × App 总目标 <small>该运营在此 App 全部负责区域的总盘子</small></h4>
|
||||||
min="0"
|
<div className="sbi-table-scroll">
|
||||||
onChange={(event) => updateDraft(index, "dailyText", event.target.value)}
|
<table className="sbi-table">
|
||||||
step="0.01"
|
<thead>
|
||||||
type="number"
|
<tr>
|
||||||
value={draft.dailyText}
|
<th className="is-left">运营人员</th>
|
||||||
/>
|
<th className="is-left">App</th>
|
||||||
</td>
|
<th className="is-left">负责区域</th>
|
||||||
</tr>
|
<th>当月目标 ($)</th>
|
||||||
))}
|
<th>当日目标 ($)</th>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
|
{groupDrafts.map((draft, index) => (
|
||||||
|
<tr key={`${draft.userId}-${draft.appCode}`}>
|
||||||
|
<td className="is-left">
|
||||||
|
<div className="sbi-kpi-person">
|
||||||
|
<span className="sbi-kpi-person-top">
|
||||||
|
<strong>{draft.operatorLabel}</strong>
|
||||||
|
</span>
|
||||||
|
{draft.operatorAccount ? <small>{draft.operatorAccount}</small> : null}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-left">
|
||||||
|
<span className="sbi-kpi-app">
|
||||||
|
<span className="sbi-kpi-app-dot" style={{ background: appColor(draft.appCode, appCodes) }} />
|
||||||
|
{draft.appName}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="is-left">{draft.regionNames}</td>
|
||||||
|
<td>
|
||||||
|
<TargetInputCell
|
||||||
|
draft={{ ...draft, value: draft.monthText }}
|
||||||
|
label={`${draft.operatorLabel} ${draft.appName} 当月总目标(美元)`}
|
||||||
|
onChange={(event) => updateGroupDraft(index, "monthText", event.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TargetInputCell
|
||||||
|
draft={{ ...draft, value: draft.dailyText }}
|
||||||
|
label={`${draft.operatorLabel} ${draft.appName} 当日总目标(美元)`}
|
||||||
|
onChange={(event) => updateGroupDraft(index, "dailyText", event.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
{regionDrafts.length ? (
|
||||||
|
<section className="sbi-kpi-drawer-section">
|
||||||
|
<h4>运营 × App × 区域 明细目标</h4>
|
||||||
|
<div className="sbi-table-scroll">
|
||||||
|
<table className="sbi-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="is-left">运营人员</th>
|
||||||
|
<th className="is-left">App</th>
|
||||||
|
<th className="is-left">区域</th>
|
||||||
|
<th>当月目标 ($)</th>
|
||||||
|
<th>当日目标 ($)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{regionDrafts.map((draft, index) => (
|
||||||
|
<tr key={`${draft.userId}-${draft.appCode}-${draft.regionId}-${index}`}>
|
||||||
|
<td className="is-left">
|
||||||
|
<div className="sbi-kpi-person">
|
||||||
|
<span className="sbi-kpi-person-top">
|
||||||
|
<strong>{draft.operatorLabel}</strong>
|
||||||
|
</span>
|
||||||
|
{draft.operatorAccount ? <small>{draft.operatorAccount}</small> : null}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-left">
|
||||||
|
<span className="sbi-kpi-app">
|
||||||
|
<span className="sbi-kpi-app-dot" style={{ background: appColor(draft.appCode, appCodes) }} />
|
||||||
|
{draft.appName}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="is-left">{draft.regionName}</td>
|
||||||
|
<td>
|
||||||
|
<TargetInputCell
|
||||||
|
draft={{ ...draft, value: draft.monthText }}
|
||||||
|
label={`${draft.operatorLabel} ${draft.appName} ${draft.regionName} 当月目标(美元)`}
|
||||||
|
onChange={(event) => updateRegionDraft(index, "monthText", event.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TargetInputCell
|
||||||
|
draft={{ ...draft, value: draft.dailyText }}
|
||||||
|
label={`${draft.operatorLabel} ${draft.appName} ${draft.regionName} 当日目标(美元)`}
|
||||||
|
onChange={(event) => updateRegionDraft(index, "dailyText", event.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="sbi-empty">
|
<div className="sbi-empty">
|
||||||
<strong>暂无可配置的目标行</strong>
|
<strong>暂无可配置的目标行</strong>
|
||||||
@ -591,7 +873,7 @@ function KpiTargetDrawer({ appCodes, kpi, onClose, onSaved }) {
|
|||||||
<Button disabled={isSaving} onClick={onClose} variant="text">
|
<Button disabled={isSaving} onClick={onClose} variant="text">
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={hasInvalid || isSaving || !drafts.length} onClick={handleSave} variant="contained">
|
<Button disabled={hasInvalid || isSaving || !hasRows} onClick={handleSave} variant="contained">
|
||||||
{isSaving ? "保存中…" : "保存目标"}
|
{isSaving ? "保存中…" : "保存目标"}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -369,3 +369,64 @@
|
|||||||
.sbi-kpi-input.is-invalid {
|
.sbi-kpi-input.is-invalid {
|
||||||
border-color: var(--sbi-down);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -692,6 +692,7 @@ export function FinanceApp() {
|
|||||||
return (
|
return (
|
||||||
<FinanceShell
|
<FinanceShell
|
||||||
activeView={activeView}
|
activeView={activeView}
|
||||||
|
appBar={appBar}
|
||||||
canAudit={session.canAuditApplication}
|
canAudit={session.canAuditApplication}
|
||||||
canCreate={session.canCreateApplication}
|
canCreate={session.canCreateApplication}
|
||||||
canViewRechargeDetails={session.canViewAppRechargeDetails}
|
canViewRechargeDetails={session.canViewAppRechargeDetails}
|
||||||
@ -704,11 +705,9 @@ export function FinanceApp() {
|
|||||||
withdrawalsState.loading
|
withdrawalsState.loading
|
||||||
}
|
}
|
||||||
session={session}
|
session={session}
|
||||||
subtitle={isFinanceView ? "中国时区 · 数据实时" : ""}
|
|
||||||
toolbar={financeToolbar}
|
toolbar={financeToolbar}
|
||||||
onViewChange={setActiveView}
|
onViewChange={setActiveView}
|
||||||
>
|
>
|
||||||
{appBar}
|
|
||||||
{activeView === "overview" && session.canViewAppRechargeDetails ? (
|
{activeView === "overview" && session.canViewAppRechargeDetails ? (
|
||||||
<FinanceOverview
|
<FinanceOverview
|
||||||
apps={rechargeApps}
|
apps={rechargeApps}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const VIEW_TITLES = {
|
|||||||
|
|
||||||
export function FinanceShell({
|
export function FinanceShell({
|
||||||
activeView,
|
activeView,
|
||||||
|
appBar,
|
||||||
canAudit,
|
canAudit,
|
||||||
canCreate,
|
canCreate,
|
||||||
canViewRechargeDetails,
|
canViewRechargeDetails,
|
||||||
@ -65,7 +66,7 @@ export function FinanceShell({
|
|||||||
<div className="finance-brand">
|
<div className="finance-brand">
|
||||||
<span>HY</span>
|
<span>HY</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>财务中台</strong>
|
<strong>财务工作台</strong>
|
||||||
<small>Finance Console</small>
|
<small>Finance Console</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -104,10 +105,12 @@ export function FinanceShell({
|
|||||||
<section className="finance-main">
|
<section className="finance-main">
|
||||||
{loading ? <LinearProgress className="finance-progress" /> : null}
|
{loading ? <LinearProgress className="finance-progress" /> : null}
|
||||||
<header className="finance-topbar">
|
<header className="finance-topbar">
|
||||||
<div className="finance-topbar__title">
|
{appBar || (
|
||||||
<h1>{VIEW_TITLES[activeView] || "财务系统"}</h1>
|
<div className="finance-topbar__title">
|
||||||
{subtitle ? <small>{subtitle}</small> : null}
|
<h1>{VIEW_TITLES[activeView] || "财务系统"}</h1>
|
||||||
</div>
|
{subtitle ? <small>{subtitle}</small> : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{toolbar ? <div className="finance-topbar__tools">{toolbar}</div> : null}
|
{toolbar ? <div className="finance-topbar__tools">{toolbar}</div> : null}
|
||||||
</header>
|
</header>
|
||||||
<div className="finance-content">{children}</div>
|
<div className="finance-content">{children}</div>
|
||||||
|
|||||||
@ -747,7 +747,7 @@ a {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-bottom: var(--space-4);
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-appchip {
|
.finance-appchip {
|
||||||
|
|||||||
8
src/shared/api/generated/schema.d.ts
vendored
8
src/shared/api/generated/schema.d.ts
vendored
@ -7133,6 +7133,14 @@ export interface operations {
|
|||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
dailyTargetUsdMinor?: number;
|
dailyTargetUsdMinor?: number;
|
||||||
}[];
|
}[];
|
||||||
|
appItems?: {
|
||||||
|
appCode?: string;
|
||||||
|
periodMonth?: string;
|
||||||
|
/** Format: int64 */
|
||||||
|
targetUsdMinor?: number;
|
||||||
|
/** Format: int64 */
|
||||||
|
dailyTargetUsdMinor?: number;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user