199 lines
8.2 KiB
JavaScript
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));
|
|
}
|