245 lines
11 KiB
JavaScript
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") : "-";
|
|
}
|