优化靓号页面展示

This commit is contained in:
zhx 2026-06-27 01:09:55 +08:00
parent 8c9f5d35b2
commit 63d4ffbacd
7 changed files with 596 additions and 222 deletions

View File

@ -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",

View File

@ -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,
};
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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 (

View 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",
};
}

View File

@ -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 {