2026-05-25 19:36:44 +08:00

199 lines
8.2 KiB
JavaScript

import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
import TextField from "@mui/material/TextField";
import { LuckyGiftConfigDrawer } from "@/features/lucky-gift/components/LuckyGiftConfigDrawer.jsx";
import { LuckyGiftConfigSummary } from "@/features/lucky-gift/components/LuckyGiftConfigSummary.jsx";
import { LuckyGiftSimulationPanel } from "@/features/lucky-gift/components/LuckyGiftSimulationPanel.jsx";
import { drawStatusOptions } from "@/features/lucky-gift/constants.js";
import { useLuckyGiftPage } from "@/features/lucky-gift/hooks/useLuckyGiftPage.js";
import styles from "@/features/lucky-gift/lucky-gift.module.css";
import { AdminFilterResetButton, AdminFilterSelect, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
import { Button } from "@/shared/ui/Button.jsx";
export function LuckyGiftConfigPage() {
const page = useLuckyGiftPage();
return (
<AdminListPage>
<LuckyGiftConfigSummary
canUpdate={page.abilities.canUpdate}
config={page.config}
configLoading={page.configLoading}
poolId={page.poolId}
poolOptions={page.poolOptions}
onAddPool={page.openAddPool}
onEdit={page.openConfigDrawer}
onPoolChange={page.changePoolId}
onRefresh={page.reloadConfig}
/>
<LuckyGiftSimulationPanel config={page.config} />
<LuckyGiftDrawSummaryPanel page={page} />
<LuckyGiftConfigDrawer
abilities={page.abilities}
configLoading={page.configLoading}
configSaving={page.configSaving}
form={page.form}
open={page.configDrawerOpen}
setForm={page.setForm}
onClose={page.closeConfigDrawer}
onSubmit={page.submitConfig}
/>
<AddPoolDialog
disabled={page.configSaving}
open={page.addPoolOpen}
value={page.addPoolId}
onChange={page.setAddPoolId}
onClose={page.closeAddPool}
onSubmit={page.submitAddPool}
/>
</AdminListPage>
);
}
function LuckyGiftDrawSummaryPanel({ page }) {
const summary = page.drawSummary || {};
const netSpent = Number(summary.totalSpentCoins || 0) - Number(summary.totalRewardCoins || 0);
const stats = [
{
hint: `房间 ${formatNumber(summary.uniqueRooms)}`,
label: "参与用户",
value: formatNumber(summary.uniqueUsers),
},
{
hint: `成功 ${formatNumber(summary.grantedDraws)} / 待发放 ${formatNumber(summary.pendingDraws)} / 失败 ${formatNumber(summary.failedDraws)}`,
label: "抽奖次数",
value: formatNumber(summary.totalDraws),
},
{
hint: `目标 ${formatPPM(page.config?.targetRTPPPM)}`,
label: "实际 RTP",
value: formatPPM(summary.actualRTPPPM),
},
{
hint: `净消耗 ${formatNumber(netSpent)}`,
label: "消耗金币",
value: formatNumber(summary.totalSpentCoins),
},
{
hint: `基础 ${formatNumber(summary.baseRewardCoins)}`,
label: "返还金币",
value: formatNumber(summary.totalRewardCoins),
},
{
hint: `房间 ${formatNumber(summary.roomAtmosphereRewardCoins)} / 活动 ${formatNumber(summary.activitySubsidyCoins)}`,
label: "额外预算",
value: formatNumber(
Number(summary.roomAtmosphereRewardCoins || 0) + Number(summary.activitySubsidyCoins || 0),
),
},
];
return (
<section className={styles.drawSummaryPanel}>
<header className={styles.drawSummaryHeader}>
<div className={styles.stack}>
<h2 className={styles.sectionTitle}>抽奖数据概览</h2>
<span className={styles.meta}>奖池 {summary.poolId || page.poolId}</span>
</div>
<Button
disabled={page.drawSummaryLoading}
startIcon={<RefreshOutlined fontSize="small" />}
onClick={page.reloadDrawSummary}
>
刷新
</Button>
</header>
<div className={styles.drawSummaryFilters}>
<TextField className={styles.filterInput} disabled label="当前奖池" value={page.poolId} />
<TextField
className={styles.filterInput}
label="礼物 ID"
value={page.giftId}
onChange={(event) => page.changeGiftId(event.target.value)}
/>
<TextField
className={styles.filterInput}
label="用户 ID"
value={page.userId}
onChange={(event) => page.changeUserId(event.target.value)}
/>
<TextField
className={styles.filterInput}
label="房间 ID"
value={page.roomId}
onChange={(event) => page.changeRoomId(event.target.value)}
/>
<AdminFilterSelect
label="发放状态"
options={[["", "全部状态"], ...drawStatusOptions]}
value={page.status}
onChange={page.changeStatus}
/>
<AdminFilterResetButton
disabled={!page.giftId && !page.userId && !page.roomId && !page.status}
onClick={page.resetDrawFilters}
/>
</div>
{page.drawSummaryError ? (
<div className={styles.summaryError}>
<span>{page.drawSummaryError}</span>
<Button startIcon={<RefreshOutlined fontSize="small" />} onClick={page.reloadDrawSummary}>
重试
</Button>
</div>
) : (
<div className={styles.drawSummaryGrid} aria-busy={page.drawSummaryLoading}>
{stats.map((item) => (
<div className={styles.metricItem} key={item.label}>
<span>{item.label}</span>
<strong>{page.drawSummaryLoading ? "-" : item.value}</strong>
<small>{page.drawSummaryLoading ? "" : item.hint}</small>
</div>
))}
</div>
)}
</section>
);
}
function AddPoolDialog({ disabled, onChange, onClose, onSubmit, open, value }) {
return (
<Dialog fullWidth maxWidth="xs" open={open} onClose={disabled ? undefined : onClose}>
<form onSubmit={onSubmit}>
<DialogTitle>添加奖池</DialogTitle>
<DialogContent>
<TextField
autoFocus
fullWidth
required
disabled={disabled}
label="奖池 ID"
margin="dense"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</DialogContent>
<DialogActions>
<Button disabled={disabled} type="button" onClick={onClose}>
取消
</Button>
<Button disabled={disabled} type="submit" variant="primary">
添加并编辑
</Button>
</DialogActions>
</form>
</Dialog>
);
}
function formatPPM(value) {
return `${(Number(value || 0) / 10_000).toFixed(2).replace(/\.?0+$/, "")}%`;
}
function formatNumber(value) {
return new Intl.NumberFormat("zh-CN").format(Number(value || 0));
}