887 lines
36 KiB
JavaScript
887 lines
36 KiB
JavaScript
import Add from "@mui/icons-material/Add";
|
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
|
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
import Tab from "@mui/material/Tab";
|
|
import Tabs from "@mui/material/Tabs";
|
|
import TextField from "@mui/material/TextField";
|
|
import {
|
|
AdminFormDialog,
|
|
AdminFormFieldGrid,
|
|
AdminFormSection,
|
|
AdminFormSwitchField,
|
|
} from "@/shared/ui/AdminFormDialog.jsx";
|
|
import { Button } from "@/shared/ui/Button.jsx";
|
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
|
import { AdminActionIconButton, AdminListBody, AdminListPage, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
|
import { ResourceGroupSelectField } from "@/shared/ui/ResourceGroupSelectDrawer.jsx";
|
|
import { formatMillis } from "@/shared/utils/time.js";
|
|
import { levelStatusLabels, levelTrackLabels, levelTrackOptions } from "@/features/level-config/constants.js";
|
|
import { useLevelConfigPage } from "@/features/level-config/hooks/useLevelConfigPage.js";
|
|
import { ResourceSelectField } from "@/features/resources/components/ResourceSelectDrawer.jsx";
|
|
import { LevelConfigBatchDialog } from "@/features/level-config/components/LevelConfigBatchDialog.jsx";
|
|
import {
|
|
levelAvatarFrameResourcesForTrack,
|
|
levelBadgeResourcesForTrack,
|
|
levelShortBadgeResourcesForTrack,
|
|
withSelectedResource,
|
|
} from "@/features/level-config/resourceFilters.js";
|
|
import styles from "@/features/level-config/level-config.module.css";
|
|
|
|
export function LevelConfigPage() {
|
|
const page = useLevelConfigPage();
|
|
const trackTabs = buildTrackTabs(page.config.tracks);
|
|
|
|
return (
|
|
<AdminListPage>
|
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
|
<AdminListBody>
|
|
<div className={styles.pageBody}>
|
|
<div className={styles.trackTabsBar}>
|
|
<Tabs
|
|
allowScrollButtonsMobile
|
|
className={styles.trackTabs}
|
|
value={page.track}
|
|
variant="scrollable"
|
|
onChange={(_, nextTrack) => page.changeTrack(nextTrack)}
|
|
>
|
|
{trackTabs.map((item) => (
|
|
<Tab
|
|
className={styles.trackTab}
|
|
key={item.value}
|
|
label={
|
|
<span className={styles.trackTabLabel}>
|
|
<span>{item.label}</span>
|
|
<span className={styles.trackTabMeta}>
|
|
{item.ruleCount} 条规则 · {item.tierCount} 个等级层
|
|
</span>
|
|
</span>
|
|
}
|
|
value={item.value}
|
|
/>
|
|
))}
|
|
</Tabs>
|
|
<div className={styles.tabActions}>
|
|
<AdminActionIconButton
|
|
disabled={page.loading}
|
|
label="刷新配置"
|
|
type="button"
|
|
onClick={page.reload}
|
|
>
|
|
<RefreshOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
<Button
|
|
disabled={!page.abilities.canUpdate}
|
|
startIcon={<Add fontSize="small" />}
|
|
type="button"
|
|
variant="primary"
|
|
onClick={() => page.openRuleCreate(page.track)}
|
|
>
|
|
添加规则
|
|
</Button>
|
|
<Button
|
|
disabled={!page.abilities.canUpdate}
|
|
startIcon={<Add fontSize="small" />}
|
|
type="button"
|
|
variant="primary"
|
|
onClick={() => page.openTierCreate(page.track)}
|
|
>
|
|
添加等级层
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{page.tracks.length ? (
|
|
<div className={styles.trackList}>
|
|
{page.tracks.map((track) => (
|
|
<LevelTrackSection key={track.track} page={page} track={track} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className={styles.empty}>当前无数据</div>
|
|
)}
|
|
</div>
|
|
</AdminListBody>
|
|
</DataState>
|
|
<RuleFormDialog page={page} />
|
|
<TierFormDialog page={page} />
|
|
<TrackFormDialog page={page} />
|
|
<LevelConfigBatchDialog page={page} />
|
|
</AdminListPage>
|
|
);
|
|
}
|
|
|
|
function LevelTrackSection({ page, track }) {
|
|
const rules = track.rules || [];
|
|
const tiers = track.tiers || [];
|
|
const totalRules = track.rules?.length || 0;
|
|
const totalTiers = track.tiers?.length || 0;
|
|
const ruleColumns = buildRuleColumns(page);
|
|
const tierColumns = buildTierColumns(page);
|
|
|
|
return (
|
|
<section className={styles.trackSection}>
|
|
<div className={styles.trackHeader}>
|
|
<div className={styles.trackTitleGroup}>
|
|
<div className={styles.trackTitleRow}>
|
|
<span className={styles.trackTitle}>
|
|
{track.name || levelTrackLabels[track.track] || track.track}
|
|
</span>
|
|
</div>
|
|
<div className={styles.trackMetaList}>
|
|
<span>{track.track}</span>
|
|
<span>{totalRules} 条规则</span>
|
|
<span>{totalTiers} 个等级层</span>
|
|
<span>排序 {track.sortOrder || 0}</span>
|
|
</div>
|
|
</div>
|
|
<AdminRowActions>
|
|
<AdminActionIconButton
|
|
disabled={!page.abilities.canUpdate}
|
|
label="编辑等级类型"
|
|
type="button"
|
|
onClick={() => page.openTrackEdit(track)}
|
|
>
|
|
<EditOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
<AdminActionIconButton
|
|
disabled={!page.abilities.canUpdate}
|
|
label="添加本类型等级规则"
|
|
type="button"
|
|
onClick={() => page.openRuleCreate(track.track)}
|
|
>
|
|
<Add fontSize="small" />
|
|
</AdminActionIconButton>
|
|
<AdminActionIconButton
|
|
disabled={!page.abilities.canUpdate}
|
|
label="添加本类型等级层"
|
|
type="button"
|
|
onClick={() => page.openTierCreate(track.track)}
|
|
>
|
|
<Add fontSize="small" />
|
|
</AdminActionIconButton>
|
|
</AdminRowActions>
|
|
</div>
|
|
<div className={styles.tableStack}>
|
|
<div className={styles.tableBlock}>
|
|
<TableBlockHeader
|
|
actions={
|
|
<Button
|
|
disabled={!page.abilities.canUpdate || !rules.length}
|
|
startIcon={<EditOutlined fontSize="small" />}
|
|
onClick={() => page.openRuleBatchEdit(track)}
|
|
>
|
|
批量编辑
|
|
</Button>
|
|
}
|
|
countLabel={`${rules.length} 条`}
|
|
title="每级升级数值"
|
|
/>
|
|
<DataTable
|
|
className={styles.compactTable}
|
|
columns={ruleColumns}
|
|
items={rules}
|
|
minWidth="1180px"
|
|
rowKey={(rule) => `${rule.track}-${rule.level}`}
|
|
/>
|
|
</div>
|
|
<div className={styles.tableBlock}>
|
|
<TableBlockHeader
|
|
actions={
|
|
<Button
|
|
disabled={!page.abilities.canUpdate || !tiers.length}
|
|
startIcon={<EditOutlined fontSize="small" />}
|
|
onClick={() => page.openTierBatchEdit(track)}
|
|
>
|
|
批量编辑
|
|
</Button>
|
|
}
|
|
countLabel={`${tiers.length} 个`}
|
|
title="等级层展示与礼物"
|
|
/>
|
|
<DataTable
|
|
className={styles.compactTable}
|
|
columns={tierColumns}
|
|
items={tiers}
|
|
minWidth="1180px"
|
|
rowKey={(tier) => tier.tierId || `${tier.track}-${tier.minLevel}-${tier.maxLevel}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function TableBlockHeader({ actions, countLabel, title }) {
|
|
return (
|
|
<div className={styles.tableHeader}>
|
|
<div className={styles.tableTitle}>{title}</div>
|
|
<span className={styles.tableCount}>{countLabel}</span>
|
|
{actions ? <div className={styles.tableActions}>{actions}</div> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function buildTrackTabs(tracks = []) {
|
|
return levelTrackOptions.map(([value, label]) => {
|
|
const track = tracks.find((item) => item.track === value);
|
|
return {
|
|
label: track?.name || label,
|
|
ruleCount: track?.rules?.length || 0,
|
|
tierCount: track?.tiers?.length || 0,
|
|
value,
|
|
};
|
|
});
|
|
}
|
|
|
|
function buildRuleColumns(page) {
|
|
return [
|
|
{
|
|
key: "level",
|
|
label: "等级",
|
|
width: "minmax(180px, 0.8fr)",
|
|
render: (rule) => <RuleIdentity rule={rule} />,
|
|
},
|
|
{
|
|
key: "requiredValue",
|
|
label: "升级数值",
|
|
width: "minmax(140px, 0.65fr)",
|
|
render: (rule) => formatNumber(rule.requiredValue),
|
|
},
|
|
{
|
|
key: "longBadge",
|
|
label: "长徽章素材",
|
|
width: "minmax(200px, 1fr)",
|
|
render: (rule) => <ResourceLabel id={rule.longBadgeResourceId} resources={page.badgeResources} />,
|
|
},
|
|
{
|
|
key: "avatarFrame",
|
|
label: "等级头像框",
|
|
width: "minmax(200px, 1fr)",
|
|
render: (rule) => <ResourceLabel id={rule.avatarFrameResourceId} resources={page.avatarFrameResources} />,
|
|
},
|
|
{
|
|
key: "shortBadge",
|
|
label: "短徽章素材",
|
|
width: "minmax(200px, 1fr)",
|
|
render: (rule) => <ResourceLabel id={rule.shortBadgeResourceId} resources={page.badgeResources} />,
|
|
},
|
|
{
|
|
key: "reward",
|
|
label: "升级礼物",
|
|
width: "minmax(200px, 1fr)",
|
|
render: (rule) => <ResourceGroupLabel id={rule.rewardResourceGroupId} groups={page.resourceGroups} />,
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "状态",
|
|
width: "minmax(116px, 0.55fr)",
|
|
render: (rule) => <RuleStatusSwitch page={page} rule={rule} />,
|
|
},
|
|
{
|
|
key: "sort",
|
|
label: "排序",
|
|
width: "minmax(90px, 0.45fr)",
|
|
render: (rule) => rule.sortOrder || 0,
|
|
},
|
|
{
|
|
key: "updatedAt",
|
|
label: "更新时间",
|
|
width: "minmax(170px, 0.75fr)",
|
|
render: (rule) => formatMillis(rule.updatedAtMs || rule.createdAtMs),
|
|
},
|
|
{
|
|
key: "actions",
|
|
label: "操作",
|
|
width: "minmax(76px, 0.35fr)",
|
|
render: (rule) => <RuleRowActions page={page} rule={rule} />,
|
|
},
|
|
];
|
|
}
|
|
|
|
function buildTierColumns(page) {
|
|
return [
|
|
{
|
|
key: "range",
|
|
label: "等级层",
|
|
width: "minmax(220px, 1fr)",
|
|
render: (tier) => <TierIdentity tier={tier} />,
|
|
},
|
|
{
|
|
key: "frame",
|
|
label: "头像框",
|
|
width: "minmax(200px, 0.9fr)",
|
|
render: (tier) => (
|
|
<ResourceLabel id={tier.displayAvatarFrameResourceId} resources={page.avatarFrameResources} />
|
|
),
|
|
},
|
|
{
|
|
key: "badge",
|
|
label: "徽章",
|
|
width: "minmax(200px, 0.9fr)",
|
|
render: (tier) => <ResourceLabel id={tier.displayBadgeResourceId} resources={page.badgeResources} />,
|
|
},
|
|
{
|
|
key: "reward",
|
|
label: "升级礼物",
|
|
width: "minmax(200px, 1fr)",
|
|
render: (tier) => <ResourceGroupLabel id={tier.rewardResourceGroupId} groups={page.resourceGroups} />,
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "状态",
|
|
width: "minmax(116px, 0.55fr)",
|
|
render: (tier) => <TierStatusSwitch page={page} tier={tier} />,
|
|
},
|
|
{
|
|
key: "updatedAt",
|
|
label: "更新时间",
|
|
width: "minmax(170px, 0.75fr)",
|
|
render: (tier) => formatMillis(tier.updatedAtMs || tier.createdAtMs),
|
|
},
|
|
{
|
|
key: "actions",
|
|
label: "操作",
|
|
width: "minmax(76px, 0.35fr)",
|
|
render: (tier) => <TierRowActions page={page} tier={tier} />,
|
|
},
|
|
];
|
|
}
|
|
|
|
function RuleIdentity({ rule }) {
|
|
return (
|
|
<div className={styles.identity}>
|
|
<span className={styles.levelBadge}>Lv.{rule.level}</span>
|
|
<div className={styles.identityText}>
|
|
<span className={styles.name}>{rule.name || `${levelTrackLabels[rule.track]} ${rule.level}级`}</span>
|
|
<span className={styles.meta}>{levelTrackLabels[rule.track] || rule.track}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TierIdentity({ tier }) {
|
|
return (
|
|
<div className={styles.identity}>
|
|
<span className={styles.rangeBadge}>
|
|
{tier.minLevel}-{tier.maxLevel}
|
|
</span>
|
|
<div className={styles.identityText}>
|
|
<span className={styles.name}>{tier.name || `${tier.minLevel}-${tier.maxLevel}级`}</span>
|
|
<span className={styles.meta}>{levelTrackLabels[tier.track] || tier.track}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RuleStatusSwitch({ page, rule }) {
|
|
const checked = rule.status === "active";
|
|
return (
|
|
<div className={styles.statusCell}>
|
|
<AdminSwitch
|
|
checked={checked}
|
|
disabled={!page.abilities.canUpdate || page.loadingAction === `rule-status-${rule.track}-${rule.level}`}
|
|
inputProps={{ "aria-label": checked ? "停用等级规则" : "启用等级规则" }}
|
|
onChange={(event) => page.toggleRule(rule, event.target.checked)}
|
|
/>
|
|
<span className={styles.meta}>{levelStatusLabel(rule.status)}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TierStatusSwitch({ page, tier }) {
|
|
const checked = tier.status === "active";
|
|
return (
|
|
<div className={styles.statusCell}>
|
|
<AdminSwitch
|
|
checked={checked}
|
|
disabled={!page.abilities.canUpdate || page.loadingAction === `tier-status-${tier.tierId}`}
|
|
inputProps={{ "aria-label": checked ? "停用等级层" : "启用等级层" }}
|
|
onChange={(event) => page.toggleTier(tier, event.target.checked)}
|
|
/>
|
|
<span className={styles.meta}>{levelStatusLabel(tier.status)}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RuleRowActions({ page, rule }) {
|
|
return (
|
|
<AdminRowActions>
|
|
<AdminActionIconButton
|
|
disabled={!page.abilities.canUpdate}
|
|
label="编辑等级规则"
|
|
type="button"
|
|
onClick={() => page.openRuleEdit(rule)}
|
|
>
|
|
<EditOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
</AdminRowActions>
|
|
);
|
|
}
|
|
|
|
function TierRowActions({ page, tier }) {
|
|
return (
|
|
<AdminRowActions>
|
|
<AdminActionIconButton
|
|
disabled={!page.abilities.canUpdate}
|
|
label="编辑等级层"
|
|
type="button"
|
|
onClick={() => page.openTierEdit(tier)}
|
|
>
|
|
<EditOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
</AdminRowActions>
|
|
);
|
|
}
|
|
|
|
function RuleFormDialog({ page }) {
|
|
const open = page.activeAction === "rule-create" || page.activeAction === "rule-edit";
|
|
const editing = page.activeAction === "rule-edit";
|
|
const form = page.ruleForm;
|
|
|
|
return (
|
|
<AdminFormDialog
|
|
disabled={!page.abilities.canUpdate}
|
|
loading={page.loadingAction === page.activeAction}
|
|
open={open}
|
|
size="wide"
|
|
submitLabel={editing ? "保存规则" : "创建规则"}
|
|
title={editing ? "编辑等级规则" : "添加等级规则"}
|
|
onClose={page.closeAction}
|
|
onSubmit={page.submitRule}
|
|
>
|
|
<AdminFormSection title="升级配置">
|
|
<AdminFormFieldGrid>
|
|
<TextField
|
|
select
|
|
disabled={editing || !page.abilities.canUpdate}
|
|
label="等级类型"
|
|
value={form.track}
|
|
onChange={(event) =>
|
|
page.setRuleForm((current) => ({
|
|
...current,
|
|
avatarFrameResourceId: "",
|
|
longBadgeResourceId: "",
|
|
shortBadgeResourceId: "",
|
|
track: event.target.value,
|
|
}))
|
|
}
|
|
>
|
|
{levelTrackOptions.map(([value, label]) => (
|
|
<MenuItem key={value} value={value}>
|
|
{label}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<TextField
|
|
disabled={editing || !page.abilities.canUpdate}
|
|
inputProps={{ min: 0 }}
|
|
label="等级"
|
|
type="number"
|
|
value={form.level}
|
|
onChange={changeField(page.setRuleForm, "level")}
|
|
/>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
inputProps={{ min: 0 }}
|
|
label="升级数值"
|
|
type="number"
|
|
value={form.requiredValue}
|
|
onChange={changeField(page.setRuleForm, "requiredValue")}
|
|
/>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
label="展示名称"
|
|
value={form.name}
|
|
onChange={changeField(page.setRuleForm, "name")}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
<AdminFormSection title="展示与礼物">
|
|
<AdminFormFieldGrid>
|
|
<ResourceSelectField
|
|
disabled={!page.abilities.canUpdate}
|
|
drawerTitle="选择长徽章素材"
|
|
label="长徽章素材"
|
|
placeholder="请选择长徽章素材"
|
|
required={false}
|
|
resources={withSelectedResource(
|
|
levelBadgeResourcesForTrack(page.badgeResources, form.track),
|
|
form.longBadgeResourceId,
|
|
)}
|
|
value={form.longBadgeResourceId}
|
|
onChange={(value) =>
|
|
page.setRuleForm((current) => ({ ...current, longBadgeResourceId: value }))
|
|
}
|
|
/>
|
|
<ResourceSelectField
|
|
disabled={!page.abilities.canUpdate}
|
|
drawerTitle="选择等级头像框素材"
|
|
label="等级头像框素材"
|
|
placeholder="请选择等级头像框素材"
|
|
required={false}
|
|
resources={withSelectedResource(
|
|
levelAvatarFrameResourcesForTrack(page.avatarFrameResources, form.track),
|
|
form.avatarFrameResourceId,
|
|
)}
|
|
value={form.avatarFrameResourceId}
|
|
onChange={(value) =>
|
|
page.setRuleForm((current) => ({ ...current, avatarFrameResourceId: value }))
|
|
}
|
|
/>
|
|
<ResourceSelectField
|
|
disabled={!page.abilities.canUpdate}
|
|
drawerTitle="选择短徽章素材"
|
|
label="短徽章素材"
|
|
placeholder="请选择短徽章素材"
|
|
required={false}
|
|
resources={withSelectedResource(
|
|
levelShortBadgeResourcesForTrack(page.badgeResources, form.track),
|
|
form.shortBadgeResourceId,
|
|
)}
|
|
value={form.shortBadgeResourceId}
|
|
onChange={(value) =>
|
|
page.setRuleForm((current) => ({ ...current, shortBadgeResourceId: value }))
|
|
}
|
|
/>
|
|
<ResourceGroupSelectField
|
|
allowEmpty
|
|
disabled={!page.abilities.canUpdate}
|
|
drawerTitle="选择升级礼物"
|
|
groups={page.resourceGroups}
|
|
label="升级礼物"
|
|
placeholder="请选择升级礼物"
|
|
value={form.rewardResourceGroupId}
|
|
onChange={(value) =>
|
|
page.setRuleForm((current) => ({ ...current, rewardResourceGroupId: value }))
|
|
}
|
|
/>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
label="排序"
|
|
type="number"
|
|
value={form.sortOrder}
|
|
onChange={changeField(page.setRuleForm, "sortOrder")}
|
|
/>
|
|
<AdminFormSwitchField
|
|
checked={form.status === "active"}
|
|
checkedLabel="启用"
|
|
disabled={!page.abilities.canUpdate}
|
|
label="状态"
|
|
uncheckedLabel="停用"
|
|
onChange={(checked) =>
|
|
page.setRuleForm((current) => ({ ...current, status: checked ? "active" : "disabled" }))
|
|
}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
</AdminFormDialog>
|
|
);
|
|
}
|
|
|
|
function TierFormDialog({ page }) {
|
|
const open = page.activeAction === "tier-create" || page.activeAction === "tier-edit";
|
|
const editing = page.activeAction === "tier-edit";
|
|
const form = page.tierForm;
|
|
|
|
return (
|
|
<AdminFormDialog
|
|
disabled={!page.abilities.canUpdate}
|
|
loading={page.loadingAction === page.activeAction}
|
|
open={open}
|
|
size="wide"
|
|
submitLabel={editing ? "保存等级层" : "创建等级层"}
|
|
title={editing ? "编辑等级层" : "添加等级层"}
|
|
onClose={page.closeAction}
|
|
onSubmit={page.submitTier}
|
|
>
|
|
<AdminFormSection title="等级范围">
|
|
<AdminFormFieldGrid>
|
|
<TextField
|
|
select
|
|
disabled={editing || !page.abilities.canUpdate}
|
|
label="等级类型"
|
|
value={form.track}
|
|
onChange={(event) =>
|
|
page.setTierForm((current) => ({
|
|
...current,
|
|
displayAvatarFrameResourceId: "",
|
|
displayBadgeResourceId: "",
|
|
track: event.target.value,
|
|
}))
|
|
}
|
|
>
|
|
{levelTrackOptions.map(([value, label]) => (
|
|
<MenuItem key={value} value={value}>
|
|
{label}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
inputProps={{ min: 0 }}
|
|
label="起始等级"
|
|
type="number"
|
|
value={form.minLevel}
|
|
onChange={changeField(page.setTierForm, "minLevel")}
|
|
/>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
inputProps={{ min: 0 }}
|
|
label="结束等级"
|
|
type="number"
|
|
value={form.maxLevel}
|
|
onChange={changeField(page.setTierForm, "maxLevel")}
|
|
/>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
label="等级层名称"
|
|
value={form.name}
|
|
onChange={changeField(page.setTierForm, "name")}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
<AdminFormSection title="展示与礼物">
|
|
<AdminFormFieldGrid>
|
|
<ResourceSelectField
|
|
disabled={!page.abilities.canUpdate}
|
|
drawerTitle="选择头像框素材"
|
|
label="头像框素材"
|
|
placeholder="请选择头像框素材"
|
|
required={false}
|
|
resources={withSelectedResource(
|
|
levelAvatarFrameResourcesForTrack(page.avatarFrameResources, form.track),
|
|
form.displayAvatarFrameResourceId,
|
|
)}
|
|
value={form.displayAvatarFrameResourceId}
|
|
onChange={(value) =>
|
|
page.setTierForm((current) => ({ ...current, displayAvatarFrameResourceId: value }))
|
|
}
|
|
/>
|
|
<ResourceSelectField
|
|
disabled={!page.abilities.canUpdate}
|
|
drawerTitle="选择徽章素材"
|
|
label="徽章素材"
|
|
placeholder="请选择徽章素材"
|
|
required={false}
|
|
resources={withSelectedResource(
|
|
levelBadgeResourcesForTrack(page.badgeResources, form.track),
|
|
form.displayBadgeResourceId,
|
|
)}
|
|
value={form.displayBadgeResourceId}
|
|
onChange={(value) =>
|
|
page.setTierForm((current) => ({ ...current, displayBadgeResourceId: value }))
|
|
}
|
|
/>
|
|
<ResourceGroupSelectField
|
|
allowEmpty
|
|
disabled={!page.abilities.canUpdate}
|
|
drawerTitle="选择升级礼物"
|
|
groups={page.resourceGroups}
|
|
label="升级礼物"
|
|
placeholder="请选择升级礼物"
|
|
value={form.rewardResourceGroupId}
|
|
onChange={(value) =>
|
|
page.setTierForm((current) => ({ ...current, rewardResourceGroupId: value }))
|
|
}
|
|
/>
|
|
<AdminFormSwitchField
|
|
checked={form.status === "active"}
|
|
checkedLabel="启用"
|
|
disabled={!page.abilities.canUpdate}
|
|
label="状态"
|
|
uncheckedLabel="停用"
|
|
onChange={(checked) =>
|
|
page.setTierForm((current) => ({ ...current, status: checked ? "active" : "disabled" }))
|
|
}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
</AdminFormDialog>
|
|
);
|
|
}
|
|
|
|
function TrackFormDialog({ page }) {
|
|
const form = page.trackForm;
|
|
|
|
return (
|
|
<AdminFormDialog
|
|
disabled={!page.abilities.canUpdate}
|
|
loading={page.loadingAction === "track-edit"}
|
|
open={page.activeAction === "track-edit"}
|
|
submitLabel="保存等级类型"
|
|
title="编辑等级类型"
|
|
onClose={page.closeAction}
|
|
onSubmit={page.submitTrack}
|
|
>
|
|
<AdminFormSection title={levelTrackLabels[page.selectedTrack?.track] || "等级类型"}>
|
|
<AdminFormFieldGrid>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
label="名称"
|
|
value={form.name}
|
|
onChange={changeField(page.setTrackForm, "name")}
|
|
/>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
label="排序"
|
|
type="number"
|
|
value={form.sortOrder}
|
|
onChange={changeField(page.setTrackForm, "sortOrder")}
|
|
/>
|
|
<AdminFormSwitchField
|
|
checked={form.status === "active"}
|
|
checkedLabel="启用"
|
|
disabled={!page.abilities.canUpdate}
|
|
label="状态"
|
|
uncheckedLabel="停用"
|
|
onChange={(checked) =>
|
|
page.setTrackForm((current) => ({ ...current, status: checked ? "active" : "disabled" }))
|
|
}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
</AdminFormDialog>
|
|
);
|
|
}
|
|
|
|
function ResourceLabel({ id, resources }) {
|
|
if (!id) {
|
|
return <span className={styles.emptyValue}>未配置</span>;
|
|
}
|
|
const resource = resources.find((item) => Number(item.resourceId) === Number(id));
|
|
const preview = resourceImageURL(resource);
|
|
return (
|
|
<div className={styles.resourceCell}>
|
|
<span className={styles.resourceThumb}>
|
|
{preview ? <img alt="" src={preview} /> : <Inventory2Outlined fontSize="small" />}
|
|
</span>
|
|
<div className={styles.stack}>
|
|
<span className={styles.resourceText}>{resource?.name || resource?.resourceCode || `资源 #${id}`}</span>
|
|
<span className={styles.meta}>{resource ? resourceMetaLabel(resource, id) : `ID ${id}`}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ResourceGroupLabel({ groups, id }) {
|
|
if (!id) {
|
|
return <span className={styles.emptyValue}>未配置</span>;
|
|
}
|
|
const group = groups.find((item) => Number(item.groupId) === Number(id));
|
|
return (
|
|
<div className={styles.stack}>
|
|
<span className={styles.resourceText}>{group?.name || group?.groupCode || `资源组 #${id}`}</span>
|
|
<span className={styles.meta}>ID {id}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function changeField(setter, key) {
|
|
return (event) => {
|
|
const value = event.target.value;
|
|
setter((current) => ({ ...current, [key]: value }));
|
|
};
|
|
}
|
|
|
|
function resourceMetaLabel(resource, id) {
|
|
if (resource.resourceType === "avatar_frame") {
|
|
const avatarFrameKind = resource.avatarFrameKind || avatarFrameKindFromMetadata(resource.metadataJson);
|
|
const levelTrack = resource.levelTrack || levelTrackFromMetadata(resource.metadataJson);
|
|
if (avatarFrameKind === "level" && levelTrack) {
|
|
return `ID ${id} · 等级头像框 · ${levelTrackLabels[levelTrack] || levelTrack}`;
|
|
}
|
|
}
|
|
const badgeForm = resource.badgeForm || badgeFormFromMetadata(resource.metadataJson);
|
|
const badgeKind = resource.badgeKind || badgeKindFromMetadata(resource.metadataJson);
|
|
const levelTrack = resource.levelTrack || levelTrackFromMetadata(resource.metadataJson);
|
|
if (resource.resourceType === "badge" && badgeKind === "level" && levelTrack) {
|
|
const badgeFormLabel = badgeForm === "tile" ? "短徽章" : "长徽章";
|
|
return `ID ${id} · ${badgeFormLabel} · ${levelTrackLabels[levelTrack] || levelTrack}`;
|
|
}
|
|
return `ID ${id}`;
|
|
}
|
|
|
|
function resourceImageURL(resource) {
|
|
return imageURL(resource?.previewUrl) || imageURL(resource?.assetUrl) || imageURL(resource?.animationUrl);
|
|
}
|
|
|
|
function imageURL(value) {
|
|
const url = String(value || "").trim();
|
|
if (!url) {
|
|
return "";
|
|
}
|
|
return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : "";
|
|
}
|
|
|
|
function badgeFormFromMetadata(metadataJson) {
|
|
if (!metadataJson) {
|
|
return "";
|
|
}
|
|
try {
|
|
const metadata = JSON.parse(metadataJson);
|
|
const badgeForm = String(metadata?.badge_form || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
return badgeForm === "strip" || badgeForm === "tile" ? badgeForm : "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function badgeKindFromMetadata(metadataJson) {
|
|
if (!metadataJson) {
|
|
return "";
|
|
}
|
|
try {
|
|
const metadata = JSON.parse(metadataJson);
|
|
return String(metadata?.badge_kind || "")
|
|
.trim()
|
|
.toLowerCase() === "level"
|
|
? "level"
|
|
: "normal";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function avatarFrameKindFromMetadata(metadataJson) {
|
|
if (!metadataJson) {
|
|
return "";
|
|
}
|
|
try {
|
|
const metadata = JSON.parse(metadataJson);
|
|
const avatarFrameKind = String(metadata?.avatar_frame_kind || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
return avatarFrameKind === "level" || avatarFrameKind === "normal" ? avatarFrameKind : "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function levelTrackFromMetadata(metadataJson) {
|
|
if (!metadataJson) {
|
|
return "";
|
|
}
|
|
try {
|
|
const metadata = JSON.parse(metadataJson);
|
|
const levelTrack = String(metadata?.level_track || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
return levelTrack === "wealth" || levelTrack === "game" || levelTrack === "charm" ? levelTrack : "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function levelStatusLabel(status) {
|
|
return levelStatusLabels[status] || status || "-";
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
return Number(value || 0).toLocaleString("zh-CN");
|
|
}
|