Merge branch 'codex/profile-mobile-ui-reference'
This commit is contained in:
@@ -35,8 +35,8 @@ import type {
|
||||
CustomWorldGalleryCard,
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import {
|
||||
readPublicWorkCodeFromLocationSearch,
|
||||
resolveSelectionStageFromPath,
|
||||
@@ -196,9 +196,30 @@ async function clickFirstAsyncButtonByName(
|
||||
|
||||
async function openCreateTemplateHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
await clickFirstButtonByName(user, '创作');
|
||||
expect(await screen.findByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: '拼图' })).toBeTruthy();
|
||||
expect(screen.getByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
const panel = getPlatformTabPanel('create');
|
||||
await waitFor(() => {
|
||||
expect(panel.getAttribute('aria-hidden')).toBe('false');
|
||||
});
|
||||
expect(
|
||||
await within(panel).findByRole('tablist', { name: '玩法模板分类' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
await within(panel).findByRole('button', { name: /拼图/u }),
|
||||
).toBeTruthy();
|
||||
expect(within(panel).queryByText('拼图工作区:missing-session')).toBeNull();
|
||||
return panel;
|
||||
}
|
||||
|
||||
async function findCreationTypeButton(name: string | RegExp) {
|
||||
const matcher =
|
||||
typeof name === 'string' ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u') : name;
|
||||
return within(getPlatformTabPanel('create')).findByRole('button', { name: matcher });
|
||||
}
|
||||
|
||||
function queryCreationTypeButton(name: string | RegExp) {
|
||||
const matcher =
|
||||
typeof name === 'string' ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u') : name;
|
||||
return within(getPlatformTabPanel('create')).queryByRole('button', { name: matcher });
|
||||
}
|
||||
|
||||
async function openDraftHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
@@ -208,7 +229,7 @@ async function openDraftHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
expect(panel.getAttribute('aria-hidden')).toBe('false');
|
||||
});
|
||||
expect(
|
||||
await within(panel).findByRole('button', { name: /全部/u }),
|
||||
await within(panel).findByRole('tab', { name: /全部/u }),
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
@@ -276,6 +297,14 @@ const testCreationEntryConfig = {
|
||||
title: '选择创作类型',
|
||||
description: '先选玩法类型,再进入对应创作工作台。',
|
||||
},
|
||||
eventBanner: {
|
||||
title: '泥点挑战',
|
||||
description: '创作活动测试横幅。',
|
||||
coverImageSrc: '/creation-type-references/puzzle.webp',
|
||||
prizePoolMudPoints: 1000,
|
||||
startsAtText: '2026-05-01',
|
||||
endsAtText: '2026-05-31',
|
||||
},
|
||||
creationTypes: [
|
||||
{
|
||||
id: 'rpg',
|
||||
@@ -286,6 +315,9 @@ const testCreationEntryConfig = {
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 10,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -297,6 +329,9 @@ const testCreationEntryConfig = {
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 30,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -308,6 +343,9 @@ const testCreationEntryConfig = {
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 40,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -319,6 +357,9 @@ const testCreationEntryConfig = {
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 45,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -330,6 +371,9 @@ const testCreationEntryConfig = {
|
||||
visible: false,
|
||||
open: true,
|
||||
sortOrder: 50,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -341,6 +385,9 @@ const testCreationEntryConfig = {
|
||||
visible: false,
|
||||
open: false,
|
||||
sortOrder: 60,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -352,6 +399,9 @@ const testCreationEntryConfig = {
|
||||
visible: true,
|
||||
open: false,
|
||||
sortOrder: 70,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -363,6 +413,9 @@ const testCreationEntryConfig = {
|
||||
visible: false,
|
||||
open: true,
|
||||
sortOrder: 80,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
@@ -374,6 +427,9 @@ const testCreationEntryConfig = {
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 90,
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
categorySortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
],
|
||||
@@ -3345,81 +3401,80 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
|
||||
expect(screen.getByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(screen.getByRole('tablist', { name: '选择模板' }).className).toContain(
|
||||
expect(screen.getByRole('tablist', { name: '玩法模板分类' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('tablist', { name: '玩法模板分类' }).className,
|
||||
).toContain(
|
||||
'scroll-px-3',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).getAttribute('aria-selected'),
|
||||
screen.getByRole('tab', { name: '最近创作' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/puzzle.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '文字冒险' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/rpg.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '抓大鹅' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/match3d.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '汪汪声浪' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/bark-battle.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '宝贝识物' }).querySelector('img')?.src,
|
||||
).toContain('/child-motion-demo/picture-book-grass-stage.png');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('.text-white'),
|
||||
await findCreationTypeButton('拼图'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('.text-inherit'),
|
||||
await findCreationTypeButton('文字冒险'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
await findCreationTypeButton('抓大鹅'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
await findCreationTypeButton('汪汪声浪'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
await findCreationTypeButton('宝贝识物'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
queryCreationTypeButton('智能创作'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
screen
|
||||
.getByRole('tab', { name: '最近创作' })
|
||||
.querySelector('[class*="bg-[#d9793f]"]'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: /智能创作/u })).toBeNull();
|
||||
expect(screen.queryByPlaceholderText('问一问陶泥儿')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /角色扮演/u })).toBeNull();
|
||||
expect(screen.queryByRole('tab', { name: /方洞挑战/u })).toBeNull();
|
||||
expect(screen.queryByRole('tab', { name: '视觉小说' })).toBeNull();
|
||||
expect(screen.getByRole('tab', { name: /抓大鹅/u })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /汪汪声浪/u })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /宝贝识物/u })).toBeTruthy();
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('create tab switches match3d into the embedded entry form', async () => {
|
||||
test('create tab opens match3d entry form from the template card', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '抓大鹅' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(await screen.findByText('抓大鹅工作区:missing-session')).toBeTruthy();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('create tab switches bark battle into the embedded config form', async () => {
|
||||
test('create tab opens puzzle entry form from the template card', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await findCreationTypeButton('拼图'));
|
||||
|
||||
expect(await screen.findByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('create tab opens bark battle entry form from the template card', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await findCreationTypeButton('汪汪声浪'));
|
||||
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '汪汪声浪' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(await screen.findByText('汪汪声浪配置表单')).toBeTruthy();
|
||||
expect(screen.getByTestId('bark-battle-editor-back-state').textContent).toBe(
|
||||
'back-hidden',
|
||||
);
|
||||
expect(screen.getByTestId('bark-battle-editor-title-state').textContent).toBe(
|
||||
'title-hidden',
|
||||
);
|
||||
expect(screen.queryByText('汪汪声浪运行态')).toBeNull();
|
||||
expect(createBarkBattleDraft).not.toHaveBeenCalled();
|
||||
expect(publishBarkBattleWork).not.toHaveBeenCalled();
|
||||
@@ -3431,7 +3486,7 @@ test('bark battle draft result can test before publish and publish to work detai
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await findCreationTypeButton('汪汪声浪'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
expect(createBarkBattleDraft).toHaveBeenCalledWith({
|
||||
@@ -3525,7 +3580,7 @@ test('bark battle form checks mud points before creating image assets', async ()
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await findCreationTypeButton('汪汪声浪'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
expect(
|
||||
@@ -3547,7 +3602,7 @@ test('bark battle draft is visible in draft shelf while image assets are generat
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await findCreationTypeButton('汪汪声浪'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
expect(await screen.findByText('自动生成素材')).toBeTruthy();
|
||||
@@ -3596,7 +3651,7 @@ test('published bark battle stays visible when refresh temporarily returns only
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await findCreationTypeButton('汪汪声浪'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
await waitFor(() => {
|
||||
expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith(
|
||||
@@ -3811,7 +3866,17 @@ test('persisted generating match3d draft opens generation progress after refresh
|
||||
'match3d-session-generating',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('抓大鹅草稿生成进度')).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '抓大鹅草稿生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '抓大鹅草稿生成进度' })
|
||||
.getAttribute('aria-valuenow'),
|
||||
).toBe('0');
|
||||
expect(screen.getByText('0%')).toBeTruthy();
|
||||
expect(screen.queryByText('抓大鹅结果页')).toBeNull();
|
||||
expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
|
||||
'match3d-profile-generating',
|
||||
@@ -4514,7 +4579,9 @@ test('match3d result back returns to platform creation page', async () => {
|
||||
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
expect(await screen.findByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('tablist', { name: '玩法模板分类' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('抓大鹅结果页')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -6788,7 +6855,9 @@ test('puzzle draft result back button returns to creation hub', async () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
expect(await screen.findByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('tablist', { name: '玩法模板分类' }),
|
||||
).toBeTruthy();
|
||||
expect(await screen.findByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
|
||||
@@ -6825,14 +6894,15 @@ test('persisted generating puzzle draft opens generation progress after refresh'
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(getPuzzleAgentSession).mockResolvedValueOnce({
|
||||
session: buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-session-generating',
|
||||
stage: 'collecting_anchors',
|
||||
progressPercent: 42,
|
||||
lastAssistantReply: '正在生成拼图草稿。',
|
||||
updatedAt: '2026-05-18T12:00:00.000Z',
|
||||
}),
|
||||
const persistedGeneratingPuzzleSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-session-generating',
|
||||
stage: 'collecting_anchors',
|
||||
progressPercent: 88,
|
||||
lastAssistantReply: '正在生成拼图草稿。',
|
||||
updatedAt: '2026-05-18T12:00:00.000Z',
|
||||
});
|
||||
vi.mocked(getPuzzleAgentSession).mockResolvedValue({
|
||||
session: persistedGeneratingPuzzleSession,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
@@ -6845,7 +6915,19 @@ test('persisted generating puzzle draft opens generation progress after refresh'
|
||||
'puzzle-session-generating',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('拼图草稿生成进度')).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
Number(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '拼图草稿生成进度' })
|
||||
.getAttribute('aria-valuenow'),
|
||||
),
|
||||
).toBe(0);
|
||||
expect(screen.getByText('0%')).toBeTruthy();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -7844,7 +7926,9 @@ test('running custom world draft generation can return to creation center with s
|
||||
expect(await screen.findByText('世界草稿生成进度')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
|
||||
expect(await screen.findByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('tablist', { name: '玩法模板分类' }),
|
||||
).toBeTruthy();
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByText('潮雾列岛')).toBeTruthy();
|
||||
@@ -8892,7 +8976,9 @@ test('agent draft result back button returns to creation hub without syncing res
|
||||
await user.click(screen.getByRole('button', { name: /返回创作/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('tablist', { name: '玩法模板分类' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -9215,14 +9301,16 @@ test('manual tab switch is preserved after platform bootstrap requests finish',
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await clickFirstButtonByName(user, '创作');
|
||||
expect(await screen.findByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('tablist', { name: '玩法模板分类' }),
|
||||
).toBeTruthy();
|
||||
|
||||
resolveGalleryRequest([]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getByRole('tablist', {
|
||||
name: '选择模板',
|
||||
name: '玩法模板分类',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -413,6 +413,11 @@ const originalUserAgent = navigator.userAgent;
|
||||
const originalMaxTouchPoints = navigator.maxTouchPoints;
|
||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||
const originalCancelAnimationFrame = window.cancelAnimationFrame;
|
||||
const DEFAULT_PROFILE_CREATED_AT = '2026-04-01T00:00:00.000Z';
|
||||
|
||||
function buildFreshProfileCreatedAt() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function dispatchPointerEvent(
|
||||
target: HTMLElement,
|
||||
@@ -670,6 +675,33 @@ function mockWechatMobileLayout() {
|
||||
});
|
||||
}
|
||||
|
||||
function mockNarrowMobileLayout() {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
configurable: true,
|
||||
value:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit Mobile',
|
||||
});
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => {
|
||||
const normalizedQuery = query.replace(/\s/g, '');
|
||||
return {
|
||||
matches:
|
||||
normalizedQuery.includes('max-width:767px') ||
|
||||
normalizedQuery.includes('max-width:768px'),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function renderProfileView(
|
||||
onRechargeSuccess = vi.fn(),
|
||||
profileDashboardOverrides: Partial<
|
||||
@@ -690,7 +722,7 @@ function renderProfileView(
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdAt: DEFAULT_PROFILE_CREATED_AT,
|
||||
...userOverrides,
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
@@ -1056,7 +1088,7 @@ afterEach(() => {
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdAt: DEFAULT_PROFILE_CREATED_AT,
|
||||
});
|
||||
mockQrCodeToDataUrl.mockResolvedValue('data:image/png;base64,QR');
|
||||
mockRedirectToPaymentUrl.mockReset();
|
||||
@@ -1094,7 +1126,9 @@ test('opens wallet ledger modal from narrative coin card', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView();
|
||||
await user.click(screen.getByRole('button', { name: /泥点\s*0/u }));
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /泥点余额\s*0/u }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('泥点账单')).toBeTruthy();
|
||||
expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1);
|
||||
@@ -1760,19 +1794,21 @@ test('profile native qr confirmation refreshes only after server reports paid',
|
||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('non-wechat profile shows reward code instead of recharge entry', async () => {
|
||||
test('non-wechat profile opens reward code from recharge-shaped entry', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView();
|
||||
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
expect(
|
||||
within(shortcutRegion).queryByRole('button', { name: /充值/u }),
|
||||
).toBeNull();
|
||||
within(shortcutRegion).getByRole('button', { name: /泥点充值/u }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: /兑换码/u }),
|
||||
).toBeTruthy();
|
||||
await user.click(within(shortcutRegion).getByRole('button', { name: /兑换码/u }));
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /泥点充值/u }),
|
||||
);
|
||||
expect(await screen.findByPlaceholderText('输入兑换码')).toBeTruthy();
|
||||
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1821,7 +1857,7 @@ test('profile played works card shows count unit', () => {
|
||||
});
|
||||
|
||||
const playedCard = screen.getByRole('button', {
|
||||
name: /玩过\s*1个/u,
|
||||
name: /已玩游戏数量\s*1个/u,
|
||||
});
|
||||
|
||||
expect(within(playedCard).getByText('1个')).toBeTruthy();
|
||||
@@ -1832,18 +1868,120 @@ test('profile stats cards are centered without update timestamp', () => {
|
||||
updatedAt: '2026-05-03T08:01:00Z',
|
||||
});
|
||||
|
||||
const walletCard = screen.getByRole('button', { name: /泥点\s*0/u });
|
||||
const playTimeCard = screen.getByRole('button', { name: /游戏时长/u });
|
||||
const playedCard = screen.getByRole('button', { name: /玩过\s*0个/u });
|
||||
const walletCard = screen.getByRole('button', {
|
||||
name: /泥点余额\s*0/u,
|
||||
});
|
||||
const playTimeCard = screen.getByRole('button', { name: /游戏时长|累计游戏时长/u });
|
||||
const playedCard = screen.getByRole('button', { name: /已玩游戏数量\s*0个/u });
|
||||
|
||||
for (const card of [walletCard, playTimeCard, playedCard]) {
|
||||
expect(card.className).toContain('items-center');
|
||||
expect(card.className).toContain('justify-center');
|
||||
expect(card.className).toContain('platform-profile-stat-card');
|
||||
expect(card.className).toContain('text-center');
|
||||
}
|
||||
expect(screen.queryByText(/更新于/u)).toBeNull();
|
||||
});
|
||||
|
||||
test('mobile profile page matches the reference layout sections', async () => {
|
||||
mockWechatMobileLayout();
|
||||
|
||||
const { container } = renderProfileView(vi.fn(), {
|
||||
walletBalance: 70,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
}, { createdAt: buildFreshProfileCreatedAt() });
|
||||
|
||||
const profilePage = container.querySelector('.platform-profile-page');
|
||||
expect(profilePage).toBeTruthy();
|
||||
expect(profilePage?.classList.contains('platform-page-stage')).toBe(true);
|
||||
expect(profilePage?.querySelector('.platform-profile-scene-decor')).toBeTruthy();
|
||||
expect(profilePage?.classList.contains('platform-profile-page')).toBe(true);
|
||||
expect(profilePage?.getAttribute('style') ?? '').not.toContain('overflow: hidden');
|
||||
|
||||
const membershipCard = screen.getByRole('button', { name: '查看权益' });
|
||||
expect(membershipCard.className).toContain('platform-profile-membership-card');
|
||||
expect(
|
||||
within(membershipCard).getByText('普通用户').className,
|
||||
).toContain('platform-profile-membership-card__title');
|
||||
expect(within(membershipCard).getByText('普通用户')).toBeTruthy();
|
||||
expect(within(membershipCard).getByText('升级会员,享专属特权与福利')).toBeTruthy();
|
||||
|
||||
const statPanel = screen.getByRole('region', { name: '我的数据' });
|
||||
expect(statPanel.className).toContain('platform-profile-stats-panel');
|
||||
expect(statPanel.querySelector('.platform-profile-stats-grid')).toBeTruthy();
|
||||
expect(within(statPanel).getByRole('button', { name: /泥点余额\s*70/u })).toBeTruthy();
|
||||
expect(within(statPanel).getByRole('button', { name: /累计游戏时长\s*0小时/u })).toBeTruthy();
|
||||
expect(within(statPanel).getByRole('button', { name: /已玩游戏数量\s*0个/u })).toBeTruthy();
|
||||
expect(
|
||||
within(statPanel).getByRole('button', { name: /泥点余额\s*70/u }).className,
|
||||
).toContain('platform-profile-stat-card');
|
||||
|
||||
const dailyTask = screen.getByRole('button', { name: /每日任务/u });
|
||||
expect(dailyTask.className).toContain('platform-profile-daily-task-card');
|
||||
expect(dailyTask.querySelector('.platform-profile-daily-task-card__title')).toBeTruthy();
|
||||
expect(dailyTask.querySelector('.platform-profile-daily-task-card__desc')).toBeTruthy();
|
||||
expect(dailyTask.querySelector('.platform-profile-daily-task-card__progress')).toBeTruthy();
|
||||
expect(dailyTask.textContent).toContain('完成任务可领取 10 泥点');
|
||||
expect(within(dailyTask).getByText('0 / 1')).toBeTruthy();
|
||||
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
expect(
|
||||
shortcutRegion.querySelector('.platform-profile-shortcut-grid'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
shortcutRegion.querySelectorAll('.platform-profile-shortcut-button'),
|
||||
).toHaveLength(5);
|
||||
expect(
|
||||
shortcutRegion
|
||||
.querySelector('.platform-profile-shortcut-grid')
|
||||
?.classList.contains('platform-profile-shortcut-grid'),
|
||||
).toBe(true);
|
||||
for (const label of [
|
||||
'泥点充值',
|
||||
'邀请好友',
|
||||
'兑换码',
|
||||
'玩家社区',
|
||||
'反馈与建议',
|
||||
]) {
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: new RegExp(label, 'u') }),
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
const settingsRegion = screen.getByRole('region', { name: '设置入口' });
|
||||
for (const label of ['主题设置', '账号与安全', '通用设置']) {
|
||||
expect(
|
||||
within(settingsRegion).getByRole('button', { name: new RegExp(label, 'u') }),
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
const secondaryShortcuts = screen.getByRole('region', {
|
||||
name: '次级入口',
|
||||
});
|
||||
expect(
|
||||
within(secondaryShortcuts).getByRole('button', { name: /存档/u }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
await within(secondaryShortcuts).findByRole('button', {
|
||||
name: /填邀请码/u,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
|
||||
const profileHeader = profilePage?.querySelector('.platform-profile-header');
|
||||
expect(profileHeader).toBeTruthy();
|
||||
expect(profileHeader?.querySelector('.platform-profile-header__identity-row')).toBeTruthy();
|
||||
expect(profileHeader?.querySelector('.platform-profile-header__name')).toBeTruthy();
|
||||
expect(profileHeader?.querySelector('.platform-profile-header__code')).toBeTruthy();
|
||||
|
||||
const legalRegion = screen.getByRole('region', { name: '法律信息' });
|
||||
expect(legalRegion.className).toContain('platform-profile-legal-strip');
|
||||
expect(legalRegion.textContent).toContain('用户协议');
|
||||
expect(legalRegion.textContent).toContain('隐私政策');
|
||||
expect(legalRegion.textContent).toContain('免责声明');
|
||||
expect(legalRegion.textContent).toContain(ICP_RECORD_NUMBER);
|
||||
expect(legalRegion.textContent).toContain('2026025677');
|
||||
expect(legalRegion.querySelector('.platform-profile-legal-strip__link')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('desktop account entry uses saved avatar image when available', () => {
|
||||
mockDesktopLayout();
|
||||
const avatarUrl = 'data:image/png;base64,AAAA';
|
||||
@@ -1886,27 +2024,33 @@ test('wallet ledger modal shows empty and error states', async () => {
|
||||
mockGetRpgProfileWalletLedger.mockResolvedValueOnce({ entries: [] });
|
||||
|
||||
renderProfileView();
|
||||
await user.click(screen.getByRole('button', { name: /泥点\s*0/u }));
|
||||
expect(await screen.findByText('暂无账单记录')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: /泥点余额\s*0/u }));
|
||||
expect(await screen.findByText('泥点账单')).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('暂无账单记录')).toBeTruthy();
|
||||
});
|
||||
|
||||
await user.click(screen.getByLabelText('关闭泥点账单'));
|
||||
mockGetRpgProfileWalletLedger.mockRejectedValueOnce(new Error('加载失败'));
|
||||
await user.click(screen.getByRole('button', { name: /泥点\s*0/u }));
|
||||
await user.click(screen.getByRole('button', { name: /泥点余额\s*0/u }));
|
||||
|
||||
expect(await screen.findByText('加载失败')).toBeTruthy();
|
||||
expect(await screen.findByText('泥点账单')).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('加载失败')).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText('重新加载')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('profile invite shortcut shows reward subtitle and invited users', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView();
|
||||
renderProfileView(vi.fn(), {}, { createdAt: buildFreshProfileCreatedAt() });
|
||||
|
||||
const inviteButton = screen.getByRole('button', { name: /邀请好友/u });
|
||||
expect(within(inviteButton).getByText('双方得30')).toBeTruthy();
|
||||
expect(within(inviteButton).getByText('双方得 30 泥点')).toBeTruthy();
|
||||
|
||||
const communityButton = screen.getByRole('button', { name: /玩家社区/u });
|
||||
expect(within(communityButton).getByText('每日领福利')).toBeTruthy();
|
||||
expect(within(communityButton).getByText('交流心得 领取福利')).toBeTruthy();
|
||||
|
||||
await user.click(inviteButton);
|
||||
|
||||
@@ -1922,21 +2066,25 @@ test('profile invite shortcut shows reward subtitle and invited users', async ()
|
||||
});
|
||||
|
||||
test('profile redeem invite shortcut sits between invite and community for fresh accounts', async () => {
|
||||
renderProfileView();
|
||||
renderProfileView(
|
||||
vi.fn(),
|
||||
{},
|
||||
{ createdAt: buildFreshProfileCreatedAt() },
|
||||
);
|
||||
|
||||
const inviteButton = screen.getByRole('button', { name: /邀请好友/u });
|
||||
const redeemButton = await screen.findByRole('button', {
|
||||
name: /填邀请码/u,
|
||||
});
|
||||
const communityButton = screen.getByRole('button', { name: /玩家社区/u });
|
||||
const secondaryShortcuts = screen.getByRole('region', {
|
||||
name: '次级入口',
|
||||
});
|
||||
|
||||
expect(inviteButton).toBeTruthy();
|
||||
expect(communityButton).toBeTruthy();
|
||||
expect(
|
||||
inviteButton.compareDocumentPosition(redeemButton) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
redeemButton.compareDocumentPosition(communityButton) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
within(secondaryShortcuts).getByRole('button', { name: /填邀请码/u }),
|
||||
).toBeTruthy();
|
||||
expect(within(redeemButton).getByText('新用户奖励')).toBeTruthy();
|
||||
});
|
||||
@@ -2006,7 +2154,11 @@ test('profile redeem invite modal submits code and hides shortcut after success'
|
||||
const user = userEvent.setup();
|
||||
const onRechargeSuccess = vi.fn();
|
||||
|
||||
renderProfileView(onRechargeSuccess);
|
||||
renderProfileView(
|
||||
onRechargeSuccess,
|
||||
{},
|
||||
{ createdAt: buildFreshProfileCreatedAt() },
|
||||
);
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /填邀请码/u }));
|
||||
const input = await screen.findByLabelText('邀请码');
|
||||
@@ -2050,11 +2202,10 @@ test('profile page shows legal entries and ICP record link', async () => {
|
||||
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
expect(
|
||||
shortcutRegion.querySelector('.grid')?.className.includes('grid-cols-3'),
|
||||
shortcutRegion
|
||||
.querySelector('.platform-profile-shortcut-grid')
|
||||
?.classList.contains('platform-profile-shortcut-grid'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: /每日任务/u }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: /邀请好友/u }),
|
||||
).toBeTruthy();
|
||||
@@ -2064,6 +2215,24 @@ test('profile page shows legal entries and ICP record link', async () => {
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: /反馈/u }),
|
||||
).toBeTruthy();
|
||||
const dailyTask = screen.getByRole('button', { name: /每日任务/u });
|
||||
expect(dailyTask).toBeTruthy();
|
||||
expect(dailyTask.textContent).toContain('完成任务可领取 10 泥点');
|
||||
|
||||
const settingsRegion = screen.getByRole('region', { name: '设置入口' });
|
||||
expect(
|
||||
within(settingsRegion).getByRole('button', { name: /存档/u }),
|
||||
).toBeTruthy();
|
||||
|
||||
const secondaryShortcuts = screen.getByRole('region', {
|
||||
name: '次级入口',
|
||||
});
|
||||
expect(
|
||||
within(secondaryShortcuts).getByRole('button', { name: /存档/u }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(secondaryShortcuts).queryByRole('button', { name: /填邀请码/u }),
|
||||
).toBeNull();
|
||||
|
||||
const legalRegion = screen.getByRole('region', { name: '法律信息' });
|
||||
expect(
|
||||
@@ -2138,6 +2307,83 @@ test('logged in draft bottom tab shows unread marker', () => {
|
||||
expect(draftButton.querySelector('.platform-nav-unread-dot')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('logged in create tab shows real wallet balance beside the brand', () => {
|
||||
mockNarrowMobileLayout();
|
||||
|
||||
const { container } = render(
|
||||
<AuthUiContext.Provider
|
||||
value={{
|
||||
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(),
|
||||
requireAuth: (action) => action(),
|
||||
openSettingsModal: vi.fn(),
|
||||
openAccountModal: vi.fn(),
|
||||
setCurrentUser: vi.fn(),
|
||||
logout: vi.fn(async () => undefined),
|
||||
musicVolume: 0.42,
|
||||
setMusicVolume: vi.fn(),
|
||||
platformTheme: 'light',
|
||||
setPlatformTheme: vi.fn(),
|
||||
isHydratingSettings: false,
|
||||
isPersistingSettings: false,
|
||||
settingsError: null,
|
||||
}}
|
||||
>
|
||||
<RpgEntryHomeView
|
||||
activeTab="create"
|
||||
onTabChange={vi.fn()}
|
||||
hasSavedGame={false}
|
||||
savedSnapshot={null}
|
||||
saveEntries={[]}
|
||||
saveError={null}
|
||||
featuredEntries={[]}
|
||||
latestEntries={[]}
|
||||
myEntries={[]}
|
||||
historyEntries={[]}
|
||||
profileDashboard={{
|
||||
walletBalance: 1234,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
updatedAt: null,
|
||||
}}
|
||||
isLoadingPlatform={false}
|
||||
isLoadingDashboard={false}
|
||||
isResumingSaveWorldKey={null}
|
||||
platformError={null}
|
||||
dashboardError={null}
|
||||
onContinueGame={vi.fn()}
|
||||
onResumeSave={vi.fn()}
|
||||
onOpenCreateWorld={vi.fn()}
|
||||
onOpenCreateTypePicker={vi.fn()}
|
||||
onOpenGalleryDetail={vi.fn()}
|
||||
onOpenLibraryDetail={vi.fn()}
|
||||
onSearchPublicCode={vi.fn()}
|
||||
createTabContent={<div>创作内容</div>}
|
||||
/>
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
const topbar = container.querySelector('.platform-mobile-topbar');
|
||||
expect(topbar).toBeTruthy();
|
||||
expect(
|
||||
topbar?.querySelector('.platform-mobile-create-wallet-chip'),
|
||||
).toBeTruthy();
|
||||
expect(topbar?.textContent).toContain('陶泥儿');
|
||||
expect(topbar?.textContent).toContain('1,234泥点');
|
||||
});
|
||||
|
||||
test('mobile discover search submits public work code', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSearchPublicCode = vi.fn();
|
||||
@@ -2248,6 +2494,15 @@ test('mobile discover keeps edutainment works in the last dedicated channel only
|
||||
throw new Error('缺少发现面板');
|
||||
}
|
||||
|
||||
const discoverStage = discoverPanel.querySelector(
|
||||
'.platform-mobile-home-stage',
|
||||
);
|
||||
expect(discoverStage).toBeTruthy();
|
||||
expect(discoverStage?.classList.contains('platform-remap-surface')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(discoverStage?.classList.contains('platform-page-stage')).toBe(false);
|
||||
|
||||
const channels = Array.from(
|
||||
discoverPanel.querySelectorAll('.platform-mobile-home-channel'),
|
||||
).map((button) => button.textContent);
|
||||
@@ -3135,7 +3390,6 @@ test('desktop logged in home syncs mobile home modules without square or latest
|
||||
expect(screen.queryByText('作品广场')).toBeNull();
|
||||
expect(screen.queryByText('公开作品')).toBeNull();
|
||||
expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
|
||||
expect(screen.getAllByText('拼图').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('1777110165.990127Z')).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Coins,
|
||||
Compass,
|
||||
Copy,
|
||||
FileText,
|
||||
Crown,
|
||||
Gamepad2,
|
||||
GitFork,
|
||||
Heart,
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
ScanLine,
|
||||
Search,
|
||||
Settings,
|
||||
Share2,
|
||||
ShieldCheck,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Star,
|
||||
@@ -45,6 +47,16 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import profileClockImage from '../../../media/profile/_Image (1).png';
|
||||
import profileGamepadImage from '../../../media/profile/_Image (2).png';
|
||||
import profileStillLifeImage from '../../../media/profile/_Image (3).png';
|
||||
import profileCoinsImage from '../../../media/profile/_Image (4).png';
|
||||
import profileInviteImage from '../../../media/profile/_Image (5).png';
|
||||
import profileGiftImage from '../../../media/profile/_Image (6).png';
|
||||
import profileCommunityImage from '../../../media/profile/_Image (7).png';
|
||||
import profileFeedbackImage from '../../../media/profile/_Image (8).png';
|
||||
import profileMascotImage from '../../../media/profile/_Image (9).png';
|
||||
import profilePointImage from '../../../media/profile/_Image.png';
|
||||
import communityQqQrImage from '../../../media/social-media-group/qq.png';
|
||||
import communityWechatQrImage from '../../../media/social-media-group/wechat.png';
|
||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||
@@ -215,8 +227,12 @@ const MOBILE_PAGE_STAGE_CLASS =
|
||||
'platform-page-stage platform-remap-surface min-w-0 space-y-4 overflow-hidden pb-2';
|
||||
const MOBILE_RECOMMEND_PAGE_STAGE_CLASS =
|
||||
'platform-page-stage min-w-0 space-y-4 overflow-hidden pb-2';
|
||||
const MOBILE_DISCOVER_PAGE_STAGE_CLASS =
|
||||
'platform-remap-surface min-w-0 space-y-4 overflow-hidden pb-2';
|
||||
const DESKTOP_PAGE_STAGE_CLASS =
|
||||
'platform-page-stage platform-remap-surface min-w-0 space-y-5 pb-4';
|
||||
const DESKTOP_DISCOVER_PAGE_STAGE_CLASS =
|
||||
'platform-remap-surface min-w-0 space-y-5 pb-4';
|
||||
const DESKTOP_LAYOUT_QUERY = '(min-width: 1024px)';
|
||||
const PLATFORM_HOME_TABS: PlatformHomeTab[] = [
|
||||
'home',
|
||||
@@ -2384,12 +2400,14 @@ function ProfileStatCard({
|
||||
value,
|
||||
onClick,
|
||||
icon,
|
||||
imageSrc,
|
||||
}: {
|
||||
cardKey: ProfileDashboardCardKey;
|
||||
label: string;
|
||||
value: string;
|
||||
onClick?: ((cardKey: ProfileDashboardCardKey) => void) | null;
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
imageSrc?: string;
|
||||
}) {
|
||||
const Icon = icon;
|
||||
|
||||
@@ -2397,16 +2415,23 @@ function ProfileStatCard({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick ? () => onClick(cardKey) : undefined}
|
||||
className="platform-subpanel flex min-h-[5.75rem] flex-col items-center justify-center rounded-[1.35rem] px-3 py-3 text-center transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
|
||||
aria-label={`${label} ${value}`}
|
||||
className="platform-profile-stat-card flex min-h-[5.75rem] items-center justify-center gap-2 px-3 py-3 text-center transition"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2 text-[var(--platform-text-soft)]">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="whitespace-nowrap text-[11px] tracking-[0.16em]">
|
||||
{label}
|
||||
</span>
|
||||
<div className="platform-profile-stat-card__icon">
|
||||
{imageSrc ? (
|
||||
<img src={imageSrc} alt="" className="h-full w-full object-contain" />
|
||||
) : (
|
||||
<Icon className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 whitespace-nowrap text-lg font-black leading-none text-[var(--platform-text-strong)]">
|
||||
{value}
|
||||
<div className="min-w-0 text-left">
|
||||
<div className="platform-profile-stat-card__value whitespace-nowrap text-lg font-black leading-none text-[var(--platform-text-strong)]">
|
||||
{value}
|
||||
</div>
|
||||
<div className="platform-profile-stat-card__label mt-1 whitespace-nowrap text-[12px] font-medium text-[var(--platform-text-soft)]">
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -2426,11 +2451,13 @@ function ProfileShortcutButton({
|
||||
subLabel,
|
||||
icon,
|
||||
onClick,
|
||||
imageSrc,
|
||||
}: {
|
||||
label: string;
|
||||
subLabel?: ReactNode;
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
onClick?: (() => void) | null;
|
||||
imageSrc?: string;
|
||||
}) {
|
||||
const Icon = icon;
|
||||
|
||||
@@ -2438,16 +2465,20 @@ function ProfileShortcutButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick ?? undefined}
|
||||
className="platform-subpanel flex min-h-[5.25rem] flex-col items-center justify-center gap-2 rounded-[1.2rem] px-3 py-3 text-center transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
|
||||
className="platform-profile-shortcut-button flex min-h-[5.25rem] flex-col items-center justify-center gap-2 px-2.5 py-3 text-center transition"
|
||||
>
|
||||
<div className="platform-profile-chip flex h-10 w-10 items-center justify-center rounded-full">
|
||||
<Icon className="h-[1.125rem] w-[1.125rem]" />
|
||||
<div className="platform-profile-shortcut-button__icon">
|
||||
{imageSrc ? (
|
||||
<img src={imageSrc} alt="" className="h-full w-full object-contain" />
|
||||
) : (
|
||||
<Icon className="h-[1.125rem] w-[1.125rem]" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
<div className="platform-profile-shortcut-button__label whitespace-nowrap text-[13px] font-semibold text-[var(--platform-text-strong)]">
|
||||
{label}
|
||||
</div>
|
||||
{subLabel ? (
|
||||
<div className="flex min-h-4 items-center justify-center gap-1 text-[11px] font-semibold text-[var(--platform-text-soft)]">
|
||||
<div className="platform-profile-shortcut-button__sub-label flex min-h-4 items-center justify-center gap-1 whitespace-nowrap text-[11px] font-medium text-[var(--platform-text-soft)]">
|
||||
{subLabel}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -2455,6 +2486,72 @@ function ProfileShortcutButton({
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileSettingsRow({
|
||||
label,
|
||||
icon,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const Icon = icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="platform-profile-settings-row flex w-full items-center justify-between gap-3 px-4 py-4 text-left transition"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<span className="platform-profile-settings-row__icon">
|
||||
<Icon className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="truncate text-[15px] font-semibold text-[var(--platform-text-strong)]">
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className="h-4 w-4 shrink-0 text-[var(--platform-text-soft)]" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileSecondaryShortcutButton({
|
||||
label,
|
||||
subLabel,
|
||||
icon,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
subLabel?: string;
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const Icon = icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="platform-profile-secondary-shortcut inline-flex items-center gap-2 rounded-full px-3 py-2 text-left"
|
||||
>
|
||||
<span className="platform-profile-secondary-shortcut__icon">
|
||||
<Icon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[13px] font-semibold text-[var(--platform-text-strong)]">
|
||||
{label}
|
||||
</span>
|
||||
{subLabel ? (
|
||||
<span className="mt-0.5 block truncate text-[11px] font-medium text-[var(--platform-text-soft)]">
|
||||
{subLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileLegalSection({
|
||||
onOpenDocument,
|
||||
}: {
|
||||
@@ -2462,33 +2559,21 @@ function ProfileLegalSection({
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}
|
||||
className="platform-profile-legal-strip"
|
||||
aria-label="法律信息"
|
||||
>
|
||||
<div className="mb-3 text-sm font-black text-[var(--platform-text-strong)]">
|
||||
法律信息
|
||||
</div>
|
||||
<div className="platform-subpanel overflow-hidden rounded-[1.25rem]">
|
||||
<div className="platform-profile-legal-strip__links">
|
||||
{LEGAL_DOCUMENTS.map((document, index) => (
|
||||
<button
|
||||
key={document.id}
|
||||
type="button"
|
||||
onClick={() => onOpenDocument(document.id)}
|
||||
className={`flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition hover:bg-[var(--platform-button-secondary-fill)] ${
|
||||
index > 0
|
||||
? 'border-t border-[var(--platform-subpanel-border)]'
|
||||
: ''
|
||||
}`}
|
||||
className="platform-profile-legal-strip__link"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<span className="platform-profile-chip flex h-8 w-8 shrink-0 items-center justify-center rounded-full">
|
||||
<FileText className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{document.title}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className="h-4 w-4 shrink-0 text-[var(--platform-text-soft)]" />
|
||||
{document.title}
|
||||
{index < LEGAL_DOCUMENTS.length - 1 ? (
|
||||
<span aria-hidden="true" className="platform-profile-legal-strip__divider" />
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -2496,7 +2581,7 @@ function ProfileLegalSection({
|
||||
href={ICP_RECORD_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-3 block text-center text-xs font-semibold text-[var(--platform-text-soft)] transition hover:text-[var(--platform-cool-text)]"
|
||||
className="platform-profile-legal-strip__record"
|
||||
>
|
||||
{ICP_RECORD_NUMBER}
|
||||
</a>
|
||||
@@ -5369,7 +5454,7 @@ export function RpgEntryHomeView({
|
||||
);
|
||||
|
||||
const mobileDiscoverContent: ReactNode = (
|
||||
<div className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage`}>
|
||||
<div className={`${MOBILE_DISCOVER_PAGE_STAGE_CLASS} platform-mobile-home-stage`}>
|
||||
<PublicCodeSearchBar
|
||||
value={mobileSearchKeyword}
|
||||
onChange={updateMobileSearchKeyword}
|
||||
@@ -5584,7 +5669,7 @@ export function RpgEntryHomeView({
|
||||
);
|
||||
|
||||
const desktopDiscoverContent: ReactNode = (
|
||||
<div className={DESKTOP_PAGE_STAGE_CLASS}>
|
||||
<div className={DESKTOP_DISCOVER_PAGE_STAGE_CLASS}>
|
||||
<div className="platform-mobile-home-channelbar flex min-w-0 gap-4 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{visibleDiscoverChannels.map((channel) => {
|
||||
const active = discoverChannel === channel.id;
|
||||
@@ -5839,30 +5924,55 @@ export function RpgEntryHomeView({
|
||||
const savesContent: ReactNode = draftTabContent ?? fallbackDraftContent;
|
||||
|
||||
const profileContent: ReactNode = (
|
||||
<div className={MOBILE_PAGE_STAGE_CLASS}>
|
||||
<div className={`${MOBILE_PAGE_STAGE_CLASS} platform-profile-page`}>
|
||||
{authUi?.user ? (
|
||||
<>
|
||||
<section className="platform-profile-hero rounded-[1.8rem] p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<section className="platform-profile-header">
|
||||
<div className="platform-profile-header__actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
className="platform-profile-header__icon-button"
|
||||
aria-label="打开充值入口"
|
||||
>
|
||||
<ScanLine className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => authUi.openSettingsModal()}
|
||||
className="platform-profile-header__icon-button"
|
||||
aria-label="打开设置"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<img
|
||||
src={profileStillLifeImage}
|
||||
alt=""
|
||||
className="platform-profile-scene-decor"
|
||||
/>
|
||||
<div className="platform-profile-header__identity">
|
||||
<div className="platform-profile-header__identity-row flex min-w-0 items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openAvatarPicker}
|
||||
className="platform-profile-avatar relative h-16 w-16 shrink-0 rounded-[1.4rem]"
|
||||
className="platform-profile-avatar relative h-[5.15rem] w-[5.15rem] shrink-0 rounded-full"
|
||||
aria-label="上传头像"
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
className="h-full w-full rounded-[1.4rem] object-cover"
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-2xl font-black">
|
||||
{avatarLabel}
|
||||
</span>
|
||||
<img
|
||||
src={profileMascotImage}
|
||||
alt=""
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<span className="platform-profile-camera absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full">
|
||||
<span className="platform-profile-camera absolute bottom-0 right-0 flex h-7 w-7 items-center justify-center rounded-full">
|
||||
<Camera className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</button>
|
||||
@@ -5877,28 +5987,27 @@ export function RpgEntryHomeView({
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="platform-profile-header__text min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="truncate text-xl font-black text-[var(--platform-text-strong)]">
|
||||
<div className="platform-profile-header__name truncate text-[20px] font-black leading-tight text-[var(--platform-text-strong)]">
|
||||
{authUi.user.displayName}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openNicknameModal}
|
||||
className="platform-profile-icon-button flex h-7 w-7 items-center justify-center rounded-full"
|
||||
className="platform-profile-edit-button"
|
||||
aria-label="修改昵称"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
<Pencil className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--platform-text-soft)]">
|
||||
<span>陶泥号 {publicUserCode}</span>
|
||||
<div className="platform-profile-header__code mt-3 flex flex-wrap items-center gap-2 text-[13px] text-[var(--platform-text-base)]">
|
||||
<span>陶泥号: {publicUserCode}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyProfilePublicUserCode}
|
||||
className="platform-profile-chip flex items-center gap-1 rounded-full px-2 py-1"
|
||||
className="platform-profile-copy-button"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
{profileCopyState === 'copied'
|
||||
? '已复制'
|
||||
: profileCopyState === 'failed'
|
||||
@@ -5908,32 +6017,33 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
className="platform-profile-action flex shrink-0 items-center gap-2 rounded-[1.1rem] px-3 py-2 text-left"
|
||||
>
|
||||
{showRechargeEntry ? (
|
||||
<Coins className="h-4 w-4" />
|
||||
) : (
|
||||
<Ticket className="h-4 w-4" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xs font-bold">
|
||||
{showRechargeEntry ? '充值' : '兑换码'}
|
||||
</div>
|
||||
<div className="text-[10px] opacity-80">
|
||||
{showRechargeEntry ? '泥点/会员' : '福利奖励'}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 opacity-80" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
className="platform-profile-membership-card"
|
||||
aria-label="查看权益"
|
||||
>
|
||||
<span className="platform-profile-membership-card__badge">
|
||||
<Crown className="platform-profile-membership-card__crown" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="platform-profile-membership-card__title block text-[18px] font-black leading-tight text-white">
|
||||
普通用户
|
||||
</span>
|
||||
<span className="platform-profile-membership-card__subtitle mt-2 block text-[13px] font-medium text-white/92">
|
||||
升级会员,享专属特权与福利
|
||||
</span>
|
||||
</span>
|
||||
<span className="platform-profile-membership-card__action">
|
||||
查看权益
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<section className="platform-profile-stats-panel" aria-label="我的数据">
|
||||
<div className="platform-profile-stats-grid grid grid-cols-3 gap-3">
|
||||
{isLoadingDashboard ? (
|
||||
<>
|
||||
<ProfileStatCardSkeleton />
|
||||
@@ -5944,23 +6054,26 @@ export function RpgEntryHomeView({
|
||||
<>
|
||||
<ProfileStatCard
|
||||
cardKey="wallet"
|
||||
label="泥点"
|
||||
label="泥点余额"
|
||||
value="暂不可用"
|
||||
icon={Coins}
|
||||
imageSrc={profilePointImage}
|
||||
onClick={openWalletLedgerPanel}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playTime"
|
||||
label="游戏时长"
|
||||
label="累计游戏时长"
|
||||
value="暂不可用"
|
||||
icon={Clock3}
|
||||
imageSrc={profileClockImage}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playedWorks"
|
||||
label="玩过"
|
||||
label="已玩游戏数量"
|
||||
value="暂不可用"
|
||||
icon={BookOpen}
|
||||
imageSrc={profileGamepadImage}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
/>
|
||||
</>
|
||||
@@ -5968,23 +6081,26 @@ export function RpgEntryHomeView({
|
||||
<>
|
||||
<ProfileStatCard
|
||||
cardKey="wallet"
|
||||
label="泥点"
|
||||
label="泥点余额"
|
||||
value={formatDashboardCount(remainingNarrativeCoins)}
|
||||
icon={Coins}
|
||||
imageSrc={profilePointImage}
|
||||
onClick={openWalletLedgerPanel}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playTime"
|
||||
label="游戏时长"
|
||||
label="累计游戏时长"
|
||||
value={totalPlayTime}
|
||||
icon={Clock3}
|
||||
imageSrc={profileClockImage}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playedWorks"
|
||||
label="玩过"
|
||||
label="已玩游戏数量"
|
||||
value={`${formatDashboardCount(playedWorkCount)}个`}
|
||||
icon={BookOpen}
|
||||
imageSrc={profileGamepadImage}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
/>
|
||||
</>
|
||||
@@ -5992,101 +6108,125 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTaskCenterPanel}
|
||||
className="platform-profile-daily-task-card"
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="platform-profile-daily-task-card__title block text-[15px] font-black text-[var(--platform-text-strong)]">
|
||||
每日任务
|
||||
</span>
|
||||
<span className="platform-profile-daily-task-card__desc mt-4 block text-[13px] font-medium text-[var(--platform-text-base)]">
|
||||
完成任务可领取 <span className="text-[#c45b2a]">10</span> 泥点
|
||||
</span>
|
||||
<span className="platform-profile-daily-task-card__progress mt-4 flex items-center gap-3">
|
||||
<span className="platform-profile-daily-task-card__progress-value text-[14px] font-semibold text-[#dc3f0e]">
|
||||
0 / 1
|
||||
</span>
|
||||
<span className="platform-profile-daily-task-card__track">
|
||||
<span className="platform-profile-daily-task-card__bar" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<img
|
||||
src={profileMascotImage}
|
||||
alt=""
|
||||
className="platform-profile-daily-task-card__mascot"
|
||||
/>
|
||||
<span className="platform-profile-daily-task-card__action">
|
||||
去完成
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<section
|
||||
className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}
|
||||
className="platform-profile-shortcut-panel"
|
||||
aria-label="常用功能"
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="platform-profile-shortcut-grid">
|
||||
<ProfileShortcutButton
|
||||
label="每日任务"
|
||||
subLabel={
|
||||
<>
|
||||
<span>领10</span>
|
||||
<Coins className="h-3 w-3" />
|
||||
</>
|
||||
}
|
||||
icon={Star}
|
||||
onClick={openTaskCenterPanel}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label={showRechargeEntry ? '充值' : '兑换码'}
|
||||
subLabel={showRechargeEntry ? '泥点/会员' : '福利奖励'}
|
||||
icon={showRechargeEntry ? Coins : Ticket}
|
||||
label="泥点充值"
|
||||
subLabel="充值泥点"
|
||||
icon={Coins}
|
||||
imageSrc={profileCoinsImage}
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="存档"
|
||||
subLabel={
|
||||
saveEntries.length > 0
|
||||
? `${saveEntries.length}个可继续`
|
||||
: '继续游玩'
|
||||
}
|
||||
icon={Archive}
|
||||
onClick={() => setProfilePopupPanel('saveArchives')}
|
||||
/>
|
||||
{showRechargeEntry ? (
|
||||
<ProfileShortcutButton
|
||||
label="兑换码"
|
||||
subLabel="福利奖励"
|
||||
icon={Ticket}
|
||||
onClick={openRewardCodeModal}
|
||||
/>
|
||||
) : null}
|
||||
<ProfileShortcutButton
|
||||
label="邀请好友"
|
||||
subLabel={
|
||||
<>
|
||||
<span>双方得30</span>
|
||||
<Coins className="h-3 w-3" />
|
||||
</>
|
||||
}
|
||||
subLabel="双方得 30 泥点"
|
||||
icon={UserPlus}
|
||||
imageSrc={profileInviteImage}
|
||||
onClick={() => openProfilePopupPanel('invite')}
|
||||
/>
|
||||
{canShowReferralRedeemShortcut ? (
|
||||
<ProfileShortcutButton
|
||||
label="填邀请码"
|
||||
subLabel="新用户奖励"
|
||||
icon={Ticket}
|
||||
onClick={() => openProfilePopupPanel('redeem')}
|
||||
/>
|
||||
) : null}
|
||||
<ProfileShortcutButton
|
||||
label="兑换码"
|
||||
subLabel="领取福利"
|
||||
icon={Ticket}
|
||||
imageSrc={profileGiftImage}
|
||||
onClick={openRewardCodeModal}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="玩家社区"
|
||||
subLabel="每日领福利"
|
||||
subLabel="交流心得 领取福利"
|
||||
icon={MessageCircle}
|
||||
imageSrc={profileCommunityImage}
|
||||
onClick={() => openProfilePopupPanel('community')}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="反馈"
|
||||
subLabel="问题与建议"
|
||||
label="反馈与建议"
|
||||
subLabel="帮助我们做得更好"
|
||||
icon={MessageCircle}
|
||||
imageSrc={profileFeedbackImage}
|
||||
onClick={onOpenFeedback}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
|
||||
<button
|
||||
type="button"
|
||||
<section className="platform-profile-settings-panel" aria-label="设置入口">
|
||||
<ProfileSettingsRow
|
||||
label="主题设置"
|
||||
icon={Palette}
|
||||
onClick={() => authUi.openSettingsModal('appearance')}
|
||||
/>
|
||||
<ProfileSettingsRow
|
||||
label="账号与安全"
|
||||
icon={ShieldCheck}
|
||||
onClick={() => authUi.openSettingsModal('account')}
|
||||
/>
|
||||
<ProfileSettingsRow
|
||||
label="通用设置"
|
||||
icon={Settings}
|
||||
onClick={() => authUi.openSettingsModal()}
|
||||
className="platform-subpanel platform-interactive-card flex w-full items-center justify-between gap-3 rounded-[1.25rem] px-4 py-4 text-left transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="platform-profile-chip flex h-10 w-10 items-center justify-center rounded-full">
|
||||
<Settings className="h-[1.125rem] w-[1.125rem]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
设置
|
||||
</div>
|
||||
<div className="text-xs text-[var(--platform-text-soft)]">
|
||||
主题与账号
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-[var(--platform-text-soft)]" />
|
||||
</button>
|
||||
/>
|
||||
<ProfileSettingsRow
|
||||
label="存档"
|
||||
icon={Archive}
|
||||
onClick={() => setProfilePopupPanel('saveArchives')}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="platform-profile-secondary-shortcuts"
|
||||
aria-label="次级入口"
|
||||
>
|
||||
<ProfileSecondaryShortcutButton
|
||||
label="存档"
|
||||
subLabel={
|
||||
saveEntries.length > 0
|
||||
? `${saveEntries.length}个可继续`
|
||||
: '继续游玩'
|
||||
}
|
||||
icon={Archive}
|
||||
onClick={() => setProfilePopupPanel('saveArchives')}
|
||||
/>
|
||||
{canShowReferralRedeemShortcut ? (
|
||||
<ProfileSecondaryShortcutButton
|
||||
label="填邀请码"
|
||||
subLabel="新用户奖励"
|
||||
icon={Ticket}
|
||||
onClick={() => openProfilePopupPanel('redeem')}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<ProfileLegalSection onOpenDocument={setActiveLegalDocumentId} />
|
||||
@@ -6566,7 +6706,19 @@ export function RpgEntryHomeView({
|
||||
{!isMobileRecommendTab ? (
|
||||
<div className="platform-mobile-topbar mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
|
||||
<RpgEntryBrandLogo />
|
||||
{!isAuthenticated ? (
|
||||
{isAuthenticated && activeTab === 'create' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-mobile-create-wallet-chip inline-flex shrink-0 items-center gap-1.5 rounded-full border border-[#f0cfae] bg-[#fff5eb] px-2.5 py-1.5 text-xs font-black text-[#b65f2c] shadow-[0_10px_22px_rgba(174,111,73,0.12)]"
|
||||
aria-label={`${formatDashboardCount(remainingNarrativeCoins)}泥点`}
|
||||
>
|
||||
<span className="grid h-6 w-6 place-items-center rounded-full bg-[#ffe0ab] text-[#cf7b34]">
|
||||
<Coins className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span>{formatDashboardCount(remainingNarrativeCoins)}泥点</span>
|
||||
</button>
|
||||
) : !isAuthenticated ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
@@ -6714,6 +6866,19 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isAuthenticated && activeTab === 'create' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-desktop-create-wallet-chip platform-desktop-search inline-flex items-center gap-2 px-3 py-2.5 text-xs font-black text-[#b65f2c]"
|
||||
aria-label={`${formatDashboardCount(remainingNarrativeCoins)}泥点`}
|
||||
>
|
||||
<span className="grid h-7 w-7 place-items-center rounded-full bg-[#ffe0ab] text-[#cf7b34]">
|
||||
<Coins className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span>{formatDashboardCount(remainingNarrativeCoins)}泥点</span>
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
|
||||
Reference in New Issue
Block a user