-
{formatEntityID(item.assignedUserId)}
+
{item.assignedAtMs ? formatMillis(item.assignedAtMs) : formatMillis(item.createdAtMs)}
@@ -366,6 +367,32 @@ function idColumns(page) {
];
}
+function AssignedUserCell({ item }) {
+ const { openAppUserDetail } = useAppUserDetail();
+ const assignedUserId = formatEntityID(item.assignedUserId);
+ if (!item.assignedUserId || !openAppUserDetail) {
+ return
{assignedUserId};
+ }
+
+ return (
+
+ );
+}
+
function PoolDialog({ page }) {
const editing = Boolean(page.activePool);
return (
diff --git a/src/features/cumulative-recharge-reward/api.ts b/src/features/cumulative-recharge-reward/api.ts
index 668c603..2f3c89f 100644
--- a/src/features/cumulative-recharge-reward/api.ts
+++ b/src/features/cumulative-recharge-reward/api.ts
@@ -39,6 +39,8 @@ export interface CumulativeRechargeRewardConfigPayload {
export interface CumulativeRechargeRewardGrantUserDto {
userId?: number;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
username?: string;
avatar?: string;
}
@@ -145,6 +147,8 @@ function normalizeGrant(item: RawGrant): CumulativeRechargeRewardGrantDto {
user: {
userId: numberValue(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),
username: stringValue(rawUser.username),
avatar: stringValue(rawUser.avatar),
},
diff --git a/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx b/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx
index 53fd867..67820d6 100644
--- a/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx
+++ b/src/features/cumulative-recharge-reward/pages/CumulativeRechargeRewardPage.jsx
@@ -1,9 +1,9 @@
-import Avatar from "@mui/material/Avatar";
import { CumulativeRechargeRewardConfigDrawer } from "@/features/cumulative-recharge-reward/components/CumulativeRechargeRewardConfigDrawer.jsx";
import { CumulativeRechargeRewardConfigSummary } from "@/features/cumulative-recharge-reward/components/CumulativeRechargeRewardConfigSummary.jsx";
import { useCumulativeRechargeRewardPage } from "@/features/cumulative-recharge-reward/hooks/useCumulativeRechargeRewardPage.js";
import styles from "@/features/cumulative-recharge-reward/cumulative-recharge-reward.module.css";
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
@@ -179,21 +179,7 @@ export function CumulativeRechargeRewardPage() {
function GrantUser({ grant }) {
const user = grant.user || {};
- return (
-
-
-
- {user.username || `用户 ${grant.userId}`}
-
- {user.displayUserId ? `短号 ${user.displayUserId}` : `ID ${grant.userId}`}
-
-
-
- );
+ return
;
}
function grantStatusLabel(status) {
diff --git a/src/features/daily-tasks/api.ts b/src/features/daily-tasks/api.ts
index 806fceb..405bbf6 100644
--- a/src/features/daily-tasks/api.ts
+++ b/src/features/daily-tasks/api.ts
@@ -9,6 +9,13 @@ export interface TaskDefinitionDto {
metricType: string;
title: string;
description?: string;
+ audienceType: string;
+ iconKey: string;
+ iconUrl: string;
+ actionType: string;
+ actionParam: string;
+ actionPayloadJson: string;
+ dimensionFilterJson: string;
targetValue: number;
targetUnit: string;
rewardCoinAmount: number;
@@ -29,6 +36,13 @@ export interface TaskDefinitionPayload {
metric_type: string;
title: string;
description: string;
+ audience_type: string;
+ icon_key: string;
+ icon_url: string;
+ action_type: string;
+ action_param: string;
+ action_payload_json: string;
+ dimension_filter_json: string;
target_value: number;
target_unit: string;
reward_coin_amount: number;
@@ -100,6 +114,13 @@ function normalizeTaskDefinition(task: RawTaskDefinitionDto): TaskDefinitionDto
metricType: stringValue(task.metricType ?? task.metric_type),
title: stringValue(task.title),
description: stringValue(task.description),
+ audienceType: stringValue(task.audienceType ?? task.audience_type),
+ iconKey: stringValue(task.iconKey ?? task.icon_key),
+ iconUrl: stringValue(task.iconUrl ?? task.icon_url),
+ actionType: stringValue(task.actionType ?? task.action_type),
+ actionParam: stringValue(task.actionParam ?? task.action_param),
+ actionPayloadJson: stringValue(task.actionPayloadJson ?? task.action_payload_json),
+ dimensionFilterJson: stringValue(task.dimensionFilterJson ?? task.dimension_filter_json),
targetValue: numberValue(task.targetValue ?? task.target_value),
targetUnit: stringValue(task.targetUnit ?? task.target_unit),
rewardCoinAmount: numberValue(task.rewardCoinAmount ?? task.reward_coin_amount),
diff --git a/src/features/daily-tasks/constants.js b/src/features/daily-tasks/constants.js
index 04fbf1e..8e8ddf8 100644
--- a/src/features/daily-tasks/constants.js
+++ b/src/features/daily-tasks/constants.js
@@ -29,3 +29,43 @@ export const taskUnitLabels = {
count: "次数",
minute: "分钟",
};
+
+export const taskAudienceOptions = [
+ ["all", "全部用户"],
+ ["newbie", "新人专属"],
+];
+
+export const taskAudienceLabels = {
+ all: "全部用户",
+ newbie: "新人专属",
+};
+
+export const taskActionOptions = [
+ ["none", "无跳转"],
+ ["room_random", "随机房间"],
+ ["room_random_game", "随机房间并打开游戏"],
+ ["wallet", "钱包"],
+ ["gift_panel", "礼物面板"],
+ ["gift_panel_specific", "指定礼物面板"],
+];
+
+export const taskActionLabels = {
+ gift_panel: "礼物面板",
+ gift_panel_specific: "指定礼物面板",
+ none: "无跳转",
+ room_random: "随机房间",
+ room_random_game: "随机房间并打开游戏",
+ wallet: "钱包",
+};
+
+export const taskMetricOptions = [
+ ["game_spend_coin", "游戏消耗金币"],
+ ["gift_spend_coin", "普通礼物消耗金币"],
+ ["gift_send_count", "赠送礼物次数"],
+ ["lucky_gift_spend_coin", "幸运礼物消耗金币"],
+ ["lucky_gift_send_count", "赠送幸运礼物次数"],
+ ["recharge_coin", "累计充值金币"],
+ ["cp_relationship_created", "组成 CP"],
+ ["mic_online_minute", "麦克风使用时长"],
+ ["mood_publish_count", "发布心情"],
+];
diff --git a/src/features/daily-tasks/daily-tasks.module.css b/src/features/daily-tasks/daily-tasks.module.css
index a8b0972..eef791b 100644
--- a/src/features/daily-tasks/daily-tasks.module.css
+++ b/src/features/daily-tasks/daily-tasks.module.css
@@ -43,6 +43,28 @@
line-height: 1;
}
+.taskIcon {
+ display: inline-flex;
+ width: 32px;
+ height: 32px;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ background: var(--surface-muted);
+ color: var(--text-secondary);
+ font-size: var(--admin-font-size-sm);
+ font-weight: 700;
+}
+
+.taskIcon img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
.statusCell {
display: inline-flex;
min-width: 0;
diff --git a/src/features/daily-tasks/hooks/useDailyTasksPage.js b/src/features/daily-tasks/hooks/useDailyTasksPage.js
index f1e4332..0f7f939 100644
--- a/src/features/daily-tasks/hooks/useDailyTasksPage.js
+++ b/src/features/daily-tasks/hooks/useDailyTasksPage.js
@@ -14,10 +14,17 @@ import { dailyTaskFormSchema } from "@/features/daily-tasks/schema";
const pageSize = 50;
const emptyData = { items: [], page: 1, pageSize, total: 0 };
const emptyForm = (task = {}) => ({
+ actionParam: task.actionParam || "",
+ actionPayloadJson: task.actionPayloadJson || "{}",
+ actionType: task.actionType || "none",
+ audienceType: task.audienceType || "all",
category: task.category || "",
description: task.description || "",
+ dimensionFilterJson: task.dimensionFilterJson || "{}",
effectiveFrom: msToDatetimeLocal(task.effectiveFromMs),
effectiveTo: msToDatetimeLocal(task.effectiveToMs),
+ iconKey: task.iconKey || "",
+ iconUrl: task.iconUrl || "",
metricType: task.metricType || "",
rewardCoinAmount: task.rewardCoinAmount === 0 || task.rewardCoinAmount ? String(task.rewardCoinAmount) : "",
sortOrder: task.sortOrder === 0 || task.sortOrder ? String(task.sortOrder) : "0",
@@ -173,10 +180,17 @@ export function useDailyTasksPage() {
function buildTaskPayload(form) {
return {
+ action_param: form.actionParam.trim(),
+ action_payload_json: normalizedJSONObject(form.actionPayloadJson),
+ action_type: form.actionType,
+ audience_type: form.audienceType,
category: form.category.trim(),
description: form.description.trim(),
+ dimension_filter_json: normalizedJSONObject(form.dimensionFilterJson),
effective_from_ms: datetimeLocalToMs(form.effectiveFrom),
effective_to_ms: datetimeLocalToMs(form.effectiveTo),
+ icon_key: form.iconKey.trim(),
+ icon_url: form.iconUrl.trim(),
metric_type: form.metricType.trim(),
reward_coin_amount: Number(form.rewardCoinAmount),
sort_order: Number(form.sortOrder || 0),
@@ -188,6 +202,22 @@ function buildTaskPayload(form) {
};
}
+function normalizedJSONObject(value) {
+ const trimmed = String(value || "").trim();
+ if (!trimmed) {
+ return "{}";
+ }
+ try {
+ const parsed = JSON.parse(trimmed);
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
+ return "{}";
+ }
+ return JSON.stringify(parsed);
+ } catch {
+ return trimmed;
+ }
+}
+
function resetSetter(setter, setPage) {
return (value) => {
setter(value);
diff --git a/src/features/daily-tasks/pages/DailyTaskListPage.jsx b/src/features/daily-tasks/pages/DailyTaskListPage.jsx
index 6ca870e..12eacb8 100644
--- a/src/features/daily-tasks/pages/DailyTaskListPage.jsx
+++ b/src/features/daily-tasks/pages/DailyTaskListPage.jsx
@@ -23,6 +23,11 @@ import {
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { formatMillis } from "@/shared/utils/time.js";
import {
+ taskActionLabels,
+ taskActionOptions,
+ taskAudienceLabels,
+ taskAudienceOptions,
+ taskMetricOptions,
taskStatusLabels,
taskStatusOptions,
taskTypeOptions,
@@ -50,6 +55,20 @@ const baseColumns = [
),
},
+ {
+ key: "display",
+ label: "属性 / 跳转",
+ width: "minmax(220px, 1fr)",
+ render: (task) => (
+ 0
? {
@@ -214,6 +233,7 @@ export function DailyTaskListPage() {
function TaskIdentity({ task }) {
return (
+
{taskTypeLabel(task.taskType)}
{task.title || task.taskId}
@@ -223,6 +243,15 @@ function TaskIdentity({ task }) {
);
}
+function TaskIconPreview({ task }) {
+ const label = String(task.iconKey || "任务").slice(0, 2);
+ return (
+
+ {task.iconUrl ?
: label}
+
+ );
+}
+
function TaskStatusSwitch({ page, task }) {
const checked = task.status === "active";
const disabled =
@@ -314,9 +343,16 @@ function TaskFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open
disabled={disabled}
label="指标类型"
required
+ select
value={form.metricType}
onChange={(event) => page.setForm({ ...form, metricType: event.target.value })}
- />
+ >
+ {taskMetricOptions.map(([value, label]) => (
+
+ ))}
+
+
+
+ page.setForm({ ...form, audienceType: event.target.value })}
+ >
+ {taskAudienceOptions.map(([value, label]) => (
+
+ ))}
+
+ page.setForm({ ...form, actionType: event.target.value })}
+ >
+ {taskActionOptions.map(([value, label]) => (
+
+ ))}
+
+ page.setForm({ ...form, actionParam: event.target.value })}
+ />
+ page.setForm({ ...form, iconKey: event.target.value })}
+ />
+ page.setForm({ ...form, iconUrl: event.target.value })}
+ />
+ page.setForm({ ...form, dimensionFilterJson: event.target.value })}
+ />
+ page.setForm({ ...form, actionPayloadJson: event.target.value })}
+ />
+
+
0 && effectiveToMs > 0 && effectiveToMs <= effectiveFromMs) {
context.addIssue({ code: "custom", message: "结束时间必须晚于开始时间", path: ["effectiveTo"] });
}
+ if (!isJSONObject(value.actionPayloadJson)) {
+ context.addIssue({ code: "custom", message: "跳转扩展必须是 JSON 对象", path: ["actionPayloadJson"] });
+ }
+ if (!isJSONObject(value.dimensionFilterJson)) {
+ context.addIssue({ code: "custom", message: "维度过滤必须是 JSON 对象", path: ["dimensionFilterJson"] });
+ }
});
export type DailyTaskForm = z.infer;
@@ -52,3 +65,16 @@ function datetimeLocalToMs(value: unknown) {
const parsed = new Date(trimmed).getTime();
return Number.isFinite(parsed) ? parsed : -1;
}
+
+function isJSONObject(value: unknown) {
+ const trimmed = String(value || "").trim();
+ if (!trimmed) {
+ return true;
+ }
+ try {
+ const parsed = JSON.parse(trimmed);
+ return Boolean(parsed) && !Array.isArray(parsed) && typeof parsed === "object";
+ } catch {
+ return false;
+ }
+}
diff --git a/src/features/first-recharge-reward/api.ts b/src/features/first-recharge-reward/api.ts
index 84eda8a..2504bbc 100644
--- a/src/features/first-recharge-reward/api.ts
+++ b/src/features/first-recharge-reward/api.ts
@@ -47,6 +47,8 @@ export interface FirstRechargeRewardConfigPayload {
export interface FirstRechargeRewardClaimUserDto {
userId?: number;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
username?: string;
avatar?: string;
}
@@ -153,6 +155,8 @@ function normalizeClaim(item: RawClaim): FirstRechargeRewardClaimDto {
user: {
userId: numberValue(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),
username: stringValue(rawUser.username),
avatar: stringValue(rawUser.avatar),
},
diff --git a/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx b/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx
index e2d436a..51c2e51 100644
--- a/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx
+++ b/src/features/first-recharge-reward/pages/FirstRechargeRewardPage.jsx
@@ -1,9 +1,9 @@
-import Avatar from "@mui/material/Avatar";
import { FirstRechargeRewardConfigDrawer } from "@/features/first-recharge-reward/components/FirstRechargeRewardConfigDrawer.jsx";
import { FirstRechargeRewardConfigSummary } from "@/features/first-recharge-reward/components/FirstRechargeRewardConfigSummary.jsx";
import { useFirstRechargeRewardPage } from "@/features/first-recharge-reward/hooks/useFirstRechargeRewardPage.js";
import styles from "@/features/first-recharge-reward/first-recharge-reward.module.css";
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
@@ -152,21 +152,7 @@ export function FirstRechargeRewardPage() {
function ClaimUser({ claim }) {
const user = claim.user || {};
- return (
-
-
-
- {user.username || `用户 ${claim.userId}`}
-
- {user.displayUserId ? `短号 ${user.displayUserId}` : `ID ${claim.userId}`}
-
-
-
- );
+ return ;
}
function claimStatusLabel(status) {
diff --git a/src/features/games/api.ts b/src/features/games/api.ts
index b4647da..8e9972d 100644
--- a/src/features/games/api.ts
+++ b/src/features/games/api.ts
@@ -117,8 +117,79 @@ export interface DiceRobotPage {
serverTimeMs?: number;
}
+export interface RoomRpsStakeGiftDto {
+ giftId: string;
+ giftIdNumber?: number;
+ giftName?: string;
+ giftIconUrl?: string;
+ giftPriceCoin?: number;
+ enabled: boolean;
+ sortOrder: number;
+}
+
+export interface RoomRpsConfigDto {
+ appCode?: string;
+ gameId: string;
+ status: string;
+ challengeTimeoutMs: number;
+ revealCountdownMs: number;
+ stakeGifts: RoomRpsStakeGiftDto[];
+ createdAtMs?: number;
+ updatedAtMs?: number;
+}
+
+export interface RoomRpsPlayerDto {
+ userId: string;
+ userIdNumber?: number;
+ gesture: string;
+ result: string;
+ balanceAfter?: number;
+ joinedAtMs?: number;
+}
+
+export interface RoomRpsChallengeDto {
+ appCode?: string;
+ challengeId: string;
+ roomId: string;
+ regionId?: number;
+ status: string;
+ stakeGiftId: string;
+ stakeGiftIdNumber?: number;
+ stakeCoin: number;
+ initiator: RoomRpsPlayerDto;
+ challenger: RoomRpsPlayerDto;
+ winnerUserId: string;
+ winnerUserIdNumber?: number;
+ settlementStatus: string;
+ failureReason?: string;
+ timeoutAtMs?: number;
+ revealAtMs?: number;
+ createdAtMs?: number;
+ matchedAtMs?: number;
+ settledAtMs?: number;
+ updatedAtMs?: number;
+}
+
+export interface RoomRpsChallengePage {
+ items: RoomRpsChallengeDto[];
+ nextCursor?: string;
+ pageSize?: number;
+ serverTimeMs?: number;
+}
+
export type DiceConfigPayload = Omit;
+export interface RoomRpsConfigPayload {
+ status: string;
+ challengeTimeoutMs: number;
+ revealCountdownMs: number;
+ stakeGifts: Array<{
+ giftId: number;
+ enabled: boolean;
+ sortOrder: number;
+ }>;
+}
+
export interface DicePoolAdjustPayload {
amountCoin: number;
direction: "in" | "out";
@@ -146,6 +217,17 @@ export interface GameCatalogQuery {
cursor?: string;
}
+export interface RoomRpsChallengeQuery {
+ roomId?: string;
+ status?: string;
+ initiatorUserId?: string;
+ challengerUserId?: string;
+ startTimeMs?: number;
+ endTimeMs?: number;
+ pageSize?: number;
+ cursor?: string;
+}
+
export function listGamePlatforms(status = ""): Promise {
const endpoint = API_ENDPOINTS.listPlatforms;
return apiRequest(apiEndpointPath(API_OPERATIONS.listPlatforms), {
@@ -227,9 +309,12 @@ export function syncGamePlatformCatalog(platformCode: string, payload: GameSyncP
export function listSelfGames(): Promise<{ items: DiceConfigDto[]; serverTimeMs?: number }> {
const endpoint = API_ENDPOINTS.listSelfGames;
- return apiRequest<{ items: DiceConfigDto[]; serverTimeMs?: number }>(apiEndpointPath(API_OPERATIONS.listSelfGames), {
- method: endpoint.method,
- }).then((page) => ({
+ return apiRequest<{ items: DiceConfigDto[]; serverTimeMs?: number }>(
+ apiEndpointPath(API_OPERATIONS.listSelfGames),
+ {
+ method: endpoint.method,
+ },
+ ).then((page) => ({
...page,
items: (page.items || []).map(normalizeDiceConfig),
}));
@@ -289,6 +374,65 @@ export function deleteDiceRobot(userId: string, gameId = "dice"): Promise<{ dele
});
}
+export function getRoomRpsConfig(): Promise<{ config: RoomRpsConfigDto; serverTimeMs?: number }> {
+ const endpoint = API_ENDPOINTS.getRoomRpsConfig;
+ return apiRequest<{ config: RoomRpsConfigDto; serverTimeMs?: number }>(
+ apiEndpointPath(API_OPERATIONS.getRoomRpsConfig),
+ { method: endpoint.method },
+ ).then((result) => ({ ...result, config: normalizeRoomRpsConfig(result.config || {}) }));
+}
+
+export function updateRoomRpsConfig(
+ payload: RoomRpsConfigPayload,
+): Promise<{ config: RoomRpsConfigDto; serverTimeMs?: number }> {
+ const endpoint = API_ENDPOINTS.updateRoomRpsConfig;
+ return apiRequest<{ config: RoomRpsConfigDto; serverTimeMs?: number }, RoomRpsConfigPayload>(
+ apiEndpointPath(API_OPERATIONS.updateRoomRpsConfig),
+ { body: payload, method: endpoint.method },
+ ).then((result) => ({ ...result, config: normalizeRoomRpsConfig(result.config || {}) }));
+}
+
+export function listRoomRpsChallenges(query: RoomRpsChallengeQuery = {}): Promise {
+ const endpoint = API_ENDPOINTS.listRoomRpsChallenges;
+ return apiRequest(apiEndpointPath(API_OPERATIONS.listRoomRpsChallenges), {
+ method: endpoint.method,
+ query: query as QueryParams,
+ }).then((page) => ({
+ ...page,
+ items: (page.items || []).map(normalizeRoomRpsChallenge),
+ }));
+}
+
+export function getRoomRpsChallenge(
+ challengeId: string,
+): Promise<{ challenge: RoomRpsChallengeDto; serverTimeMs?: number }> {
+ const endpoint = API_ENDPOINTS.getRoomRpsChallenge;
+ return apiRequest<{ challenge: RoomRpsChallengeDto; serverTimeMs?: number }>(
+ apiEndpointPath(API_OPERATIONS.getRoomRpsChallenge, { challenge_id: challengeId }),
+ { method: endpoint.method },
+ ).then((result) => ({ ...result, challenge: normalizeRoomRpsChallenge(result.challenge || {}) }));
+}
+
+export function retryRoomRpsSettlement(
+ challengeId: string,
+): Promise<{ challenge: RoomRpsChallengeDto; serverTimeMs?: number }> {
+ const endpoint = API_ENDPOINTS.retryRoomRpsSettlement;
+ return apiRequest<{ challenge: RoomRpsChallengeDto; serverTimeMs?: number }>(
+ apiEndpointPath(API_OPERATIONS.retryRoomRpsSettlement, { challenge_id: challengeId }),
+ { method: endpoint.method },
+ ).then((result) => ({ ...result, challenge: normalizeRoomRpsChallenge(result.challenge || {}) }));
+}
+
+export function expireRoomRpsChallenge(
+ challengeId: string,
+): Promise<{ challenge: RoomRpsChallengeDto; serverTimeMs?: number }> {
+ const endpoint = API_ENDPOINTS.expireRoomRpsChallenge;
+ return apiRequest<{ challenge: RoomRpsChallengeDto; serverTimeMs?: number }>(
+ apiEndpointPath(API_OPERATIONS.expireRoomRpsChallenge, { challenge_id: challengeId }),
+ { method: endpoint.method },
+ ).then((result) => ({ ...result, challenge: normalizeRoomRpsChallenge(result.challenge || {}) }));
+}
+
function normalizePlatform(platform: Partial): GamePlatformDto {
return {
appCode: stringValue(platform.appCode),
@@ -372,6 +516,67 @@ function normalizeDiceRobot(robot: Partial): DiceRobotDto {
};
}
+function normalizeRoomRpsConfig(config: Partial): RoomRpsConfigDto {
+ return {
+ appCode: stringValue(config.appCode),
+ gameId: stringValue(config.gameId || "room_rps"),
+ status: stringValue(config.status || "active"),
+ challengeTimeoutMs: numberValue(config.challengeTimeoutMs || 600000),
+ revealCountdownMs: numberValue(config.revealCountdownMs || 3000),
+ stakeGifts: Array.isArray(config.stakeGifts) ? config.stakeGifts.map(normalizeRoomRpsStakeGift) : [],
+ createdAtMs: numberValue(config.createdAtMs),
+ updatedAtMs: numberValue(config.updatedAtMs),
+ };
+}
+
+function normalizeRoomRpsStakeGift(gift: Partial): RoomRpsStakeGiftDto {
+ return {
+ giftId: stringValue(gift.giftId),
+ giftIdNumber: numberValue(gift.giftIdNumber),
+ giftName: stringValue(gift.giftName),
+ giftIconUrl: stringValue(gift.giftIconUrl),
+ giftPriceCoin: numberValue(gift.giftPriceCoin),
+ enabled: gift.enabled !== false,
+ sortOrder: numberValue(gift.sortOrder),
+ };
+}
+
+function normalizeRoomRpsChallenge(challenge: Partial): RoomRpsChallengeDto {
+ return {
+ appCode: stringValue(challenge.appCode),
+ challengeId: stringValue(challenge.challengeId),
+ roomId: stringValue(challenge.roomId),
+ regionId: numberValue(challenge.regionId),
+ status: stringValue(challenge.status || "pending"),
+ stakeGiftId: stringValue(challenge.stakeGiftId),
+ stakeGiftIdNumber: numberValue(challenge.stakeGiftIdNumber),
+ stakeCoin: numberValue(challenge.stakeCoin),
+ initiator: normalizeRoomRpsPlayer(challenge.initiator || {}),
+ challenger: normalizeRoomRpsPlayer(challenge.challenger || {}),
+ winnerUserId: stringValue(challenge.winnerUserId),
+ winnerUserIdNumber: numberValue(challenge.winnerUserIdNumber),
+ settlementStatus: stringValue(challenge.settlementStatus),
+ failureReason: stringValue(challenge.failureReason),
+ timeoutAtMs: numberValue(challenge.timeoutAtMs),
+ revealAtMs: numberValue(challenge.revealAtMs),
+ createdAtMs: numberValue(challenge.createdAtMs),
+ matchedAtMs: numberValue(challenge.matchedAtMs),
+ settledAtMs: numberValue(challenge.settledAtMs),
+ updatedAtMs: numberValue(challenge.updatedAtMs),
+ };
+}
+
+function normalizeRoomRpsPlayer(player: Partial): RoomRpsPlayerDto {
+ return {
+ userId: stringValue(player.userId),
+ userIdNumber: numberValue(player.userIdNumber),
+ gesture: stringValue(player.gesture),
+ result: stringValue(player.result),
+ balanceAfter: numberValue(player.balanceAfter),
+ joinedAtMs: numberValue(player.joinedAtMs),
+ };
+}
+
function stringValue(value: unknown) {
return typeof value === "string" ? value : value === undefined || value === null ? "" : String(value);
}
diff --git a/src/features/games/games.module.css b/src/features/games/games.module.css
index 9b4534f..6d50c33 100644
--- a/src/features/games/games.module.css
+++ b/src/features/games/games.module.css
@@ -64,10 +64,29 @@
width: 100%;
}
+.giftTierList {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.giftTierRow {
+ display: grid;
+ grid-template-columns: minmax(180px, 1fr) minmax(110px, 0.5fr) minmax(96px, auto);
+ gap: 12px;
+ align-items: center;
+}
+
.fullField {
grid-column: 1 / -1;
}
+@media (max-width: 720px) {
+ .giftTierRow {
+ grid-template-columns: minmax(0, 1fr);
+ }
+}
+
.platformList {
display: flex;
flex-direction: column;
diff --git a/src/features/games/pages/GameRobotPage.jsx b/src/features/games/pages/GameRobotPage.jsx
index 807bd09..329082e 100644
--- a/src/features/games/pages/GameRobotPage.jsx
+++ b/src/features/games/pages/GameRobotPage.jsx
@@ -54,7 +54,7 @@ export function GameRobotPage() {
setItems((current) => (append ? [...current, ...(result.items || [])] : result.items || []));
setNextCursor(result.nextCursor || "");
} catch (err) {
- showToast(err.message || "加载游戏机器人失败", "error");
+ showToast(err.message || "加载全站机器人失败", "error");
} finally {
setLoading(false);
}
@@ -86,8 +86,8 @@ export function GameRobotPage() {
async (robot) => {
const ok = await confirm({
confirmText: "删除",
- message: `${robot.nickname || robot.userId} 会从游戏机器人列表移除,历史用户资料和对局记录保留。`,
- title: "删除游戏机器人",
+ message: `${robot.nickname || robot.userId} 会从全站机器人列表移除,历史用户资料和对局记录保留。`,
+ title: "删除全站机器人",
tone: "danger",
});
if (!ok) {
@@ -97,9 +97,9 @@ export function GameRobotPage() {
try {
await deleteDiceRobot(robot.userId, robot.gameId || "dice");
await load("", false);
- showToast("游戏机器人已删除", "success");
+ showToast("全站机器人已删除", "success");
} catch (err) {
- showToast(err.message || "删除游戏机器人失败", "error");
+ showToast(err.message || "删除全站机器人失败", "error");
} finally {
setLoadingAction("");
}
@@ -157,7 +157,9 @@ export function GameRobotPage() {
render: (robot) => (
removeRobot(robot)}
@@ -183,7 +185,7 @@ export function GameRobotPage() {
showToast(`已批量创建 ${result.created || 0} 个机器人`, "success");
setDialogOpen(false);
} catch (err) {
- showToast(err.message || "批量创建机器人失败", "error");
+ showToast(err.message || "批量创建全站机器人失败", "error");
} finally {
setLoadingAction("");
}
@@ -201,14 +203,23 @@ export function GameRobotPage() {
load("", false)}>
- setDialogOpen(true)}>
+ setDialogOpen(true)}
+ >
>
}
filters={
<>
- setStatus(event.target.value)}>
+ setStatus(event.target.value)}
+ >
@@ -244,7 +255,14 @@ export function GameRobotPage() {
function GenerateDialog({ form, loading, open, setForm, onClose, onSubmit }) {
return (
-
+
{
+ setLoading(true);
+ try {
+ const result = await listRoomRpsChallenges({
+ challengerUserId: filters.challengerUserId || undefined,
+ cursor: nextCursorValue,
+ endTimeMs: timeRange.endMs || undefined,
+ initiatorUserId: filters.initiatorUserId || undefined,
+ pageSize: 50,
+ roomId: filters.roomId || undefined,
+ startTimeMs: timeRange.startMs || undefined,
+ status: filters.status || undefined,
+ });
+ setItems(result.items || []);
+ setCursor(nextCursorValue);
+ setNextCursor(result.nextCursor || "");
+ } catch (err) {
+ showToast(err.message || "加载房内猜拳订单失败", "error");
+ } finally {
+ setLoading(false);
+ }
+ },
+ [filters, showToast, timeRange],
+ );
+
+ useEffect(() => {
+ load("");
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const columns = [
+ {
+ key: "challenge",
+ label: "挑战单",
+ width: "minmax(220px, 1.3fr)",
+ render: (item) => (
+
+ {item.challengeId}
+ {item.roomId || "-"}
+
+ ),
+ },
+ {
+ key: "status",
+ label: "状态",
+ width: "120px",
+ render: (item) => statusText(item.status),
+ },
+ {
+ key: "stake",
+ label: "下注",
+ width: "minmax(150px, .9fr)",
+ render: (item) => `${item.stakeGiftId || "-"} / ${formatNumber(item.stakeCoin)}`,
+ },
+ {
+ key: "users",
+ label: "双方",
+ width: "minmax(220px, 1.2fr)",
+ render: (item) => `${playerText(item.initiator)} / ${playerText(item.challenger)}`,
+ },
+ {
+ key: "settlement",
+ label: "结算",
+ width: "minmax(160px, .9fr)",
+ render: (item) => item.settlementStatus || "-",
+ },
+ {
+ key: "created",
+ label: "创建时间",
+ width: "minmax(170px, 1fr)",
+ render: (item) => (item.createdAtMs ? formatMillis(item.createdAtMs) : "-"),
+ },
+ {
+ key: "actions",
+ label: "操作",
+ width: "132px",
+ render: (item) => (
+
+ openDetail(item.challengeId)}>
+
+
+ retrySettlement(item.challengeId)}
+ >
+
+
+ expireChallenge(item.challengeId)}
+ >
+
+
+
+ ),
+ },
+ ];
+
+ const updateFilter = (patch) => setFilters((current) => ({ ...current, ...patch }));
+ const resetFilters = () => {
+ setFilters(emptyFilters);
+ setTimeRange({ endMs: "", startMs: "" });
+ };
+
+ const openDetail = async (challengeId) => {
+ setActionLoading(challengeId);
+ try {
+ const result = await getRoomRpsChallenge(challengeId);
+ setDetail(result.challenge);
+ setDetailOpen(true);
+ } catch (err) {
+ showToast(err.message || "获取房内猜拳订单详情失败", "error");
+ } finally {
+ setActionLoading("");
+ }
+ };
+
+ const retrySettlement = async (challengeId) => {
+ setActionLoading(challengeId);
+ try {
+ const result = await retryRoomRpsSettlement(challengeId);
+ patchChallenge(result.challenge);
+ showToast("结算重试已提交", "success");
+ } catch (err) {
+ showToast(err.message || "重试结算失败", "error");
+ } finally {
+ setActionLoading("");
+ }
+ };
+
+ const expireChallenge = async (challengeId) => {
+ setActionLoading(challengeId);
+ try {
+ const result = await expireRoomRpsChallenge(challengeId);
+ patchChallenge(result.challenge);
+ showToast("挑战单已过期", "success");
+ } catch (err) {
+ showToast(err.message || "手动过期失败", "error");
+ } finally {
+ setActionLoading("");
+ }
+ };
+
+ const patchChallenge = (challenge) => {
+ setItems((current) => current.map((item) => (item.challengeId === challenge.challengeId ? challenge : item)));
+ if (detail?.challengeId === challenge.challengeId) {
+ setDetail(challenge);
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+ load("")}>
+
+
+
+ >
+ }
+ filters={
+ <>
+ updateFilter({ roomId: event.target.value })}
+ />
+ updateFilter({ status: value })}
+ />
+ updateFilter({ initiatorUserId: event.target.value })}
+ />
+ updateFilter({ challengerUserId: event.target.value })}
+ />
+
+ >
+ }
+ />
+
+ item.challengeId} />
+
+
+ load("")}>
+
+
+ load(nextCursor)}
+ >
+
+
+ >
+ ) : null
+ }
+ />
+ setDetailOpen(false)} />
+
+ );
+}
+
+function RoomRpsChallengeDetail({ challenge, open, onClose }) {
+ const closeBySubmit = (event) => {
+ event.preventDefault();
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function playerText(player) {
+ if (!player?.userId) return "-";
+ return `${player.userId}:${player.gesture || "-"}`;
+}
+
+function detailPlayerText(player) {
+ if (!player?.userId) return "-";
+ return `${player.userId} / ${player.gesture || "-"} / ${player.result || "-"}`;
+}
+
+function statusText(status) {
+ return Object.fromEntries(statusOptions)[status] || status || "-";
+}
+
+function timeText(ms) {
+ return ms ? formatMillis(ms) : "-";
+}
+
+function formatNumber(value) {
+ return Number(value || 0).toLocaleString("en-US");
+}
diff --git a/src/features/games/pages/RoomRpsConfigPage.jsx b/src/features/games/pages/RoomRpsConfigPage.jsx
new file mode 100644
index 0000000..7d8bb4b
--- /dev/null
+++ b/src/features/games/pages/RoomRpsConfigPage.jsx
@@ -0,0 +1,298 @@
+import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
+import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
+import MenuItem from "@mui/material/MenuItem";
+import TextField from "@mui/material/TextField";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { PERMISSIONS } from "@/app/permissions";
+import { useAuth } from "@/app/auth/AuthProvider.jsx";
+import { getRoomRpsConfig, updateRoomRpsConfig } from "@/features/games/api";
+import styles from "@/features/games/games.module.css";
+import {
+ AdminActionIconButton,
+ AdminListBody,
+ AdminListPage,
+ AdminListToolbar,
+ AdminRowActions,
+} from "@/shared/ui/AdminListLayout.jsx";
+import {
+ AdminFormDialog,
+ AdminFormFieldGrid,
+ AdminFormSection,
+ AdminFormSwitchField,
+} from "@/shared/ui/AdminFormDialog.jsx";
+import { DataTable } from "@/shared/ui/DataTable.jsx";
+import { PageSkeleton } from "@/shared/ui/PageSkeleton.jsx";
+import { useToast } from "@/shared/ui/ToastProvider.jsx";
+import { formatMillis } from "@/shared/utils/time.js";
+
+const defaultStakeGifts = [1, 2, 3, 4].map((sortOrder) => ({
+ enabled: true,
+ giftId: "",
+ sortOrder,
+}));
+
+const defaultForm = {
+ status: "active",
+ challengeTimeoutMs: 600000,
+ revealCountdownMs: 3000,
+ stakeGifts: defaultStakeGifts,
+};
+
+export function RoomRpsConfigPage() {
+ const { can } = useAuth();
+ const { showToast } = useToast();
+ const canUpdate = can(PERMISSIONS.gameUpdate);
+ const [config, setConfig] = useState(null);
+ const [form, setForm] = useState(defaultForm);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [editing, setEditing] = useState(false);
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const result = await getRoomRpsConfig();
+ setConfig(result.config);
+ } catch (err) {
+ showToast(err.message || "加载房内猜拳配置失败", "error");
+ } finally {
+ setLoading(false);
+ }
+ }, [showToast]);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const rows = useMemo(() => (config ? [config] : []), [config]);
+ const columns = useMemo(
+ () => [
+ {
+ key: "game",
+ label: "玩法",
+ width: "minmax(180px, 1fr)",
+ render: (item) => (
+
+ 房内猜拳
+ {item.gameId || "room_rps"}
+
+ ),
+ },
+ {
+ key: "status",
+ label: "状态",
+ width: "120px",
+ render: (item) => statusText(item.status),
+ },
+ {
+ key: "timeout",
+ label: "时长",
+ width: "minmax(180px, 1fr)",
+ render: (item) =>
+ `${formatSeconds(item.challengeTimeoutMs)} / ${formatSeconds(item.revealCountdownMs)}`,
+ },
+ {
+ key: "gifts",
+ label: "礼物档位",
+ width: "minmax(260px, 1.5fr)",
+ render: (item) =>
+ (item.stakeGifts || [])
+ .map(
+ (gift) =>
+ `${gift.sortOrder}:${gift.giftName || gift.giftId}${gift.enabled ? "" : "(停用)"}`,
+ )
+ .join(" / "),
+ },
+ {
+ key: "updated",
+ label: "更新时间",
+ width: "minmax(170px, 1fr)",
+ render: (item) => (item.updatedAtMs ? formatMillis(item.updatedAtMs) : "-"),
+ },
+ {
+ key: "actions",
+ label: "操作",
+ width: "88px",
+ render: (item) => (
+
+ openEditor(item)}>
+
+
+
+ ),
+ },
+ ],
+ [canUpdate],
+ );
+
+ const openEditor = (item) => {
+ setForm(configToForm(item));
+ setEditing(true);
+ };
+
+ const submit = async (event) => {
+ event.preventDefault();
+ const payload = formToPayload(form);
+ if (payload.stakeGifts.length !== 4 || payload.stakeGifts.some((gift) => !gift.giftId)) {
+ showToast("房内猜拳必须配置 4 个礼物档位", "error");
+ return;
+ }
+ setSaving(true);
+ try {
+ const result = await updateRoomRpsConfig(payload);
+ setConfig(result.config);
+ setEditing(false);
+ showToast("房内猜拳配置已保存", "success");
+ } catch (err) {
+ showToast(err.message || "保存房内猜拳配置失败", "error");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ }
+ />
+
+ item.gameId || "room_rps"}
+ />
+
+ setEditing(false)}
+ onSubmit={submit}
+ />
+
+ );
+}
+
+function RoomRpsConfigDialog({ form, loading, open, setForm, onClose, onSubmit }) {
+ return (
+
+
+
+ setForm({ ...form, status: event.target.value })}
+ >
+
+
+
+ setForm({ ...form, challengeTimeoutMs: event.target.value })}
+ />
+ setForm({ ...form, revealCountdownMs: event.target.value })}
+ />
+
+
+
+
+ {form.stakeGifts.map((gift, index) => (
+
+
setGiftTier(setForm, form, index, { giftId: event.target.value })}
+ />
+
+ setGiftTier(setForm, form, index, { sortOrder: event.target.value })
+ }
+ />
+ setGiftTier(setForm, form, index, { enabled: checked })}
+ />
+
+ ))}
+
+
+
+ );
+}
+
+function setGiftTier(setForm, form, index, patch) {
+ const stakeGifts = form.stakeGifts.map((gift, giftIndex) => (giftIndex === index ? { ...gift, ...patch } : gift));
+ setForm({ ...form, stakeGifts });
+}
+
+function configToForm(config) {
+ const stakeGifts = [...(config.stakeGifts || [])]
+ .sort((left, right) => Number(left.sortOrder || 0) - Number(right.sortOrder || 0))
+ .slice(0, 4)
+ .map((gift, index) => ({
+ enabled: gift.enabled !== false,
+ giftId: gift.giftId || "",
+ sortOrder: gift.sortOrder || index + 1,
+ }));
+ while (stakeGifts.length < 4) {
+ stakeGifts.push({ ...defaultStakeGifts[stakeGifts.length] });
+ }
+ return {
+ status: config.status || "active",
+ challengeTimeoutMs: config.challengeTimeoutMs || 600000,
+ revealCountdownMs: config.revealCountdownMs || 3000,
+ stakeGifts,
+ };
+}
+
+function formToPayload(form) {
+ return {
+ status: form.status || "active",
+ challengeTimeoutMs: Number(form.challengeTimeoutMs || 600000),
+ revealCountdownMs: Number(form.revealCountdownMs || 3000),
+ stakeGifts: form.stakeGifts.map((gift, index) => ({
+ enabled: gift.enabled !== false,
+ giftId: Number(gift.giftId || 0),
+ sortOrder: Number(gift.sortOrder || index + 1),
+ })),
+ };
+}
+
+function statusText(status) {
+ return status === "disabled" ? "停用" : "启用";
+}
+
+function formatSeconds(ms) {
+ const value = Number(ms || 0);
+ if (!value) return "-";
+ return `${Math.round(value / 1000)}s`;
+}
diff --git a/src/features/games/routes.js b/src/features/games/routes.js
index d54a726..1193892 100644
--- a/src/features/games/routes.js
+++ b/src/features/games/routes.js
@@ -18,11 +18,27 @@ export const gameRoutes = [
permission: PERMISSIONS.gameView,
},
{
- label: "游戏机器人",
+ label: "全站机器人",
loader: () => import("./pages/GameRobotPage.jsx").then((module) => module.GameRobotPage),
menuCode: MENU_CODES.gameRobots,
pageKey: "game-robots",
path: "/games/robots",
permission: PERMISSIONS.gameView,
},
+ {
+ label: "房内猜拳配置",
+ loader: () => import("./pages/RoomRpsConfigPage.jsx").then((module) => module.RoomRpsConfigPage),
+ menuCode: MENU_CODES.roomRpsConfig,
+ pageKey: "room-rps-config",
+ path: "/games/room-rps/config",
+ permission: PERMISSIONS.gameView,
+ },
+ {
+ label: "房内猜拳订单",
+ loader: () => import("./pages/RoomRpsChallengePage.jsx").then((module) => module.RoomRpsChallengePage),
+ menuCode: MENU_CODES.roomRpsChallenges,
+ pageKey: "room-rps-challenges",
+ path: "/games/room-rps/challenges",
+ permission: PERMISSIONS.gameView,
+ },
];
diff --git a/src/features/host-agency-policy/api.ts b/src/features/host-agency-policy/api.ts
index 0349b85..371c728 100644
--- a/src/features/host-agency-policy/api.ts
+++ b/src/features/host-agency-policy/api.ts
@@ -210,6 +210,8 @@ export function deleteTeamSalaryPolicy(policyType: string, policyId: EntityId):
export interface HostSalarySettlementUserDto {
userId: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
username?: string;
avatar?: string;
}
@@ -415,6 +417,8 @@ function normalizeSettlementUser(raw: Record): HostSalarySettle
return {
userId: stringValue(raw.userId ?? raw.user_id),
displayUserId: stringValue(raw.displayUserId ?? raw.display_user_id),
+ prettyDisplayUserId: stringValue(raw.prettyDisplayUserId ?? raw.pretty_display_user_id),
+ prettyId: stringValue(raw.prettyId ?? raw.pretty_id),
username: stringValue(raw.username),
avatar: stringValue(raw.avatar),
};
diff --git a/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx b/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx
index b4d3efe..26aeb99 100644
--- a/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx
+++ b/src/features/host-agency-policy/pages/HostSalarySettlementPage.jsx
@@ -1,5 +1,4 @@
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import Checkbox from "@mui/material/Checkbox";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
@@ -18,6 +17,7 @@ import {
AdminListPage,
AdminListToolbar,
} from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { Button } from "@/shared/ui/Button.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
@@ -49,20 +49,14 @@ const policyTypeOptions = [
["admin", "Admin"],
];
-const policyTypeAllOptions = [
- ["", "全部角色"],
- ...policyTypeOptions,
-];
+const policyTypeAllOptions = [["", "全部角色"], ...policyTypeOptions];
const triggerOptions = [
["automatic", "自动政策"],
["manual", "手动政策"],
];
-const triggerAllOptions = [
- ["", "全部触发"],
- ...triggerOptions,
-];
+const triggerAllOptions = [["", "全部触发"], ...triggerOptions];
export function HostSalarySettlementPage() {
const [activeTab, setActiveTab] = useState("team-pending");
@@ -184,13 +178,7 @@ function HostRecordsPanel() {
return column;
});
const hasFilters =
- agencyOwnerUserId ||
- cycleKey ||
- settlementType ||
- status ||
- timeRange.startMs ||
- timeRange.endMs ||
- userId;
+ agencyOwnerUserId || cycleKey || settlementType || status || timeRange.startMs || timeRange.endMs || userId;
return (
<>
@@ -360,7 +348,8 @@ function TeamPendingPanel() {
...teamPendingColumns,
];
// 默认值也纳入 hasFilters,这样运营能一键回到“BD + 自动政策 + 上月周期”的标准入口。
- const hasFilters = policyType !== "bd" || triggerMode !== "automatic" || cycleKey !== defaultCycleKey() || countryCode || regionId;
+ const hasFilters =
+ policyType !== "bd" || triggerMode !== "automatic" || cycleKey !== defaultCycleKey() || countryCode || regionId;
return (
<>
@@ -377,11 +366,36 @@ function TeamPendingPanel() {
}
filters={
}
@@ -502,10 +516,31 @@ function TeamRecordsPanel() {
- { setTimeRange(value); setPage(1); }} />
-
-
-
+ {
+ setTimeRange(value);
+ setPage(1);
+ }}
+ />
+
+
+
}
@@ -591,7 +626,9 @@ const teamPendingColumns = [
width: "minmax(180px, 0.8fr)",
render: (item) => (
- {roleLabel(item.policyType)} · {item.cycleKey || "-"}
+
+ {roleLabel(item.policyType)} · {item.cycleKey || "-"}
+
{item.regionName || `区域 ${item.regionId || "-"}`}
),
@@ -652,7 +689,9 @@ const teamRecordColumns = [
render: (item) => (
{item.cycleKey || "-"}
- {roleLabel(item.policyType)} · {triggerLabel(item.triggerMode)}
+
+ {roleLabel(item.policyType)} · {triggerLabel(item.triggerMode)}
+
),
},
@@ -704,17 +743,7 @@ function UserCell({ fallbackId, user }) {
if (!idText) {
return "-";
}
- return (
-
-
- {user?.avatar ?
: }
-
-
- {user?.username || "-"}
- {idText}
-
-
- );
+ return
;
}
// CycleCell 展示 Host/Agency 周期和结算类型。
@@ -820,7 +849,9 @@ function settlementTypeLabel(value) {
daily: "日结",
half_month: "半月结算",
month_end: "月底清算",
- }[value] || value || "-"
+ }[value] ||
+ value ||
+ "-"
);
}
@@ -831,7 +862,9 @@ function statusLabel(value) {
failed: "失败",
skipped: "跳过",
succeeded: "成功",
- }[value] || value || "-"
+ }[value] ||
+ value ||
+ "-"
);
}
diff --git a/src/features/host-org/components/HostOrgIdentity.jsx b/src/features/host-org/components/HostOrgIdentity.jsx
index dd432d6..2725039 100644
--- a/src/features/host-org/components/HostOrgIdentity.jsx
+++ b/src/features/host-org/components/HostOrgIdentity.jsx
@@ -1,25 +1,17 @@
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import styles from "@/features/host-org/host-org.module.css";
-export function HostOrgPerson({ avatar, displayUserId, fallbackId, username }) {
- const name = username || "-";
- const meta = displayUserId || fallbackId || "-";
-
+export function HostOrgPerson({ avatar, displayUserId, fallbackId, prettyDisplayUserId, prettyId, username }) {
return (
-
- {avatar ? (
-

- ) : (
-
- {String(name || meta)
- .slice(0, 1)
- .toUpperCase()}
-
- )}
-
- {name}
- {meta}
-
-
+
);
}
diff --git a/src/features/host-org/pages/HostBdLeadersPage.jsx b/src/features/host-org/pages/HostBdLeadersPage.jsx
index 2a6cb1d..b279e97 100644
--- a/src/features/host-org/pages/HostBdLeadersPage.jsx
+++ b/src/features/host-org/pages/HostBdLeadersPage.jsx
@@ -7,6 +7,7 @@ import TextField from "@mui/material/TextField";
import { formatMillis } from "@/shared/utils/time.js";
import { DataState } from "@/shared/ui/DataState.jsx";
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
@@ -211,25 +212,7 @@ export function HostBdLeadersPage() {
}
function PersonIdentity({ item }) {
- const name = item.username || "-";
- const meta = item.displayUserId || item.userId || "-";
- return (
-
- {item.avatar ? (
-

- ) : (
-
- {String(name || meta)
- .slice(0, 1)
- .toUpperCase()}
-
- )}
-
- {name}
- {meta}
-
-
- );
+ return
;
}
function CreatorIdentity({ item }) {
diff --git a/src/features/host-org/pages/HostCoinSellersPage.jsx b/src/features/host-org/pages/HostCoinSellersPage.jsx
index 7d0b20f..af24c32 100644
--- a/src/features/host-org/pages/HostCoinSellersPage.jsx
+++ b/src/features/host-org/pages/HostCoinSellersPage.jsx
@@ -19,6 +19,7 @@ import {
import { formatMillis } from "@/shared/utils/time.js";
import { DataState } from "@/shared/ui/DataState.jsx";
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { InlineEditableCell } from "@/shared/ui/InlineEditableCell.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
import { coinSellerStatusFilters } from "@/features/host-org/constants.js";
@@ -157,10 +158,7 @@ export function HostCoinSellersPage() {
return (
-
+
@@ -314,7 +312,10 @@ export function HostCoinSellersPage() {
onClose={page.closeAction}
>
{page.selectedSeller ? (
-
+
) : null}
@@ -377,9 +378,7 @@ export function HostCoinSellersPage() {
label="coin per USD"
required
value={tier.coinPerUsd}
- onChange={(event) =>
- page.updateRateTier(index, { coinPerUsd: event.target.value })
- }
+ onChange={(event) => page.updateRateTier(index, { coinPerUsd: event.target.value })}
/>
page.updateRateTier(index, { enabled: event.target.checked })}
+ onChange={(event) =>
+ page.updateRateTier(index, { enabled: event.target.checked })
+ }
/>
- {item.avatar ? (
-
- ) : (
- {name.slice(0, 1).toUpperCase()}
- )}
-
- {name}
- {meta}
-
-
- );
+ return ;
}
function SellerStatusSwitch({ item, page }) {
@@ -460,7 +446,11 @@ function SellerActions({ item, page }) {
return (
{page.abilities.canLedger ? (
- page.openSellerLedger(item)}>
+ page.openSellerLedger(item)}
+ >
) : null}
diff --git a/src/features/operations/components/CoinSellerLedgerTable.jsx b/src/features/operations/components/CoinSellerLedgerTable.jsx
index ea610e7..29a101d 100644
--- a/src/features/operations/components/CoinSellerLedgerTable.jsx
+++ b/src/features/operations/components/CoinSellerLedgerTable.jsx
@@ -1,18 +1,14 @@
import FileDownloadOutlined from "@mui/icons-material/FileDownloadOutlined";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import { useMemo, useState } from "react";
import { exportCoinSellerLedger, listCoinSellerLedger } from "@/features/operations/api";
import styles from "@/features/operations/operations.module.css";
import { downloadCsv } from "@/shared/api/download";
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
-import {
- AdminFilterResetButton,
- AdminListBody,
- AdminListToolbar,
-} from "@/shared/ui/AdminListLayout.jsx";
+import { AdminFilterResetButton, AdminListBody, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx";
import { Button } from "@/shared/ui/Button.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
@@ -182,14 +178,15 @@ export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" }
filters={
<>
-
+
>
}
actions={
- } onClick={downloadLedger}>
+ }
+ onClick={downloadLedger}
+ >
{exporting ? "导出中" : "导出"}
}
@@ -235,18 +232,7 @@ function OperatorCell({ entry }) {
}
function UserCell({ user = {} }) {
- const name = user.username || "-";
- return (
-
-
- {user.avatar ?
: }
-
-
- {name}
- {userIdText(user)}
-
-
- );
+ return ;
}
function AmountValue({ entry }) {
@@ -261,16 +247,9 @@ function AmountValue({ entry }) {
}
function ledgerTypeLabel(entry) {
- return ledgerTypeLabels[entry.ledgerType] || bizTypeLabels[entry.bizType] || entry.ledgerType || entry.bizType || "-";
-}
-
-function userIdText(user = {}) {
- const displayUserId = String(user.displayUserId || "").trim();
- const userId = String(user.userId || "").trim();
- if (displayUserId && userId && displayUserId !== userId) {
- return `${displayUserId} / ${userId}`;
- }
- return displayUserId || userId || "-";
+ return (
+ ledgerTypeLabels[entry.ledgerType] || bizTypeLabels[entry.bizType] || entry.ledgerType || entry.bizType || "-"
+ );
}
function formatNumber(value) {
diff --git a/src/features/operations/pages/CoinAdjustmentPage.jsx b/src/features/operations/pages/CoinAdjustmentPage.jsx
index b944b07..78f5f87 100644
--- a/src/features/operations/pages/CoinAdjustmentPage.jsx
+++ b/src/features/operations/pages/CoinAdjustmentPage.jsx
@@ -1,5 +1,4 @@
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import SearchOutlined from "@mui/icons-material/SearchOutlined";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
@@ -24,6 +23,7 @@ import {
import { Button } from "@/shared/ui/Button.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
@@ -320,33 +320,11 @@ export function CoinAdjustmentPage() {
function AdjustmentUserCell({ item }) {
const user = item.user || {};
- const name = user.username || "-";
-
- return (
-
-
- {user.avatar ?
: }
-
-
- {name}
- {userIdText(user, item.userId)}
-
-
- );
+ return ;
}
function TargetUserPreview({ user }) {
- return (
-
-
- {user.avatar ?
: }
-
-
- {user.username || "-"}
- {userIdText(user, user.userId)}
-
-
- );
+ return ;
}
function AdjustmentAmount({ item }) {
@@ -388,11 +366,6 @@ function buildAdjustmentPayload(form, targetUser) {
};
}
-function userIdText(user, fallbackUserId) {
- const ids = [user.displayUserId, user.userId || fallbackUserId].filter(Boolean);
- return ids.length ? ids.join(" / ") : "-";
-}
-
function operatorName(item) {
const operator = item.operator || {};
return operator.name || operator.username || item.operatorUserId || "-";
diff --git a/src/features/operations/pages/CoinLedgerPage.jsx b/src/features/operations/pages/CoinLedgerPage.jsx
index 11475ee..a36a961 100644
--- a/src/features/operations/pages/CoinLedgerPage.jsx
+++ b/src/features/operations/pages/CoinLedgerPage.jsx
@@ -1,4 +1,3 @@
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import { useMemo, useState } from "react";
import { listCoinLedger } from "@/features/operations/api";
import {
@@ -7,6 +6,7 @@ import {
AdminListPage,
AdminListToolbar,
} from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
@@ -232,16 +232,12 @@ function CoinLedgerDetailDrawer({ entry, onClose, open }) {
{entry ? (
<>
-
- {user.avatar ?
: }
-
-
-
-
{user.username || "-"}
-
-
-
{userIdText(entry)}
-
+
+
-
- {user.avatar ?
: }
-
-
- {user.username || "-"}
- {userIdText(entry)}
-
-
- );
+ return ;
}
function AmountValue({ entry }) {
@@ -366,7 +352,12 @@ function businessContextRows(entry, metadata) {
}
return compactRows([
- metadataRow("关联用户", metadata, ["target_user_id", "targetUserId", "seller_user_id", "sellerUserId"], entry.counterpartyUserId),
+ metadataRow(
+ "关联用户",
+ metadata,
+ ["target_user_id", "targetUserId", "seller_user_id", "sellerUserId"],
+ entry.counterpartyUserId,
+ ),
metadataRow("房间 ID", metadata, ["room_id", "roomId"], entry.roomId),
metadataRow("任务 ID", metadata, ["task_id", "taskId"]),
metadataRow("任务类型", metadata, ["task_type", "taskType"]),
@@ -440,16 +431,6 @@ function displayDetailValue(value) {
return String(value);
}
-function userIdText(entry) {
- const user = entry.user || {};
- const longId = user.userId || entry.userId || "";
- const shortId = user.displayUserId || "";
- if (longId && shortId && longId !== shortId) {
- return `${longId}(${shortId})`;
- }
- return longId || shortId || "-";
-}
-
function hasDetailValue(value) {
return value === 0 || value === false || Boolean(value);
}
diff --git a/src/features/operations/pages/ReportListPage.jsx b/src/features/operations/pages/ReportListPage.jsx
index ef241d1..42f3efd 100644
--- a/src/features/operations/pages/ReportListPage.jsx
+++ b/src/features/operations/pages/ReportListPage.jsx
@@ -10,6 +10,7 @@ import {
AdminListPage,
AdminListToolbar,
} from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
@@ -219,19 +220,7 @@ function TargetCell({ report }) {
);
}
const user = target.user || {};
- return (
-
-
- {user.avatar ?
: }
-
-
- {user.username || "-"}
-
- {targetMeta(user.displayUserId || user.defaultDisplayUserId, user.userId || report.userId)}
-
-
-
- );
+ return ;
}
function EvidenceImages({ imageUrls = [] }) {
diff --git a/src/features/payment/pages/PaymentBillListPage.jsx b/src/features/payment/pages/PaymentBillListPage.jsx
index bd28f4d..44ae657 100644
--- a/src/features/payment/pages/PaymentBillListPage.jsx
+++ b/src/features/payment/pages/PaymentBillListPage.jsx
@@ -1,14 +1,10 @@
import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined";
import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import Tooltip from "@mui/material/Tooltip";
import { useEffect, useMemo, useState } from "react";
import { listRechargeBills } from "@/features/payment/api";
-import {
- AdminListBody,
- AdminListPage,
- AdminListToolbar,
-} from "@/shared/ui/AdminListLayout.jsx";
+import { AdminListBody, AdminListPage, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { Button } from "@/shared/ui/Button.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
@@ -70,10 +66,7 @@ const columns = [
label: "国家/区域",
width: "minmax(150px, 0.75fr)",
render: (bill, _index, context) => (
-
+
),
},
{
@@ -315,19 +308,7 @@ function BillUser({ bill, type }) {
return "-";
}
- const shortId = profile.displayUserId || profile.userId || "-";
- const name = profile.username || "-";
- return (
-
-
- {profile.avatar ?
: }
-
-
- {name}
- {shortId}
-
-
- );
+ return ;
}
function StatusBadge({ status }) {
@@ -386,6 +367,8 @@ function normalizeBillUser(profile, fallbackId) {
countryDisplayName: source.countryDisplayName || source.country_display_name || "",
countryName: source.countryName || source.country_name || "",
displayUserId: source.displayUserId || source.display_user_id || "",
+ prettyDisplayUserId: source.prettyDisplayUserId || source.pretty_display_user_id || "",
+ prettyId: source.prettyId || source.pretty_id || "",
userId: String(source.userId || source.user_id || fallbackId || ""),
username: source.username || source.name || "",
};
diff --git a/src/features/registration-reward/api.ts b/src/features/registration-reward/api.ts
index 3c3be2c..829246b 100644
--- a/src/features/registration-reward/api.ts
+++ b/src/features/registration-reward/api.ts
@@ -25,6 +25,8 @@ export interface RegistrationRewardConfigPayload {
export interface RegistrationRewardClaimUserDto {
userId?: number;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
username?: string;
avatar?: string;
}
@@ -111,6 +113,8 @@ function normalizeClaim(item: RawClaim): RegistrationRewardClaimDto {
user: {
userId: numberValue(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),
username: stringValue(rawUser.username),
avatar: stringValue(rawUser.avatar),
},
diff --git a/src/features/registration-reward/pages/RegistrationRewardPage.jsx b/src/features/registration-reward/pages/RegistrationRewardPage.jsx
index a17d5d4..ec9f2ba 100644
--- a/src/features/registration-reward/pages/RegistrationRewardPage.jsx
+++ b/src/features/registration-reward/pages/RegistrationRewardPage.jsx
@@ -1,7 +1,7 @@
-import Avatar from "@mui/material/Avatar";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { formatMillis } from "@/shared/utils/time.js";
import { RegistrationRewardConfigDrawer } from "@/features/registration-reward/components/RegistrationRewardConfigDrawer.jsx";
@@ -132,17 +132,7 @@ export function RegistrationRewardPage() {
function ClaimUser({ claim }) {
const user = claim.user || {};
- const name = user.username || `用户 ${claim.userId}`;
- const displayUserId = user.displayUserId || "";
- return (
-
-
-
- {displayUserId ? `${name} (${displayUserId})` : name}
- {displayUserId ? null : ID {claim.userId}}
-
-
- );
+ return ;
}
function RewardSummary({ claim }) {
diff --git a/src/features/registration-reward/pages/RegistrationRewardPage.test.jsx b/src/features/registration-reward/pages/RegistrationRewardPage.test.jsx
index 5cd46f1..ccc2437 100644
--- a/src/features/registration-reward/pages/RegistrationRewardPage.test.jsx
+++ b/src/features/registration-reward/pages/RegistrationRewardPage.test.jsx
@@ -18,11 +18,11 @@ test("registration reward list shows receive filter today count and compact row
expect(screen.getByText("今日已领")).toBeInTheDocument();
expect(screen.getByText("7 份")).toBeInTheDocument();
- expect(screen.getAllByRole("button", { name: /领取时间/ }).some((node) => node.textContent.includes("领取时间"))).toBe(
- true,
- );
- expect(screen.getByText("jayCiE (165121)")).toBeInTheDocument();
- expect(screen.queryByText("短号 165121")).not.toBeInTheDocument();
+ expect(
+ screen.getAllByRole("button", { name: /领取时间/ }).some((node) => node.textContent.includes("领取时间")),
+ ).toBe(true);
+ expect(screen.getByText("jayCiE")).toBeInTheDocument();
+ expect(screen.getByText("165121")).toBeInTheDocument();
expect(screen.getAllByText("1,000 金币").length).toBeGreaterThanOrEqual(1);
expect(screen.queryByText(/^注册奖励$/)).not.toBeInTheDocument();
expect(screen.getByText("rclaim_1780684209628_899e9717839e0109")).toBeInTheDocument();
diff --git a/src/features/resources/api.ts b/src/features/resources/api.ts
index df290ab..6f86628 100644
--- a/src/features/resources/api.ts
+++ b/src/features/resources/api.ts
@@ -205,6 +205,8 @@ export interface ResourceGrantItemDto {
export interface ResourceGrantOperatorDto {
avatar?: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
name?: string;
source?: string;
userId?: number | string;
@@ -214,6 +216,8 @@ export interface ResourceGrantOperatorDto {
export interface ResourceGrantUserDto {
avatar?: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
userId?: number | string;
username?: string;
}
diff --git a/src/features/resources/pages/ResourceGrantListPage.jsx b/src/features/resources/pages/ResourceGrantListPage.jsx
index e8acbdd..fa8040e 100644
--- a/src/features/resources/pages/ResourceGrantListPage.jsx
+++ b/src/features/resources/pages/ResourceGrantListPage.jsx
@@ -1,16 +1,11 @@
import Add from "@mui/icons-material/Add";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { AdminFormDialog, AdminFormFieldGrid, AdminFormSection } from "@/shared/ui/AdminFormDialog.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
-import {
- AdminActionIconButton,
- AdminListBody,
- AdminListPage,
- AdminListToolbar,
-} from "@/shared/ui/AdminListLayout.jsx";
+import { AdminActionIconButton, AdminListBody, AdminListPage, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { formatMillis } from "@/shared/utils/time.js";
import { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx";
@@ -233,7 +228,9 @@ function ResourceGrantDialog({
required
resources={resourceOptions}
selectedResourceIds={form.resourceIds || []}
- onChange={(resourceIds) => setForm({ ...form, resourceId: resourceIds[0] || "", resourceIds })}
+ onChange={(resourceIds) =>
+ setForm({ ...form, resourceId: resourceIds[0] || "", resourceIds })
+ }
/>
-
- {user.avatar ?
: }
-
-
- {name}
- 长ID {userId || "-"}
- {displayUserId ? 短ID {displayUserId} : null}
-
-
+
);
}
@@ -327,22 +316,12 @@ function GrantOperator({ grant }) {
const source = operator.source || grant.grantSource || "";
const operatorUserId = operator.userId || grant.operatorUserId;
if (source === "manager_center") {
- const name = operator.username || operator.name || (operatorUserId ? `用户 ${operatorUserId}` : "-");
- const shortId = operator.displayUserId || operatorUserId || "-";
return (
-
-
- {operator.avatar ? (
-
- ) : (
-
- )}
-
-
- {name}
- {shortId}
-
-
+
);
}
if (source === "admin") {
diff --git a/src/features/rooms/components/RoomDetailDrawer.jsx b/src/features/rooms/components/RoomDetailDrawer.jsx
index 1f23275..45b32de 100644
--- a/src/features/rooms/components/RoomDetailDrawer.jsx
+++ b/src/features/rooms/components/RoomDetailDrawer.jsx
@@ -1,198 +1,206 @@
import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined";
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import { useEffect, useState } from "react";
+import { AdminPrettyDisplayID, AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
import { TimeText } from "@/shared/ui/TimeText.jsx";
import { roomStatusLabels } from "@/features/rooms/constants.js";
import styles from "@/features/rooms/rooms.module.css";
export function RoomDetailDrawer({ onClose, open, room }) {
- const owner = room?.owner || {};
- const normalizedStatus = room?.status === "active" ? "active" : "closed";
- const ownerDisplayId = ownerLongShortId(owner, room);
- const closeOperator = room?.closedByAdminName || (room?.closedByAdminId ? String(room.closedByAdminId) : "");
+ const owner = room?.owner || {};
+ const normalizedStatus = room?.status === "active" ? "active" : "closed";
+ const closeOperator = room?.closedByAdminName || (room?.closedByAdminId ? String(room.closedByAdminId) : "");
- return (
-
- {room ? (
- <>
-
+ return (
+
+ {room ? (
+ <>
+
- ],
- ["房间状态", roomStatusLabels[normalizedStatus]],
- ["房间模式", room.mode],
- ["区域", room.regionName],
- ["创建时间", ],
- ["更新时间", ]
- ]}
- title="基础信息"
- />
+ ],
+ ["房间状态", roomStatusLabels[normalizedStatus]],
+ ["房间模式", room.mode],
+ ["区域", room.regionName],
+ ["创建时间", ],
+ ["更新时间", ],
+ ]}
+ title="基础信息"
+ />
- {normalizedStatus === "closed" ? (
- ],
- ["关闭原因", room.closeReason]
- ]}
- title="关闭信息"
- />
- ) : null}
+ {normalizedStatus === "closed" ? (
+ ],
+ ["关闭原因", room.closeReason],
+ ]}
+ title="关闭信息"
+ />
+ ) : null}
-
- 房主信息
-
-
- {owner.avatar ?
: }
-
-
- {owner.username || "-"}
- {ownerDisplayId}
-
-
-
-
-
-
+
+ 房主信息
+
+
+
+
+
+ ) : (
+ ""
+ )
+ }
+ />
+
+
-
- >
- ) : null}
-
- );
+
+ >
+ ) : null}
+
+ );
}
function DetailSection({ items, title }) {
- return (
-
- {title}
-
- {items.map(([label, value]) => (
-
- ))}
-
-
- );
+ return (
+
+ {title}
+
+ {items.map(([label, value]) => (
+
+ ))}
+
+
+ );
}
function DetailItem({ label, value }) {
- return (
-
-
{label}
-
{displayValue(value)}
-
- );
+ return (
+
+
{label}
+
{displayValue(value)}
+
+ );
}
function CopyableRoomId({ className = "", prefix = "", roomId }) {
- const [copied, setCopied] = useState(false);
- const value = displayValue(roomId);
+ const [copied, setCopied] = useState(false);
+ const value = displayValue(roomId);
- useEffect(() => {
- if (!copied) {
- return undefined;
- }
- const timer = window.setTimeout(() => setCopied(false), 1200);
- return () => window.clearTimeout(timer);
- }, [copied]);
+ useEffect(() => {
+ if (!copied) {
+ return undefined;
+ }
+ const timer = window.setTimeout(() => setCopied(false), 1200);
+ return () => window.clearTimeout(timer);
+ }, [copied]);
- const copyRoomId = async () => {
- if (!roomId) {
- return;
- }
- await copyText(String(roomId));
- setCopied(true);
- };
+ const copyRoomId = async () => {
+ if (!roomId) {
+ return;
+ }
+ await copyText(String(roomId));
+ setCopied(true);
+ };
- return (
-
- );
+ return (
+
+ );
}
function StatusBadge({ status }) {
- const tone = status === "active" ? "running" : "stopped";
+ const tone = status === "active" ? "running" : "stopped";
- return (
-
-
- {roomStatusLabels[status]}
-
- );
+ return (
+
+
+ {roomStatusLabels[status]}
+
+ );
}
function displayValue(value) {
- return value === 0 || value ? value : "-";
+ return value === 0 || value ? value : "-";
}
function formatNumber(value) {
- return Number(value || 0).toLocaleString("zh-CN");
+ return Number(value || 0).toLocaleString("zh-CN");
}
function roomContributionValue(room) {
- return room?.roomContribution ?? room?.heat ?? 0;
+ return room?.roomContribution ?? room?.heat ?? 0;
}
async function copyText(value) {
- if (navigator.clipboard?.writeText) {
- try {
- await navigator.clipboard.writeText(value);
- return;
- } catch {
- // 本地开发和部分浏览器策略会拒绝 Clipboard API,退回同步复制路径。
+ if (navigator.clipboard?.writeText) {
+ try {
+ await navigator.clipboard.writeText(value);
+ return;
+ } catch {
+ // 本地开发和部分浏览器策略会拒绝 Clipboard API,退回同步复制路径。
+ }
}
- }
- const input = document.createElement("textarea");
- input.value = value;
- input.setAttribute("readonly", "");
- input.style.position = "fixed";
- input.style.opacity = "0";
- document.body.appendChild(input);
- input.select();
- document.execCommand("copy");
- input.remove();
-}
-
-function ownerLongShortId(owner, room) {
- const longId = owner.userId || room?.ownerUserId || "";
- const shortId = owner.displayUserId || "";
- if (longId && shortId && longId !== shortId) {
- return `${longId}(${shortId})`;
- }
- return longId || shortId || "-";
+ const input = document.createElement("textarea");
+ input.value = value;
+ input.setAttribute("readonly", "");
+ input.style.position = "fixed";
+ input.style.opacity = "0";
+ document.body.appendChild(input);
+ input.select();
+ document.execCommand("copy");
+ input.remove();
}
diff --git a/src/features/rooms/pages/RoomListPage.jsx b/src/features/rooms/pages/RoomListPage.jsx
index 1597fe5..29e39c8 100644
--- a/src/features/rooms/pages/RoomListPage.jsx
+++ b/src/features/rooms/pages/RoomListPage.jsx
@@ -1,11 +1,11 @@
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import EditOutlined from "@mui/icons-material/EditOutlined";
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { AdminFormDialog, AdminFormSection } from "@/shared/ui/AdminFormDialog.jsx";
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { IconButton } from "@/shared/ui/IconButton.jsx";
@@ -90,7 +90,7 @@ export function RoomListPage() {
}),
render: (room) => page.openDetail(room)} />,
}
- : column.key === "status"
+ : column.key === "status"
? {
...column,
filter: createOptionsColumnFilter({
@@ -111,7 +111,7 @@ export function RoomListPage() {
/>
),
}
- : column,
+ : column,
),
{
key: "actions",
@@ -208,7 +208,6 @@ export function RoomListPage() {
onChange={(event) => page.setStatusForm({ ...page.statusForm, closeReason: event.target.value })}
/>
-
);
}
@@ -243,20 +242,7 @@ function RoomIdentity({ onClick, room }) {
function OwnerIdentity({ room }) {
const owner = room.owner || {};
- const ownerName = owner.username || "-";
- const ownerDisplayId = ownerLongShortId(owner, room);
-
- return (
-
-
- {owner.avatar ?
: }
-
-
-
{ownerName}
-
{ownerDisplayId}
-
-
- );
+ return ;
}
function RoomActions({ page, room }) {
@@ -344,12 +330,3 @@ function ContributionSortHeader({ direction, onClick }) {
);
}
-
-function ownerLongShortId(owner, room) {
- const longId = owner.userId || room.ownerUserId || "";
- const shortId = owner.displayUserId || "";
- if (longId && shortId && longId !== shortId) {
- return `${longId}(${shortId})`;
- }
- return longId || shortId || "-";
-}
diff --git a/src/features/rooms/pages/RoomPinsPage.jsx b/src/features/rooms/pages/RoomPinsPage.jsx
index 9b9fd87..64cfd0a 100644
--- a/src/features/rooms/pages/RoomPinsPage.jsx
+++ b/src/features/rooms/pages/RoomPinsPage.jsx
@@ -1,13 +1,13 @@
import AddOutlined from "@mui/icons-material/AddOutlined";
import CloseOutlined from "@mui/icons-material/CloseOutlined";
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import Autocomplete from "@mui/material/Autocomplete";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { Button } from "@/shared/ui/Button.jsx";
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { IconButton } from "@/shared/ui/IconButton.jsx";
@@ -138,7 +138,11 @@ export function RoomPinsPage() {
{page.abilities.canPinCreate ? (
- } variant="primary" onClick={page.openCreate}>
+ }
+ variant="primary"
+ onClick={page.openCreate}
+ >
新增置顶
@@ -268,18 +272,7 @@ function PinRoomIdentity({ room = {} }) {
}
function PinUserIdentity({ user = {} }) {
- return (
-
-
- {user.avatar ?
: }
-
-
-
{user.username || "-"}
-
长ID {user.userId || "-"}
-
短ID {user.displayUserId || "-"}
-
-
- );
+ return ;
}
function RoomPinActions({ page, pin }) {
diff --git a/src/features/seven-day-checkin/api.ts b/src/features/seven-day-checkin/api.ts
index 841e923..e3a369b 100644
--- a/src/features/seven-day-checkin/api.ts
+++ b/src/features/seven-day-checkin/api.ts
@@ -28,6 +28,8 @@ export interface SevenDayCheckInConfigPayload {
export interface SevenDayCheckInClaimUserDto {
userId?: number;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
username?: string;
avatar?: string;
}
@@ -123,6 +125,8 @@ function normalizeClaim(item: RawClaim): SevenDayCheckInClaimDto {
user: {
userId: numberValue(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),
username: stringValue(rawUser.username),
avatar: stringValue(rawUser.avatar),
},
diff --git a/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx b/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx
index 36deeab..d5584a2 100644
--- a/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx
+++ b/src/features/seven-day-checkin/pages/SevenDayCheckInPage.jsx
@@ -1,7 +1,7 @@
-import Avatar from "@mui/material/Avatar";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { formatMillis } from "@/shared/utils/time.js";
import { SevenDayCheckInConfigDrawer } from "@/features/seven-day-checkin/components/SevenDayCheckInConfigDrawer.jsx";
@@ -188,21 +188,7 @@ export function SevenDayCheckInPage() {
function ClaimUser({ claim }) {
const user = claim.user || {};
- return (
-
-
-
- {user.username || `用户 ${claim.userId}`}
-
- {user.displayUserId ? `短号 ${user.displayUserId}` : `ID ${claim.userId}`}
-
-
-
- );
+ return ;
}
function ResourceGroupLabel({ groups, id }) {
diff --git a/src/features/user-leaderboard/api.ts b/src/features/user-leaderboard/api.ts
index 304fedc..34d35b1 100644
--- a/src/features/user-leaderboard/api.ts
+++ b/src/features/user-leaderboard/api.ts
@@ -8,6 +8,8 @@ export type UserLeaderboardPeriod = "today" | "week" | "month";
export interface UserLeaderboardUserDto {
userId: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
username?: string;
avatar?: string;
}
@@ -94,7 +96,10 @@ function normalizeResponse(response: RawResponse): UserLeaderboardResponseDto {
startAtMs: numberValue(response.startAtMs ?? response.start_at_ms),
endAtMs: numberValue(response.endAtMs ?? response.end_at_ms),
serverTimeMs: numberValue(response.serverTimeMs ?? response.server_time_ms),
- myRank: response.myRank || response.my_rank ? normalizeItem((response.myRank || response.my_rank) as RawItem) : null,
+ myRank:
+ response.myRank || response.my_rank
+ ? normalizeItem((response.myRank || response.my_rank) as RawItem)
+ : null,
};
}
@@ -116,6 +121,8 @@ function normalizeUser(user: RawUser): UserLeaderboardUserDto {
return {
userId: stringValue(user.userId ?? user.user_id),
displayUserId: optionalString(user.displayUserId ?? user.display_user_id),
+ prettyDisplayUserId: optionalString(user.prettyDisplayUserId ?? user.pretty_display_user_id),
+ prettyId: optionalString(user.prettyId ?? user.pretty_id),
username: optionalString(user.username),
avatar: optionalString(user.avatar),
};
diff --git a/src/features/user-leaderboard/pages/UserLeaderboardPage.jsx b/src/features/user-leaderboard/pages/UserLeaderboardPage.jsx
index ea430f8..7136ba8 100644
--- a/src/features/user-leaderboard/pages/UserLeaderboardPage.jsx
+++ b/src/features/user-leaderboard/pages/UserLeaderboardPage.jsx
@@ -1,4 +1,3 @@
-import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import {
@@ -8,6 +7,7 @@ import {
AdminListPage,
AdminListToolbar,
} from "@/shared/ui/AdminListLayout.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { TimeRangeText, TimeText } from "@/shared/ui/TimeText.jsx";
import { useUserLeaderboardPage } from "@/features/user-leaderboard/hooks/useUserLeaderboardPage.js";
import styles from "@/features/user-leaderboard/user-leaderboard.module.css";
@@ -147,19 +147,7 @@ function LeaderboardSubject({ item, type }) {
);
}
const user = item.user || {};
- const shortId = user.displayUserId || "";
- const displayName = user.username || (shortId ? `用户 ${shortId}` : "用户");
- return (
-
-
- {user.avatar ?
: }
-
-
- {displayName}
- 短ID {shortId || "-"}
-
-
- );
+ return ;
}
function formatNumber(value) {
diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts
index 1dcb329..b813915 100644
--- a/src/shared/api/generated/endpoints.ts
+++ b/src/shared/api/generated/endpoints.ts
@@ -89,6 +89,7 @@ export const API_OPERATIONS = {
enableResource: "enableResource",
enableResourceGroup: "enableResourceGroup",
enableResourceShopItem: "enableResourceShopItem",
+ expireRoomRpsChallenge: "expireRoomRpsChallenge",
exportLoginLogs: "exportLoginLogs",
exportOperationLogs: "exportOperationLogs",
exportUsers: "exportUsers",
@@ -109,6 +110,8 @@ export const API_OPERATIONS = {
getRoleDataScopes: "getRoleDataScopes",
getRoomConfig: "getRoomConfig",
getRoomRocketConfig: "getRoomRocketConfig",
+ getRoomRpsChallenge: "getRoomRpsChallenge",
+ getRoomRpsConfig: "getRoomRpsConfig",
getRoomTurnoverRewardConfig: "getRoomTurnoverRewardConfig",
getSevenDayCheckInConfig: "getSevenDayCheckInConfig",
getUser: "getUser",
@@ -163,6 +166,7 @@ export const API_OPERATIONS = {
listResourceShopItems: "listResourceShopItems",
listRoles: "listRoles",
listRoomPins: "listRoomPins",
+ listRoomRpsChallenges: "listRoomRpsChallenges",
listRooms: "listRooms",
listRoomTurnoverRewardSettlements: "listRoomTurnoverRewardSettlements",
listSelfGames: "listSelfGames",
@@ -192,6 +196,7 @@ export const API_OPERATIONS = {
replaceRolePermissions: "replaceRolePermissions",
resetUserPassword: "resetUserPassword",
retryRedPacketRefund: "retryRedPacketRefund",
+ retryRoomRpsSettlement: "retryRoomRpsSettlement",
retryRoomTurnoverRewardSettlement: "retryRoomTurnoverRewardSettlement",
search: "search",
setAgencyJoinEnabled: "setAgencyJoinEnabled",
@@ -237,6 +242,7 @@ export const API_OPERATIONS = {
updateRoom: "updateRoom",
updateRoomConfig: "updateRoomConfig",
updateRoomRocketConfig: "updateRoomRocketConfig",
+ updateRoomRpsConfig: "updateRoomRpsConfig",
updateRoomTurnoverRewardConfig: "updateRoomTurnoverRewardConfig",
updateSevenDayCheckInConfig: "updateSevenDayCheckInConfig",
updateSplashScreen: "updateSplashScreen",
@@ -788,6 +794,13 @@ export const API_ENDPOINTS: Record = {
permission: "resource-shop:update",
permissions: ["resource-shop:update"]
},
+ expireRoomRpsChallenge: {
+ method: "POST",
+ operationId: API_OPERATIONS.expireRoomRpsChallenge,
+ path: "/v1/admin/game/room-rps/challenges/{challenge_id}/expire",
+ permission: "game:update",
+ permissions: ["game:update"]
+ },
exportLoginLogs: {
method: "GET",
operationId: API_OPERATIONS.exportLoginLogs,
@@ -928,6 +941,20 @@ export const API_ENDPOINTS: Record = {
permission: "room-rocket:view",
permissions: ["room-rocket:view"]
},
+ getRoomRpsChallenge: {
+ method: "GET",
+ operationId: API_OPERATIONS.getRoomRpsChallenge,
+ path: "/v1/admin/game/room-rps/challenges/{challenge_id}",
+ permission: "game:view",
+ permissions: ["game:view"]
+ },
+ getRoomRpsConfig: {
+ method: "GET",
+ operationId: API_OPERATIONS.getRoomRpsConfig,
+ path: "/v1/admin/game/room-rps/config",
+ permission: "game:view",
+ permissions: ["game:view"]
+ },
getRoomTurnoverRewardConfig: {
method: "GET",
operationId: API_OPERATIONS.getRoomTurnoverRewardConfig,
@@ -1304,6 +1331,13 @@ export const API_ENDPOINTS: Record = {
permission: "room-pin:view",
permissions: ["room-pin:view"]
},
+ listRoomRpsChallenges: {
+ method: "GET",
+ operationId: API_OPERATIONS.listRoomRpsChallenges,
+ path: "/v1/admin/game/room-rps/challenges",
+ permission: "game:view",
+ permissions: ["game:view"]
+ },
listRooms: {
method: "GET",
operationId: API_OPERATIONS.listRooms,
@@ -1497,6 +1531,13 @@ export const API_ENDPOINTS: Record = {
permission: "red-packet:update",
permissions: ["red-packet:update"]
},
+ retryRoomRpsSettlement: {
+ method: "POST",
+ operationId: API_OPERATIONS.retryRoomRpsSettlement,
+ path: "/v1/admin/game/room-rps/challenges/{challenge_id}/retry-settlement",
+ permission: "game:update",
+ permissions: ["game:update"]
+ },
retryRoomTurnoverRewardSettlement: {
method: "POST",
operationId: API_OPERATIONS.retryRoomTurnoverRewardSettlement,
@@ -1810,6 +1851,13 @@ export const API_ENDPOINTS: Record = {
permission: "room-rocket:update",
permissions: ["room-rocket:update"]
},
+ updateRoomRpsConfig: {
+ method: "PATCH",
+ operationId: API_OPERATIONS.updateRoomRpsConfig,
+ path: "/v1/admin/game/room-rps/config",
+ permission: "game:update",
+ permissions: ["game:update"]
+ },
updateRoomTurnoverRewardConfig: {
method: "PUT",
operationId: API_OPERATIONS.updateRoomTurnoverRewardConfig,
diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts
index 3c30c69..bccf508 100644
--- a/src/shared/api/generated/schema.d.ts
+++ b/src/shared/api/generated/schema.d.ts
@@ -1204,6 +1204,86 @@ export interface paths {
patch: operations["setDiceRobotStatus"];
trace?: never;
};
+ "/admin/game/room-rps/config": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getRoomRpsConfig"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch: operations["updateRoomRpsConfig"];
+ trace?: never;
+ };
+ "/admin/game/room-rps/challenges": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["listRoomRpsChallenges"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/game/room-rps/challenges/{challenge_id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getRoomRpsChallenge"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/game/room-rps/challenges/{challenge_id}/retry-settlement": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["retryRoomRpsSettlement"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/game/room-rps/challenges/{challenge_id}/expire": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["expireRoomRpsChallenge"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/admin/gift-types": {
parameters: {
query?: never;
@@ -3273,7 +3353,7 @@ export interface operations {
query?: never;
header?: never;
path: {
- task_id: number;
+ task_id: string;
};
cookie?: never;
};
@@ -3287,7 +3367,7 @@ export interface operations {
query?: never;
header?: never;
path: {
- task_id: number;
+ task_id: string;
};
cookie?: never;
};
@@ -4627,6 +4707,84 @@ export interface operations {
200: components["responses"]["EmptyResponse"];
};
};
+ getRoomRpsConfig: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["EmptyResponse"];
+ };
+ };
+ updateRoomRpsConfig: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["EmptyResponse"];
+ };
+ };
+ listRoomRpsChallenges: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["EmptyResponse"];
+ };
+ };
+ getRoomRpsChallenge: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ challenge_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["EmptyResponse"];
+ };
+ };
+ retryRoomRpsSettlement: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ challenge_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["EmptyResponse"];
+ };
+ };
+ expireRoomRpsChallenge: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ challenge_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["EmptyResponse"];
+ };
+ };
listGiftTypes: {
parameters: {
query?: never;
diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts
index dace89d..53de7e6 100644
--- a/src/shared/api/types.ts
+++ b/src/shared/api/types.ts
@@ -191,6 +191,8 @@ export interface ChangePasswordPayload {
export interface CoinLedgerUserDto {
avatar?: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
userId: string;
username?: string;
}
@@ -274,6 +276,8 @@ export interface ReportUserDto {
avatar?: string;
defaultDisplayUserId?: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
userId?: string;
username?: string;
}
@@ -336,6 +340,8 @@ export interface RechargeBillUserDto {
countryDisplayName?: string;
countryName?: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
userId?: string;
username?: string;
}
@@ -433,6 +439,8 @@ export interface BDProfileDto {
createdByUsername?: string;
createdByUserId?: number;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
parentLeaderUserId?: number;
parentLeaderDisplayUserId?: string;
parentLeaderUsername?: string;
@@ -481,6 +489,8 @@ export interface HostProfileDto {
currentMembershipId?: number;
diamond?: number;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
firstBecameHostAtMs?: number;
regionId?: number;
regionName?: string;
@@ -497,6 +507,8 @@ export interface CoinSellerDto {
createdAtMs?: number;
createdByUserId?: number;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
merchantAssetType?: string;
merchantBalance?: number;
regionId?: number;
@@ -568,10 +580,13 @@ export interface AppUserDto {
countryDisplayName?: string;
countryName?: string;
createdAtMs?: number;
+ defaultDisplayUserId?: string;
diamond?: number;
displayUserId?: string;
gender?: string;
lastActiveAtMs?: number;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
regionId?: number;
regionName?: string;
status?: string;
@@ -582,7 +597,10 @@ export interface AppUserDto {
export interface AppUserBriefDto {
avatar?: string;
+ defaultDisplayUserId?: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
userId: string;
username?: string;
}
@@ -591,8 +609,11 @@ export interface AppUserBanOperatorDto {
account?: string;
adminId?: string;
avatar?: string;
+ defaultDisplayUserId?: string;
displayUserId?: string;
name?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
type: "admin" | "app_user" | "system" | string;
userId?: string;
}
@@ -615,6 +636,7 @@ export interface AppUserLoginLogDto {
channel?: string;
country?: string;
createdAtMs?: number;
+ defaultDisplayUserId?: string;
displayUserId?: string;
failureCode?: string;
id: number;
@@ -622,6 +644,8 @@ export interface AppUserLoginLogDto {
loginIp?: string;
loginType?: string;
platform?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
provider?: string;
regionId?: number;
regionName?: string;
@@ -800,6 +824,8 @@ export interface RoomPinPayload {
export interface RoomOwnerDto {
avatar?: string;
displayUserId?: string;
+ prettyDisplayUserId?: string;
+ prettyId?: string;
userId?: string;
username?: string;
}
diff --git a/src/shared/ui/AdminUserIdentity.jsx b/src/shared/ui/AdminUserIdentity.jsx
new file mode 100644
index 0000000..c67217b
--- /dev/null
+++ b/src/shared/ui/AdminUserIdentity.jsx
@@ -0,0 +1,219 @@
+import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
+import { useAppUserDetail } from "@/features/app-users/components/AppUserDetailContext.jsx";
+import styles from "@/shared/ui/AdminUserIdentity.module.css";
+
+export function AdminUserIdentity({
+ avatar,
+ className = "",
+ defaultDisplayUserId,
+ displayUserId,
+ name,
+ onClick,
+ openInAppUserDetail = false,
+ prettyDisplayUserId,
+ prettyId,
+ rows,
+ size = "table",
+ user,
+ userId,
+}) {
+ const { openAppUserDetail } = useAppUserDetail();
+ const normalized = normalizeUserIdentity({
+ avatar,
+ defaultDisplayUserId,
+ displayUserId,
+ name,
+ prettyDisplayUserId,
+ prettyId,
+ user,
+ userId,
+ });
+ const shouldOpenAppUserDetail = openInAppUserDetail && normalized.userId && openAppUserDetail;
+ const Root = onClick || shouldOpenAppUserDetail ? "button" : "div";
+ const rootProps = resolveRootProps({
+ displayName: normalized.displayName,
+ onClick,
+ openAppUserDetail,
+ shouldOpenAppUserDetail,
+ user: normalized.detailUser,
+ });
+ const visibleRows = Array.isArray(rows)
+ ? rows.map((row) => String(row || "").trim()).filter(Boolean)
+ : [normalized.shortId].filter(Boolean);
+
+ return (
+
+
+ {normalized.avatar ? (
+
+ ) : (
+
+ )}
+
+
+
+ {normalized.displayName}
+
+ {visibleRows.length ? (
+
+
{visibleRows[0]}
+ {normalized.hasPretty ? (
+
+ ) : null}
+
+ ) : normalized.hasPretty ? (
+
+ ) : null}
+ {visibleRows.slice(1).map((row, index) => (
+
+ {row}
+
+ ))}
+
+
+ );
+}
+
+export function AdminPrettyDisplayID({ prettyDisplayUserId, prettyId }) {
+ const prettyValue = String(prettyDisplayUserId || "").trim();
+ const recordId = String(prettyId || "").trim();
+ if (!prettyValue || !recordId) {
+ return null;
+ }
+
+ return (
+
+ {prettyValue}
+
+ );
+}
+
+function normalizeUserIdentity({
+ avatar,
+ defaultDisplayUserId,
+ displayUserId,
+ name,
+ prettyDisplayUserId,
+ prettyId,
+ user,
+ userId,
+}) {
+ const source = user || {};
+ // 后台各模块的数据来源不统一:新接口是 camelCase,部分老接口仍透传 snake_case。
+ // 这里在公共入口一次性归一化,后续页面只关心“能否展示当前有效靓号”和“点击能否定位到用户”。
+ const resolvedUserId = firstNonEmpty(
+ userId,
+ source.userId,
+ source.user_id,
+ source.assignedUserId,
+ source.assigned_user_id,
+ source.targetUserId,
+ source.target_user_id,
+ );
+ const resolvedDefaultDisplayUserId = firstNonEmpty(
+ defaultDisplayUserId,
+ source.defaultDisplayUserId,
+ source.default_display_user_id,
+ );
+ const resolvedDisplayUserId = firstNonEmpty(
+ displayUserId,
+ source.displayUserId,
+ source.display_user_id,
+ resolvedDefaultDisplayUserId,
+ resolvedUserId,
+ );
+ const resolvedPrettyId = firstNonEmpty(prettyId, source.prettyId, source.pretty_id);
+ const resolvedPrettyDisplayUserId = firstNonEmpty(
+ prettyDisplayUserId,
+ source.prettyDisplayUserId,
+ source.pretty_display_user_id,
+ );
+ // 靓号只在 record id 和展示内容同时存在时显示;短号行优先取 defaultDisplayUserId,避免当前展示号被靓号覆盖后重复展示。
+ const hasPretty = Boolean(resolvedPrettyId && resolvedPrettyDisplayUserId);
+ const resolvedShortId = hasPretty
+ ? firstNonEmpty(resolvedDefaultDisplayUserId, resolvedUserId, resolvedDisplayUserId)
+ : firstNonEmpty(resolvedDisplayUserId, resolvedDefaultDisplayUserId, resolvedUserId);
+ const displayName = firstNonEmpty(
+ name,
+ source.username,
+ source.name,
+ source.account,
+ resolvedDisplayUserId,
+ resolvedUserId,
+ "-",
+ );
+
+ return {
+ avatar: firstNonEmpty(avatar, source.avatar),
+ detailUser: {
+ ...source,
+ avatar: firstNonEmpty(avatar, source.avatar),
+ defaultDisplayUserId: resolvedDefaultDisplayUserId,
+ displayUserId: resolvedDisplayUserId,
+ prettyDisplayUserId: resolvedPrettyDisplayUserId,
+ prettyId: resolvedPrettyId,
+ userId: resolvedUserId,
+ username: firstNonEmpty(name, source.username, source.name, source.account),
+ },
+ displayName,
+ hasPretty,
+ prettyDisplayUserId: resolvedPrettyDisplayUserId,
+ prettyId: resolvedPrettyId,
+ shortId: resolvedShortId || "-",
+ userId: resolvedUserId,
+ };
+}
+
+function resolveRootProps({ displayName, onClick, openAppUserDetail, shouldOpenAppUserDetail, user }) {
+ if (onClick) {
+ return {
+ "aria-label": `查看用户 ${displayName}`,
+ onClick: (event) => {
+ event.stopPropagation();
+ onClick(event);
+ },
+ type: "button",
+ };
+ }
+ if (shouldOpenAppUserDetail) {
+ return {
+ "aria-label": `查看用户详情 ${displayName}`,
+ onClick: (event) => {
+ event.stopPropagation();
+ openAppUserDetail(user);
+ },
+ type: "button",
+ };
+ }
+ return {};
+}
+
+function firstNonEmpty(...values) {
+ for (const value of values) {
+ const normalized = String(value ?? "").trim();
+ if (normalized) {
+ return normalized;
+ }
+ }
+ return "";
+}
diff --git a/src/shared/ui/AdminUserIdentity.module.css b/src/shared/ui/AdminUserIdentity.module.css
new file mode 100644
index 0000000..3a0c4ca
--- /dev/null
+++ b/src/shared/ui/AdminUserIdentity.module.css
@@ -0,0 +1,143 @@
+.root {
+ display: inline-flex;
+ max-width: 100%;
+ min-width: 0;
+ align-items: center;
+ gap: var(--space-3);
+ color: inherit;
+ text-align: left;
+}
+
+.button {
+ width: 100%;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ font: inherit;
+}
+
+.link {
+ text-decoration: none;
+}
+
+.clickable {
+ cursor: pointer;
+}
+
+.clickable:hover .name,
+.clickable:focus-visible .name {
+ color: var(--primary);
+}
+
+.clickable:focus-visible {
+ border-radius: var(--radius-sm);
+ outline: 2px solid var(--primary);
+ outline-offset: 3px;
+}
+
+.avatar {
+ display: inline-flex;
+ width: 36px;
+ height: 36px;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: var(--bg-card-strong);
+ color: var(--text-secondary);
+ font-weight: 700;
+}
+
+.large .avatar {
+ width: 64px;
+ height: 64px;
+}
+
+.avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.text {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+}
+
+.nameLine,
+.idLine {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+}
+
+.name {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--text-primary);
+ font-weight: 700;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.large .name {
+ font-size: 18px;
+ font-weight: 800;
+}
+
+.idText,
+.extraRow {
+ min-width: 0;
+ overflow-wrap: anywhere;
+ color: var(--text-tertiary);
+ font-size: var(--admin-font-size);
+ line-height: 1.3;
+}
+
+.prettyBadge {
+ display: inline-flex;
+ max-width: 100%;
+ min-width: 0;
+ align-items: center;
+ gap: 5px;
+ padding: 1px 7px;
+ border: 1px solid rgba(124, 58, 237, 0.26);
+ border-radius: 999px;
+ background: linear-gradient(90deg, rgba(255, 247, 179, 0.8), rgba(255, 225, 244, 0.78), rgba(219, 234, 254, 0.8));
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45);
+ line-height: 1.2;
+}
+
+.prettyValue {
+ min-width: 0;
+ overflow: hidden;
+ background: linear-gradient(90deg, #f6da57 0%, #ffa4a8 30%, #ff75c6 55%, #c089ff 78%, #56fffa 100%);
+ background-clip: text;
+ color: transparent;
+ font-weight: 850;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ -webkit-background-clip: text;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .name,
+ .prettyBadge {
+ transition:
+ color 160ms ease,
+ border-color 160ms ease,
+ box-shadow 160ms ease;
+ }
+
+ .clickable:hover .prettyBadge {
+ border-color: rgba(37, 99, 235, 0.38);
+ box-shadow:
+ 0 6px 16px rgba(124, 58, 237, 0.12),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.55);
+ }
+}
diff --git a/src/shared/ui/AdminUserIdentity.test.jsx b/src/shared/ui/AdminUserIdentity.test.jsx
new file mode 100644
index 0000000..3be1e1f
--- /dev/null
+++ b/src/shared/ui/AdminUserIdentity.test.jsx
@@ -0,0 +1,121 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, expect, test, vi } from "vitest";
+import { AppUserDetailProvider } from "@/features/app-users/components/AppUserDetailProvider.jsx";
+import { AppUserDetailContext } from "@/features/app-users/components/AppUserDetailContext.jsx";
+import { setAccessToken } from "@/shared/api/request";
+import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
+import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
+
+afterEach(() => {
+ setAccessToken("");
+ vi.unstubAllGlobals();
+});
+
+test("renders avatar name original id and pretty id without label", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("tester")).toBeInTheDocument();
+ expect(screen.getByText("123456")).toBeInTheDocument();
+ expect(screen.queryByText(["彩色", "靓号"].join(""))).not.toBeInTheDocument();
+ expect(screen.getByText("VIP2026")).toBeInTheDocument();
+ expect(document.querySelector("img")).toHaveAttribute("src", "https://cdn.example/avatar.png");
+});
+
+test("clickable identity opens detail handler", async () => {
+ const user = userEvent.setup();
+ const onClick = vi.fn();
+ render();
+
+ await user.click(screen.getByRole("button", { name: /查看用户/ }));
+
+ expect(onClick).toHaveBeenCalledTimes(1);
+});
+
+test("open detail uses provider instead of app user list link", async () => {
+ const user = userEvent.setup();
+ const openAppUserDetail = vi.fn();
+ render(
+
+
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /查看用户详情/ }));
+
+ expect(screen.queryByRole("link", { name: /跳转用户详情/ })).not.toBeInTheDocument();
+ expect(openAppUserDetail).toHaveBeenCalledWith(
+ expect.objectContaining({ displayUserId: "123456", userId: "10001" }),
+ );
+});
+
+test("open detail accepts assigned user id from pretty id records", async () => {
+ const user = userEvent.setup();
+ const openAppUserDetail = vi.fn();
+ render(
+
+
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /查看用户详情/ }));
+
+ expect(openAppUserDetail).toHaveBeenCalledWith(expect.objectContaining({ userId: "10001" }));
+});
+
+test("app user detail provider fetches detail and renders current pretty id", async () => {
+ const user = userEvent.setup();
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(
+ async () =>
+ new Response(
+ JSON.stringify({
+ code: 0,
+ data: {
+ display_user_id: "123456",
+ default_display_user_id: "123456",
+ pretty_display_user_id: "VIP2026",
+ pretty_id: "pretty-2026",
+ status: "active",
+ user_id: "10001",
+ username: "tester",
+ },
+ }),
+ ),
+ ),
+ );
+
+ render(
+
+
+
+
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /查看用户详情/ }));
+
+ expect((await screen.findAllByText("VIP2026")).length).toBeGreaterThan(0);
+ expect(screen.getAllByText("pretty-2026").length).toBeGreaterThan(0);
+ expect(String(vi.mocked(fetch).mock.calls[0][0])).toContain("/api/v1/app/users/10001");
+});