660 lines
27 KiB
JavaScript
660 lines
27 KiB
JavaScript
import BlockOutlined from "@mui/icons-material/BlockOutlined";
|
||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||
import FileDownloadOutlined from "@mui/icons-material/FileDownloadOutlined";
|
||
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined";
|
||
import KeyboardArrowUpOutlined from "@mui/icons-material/KeyboardArrowUpOutlined";
|
||
import LockOpenOutlined from "@mui/icons-material/LockOpenOutlined";
|
||
import PasswordOutlined from "@mui/icons-material/PasswordOutlined";
|
||
import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined";
|
||
import UnfoldMoreOutlined from "@mui/icons-material/UnfoldMoreOutlined";
|
||
import Autocomplete from "@mui/material/Autocomplete";
|
||
import Checkbox from "@mui/material/Checkbox";
|
||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||
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 { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.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 { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
|
||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||
import { formatMillis } from "@/shared/utils/time.js";
|
||
import { appUserStatusFilters, appUserStatusLabels, genderOptions } from "@/features/app-users/constants.js";
|
||
import {
|
||
BanCountdown,
|
||
GenderValue,
|
||
LevelValue,
|
||
OperatorValue,
|
||
RolesValue,
|
||
} from "@/features/app-users/components/AppUserValues.jsx";
|
||
import { useAppUsersPage } from "@/features/app-users/hooks/useAppUsersPage.js";
|
||
import { CoinLedgerTable } from "@/features/operations/components/CoinLedgerTable.jsx";
|
||
import styles from "@/features/app-users/app-users.module.css";
|
||
|
||
export function AppUserListPage() {
|
||
const page = useAppUsersPage();
|
||
const items = page.data.items || [];
|
||
const total = page.data.total || 0;
|
||
const selectableItems = items.filter((user) => !isBannedUser(user));
|
||
const allChecked =
|
||
selectableItems.length > 0 && selectableItems.every((user) => page.selectedUserIds.includes(user.userId));
|
||
const selectedOnPageCount = selectableItems.filter((user) => page.selectedUserIds.includes(user.userId)).length;
|
||
const someChecked = selectedOnPageCount > 0 && !allChecked;
|
||
const toggleAll = (checked) => {
|
||
if (!checked) {
|
||
page.setSelectedUserIds((current) =>
|
||
current.filter((userId) => !selectableItems.some((user) => user.userId === userId)),
|
||
);
|
||
return;
|
||
}
|
||
page.setSelectedUserIds((current) =>
|
||
Array.from(new Set([...current, ...selectableItems.map((user) => user.userId)])),
|
||
);
|
||
};
|
||
const tableColumns = [
|
||
{
|
||
key: "select",
|
||
className: styles.selectColumn,
|
||
header: (
|
||
<Checkbox
|
||
checked={allChecked}
|
||
disabled={!page.abilities.canStatus || selectableItems.length === 0}
|
||
indeterminate={someChecked}
|
||
size="small"
|
||
onChange={(event) => toggleAll(event.target.checked)}
|
||
/>
|
||
),
|
||
resizable: false,
|
||
width: "64px",
|
||
render: (user) => {
|
||
const banned = isBannedUser(user);
|
||
return (
|
||
<Checkbox
|
||
checked={page.selectedUserIds.includes(user.userId)}
|
||
disabled={!page.abilities.canStatus || banned || page.loadingAction === "batch-ban"}
|
||
size="small"
|
||
onChange={(event) => {
|
||
page.setSelectedUserIds((current) =>
|
||
event.target.checked
|
||
? Array.from(new Set([...current, user.userId]))
|
||
: current.filter((userId) => userId !== user.userId),
|
||
);
|
||
}}
|
||
/>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
key: "identity",
|
||
label: "用户",
|
||
width: "minmax(240px, 1.5fr)",
|
||
filter: createUserIdentityColumnFilter({
|
||
value: page.userFilter,
|
||
onChange: page.changeUserFilter,
|
||
}),
|
||
render: (user) => <AdminUserIdentity openInAppUserDetail user={user} />,
|
||
},
|
||
{
|
||
key: "gender",
|
||
label: "性别",
|
||
width: "minmax(72px, 0.4fr)",
|
||
render: (user) => <GenderValue gender={user.gender} />,
|
||
},
|
||
{
|
||
key: "age",
|
||
label: "年龄",
|
||
width: "minmax(72px, 0.4fr)",
|
||
render: (user) => (user.age === undefined || user.age === null ? "-" : `${user.age} 岁`),
|
||
},
|
||
{
|
||
key: "wealthLevel",
|
||
label: "富豪等级",
|
||
width: "minmax(118px, 0.7fr)",
|
||
render: (user) => <LevelValue level={user.levels?.wealth} />,
|
||
},
|
||
{
|
||
key: "charmLevel",
|
||
label: "魅力等级",
|
||
width: "minmax(118px, 0.7fr)",
|
||
render: (user) => <LevelValue level={user.levels?.charm} />,
|
||
},
|
||
{
|
||
key: "gameLevel",
|
||
label: "游戏等级",
|
||
width: "minmax(96px, 0.55fr)",
|
||
render: (user) => <LevelValue level={user.levels?.game} />,
|
||
},
|
||
{
|
||
key: "roles",
|
||
label: "身份",
|
||
width: "minmax(150px, 0.9fr)",
|
||
render: (user) => <RolesValue roles={user.roles} />,
|
||
},
|
||
{
|
||
key: "vip",
|
||
label: "VIP",
|
||
width: "minmax(80px, 0.45fr)",
|
||
render: (user) => <VipValue vip={user.vip} />,
|
||
},
|
||
{
|
||
key: "location",
|
||
label: "国家 / 区域",
|
||
width: "minmax(170px, 1fr)",
|
||
filter: createOptionsColumnFilter({
|
||
emptyLabel: "全部国家 / 区域",
|
||
loading: page.loadingCountries || page.loadingRegions,
|
||
options: page.locationFilterOptions,
|
||
placeholder: "搜索国家或区域",
|
||
value: page.locationFilterValue,
|
||
onChange: page.changeLocationFilter,
|
||
}),
|
||
render: (user) => <UserLocation page={page} user={user} />,
|
||
},
|
||
{
|
||
key: "coin",
|
||
header: (
|
||
<SortHeader
|
||
field="coin"
|
||
label="金币"
|
||
sortBy={page.sortBy}
|
||
sortDirection={page.sortDirection}
|
||
onClick={page.changeSort}
|
||
/>
|
||
),
|
||
label: "金币",
|
||
width: "minmax(90px, 0.6fr)",
|
||
render: (user) => <CoinValue page={page} user={user} />,
|
||
},
|
||
{
|
||
key: "status",
|
||
label: "状态",
|
||
width: "minmax(92px, 0.6fr)",
|
||
filter: createOptionsColumnFilter({
|
||
options: appUserStatusFilters,
|
||
placeholder: "搜索状态",
|
||
value: page.status,
|
||
onChange: page.changeStatus,
|
||
}),
|
||
render: (user) => (
|
||
<div className={styles.stack}>
|
||
<UserStatus status={user.status} />
|
||
<BanCountdown ban={user.ban} status={user.status} />
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: "lastLoginAtMs",
|
||
label: "最新成功登录",
|
||
width: "minmax(160px, 0.9fr)",
|
||
render: (user) => formatMillis(user.lastLoginAtMs),
|
||
},
|
||
{
|
||
key: "lastOperator",
|
||
label: "最新操作人",
|
||
width: "minmax(142px, 0.8fr)",
|
||
render: (user) => <OperatorValue operator={user.lastOperator} />,
|
||
},
|
||
{
|
||
key: "lastOperatedAtMs",
|
||
label: "最新操作时间",
|
||
width: "minmax(160px, 0.9fr)",
|
||
render: (user) => formatMillis(user.lastOperatedAtMs),
|
||
},
|
||
{
|
||
key: "time",
|
||
header: (
|
||
<SortHeader
|
||
field="created_at"
|
||
label="创建 / 活跃"
|
||
sortBy={page.sortBy}
|
||
sortDirection={page.sortDirection}
|
||
onClick={page.changeSort}
|
||
/>
|
||
),
|
||
label: "创建 / 活跃",
|
||
width: "minmax(180px, 1fr)",
|
||
render: (user) => (
|
||
<div className={styles.stack}>
|
||
<span>{formatMillis(user.createdAtMs)}</span>
|
||
<span className={styles.meta}>{formatMillis(user.lastActiveAtMs)}</span>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: "actions",
|
||
label: "操作",
|
||
width: "minmax(148px, 0.8fr)",
|
||
render: (user) => <UserActions page={page} user={user} />,
|
||
},
|
||
];
|
||
|
||
return (
|
||
<section className={styles.root}>
|
||
<div className={styles.contentPanel}>
|
||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||
<div className={styles.listBlock}>
|
||
<div className={styles.toolbar}>
|
||
<div className={styles.toolbarLeft}>
|
||
{page.abilities.canStatus ? (
|
||
<span className={styles.selectedMeta}>已选 {page.selectedUserIds.length} 个</span>
|
||
) : null}
|
||
<TimeRangeFilter
|
||
className={styles.timeFilter}
|
||
label="创建时间"
|
||
value={page.timeRange}
|
||
onChange={page.changeTimeRange}
|
||
/>
|
||
</div>
|
||
<div className={styles.toolbarActions}>
|
||
<Button startIcon={<RestartAltOutlined fontSize="small" />} onClick={page.resetFilters}>
|
||
重置筛选
|
||
</Button>
|
||
{page.abilities.canExport ? (
|
||
<Button
|
||
disabled={page.loadingAction === "export"}
|
||
startIcon={<FileDownloadOutlined fontSize="small" />}
|
||
onClick={page.exportUsers}
|
||
>
|
||
{page.loadingAction === "export" ? "导出中" : "导出 CSV"}
|
||
</Button>
|
||
) : null}
|
||
{page.abilities.canStatus ? (
|
||
<Button
|
||
disabled={!page.selectedUserIds.length || page.loadingAction === "batch-ban"}
|
||
startIcon={<BlockOutlined fontSize="small" />}
|
||
variant="danger"
|
||
onClick={page.batchBan}
|
||
>
|
||
批量封禁
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<DataTable
|
||
columns={tableColumns}
|
||
items={items}
|
||
minWidth="2380px"
|
||
pagination={
|
||
total > 0
|
||
? {
|
||
page: page.page,
|
||
pageSize: page.data.pageSize || 50,
|
||
total,
|
||
onPageChange: page.setPage,
|
||
}
|
||
: undefined
|
||
}
|
||
rowKey={(user) => user.userId}
|
||
/>
|
||
</div>
|
||
</DataState>
|
||
</div>
|
||
|
||
<AdminFormDialog
|
||
loading={page.loadingAction === "edit"}
|
||
open={page.activeAction === "edit"}
|
||
size="standard"
|
||
submitDisabled={!page.abilities.canUpdate || page.loadingAction === "edit"}
|
||
submitLabel="保存资料"
|
||
title="编辑用户"
|
||
onClose={page.closeAction}
|
||
onSubmit={page.abilities.canUpdate ? page.submitEdit : (event) => event.preventDefault()}
|
||
>
|
||
<AdminFormSection title="用户资料">
|
||
<TextField
|
||
disabled={!page.abilities.canUpdate}
|
||
label="用户名称"
|
||
value={page.editForm.username}
|
||
onChange={(event) => page.setEditForm({ ...page.editForm, username: event.target.value })}
|
||
/>
|
||
<UploadField
|
||
disabled={!page.abilities.canUpdate || !page.abilities.canUpload}
|
||
label="头像"
|
||
value={page.editForm.avatar}
|
||
onChange={(avatar) => page.setEditForm({ ...page.editForm, avatar })}
|
||
/>
|
||
<TextField
|
||
disabled={!page.abilities.canUpdate}
|
||
label="性别"
|
||
select
|
||
value={page.editForm.gender}
|
||
onChange={(event) => page.setEditForm({ ...page.editForm, gender: event.target.value })}
|
||
>
|
||
{genderOptions.map(([value, label]) => (
|
||
<MenuItem key={value || "empty"} value={value}>
|
||
{label}
|
||
</MenuItem>
|
||
))}
|
||
</TextField>
|
||
</AdminFormSection>
|
||
<AdminFormSection
|
||
actions={
|
||
<Button
|
||
disabled={!page.abilities.canLevel || page.loadingAction === "levels"}
|
||
variant="primary"
|
||
onClick={page.submitLevels}
|
||
>
|
||
{page.loadingAction === "levels" ? "保存中" : "保存等级"}
|
||
</Button>
|
||
}
|
||
title="等级调整"
|
||
>
|
||
<LevelAdjustmentFields page={page} />
|
||
</AdminFormSection>
|
||
</AdminFormDialog>
|
||
|
||
<ActionModal
|
||
disabled={!page.abilities.canStatus}
|
||
loading={page.loadingAction === `status-${page.activeUser?.userId}`}
|
||
open={page.activeAction === "ban"}
|
||
sectionTitle="封禁信息"
|
||
title="封禁用户"
|
||
onClose={page.closeAction}
|
||
onSubmit={page.submitBan}
|
||
>
|
||
<TextField
|
||
disabled={!page.abilities.canStatus}
|
||
label="封禁期限"
|
||
select
|
||
value={page.banForm.mode}
|
||
onChange={(event) => page.setBanForm({ ...page.banForm, mode: event.target.value })}
|
||
>
|
||
<MenuItem value="permanent">永久</MenuItem>
|
||
<MenuItem value="timed">指定截止时间</MenuItem>
|
||
</TextField>
|
||
{page.banForm.mode === "timed" ? (
|
||
<TextField
|
||
disabled={!page.abilities.canStatus}
|
||
label="封禁截止时间"
|
||
slotProps={{ inputLabel: { shrink: true } }}
|
||
type="datetime-local"
|
||
value={page.banForm.expiresAtLocal}
|
||
onChange={(event) => page.setBanForm({ ...page.banForm, expiresAtLocal: event.target.value })}
|
||
/>
|
||
) : null}
|
||
<TextField
|
||
disabled={!page.abilities.canStatus}
|
||
label="封禁原因"
|
||
multiline
|
||
minRows={2}
|
||
value={page.banForm.reason}
|
||
onChange={(event) => page.setBanForm({ ...page.banForm, reason: event.target.value })}
|
||
/>
|
||
</ActionModal>
|
||
|
||
<ActionModal
|
||
disabled={!page.abilities.canUpdate}
|
||
loading={page.loadingAction === "country"}
|
||
open={page.activeAction === "country"}
|
||
sectionTitle="国家信息"
|
||
title="修改国家"
|
||
onClose={page.closeAction}
|
||
onSubmit={page.submitCountry}
|
||
>
|
||
<Autocomplete
|
||
disabled={!page.abilities.canUpdate || page.loadingCountries}
|
||
getOptionLabel={(option) => option.label || ""}
|
||
isOptionEqualToValue={(option, value) => option.value === value.value}
|
||
loading={page.loadingCountries}
|
||
options={page.countryOptions}
|
||
renderInput={(params) => <TextField {...params} label="国家" required />}
|
||
size="small"
|
||
value={page.countryOptions.find((option) => option.value === page.countryForm.country) || null}
|
||
onChange={(_, option) => page.setCountryForm({ country: option?.value || "" })}
|
||
/>
|
||
</ActionModal>
|
||
|
||
<ActionModal
|
||
disabled={!page.abilities.canPassword}
|
||
loading={page.loadingAction === "password"}
|
||
open={page.activeAction === "password"}
|
||
sectionTitle="密码信息"
|
||
title="设置密码"
|
||
onClose={page.closeAction}
|
||
onSubmit={page.submitPassword}
|
||
>
|
||
<TextField
|
||
disabled={!page.abilities.canPassword}
|
||
label="新密码"
|
||
type="password"
|
||
value={page.passwordForm.password}
|
||
onChange={(event) => page.setPasswordForm({ ...page.passwordForm, password: event.target.value })}
|
||
/>
|
||
</ActionModal>
|
||
|
||
<SideDrawer
|
||
className={styles.coinLedgerDrawer}
|
||
contentClassName={styles.coinLedgerDrawerBody}
|
||
open={page.activeAction === "coin-ledger"}
|
||
title={`金币流水 - ${userLedgerTitle(page.activeUser)}`}
|
||
width="wide"
|
||
onClose={page.closeAction}
|
||
>
|
||
{page.activeUser ? (
|
||
<CoinLedgerTable lockedUser={page.activeUser} userId={page.activeUser.userId} />
|
||
) : null}
|
||
</SideDrawer>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function CoinValue({ page, user }) {
|
||
const value = formatNumber(user.coin);
|
||
if (!page.abilities.canCoinLedger) {
|
||
return value;
|
||
}
|
||
return (
|
||
<button
|
||
aria-label={`查看 ${userLedgerTitle(user)} 的金币流水`}
|
||
className={styles.coinButton}
|
||
type="button"
|
||
onClick={() => page.openCoinLedger(user)}
|
||
>
|
||
{value}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function LevelAdjustmentFields({ page }) {
|
||
const tracks = [
|
||
["wealth", "富豪等级"],
|
||
["charm", "魅力等级"],
|
||
];
|
||
const updateTrack = (track, patch) => {
|
||
page.setLevelForm((current) => ({
|
||
...current,
|
||
[track]: { ...current[track], ...patch },
|
||
}));
|
||
};
|
||
return (
|
||
<div className={styles.levelForm}>
|
||
{tracks.map(([track, label]) => {
|
||
const field = page.levelForm[track];
|
||
const current = page.activeUser?.levels?.[track];
|
||
return (
|
||
<div className={styles.levelTrackForm} key={track}>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={field.enabled}
|
||
disabled={!page.abilities.canLevel}
|
||
onChange={(event) => updateTrack(track, { enabled: event.target.checked })}
|
||
/>
|
||
}
|
||
label={`${label}(真实 Lv${Number(current?.realLevel || 0)},展示 Lv${Number(current?.displayLevel || 0)})`}
|
||
/>
|
||
<div className={styles.formGridTwo}>
|
||
<TextField
|
||
disabled={!page.abilities.canLevel || !field.enabled}
|
||
label="目标等级"
|
||
slotProps={{ htmlInput: { max: 50, min: 1, step: 1 } }}
|
||
type="number"
|
||
value={field.level}
|
||
onChange={(event) => updateTrack(track, { level: event.target.value })}
|
||
/>
|
||
<TextField
|
||
disabled={!page.abilities.canLevel || !field.enabled}
|
||
label="有效天数"
|
||
slotProps={{ htmlInput: { max: 36500, min: 1, step: 1 } }}
|
||
type="number"
|
||
value={field.durationDays}
|
||
onChange={(event) => updateTrack(track, { durationDays: event.target.value })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
<TextField
|
||
disabled
|
||
label="游戏等级(只读)"
|
||
value={`Lv${Number(page.activeUser?.levels?.game?.displayLevel || 0)}`}
|
||
/>
|
||
<TextField
|
||
disabled={!page.abilities.canLevel}
|
||
label="调整原因"
|
||
value={page.levelForm.reason}
|
||
onChange={(event) => page.setLevelForm((current) => ({ ...current, reason: event.target.value }))}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function VipValue({ vip }) {
|
||
const level = Number(vip?.level || 0);
|
||
if (level <= 0 || !vip?.active) {
|
||
return <span className={styles.vipBadgeMuted}>VIP 0</span>;
|
||
}
|
||
return <span className={styles.vipBadge}>VIP {level}</span>;
|
||
}
|
||
|
||
function UserLocation({ page, user }) {
|
||
const countryLabel = formatCountry(user);
|
||
const regionLabel = user.regionName || (user.regionId ? `区域 ${user.regionId}` : "-");
|
||
return (
|
||
<div className={styles.stack}>
|
||
{page.abilities.canUpdate ? (
|
||
<button className={styles.countryButton} type="button" onClick={() => page.openCountry(user)}>
|
||
{countryLabel}
|
||
</button>
|
||
) : (
|
||
<span className={styles.primaryText}>{countryLabel}</span>
|
||
)}
|
||
<span className={styles.meta}>{regionLabel}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function UserActions({ page, user }) {
|
||
const banned = user.status === "banned" || user.status === "disabled";
|
||
return (
|
||
<div className={styles.rowActions}>
|
||
{page.abilities.canUpdate || page.abilities.canLevel ? (
|
||
<Tooltip arrow title={page.abilities.canUpdate ? "编辑" : "等级调整"}>
|
||
<span>
|
||
<IconButton
|
||
label={page.abilities.canUpdate ? "编辑" : "等级调整"}
|
||
onClick={() => page.openEdit(user)}
|
||
>
|
||
<EditOutlined fontSize="small" />
|
||
</IconButton>
|
||
</span>
|
||
</Tooltip>
|
||
) : null}
|
||
{page.abilities.canStatus ? (
|
||
<Tooltip arrow title={banned ? "解封" : "封禁"}>
|
||
<span>
|
||
<IconButton
|
||
disabled={page.loadingAction === `status-${user.userId}`}
|
||
label={banned ? "解封" : "封禁"}
|
||
tone={banned ? "success" : "danger"}
|
||
onClick={() => page.toggleBan(user)}
|
||
>
|
||
{banned ? <LockOpenOutlined fontSize="small" /> : <BlockOutlined fontSize="small" />}
|
||
</IconButton>
|
||
</span>
|
||
</Tooltip>
|
||
) : null}
|
||
{page.abilities.canPassword ? (
|
||
<Tooltip arrow title="设置密码">
|
||
<span>
|
||
<IconButton label="设置密码" onClick={() => page.openPassword(user)}>
|
||
<PasswordOutlined fontSize="small" />
|
||
</IconButton>
|
||
</span>
|
||
</Tooltip>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function UserStatus({ status }) {
|
||
const tone = status === "active" ? "running" : "danger";
|
||
return (
|
||
<span className={`status-badge status-badge--${tone}`}>
|
||
<span className="status-point" />
|
||
{appUserStatusLabels[status] || status || "-"}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function SortHeader({ field, label, onClick, sortBy, sortDirection }) {
|
||
const active = sortBy === field;
|
||
const nextDirection = active && sortDirection === "desc" ? "asc" : "desc";
|
||
const Icon = !active
|
||
? UnfoldMoreOutlined
|
||
: sortDirection === "asc"
|
||
? KeyboardArrowUpOutlined
|
||
: KeyboardArrowDownOutlined;
|
||
return (
|
||
<button
|
||
aria-label={`按${label}${nextDirection === "asc" ? "正序" : "倒序"}排序`}
|
||
className={[styles.sortHeader, active ? styles.sortHeaderActive : ""].filter(Boolean).join(" ")}
|
||
type="button"
|
||
onClick={() => onClick(field)}
|
||
>
|
||
<span>{label}</span>
|
||
<Icon className={styles.sortIcon} fontSize="inherit" />
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function ActionModal({ children, disabled, loading, onClose, onSubmit, open, sectionTitle = "基本信息", title }) {
|
||
return (
|
||
<AdminFormDialog
|
||
loading={loading}
|
||
open={open}
|
||
size="compact"
|
||
submitDisabled={disabled || loading}
|
||
title={title}
|
||
onClose={onClose}
|
||
onSubmit={onSubmit}
|
||
>
|
||
<AdminFormSection title={sectionTitle}>{children}</AdminFormSection>
|
||
</AdminFormDialog>
|
||
);
|
||
}
|
||
|
||
function formatNumber(value) {
|
||
return Number(value || 0).toLocaleString("zh-CN");
|
||
}
|
||
|
||
function userLedgerTitle(user) {
|
||
if (!user) {
|
||
return "用户";
|
||
}
|
||
return user.username || user.displayUserId || user.prettyDisplayUserId || user.userId || "用户";
|
||
}
|
||
|
||
function formatCountry(user) {
|
||
return user.countryDisplayName || user.countryName || user.country || "-";
|
||
}
|
||
|
||
function isBannedUser(user) {
|
||
return user.status === "banned" || user.status === "disabled";
|
||
}
|