优化靓号页面展示
This commit is contained in:
parent
8c9f5d35b2
commit
63d4ffbacd
@ -60,6 +60,68 @@ test("listPrettyDisplayIDs uses admin endpoint and normalizes snake case fields"
|
||||
});
|
||||
});
|
||||
|
||||
test("listPrettyDisplayIDs normalizes assigned user and operator snapshots", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
assigned_user: {
|
||||
avatar: "https://cdn.example/u.png",
|
||||
default_display_user_id: "10001",
|
||||
display_user_id: "777777",
|
||||
user_id: "9001",
|
||||
username: "pretty owner",
|
||||
},
|
||||
assigned_user_id: "9001",
|
||||
created_by_admin_id: "7",
|
||||
display_user_id: "777777",
|
||||
operator: {
|
||||
account: "ops",
|
||||
admin_id: "8",
|
||||
name: "运营",
|
||||
type: "admin",
|
||||
},
|
||||
pretty_id: "pretty-2",
|
||||
source: "admin_grant",
|
||||
status: "assigned",
|
||||
updated_by_admin_id: "8",
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await listPrettyDisplayIDs({ page: 1, page_size: 20 });
|
||||
|
||||
expect(result.items[0]).toMatchObject({
|
||||
assignedUser: {
|
||||
avatar: "https://cdn.example/u.png",
|
||||
defaultDisplayUserId: "10001",
|
||||
displayUserId: "777777",
|
||||
userId: "9001",
|
||||
username: "pretty owner",
|
||||
},
|
||||
operator: {
|
||||
account: "ops",
|
||||
adminId: "8",
|
||||
name: "运营",
|
||||
type: "admin",
|
||||
},
|
||||
updatedByAdminId: "8",
|
||||
});
|
||||
});
|
||||
|
||||
test("listAppUsers sends country region and time filters", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@ -386,6 +386,8 @@ interface RawPrettyDisplayID {
|
||||
assignedAtMs?: number;
|
||||
assigned_lease_id?: string;
|
||||
assignedLeaseId?: string;
|
||||
assigned_user?: RawAppUserBrief;
|
||||
assignedUser?: RawAppUserBrief;
|
||||
assigned_user_id?: EntityId;
|
||||
assignedUserId?: EntityId;
|
||||
created_at_ms?: number;
|
||||
@ -396,6 +398,7 @@ interface RawPrettyDisplayID {
|
||||
displayUserId?: string;
|
||||
generated_batch_id?: string;
|
||||
generatedBatchId?: string;
|
||||
operator?: RawPrettyDisplayIDOperator | null;
|
||||
pool?: RawPrettyDisplayIDPool | null;
|
||||
pool_id?: string;
|
||||
poolId?: string;
|
||||
@ -409,6 +412,17 @@ interface RawPrettyDisplayID {
|
||||
status?: string;
|
||||
updated_at_ms?: number;
|
||||
updatedAtMs?: number;
|
||||
updated_by_admin_id?: EntityId;
|
||||
updatedByAdminId?: EntityId;
|
||||
}
|
||||
|
||||
interface RawPrettyDisplayIDOperator extends RawAppUserBrief {
|
||||
account?: string;
|
||||
admin_id?: EntityId;
|
||||
adminId?: EntityId;
|
||||
name?: string;
|
||||
source?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface RawPrettyDisplayIDGenerationBatch {
|
||||
@ -572,6 +586,37 @@ function normalizeAppUserBanOperator(item?: RawAppUserBanOperator | null) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePrettyDisplayIDOperator(item?: RawPrettyDisplayIDOperator | null, fallbackAdminId?: EntityId) {
|
||||
if (!item) {
|
||||
const adminId = stringValue(fallbackAdminId);
|
||||
return adminId
|
||||
? {
|
||||
adminId,
|
||||
name: "后台管理员",
|
||||
type: "admin",
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
const user = normalizeAppUserBrief(item);
|
||||
const adminId = stringValue(item.adminId ?? item.admin_id ?? fallbackAdminId);
|
||||
return {
|
||||
...user,
|
||||
account: stringValue(item.account),
|
||||
adminId,
|
||||
name: stringValue(item.name),
|
||||
source: stringValue(item.source),
|
||||
type: stringValue(item.type || (adminId ? "admin" : "app_user")),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOptionalAppUserBrief(item?: RawAppUserBrief | null) {
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
const user = normalizeAppUserBrief(item);
|
||||
return user.userId || user.displayUserId || user.username ? user : undefined;
|
||||
}
|
||||
|
||||
function normalizeAppUserBanRecord(item: RawAppUserBanRecord = {}): AppUserBanRecordDto {
|
||||
return {
|
||||
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
|
||||
@ -640,15 +685,19 @@ function numberValue(value: unknown): number {
|
||||
|
||||
// 靓号明细包含池号和后台发放两类来源,pool 可能为空,归一化后页面只判断 source/status 即可。
|
||||
function normalizePrettyDisplayID(item: RawPrettyDisplayID): PrettyDisplayIDDto {
|
||||
const createdByAdminId = item.createdByAdminId ?? item.created_by_admin_id ?? "";
|
||||
const updatedByAdminId = item.updatedByAdminId ?? item.updated_by_admin_id ?? "";
|
||||
return {
|
||||
appCode: item.appCode ?? item.app_code ?? "",
|
||||
assignedAtMs: item.assignedAtMs ?? item.assigned_at_ms ?? 0,
|
||||
assignedLeaseId: item.assignedLeaseId ?? item.assigned_lease_id ?? "",
|
||||
assignedUser: normalizeOptionalAppUserBrief(item.assignedUser ?? item.assigned_user),
|
||||
assignedUserId: item.assignedUserId ?? item.assigned_user_id ?? "",
|
||||
createdAtMs: item.createdAtMs ?? item.created_at_ms ?? 0,
|
||||
createdByAdminId: item.createdByAdminId ?? item.created_by_admin_id ?? "",
|
||||
createdByAdminId,
|
||||
displayUserId: item.displayUserId ?? item.display_user_id ?? "",
|
||||
generatedBatchId: item.generatedBatchId ?? item.generated_batch_id ?? "",
|
||||
operator: normalizePrettyDisplayIDOperator(item.operator, updatedByAdminId || createdByAdminId),
|
||||
pool: item.pool ? normalizePrettyDisplayIDPool(item.pool) : null,
|
||||
poolId: item.poolId ?? item.pool_id ?? "",
|
||||
prettyId: item.prettyId ?? item.pretty_id ?? "",
|
||||
@ -657,6 +706,7 @@ function normalizePrettyDisplayID(item: RawPrettyDisplayID): PrettyDisplayIDDto
|
||||
source: item.source ?? "",
|
||||
status: item.status ?? "",
|
||||
updatedAtMs: item.updatedAtMs ?? item.updated_at_ms ?? 0,
|
||||
updatedByAdminId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,39 @@
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.prettyTabsBar {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 var(--space-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.prettyTabsBar :global(.MuiTabs-indicator) {
|
||||
height: 3px;
|
||||
border-radius: 999px 999px 0 0;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.prettyTab.prettyTab {
|
||||
min-height: 48px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--admin-font-size);
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.prettyTab.prettyTab:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.prettyTab.prettyTab:global(.Mui-selected) {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.prettyWorkspace {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
@ -637,6 +670,10 @@
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.prettyTabsBar {
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
.prettyWorkspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ export function usePrettyIdsPage() {
|
||||
const [idSource, setIdSource] = useState("");
|
||||
const [idPoolId, setIdPoolId] = useState("");
|
||||
const [assignedUserId, setAssignedUserId] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("ids");
|
||||
const [activeDialog, setActiveDialog] = useState("");
|
||||
const [activePool, setActivePool] = useState(null);
|
||||
const [activePrettyID, setActivePrettyID] = useState(null);
|
||||
@ -299,6 +300,7 @@ export function usePrettyIdsPage() {
|
||||
activeDialog,
|
||||
activePool,
|
||||
activePrettyID,
|
||||
activeTab,
|
||||
assignedUserId,
|
||||
changeAssignedUserId,
|
||||
changeIDKeyword,
|
||||
@ -331,6 +333,7 @@ export function usePrettyIdsPage() {
|
||||
reloadAll,
|
||||
resetIDFilters,
|
||||
resetPoolFilters,
|
||||
setActiveTab,
|
||||
setGenerateForm,
|
||||
setGrantForm,
|
||||
setIdPage,
|
||||
|
||||
@ -5,14 +5,16 @@ 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 Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
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 { useAppUserDetail } from "@/features/app-users/components/AppUserDetailContext.jsx";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import {
|
||||
prettyIdSourceLabels,
|
||||
@ -31,199 +33,22 @@ export function AppUserPrettyIdsPage() {
|
||||
const page = usePrettyIdsPage();
|
||||
const pools = page.poolQuery.data.items || [];
|
||||
const prettyIds = page.idQuery.data.items || [];
|
||||
const showingPools = page.activeTab === "pools";
|
||||
|
||||
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 className={styles.prettyTabsBar}>
|
||||
<Tabs value={page.activeTab} onChange={(_, value) => page.setActiveTab(value)}>
|
||||
<Tab className={styles.prettyTab} label={`靓号列表 ${Number(page.idQuery.data.total || 0)}`} value="ids" />
|
||||
<Tab className={styles.prettyTab} label={`靓号池 ${Number(page.poolQuery.data.total || 0)}`} value="pools" />
|
||||
</Tabs>
|
||||
</div>
|
||||
{showingPools ? (
|
||||
<PrettyPoolPanel page={page} pools={pools} />
|
||||
) : (
|
||||
<PrettyIDPanel page={page} prettyIds={prettyIds} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PoolDialog page={page} />
|
||||
@ -234,6 +59,190 @@ export function AppUserPrettyIdsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function PrettyPoolPanel({ page, pools }) {
|
||||
return (
|
||||
<section className={styles.prettyPane}>
|
||||
<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="860px"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function PrettyIDPanel({ page, prettyIds }) {
|
||||
return (
|
||||
<section className={styles.prettyPane}>
|
||||
<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="1360px"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function PaneHeader({ actions, count, title }) {
|
||||
return (
|
||||
<div className={styles.prettyPaneHeader}>
|
||||
@ -306,13 +315,12 @@ function idColumns(page) {
|
||||
{
|
||||
key: "display",
|
||||
label: "靓号",
|
||||
width: "minmax(190px, 1fr)",
|
||||
width: "minmax(130px, 0.7fr)",
|
||||
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>
|
||||
),
|
||||
},
|
||||
@ -334,21 +342,25 @@ function idColumns(page) {
|
||||
render: (item) => <StatusBadge labels={prettyIdStatusLabels} status={item.status} />,
|
||||
},
|
||||
{
|
||||
key: "assigned",
|
||||
label: "占用 / 时间",
|
||||
width: "minmax(190px, 0.9fr)",
|
||||
key: "assignedUser",
|
||||
label: "占用人",
|
||||
width: "minmax(240px, 1.2fr)",
|
||||
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>
|
||||
<AssignedUserCell item={item} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "operator",
|
||||
label: "操作人",
|
||||
width: "minmax(210px, 1fr)",
|
||||
render: (item) => <OperatorCell item={item} />,
|
||||
},
|
||||
{
|
||||
key: "time",
|
||||
label: "占用 / 释放时间",
|
||||
width: "minmax(210px, 1fr)",
|
||||
render: (item) => <PrettyIDTimeCell item={item} />,
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "操作",
|
||||
@ -368,31 +380,51 @@ function idColumns(page) {
|
||||
}
|
||||
|
||||
function AssignedUserCell({ item }) {
|
||||
const { openAppUserDetail } = useAppUserDetail();
|
||||
const assignedUserId = formatEntityID(item.assignedUserId);
|
||||
if (!item.assignedUserId || !openAppUserDetail) {
|
||||
return <span>{assignedUserId}</span>;
|
||||
if (!item.assignedUserId) {
|
||||
return <span className={styles.meta}>-</span>;
|
||||
}
|
||||
|
||||
return <AdminUserIdentity openInAppUserDetail rows={[assignedUserId]} user={assignedUser(item)} />;
|
||||
}
|
||||
|
||||
function OperatorCell({ item }) {
|
||||
const operator = item.operator || {};
|
||||
const adminId = operator.adminId || item.updatedByAdminId || item.createdByAdminId;
|
||||
if (!operator.userId && !operator.adminId && !operator.name && !operator.account && !adminId) {
|
||||
return <span className={styles.meta}>-</span>;
|
||||
}
|
||||
if ((operator.type || "admin") === "app_user") {
|
||||
return <AdminUserIdentity openInAppUserDetail rows={[operator.displayUserId, operator.userId]} user={operator} />;
|
||||
}
|
||||
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>
|
||||
<AdminUserIdentity
|
||||
name={operator.account || operator.name || "后台管理员"}
|
||||
rows={[operator.name, adminId ? `Admin ID ${adminId}` : "后台管理员"]}
|
||||
user={{ ...operator, adminId }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PrettyIDTimeCell({ item }) {
|
||||
return (
|
||||
<div className={styles.stack}>
|
||||
<span>{item.assignedAtMs ? formatMillis(item.assignedAtMs) : "-"}</span>
|
||||
<span className={styles.meta}>{item.releasedAtMs ? `释放 ${formatMillis(item.releasedAtMs)}` : "-"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function assignedUser(item) {
|
||||
return {
|
||||
...(item.assignedUser || {}),
|
||||
displayUserId: item.assignedUser?.displayUserId || item.assignedUser?.defaultDisplayUserId || item.assignedUserId,
|
||||
prettyDisplayUserId: item.displayUserId,
|
||||
prettyId: item.prettyId,
|
||||
userId: item.assignedUser?.userId || item.assignedUserId,
|
||||
};
|
||||
}
|
||||
|
||||
function PoolDialog({ page }) {
|
||||
const editing = Boolean(page.activePool);
|
||||
return (
|
||||
|
||||
169
src/features/app-users/pages/AppUserPrettyIdsPage.test.jsx
Normal file
169
src/features/app-users/pages/AppUserPrettyIdsPage.test.jsx
Normal file
@ -0,0 +1,169 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { AppUserPrettyIdsPage } from "./AppUserPrettyIdsPage.jsx";
|
||||
import { usePrettyIdsPage } from "@/features/app-users/hooks/usePrettyIdsPage.js";
|
||||
|
||||
vi.mock("@/features/app-users/hooks/usePrettyIdsPage.js", () => ({
|
||||
usePrettyIdsPage: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("pretty id page uses tabs and hides internal pretty record ids", () => {
|
||||
vi.mocked(usePrettyIdsPage).mockReturnValue(pageFixture());
|
||||
|
||||
render(<AppUserPrettyIdsPage />);
|
||||
|
||||
expect(screen.getByRole("tab", { name: "靓号列表 1" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tab", { name: "靓号池 1" })).toBeInTheDocument();
|
||||
expect(screen.getAllByText("777777").length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText("pretty_1782492525506_0b70bb6c6dbc63cf")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("占用人 A")).toBeInTheDocument();
|
||||
expect(screen.getByText("Admin ID 8")).toBeInTheDocument();
|
||||
expect(screen.getByText("释放 2026-06-27 01:00:00")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("pretty id page can switch to pool tab", () => {
|
||||
const page = pageFixture({ activeTab: "pools" });
|
||||
vi.mocked(usePrettyIdsPage).mockReturnValue(page);
|
||||
|
||||
render(<AppUserPrettyIdsPage />);
|
||||
|
||||
expect(screen.getByRole("tab", { name: "靓号池 1" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByText("≥LV.10")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
function pageFixture(patch = {}) {
|
||||
return {
|
||||
abilities: {
|
||||
canPrettyGenerate: true,
|
||||
canPrettyGrant: true,
|
||||
canPrettyUpdate: true,
|
||||
},
|
||||
activeDialog: "",
|
||||
activePool: null,
|
||||
activePrettyID: null,
|
||||
activeTab: "ids",
|
||||
assignedUserId: "",
|
||||
changeAssignedUserId: vi.fn(),
|
||||
changeIDKeyword: vi.fn(),
|
||||
changeIDPool: vi.fn(),
|
||||
changeIDSource: vi.fn(),
|
||||
changeIDStatus: vi.fn(),
|
||||
changePoolStatus: vi.fn(),
|
||||
changePoolTrack: vi.fn(),
|
||||
closeDialog: vi.fn(),
|
||||
generateForm: { count: 100, poolId: "", ruleConfigJson: "", ruleType: "aabbcc" },
|
||||
grantForm: { displayUserId: "", durationDays: "", reason: "", targetUserId: "" },
|
||||
idKeyword: "",
|
||||
idPage: 1,
|
||||
idPoolId: "",
|
||||
idQuery: {
|
||||
data: {
|
||||
items: [prettyIdFixture()],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
},
|
||||
error: null,
|
||||
loading: false,
|
||||
reload: vi.fn(),
|
||||
},
|
||||
idSource: "",
|
||||
idStatus: "",
|
||||
loadingAction: "",
|
||||
openCreatePool: vi.fn(),
|
||||
openEditPool: vi.fn(),
|
||||
openGenerate: vi.fn(),
|
||||
openGrant: vi.fn(),
|
||||
openStatus: vi.fn(),
|
||||
poolForm: {
|
||||
levelTrack: "wealth",
|
||||
maxLevel: 19,
|
||||
minLevel: 10,
|
||||
name: "",
|
||||
ruleConfigJson: "",
|
||||
ruleType: "aabbcc",
|
||||
sortOrder: 0,
|
||||
status: "active",
|
||||
},
|
||||
poolOptions: [{ label: "≥LV.10 · 10-19", value: "pool-1" }],
|
||||
poolPage: 1,
|
||||
poolQuery: {
|
||||
data: {
|
||||
items: [poolFixture()],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
},
|
||||
error: null,
|
||||
loading: false,
|
||||
reload: vi.fn(),
|
||||
},
|
||||
poolStatus: "",
|
||||
poolTrack: "",
|
||||
reloadAll: vi.fn(),
|
||||
resetIDFilters: vi.fn(),
|
||||
resetPoolFilters: vi.fn(),
|
||||
setActiveTab: vi.fn(),
|
||||
setGenerateForm: vi.fn(),
|
||||
setGrantForm: vi.fn(),
|
||||
setIdPage: vi.fn(),
|
||||
setPoolForm: vi.fn(),
|
||||
setPoolPage: vi.fn(),
|
||||
setStatusForm: vi.fn(),
|
||||
statusForm: { prettyId: "", reason: "", status: "available" },
|
||||
submitGenerate: vi.fn(),
|
||||
submitGrant: vi.fn(),
|
||||
submitPool: vi.fn(),
|
||||
submitStatus: vi.fn(),
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function poolFixture() {
|
||||
return {
|
||||
createdAtMs: 1781249886288,
|
||||
levelTrack: "wealth",
|
||||
maxLevel: 19,
|
||||
minLevel: 10,
|
||||
name: "≥LV.10",
|
||||
poolId: "pretty_pool_1781249886288_301d1a449c51fbc0",
|
||||
ruleType: "abccba",
|
||||
status: "active",
|
||||
updatedAtMs: 1781249886288,
|
||||
};
|
||||
}
|
||||
|
||||
function prettyIdFixture() {
|
||||
return {
|
||||
assignedAtMs: 1782499200000,
|
||||
assignedUser: {
|
||||
avatar: "",
|
||||
defaultDisplayUserId: "10001",
|
||||
displayUserId: "777777",
|
||||
userId: "9001",
|
||||
username: "占用人 A",
|
||||
},
|
||||
assignedUserId: "9001",
|
||||
createdAtMs: 1782499100000,
|
||||
createdByAdminId: "7",
|
||||
displayUserId: "777777",
|
||||
operator: {
|
||||
account: "ops",
|
||||
adminId: "8",
|
||||
name: "运营",
|
||||
type: "admin",
|
||||
},
|
||||
pool: poolFixture(),
|
||||
poolId: "pool-1",
|
||||
prettyId: "pretty_1782492525506_0b70bb6c6dbc63cf",
|
||||
releasedAtMs: 1782493200000,
|
||||
source: "admin_grant",
|
||||
status: "assigned",
|
||||
updatedAtMs: 1782499300000,
|
||||
updatedByAdminId: "8",
|
||||
};
|
||||
}
|
||||
@ -933,11 +933,31 @@ export interface PrettyDisplayIDDto {
|
||||
appCode?: string;
|
||||
assignedAtMs?: number;
|
||||
assignedLeaseId?: string;
|
||||
assignedUser?: {
|
||||
avatar?: string;
|
||||
defaultDisplayUserId?: string;
|
||||
displayUserId?: string;
|
||||
prettyDisplayUserId?: string;
|
||||
prettyId?: string;
|
||||
userId?: EntityId;
|
||||
username?: string;
|
||||
};
|
||||
assignedUserId?: EntityId;
|
||||
createdAtMs?: number;
|
||||
createdByAdminId?: EntityId;
|
||||
displayUserId: string;
|
||||
generatedBatchId?: string;
|
||||
operator?: {
|
||||
account?: string;
|
||||
adminId?: EntityId;
|
||||
avatar?: string;
|
||||
displayUserId?: string;
|
||||
name?: string;
|
||||
source?: string;
|
||||
type?: string;
|
||||
userId?: EntityId;
|
||||
username?: string;
|
||||
};
|
||||
pool?: PrettyDisplayIDPoolDto | null;
|
||||
poolId?: string;
|
||||
prettyId: string;
|
||||
@ -946,6 +966,7 @@ export interface PrettyDisplayIDDto {
|
||||
source: string;
|
||||
status: string;
|
||||
updatedAtMs?: number;
|
||||
updatedByAdminId?: EntityId;
|
||||
}
|
||||
|
||||
export interface PrettyDisplayIDGenerationBatchDto {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user