78 lines
5.9 KiB
JavaScript
78 lines
5.9 KiB
JavaScript
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||
import GppBadOutlined from "@mui/icons-material/GppBadOutlined";
|
||
import TrendingUpOutlined from "@mui/icons-material/TrendingUpOutlined";
|
||
import Avatar from "@mui/material/Avatar";
|
||
import Box from "@mui/material/Box";
|
||
import Button from "@mui/material/Button";
|
||
import MenuItem from "@mui/material/MenuItem";
|
||
import Stack from "@mui/material/Stack";
|
||
import TextField from "@mui/material/TextField";
|
||
import Typography from "@mui/material/Typography";
|
||
import { useCallback, useMemo, useState } from "react";
|
||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||
import { banUser, grantUserVip, listUsers, updateUser, updateUserLevels } from "../api/business.js";
|
||
import { useExternalAuth } from "../auth/ExternalAuthProvider.jsx";
|
||
import { UserBanDialog, UserEditDialog, UserLevelDialog } from "../components/UserDialogs.jsx";
|
||
import { hasExternalCapability } from "../config/capabilities.js";
|
||
import { ExternalPage, ExternalPageState, PagePagination, ResponsiveDataList, StatusChip } from "../shared/PageComponents.jsx";
|
||
import { usePagedResource } from "../shared/usePagedResource.js";
|
||
|
||
export default function UsersPage() {
|
||
const { session } = useExternalAuth();
|
||
const { showToast } = useToast();
|
||
const [filters, setFilters] = useState({ keyword: "", status: "" });
|
||
const [query, setQuery] = useState({ keyword: "", page: 1, status: "" });
|
||
const [dialog, setDialog] = useState({ type: "", user: null });
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const load = useCallback(() => listUsers({ keyword: query.keyword, page: query.page, page_size: 20, status: query.status }), [query]);
|
||
const { data, error, loading, reload } = usePagedResource(load);
|
||
const canEdit = hasExternalCapability(session, "user:update");
|
||
const canBan = hasExternalCapability(session, "user:ban");
|
||
const canGrantLevel = hasExternalCapability(session, "user-level:grant");
|
||
|
||
const closeDialog = () => setDialog({ type: "", user: null });
|
||
const execute = async (request, successMessage) => {
|
||
setSubmitting(true);
|
||
try {
|
||
await request();
|
||
showToast(successMessage, "success");
|
||
closeDialog();
|
||
reload();
|
||
} catch (requestError) {
|
||
showToast(requestError.message || "用户操作失败", "error");
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const columns = useMemo(() => [
|
||
{ key: "user", label: "用户", render: (user) => <Stack alignItems="center" direction="row" spacing={1}><Avatar src={user.avatar}>{user.username.slice(0, 1)}</Avatar><Box><Typography fontWeight={650}>{user.username || "-"}</Typography><Typography color="text.secondary" variant="caption">ID {user.id}</Typography></Box></Stack> },
|
||
{ key: "prettyId", label: "短 ID(靓号)", render: (user) => user.prettyId || "-" },
|
||
{ key: "country", label: "国家/性别", render: (user) => `${user.country || "-"} / ${user.gender || "-"}` },
|
||
{ key: "level", label: "等级", render: (user) => `财富 ${user.wealthLevel} / 魅力 ${user.charmLevel} / VIP ${user.vipLevel}` },
|
||
{ key: "status", label: "状态", render: (user) => <StatusChip status={user.banned ? "banned" : user.status} /> },
|
||
{ key: "actions", label: "操作", render: (user) => <Stack direction="row" flexWrap="wrap" gap={0.5}>{canEdit ? <Button onClick={() => setDialog({ type: "edit", user })} startIcon={<EditOutlined />}>编辑</Button> : null}{canGrantLevel ? <Button onClick={() => setDialog({ type: "level", user })} startIcon={<TrendingUpOutlined />}>等级</Button> : null}{canBan && !user.banned ? <Button color="error" onClick={() => setDialog({ type: "ban", user })} startIcon={<GppBadOutlined />}>封禁</Button> : null}</Stack> }
|
||
], [canBan, canEdit, canGrantLevel]);
|
||
|
||
return (
|
||
<ExternalPage title="用户管理">
|
||
<Stack className="external-filter-bar" component="form" direction={{ xs: "column", sm: "row" }} spacing={1.5} onSubmit={(event) => { event.preventDefault(); setQuery({ ...filters, page: 1 }); }}>
|
||
<TextField label="用户 ID / 短 ID / 名称" value={filters.keyword} onChange={(event) => setFilters({ ...filters, keyword: event.target.value })} />
|
||
<TextField label="状态" select value={filters.status} onChange={(event) => setFilters({ ...filters, status: event.target.value })}><MenuItem value="">全部状态</MenuItem><MenuItem value="active">正常</MenuItem><MenuItem value="banned">已封禁</MenuItem></TextField>
|
||
<Button type="submit" variant="contained">查询</Button>
|
||
</Stack>
|
||
<ExternalPageState error={error} loading={loading} onRetry={reload} />
|
||
{!loading && !error ? <><ResponsiveDataList columns={columns} items={data.items} rowKey={(item) => item.id} /><PagePagination {...data} onChange={(page) => setQuery({ ...query, page })} /></> : null}
|
||
<UserEditDialog loading={submitting} open={dialog.type === "edit"} user={dialog.user} onClose={closeDialog} onSubmit={(form) => execute(() => updateUser(dialog.user.id, form), "用户信息已更新")} />
|
||
<UserBanDialog loading={submitting} open={dialog.type === "ban"} user={dialog.user} onClose={closeDialog} onSubmit={(form) => execute(() => banUser(dialog.user.id, { expires_at_ms: Number(form.days) === 0 ? 0 : Date.now() + Number(form.days) * 86400000, reason: form.reason.trim() }), "用户已封禁")} />
|
||
<UserLevelDialog loading={submitting} open={dialog.type === "level"} user={dialog.user} onClose={closeDialog} onSubmit={(form) => execute(() => form.track === "vip"
|
||
? grantUserVip(dialog.user.id, { commandId: createLevelCommandId(), durationMs: Number(form.days) * 86400000, level: Number(form.level), reason: form.reason.trim() })
|
||
: updateUserLevels(dialog.user.id, { adjustments: [{ duration_days: Number(form.days), level: Number(form.level), track: form.track }], reason: form.reason.trim() }), "用户等级已下发")} />
|
||
</ExternalPage>
|
||
);
|
||
}
|
||
|
||
function createLevelCommandId() {
|
||
return `external-level-grant-${globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`}`;
|
||
}
|