修改bi后台数据

This commit is contained in:
zhx 2026-07-03 22:42:42 +08:00
parent a83907fc05
commit 68ef0dfee3
10 changed files with 454 additions and 87 deletions

View File

@ -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" }
}
}
} }
} }
} }

View File

@ -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>

View File

@ -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"
}); });
} }

View File

@ -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="数据视图">

View File

@ -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-targetsreplaceSocialBiKpiTargets/ 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>

View File

@ -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;
}

View File

@ -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}

View File

@ -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>

View File

@ -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 {

View File

@ -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;
}[];
}; };
}; };
}; };