hyapp-admin-platform/src/features/app-users/pages/AppUserPrettyIdsPage.jsx
2026-06-11 13:33:30 +08:00

680 lines
28 KiB
JavaScript

import AddCircleOutlineOutlined from "@mui/icons-material/AddCircleOutlineOutlined";
import AutoAwesomeOutlined from "@mui/icons-material/AutoAwesomeOutlined";
import EditOutlined from "@mui/icons-material/EditOutlined";
import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined";
import SendOutlined from "@mui/icons-material/SendOutlined";
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { AdminFormDialog, AdminFormSection } 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 { IconButton } from "@/shared/ui/IconButton.jsx";
import { useAppUserDetail } from "@/features/app-users/components/AppUserDetailContext.jsx";
import { formatMillis } from "@/shared/utils/time.js";
import {
prettyIdSourceLabels,
prettyIdSourceOptions,
prettyIdStatusLabels,
prettyIdStatusOptions,
prettyLevelTrackOptions,
prettyPoolStatusLabels,
prettyPoolStatusOptions,
prettyRuleOptions,
} from "@/features/app-users/constants.js";
import { usePrettyIdsPage } from "@/features/app-users/hooks/usePrettyIdsPage.js";
import styles from "@/features/app-users/app-users.module.css";
export function AppUserPrettyIdsPage() {
const page = usePrettyIdsPage();
const pools = page.poolQuery.data.items || [];
const prettyIds = page.idQuery.data.items || [];
return (
<section className={styles.root}>
<div className={`${styles.contentPanel} ${styles.prettyContentPanel}`}>
<div className={styles.prettyWorkspace}>
<section className={`${styles.prettyPane} ${styles.prettyPoolPane}`}>
<PaneHeader
actions={
<>
{page.abilities.canPrettyGenerate ? (
<Button
startIcon={<AutoAwesomeOutlined fontSize="small" />}
onClick={() => page.openGenerate(null)}
>
批量生成
</Button>
) : null}
{page.abilities.canPrettyUpdate ? (
<Button
startIcon={<AddCircleOutlineOutlined fontSize="small" />}
variant="primary"
onClick={page.openCreatePool}
>
新增池
</Button>
) : null}
</>
}
count={page.poolQuery.data.total}
title="靓号池"
/>
<div className={styles.prettyFilters}>
<TextField
className={styles.compactControl}
label="等级轨道"
select
size="small"
value={page.poolTrack}
onChange={(event) => page.changePoolTrack(event.target.value)}
>
<MenuItem value="">全部等级轨道</MenuItem>
{prettyLevelTrackOptions.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
<TextField
className={styles.compactControl}
label="状态"
select
size="small"
value={page.poolStatus}
onChange={(event) => page.changePoolStatus(event.target.value)}
>
{prettyPoolStatusOptions.map(([value, label]) => (
<MenuItem key={value || "all"} value={value}>
{label}
</MenuItem>
))}
</TextField>
<Button startIcon={<RestartAltOutlined fontSize="small" />} onClick={page.resetPoolFilters}>
重置
</Button>
</div>
<DataState
error={page.poolQuery.error}
loading={page.poolQuery.loading}
onRetry={page.poolQuery.reload}
>
<DataTable
className={styles.compactTableFrame}
columns={poolColumns(page)}
items={pools}
minWidth="720px"
pagination={
page.poolQuery.data.total > 0
? {
page: page.poolPage,
pageSize: page.poolQuery.data.pageSize || 20,
total: page.poolQuery.data.total,
onPageChange: page.setPoolPage,
}
: undefined
}
rowKey={(pool) => pool.poolId}
/>
</DataState>
</section>
<section className={`${styles.prettyPane} ${styles.prettyIDPane}`}>
<PaneHeader
actions={
page.abilities.canPrettyGrant ? (
<Button
startIcon={<SendOutlined fontSize="small" />}
variant="primary"
onClick={page.openGrant}
>
后台发放
</Button>
) : null
}
count={page.idQuery.data.total}
title="靓号列表"
/>
<div className={styles.prettyFilters}>
<TextField
className={styles.search}
label="搜索"
size="small"
value={page.idKeyword}
onChange={(event) => page.changeIDKeyword(event.target.value)}
/>
<TextField
className={styles.compactControl}
label="靓号池"
select
size="small"
value={page.idPoolId}
onChange={(event) => page.changeIDPool(event.target.value)}
>
<MenuItem value="">全部靓号池</MenuItem>
{page.poolOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<TextField
className={styles.compactControl}
label="来源"
select
size="small"
value={page.idSource}
onChange={(event) => page.changeIDSource(event.target.value)}
>
{prettyIdSourceOptions.map(([value, label]) => (
<MenuItem key={value || "all"} value={value}>
{label}
</MenuItem>
))}
</TextField>
<TextField
className={styles.compactControl}
label="状态"
select
size="small"
value={page.idStatus}
onChange={(event) => page.changeIDStatus(event.target.value)}
>
{prettyIdStatusOptions.map(([value, label]) => (
<MenuItem key={value || "all"} value={value}>
{label}
</MenuItem>
))}
</TextField>
<TextField
className={styles.userIDControl}
label="占用用户"
size="small"
value={page.assignedUserId}
onChange={(event) => page.changeAssignedUserId(event.target.value)}
/>
<Button startIcon={<RestartAltOutlined fontSize="small" />} onClick={page.resetIDFilters}>
重置
</Button>
</div>
<DataState
error={page.idQuery.error}
loading={page.idQuery.loading}
onRetry={page.idQuery.reload}
>
<DataTable
className={styles.compactTableFrame}
columns={idColumns(page)}
items={prettyIds}
minWidth="980px"
pagination={
page.idQuery.data.total > 0
? {
page: page.idPage,
pageSize: page.idQuery.data.pageSize || 20,
total: page.idQuery.data.total,
onPageChange: page.setIdPage,
}
: undefined
}
rowKey={(item) => item.prettyId}
/>
</DataState>
</section>
</div>
</div>
<PoolDialog page={page} />
<GenerateDialog page={page} />
<GrantDialog page={page} />
<StatusDialog page={page} />
</section>
);
}
function PaneHeader({ actions, count, title }) {
return (
<div className={styles.prettyPaneHeader}>
<div className={styles.prettyPaneTitle}>
<h2>{title}</h2>
<span> {Number(count || 0)} </span>
</div>
<div className={styles.toolbarActions}>{actions}</div>
</div>
);
}
function poolColumns(page) {
return [
{
key: "pool",
label: "靓号池",
width: "minmax(220px, 1.2fr)",
render: (pool) => (
<div className={styles.stack}>
<span className={styles.primaryText}>{pool.name || "-"}</span>
<span className={styles.meta}>{pool.poolId}</span>
<span className={styles.meta}>更新 {formatMillis(pool.updatedAtMs || pool.createdAtMs)}</span>
</div>
),
},
{
key: "rule",
label: "规则 / 等级",
width: "minmax(160px, 0.9fr)",
render: (pool) => (
<div className={styles.stack}>
<span>{ruleLabel(pool.ruleType)}</span>
<span className={styles.meta}>
{trackLabel(pool.levelTrack)} {pool.minLevel}-{pool.maxLevel}
</span>
</div>
),
},
{
key: "status",
label: "状态",
width: "minmax(110px, 0.6fr)",
render: (pool) => <StatusBadge labels={prettyPoolStatusLabels} status={pool.status} />,
},
{
key: "actions",
label: "操作",
width: "104px",
render: (pool) => (
<div className={styles.rowActions}>
{page.abilities.canPrettyUpdate ? (
<ActionIcon label="编辑" onClick={() => page.openEditPool(pool)}>
<EditOutlined fontSize="small" />
</ActionIcon>
) : null}
{page.abilities.canPrettyGenerate ? (
<ActionIcon label="生成" onClick={() => page.openGenerate(pool)}>
<AutoAwesomeOutlined fontSize="small" />
</ActionIcon>
) : null}
</div>
),
},
];
}
function idColumns(page) {
return [
{
key: "display",
label: "靓号",
width: "minmax(190px, 1fr)",
render: (item) => (
<div className={styles.stack}>
<div className={styles.nameLine}>
<span className={styles.name}>{item.displayUserId || "-"}</span>
</div>
<div className={styles.meta}>{item.prettyId}</div>
</div>
),
},
{
key: "pool",
label: "来源 / 池",
width: "minmax(180px, 0.9fr)",
render: (item) => (
<div className={styles.stack}>
<span>{prettyIdSourceLabels[item.source] || item.source || "-"}</span>
<span className={styles.meta}>{item.pool?.name || item.poolId || "-"}</span>
</div>
),
},
{
key: "status",
label: "状态",
width: "minmax(110px, 0.6fr)",
render: (item) => <StatusBadge labels={prettyIdStatusLabels} status={item.status} />,
},
{
key: "assigned",
label: "占用 / 时间",
width: "minmax(190px, 0.9fr)",
render: (item) => (
<div className={styles.stack}>
<AssignedUserCell item={item} />
<span className={styles.meta}>
{item.assignedAtMs ? formatMillis(item.assignedAtMs) : formatMillis(item.createdAtMs)}
</span>
<span className={styles.meta}>
{item.releasedAtMs ? `释放 ${formatMillis(item.releasedAtMs)}` : "-"}
</span>
</div>
),
},
{
key: "actions",
label: "操作",
width: "minmax(96px, 0.5fr)",
render: (item) =>
page.abilities.canPrettyUpdate && canChangePrettyStatus(item) ? (
<div className={styles.rowActions}>
<ActionIcon label="状态" onClick={() => page.openStatus(item)}>
<SettingsOutlined fontSize="small" />
</ActionIcon>
</div>
) : (
"-"
),
},
];
}
function AssignedUserCell({ item }) {
const { openAppUserDetail } = useAppUserDetail();
const assignedUserId = formatEntityID(item.assignedUserId);
if (!item.assignedUserId || !openAppUserDetail) {
return <span>{assignedUserId}</span>;
}
return (
<button
className={styles.countryButton}
type="button"
onClick={(event) => {
event.stopPropagation();
openAppUserDetail({
displayUserId: assignedUserId,
prettyDisplayUserId: item.displayUserId,
prettyId: item.prettyId,
userId: item.assignedUserId,
});
}}
>
{assignedUserId}
</button>
);
}
function PoolDialog({ page }) {
const editing = Boolean(page.activePool);
return (
<AdminFormDialog
loading={page.loadingAction === "pool"}
open={page.activeDialog === "pool"}
submitDisabled={!page.abilities.canPrettyUpdate || page.loadingAction === "pool"}
title={editing ? "编辑靓号池" : "新增靓号池"}
onClose={page.closeDialog}
onSubmit={page.submitPool}
>
<AdminFormSection title="池配置">
<TextField
label="名称"
required
value={page.poolForm.name}
onChange={(event) => page.setPoolForm({ ...page.poolForm, name: event.target.value })}
/>
<TextField
label="等级轨道"
required
select
value={page.poolForm.levelTrack}
onChange={(event) => page.setPoolForm({ ...page.poolForm, levelTrack: event.target.value })}
>
{prettyLevelTrackOptions.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
<div className={styles.formGridTwo}>
<TextField
label="最低等级"
required
type="number"
value={page.poolForm.minLevel}
onChange={(event) => page.setPoolForm({ ...page.poolForm, minLevel: event.target.value })}
/>
<TextField
label="最高等级"
required
type="number"
value={page.poolForm.maxLevel}
onChange={(event) => page.setPoolForm({ ...page.poolForm, maxLevel: event.target.value })}
/>
</div>
<div className={styles.formGridTwo}>
<TextField
label="规则"
required
select
value={page.poolForm.ruleType}
onChange={(event) => page.setPoolForm({ ...page.poolForm, ruleType: event.target.value })}
>
{prettyRuleOptions.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
<TextField
label="状态"
required
select
value={page.poolForm.status}
onChange={(event) => page.setPoolForm({ ...page.poolForm, status: event.target.value })}
>
{prettyPoolStatusOptions
.filter(([value]) => value)
.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
</div>
<TextField
label="排序"
type="number"
value={page.poolForm.sortOrder}
onChange={(event) => page.setPoolForm({ ...page.poolForm, sortOrder: event.target.value })}
/>
<TextField
label="规则配置"
minRows={3}
multiline
value={page.poolForm.ruleConfigJson}
onChange={(event) => page.setPoolForm({ ...page.poolForm, ruleConfigJson: event.target.value })}
/>
</AdminFormSection>
</AdminFormDialog>
);
}
function GenerateDialog({ page }) {
return (
<AdminFormDialog
loading={page.loadingAction === "generate"}
open={page.activeDialog === "generate"}
submitDisabled={!page.abilities.canPrettyGenerate || page.loadingAction === "generate"}
title="批量生成靓号"
onClose={page.closeDialog}
onSubmit={page.submitGenerate}
>
<AdminFormSection title="生成配置">
<TextField
label="靓号池"
required
select
value={page.generateForm.poolId}
onChange={(event) => page.setGenerateForm({ ...page.generateForm, poolId: event.target.value })}
>
{page.poolOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<TextField
label="规则"
required
select
value={page.generateForm.ruleType}
onChange={(event) => page.setGenerateForm({ ...page.generateForm, ruleType: event.target.value })}
>
{prettyRuleOptions.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
<TextField
label="生成数量"
required
type="number"
value={page.generateForm.count}
onChange={(event) => page.setGenerateForm({ ...page.generateForm, count: event.target.value })}
/>
<TextField
label="规则配置"
minRows={3}
multiline
value={page.generateForm.ruleConfigJson}
onChange={(event) =>
page.setGenerateForm({ ...page.generateForm, ruleConfigJson: event.target.value })
}
/>
</AdminFormSection>
</AdminFormDialog>
);
}
function GrantDialog({ page }) {
return (
<AdminFormDialog
loading={page.loadingAction === "grant"}
open={page.activeDialog === "grant"}
submitDisabled={!page.abilities.canPrettyGrant || page.loadingAction === "grant"}
title="后台发放靓号"
onClose={page.closeDialog}
onSubmit={page.submitGrant}
>
<AdminFormSection title="发放信息">
<TextField
label="用户 ID"
required
value={page.grantForm.targetUserId}
onChange={(event) => page.setGrantForm({ ...page.grantForm, targetUserId: event.target.value })}
/>
<TextField
label="靓号内容"
required
value={page.grantForm.displayUserId}
onChange={(event) => page.setGrantForm({ ...page.grantForm, displayUserId: event.target.value })}
/>
<TextField
label="有效天数"
type="number"
value={page.grantForm.durationDays}
onChange={(event) => page.setGrantForm({ ...page.grantForm, durationDays: event.target.value })}
/>
<TextField
label="原因"
value={page.grantForm.reason}
onChange={(event) => page.setGrantForm({ ...page.grantForm, reason: event.target.value })}
/>
</AdminFormSection>
</AdminFormDialog>
);
}
function StatusDialog({ page }) {
return (
<AdminFormDialog
loading={page.loadingAction === "status"}
open={page.activeDialog === "status"}
submitDisabled={!page.abilities.canPrettyUpdate || page.loadingAction === "status"}
title="修改靓号状态"
onClose={page.closeDialog}
onSubmit={page.submitStatus}
>
<AdminFormSection title="状态信息">
<TextField disabled label="靓号" value={page.activePrettyID?.displayUserId || ""} />
<TextField
label="状态"
required
select
value={page.statusForm.status}
onChange={(event) => page.setStatusForm({ ...page.statusForm, status: event.target.value })}
>
{prettyIdStatusOptions
.filter(([value]) => ["available", "disabled", "reserved"].includes(value))
.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
<TextField
label="原因"
value={page.statusForm.reason}
onChange={(event) => page.setStatusForm({ ...page.statusForm, reason: event.target.value })}
/>
</AdminFormSection>
</AdminFormDialog>
);
}
function ActionIcon({ children, label, onClick }) {
return (
<Tooltip arrow title={label}>
<span>
<IconButton label={label} onClick={onClick}>
{children}
</IconButton>
</span>
</Tooltip>
);
}
function StatusBadge({ labels, status }) {
return (
<span className={`status-badge status-badge--${statusTone(status)}`}>
<span className="status-point" />
{labels[status] || status || "-"}
</span>
);
}
function statusTone(status) {
if (status === "active" || status === "available") {
return "running";
}
if (status === "assigned" || status === "reserved") {
return "warning";
}
if (status === "released" || status === "deleted") {
return "stopped";
}
return "danger";
}
function trackLabel(track) {
return prettyLevelTrackOptions.find(([value]) => value === track)?.[1] || track || "-";
}
function ruleLabel(ruleType) {
return prettyRuleOptions.find(([value]) => value === ruleType)?.[1] || ruleType || "-";
}
function formatEntityID(value) {
if (value === 0 || value === "0" || value === "" || value === undefined || value === null) {
return "-";
}
return String(value);
}
function canChangePrettyStatus(item) {
const assigned = item.assignedUserId;
const unassigned =
assigned === 0 || assigned === "0" || assigned === "" || assigned === undefined || assigned === null;
return ["available", "disabled", "reserved"].includes(item.status) && unassigned;
}