收口个人中心邀请弹层壳层

RpgEntryHomeView 的 ProfileReferralModal 改用 UnifiedModal 并保留社区与邀请码表单语义
更新玩家社区与邀请码弹层测试的具名 dialog 与关闭路径断言
同步 PlatformUiKit 收口计划与 .hermes 决策记录
This commit is contained in:
2026-06-10 17:46:57 +08:00
parent fdc3b5f440
commit 2af0916c04
4 changed files with 220 additions and 185 deletions

View File

@@ -59,6 +59,7 @@
- 2026-06-10 追加RPG 首页发现页分类筛选弹窗和个人中心扫码面板改用 `UnifiedModal` 承接 backdrop、dialog 语义和层级;分类筛选保留本地选项 / 动作布局,扫码面板继续使用 `showHeader={false}` 保留深色自定义头部与摄像头 viewport并显式维持 `closeOnBackdrop={false}``closeOnEscape={false}`。验证命令:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/common/UnifiedModal.test.tsx`
- 2026-06-10 追加RPG 首页个人中心泥点账单改用 `UnifiedModal showHeader={false}` 承接 `dialog` 语义和遮罩层级,同时保留渐变面板、`PlatformModalCloseButton variant="floating"`、余额 badge 与账单列表布局;账单继续显式维持 `closeOnBackdrop={false}``closeOnEscape={false}`,测试改为直接断言具名 dialog 和关闭后卸载。验证命令:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "opens wallet ledger modal from narrative coin card|wallet ledger modal shows empty and error states" src/components/common/UnifiedModal.test.tsx`
- 2026-06-10 追加RPG 首页个人中心“玩过作品”面板改用 `UnifiedModal showHeader={false}` 承接 `dialog` 语义和遮罩层级,同时保留 `PLAYED` kicker、总时长 badge、`PlatformModalCloseButton variant="floating"``可继续 / 玩过` 双分区与作品卡布局;存档入口继续留在同一个“玩过”面板内,不再回退成独立 `SAVE ARCHIVE` / `ARCHIVE` 壳层。验证命令:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "profile played modal summary and work type use platform pill badges|profile played modal empty state uses platform empty state" src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "authenticated users can open save archives from the profile played panel|profile page keeps save archives inside played stats panel" src/components/common/UnifiedModal.test.tsx`
- 2026-06-10 追加RPG 首页个人中心邀请相关弹层里的 live `community / redeem` 分支改用 `UnifiedModal showHeader={false}` 承接 `dialog` 语义和遮罩层级,同时保留 `PlatformModalCloseButton variant="floatingPlain"`、居中标题、社区二维码卡片、邀请码输入 / 已填写空态和成功 / 失败提示;历史 `invite` 分支没有新的入口,当前只随同一壳层维持现状。验证命令:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "profile community shortcut shows reward subtitle and invited users|invite query opens redeem modal directly for logged in users|profile redeem invite query modal submits code after login" src/components/common/UnifiedModal.test.tsx`
- 2026-06-09 追加RPG 大编辑器暗色面板内的保存和角色槽动作继续走本地 `ActionButton`,不再混用白底平台 `platform-button` class平台白底动作收口和编辑器暗色动作收口保持两套视觉边界。
- 2026-06-10 追加:`PlatformActionButton surface="editorDark"` 承接 RPG 暗色弹窗 / 运行面板里的普通取消、确认、刷新和编组动作,支持 `size="xxs"``tone="success" | "warning"``tone="accent"` 承接暗色壳层内的琥珀实心 CTA`tone="accentSoft"` 承接依赖局部 accent 变量的柔和强调按钮。角色自定义 footer、自定义世界生成 footer、地图切换确认、营地编组普通动作和角色聊天刷新动作已迁移。暗色可选项卡仍使用 `PlatformDarkOptionCard`,像素风发送 / 强品牌动作继续保留专用布局。验证命令:`npm run test -- src/components/common/platformActionButtonModel.test.ts src/components/common/PlatformActionButton.test.tsx src/components/SelectionCustomizationModals.test.tsx src/components/CompanionCampModal.test.tsx src/components/MapModal.test.tsx src/components/CharacterChatModal.test.tsx`
- 2026-06-10 追加RPG 首页创作 / 草稿顶栏的钱包快捷入口通过同文件 `TopbarWalletShortcutButton` 复用 `PlatformActionButton tone="accentSoft" shape="pill" size="xs"``PlatformIconBadge`;移动端 / 桌面端继续保留 `.platform-mobile-create-wallet-chip``.platform-desktop-create-wallet-chip``.platform-desktop-search` 兼容 class承接余额截断、桌面顶栏胶囊壳和既有测试锚点点击语义仍统一走 `openRechargeOrRewardCodeModal`。验证命令:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`

View File

@@ -229,6 +229,7 @@
19.3.5. 发现页分类筛选弹窗与个人中心扫码面板迁移到 `UnifiedModal`;分类筛选继续复用本地选项栅格和底部动作区样式,但 backdrop、dialog 语义、头部关闭入口和 `closeOnEscape={false}` 统一收口到共享壳层。扫码面板复用 `showHeader={false}` 模式保留深色自定义头部、摄像头 viewport 和状态提示,同时显式保持 `closeOnBackdrop={false}``closeOnEscape={false}`,确保不会把扫码中的资源清理语义改散到页面外层。
19.3.6. 个人中心泥点账单弹窗迁移到 `UnifiedModal` 的 headerless 模式;共享壳层承接 `dialog` 语义、层级和关闭策略,账单弹窗继续保留自定义渐变面板、浮动关闭按钮、余额 badge、列表 / 空态 / 错误态布局以及 `closeOnBackdrop={false}``closeOnEscape={false}` 的原有交互,不再手写 `fixed inset-0` 遮罩壳层。
19.3.7. 个人中心“玩过作品”面板迁移到 `UnifiedModal` 的 headerless 模式;共享壳层承接 `dialog` 语义、层级与关闭策略,面板继续保留 `PLAYED` kicker、总时长 badge、浮动关闭按钮、`可继续 / 玩过` 双分区、作品卡与空态布局,以及 `closeOnBackdrop={false}``closeOnEscape={false}` 的原有交互。存档入口仍留在同一个“玩过”面板内,不再回退成独立的 `SAVE ARCHIVE` / `ARCHIVE` 壳层。
19.3.8. 个人中心邀请相关弹层中的 live 分支迁移到 `UnifiedModal` 的 headerless 模式;玩家社区与填邀请码继续保留浮动关闭按钮、居中标题、二维码卡片、邀请码表单 / 已填写空态和成功 / 失败提示,但 `dialog` 语义、层级与关闭策略统一由共享壳层承接。`community / redeem` 两条真实入口继续显式保持 `closeOnBackdrop={false}``closeOnEscape={false}`;历史 `invite` 分支暂不扩张能力面,只随同一壳层复用现状内容。
19.3. creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作、首页激励 CTA 和 prompt suggestion 按钮迁移到 `PlatformIconButton` / `PlatformActionButton`;首页继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏、抽屉和品牌化胶囊视觉,不把视觉回收和语义收口绑成一次大改。`Beta` 徽标和历史记录纯文本行暂保留本地实现,等出现更多同构轻量列表行后再评估是否抽新的共享 row primitive。
19.4. 大鱼吃小鱼结果页 hero 的返回入口迁移到 `PlatformIconButton variant="darkMini"`,测试 / 发布动作迁移到 `PlatformActionButton surface="editorDark"`;结果页只保留测试运行、发布提交和文案状态语义,不再手写 hero 顶栏按钮壳。
20. 平台方形上传入口和紧凑虚线新增入口迁移到 `PlatformUploadTile`,上传后的图片预览迁移到 `PlatformUploadPreviewCard`;反馈页上传凭证入口 / 预览、敲木鱼工作台新增功德词条入口、通用创作图片面板的提示词参考图缩略图、抓大鹅封面编辑参考图缩略图、通用输入 Composer 已选参考图条、creation-agent 已选参考图条和拼图结果页关卡引用图横条已先迁移。方形缩略图使用默认 `layout="square"`,横向“已选择参考图 / 文件名 / 素材名 / 移除”条使用 `layout="inline"`;只读引用图条不传 `onRemove`,避免公共组件额外渲染删除入口。后续继续收口结果页素材上传、工作台参考图上传、紧凑虚线新增入口等上传 / 动作块时,业务页只保留文件选择、预览数组、预览回调、删除回调、校验逻辑或新增回调,上传方块外观、主副文案、缩略图壳、预览按钮、标题行、横向已选条、移除按钮和禁用态统一由 Module 承接;工具栏中的小图标上传仍继续使用 `PlatformIconButton asChild="label"`

View File

@@ -3227,7 +3227,14 @@ test('profile community shortcut shows reward subtitle and invited users', async
await user.click(communityButton);
const communityDialog = await screen.findByRole('dialog', {
name: '玩家社区',
});
expect(mockGetRpgProfileReferralInviteCenter).toHaveBeenCalledTimes(1);
expect(
within(communityDialog).getByRole('button', { name: '关闭玩家社区' }),
).toBeTruthy();
expect(screen.getByAltText('玩家社区微信群二维码')).toBeTruthy();
expect(screen.getByAltText('玩家社区 QQ 群二维码')).toBeTruthy();
expect(screen.getByText('微信群')).toBeTruthy();
@@ -3240,6 +3247,13 @@ test('profile community shortcut shows reward subtitle and invited users', async
);
expect(screen.queryByText('成功邀请')).toBeNull();
expect(screen.queryByText('被邀请玩家')).toBeNull();
await user.click(
within(communityDialog).getByRole('button', { name: '关闭玩家社区' }),
);
await waitFor(() => {
expect(screen.queryByRole('dialog', { name: '玩家社区' })).toBeNull();
});
});
test('profile page hides legacy redeem invite secondary shortcut for fresh accounts', async () => {
@@ -3312,6 +3326,7 @@ test('invite query opens redeem modal directly for logged in users', async () =>
renderProfileView();
expect(await screen.findByRole('dialog', { name: '填邀请码' })).toBeTruthy();
const input = await screen.findByLabelText('邀请码');
expect((input as HTMLInputElement).value).toBe('SPRING2026');
expect(input.className).toContain('platform-text-field');
@@ -3334,6 +3349,7 @@ test('profile redeem invite query modal submits code after login', async () => {
renderProfileView(onRechargeSuccess, {});
expect(await screen.findByRole('dialog', { name: '填邀请码' })).toBeTruthy();
expect(await screen.findByLabelText('邀请码')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '提交' }));

View File

@@ -2815,6 +2815,8 @@ const PROFILE_MODAL_OVERLAY_CLASS =
'platform-modal-backdrop !items-center !justify-center !px-4 !py-6';
const PROFILE_SECONDARY_MODAL_OVERLAY_CLASS =
'!items-center !bg-black/48 !px-3 !py-5 !backdrop-blur-none';
const PROFILE_SECONDARY_MODAL_SOFT_OVERLAY_CLASS =
'!items-center !bg-black/42 !px-3 !py-5 !backdrop-blur-none';
const PROFILE_MODAL_HEADER_CLASS = 'border-white/10 px-5 py-4';
const PROFILE_MODAL_TITLE_CLASS = 'text-base font-black';
const PROFILE_MODAL_DESCRIPTION_CLASS =
@@ -3957,201 +3959,216 @@ function ProfileReferralModal({
.trim()
.replace(/[^0-9a-z]/gi, '')
.toUpperCase();
let content: ReactNode;
if (panel === 'community') {
content = (
<div className="mt-5 grid grid-cols-2 gap-3">
{COMMUNITY_QR_CODES.map((qrCode) => (
<PlatformSubpanel
as="div"
key={qrCode.label}
surface="flat"
radius="xs"
padding="xs"
className="text-center"
>
<div className="aspect-square overflow-hidden rounded-lg border border-zinc-200 bg-white p-1.5">
<img
src={qrCode.src}
alt={qrCode.alt}
className="h-full w-full object-contain"
loading="lazy"
decoding="async"
/>
</div>
<div className="mt-2 text-sm font-bold text-zinc-700">
{qrCode.label}
</div>
</PlatformSubpanel>
))}
</div>
);
} else if (panel === 'redeem') {
content = isLoading ? (
<div className="mt-5 space-y-3">
<div className="h-12 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-11 animate-pulse rounded-xl bg-zinc-100" />
</div>
) : center?.hasRedeemedCode ? (
<PlatformEmptyState
surface="subpanel"
size="inline"
tone="base"
className="mt-5"
>
</PlatformEmptyState>
) : (
<form
className="mt-5 space-y-3"
onSubmit={(event) => {
event.preventDefault();
onSubmitRedeemCode();
}}
>
<PlatformTextField
value={redeemCode}
onChange={(event) => onRedeemCodeChange(event.target.value)}
size="lg"
density="roomy"
tone="rose"
className="rounded-xl text-center font-black uppercase tracking-[0.16em]"
placeholder="邀请码"
aria-label="邀请码"
autoComplete="off"
autoFocus
/>
<PlatformActionButton
type="submit"
surface="profile"
fullWidth
size="md"
className="rounded-xl"
disabled={isSubmittingRedeem || !normalizedRedeemCode}
>
{isSubmittingRedeem ? '提交中' : '提交'}
</PlatformActionButton>
</form>
);
} else if (isLoading) {
content = (
<div className="mt-5 space-y-3">
<div className="h-20 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-10 animate-pulse rounded-xl bg-zinc-100" />
</div>
);
} else {
content = (
<div className="mt-5 space-y-3">
<PlatformSubpanel
as="div"
surface="flat"
radius="xs"
padding="md"
className="text-center"
>
<PlatformFieldLabel
variant="section"
className="block text-[11px] text-zinc-500"
>
</PlatformFieldLabel>
<div className="mt-1 text-3xl font-black tracking-[0.16em] text-[#ff4056]">
{center?.inviteCode ?? '--------'}
</div>
</PlatformSubpanel>
<PlatformStatusMessage
tone="warning"
surface="profile"
size="md"
className="space-y-0.5 px-3.5 font-semibold"
>
<div>
{`邀请一个用户注册,双方都可以获得${center?.rewardPoints ?? 30}泥点。`}
</div>
<div></div>
</PlatformStatusMessage>
<CopyFeedbackButton
state={copyInviteState}
onClick={onCopyInvite}
disabled={!center?.inviteCode}
idleLabel="复制邀请"
copiedLabel="已复制"
failedLabel="复制失败"
idleIcon={<Copy className="h-4 w-4" />}
actionSurface="profile"
actionSize="md"
actionFullWidth
className="gap-2 rounded-xl"
/>
<PlatformSubpanel as="div" surface="flat" radius="xs" padding="sm">
<PlatformFieldLabel
variant="section"
className="block text-zinc-900"
>
</PlatformFieldLabel>
{center?.invitedUsers?.length ? (
<div className="mt-3 max-h-44 space-y-2 overflow-y-auto pr-1">
{center.invitedUsers.map((user) => (
<PlatformSubpanel
as="div"
key={`${user.userId}-${user.boundAt}`}
surface="soft"
radius="xs"
padding="row"
className="flex items-center gap-3"
>
<ProfileReferralUserAvatar
name={user.displayName}
avatarUrl={user.avatarUrl}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-bold text-zinc-900">
{user.displayName || '玩家'}
</div>
</div>
</PlatformSubpanel>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="compact"
className="mt-3 text-center text-xs font-semibold leading-normal"
>
</PlatformEmptyState>
)}
</PlatformSubpanel>
</div>
);
}
return (
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/42 px-3 py-5">
<div className="relative w-full max-w-[24rem] overflow-hidden rounded-[1.35rem] bg-white text-zinc-950 shadow-2xl">
<UnifiedModal
open
title={title}
onClose={onClose}
showHeader={false}
showCloseButton={false}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[80]"
overlayClassName={PROFILE_SECONDARY_MODAL_SOFT_OVERLAY_CLASS}
panelClassName="relative !max-w-[24rem] bg-white text-zinc-950 shadow-2xl !rounded-[1.35rem] sm:!rounded-[1.35rem]"
bodyClassName="!p-0"
>
<div className="relative px-5 pb-5 pt-4">
<PlatformModalCloseButton
label={`关闭${title}`}
variant="floatingPlain"
onClick={onClose}
icon="×"
/>
<div className="px-5 pb-5 pt-4">
<div className="text-center text-xl font-black">{title}</div>
<div className="text-center text-xl font-black">{title}</div>
{content}
{panel === 'community' ? (
<div className="mt-5 grid grid-cols-2 gap-3">
{COMMUNITY_QR_CODES.map((qrCode) => (
<PlatformSubpanel
as="div"
key={qrCode.label}
surface="flat"
radius="xs"
padding="xs"
className="text-center"
>
<div className="aspect-square overflow-hidden rounded-lg border border-zinc-200 bg-white p-1.5">
<img
src={qrCode.src}
alt={qrCode.alt}
className="h-full w-full object-contain"
loading="lazy"
decoding="async"
/>
</div>
<div className="mt-2 text-sm font-bold text-zinc-700">
{qrCode.label}
</div>
</PlatformSubpanel>
))}
</div>
) : panel === 'redeem' ? (
isLoading ? (
<div className="mt-5 space-y-3">
<div className="h-12 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-11 animate-pulse rounded-xl bg-zinc-100" />
</div>
) : center?.hasRedeemedCode ? (
<PlatformEmptyState
surface="subpanel"
size="inline"
tone="base"
className="mt-5"
>
</PlatformEmptyState>
) : (
<form
className="mt-5 space-y-3"
onSubmit={(event) => {
event.preventDefault();
onSubmitRedeemCode();
}}
>
<PlatformTextField
value={redeemCode}
onChange={(event) => onRedeemCodeChange(event.target.value)}
size="lg"
density="roomy"
tone="rose"
className="rounded-xl text-center font-black uppercase tracking-[0.16em]"
placeholder="邀请码"
aria-label="邀请码"
autoComplete="off"
autoFocus
/>
<PlatformActionButton
type="submit"
surface="profile"
fullWidth
size="md"
className="rounded-xl"
disabled={isSubmittingRedeem || !normalizedRedeemCode}
>
{isSubmittingRedeem ? '提交中' : '提交'}
</PlatformActionButton>
</form>
)
) : isLoading ? (
<div className="mt-5 space-y-3">
<div className="h-20 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-10 animate-pulse rounded-xl bg-zinc-100" />
</div>
) : (
<div className="mt-5 space-y-3">
<PlatformSubpanel
as="div"
surface="flat"
radius="xs"
padding="md"
className="text-center"
>
<PlatformFieldLabel
variant="section"
className="block text-[11px] text-zinc-500"
>
</PlatformFieldLabel>
<div className="mt-1 text-3xl font-black tracking-[0.16em] text-[#ff4056]">
{center?.inviteCode ?? '--------'}
</div>
</PlatformSubpanel>
<PlatformStatusMessage
tone="warning"
surface="profile"
size="md"
className="space-y-0.5 px-3.5 font-semibold"
>
<div>
{`邀请一个用户注册,双方都可以获得${center?.rewardPoints ?? 30}泥点。`}
</div>
<div></div>
</PlatformStatusMessage>
<CopyFeedbackButton
state={copyInviteState}
onClick={onCopyInvite}
disabled={!center?.inviteCode}
idleLabel="复制邀请"
copiedLabel="已复制"
failedLabel="复制失败"
idleIcon={<Copy className="h-4 w-4" />}
actionSurface="profile"
actionSize="md"
actionFullWidth
className="gap-2 rounded-xl"
/>
<PlatformSubpanel
as="div"
surface="flat"
radius="xs"
padding="sm"
>
<PlatformFieldLabel
variant="section"
className="block text-zinc-900"
>
</PlatformFieldLabel>
{center?.invitedUsers?.length ? (
<div className="mt-3 max-h-44 space-y-2 overflow-y-auto pr-1">
{center.invitedUsers.map((user) => (
<PlatformSubpanel
as="div"
key={`${user.userId}-${user.boundAt}`}
surface="soft"
radius="xs"
padding="row"
className="flex items-center gap-3"
>
<ProfileReferralUserAvatar
name={user.displayName}
avatarUrl={user.avatarUrl}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-bold text-zinc-900">
{user.displayName || '玩家'}
</div>
</div>
</PlatformSubpanel>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="compact"
className="mt-3 text-center text-xs font-semibold leading-normal"
>
</PlatformEmptyState>
)}
</PlatformSubpanel>
</div>
)}
{error ? (
<PlatformStatusMessage tone="error" className="mt-4">
{error}
</PlatformStatusMessage>
) : null}
{success ? (
<PlatformStatusMessage tone="success" className="mt-4">
{success}
</PlatformStatusMessage>
) : null}
</div>
{error ? (
<PlatformStatusMessage tone="error" className="mt-4">
{error}
</PlatformStatusMessage>
) : null}
{success ? (
<PlatformStatusMessage tone="success" className="mt-4">
{success}
</PlatformStatusMessage>
) : null}
</div>
</div>
</UnifiedModal>
);
}