修复报错

This commit is contained in:
zhx 2026-06-12 11:20:02 +08:00
parent ac5d3a53d9
commit 2ebacae7c2
32 changed files with 370 additions and 210 deletions

View File

@ -4892,10 +4892,10 @@
"type": "string"
},
"ownerUserId": {
"type": "integer"
"type": "string"
},
"parentBdUserId": {
"type": "integer"
"type": "string"
},
"regionId": {
"type": "integer"
@ -4919,7 +4919,7 @@
"type": "integer"
},
"parentLeaderUserId": {
"type": "integer"
"type": "string"
},
"regionId": {
"type": "integer"
@ -4934,7 +4934,7 @@
"type": "integer"
},
"userId": {
"type": "integer"
"type": "string"
}
}
},
@ -4991,7 +4991,7 @@
"type": "integer"
},
"userId": {
"type": "integer"
"type": "string"
}
}
},

View File

@ -71,7 +71,7 @@ test("loads self game statistics from the big screen switch", async () => {
expect(fetchSelfGameStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "lalu", gameId: "all" }));
expect(screen.getByText("Dice / Rock H5 聚合统计")).toBeTruthy();
expect(screen.getByText("对局数")).toBeTruthy();
expect(screen.getByText("创建局")).toBeTruthy();
});
async function flushEffects() {

View File

@ -158,11 +158,11 @@ function FilterMenu({ className = "", icon, isOpen, label, loading = false, onSe
return (
<div className={["filter-control", className].filter(Boolean).join(" ")}>
<button aria-expanded={isOpen} className="filter-trigger" disabled={loading} onClick={onToggle} type="button">
{icon ? <span className="filter-icon">{icon}</span> : null}
<span className="filter-copy">
<small>{label}</small>
<b>{valueLabel}</b>
<span className="filter-label">
{icon ? <span className="filter-icon">{icon}</span> : null}
<span>{label}</span>
</span>
<span className="filter-value">{valueLabel}</span>
<KeyboardArrowDownOutlined className="filter-chevron" fontSize="small" />
</button>
{isOpen ? (
@ -201,11 +201,11 @@ function SearchableFilterMenu({ icon, isOpen, label, loading = false, onSelect,
return (
<div className="filter-control filter-control--country">
<button aria-expanded={isOpen} className="filter-trigger" disabled={loading} onClick={onToggle} type="button">
{icon ? <span className="filter-icon">{icon}</span> : null}
<span className="filter-copy">
<small>{label}</small>
<b>{valueLabel}</b>
<span className="filter-label">
{icon ? <span className="filter-icon">{icon}</span> : null}
<span>{label}</span>
</span>
<span className="filter-value">{valueLabel}</span>
<KeyboardArrowDownOutlined className="filter-chevron" fontSize="small" />
</button>
{isOpen ? (
@ -301,14 +301,14 @@ function RangeMenu({ activeValue, isOpen, onSelect, onToggle, range, timeZone })
return (
<div className="filter-control filter-control--range">
<button aria-expanded={isOpen} aria-haspopup="listbox" className="date-range-trigger" onClick={onToggle} type="button">
<span className="date-range-icon"><CalendarMonthOutlined fontSize="small" /></span>
<span className="date-range-copy">
<span className="date-range-label">{activeOption?.label || "时间范围"}</span>
<span className="date-range-values">
<b>{range.start}</b>
<i>~</i>
<b>{range.end}</b>
</span>
<span className="date-range-label">
<CalendarMonthOutlined fontSize="inherit" />
<span>{activeOption?.label || "时间范围"}</span>
</span>
<span className="date-range-values">
<b>{range.start}</b>
<i>~</i>
<b>{range.end}</b>
</span>
<KeyboardArrowDownOutlined className="date-range-chevron" fontSize="small" />
</button>

View File

@ -131,13 +131,14 @@
}
.filter-trigger {
position: relative;
display: grid;
min-width: 138px;
height: 44px;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 0 12px;
height: 34px;
grid-template-columns: minmax(0, 1fr) 18px;
align-items: end;
gap: 0;
padding: 7px 28px 3px 12px;
border: 1px solid rgba(64, 148, 197, 0.48);
border-radius: 8px;
background: rgba(7, 29, 48, 0.9);
@ -167,6 +168,13 @@
box-shadow: 0 0 0 3px rgba(39, 228, 245, 0.08);
}
.filter-trigger:hover .filter-label,
.filter-trigger[aria-expanded="true"] .filter-label,
.date-range-trigger:hover .date-range-label,
.date-range-trigger[aria-expanded="true"] .date-range-label {
color: #27e4f5;
}
.filter-trigger:disabled {
cursor: progress;
opacity: 0.68;
@ -177,10 +185,24 @@
color: #cbd9e9;
}
.filter-icon {
display: grid;
min-width: 20px;
place-items: center;
.filter-label {
position: absolute;
top: -8px;
left: 10px;
display: inline-flex;
max-width: calc(100% - 20px);
align-items: center;
gap: 5px;
overflow: hidden;
padding: 0 5px;
border-radius: 5px;
background: rgba(2, 15, 31, 0.98);
color: #7f9bb7;
font-size: 10px;
font-weight: 760;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.control-glyph {
@ -190,38 +212,46 @@
letter-spacing: 0;
}
.filter-copy {
display: grid;
.filter-icon {
display: inline-grid;
flex: 0 0 auto;
min-width: 16px;
place-items: center;
color: #cbd9e9;
}
.filter-label .filter-icon {
color: #27e4f5;
}
.filter-value {
display: block;
min-width: 0;
}
.filter-copy small {
overflow: hidden;
color: #7f9bb7;
font-size: 10px;
line-height: 1.1;
text-overflow: ellipsis;
white-space: nowrap;
}
.filter-copy b {
overflow: hidden;
color: #e6f3ff;
font-size: 13px;
font-weight: 720;
line-height: 1.2;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.filter-chevron {
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
}
.date-range-trigger {
position: relative;
display: grid;
width: 336px;
height: 44px;
grid-template-columns: 24px minmax(0, 1fr) 20px;
align-items: center;
gap: 10px;
padding: 0 12px;
width: 318px;
height: 34px;
grid-template-columns: minmax(0, 1fr) 18px;
align-items: end;
gap: 0;
padding: 7px 28px 3px 12px;
border: 1px solid rgba(64, 148, 197, 0.48);
border-radius: 8px;
background: rgba(7, 29, 48, 0.9);
@ -238,49 +268,46 @@
box-shadow: 0 0 0 3px rgba(39, 228, 245, 0.08);
}
.date-range-icon {
display: grid;
width: 24px;
height: 24px;
place-items: center;
color: #cbd9e9;
}
.date-range-icon svg {
width: 19px;
height: 19px;
}
.date-range-copy {
display: grid;
min-width: 0;
gap: 3px;
}
.date-range-label {
position: absolute;
top: -8px;
left: 10px;
display: inline-flex;
max-width: calc(100% - 20px);
align-items: center;
gap: 5px;
overflow: hidden;
padding: 0 5px;
border-radius: 5px;
background: rgba(2, 15, 31, 0.98);
color: #7f9bb7;
font-size: 10px;
font-weight: 720;
line-height: 1.05;
font-weight: 760;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.date-range-label svg {
flex: 0 0 auto;
color: #cbd9e9;
}
.date-range-values {
display: grid;
display: flex;
min-width: 0;
grid-template-columns: minmax(0, auto) 16px minmax(0, auto);
align-items: center;
gap: 8px;
gap: 7px;
overflow: hidden;
}
.date-range-values b {
min-width: 0;
overflow: hidden;
color: #eaf6ff;
font-size: 14px;
font-size: 13px;
font-weight: 760;
line-height: 1.15;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
@ -293,8 +320,11 @@
}
.date-range-chevron {
position: absolute;
top: 50%;
right: 8px;
color: #cbd9e9;
justify-self: end;
transform: translateY(-50%);
}
.filter-popover {

View File

@ -155,6 +155,7 @@ export function Header({ activeLabel, isSidebarCollapsed, menuItems = [], onRefr
<TextField
className="app-switch"
disabled={appScope.loading || !appScope.apps.length}
label="应用"
select
size="small"
value={appScope.appCode}
@ -172,6 +173,7 @@ export function Header({ activeLabel, isSidebarCollapsed, menuItems = [], onRefr
</TextField>
<TextField
className="timezone-switch"
label="时区"
select
size="small"
slotProps={{ htmlInput: { "aria-label": "显示时区" } }}
@ -185,8 +187,11 @@ export function Header({ activeLabel, isSidebarCollapsed, menuItems = [], onRefr
))}
</TextField>
<button className="search-trigger" type="button" onClick={openSearch}>
<Search fontSize="small" />
<span>搜索菜单用户角色...</span>
<span className="search-trigger__label">
<Search fontSize="inherit" />
<span>搜索</span>
</span>
<span className="search-trigger__value">搜索菜单用户角色...</span>
</button>
<IconButton label="刷新" onClick={refresh}>
<Refresh fontSize="small" />

View File

@ -37,7 +37,7 @@ export interface CumulativeRechargeRewardConfigPayload {
}
export interface CumulativeRechargeRewardGrantUserDto {
userId?: number;
userId?: string;
displayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
@ -52,7 +52,7 @@ export interface CumulativeRechargeRewardGrantDto {
eventId?: string;
transactionId?: string;
commandId?: string;
userId: number;
userId: string;
user?: CumulativeRechargeRewardGrantUserDto;
tierId?: number;
tierCode?: string;
@ -143,9 +143,9 @@ function normalizeGrant(item: RawGrant): CumulativeRechargeRewardGrantDto {
eventId: stringValue(item.eventId ?? item.event_id),
transactionId: stringValue(item.transactionId ?? item.transaction_id),
commandId: stringValue(item.commandId ?? item.command_id),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
user: {
userId: numberValue(rawUser.userId ?? rawUser.user_id),
userId: stringValue(rawUser.userId ?? rawUser.user_id),
displayUserId: stringValue(rawUser.displayUserId ?? rawUser.display_user_id),
prettyDisplayUserId: stringValue(rawUser.prettyDisplayUserId ?? rawUser.pretty_display_user_id),
prettyId: stringValue(rawUser.prettyId ?? rawUser.pretty_id),

View File

@ -45,7 +45,7 @@ export interface FirstRechargeRewardConfigPayload {
}
export interface FirstRechargeRewardClaimUserDto {
userId?: number;
userId?: string;
displayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
@ -58,7 +58,7 @@ export interface FirstRechargeRewardClaimDto {
eventId?: string;
transactionId?: string;
commandId?: string;
userId: number;
userId: string;
user?: FirstRechargeRewardClaimUserDto;
tierId?: number;
tierCode?: string;
@ -151,9 +151,9 @@ function normalizeClaim(item: RawClaim): FirstRechargeRewardClaimDto {
eventId: stringValue(item.eventId ?? item.event_id),
transactionId: stringValue(item.transactionId ?? item.transaction_id),
commandId: stringValue(item.commandId ?? item.command_id),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
user: {
userId: numberValue(rawUser.userId ?? rawUser.user_id),
userId: stringValue(rawUser.userId ?? rawUser.user_id),
displayUserId: stringValue(rawUser.displayUserId ?? rawUser.display_user_id),
prettyDisplayUserId: stringValue(rawUser.prettyDisplayUserId ?? rawUser.pretty_display_user_id),
prettyId: stringValue(rawUser.prettyId ?? rawUser.pretty_id),

View File

@ -305,7 +305,7 @@ export interface TeamSalarySettlePayload {
cycle_key: string;
region_id?: number;
country_code?: string;
user_ids?: number[];
user_ids?: string[];
}
export interface TeamSalarySettleResult {

View File

@ -298,8 +298,8 @@ function TeamPendingPanel() {
// items user_ids
const userIds = items
.filter((item) => selected.has(teamPendingKey(item)))
.map((item) => Number(item.userId))
.filter((id) => Number.isFinite(id) && id > 0);
.map((item) => String(item.userId || "").trim())
.filter((id) => /^[1-9]\d*$/.test(id));
if (!userIds.length) {
return;
}

View File

@ -47,15 +47,15 @@ test("host org list APIs use generated admin paths and filters", async () => {
await createBDLeader({
commandId: "bd-leader-test",
positionAlias: "Senior Admin",
targetUserId: 1002,
targetUserId: "1002",
});
await updateBDLeaderPositionAlias(1002, {
await updateBDLeaderPositionAlias("1002", {
commandId: "bd-leader-position-alias-test",
positionAlias: "Regional Admin",
});
await createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: 1001 });
await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" });
await creditCoinSellerStock(1001, {
await createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: "1001" });
await setCoinSellerStatus("1001", { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" });
await creditCoinSellerStock("1001", {
coinAmount: 8000000,
commandId: "coin-seller-stock-test",
rechargeAmount: "100.000000",

View File

@ -11,7 +11,15 @@ const commandContactSchema = z.object({
reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default(""),
});
const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID");
const userIdSchema = z.preprocess(
(value) => {
if (value === null || value === undefined) {
return "";
}
return String(value).trim();
},
z.string().min(1, "请输入有效用户短 ID").regex(/^[1-9]\d*$/, "请输入有效用户短 ID"),
);
const entityIdSchema = z.preprocess(
(value) => {
if (value === null || value === undefined) {
@ -24,14 +32,14 @@ const entityIdSchema = z.preprocess(
const optionalUserIdSchema = z.preprocess(
(value) => {
if (value === null || value === undefined) {
return 0;
return "0";
}
if (typeof value === "string" && value.trim() === "") {
return 0;
return "0";
}
return value;
return String(value).trim();
},
z.coerce.number().int().min(0, "请输入有效用户短 ID"),
z.string().regex(/^(0|[1-9]\d*)$/, "请输入有效用户短 ID"),
);
const regionIdSchema = z.coerce.number().int().positive("请输入有效区域 ID");
const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").default(0);

View File

@ -45,7 +45,7 @@ export interface InviteActivityRewardConfigPayload {
}
export interface InviteActivityRewardClaimUserDto {
userId?: number;
userId?: string;
displayUserId?: string;
username?: string;
avatar?: string;
@ -55,7 +55,7 @@ export interface InviteActivityRewardClaimDto {
claimId: string;
appCode?: string;
cycleKey?: string;
userId: number;
userId: string;
user?: InviteActivityRewardClaimUserDto;
rewardType?: string;
commandId?: string;
@ -150,9 +150,9 @@ function normalizeClaim(item: RawClaim): InviteActivityRewardClaimDto {
claimId: stringValue(item.claimId ?? item.claim_id),
appCode: stringValue(item.appCode ?? item.app_code),
cycleKey: stringValue(item.cycleKey ?? item.cycle_key),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
user: {
userId: numberValue(rawUser.userId ?? rawUser.user_id),
userId: stringValue(rawUser.userId ?? rawUser.user_id),
displayUserId: stringValue(rawUser.displayUserId ?? rawUser.display_user_id),
username: stringValue(rawUser.username),
avatar: stringValue(rawUser.avatar),

View File

@ -10,7 +10,7 @@ import styles from "@/features/invite-activity-reward/invite-activity-reward.mod
const rewardTypeOptions = [
["recharge", "累计充值"],
["valid_invite", "有效人数"],
["valid_invite", "Valid Users"],
];
export function InviteActivityRewardConfigDrawer({

View File

@ -25,8 +25,8 @@ export function InviteActivityRewardConfigSummary({ canUpdate, config, configLoa
<SummaryItem label="累计充值档位">
{thresholdSummary(rechargeTiers, "thresholdCoinAmount", "金币")}
</SummaryItem>
<SummaryItem label="有效人数档位">
{thresholdSummary(validInviteTiers, "thresholdValidInviteCount", "")}
<SummaryItem label="Valid Users Tiers">
{thresholdSummary(validInviteTiers, "thresholdValidInviteCount", "people")}
</SummaryItem>
<SummaryItem label="邀请人即时奖励">
{coinSummary(config?.perInviteInviterRewardCoinAmount)}

View File

@ -180,17 +180,16 @@ function payloadFromForm(form) {
throw new Error("累计充值金币门槛必须大于 0");
}
if (rewardType === "valid_invite" && thresholdValidInviteCount <= 0) {
throw new Error("有效人数门槛必须大于 0");
throw new Error("Valid users threshold must be greater than 0");
}
if (rewardCoinAmount <= 0) {
throw new Error("奖励金币必须大于 0");
}
const tierCode = String(tier.tierCode || "").trim() || `invite_${rewardType}_${Date.now()}_${index + 1}`;
const tierCode = normalizeTierCode(tier.tierCode, rewardType, index);
const tierName =
String(tier.tierName || "").trim() ||
(rewardType === "recharge"
? `累计充值 ${formatNumber(thresholdCoinAmount)} 金币`
: `有效邀请 ${formatNumber(thresholdValidInviteCount)}`);
rewardType === "recharge"
? `Cumulative Recharge ${formatNumber(thresholdCoinAmount)} coins`
: `Valid Users ${formatNumber(thresholdValidInviteCount)} people`;
return {
tier_id: Number(tier.tierId || 0),
reward_type: rewardType,
@ -220,5 +219,13 @@ function payloadFromForm(form) {
}
function formatNumber(value) {
return new Intl.NumberFormat("zh-CN").format(Number(value || 0));
return new Intl.NumberFormat("en-US").format(Number(value || 0));
}
function normalizeTierCode(value, rewardType, index) {
const code = String(value || "").trim();
if (code && !/[\u3400-\u9fff]/.test(code)) {
return code;
}
return `invite_${rewardType}_${Date.now()}_${index + 1}`;
}

View File

@ -17,7 +17,7 @@ const claimStatusOptions = [
const rewardTypeOptions = [
["recharge", "累计充值"],
["valid_invite", "有效人数"],
["valid_invite", "Valid Users"],
["invite_inviter", "邀请人即时"],
["invite_invitee", "被邀请人即时"],
];
@ -202,7 +202,7 @@ function claimStatusLabel(status) {
function thresholdLabel(claim) {
if (claim.rewardType === "valid_invite") {
return `${formatNumber(claim.thresholdValidInviteCount)} `;
return `${formatNumber(claim.thresholdValidInviteCount)} people`;
}
if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") {
return claim.tierId ? `关联用户 ${claim.tierId}` : "-";
@ -214,7 +214,7 @@ function reachedLabel(claim) {
if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") {
return "1 人";
}
const unit = claim.rewardType === "valid_invite" ? "" : "金币";
const unit = claim.rewardType === "valid_invite" ? "people" : "金币";
return `${formatNumber(claim.reachedValue)} ${unit}`;
}

View File

@ -30,7 +30,7 @@ export interface RedPacketClaimDto {
claimId: string;
commandId?: string;
packetId: string;
userId: number;
userId: string;
amount: number;
walletTransactionId?: string;
status?: string;
@ -43,7 +43,7 @@ export interface RedPacketDto {
appCode?: string;
packetId: string;
commandId?: string;
senderUserId: number;
senderUserId: string;
roomId: string;
regionId: number;
packetType: string;
@ -139,7 +139,7 @@ function normalizePacket(item: RawPacket): RedPacketDto {
appCode: stringValue(item.appCode ?? item.app_code),
packetId: stringValue(item.packetId ?? item.packet_id),
commandId: stringValue(item.commandId ?? item.command_id),
senderUserId: numberValue(item.senderUserId ?? item.sender_user_id),
senderUserId: stringValue(item.senderUserId ?? item.sender_user_id),
roomId: stringValue(item.roomId ?? item.room_id),
regionId: numberValue(item.regionId ?? item.region_id),
packetType: stringValue(item.packetType ?? item.packet_type),
@ -164,7 +164,7 @@ function normalizeClaim(item: RawClaim): RedPacketClaimDto {
claimId: stringValue(item.claimId ?? item.claim_id),
commandId: stringValue(item.commandId ?? item.command_id),
packetId: stringValue(item.packetId ?? item.packet_id),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
amount: numberValue(item.amount),
walletTransactionId: stringValue(item.walletTransactionId ?? item.wallet_transaction_id),
status: stringValue(item.status),

View File

@ -23,7 +23,7 @@ export interface RegistrationRewardConfigPayload {
}
export interface RegistrationRewardClaimUserDto {
userId?: number;
userId?: string;
displayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
@ -34,7 +34,7 @@ export interface RegistrationRewardClaimUserDto {
export interface RegistrationRewardClaimDto {
claimId: string;
commandId?: string;
userId: number;
userId: string;
user?: RegistrationRewardClaimUserDto;
rewardDay?: string;
rewardType?: string;
@ -109,9 +109,9 @@ function normalizeClaim(item: RawClaim): RegistrationRewardClaimDto {
return {
claimId: stringValue(item.claimId ?? item.claim_id),
commandId: stringValue(item.commandId ?? item.command_id),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
user: {
userId: numberValue(rawUser.userId ?? rawUser.user_id),
userId: stringValue(rawUser.userId ?? rawUser.user_id),
displayUserId: stringValue(rawUser.displayUserId ?? rawUser.display_user_id),
prettyDisplayUserId: stringValue(rawUser.prettyDisplayUserId ?? rawUser.pretty_display_user_id),
prettyId: stringValue(rawUser.prettyId ?? rawUser.pretty_id),

View File

@ -166,7 +166,7 @@ test("resource APIs use generated admin paths", async () => {
commandId: "resource-group-grant-test",
groupId: 22,
reason: "manual",
targetUserId: 1001,
targetUserId: "1001",
});
const [listUrl, listInit] = vi.mocked(fetch).mock.calls[0];
@ -258,7 +258,7 @@ test("resource APIs use generated admin paths", async () => {
});
expect(String(grantGroupUrl)).toContain("/api/v1/admin/resource-grants/group");
expect(grantGroupInit?.method).toBe("POST");
expect(JSON.parse(String(grantGroupInit?.body))).toMatchObject({ groupId: 22, targetUserId: 1001 });
expect(JSON.parse(String(grantGroupInit?.body))).toMatchObject({ groupId: 22, targetUserId: "1001" });
});
test("resource mp4 layout batch API uses narrow update endpoint", async () => {

View File

@ -209,7 +209,7 @@ export interface ResourceGrantOperatorDto {
prettyId?: string;
name?: string;
source?: string;
userId?: number | string;
userId?: string;
username?: string;
}
@ -218,7 +218,7 @@ export interface ResourceGrantUserDto {
displayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
userId?: number | string;
userId?: string;
username?: string;
}
@ -236,7 +236,7 @@ export interface ResourceGrantDto {
reason?: string;
status?: string;
targetUser?: ResourceGrantUserDto;
targetUserId?: number | string;
targetUserId?: string;
updatedAtMs?: number;
}
@ -246,14 +246,14 @@ export interface GrantResourcePayload {
quantity: number;
reason: string;
resourceId: number;
targetUserId: number | string;
targetUserId: string;
}
export interface GrantResourceGroupPayload {
commandId: string;
groupId: number;
reason: string;
targetUserId: number | string;
targetUserId: string;
}
export interface ResourceGroupItemPayload {

View File

@ -389,13 +389,13 @@ export const resourceGrantFormSchema = z
targetUserId: z.union([z.string(), z.number()]),
})
.superRefine((value, context) => {
const targetUserId = Number(value.targetUserId);
const targetUserId = String(value.targetUserId || "").trim();
const resourceIds = grantResourceIds(value);
const groupId = Number(value.groupId || 0);
const quantity = Number(value.quantity || 1);
const durationDays = Number(value.durationDays || 0);
if (!Number.isInteger(targetUserId) || targetUserId <= 0) {
if (!/^[1-9]\d*$/.test(targetUserId)) {
context.addIssue({
code: "custom",
message: "请输入用户 ID",

View File

@ -39,7 +39,7 @@ export interface RoomTurnoverRewardConfigPayload {
export interface RoomTurnoverRewardSettlementDto {
settlementId: string;
roomId: string;
ownerUserId: number;
ownerUserId: string;
periodStartMs: number;
periodEndMs: number;
coinSpent: number;
@ -140,7 +140,7 @@ function normalizeSettlement(item: RawSettlement): RoomTurnoverRewardSettlementD
return {
settlementId: stringValue(item.settlementId ?? item.settlement_id),
roomId: stringValue(item.roomId ?? item.room_id),
ownerUserId: numberValue(item.ownerUserId ?? item.owner_user_id),
ownerUserId: stringValue(item.ownerUserId ?? item.owner_user_id),
periodStartMs: numberValue(item.periodStartMs ?? item.period_start_ms),
periodEndMs: numberValue(item.periodEndMs ?? item.period_end_ms),
coinSpent: numberValue(item.coinSpent ?? item.coin_spent),

View File

@ -26,7 +26,7 @@ export interface SevenDayCheckInConfigPayload {
}
export interface SevenDayCheckInClaimUserDto {
userId?: number;
userId?: string;
displayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
@ -37,7 +37,7 @@ export interface SevenDayCheckInClaimUserDto {
export interface SevenDayCheckInClaimDto {
claimId: string;
commandId?: string;
userId: number;
userId: string;
user?: SevenDayCheckInClaimUserDto;
cycleNo: number;
checkinDay?: string;
@ -121,9 +121,9 @@ function normalizeClaim(item: RawClaim): SevenDayCheckInClaimDto {
return {
claimId: stringValue(item.claimId ?? item.claim_id),
commandId: stringValue(item.commandId ?? item.command_id),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
user: {
userId: numberValue(rawUser.userId ?? rawUser.user_id),
userId: stringValue(rawUser.userId ?? rawUser.user_id),
displayUserId: stringValue(rawUser.displayUserId ?? rawUser.display_user_id),
prettyDisplayUserId: stringValue(rawUser.prettyDisplayUserId ?? rawUser.pretty_display_user_id),
prettyId: stringValue(rawUser.prettyId ?? rawUser.pretty_id),

View File

@ -41,7 +41,7 @@ export interface WeeklyStarCyclePayload {
export interface WeeklyStarEntryDto {
rankNo: number;
userId: number;
userId: string;
score: number;
firstScoredAtMs: number;
lastScoredAtMs: number;
@ -51,7 +51,7 @@ export interface WeeklyStarSettlementDto {
settlementId: string;
cycleId: string;
rankNo: number;
userId: number;
userId: string;
score: number;
resourceGroupId: number;
walletCommandId: string;
@ -161,7 +161,7 @@ function normalizeReward(item: Raw): WeeklyStarRewardDto {
function normalizeEntry(item: Raw): WeeklyStarEntryDto {
return {
rankNo: numberValue(item.rankNo ?? item.rank_no),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
score: numberValue(item.score),
firstScoredAtMs: numberValue(item.firstScoredAtMs ?? item.first_scored_at_ms),
lastScoredAtMs: numberValue(item.lastScoredAtMs ?? item.last_scored_at_ms),
@ -173,7 +173,7 @@ function normalizeSettlement(item: Raw): WeeklyStarSettlementDto {
settlementId: stringValue(item.settlementId ?? item.settlement_id),
cycleId: stringValue(item.cycleId ?? item.cycle_id),
rankNo: numberValue(item.rankNo ?? item.rank_no),
userId: numberValue(item.userId ?? item.user_id),
userId: stringValue(item.userId ?? item.user_id),
score: numberValue(item.score),
resourceGroupId: numberValue(item.resourceGroupId ?? item.resource_group_id),
walletCommandId: stringValue(item.walletCommandId ?? item.wallet_command_id),

View File

@ -3021,8 +3021,8 @@ export interface components {
joinEnabled?: boolean;
maxHosts?: number;
name?: string;
ownerUserId?: number;
parentBdUserId?: number;
ownerUserId?: string;
parentBdUserId?: string;
regionId?: number;
status?: string;
updatedAtMs?: number;
@ -3030,12 +3030,12 @@ export interface components {
BDProfile: {
createdAtMs?: number;
createdByUserId?: number;
parentLeaderUserId?: number;
parentLeaderUserId?: string;
regionId?: number;
role?: string;
status?: string;
updatedAtMs?: number;
userId: number;
userId: string;
};
CreateUserResult: {
initialPassword?: string;
@ -3057,7 +3057,7 @@ export interface components {
source?: string;
status?: string;
updatedAtMs?: number;
userId: number;
userId: string;
};
Log: {
[key: string]: unknown;

View File

@ -324,13 +324,13 @@ export interface RechargeBillDto {
policyVersion?: string;
rechargeType?: string;
sellerRegionId?: number;
sellerUserId?: number;
sellerUserId?: string;
status?: string;
targetRegionId?: number;
transactionId: string;
usdMinorAmount: number;
user?: RechargeBillUserDto;
userId: number;
userId: string;
seller?: RechargeBillUserDto;
}
@ -441,7 +441,7 @@ export interface BDProfileDto {
displayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
parentLeaderUserId?: number;
parentLeaderUserId?: string;
parentLeaderDisplayUserId?: string;
parentLeaderUsername?: string;
parentLeaderAvatar?: string;
@ -452,7 +452,7 @@ export interface BDProfileDto {
status?: string;
subBdCount?: number;
updatedAtMs?: number;
userId: number;
userId: string;
username?: string;
}
@ -466,11 +466,11 @@ export interface AgencyDto {
ownerAvatar?: string;
ownerDisplayUserId?: string;
ownerUsername?: string;
ownerUserId?: number;
ownerUserId?: string;
parentBdAvatar?: string;
parentBdDisplayUserId?: string;
parentBdUsername?: string;
parentBdUserId?: number;
parentBdUserId?: string;
regionId?: number;
regionName?: string;
status?: string;
@ -484,7 +484,7 @@ export interface HostProfileDto {
currentAgencyName?: string;
currentAgencyOwnerAvatar?: string;
currentAgencyOwnerDisplayUserId?: string;
currentAgencyOwnerUserId?: number;
currentAgencyOwnerUserId?: string;
currentAgencyOwnerUsername?: string;
currentMembershipId?: number;
diamond?: number;
@ -497,7 +497,7 @@ export interface HostProfileDto {
source?: string;
status?: string;
updatedAtMs?: number;
userId: number;
userId: string;
username?: string;
}
@ -516,7 +516,7 @@ export interface CoinSellerDto {
status?: string;
totalRechargeUsdtMicro?: number;
updatedAtMs?: number;
userId: number;
userId: string;
username?: string;
}
@ -1038,7 +1038,7 @@ export interface HostCommandPayload {
export interface CreateBDLeaderPayload extends HostCommandPayload {
positionAlias?: string;
targetUserId: number;
targetUserId: string;
}
export interface BDLeaderPositionAliasPayload extends HostCommandPayload {
@ -1046,8 +1046,8 @@ export interface BDLeaderPositionAliasPayload extends HostCommandPayload {
}
export interface CreateBDPayload extends HostCommandPayload {
parentLeaderUserId: number;
targetUserId: number;
parentLeaderUserId: string;
targetUserId: string;
}
export interface BDStatusPayload extends HostCommandPayload {
@ -1056,7 +1056,7 @@ export interface BDStatusPayload extends HostCommandPayload {
export interface CreateCoinSellerPayload extends HostCommandPayload {
contact?: string;
targetUserId: number;
targetUserId: string;
}
export interface CoinSellerStatusPayload extends BDStatusPayload {
@ -1066,8 +1066,8 @@ export interface CoinSellerStatusPayload extends BDStatusPayload {
export interface CreateAgencyPayload extends HostCommandPayload {
joinEnabled: boolean;
name: string;
ownerUserId: number;
parentBdUserId: number;
ownerUserId: string;
parentBdUserId: string;
}
export interface AgencyJoinEnabledPayload extends HostCommandPayload {

View File

@ -1,10 +1,15 @@
import InputBase from "@mui/material/InputBase";
import Search from "@mui/icons-material/Search";
export function SearchBox({ autoFocus = false, className = "", onChange, placeholder, value }) {
export function SearchBox({ autoFocus = false, className = "", label = "搜索", onChange, placeholder, value }) {
const hasValue = value !== undefined && value !== null && String(value).length > 0;
return (
<label className={["search-box", className].filter(Boolean).join(" ")}>
<Search fontSize="small" />
<label className={["search-box", hasValue ? "search-box--active" : "", className].filter(Boolean).join(" ")}>
<span className="search-box__label">
<Search fontSize="inherit" />
<span>{label}</span>
</span>
<InputBase
autoFocus={autoFocus}
value={value}

View File

@ -28,6 +28,7 @@ export function TimeRangeFilter({ className = "", disabled = false, label = "时
const open = Boolean(anchorEl);
const days = useMemo(() => calendarDays(viewDate), [viewDate]);
const selectedDate = dateForField(draft, activeField, viewDate);
const hasRange = Boolean(range.startMs || range.endMs);
useEffect(() => {
if (!open) {
@ -110,14 +111,17 @@ export function TimeRangeFilter({ className = "", disabled = false, label = "时
<>
<Button
className={[styles.trigger, className].filter(Boolean).join(" ")}
endIcon={<KeyboardArrowDownOutlined fontSize="small" />}
startIcon={<AccessTimeOutlined fontSize="small" />}
aria-expanded={open}
aria-haspopup="dialog"
disabled={disabled}
onClick={openPopover}
>
<span className={styles.triggerText}>{displayLabel}</span>
<span className={styles.triggerMeta}>
<AccessTimeOutlined fontSize="inherit" />
<span>{label}</span>
</span>
<span className={styles.triggerText}>{hasRange ? displayLabel : "不限"}</span>
<KeyboardArrowDownOutlined className={styles.triggerChevron} fontSize="small" />
</Button>
<Popover
anchorEl={anchorEl}

View File

@ -1,9 +1,15 @@
.trigger {
position: relative;
display: inline-grid !important;
grid-template-columns: minmax(0, 1fr) 18px;
flex: 0 0 auto;
justify-content: flex-start !important;
align-items: end !important;
justify-content: stretch !important;
gap: 0 !important;
width: auto;
min-width: 132px !important;
max-width: min(420px, 100%);
padding: 7px 28px 3px var(--control-padding-x) !important;
border-color: var(--border) !important;
background: var(--bg-input-strong) !important;
box-shadow: none !important;
@ -21,24 +27,50 @@
transform: none !important;
}
.trigger :global(.MuiButton-startIcon) {
margin-right: var(--space-2);
margin-left: 0;
.triggerMeta {
position: absolute;
top: -7px;
left: var(--space-2);
display: inline-flex;
max-width: calc(100% - var(--space-4));
align-items: center;
gap: 4px;
overflow: hidden;
padding: 0 var(--space-1);
border-radius: 4px;
background: var(--bg-card);
color: var(--text-tertiary);
font-size: 11px;
font-weight: 650;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.trigger :global(.MuiButton-endIcon) {
margin-right: 0;
margin-left: var(--space-4);
.trigger:hover .triggerMeta,
.trigger:focus-visible .triggerMeta {
color: var(--primary);
}
.triggerText {
flex: 0 1 auto;
overflow: hidden;
color: var(--text-primary);
font-size: var(--control-font-size);
line-height: var(--control-line-height);
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.triggerChevron {
position: absolute;
top: 50%;
right: var(--space-2);
color: var(--text-tertiary);
transform: translateY(-50%);
}
.paper {
overflow: hidden;
border: 1px solid var(--border);

View File

@ -1,16 +1,17 @@
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import TextField from "@mui/material/TextField";
const ranges = ["近 1 小时", "近 24 小时", "近 7 天"];
export function TimeRangeSelect({ className = "", onChange, value }) {
export function TimeRangeSelect({ className = "", label = "时间", onChange, value }) {
return (
<Select
<TextField
className={["time-select", className].filter(Boolean).join(" ")}
label={label}
select
value={value}
onChange={(event) => onChange(event.target.value)}
size="small"
sx={{ width: 116, height: 36, fontSize: "var(--admin-font-size)" }}
variant="outlined"
>
{ranges.map((range) => (
@ -18,6 +19,6 @@ export function TimeRangeSelect({ className = "", onChange, value }) {
{range}
</MenuItem>
))}
</Select>
</TextField>
);
}

View File

@ -165,11 +165,11 @@
}
.app-switch {
flex: 0 0 156px;
flex: 0 0 164px;
}
.timezone-switch {
flex: 0 0 124px;
flex: 0 0 140px;
}
.app-switch .MuiOutlinedInput-root,
@ -202,14 +202,15 @@
}
.search-box {
position: relative;
display: flex;
flex: 1 1 420px;
min-width: 180px;
max-width: 520px;
height: var(--control-height);
align-items: center;
gap: var(--space-3);
padding: 0 var(--space-3);
align-items: flex-end;
gap: 0;
padding: 7px var(--control-padding-x) 3px;
border: 1px solid var(--border);
border-radius: var(--radius-control);
background: var(--bg-input);
@ -222,31 +223,58 @@
transform var(--motion-base) var(--ease-emphasized);
}
.search-box__label {
position: absolute;
top: -7px;
left: var(--space-2);
display: inline-flex;
max-width: calc(100% - var(--space-4));
align-items: center;
gap: 4px;
overflow: hidden;
padding: 0 var(--space-1);
border-radius: 4px;
background: var(--bg-card);
color: var(--text-tertiary);
font-size: 11px;
font-weight: 650;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-box:focus-within {
border-color: var(--primary-border-active);
background: var(--bg-input-strong);
transform: translateY(-1px);
}
.search-box svg {
.search-box__label svg {
flex: 0 0 auto;
transition:
color var(--motion-base) var(--ease-standard),
transform var(--motion-base) var(--ease-emphasized);
}
.search-box:focus-within svg {
.search-box:focus-within .search-box__label,
.search-box--active .search-box__label {
color: var(--primary);
}
.search-box:focus-within .search-box__label svg {
color: var(--primary);
transform: scale(1.08);
}
.search-trigger {
display: flex;
position: relative;
display: grid;
width: min(360px, 34vw);
min-width: 260px;
height: var(--control-height);
align-items: center;
gap: var(--space-3);
padding: 0 var(--space-3);
align-items: end;
gap: 0;
padding: 7px var(--control-padding-x) 3px;
border: 1px solid var(--border);
border-radius: var(--radius-control);
background: var(--bg-input);
@ -262,8 +290,36 @@
transform var(--motion-base) var(--ease-emphasized);
}
.search-trigger span {
.search-trigger__label {
position: absolute;
top: -7px;
left: var(--space-2);
display: inline-flex;
max-width: calc(100% - var(--space-4));
align-items: center;
gap: 4px;
overflow: hidden;
padding: 0 var(--space-1);
border-radius: 4px;
background: var(--bg-card);
color: var(--text-tertiary);
font-size: 11px;
font-weight: 650;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-trigger__label svg {
flex: 0 0 auto;
}
.search-trigger__value {
overflow: hidden;
color: var(--text-primary);
font-size: var(--control-font-size);
font-weight: 650;
line-height: var(--control-line-height);
text-overflow: ellipsis;
white-space: nowrap;
}
@ -276,16 +332,22 @@
transform: translateY(-1px);
}
.search-trigger:hover .search-trigger__label,
.search-trigger:focus-visible .search-trigger__label {
color: var(--primary);
}
.search-box input {
width: 100%;
height: 20px;
height: var(--control-line-height);
min-width: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--text-primary);
font-size: var(--admin-font-size);
line-height: 20px;
font-size: var(--control-font-size);
font-weight: 650;
line-height: var(--control-line-height);
outline: 0;
}
@ -293,15 +355,10 @@
color: var(--text-tertiary);
}
.time-select {
width: 116px;
height: var(--control-height);
border: 1px solid var(--border);
border-radius: var(--radius-control);
background: var(--bg-input);
backdrop-filter: var(--glass-blur);
color: var(--text-secondary);
-webkit-backdrop-filter: var(--glass-blur);
.time-select.MuiTextField-root,
.time-select.MuiFormControl-root {
flex: 0 0 124px;
width: 124px;
}
.icon-button {

View File

@ -108,6 +108,10 @@
--admin-control-height: var(--control-height);
--admin-control-font-size: var(--control-font-size);
--admin-control-line-height: var(--control-line-height);
--admin-floating-label-font-size: 11px;
--admin-floating-label-line-height: 14px;
--admin-floating-label-offset-x: 9px;
--admin-floating-label-offset-y: -7px;
}
.region-select.MuiTextField-root,
@ -219,12 +223,19 @@
color: var(--text-tertiary);
font-size: var(--admin-control-font-size);
line-height: var(--admin-control-line-height);
transform: translate(12px, 6px) scale(1);
letter-spacing: 0;
transform: translate(var(--control-padding-x), calc((var(--admin-control-height) - var(--admin-control-line-height)) / 2)) scale(1);
}
.MuiTextField-root .MuiInputLabel-outlined.MuiInputLabel-shrink,
.MuiFormControl-root .MuiInputLabel-outlined.MuiInputLabel-shrink {
transform: translate(12px, -7px) scale(0.75);
max-width: calc((100% - 18px) / 0.78);
padding: 0 var(--space-1);
border-radius: 4px;
background: var(--bg-card);
font-size: var(--admin-floating-label-font-size);
line-height: var(--admin-floating-label-line-height);
transform: translate(var(--admin-floating-label-offset-x), var(--admin-floating-label-offset-y)) scale(0.78);
}
.MuiTextField-root .MuiFormLabel-root.Mui-focused,