This commit is contained in:
2026-05-13 00:28:07 +08:00
parent ef4f91a75e
commit 01c5ab985a
101 changed files with 10635 additions and 2292 deletions
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,10 @@ import type {
ProfileTaskCenterResponse,
} from '../../../packages/shared/src/contracts/runtime';
import { AuthUiContext } from '../auth/AuthUiContext';
import {
ICP_RECORD_NUMBER,
ICP_RECORD_URL,
} from '../common/legalDocuments';
import {
RpgEntryHomeView,
type RpgEntryHomeViewProps,
@@ -1089,11 +1093,48 @@ test('opens reward code modal from profile action on mobile', async () => {
expect(screen.getByLabelText('关闭兑换码')).toBeTruthy();
});
test('shows a reachable login entry in logged out mobile shell', async () => {
test('profile page shows legal entries and ICP record link', async () => {
const user = userEvent.setup();
renderProfileView();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
expect(
shortcutRegion.querySelector('.grid')?.className.includes('grid-cols-3'),
).toBe(true);
expect(within(shortcutRegion).getByRole('button', { name: /每日任务/u }))
.toBeTruthy();
expect(within(shortcutRegion).getByRole('button', { name: /邀请好友/u }))
.toBeTruthy();
expect(within(shortcutRegion).getByRole('button', { name: /玩家社区/u }))
.toBeTruthy();
expect(within(shortcutRegion).getByRole('button', { name: /反馈/u }))
.toBeTruthy();
const legalRegion = screen.getByRole('region', { name: '法律信息' });
expect(within(legalRegion).getByRole('button', { name: /用户协议/u }))
.toBeTruthy();
expect(within(legalRegion).getByRole('button', { name: /隐私政策/u }))
.toBeTruthy();
expect(within(legalRegion).getByRole('button', { name: /免责声明/u }))
.toBeTruthy();
const recordLink = within(legalRegion).getByRole('link', {
name: ICP_RECORD_NUMBER,
});
expect(recordLink.getAttribute('href')).toBe(ICP_RECORD_URL);
expect(recordLink.getAttribute('target')).toBe('_blank');
expect(recordLink.getAttribute('rel')).toBe('noreferrer');
await user.click(within(legalRegion).getByRole('button', { name: /隐私政策/u }));
expect(await screen.findByRole('dialog', { name: '隐私政策' })).toBeTruthy();
});
test('shows a reachable login entry outside mobile recommend tab', async () => {
const user = userEvent.setup();
const openLoginModal = vi.fn();
renderLoggedOutHomeView(openLoginModal);
renderLoggedOutHomeView(openLoginModal, {}, 'category');
await user.click(screen.getByRole('button', { name: '登录' }));
expect(openLoginModal).toHaveBeenCalledTimes(1);
@@ -1360,6 +1401,10 @@ test('logged out mobile shell defaults to discover tab', () => {
expect(
screen.getByPlaceholderText('搜索作品号、名称、作者、描述'),
).toBeTruthy();
expect(container.querySelector('.platform-mobile-topbar')).toBeTruthy();
expect(
container.querySelector('.platform-mobile-entry-shell--recommend'),
).toBeNull();
});
test('logged out recommend tab opens login modal and shows cover only', async () => {
@@ -1381,6 +1426,10 @@ test('logged out recommend tab opens login modal and shows cover only', async ()
expect(
container.querySelector('.platform-recommend-cover-only'),
).toBeTruthy();
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
expect(
container.querySelector('.platform-mobile-entry-shell--recommend'),
).toBeTruthy();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
@@ -1647,6 +1696,7 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
});
const meta = screen.getByLabelText('当前拼图 作品信息') as HTMLElement;
expect(meta.closest('[data-recommend-swipe-zone="true"]')).toBeTruthy();
const activeRecommendCard = within(meta);
const likeButton = activeRecommendCard.getByRole('button', {
name: '点赞 12',
+101 -35
View File
@@ -1,6 +1,5 @@
import {
ArrowRight,
Bell,
BookOpen,
Camera,
ChevronDown,
@@ -9,6 +8,7 @@ import {
Coins,
Compass,
Copy,
FileText,
Gamepad2,
GitFork,
Heart,
@@ -75,6 +75,14 @@ import {
} from '../../services/rpg-entry/rpgProfileClient';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { LegalDocumentModal } from '../common/LegalDocumentModal';
import {
getLegalDocument,
ICP_RECORD_NUMBER,
ICP_RECORD_URL,
LEGAL_DOCUMENTS,
type LegalDocumentId,
} from '../common/legalDocuments';
import {
canExposePublicWork,
EDUTAINMENT_WORK_TAG,
@@ -825,7 +833,10 @@ function RecommendSwipeCard({
data-active={isActive ? 'true' : 'false'}
>
<div className="platform-recommend-swipe-card__visual">{visual}</div>
<div className="platform-recommend-swipe-card__meta">
<div
className="platform-recommend-swipe-card__meta"
data-recommend-swipe-zone={isActive ? 'true' : 'false'}
>
<RecommendRuntimeMeta
entry={entry}
authorAvatarUrl={authorAvatarUrl}
@@ -2103,6 +2114,53 @@ function ProfileShortcutButton({
);
}
function ProfileLegalSection({
onOpenDocument,
}: {
onOpenDocument: (documentId: LegalDocumentId) => void;
}) {
return (
<section
className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}
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]">
{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)]' : ''
}`}
>
<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)]" />
</button>
))}
</div>
<a
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)]"
>
{ICP_RECORD_NUMBER}
</a>
</section>
);
}
function ProfileReferralUserAvatar({
name,
avatarUrl,
@@ -3176,6 +3234,8 @@ export function RpgEntryHomeView({
const [profileCopyState, setProfileCopyState] = useState<
'idle' | 'copied' | 'failed'
>('idle');
const [activeLegalDocumentId, setActiveLegalDocumentId] =
useState<LegalDocumentId | null>(null);
const profileCopyResetTimerRef = useRef<number | null>(null);
const avatarFileInputRef = useRef<HTMLInputElement | null>(null);
const [isNicknameModalOpen, setIsNicknameModalOpen] = useState(false);
@@ -3296,6 +3356,9 @@ export function RpgEntryHomeView({
const publicUserCode = buildPublicUserCode(authUi?.user);
const avatarLabel = getUserAvatarLabel(authUi?.user);
const avatarUrl = authUi?.user?.avatarUrl?.trim() || null;
const activeLegalDocument = activeLegalDocumentId
? getLegalDocument(activeLegalDocumentId)
: null;
const avatarCropSize = avatarImageSize
? Math.min(avatarImageSize.width, avatarImageSize.height) / avatarScale
: 0;
@@ -4931,7 +4994,7 @@ export function RpgEntryHomeView({
className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}
aria-label="常用功能"
>
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-3 gap-3">
<ProfileShortcutButton
label="每日任务"
subLabel={
@@ -4999,6 +5062,8 @@ export function RpgEntryHomeView({
<ChevronRight className="h-4 w-4 text-[var(--platform-text-soft)]" />
</button>
</section>
<ProfileLegalSection onOpenDocument={setActiveLegalDocumentId} />
</>
) : (
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
@@ -5385,36 +5450,33 @@ export function RpgEntryHomeView({
) : null;
if (!isDesktopLayout) {
const isMobileRecommendTab = activeTab === 'home';
return (
<div className="platform-mobile-entry-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
<div className="platform-mobile-topbar mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
<RpgEntryBrandLogo />
{!isAuthenticated ? (
<button
type="button"
onClick={openUserSurface}
className="platform-button platform-button--primary shrink-0 px-3 py-2 text-xs"
>
<LogIn className="h-3.5 w-3.5" />
登录
</button>
) : (
<button
type="button"
onClick={openUserSurface}
className="platform-icon-button platform-mobile-topbar__action shrink-0"
aria-label="通知与账户"
>
<Bell className="h-4 w-4" />
</button>
)}
</div>
<div
className={`platform-mobile-entry-shell ${isMobileRecommendTab ? 'platform-mobile-entry-shell--recommend' : ''} flex h-full min-h-0 min-w-0 flex-col overflow-hidden`}
>
{!isMobileRecommendTab ? (
<div className="platform-mobile-topbar mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
<RpgEntryBrandLogo />
{!isAuthenticated ? (
<button
type="button"
onClick={openUserSurface}
className="platform-button platform-button--primary shrink-0 px-3 py-2 text-xs"
>
<LogIn className="h-3.5 w-3.5" />
登录
</button>
) : null}
</div>
) : null}
<div className="platform-tab-panel-stack min-w-0 flex-1">
{tabPanels}
</div>
<div className="platform-mobile-bottom-dock mt-3 min-w-0 shrink-0">
<div className="platform-mobile-bottom-dock min-w-0 shrink-0">
<div
className={`platform-bottom-nav grid ${visibleTabs.length === 5 ? 'grid-cols-5' : visibleTabs.length === 4 ? 'grid-cols-4' : visibleTabs.length === 3 ? 'grid-cols-3' : 'grid-cols-2'}`}
>
@@ -5504,6 +5566,12 @@ export function RpgEntryHomeView({
onRetry={loadWalletLedger}
/>
) : null}
<LegalDocumentModal
document={activeLegalDocument}
open={Boolean(activeLegalDocument)}
platformTheme={authUi?.platformTheme}
onClose={() => setActiveLegalDocumentId(null)}
/>
{profileEditModals}
</div>
);
@@ -5528,14 +5596,6 @@ export function RpgEntryHomeView({
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={openUserSurface}
className="platform-icon-button"
aria-label="通知与账户"
>
<Bell className="h-4 w-4" />
</button>
<button
type="button"
onClick={openUserSurface}
@@ -5651,6 +5711,12 @@ export function RpgEntryHomeView({
onRetry={loadWalletLedger}
/>
) : null}
<LegalDocumentModal
document={activeLegalDocument}
open={Boolean(activeLegalDocument)}
platformTheme={authUi?.platformTheme}
onClose={() => setActiveLegalDocumentId(null)}
/>
{profileEditModals}
</div>
);
@@ -108,6 +108,9 @@ export type PlatformMatch3DGalleryCard = {
visibility: 'published';
publishedAt: string | null;
updatedAt: string;
backgroundPrompt?: string | null;
backgroundImageSrc?: string | null;
backgroundImageObjectKey?: string | null;
generatedItemAssets?: Match3DGeneratedItemAsset[];
};
@@ -255,6 +258,9 @@ export function mapMatch3DWorkToPlatformGalleryCard(
visibility: 'published',
publishedAt: work.publishedAt ?? null,
updatedAt: work.updatedAt,
backgroundPrompt: work.backgroundPrompt ?? null,
backgroundImageSrc: work.backgroundImageSrc ?? null,
backgroundImageObjectKey: work.backgroundImageObjectKey ?? null,
generatedItemAssets: work.generatedItemAssets ?? [],
};
}