2026-07-14 17:58:09 +08:00

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");
}