2026-07-06 15:14:05 +08:00

245 lines
11 KiB
JavaScript

import EditOutlined from "@mui/icons-material/EditOutlined";
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
import SaveOutlined from "@mui/icons-material/SaveOutlined";
import Drawer from "@mui/material/Drawer";
import TextField from "@mui/material/TextField";
import { useMemo } from "react";
import { useVipConfigPage } from "@/features/vip-config/hooks/useVipConfigPage.js";
import styles from "@/features/vip-config/vip-config.module.css";
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
import { Button } from "@/shared/ui/Button.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { ResourceGroupSelectField } from "@/shared/ui/ResourceGroupSelectDrawer.jsx";
import { formatMillis } from "@/shared/utils/time.js";
export function VipConfigPage() {
const page = useVipConfigPage();
const levels = page.config.levels || [];
const resourceLabels = useMemo(() => groupLabelMap(page.resourceGroups), [page.resourceGroups]);
const columns = useMemo(() => vipColumns(resourceLabels), [resourceLabels]);
return (
<AdminListPage>
<div className={styles.summaryPanel}>
<div className={styles.summaryItems}>
<SummaryItem label="总等级">{levels.length || 10}</SummaryItem>
<SummaryItem label="启用等级">{page.activeCount}</SummaryItem>
<SummaryItem label="购买门槛"></SummaryItem>
</div>
<div className={styles.summaryActions}>
<Button
disabled={page.loading}
startIcon={<RefreshOutlined fontSize="small" />}
onClick={page.reload}
>
刷新
</Button>
{page.abilities.canUpdate ? (
<Button
disabled={page.loading}
startIcon={<EditOutlined fontSize="small" />}
variant="primary"
onClick={page.openDrawer}
>
编辑配置
</Button>
) : null}
</div>
</div>
<DataState loading={page.loading} onRetry={page.reload}>
<AdminListBody>
<DataTable columns={columns} items={levels} minWidth="1120px" rowKey={(item) => item.level} />
</AdminListBody>
</DataState>
<VipConfigDrawer page={page} />
</AdminListPage>
);
}
function VipConfigDrawer({ page }) {
const disabled = !page.abilities.canUpdate || page.saving || page.loading;
return (
<Drawer anchor="right" open={page.drawerOpen} onClose={page.saving ? undefined : page.closeDrawer}>
<form className="form-drawer form-drawer--wide" onSubmit={page.submit}>
<h2>VIP配置</h2>
<section className="form-drawer__section">
<div className="form-drawer__section-title">等级配置</div>
<div className={styles.levelEditorList}>
{(page.form.levels || []).map((level) => (
<div className={styles.levelEditor} key={level.level}>
<div className={styles.levelEditorHeader}>
<span>VIP{level.level}</span>
<AdminSwitch
checked={level.status === "active"}
checkedLabel="启用"
disabled={disabled}
label="状态"
uncheckedLabel="停用"
onChange={(event) =>
page.updateLevel(level.level, {
status: event.target.checked ? "active" : "disabled",
})
}
/>
</div>
<div className="form-drawer__grid">
<TextField
disabled={disabled}
label="名称"
value={level.name}
onChange={(event) =>
page.updateLevel(level.level, { name: event.target.value })
}
/>
<TextField
disabled={disabled}
inputProps={{ min: 0, step: 1 }}
label="购买金币"
type="number"
value={level.priceCoin}
onChange={(event) =>
page.updateLevel(level.level, { priceCoin: event.target.value })
}
/>
<TextField
disabled={disabled}
inputProps={{ min: 1, step: 1 }}
label="有效天数"
type="number"
value={level.durationDays}
onChange={(event) =>
page.updateLevel(level.level, { durationDays: event.target.value })
}
/>
<ResourceGroupSelectField
disabled={disabled}
drawerTitle={`选择 VIP${level.level} 奖励资源组`}
groups={page.resourceGroups}
label="奖励资源组"
value={level.rewardResourceGroupId}
onChange={(value) =>
page.updateLevel(level.level, { rewardResourceGroupId: value })
}
/>
<TextField
disabled={disabled}
inputProps={{ min: 0, step: 1 }}
label="排序"
type="number"
value={level.sortOrder}
onChange={(event) =>
page.updateLevel(level.level, { sortOrder: event.target.value })
}
/>
</div>
</div>
))}
</div>
</section>
<div className="form-drawer__actions">
<Button disabled={page.saving} type="button" onClick={page.closeDrawer}>
取消
</Button>
<Button
disabled={disabled}
startIcon={<SaveOutlined fontSize="small" />}
type="submit"
variant="primary"
>
保存
</Button>
</div>
</form>
</Drawer>
);
}
function vipColumns(resourceLabels) {
return [
{
key: "level",
label: "等级",
render: (item) => <Stack primary={`VIP${item.level}`} secondary={item.name} />,
width: "minmax(120px, 0.5fr)",
},
{
key: "status",
label: "状态",
render: (item) => (
<span
className={[
styles.statusBadge,
item.status === "active" ? styles.statusActive : styles.statusDisabled,
].join(" ")}
>
{item.status === "active" ? "启用" : "停用"}
</span>
),
width: "minmax(100px, 0.45fr)",
},
{
key: "price",
label: "购买金币",
render: (item) => formatNumber(item.priceCoin),
width: "minmax(140px, 0.6fr)",
},
{
key: "gate",
label: "购买门槛",
render: () => "直接购买",
width: "minmax(180px, 0.8fr)",
},
{
key: "reward",
label: "奖励资源组",
render: (item) =>
resourceLabels[String(item.rewardResourceGroupId)] || `资源组 #${item.rewardResourceGroupId || "-"}`,
width: "minmax(220px, 1fr)",
},
{
key: "duration",
label: "有效期",
render: (item) => `${Math.round(Number(item.durationMs || 0) / 86400000)}`,
width: "minmax(110px, 0.45fr)",
},
{
key: "updatedAtMs",
label: "更新时间",
render: (item) => formatMillis(item.updatedAtMs),
width: "minmax(170px, 0.75fr)",
},
];
}
function SummaryItem({ children, label }) {
return (
<div className={styles.summaryItem}>
<span className={styles.summaryLabel}>{label}</span>
<span className={styles.summaryValue}>{children}</span>
</div>
);
}
function Stack({ primary, secondary }) {
return (
<div className={styles.stack}>
<span>{primary || "-"}</span>
<span className={styles.meta}>{secondary || "-"}</span>
</div>
);
}
function groupLabelMap(groups) {
return (groups || []).reduce((labels, group) => {
labels[String(group.groupId)] = group.name || group.groupCode || `资源组 #${group.groupId}`;
return labels;
}, {});
}
function formatNumber(value) {
const number = Number(value || 0);
return Number.isFinite(number) ? number.toLocaleString("zh-CN") : "-";
}