From 4e3378be658ed799bbce3ffed1988767e0140984 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 10 Jun 2026 19:44:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E5=BC=B9=E5=B1=82=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PlatformProfileModalShell 统一个人中心主弹层与副弹层壳层 - 抽离 PlatformProfilePlayedWorksModal 与 PlatformProfileReferralModal 并移除首页内联历史与邀请弹层实现 - 让昵称充值任务兑换码账单等弹层复用共享壳层并补齐测试和文档 --- .hermes/shared-memory/decision-log.md | 3 + ...】PlatformUiKit弹窗组件收口计划-2026-06-08.md | 3 + .../PlatformProfileModalShell.tsx | 146 +++ .../PlatformProfilePlayedWorksModal.test.tsx | 91 ++ .../PlatformProfilePlayedWorksModal.tsx | 260 +++++ .../PlatformProfileReferralModal.test.tsx | 124 +++ .../PlatformProfileReferralModal.tsx | 303 ++++++ .../usePlatformProfileCenterController.ts | 2 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 916 ++---------------- .../rpgEntryProfileDashboardPresentation.ts | 18 + 10 files changed, 1051 insertions(+), 815 deletions(-) create mode 100644 src/components/platform-entry/PlatformProfileModalShell.tsx create mode 100644 src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx create mode 100644 src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx create mode 100644 src/components/platform-entry/PlatformProfileReferralModal.test.tsx create mode 100644 src/components/platform-entry/PlatformProfileReferralModal.tsx diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 99051d28..f6150f07 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -39,6 +39,9 @@ - 2026-06-10 追加:推荐页运行态卡片底部的点赞 / 分享 / 改造入口,以及创作中心公开作品卡右上角分享入口统一迁移到 `PlatformIconButton`;这类和 swipe / drag 手势耦合的图标动作必须继续保留业务局部 class 与 `onPointerDown` / `onClick` 里的 `stopPropagation`,只把按钮语义、可访问名称和默认 `type="button"` 收口到共享组件,避免图标动作误触推荐卡切换、整卡打开或残留左滑状态。 - 2026-06-10 追加:RPG 首页个人中心里的统计卡、统计骨架、常用功能入口、设置行和法律信息入口统一抽到 `src/components/platform-entry/PlatformProfilePrimitives.tsx`;这组纯展示原子以后优先通过 props 接收图片资源、点击回调和展示文案,不再继续塞回 `RpgEntryHomeView` 的账户控制逻辑里。新建 `PlatformProfilePrimitives.test.tsx` 作为组件级护栏,页面级布局与法律入口继续由 `RpgEntryHomeView.recharge.test.tsx` 兜底。 - 2026-06-10 追加:RPG 首页个人中心的充值 / 钱包 / 每日任务 / 邀请 / 兑换码等商业与账户控制逻辑统一收口到 `src/components/platform-entry/usePlatformProfileCenterController.ts`;controller 负责账户动作分流、商业状态派生与相关面板控制,`RpgEntryHomeView` 只保留展示、昵称头像编辑、扫码入口和页面级交互编排,不在页面组件里继续堆叠账户控制分支。验证命令:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`。 +- 2026-06-10 追加:RPG 首页个人中心的“玩过 / 可继续”历史弹层统一抽到 `src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx`;`RpgEntryHomeView` 不再内联 `SaveArchiveCard`、`ProfilePlayedWorksModal` 和未连通的 `ProfileSaveArchivesModal`。当前产品语义已经把存档恢复并入“玩过”弹层的“可继续”分区,因此 controller 里的 `ProfilePopupPanel` 也去掉了没有真实入口的 `saveArchives` 分支。验证命令:`npm run test -- src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`。 +- 2026-06-10 追加:个人中心标准头部弹窗与白底副弹层的共享壳层统一抽到 `src/components/platform-entry/PlatformProfileModalShell.tsx`;标准头部弹窗优先复用 `PlatformProfileModalShell`,白底副弹层优先复用 `PlatformProfileSecondaryModalShell`,不再在业务页重复手写 profile overlay、header、title、description、floating close 和关闭策略。昵称修改、账户充值、每日任务、兑换码、泥点账单、“玩过 / 可继续”以及邀请相关弹层已接入这套壳层。 +- 2026-06-10 追加:RPG 首页个人中心的邀请好友 / 填邀请码 / 玩家社区三态弹层统一抽到 `src/components/platform-entry/PlatformProfileReferralModal.tsx`;首页不再内联邀请码规范化、社区二维码卡片和邀请用户头像行,后续 profile 侧同类二级弹层优先按“独立组件 + `PlatformProfileSecondaryModalShell`”继续收口。 - 2026-06-09 追加:通用输入 Composer 的上传参考图、发送和移除参考图已迁移到 `PlatformIconButton`;图标上传仍使用 `asChild="label"` 保留 label + file input 语义,公共组件会自动写入隐藏文本,确保内嵌 file input 继承可访问名称。 - 2026-06-10 追加:creation-agent composer 的上传文档 / 上传参考图入口使用 `PlatformIconButton` 默认 `platformIcon`;工作台只保留动态 label、title、busy 状态和 picker 回调,发送按钮继续保留主题色动作布局。验证命令:`npm run test -- src/components/creation-agent/CreationAgentWorkspace.test.tsx src/components/common/PlatformIconButton.test.tsx`。 - 2026-06-10 追加:作品详情顶部返回 / 分享和封面轮播上一张 / 下一张入口使用 `PlatformIconButton variant="platformIcon"`;详情页保留原 `platform-work-detail__*` 局部 class 控制位置和尺寸,点赞、复制三态等专用动作暂不迁移。验证命令:`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformIconButton.test.tsx`。 diff --git a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md index f16dc978..1bf6cf28 100644 --- a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md +++ b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md @@ -235,6 +235,9 @@ 19.3.11. 创作中心公开作品卡右上角的分享快按钮迁移到 `PlatformIconButton`;作品卡继续保留 `.creation-work-card__quick-action-button` 局部 class 承接卡片角落定位和尺寸,并显式保留 `stopPropagation`、关闭 swipe action、清理 `suppressOpenRef` 与分享回调顺序,避免右上角分享入口误触整卡打开或遗留左滑状态。验证命令:`npm run test -- src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`。 19.3.12. RPG 首页个人中心的统计卡、统计骨架、常用功能入口、设置行与法律信息入口抽离到 `src/components/platform-entry/PlatformProfilePrimitives.tsx`;`RpgEntryHomeView` 只继续保留账户数据、图片资源、点击回调和打开弹层的控制器,不再把这一组纯展示原子和个人中心页面编排混在同一个 7k+ 首页文件里。组件级验证新增 `src/components/platform-entry/PlatformProfilePrimitives.test.tsx`,并继续复用 `RpgEntryHomeView.recharge.test.tsx` 的个人中心集成断言。验证命令:`npm run test -- src/components/platform-entry/PlatformProfilePrimitives.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile profile page matches the reference layout sections|profile stats cards are centered without update timestamp|profile page shows legal entries and hides archive shortcuts"`。 19.3.13. RPG 首页个人中心的充值 / 钱包 / 每日任务 / 邀请 / 兑换码等商业与账户控制逻辑收口到 `src/components/platform-entry/usePlatformProfileCenterController.ts`;`RpgEntryHomeView` 仅保留个人中心展示、昵称头像编辑、扫码入口和页面级编排 / 交互,不再直接承接账户动作分流、商业状态派生和面板控制。该收口默认保持现有弹层与充值链路语义不变,避免在职责迁移时顺带扩张行为面。验证命令:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`。 +19.3.14. RPG 首页个人中心的“玩过 / 可继续”历史弹层抽到 `src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx`;`RpgEntryHomeView` 不再内联 `SaveArchiveCard`、`ProfilePlayedWorksModal` 和旧的 `ProfileSaveArchivesModal`。当前真实产品语义已经把存档恢复并入“玩过”弹层的“可继续”分区,因此未连通的 `saveArchives` profile popup 分支一并删除,避免继续维护没有入口的独立壳层。组件级验证新增 `src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx`,并继续复用 `RpgEntryHomeView.recharge.test.tsx` 的个人中心集成断言。验证命令:`npm run test -- src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`。 +19.3.15. 个人中心标准头部弹窗与白底副弹层壳层统一抽到 `src/components/platform-entry/PlatformProfileModalShell.tsx`;`PlatformProfileModalShell` 负责标准账户弹窗的 overlay、header、title、description、close variant 和 `closeOnBackdrop={false} / closeOnEscape={false}` 约束,`PlatformProfileSecondaryModalShell` 负责白底副弹层的 overlay、floating close、`bodyClassName="!p-0"` 和内容外壳。`RpgEntryHomeView` 内的昵称修改、账户充值、每日任务、兑换码、泥点账单与“玩过”弹层已接到共享壳层,页面不再重复手写个人中心弹窗的基础 chrome 与关闭策略。 +19.3.16. RPG 首页个人中心的邀请好友 / 填邀请码 / 玩家社区三态弹层抽到 `src/components/platform-entry/PlatformProfileReferralModal.tsx`;组件统一复用 `PlatformProfileSecondaryModalShell` 承接居中白底浮层、floatingPlain 关闭按钮和成功 / 失败提示区,`RpgEntryHomeView` 不再内联邀请码规范化、社区二维码卡片和邀请用户头像行。组件级验证新增 `src/components/platform-entry/PlatformProfileReferralModal.test.tsx`,首页继续复用 `RpgEntryHomeView.recharge.test.tsx` 的邀请链路断言。验证命令:`npm run test -- src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "renders invite panel with shared profile content|submits redeem panel with the shared form shell|renders community QR panels|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"`、`npm run typecheck`。 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"`。 diff --git a/src/components/platform-entry/PlatformProfileModalShell.tsx b/src/components/platform-entry/PlatformProfileModalShell.tsx new file mode 100644 index 00000000..5c8913cd --- /dev/null +++ b/src/components/platform-entry/PlatformProfileModalShell.tsx @@ -0,0 +1,146 @@ +import type { ReactNode } from 'react'; + +import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton'; +import { UnifiedModal } from '../common/UnifiedModal'; + +type PlatformProfileModalShellProps = { + title: string; + description?: ReactNode; + onClose: () => void; + children: ReactNode; + closeLabel?: string; + closeVariant?: 'profile' | 'profileCompact'; + closeDisabled?: boolean; + showHeader?: boolean; + showCloseButton?: boolean; + size?: 'sm' | 'md'; + zIndexClassName?: string; + panelClassName: string; + bodyClassName?: string; + descriptionClassName?: string; +}; + +type PlatformProfileSecondaryModalShellProps = { + title: string; + onClose: () => void; + children: ReactNode; + closeLabel?: string; + closeVariant?: 'floating' | 'floatingPlain'; + closeIcon?: ReactNode; + closeButtonClassName?: string; + overlayTone?: 'default' | 'soft'; + size?: 'sm' | 'md'; + zIndexClassName?: string; + panelClassName: string; + contentClassName: string; +}; + +const PROFILE_MODAL_OVERLAY_CLASS = + 'platform-modal-backdrop !items-center !justify-center !px-4 !py-6'; +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 = + 'mt-1 text-xs font-semibold text-[var(--platform-text-soft)]'; + +const PROFILE_SECONDARY_MODAL_OVERLAY_CLASS_BY_TONE = { + default: '!items-center !bg-black/48 !px-3 !py-5 !backdrop-blur-none', + soft: '!items-center !bg-black/42 !px-3 !py-5 !backdrop-blur-none', +} as const; + +/** + * 个人中心标准弹窗壳层。 + * 统一收口账户侧弹窗常用的 overlay、header 和 close button 配置。 + */ +export function PlatformProfileModalShell({ + title, + description, + onClose, + children, + closeLabel, + closeVariant = 'profile', + closeDisabled = false, + showHeader = true, + showCloseButton = true, + size = 'sm', + zIndexClassName = 'z-[80]', + panelClassName, + bodyClassName = 'px-5 py-5', + descriptionClassName = PROFILE_MODAL_DESCRIPTION_CLASS, +}: PlatformProfileModalShellProps) { + return ( + + {children} + + ); +} + +/** + * 个人中心副弹层壳层。 + * 用于“玩过 / 账单 / 邀请”这类白底浮层,统一收口 overlay、floating close 和 body 外壳。 + */ +export function PlatformProfileSecondaryModalShell({ + title, + onClose, + children, + closeLabel, + closeVariant = 'floating', + closeIcon = '×', + closeButtonClassName, + overlayTone = 'default', + size = 'sm', + zIndexClassName = 'z-[80]', + panelClassName, + contentClassName, +}: PlatformProfileSecondaryModalShellProps) { + return ( + +
+ + {children} +
+
+ ); +} diff --git a/src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx b/src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx new file mode 100644 index 00000000..53706065 --- /dev/null +++ b/src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx @@ -0,0 +1,91 @@ +/* @vitest-environment jsdom */ + +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, test, vi } from 'vitest'; + +import { PlatformProfilePlayedWorksModal } from './PlatformProfilePlayedWorksModal'; + +describe('PlatformProfilePlayedWorksModal', () => { + test('renders save archives and played works in one modal', async () => { + const user = userEvent.setup(); + const onResumeSave = vi.fn(); + const onOpenWork = vi.fn(); + const saveEntry = { + worldKey: 'custom:save-1', + ownerUserId: 'user-1', + profileId: 'save-1', + worldType: 'custom', + worldName: '回声群岛', + subtitle: '雾海码头', + summaryText: '继续推进上一次保存的故事。', + coverImageSrc: null, + lastPlayedAt: '2026-04-19T12:00:00.000Z', + }; + const playedWork = { + worldKey: 'custom:world-1', + ownerUserId: 'user-1', + profileId: 'world-1', + worldType: 'CUSTOM', + worldTitle: '潮雾列岛', + worldSubtitle: '旧灯塔与失控航路', + firstPlayedAt: '2026-04-18T12:00:00.000Z', + lastPlayedAt: '2026-04-19T12:00:00.000Z', + lastObservedPlayTimeMs: 30 * 60 * 1000, + }; + + render( + , + ); + + const dialog = screen.getByRole('dialog', { name: '玩过' }); + + expect(within(dialog).getByText('可继续')).toBeTruthy(); + expect(within(dialog).getAllByText('玩过').length).toBeGreaterThan(0); + expect(within(dialog).getByText('1.5小时')).toBeTruthy(); + + await user.click(within(dialog).getByRole('button', { name: /回声群岛/u })); + expect(onResumeSave).toHaveBeenCalledWith(saveEntry); + + await user.click(within(dialog).getByRole('button', { name: /潮雾列岛/u })); + expect(onOpenWork).toHaveBeenCalledWith(playedWork); + }); + + test('renders platform empty state when no history exists', () => { + render( + , + ); + + const emptyState = screen.getByText('暂无玩过'); + + expect(emptyState.className).toContain('platform-empty-state'); + expect(emptyState.className).toContain('text-left'); + }); +}); diff --git a/src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx b/src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx new file mode 100644 index 00000000..ebc125d7 --- /dev/null +++ b/src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx @@ -0,0 +1,260 @@ +import { ArrowRight, Clock3 } from 'lucide-react'; + +import type { + ProfilePlayedWorkSummary, + ProfilePlayStatsResponse, + ProfileSaveArchiveSummary, +} from '../../../packages/shared/src/contracts/runtime'; +import { PlatformEmptyState } from '../common/PlatformEmptyState'; +import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; +import { PlatformPillBadge } from '../common/PlatformPillBadge'; +import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; +import { PlatformSubpanel } from '../common/PlatformSubpanel'; +import { ResolvedAssetImage } from '../ResolvedAssetImage'; +import { PlatformProfileSecondaryModalShell } from './PlatformProfileModalShell'; +import { + formatCompactPlayTime, + formatPlayedWorkId, + formatPlayedWorkType, + formatSnapshotTime, + formatTotalPlayTimeHours, +} from '../rpg-entry/rpgEntryProfileDashboardPresentation'; +import { formatPlatformWorkDisplayName } from '../rpg-entry/rpgEntryWorldPresentation'; + +type PlatformProfilePlayedWorksModalProps = { + stats: ProfilePlayStatsResponse | null; + isLoading: boolean; + error: string | null; + saveEntries: ProfileSaveArchiveSummary[]; + saveError: string | null; + isResumingSaveWorldKey: string | null; + onClose: () => void; + onOpenWork?: (work: ProfilePlayedWorkSummary) => void; + onResumeSave: (entry: ProfileSaveArchiveSummary) => void; +}; + +function SaveArchivePreview({ + entry, + className, +}: { + entry: ProfileSaveArchiveSummary; + className: string; +}) { + return ( +