修复报错

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" "type": "string"
}, },
"ownerUserId": { "ownerUserId": {
"type": "integer" "type": "string"
}, },
"parentBdUserId": { "parentBdUserId": {
"type": "integer" "type": "string"
}, },
"regionId": { "regionId": {
"type": "integer" "type": "integer"
@ -4919,7 +4919,7 @@
"type": "integer" "type": "integer"
}, },
"parentLeaderUserId": { "parentLeaderUserId": {
"type": "integer" "type": "string"
}, },
"regionId": { "regionId": {
"type": "integer" "type": "integer"
@ -4934,7 +4934,7 @@
"type": "integer" "type": "integer"
}, },
"userId": { "userId": {
"type": "integer" "type": "string"
} }
} }
}, },
@ -4991,7 +4991,7 @@
"type": "integer" "type": "integer"
}, },
"userId": { "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(fetchSelfGameStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "lalu", gameId: "all" }));
expect(screen.getByText("Dice / Rock H5 聚合统计")).toBeTruthy(); expect(screen.getByText("Dice / Rock H5 聚合统计")).toBeTruthy();
expect(screen.getByText("对局数")).toBeTruthy(); expect(screen.getByText("创建局")).toBeTruthy();
}); });
async function flushEffects() { async function flushEffects() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,15 @@ const commandContactSchema = z.object({
reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default(""), 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( const entityIdSchema = z.preprocess(
(value) => { (value) => {
if (value === null || value === undefined) { if (value === null || value === undefined) {
@ -24,14 +32,14 @@ const entityIdSchema = z.preprocess(
const optionalUserIdSchema = z.preprocess( const optionalUserIdSchema = z.preprocess(
(value) => { (value) => {
if (value === null || value === undefined) { if (value === null || value === undefined) {
return 0; return "0";
} }
if (typeof value === "string" && value.trim() === "") { 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 regionIdSchema = z.coerce.number().int().positive("请输入有效区域 ID");
const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").default(0); const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").default(0);

View File

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

View File

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

View File

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

View File

@ -180,17 +180,16 @@ function payloadFromForm(form) {
throw new Error("累计充值金币门槛必须大于 0"); throw new Error("累计充值金币门槛必须大于 0");
} }
if (rewardType === "valid_invite" && thresholdValidInviteCount <= 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) { if (rewardCoinAmount <= 0) {
throw new Error("奖励金币必须大于 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 = const tierName =
String(tier.tierName || "").trim() || rewardType === "recharge"
(rewardType === "recharge" ? `Cumulative Recharge ${formatNumber(thresholdCoinAmount)} coins`
? `累计充值 ${formatNumber(thresholdCoinAmount)} 金币` : `Valid Users ${formatNumber(thresholdValidInviteCount)} people`;
: `有效邀请 ${formatNumber(thresholdValidInviteCount)}`);
return { return {
tier_id: Number(tier.tierId || 0), tier_id: Number(tier.tierId || 0),
reward_type: rewardType, reward_type: rewardType,
@ -220,5 +219,13 @@ function payloadFromForm(form) {
} }
function formatNumber(value) { 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 = [ const rewardTypeOptions = [
["recharge", "累计充值"], ["recharge", "累计充值"],
["valid_invite", "有效人数"], ["valid_invite", "Valid Users"],
["invite_inviter", "邀请人即时"], ["invite_inviter", "邀请人即时"],
["invite_invitee", "被邀请人即时"], ["invite_invitee", "被邀请人即时"],
]; ];
@ -202,7 +202,7 @@ function claimStatusLabel(status) {
function thresholdLabel(claim) { function thresholdLabel(claim) {
if (claim.rewardType === "valid_invite") { if (claim.rewardType === "valid_invite") {
return `${formatNumber(claim.thresholdValidInviteCount)} `; return `${formatNumber(claim.thresholdValidInviteCount)} people`;
} }
if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") { if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") {
return claim.tierId ? `关联用户 ${claim.tierId}` : "-"; return claim.tierId ? `关联用户 ${claim.tierId}` : "-";
@ -214,7 +214,7 @@ function reachedLabel(claim) {
if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") { if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") {
return "1 人"; return "1 人";
} }
const unit = claim.rewardType === "valid_invite" ? "" : "金币"; const unit = claim.rewardType === "valid_invite" ? "people" : "金币";
return `${formatNumber(claim.reachedValue)} ${unit}`; return `${formatNumber(claim.reachedValue)} ${unit}`;
} }

View File

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

View File

@ -23,7 +23,7 @@ export interface RegistrationRewardConfigPayload {
} }
export interface RegistrationRewardClaimUserDto { export interface RegistrationRewardClaimUserDto {
userId?: number; userId?: string;
displayUserId?: string; displayUserId?: string;
prettyDisplayUserId?: string; prettyDisplayUserId?: string;
prettyId?: string; prettyId?: string;
@ -34,7 +34,7 @@ export interface RegistrationRewardClaimUserDto {
export interface RegistrationRewardClaimDto { export interface RegistrationRewardClaimDto {
claimId: string; claimId: string;
commandId?: string; commandId?: string;
userId: number; userId: string;
user?: RegistrationRewardClaimUserDto; user?: RegistrationRewardClaimUserDto;
rewardDay?: string; rewardDay?: string;
rewardType?: string; rewardType?: string;
@ -109,9 +109,9 @@ function normalizeClaim(item: RawClaim): RegistrationRewardClaimDto {
return { return {
claimId: stringValue(item.claimId ?? item.claim_id), claimId: stringValue(item.claimId ?? item.claim_id),
commandId: stringValue(item.commandId ?? item.command_id), commandId: stringValue(item.commandId ?? item.command_id),
userId: numberValue(item.userId ?? item.user_id), userId: stringValue(item.userId ?? item.user_id),
user: { user: {
userId: numberValue(rawUser.userId ?? rawUser.user_id), userId: stringValue(rawUser.userId ?? rawUser.user_id),
displayUserId: stringValue(rawUser.displayUserId ?? rawUser.display_user_id), displayUserId: stringValue(rawUser.displayUserId ?? rawUser.display_user_id),
prettyDisplayUserId: stringValue(rawUser.prettyDisplayUserId ?? rawUser.pretty_display_user_id), prettyDisplayUserId: stringValue(rawUser.prettyDisplayUserId ?? rawUser.pretty_display_user_id),
prettyId: stringValue(rawUser.prettyId ?? rawUser.pretty_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", commandId: "resource-group-grant-test",
groupId: 22, groupId: 22,
reason: "manual", reason: "manual",
targetUserId: 1001, targetUserId: "1001",
}); });
const [listUrl, listInit] = vi.mocked(fetch).mock.calls[0]; 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(String(grantGroupUrl)).toContain("/api/v1/admin/resource-grants/group");
expect(grantGroupInit?.method).toBe("POST"); 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 () => { test("resource mp4 layout batch API uses narrow update endpoint", async () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,15 @@
import InputBase from "@mui/material/InputBase"; import InputBase from "@mui/material/InputBase";
import Search from "@mui/icons-material/Search"; 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 ( return (
<label className={["search-box", className].filter(Boolean).join(" ")}> <label className={["search-box", hasValue ? "search-box--active" : "", className].filter(Boolean).join(" ")}>
<Search fontSize="small" /> <span className="search-box__label">
<Search fontSize="inherit" />
<span>{label}</span>
</span>
<InputBase <InputBase
autoFocus={autoFocus} autoFocus={autoFocus}
value={value} value={value}

View File

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

View File

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

View File

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

View File

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

View File

@ -108,6 +108,10 @@
--admin-control-height: var(--control-height); --admin-control-height: var(--control-height);
--admin-control-font-size: var(--control-font-size); --admin-control-font-size: var(--control-font-size);
--admin-control-line-height: var(--control-line-height); --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, .region-select.MuiTextField-root,
@ -219,12 +223,19 @@
color: var(--text-tertiary); color: var(--text-tertiary);
font-size: var(--admin-control-font-size); font-size: var(--admin-control-font-size);
line-height: var(--admin-control-line-height); 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, .MuiTextField-root .MuiInputLabel-outlined.MuiInputLabel-shrink,
.MuiFormControl-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, .MuiTextField-root .MuiFormLabel-root.Mui-focused,