merge master into codex/wechat-mini-program-virtual-payment

This commit is contained in:
kdletters
2026-05-30 16:46:11 +08:00
119 changed files with 3345 additions and 428 deletions

View File

@@ -187,6 +187,7 @@ const authServiceMocks = vi.hoisted(() => ({
async (publicUserCode: string): Promise<PublicUserSummary> => ({
id: `public-user-${publicUserCode}`,
publicUserCode,
username: 'author_user',
displayName: '公开作者',
avatarUrl: null,
}),
@@ -195,6 +196,7 @@ const authServiceMocks = vi.hoisted(() => ({
async (userId: string): Promise<PublicUserSummary> => ({
id: userId,
publicUserCode: `code-${userId}`,
username: 'author_user',
displayName: '公开作者',
avatarUrl: null,
}),
@@ -1379,7 +1381,6 @@ const mockSession: CustomWorldAgentSessionSnapshot = {
const mockAuthUser: AuthUser = {
id: 'user-1',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
publicUserCode: 'user-tester',
@@ -1387,7 +1388,6 @@ const mockAuthUser: AuthUser = {
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
};
function buildMockCreativeAgentSession(
@@ -4271,7 +4271,7 @@ test('running puzzle form generation creates a new puzzle draft on same template
);
expect(
await screen.findByRole('progressbar', {
name: '拼图草稿生成进度',
name: '拼图图片生成进度',
}),
).toBeTruthy();
@@ -4307,7 +4307,7 @@ test('running puzzle form generation creates a new puzzle draft on same template
expect(
await screen.findByRole('progressbar', {
name: '拼图草稿生成进度',
name: '拼图图片生成进度',
}),
).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
@@ -4387,7 +4387,7 @@ test('running puzzle draft opens generation progress from draft tab', async () =
);
expect(
await screen.findByRole('progressbar', {
name: '拼图草稿生成进度',
name: '拼图图片生成进度',
}),
).toBeTruthy();
@@ -4401,7 +4401,7 @@ test('running puzzle draft opens generation progress from draft tab', async () =
expect(
await screen.findByRole('progressbar', {
name: '拼图草稿生成进度',
name: '拼图图片生成进度',
}),
).toBeTruthy();
expect(screen.queryByText('拼图结果页')).toBeNull();
@@ -5388,7 +5388,7 @@ test('embedded puzzle form recovers when compile request times out after backend
});
expect(screen.queryByText('执行拼图操作失败。')).toBeNull();
expect(screen.queryByText('请求超时90000ms')).toBeNull();
expect(screen.queryByText('拼图草稿生成进度')).toBeNull();
expect(screen.queryByText('拼图图片生成进度')).toBeNull();
expect(startLocalPuzzleRun).toHaveBeenCalledTimes(1);
});
@@ -7414,13 +7414,13 @@ test('persisted generating puzzle draft opens generation progress after refresh'
});
expect(
await screen.findByRole('progressbar', {
name: '拼图草稿生成进度',
name: '拼图图片生成进度',
}),
).toBeTruthy();
expect(
Number(
screen
.getByRole('progressbar', { name: '拼图草稿生成进度' })
.getByRole('progressbar', { name: '拼图图片生成进度' })
.getAttribute('aria-valuenow'),
),
).toBe(0);
@@ -7479,7 +7479,7 @@ test('persisted generating puzzle draft keeps session polling on the same sessio
expect(
await screen.findByRole('progressbar', {
name: '拼图草稿生成进度',
name: '拼图图片生成进度',
}),
).toBeTruthy();
@@ -7527,7 +7527,7 @@ test('puzzle compile timeout shows failure dialog when reread session is still g
expect(within(dialog).getByRole('button', { name: '复制报错' })).toBeTruthy();
expect(
await screen.findByRole('progressbar', {
name: '拼图草稿生成进度',
name: '拼图图片生成进度',
}),
).toBeTruthy();
});

View File

@@ -141,7 +141,6 @@ const {
amountDelta: 10,
balanceAfter: 10,
sourceType: 'daily_task_reward',
createdAt: '2026-05-03T08:01:00Z',
},
center: buildClaimedTaskCenter(),
})),
@@ -204,9 +203,9 @@ const {
amountCents: 600,
status: 'paid',
paymentChannel: 'mock',
createdAt: '2026-04-25T10:00:00Z',
paidAt: '2026-04-25T10:00:00Z',
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 120,
membershipExpiresAt: null,
},
@@ -237,9 +236,9 @@ const {
amountCents: 600,
status: 'paid',
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: '2026-04-25T10:01:00Z',
providerTransactionId: 'wx-transaction-1',
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 120,
membershipExpiresAt: null,
},
@@ -276,8 +275,8 @@ const {
amountCents: 600,
status: 'paid',
paymentChannel: 'wechat_mp_virtual',
providerTransactionId: 'wx-transaction-1',
createdAt: '2026-04-25T10:00:00Z',
providerTransactionId: 'wx-transaction-1',
paidAt: '2026-04-25T10:01:00Z',
pointsDelta: 120,
membershipExpiresAt: null,
@@ -305,14 +304,12 @@ const {
amountDelta: -1,
balanceAfter: 29,
sourceType: 'asset_operation_consume',
createdAt: '2026-04-28T10:00:00Z',
},
{
id: 'ledger-2',
amountDelta: 30,
balanceAfter: 30,
sourceType: 'invite_invitee_reward',
createdAt: '2026-04-28T09:00:00Z',
},
],
})),
@@ -328,6 +325,7 @@ const {
async (code: string): Promise<PublicUserSummary> => ({
id: `id-${code}`,
publicUserCode: code,
username: 'author_user',
displayName: '公开作者',
avatarUrl: null,
}),
@@ -336,6 +334,7 @@ const {
async (userId: string): Promise<PublicUserSummary> => ({
id: userId,
publicUserCode: `code-${userId}`,
username: 'author_user',
displayName: '公开作者',
avatarUrl: null,
}),
@@ -362,14 +361,12 @@ vi.mock('../../services/payment/paymentRedirect', () => ({
mockUpdateAuthProfile.mockResolvedValue({
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
});
vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
@@ -723,14 +720,12 @@ function ProfileHomeViewHarness({
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: DEFAULT_PROFILE_CREATED_AT,
...userOverrides,
},
canAccessProtectedData: true,
@@ -911,14 +906,12 @@ function renderLoggedInHomeView(
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),
@@ -1095,7 +1088,6 @@ afterEach(() => {
amountDelta: 10,
balanceAfter: 10,
sourceType: 'daily_task_reward',
createdAt: '2026-05-03T08:01:00Z',
},
center: mockBuildTaskCenter({
walletBalance: 10,
@@ -1121,14 +1113,12 @@ afterEach(() => {
mockUpdateAuthProfile.mockResolvedValue({
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: DEFAULT_PROFILE_CREATED_AT,
});
mockQrCodeToDataUrl.mockResolvedValue('data:image/png;base64,QR');
mockRedirectToPaymentUrl.mockReset();
@@ -1190,9 +1180,9 @@ test('profile recharge modal shows native qr code on desktop web by default', as
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_native',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -1255,9 +1245,9 @@ test('profile recharge modal jumps to h5 payment on mobile web by default', asyn
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_h5',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -1367,9 +1357,9 @@ test('profile recharge modal posts virtual payment params in mini program web-vi
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null as string | null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -1820,9 +1810,9 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null as string | null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -1859,9 +1849,9 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -1890,9 +1880,9 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
amountCents: 600,
status: 'paid' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: '2026-04-25T10:01:00Z',
providerTransactionId: 'wx-transaction-2',
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 120,
membershipExpiresAt: null,
},
@@ -1954,9 +1944,9 @@ test('profile recharge modal loads wechat js sdk before mini program payment bri
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null as string | null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -2040,9 +2030,9 @@ test('profile recharge modal releases submitting state after cancelled wechat pa
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null as string | null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -2123,9 +2113,9 @@ test('profile native qr confirmation refreshes only after server reports paid',
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_native',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
},
@@ -2157,9 +2147,9 @@ test('profile native qr confirmation refreshes only after server reports paid',
amountCents: 600,
status: 'paid' as const,
paymentChannel: 'wechat_native',
createdAt: '2026-04-25T10:00:00Z',
paidAt: '2026-04-25T10:01:00Z',
providerTransactionId: 'wx-native-1',
createdAt: '2026-04-25T10:00:00Z',
pointsDelta: 120,
membershipExpiresAt: null,
},
@@ -2564,7 +2554,7 @@ test('wallet ledger modal shows empty and error states', async () => {
test('profile community shortcut shows reward subtitle and invited users', async () => {
const user = userEvent.setup();
renderProfileView(vi.fn(), {}, { createdAt: buildFreshProfileCreatedAt() });
renderProfileView(vi.fn(), {});
expect(screen.queryByRole('button', { name: //u })).toBeNull();
@@ -2586,7 +2576,6 @@ test('profile page hides legacy redeem invite secondary shortcut for fresh accou
renderProfileView(
vi.fn(),
{},
{ createdAt: buildFreshProfileCreatedAt() },
);
const communityButton = screen.getByRole('button', { name: //u });
@@ -2618,7 +2607,7 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
});
unmount();
renderProfileView(vi.fn(), {}, { createdAt: '2026-04-01T00:00:00.000Z' });
renderProfileView(vi.fn(), {});
const expiredShortcutRegion = screen.getByRole('region', {
name: '常用功能',
});
@@ -2678,7 +2667,6 @@ test('profile redeem invite query modal submits code after login', async () => {
renderProfileView(
onRechargeSuccess,
{},
{ createdAt: buildFreshProfileCreatedAt() },
);
expect(await screen.findByLabelText('邀请码')).toBeTruthy();
@@ -2837,14 +2825,12 @@ test('logged in create tab shows real wallet balance beside the brand', () => {
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: DEFAULT_PROFILE_CREATED_AT,
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),
@@ -3335,14 +3321,12 @@ test('logged in recommend page uses gated recommend detail callback', async () =
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),
@@ -3471,14 +3455,12 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),
@@ -3648,6 +3630,7 @@ test('mobile recommend meta loads real author avatar from public user summary',
mockGetPublicAuthUserById.mockResolvedValueOnce({
id: 'user-2',
publicUserCode: 'SY-00000002',
username: 'puzzle_user',
displayName: '拼图玩家',
avatarUrl: 'data:image/png;base64,AUTHOR',
});
@@ -3907,14 +3890,12 @@ test('desktop logged in home syncs mobile home modules without square or latest
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),

View File

@@ -150,6 +150,7 @@ import {
isWoodenFishGalleryEntry,
type PlatformPublicGalleryCard,
type PlatformWorldCardLike,
resolvePlatformWorkAuthorDisplayName,
resolvePlatformPublicWorkCode,
resolvePlatformWorldCoverImage,
resolvePlatformWorldCoverSlides,
@@ -247,7 +248,6 @@ const AVATAR_MAX_FILE_SIZE = 5 * 1024 * 1024;
const AVATAR_OUTPUT_SIZE = 256;
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
@@ -615,6 +615,7 @@ function WorldCard({
onClick,
className,
authorAvatarUrl,
authorSummary,
feedCardKey,
enableCoverCarousel = false,
isCoverCarouselActive = false,
@@ -624,6 +625,7 @@ function WorldCard({
onClick: () => void;
className?: string;
authorAvatarUrl?: string | null;
authorSummary?: PublicUserSummary | null;
feedCardKey?: string;
enableCoverCarousel?: boolean;
isCoverCarouselActive?: boolean;
@@ -657,7 +659,10 @@ function WorldCard({
const remixCount = getPlatformWorldRemixCount(entry);
const likeCount = getPlatformWorldLikeCount(entry);
const typeLabel = describePublicGalleryCardKind(entry);
const authorName = entry.authorDisplayName.trim() || '玩家';
const authorName = resolvePlatformWorkAuthorDisplayName(
entry,
authorSummary,
);
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
const cardLabel = `${entry.worldName}${typeLabel}${formatCompactCount(playCount)}游玩,${formatCompactCount(remixCount)}改造,${formatCompactCount(likeCount)}点赞`;
@@ -939,6 +944,7 @@ function RecommendRuntimePreviewCard({
function RecommendSwipeCard({
entry,
authorAvatarUrl,
authorSummary,
isActive,
visual,
shareState,
@@ -952,6 +958,7 @@ function RecommendSwipeCard({
}: {
entry: PlatformPublicGalleryCard;
authorAvatarUrl?: string | null;
authorSummary?: PublicUserSummary | null;
isActive: boolean;
visual: ReactNode;
shareState?: 'idle' | 'copied' | 'failed';
@@ -976,6 +983,7 @@ function RecommendSwipeCard({
<RecommendRuntimeMeta
entry={entry}
authorAvatarUrl={authorAvatarUrl}
authorSummary={authorSummary}
isActive={isActive}
shareState={shareState}
onDragPointerDown={onDragPointerDown}
@@ -994,6 +1002,7 @@ function RecommendSwipeCard({
function RecommendRuntimeMeta({
entry,
authorAvatarUrl,
authorSummary,
onDragPointerDown,
onDragPointerMove,
onDragPointerUp,
@@ -1006,6 +1015,7 @@ function RecommendRuntimeMeta({
}: {
entry: PlatformPublicGalleryCard;
authorAvatarUrl?: string | null;
authorSummary?: PublicUserSummary | null;
onDragPointerDown?: (event: PointerEvent<HTMLElement>) => void;
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
@@ -1018,7 +1028,10 @@ function RecommendRuntimeMeta({
}) {
const likeCount = getPlatformWorldLikeCount(entry);
const remixCount = getPlatformWorldRemixCount(entry);
const authorName = entry.authorDisplayName.trim() || '玩家';
const authorName = resolvePlatformWorkAuthorDisplayName(
entry,
authorSummary,
);
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
const displayName = formatPlatformWorkDisplayName(entry.worldName);
@@ -1894,28 +1907,28 @@ async function getPublicWorkAuthorSummary(
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
if (isBigFishGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('??');
return formatPlatformWorkDisplayTag('大鱼吃小鱼');
}
if (isPuzzleGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('??');
return formatPlatformWorkDisplayTag('拼图');
}
if (isMatch3DGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('??');
return formatPlatformWorkDisplayTag('抓大鹅');
}
if (isSquareHoleGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('??');
return formatPlatformWorkDisplayTag('方洞挑战');
}
if (isJumpHopGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('???');
return formatPlatformWorkDisplayTag('跳一跳');
}
if (isWoodenFishGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('???');
return formatPlatformWorkDisplayTag('敲木鱼');
}
if (isVisualNovelGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('??');
return formatPlatformWorkDisplayTag('视觉小说');
}
if (isBarkBattleGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('??');
return formatPlatformWorkDisplayTag('汪汪声浪');
}
if (isEdutainmentGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag(entry.templateName);
@@ -2214,21 +2227,6 @@ function formatDashboardCount(value: number) {
return normalizedValue.toLocaleString('zh-CN');
}
function isWithinProfileInviteRedeemWindow(
createdAt: string | null | undefined,
) {
if (!createdAt) {
return false;
}
const createdTime = new Date(createdAt).getTime();
if (Number.isNaN(createdTime)) {
return false;
}
return Date.now() - createdTime <= PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS;
}
function normalizeProfileInviteQueryCode(value: string | null | undefined) {
return (value ?? '')
.trim()
@@ -2273,16 +2271,13 @@ function buildPublicUserCode(user: AuthUser | null | undefined) {
return user.publicUserCode.trim();
}
const raw =
user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
user?.username.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
'00000000';
const raw = user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() || '00000000';
return `SY-${raw.slice(-8).padStart(8, '0')}`;
}
function getUserAvatarLabel(user: AuthUser | null | undefined) {
return (user?.displayName || user?.username || '叙')
return (user?.displayName || '叙')
.slice(0, 1)
.toUpperCase();
}
@@ -4198,6 +4193,17 @@ export function RpgEntryHomeView({
},
[publicAuthorSummariesByKey],
);
const getPublicEntryAuthorSummary = useCallback(
(entry: PlatformPublicGalleryCard) => {
const authorLookupKey = buildPublicWorkAuthorLookupKey(entry);
if (!authorLookupKey) {
return null;
}
return publicAuthorSummariesByKey[authorLookupKey] ?? null;
},
[publicAuthorSummariesByKey],
);
const activeCategoryGroup =
categoryGroups.find((group) => group.tag === selectedCategoryTag) ??
categoryGroups[0] ??
@@ -4930,18 +4936,14 @@ export function RpgEntryHomeView({
});
}, []);
useEffect(() => {
if (
activeTab !== 'profile' ||
!isAuthenticated ||
!isWithinProfileInviteRedeemWindow(authUi?.user?.createdAt)
) {
if (activeTab !== 'profile' || !isAuthenticated) {
setIsReferralCenterInitialized(false);
setReferralCenter(null);
return;
}
loadReferralCenter();
}, [activeTab, authUi?.user?.createdAt, isAuthenticated, loadReferralCenter]);
}, [activeTab, isAuthenticated, loadReferralCenter]);
const openProfilePopupPanel = (panel: ProfileReferralPanel) => {
setProfilePopupPanel(panel);
setReferralError(null);
@@ -5589,6 +5591,9 @@ export function RpgEntryHomeView({
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
previousRecommendEntry,
)}
authorSummary={getPublicEntryAuthorSummary(
previousRecommendEntry,
)}
isActive={false}
visual={
<RecommendRuntimePreviewCard
@@ -5606,6 +5611,9 @@ export function RpgEntryHomeView({
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
activeRecommendEntry,
)}
authorSummary={getPublicEntryAuthorSummary(
activeRecommendEntry,
)}
isActive
visual={
<div className="platform-recommend-runtime-viewport">
@@ -5630,6 +5638,9 @@ export function RpgEntryHomeView({
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
nextRecommendEntry,
)}
authorSummary={getPublicEntryAuthorSummary(
nextRecommendEntry,
)}
isActive={false}
visual={
<RecommendRuntimePreviewCard
@@ -5783,6 +5794,7 @@ export function RpgEntryHomeView({
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
feedCardKey={cardKey}
/>
);
@@ -5844,13 +5856,14 @@ export function RpgEntryHomeView({
return (
<WorldCard
key={`${cardKey}:mobile-feed:${discoverChannel}`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
feedCardKey={cardKey}
enableCoverCarousel={mobileFeedCarouselEnabled}
isCoverCarouselActive={
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
feedCardKey={cardKey}
enableCoverCarousel={mobileFeedCarouselEnabled}
isCoverCarouselActive={
mobileCenteredCardKey === cardKey
}
/>
@@ -5954,6 +5967,7 @@ export function RpgEntryHomeView({
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
</div>
@@ -5981,6 +5995,7 @@ export function RpgEntryHomeView({
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
{onOpenChildMotionDemo ? (
@@ -6041,6 +6056,7 @@ export function RpgEntryHomeView({
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
</div>
@@ -6556,6 +6572,7 @@ export function RpgEntryHomeView({
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
</div>
@@ -6726,6 +6743,7 @@ export function RpgEntryHomeView({
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
</div>

View File

@@ -18,6 +18,7 @@ import {
mapWoodenFishWorkToPlatformGalleryCard,
type PlatformEdutainmentGalleryCard,
type PlatformPuzzleGalleryCard,
resolvePlatformWorkAuthorDisplayName,
resolvePlatformPublicWorkCode,
resolvePlatformWorldFallbackCoverImage,
} from './rpgEntryWorldPresentation';
@@ -197,6 +198,45 @@ test('maps wooden fish work to platform gallery card with WF public code', () =>
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['敲木鱼']);
});
test('resolves public work author from display name and public user code before stored author name', () => {
const card = mapWoodenFishWorkToPlatformGalleryCard({
publicWorkCode: 'WF-AUTHOR1',
workId: 'wooden-fish-work-author',
profileId: 'wooden-fish-profile-author',
ownerUserId: 'user-author',
authorDisplayName: '敲木鱼玩家',
workTitle: '莲花木鱼',
workDescription: '莲花主题敲木鱼。',
coverImageSrc: null,
themeTags: ['敲木鱼'],
publicationStatus: 'published',
playCount: 0,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: '2026-05-20T00:00:00.000Z',
generationStatus: 'ready',
});
expect(
resolvePlatformWorkAuthorDisplayName(card, {
id: 'user_00000004',
publicUserCode: 'SY-00000004',
username: 'phone_00000004',
displayName: '公开昵称',
avatarUrl: null,
}),
).toBe('公开昵称 · SY-00000004');
expect(
resolvePlatformWorkAuthorDisplayName(card, {
id: 'user_00000004',
publicUserCode: '',
username: 'phone_00000004',
displayName: '公开昵称',
avatarUrl: null,
}),
).toBe('公开昵称');
expect(resolvePlatformWorkAuthorDisplayName(card, null)).toBe('敲木鱼玩家');
});
test('keeps baby object match public card code and template label intact', () => {
const card: PlatformEdutainmentGalleryCard = {
sourceType: 'edutainment',

View File

@@ -1,4 +1,5 @@
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import { BABY_OBJECT_MATCH_EDUTAINMENT_TAG } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
@@ -219,6 +220,7 @@ export type PlatformWoodenFishGalleryCard = {
sourceSessionId?: string | null;
publicWorkCode: string;
ownerUserId: string;
authorUsername?: string | null;
authorDisplayName: string;
worldName: string;
subtitle: string;
@@ -562,6 +564,10 @@ export function mapWoodenFishWorkToPlatformGalleryCard(
? summary.publicWorkCode
: buildWoodenFishPublicWorkCode(summary.profileId),
ownerUserId: summary.ownerUserId,
authorUsername:
'authorUsername' in summary && typeof summary.authorUsername === 'string'
? summary.authorUsername
: null,
authorDisplayName:
'authorDisplayName' in summary ? summary.authorDisplayName : '玩家',
worldName: summary.workTitle,
@@ -857,6 +863,16 @@ export function formatPlatformWorkDisplayTags(
].slice(0, limit);
}
export function resolvePlatformWorkAuthorDisplayName(
entry: PlatformPublicGalleryCard,
authorSummary?: PublicUserSummary | null,
) {
const displayName = authorSummary?.displayName?.trim();
const publicUserCode = authorSummary?.publicUserCode?.trim();
return displayName || publicUserCode || entry.authorDisplayName.trim() || '玩家';
}
export function buildPlatformWorldDisplayTags(
entry: PlatformWorldCardLike,
limit = 3,
@@ -1063,4 +1079,4 @@ function buildBarkBattleThemeTags(work: BarkBattleWorkSummary) {
.map((tag) => tag.trim())
.filter(Boolean)
.slice(0, 3);
}
}