diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 2dc948c2..69cd9882 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -64,6 +64,8 @@ - 2026-06-11 追加:`PlatformFilterToolbar.tsx` 作为薄结构组件收口 RPG 首页分类工具条;组件只承接“筛选按钮 + tabs + 排序按钮”的排布与 `mobile / desktop` 两种布局差异,不持有筛选状态、空态或排序逻辑。后续只有在同构壳层真的复现时才继续往 `common` 扩覆盖面;如果只是单页内局部重复、接口会越抽越胖,就优先退回文件内 helper。 - 2026-06-11 追加:`SquareImageCropModal.tsx` 的白底弹窗壳层改为复用 `UnifiedModal.tsx`,同时给 `UnifiedModal` 薄补 `titleId` 与 `closeIcon` 透传,让裁剪弹窗继续保留自定义 close icon、无 backdrop / Escape 关闭和两列 footer,而不把 `PlatformProfileModalShell` 这类带页面语义的壳层倒灌回 `common/`。这条规则适用于 `common` 级工具弹窗:先看 `UnifiedModal` 能不能承接,再决定是否需要新的薄壳。 - 2026-06-11 追加:`CreativeImageInputPanel.tsx` 里参考图预览、主图预览和移除图片确认都继续并回 `UnifiedModal` 体系:两个预览弹窗直接复用 `UnifiedModal`,删除确认直接复用 `UnifiedConfirmDialog`,不再在图片面板里手写三段 `platform-modal-backdrop + platform-modal-shell`。当前没有新增 `PlatformImagePreviewModal`,因为这批差异还只在尺寸与文案层,继续组合已有 modal 原语的 leverage 更高。 +- 2026-06-11 追加:`src/components/common/PlatformUtilityInfoModal.tsx` 作为 `UnifiedModal` 之上的薄壳,统一承接 `PlatformReportDialog.tsx` 与 `PublishShareModal.tsx` 共同的工具信息弹窗骨架:平台主题 overlay、白底 panel,以及 body / footer 间距与标准 footer frame。该壳层不继续向上吸收报告字段列表、分享正文、复制逻辑、渠道按钮或品牌 icon;后续 `common` 级工具信息弹窗若只是重复这套白底信息壳,优先复用 `PlatformUtilityInfoModal`,业务正文和 footer 交互继续留在调用方。验证命令:`npx vitest run src/components/common/PlatformUtilityInfoModal.test.tsx src/components/common/PlatformReportDialog.test.tsx src/components/common/PublishShareModal.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +- 2026-06-11 追加:profile 白底副弹层里的摘要头、列表骨架和内容行继续沉到 `PlatformProfileSummaryHeader.tsx`、`PlatformProfileSkeletonList.tsx` 与 `PlatformProfileContentRow.tsx`;这组组件只承接 `kicker + title + badge` 摘要层次、重复 skeleton 行以及 `PlatformSubpanel` 上的 `div / button` 内容行语义,不持有账单金额、任务进度、邀请用户信息、充值商品结构或状态切换逻辑。后续 profile modal 若只是重复这三类白底内容骨架,优先复用这组薄组件,不再把 skeleton、摘要头和 row chrome 写回各自 modal。验证命令:`npx vitest run src/components/common/PlatformProfileModalContent.shared.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 - 2026-06-11 追加:`PlatformProfileModalShell` 继续补齐标准 footer 插槽,直接透传 `UnifiedModal.footer` 与 `footerClassName`;`RpgEntryHomeView.tsx` 的昵称修改弹窗已改成标准 profile footer,不再把双按钮动作区手写在 body 末尾。后续个人中心里同类“表单内容 + 底部双按钮”弹窗优先走壳层 footer 接法。 - 2026-06-11 追加:`PlatformProfileModalShell` 的标准 footer 接法继续扩展到单 CTA 表单收尾;`PlatformProfileRewardCodeRedeemModal.tsx` 的兑换按钮已迁到壳层 footer,body 只保留输入和反馈消息。`PlatformAsyncStatePanel` 同日继续扩展到 `PlatformAssetPickerGrid`、`VisualNovelSavePanel.tsx` 与 `AccountModal.tsx` 的账号安全三个子区块;其中公共素材网格继续把 `error` banner 放在状态壳外层,保持错误提示可与加载态或内容并存的原语义。 - 2026-06-11 追加:按钮层继续补齐轻量漏网项。`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留透明背景和原 chip 高度;`RpgEntryCharacterSelectView.tsx` 的两处“返回”按钮统一沉到局部 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`。同日 `GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton`,`CustomWorldGenerationView.tsx` 与 `BarkBattleGeneratingView.tsx` 已开始复用这套暖色生成页返回入口骨架;后续同类轻量返回按钮与 chip 删除按钮优先继续沿共享按钮 + 薄包装的方向推进。 diff --git a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md index da5dd7a5..90c397bd 100644 --- a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md +++ b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md @@ -274,6 +274,8 @@ 19.3.48. `RpgEntryHomeView.tsx` 里的分类筛选工具条继续从页面内重复 JSX 收口到 `src/components/common/PlatformFilterToolbar.tsx`;该 Module 只承接“筛选按钮 + 横向 tabs + 排序按钮”的结构排布,暴露 `mobile / desktop` 两种 layout 以覆盖移动端 divider + 独立排序行和桌面端同排布局差异,但不持有分类列表、筛选状态、空态或排序逻辑。当前 RPG 首页分类区已接入,后续若其它白底列表页也出现同构的筛选壳层,可直接复用这套薄结构组件;若场景只是在单页内局部重复、接口会为了兼容业务差异不断膨胀,则优先退回文件内 helper,不把 `common` 扩成假的“万能筛选条”。验证命令:`npx vitest run src/components/common/PlatformFilterToolbar.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 19.3.49. `SquareImageCropModal.tsx` 的白底 modal 壳层与 footer 已收口到 `src/components/common/UnifiedModal.tsx`;`UnifiedModal` 为此只薄补了 `titleId` 与 `closeIcon` 透传,继续由调用方决定 `closeOnBackdrop`、`closeOnEscape`、`portal`、header/footer 样式和按钮内容,不额外掺入 profile 业务语义,也不让 `common/` 反向依赖 `platform-entry/`。`SquareImageCropModal.tsx` 继续保留裁剪拖拽、pointer capture、保存禁用态与两列等宽 footer 行为,只把 header / body / footer 外壳交给共享 modal 承接。后续 `common` 级白底工具弹窗若只是标准标题栏 + 内容区 + footer 按钮排布,优先先看 `UnifiedModal` 是否够用,再决定是否需要新的薄壳;不要为了一个弹窗把 `PlatformProfileModalShell` 之类带页面语义的壳层倒灌回 `common`。验证命令:`npx vitest run src/components/common/SquareImageCropModal.test.tsx src/components/common/UnifiedModal.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 19.3.50. `CreativeImageInputPanel.tsx` 里内嵌的 white tool modal 继续并回 `UnifiedModal` 体系:参考图预览与主图预览都改成直接复用 `src/components/common/UnifiedModal.tsx`,继续保留各自 `max-w` / `max-h` 节奏、点击遮罩关闭与紧凑 header;移除图片确认改成复用 `src/components/common/UnifiedConfirmDialog.tsx`,不再在 panel 内手写 `platform-modal-backdrop + platform-modal-shell + 两列按钮`。这次没有新增 `PlatformImagePreviewModal`,因为当前预览弹窗差异还只在尺寸和文案层,继续直接组合 `UnifiedModal` 更深、更稳。后续 `common` 级图片面板若出现同类“预览大图 + 单标题栏 + 关闭按钮”弹窗,优先先复用 `UnifiedModal` 并把尺寸/文案留在调用方;只有当至少两到三个调用点开始重复同一套 preview body/header adapter 时,再考虑补新的薄壳。验证命令:`npx vitest run src/components/common/CreativeImageInputPanel.test.tsx src/components/common/UnifiedModal.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +19.3.51. `PlatformReportDialog.tsx` 与 `PublishShareModal.tsx` 共同的工具信息弹窗壳层继续收口到 `src/components/common/PlatformUtilityInfoModal.tsx`;该 Module 只承接平台主题 overlay、白底 panel,以及 body / footer 的基础间距与标准 footer frame,底层继续委托 `UnifiedModal.tsx`,不吸收报告字段列表、分享正文、复制逻辑、渠道按钮或品牌图标这些业务内容。`PlatformReportDialog.tsx` 继续保留 `PlatformInfoBlock` 字段列表与 joined report copy 行为,`PublishShareModal.tsx` 继续保留分享文案、主复制动作和渠道按钮网格;后续 `common` 级白底工具信息弹窗若只是重复这套“共享 modal 外壳 + 业务正文 / footer 内容”的骨架,优先复用 `PlatformUtilityInfoModal`,只有当正文编排或 footer 交互明显偏离时才回退到直接组合 `UnifiedModal`。验证命令:`npx vitest run src/components/common/PlatformUtilityInfoModal.test.tsx src/components/common/PlatformReportDialog.test.tsx src/components/common/PublishShareModal.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +19.3.52. profile 白底 modal 里的摘要头、列表骨架和内容行继续沉到 `src/components/common/PlatformProfileSummaryHeader.tsx`、`src/components/common/PlatformProfileSkeletonList.tsx` 与 `src/components/common/PlatformProfileContentRow.tsx`;这三个 Module 只承接 `kicker + title + badge` 的摘要层次、重复 skeleton 列表行,以及 `PlatformSubpanel` 上的 `div / button` 内容行语义,不持有账单金额、任务进度、邀请用户信息、充值商品结构或 modal 状态切换逻辑。`PlatformProfileWalletLedgerModal.tsx`、`PlatformProfileTaskCenterModal.tsx`、`PlatformProfilePlayedWorksModal.tsx`、`PlatformProfileReferralModal.tsx` 与 `PlatformProfileRechargeModal.tsx` 已接入;后续 profile 副弹层若只是重复这三类白底内容骨架,优先继续复用这组薄组件,不再把 skeleton、摘要头和 row chrome 写回各自 modal。验证命令:`npx vitest run src/components/common/PlatformProfileModalContent.shared.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 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 顶栏按钮壳。 19.4.1. 大鱼吃小鱼结果页的发布失败弹层迁移到 `src/components/common/PlatformStatusDialog.tsx`;`PlatformStatusDialog` 补充自定义图标、可访问标签和动作按钮样式透传后,`BigFishResultView` 不再保留 `BigFishResultErrorModal` 内联的 `UnifiedConfirmDialog + PlatformIconBadge` 组合。结果页只保留失败文案和关闭回调,发布失败的状态图标、遮罩、白底面板和“知道了”主动作统一由共享状态弹层承接。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx`、`npm run typecheck`。 diff --git a/src/components/common/PlatformProfileContentRow.tsx b/src/components/common/PlatformProfileContentRow.tsx new file mode 100644 index 00000000..aef03955 --- /dev/null +++ b/src/components/common/PlatformProfileContentRow.tsx @@ -0,0 +1,63 @@ +import type { ReactNode } from 'react'; + +import { PlatformSubpanel } from './PlatformSubpanel'; + +type PlatformProfileContentRowProps = { + as?: 'div' | 'button'; + children: ReactNode; + className?: string; + surface?: 'platform' | 'flat' | 'soft'; + radius?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + padding?: 'none' | 'row' | 'xs' | 'sm' | 'md' | 'lg' | 'tight'; + interactive?: boolean; + disabled?: boolean; + type?: 'button' | 'submit' | 'reset'; + onClick?: () => void; +}; + +/** + * 个人中心白底 modal 内容行骨架。 + * 只承接常见的 row 外壳与可点击语义,具体字段布局仍留在业务 modal 内部。 + */ +export function PlatformProfileContentRow({ + as = 'div', + children, + className, + surface = 'flat', + radius = 'xs', + padding = 'md', + interactive = as === 'button', + disabled, + type = 'button', + onClick, +}: PlatformProfileContentRowProps) { + if (as === 'button') { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/src/components/common/PlatformProfileModalContent.shared.test.tsx b/src/components/common/PlatformProfileModalContent.shared.test.tsx new file mode 100644 index 00000000..d3dd4eec --- /dev/null +++ b/src/components/common/PlatformProfileModalContent.shared.test.tsx @@ -0,0 +1,51 @@ +/* @vitest-environment jsdom */ + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect, test, vi } from 'vitest'; + +import { PlatformProfileContentRow } from './PlatformProfileContentRow'; +import { PlatformProfileSkeletonList } from './PlatformProfileSkeletonList'; +import { PlatformProfileSummaryHeader } from './PlatformProfileSummaryHeader'; + +test('platform profile summary header renders kicker, title and badge slot', () => { + render( + 1.5小时} + />, + ); + + expect(screen.getByText('PLAYED')).toBeTruthy(); + expect(screen.getByText('玩过')).toBeTruthy(); + expect(screen.getByText('1.5小时')).toBeTruthy(); +}); + +test('platform profile skeleton list renders requested skeleton rows', () => { + const { container } = render( + , + ); + + expect(container.querySelectorAll('.animate-pulse')).toHaveLength(3); + expect(container.querySelector('.space-y-3')).toBeTruthy(); + expect(container.querySelector('.h-16')).toBeTruthy(); +}); + +test('platform profile content row preserves interactive button semantics', async () => { + const user = userEvent.setup(); + const onClick = vi.fn(); + + render( + + 点击行 + , + ); + + await user.click(screen.getByRole('button', { name: '点击行' })); + expect(onClick).toHaveBeenCalledTimes(1); +}); diff --git a/src/components/common/PlatformProfileSkeletonList.tsx b/src/components/common/PlatformProfileSkeletonList.tsx new file mode 100644 index 00000000..15fc56d8 --- /dev/null +++ b/src/components/common/PlatformProfileSkeletonList.tsx @@ -0,0 +1,31 @@ +type PlatformProfileSkeletonListProps = { + count: number; + containerClassName?: string; + itemClassName?: string; +}; + +/** + * 个人中心白底 modal 列表读取骨架。 + * 只负责重复 skeleton 行与容器节奏,行高和栅格继续由调用方微调。 + */ +export function PlatformProfileSkeletonList({ + count, + containerClassName, + itemClassName, +}: PlatformProfileSkeletonListProps) { + return ( +
+ {Array.from({ length: count }).map((_, index) => ( +
+ ))} +
+ ); +} diff --git a/src/components/common/PlatformProfileSummaryHeader.tsx b/src/components/common/PlatformProfileSummaryHeader.tsx new file mode 100644 index 00000000..2da62320 --- /dev/null +++ b/src/components/common/PlatformProfileSummaryHeader.tsx @@ -0,0 +1,39 @@ +import type { ReactNode } from 'react'; + +type PlatformProfileSummaryHeaderProps = { + kicker: ReactNode; + title: ReactNode; + badge?: ReactNode; + className?: string; + titleClassName?: string; + badgeClassName?: string; +}; + +/** + * 个人中心白底副弹层标题摘要骨架。 + * 只收口 kicker、标题和摘要 badge 的层次,不承接业务数值拼装。 + */ +export function PlatformProfileSummaryHeader({ + kicker, + title, + badge, + className, + titleClassName, + badgeClassName, +}: PlatformProfileSummaryHeaderProps) { + return ( +
+
+ {kicker} +
+
+ {title} +
+ {badge ? ( +
+ {badge} +
+ ) : null} +
+ ); +} diff --git a/src/components/common/PlatformReportDialog.tsx b/src/components/common/PlatformReportDialog.tsx index eebc7ee8..8aaedf52 100644 --- a/src/components/common/PlatformReportDialog.tsx +++ b/src/components/common/PlatformReportDialog.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react'; import { CopyFeedbackButton } from './CopyFeedbackButton'; import { PlatformInfoBlock } from './PlatformInfoBlock'; -import { UnifiedModal } from './UnifiedModal'; +import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal'; import { useCopyFeedback } from './useCopyFeedback'; export type PlatformReportDialogField = { @@ -31,8 +31,8 @@ export function PlatformReportDialog({ onClose, copyIdleLabel, fields, - overlayClassName = 'platform-theme platform-theme--light !items-center', - panelClassName = 'platform-remap-surface rounded-[1.5rem]', + overlayClassName, + panelClassName = 'rounded-[1.5rem]', }: PlatformReportDialogProps) { const { copyState, copyText, resetCopyState } = useCopyFeedback(); const reportText = useMemo(() => buildPlatformReportText(fields), [fields]); @@ -50,14 +50,13 @@ export function PlatformReportDialog({ }; return ( - )) : null} - + ); } diff --git a/src/components/common/PlatformUtilityInfoModal.test.tsx b/src/components/common/PlatformUtilityInfoModal.test.tsx new file mode 100644 index 00000000..ed04eda6 --- /dev/null +++ b/src/components/common/PlatformUtilityInfoModal.test.tsx @@ -0,0 +1,51 @@ +/* @vitest-environment jsdom */ + +import { render, screen, within } from '@testing-library/react'; +import { expect, test } from 'vitest'; + +import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal'; + +test('renders platform utility info modal shell with default platform styling', () => { + render( + {}} + footer={} + panelClassName="rounded-[1.5rem]" + > +
这里是正文
+
, + ); + + const dialog = screen.getByRole('dialog', { name: '工具信息' }); + expect(dialog.parentElement?.className).toContain('platform-theme--light'); + expect(dialog.parentElement?.className).toContain('!items-center'); + expect(dialog.className).toContain('platform-remap-surface'); + expect(dialog.className).toContain('rounded-[1.5rem]'); + expect(within(dialog).getByText('这里是正文')).toBeTruthy(); + expect(within(dialog).getByRole('button', { name: '知道了' })).toBeTruthy(); +}); + +test('supports custom theme and spacing overrides', () => { + render( + {}} + platformTheme="dark" + bodyClassName="space-y-3" + footerClassName="justify-center pt-0" + footer={} + > +
这里是正文
+
, + ); + + const dialog = screen.getByRole('dialog', { name: '工具信息' }); + expect(dialog.parentElement?.className).toContain('platform-theme--dark'); + expect(dialog.querySelector('.space-y-3')).toBeTruthy(); + expect(dialog.querySelector('.justify-center')).toBeTruthy(); + expect(dialog.querySelector('.pt-0')).toBeTruthy(); + expect(within(dialog).getByRole('button', { name: '继续' })).toBeTruthy(); +}); diff --git a/src/components/common/PlatformUtilityInfoModal.tsx b/src/components/common/PlatformUtilityInfoModal.tsx new file mode 100644 index 00000000..9ddb1c2c --- /dev/null +++ b/src/components/common/PlatformUtilityInfoModal.tsx @@ -0,0 +1,63 @@ +import type { ReactNode } from 'react'; + +import { UnifiedModal } from './UnifiedModal'; + +type PlatformUtilityInfoModalProps = { + open: boolean; + title: string; + onClose: () => void; + children: ReactNode; + footer?: ReactNode; + platformTheme?: 'light' | 'dark'; + overlayClassName?: string; + panelClassName?: string; + bodyClassName?: string; + footerClassName?: string; +}; + +function joinClassNames(...classNames: Array) { + return classNames.filter(Boolean).join(' '); +} + +/** + * 工具信息类弹窗统一走平台主题 overlay + 白底 panel 壳层。 + * 这里只收口通用窗口骨架,字段内容、分享渠道和复制按钮仍由业务组件自己渲染。 + */ +export function PlatformUtilityInfoModal({ + open, + title, + onClose, + children, + footer, + platformTheme = 'light', + overlayClassName, + panelClassName, + bodyClassName, + footerClassName, +}: PlatformUtilityInfoModalProps) { + return ( + + {children} + + ); +} diff --git a/src/components/common/PublishShareModal.tsx b/src/components/common/PublishShareModal.tsx index 1fa57d16..43446b89 100644 --- a/src/components/common/PublishShareModal.tsx +++ b/src/components/common/PublishShareModal.tsx @@ -4,11 +4,11 @@ import { useAuthUi } from '../auth/AuthUiContext'; import { CopyFeedbackButton } from './CopyFeedbackButton'; import { PlatformInfoBlock } from './PlatformInfoBlock'; import { PlatformSubpanel } from './PlatformSubpanel'; +import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal'; import { buildPublishShareText, type PublishShareModalPayload, } from './publishShareModalModel'; -import { UnifiedModal } from './UnifiedModal'; import { useCopyFeedback } from './useCopyFeedback'; type PublishShareModalProps = { @@ -115,14 +115,12 @@ export function PublishShareModal({ }; return ( - @@ -168,6 +166,6 @@ export function PublishShareModal({ actionFullWidth className="disabled:opacity-55" /> - + ); } diff --git a/src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx b/src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx index cfcfda16..366d3dce 100644 --- a/src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx +++ b/src/components/platform-entry/PlatformProfilePlayedWorksModal.tsx @@ -8,9 +8,11 @@ import type { import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; +import { PlatformProfileContentRow } from '../common/PlatformProfileContentRow'; +import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList'; +import { PlatformProfileSummaryHeader } from '../common/PlatformProfileSummaryHeader'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; -import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; import { PlatformProfileSecondaryModalShell } from './PlatformProfileModalShell'; import { @@ -143,19 +145,19 @@ export function PlatformProfilePlayedWorksModal({ panelClassName="relative !max-h-[min(92vh,42rem)] !max-w-[38rem] bg-white text-zinc-950 shadow-2xl !rounded-[1.35rem] sm:!rounded-[1.35rem]" contentClassName="relative max-h-[min(92vh,42rem)] overflow-y-auto px-4 pb-5 pt-4 sm:px-5" > -
-
- PLAYED -
-
玩过
- } - className="mt-2" - > - {formatTotalPlayTimeHours(stats?.totalPlayTimeMs ?? 0)} - -
+ } + > + {formatTotalPlayTimeHours(stats?.totalPlayTimeMs ?? 0)} + + } + badgeClassName="mt-2" + /> {error ? ( @@ -171,14 +173,11 @@ export function PlatformProfilePlayedWorksModal({ - {Array.from({ length: 4 }).map((_, index) => ( -
- ))} -
+ } isEmpty={!hasArchiveEntries && !hasPlayedWorks} emptyState={ @@ -218,9 +217,8 @@ export function PlatformProfilePlayedWorksModal({
{playedWorks.map((work) => ( - onOpenWork?.(work)} surface="flat" @@ -244,20 +242,20 @@ export function PlatformProfilePlayedWorksModal({ tone="profileAccent" size="xs" className="shrink-0 border-transparent" - > - {formatPlayedWorkType(work.worldType)} - -
-
+ > + {formatPlayedWorkType(work.worldType)} + +
+
作品号 {formatPlayedWorkId(work)} 最近 {formatSnapshotTime(work.lastPlayedAt)} - 时长 {formatCompactPlayTime(work.lastObservedPlayTimeMs)} - -
- + 时长 {formatCompactPlayTime(work.lastObservedPlayTimeMs)} + +
+ ))} diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index a584c0f4..c66b351d 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -9,6 +9,7 @@ import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; +import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList'; import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; @@ -220,14 +221,11 @@ export function PlatformProfileRechargeModal({ } isLoading={isLoading} loadingState={ -
- {Array.from({ length: 4 }).map((_, index) => ( -
- ))} -
+ } isEmpty={products.length === 0} emptyState={ diff --git a/src/components/platform-entry/PlatformProfileReferralModal.tsx b/src/components/platform-entry/PlatformProfileReferralModal.tsx index f3cf176d..0e2afe71 100644 --- a/src/components/platform-entry/PlatformProfileReferralModal.tsx +++ b/src/components/platform-entry/PlatformProfileReferralModal.tsx @@ -9,6 +9,8 @@ import { CopyFeedbackButton } from '../common/CopyFeedbackButton'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; +import { PlatformProfileContentRow } from '../common/PlatformProfileContentRow'; +import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { PlatformTextField } from '../common/PlatformTextField'; @@ -139,10 +141,11 @@ export function PlatformProfileReferralModal({ -
-
-
+ } isEmpty={Boolean(center?.hasRedeemedCode)} emptyState={ @@ -193,10 +196,11 @@ export function PlatformProfileReferralModal({ -
-
-
+ } >
@@ -251,8 +255,7 @@ export function PlatformProfileReferralModal({ {center?.invitedUsers?.length ? (
{center.invitedUsers.map((user) => ( -
- + ))}
) : ( diff --git a/src/components/platform-entry/PlatformProfileTaskCenterModal.tsx b/src/components/platform-entry/PlatformProfileTaskCenterModal.tsx index c147f9bf..419b3ed3 100644 --- a/src/components/platform-entry/PlatformProfileTaskCenterModal.tsx +++ b/src/components/platform-entry/PlatformProfileTaskCenterModal.tsx @@ -1,9 +1,10 @@ import type { ProfileTaskCenterResponse } from '../../../packages/shared/src/contracts/runtime'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; +import { PlatformProfileContentRow } from '../common/PlatformProfileContentRow'; +import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; -import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import { buildProfileTaskProgressLabel, @@ -84,14 +85,11 @@ export function PlatformProfileTaskCenterModal({ } isLoading={isLoading} loadingState={ -
- {Array.from({ length: 2 }).map((_, index) => ( -
- ))} -
+ } isEmpty={tasks.length === 0} emptyState={ @@ -107,8 +105,7 @@ export function PlatformProfileTaskCenterModal({ const progressLabel = buildProfileTaskProgressLabel(task); return ( - {getProfileTaskClaimButtonLabel(task, isClaiming)} - + ); })}
diff --git a/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx b/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx index bc4e445e..a5799c62 100644 --- a/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx +++ b/src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx @@ -4,9 +4,11 @@ import type { ProfileWalletLedgerResponse } from '../../../packages/shared/src/c import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; +import { PlatformProfileContentRow } from '../common/PlatformProfileContentRow'; +import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList'; +import { PlatformProfileSummaryHeader } from '../common/PlatformProfileSummaryHeader'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; -import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { PlatformProfileSecondaryModalShell } from './PlatformProfileModalShell'; import { buildWalletLedgerPresentation } from '../rpg-entry/rpgEntryProfileFundsViewModel'; import { formatPlatformWorldTime } from '../rpg-entry/rpgEntryWorldPresentation'; @@ -47,19 +49,19 @@ export function PlatformProfileWalletLedgerModal({ panelClassName="relative !max-h-[min(92vh,42rem)] !max-w-[30rem] bg-[linear-gradient(180deg,#fff7f8_0%,#ffffff_38%,#f8fafc_100%)] text-zinc-950 shadow-2xl !rounded-[1.35rem] sm:!rounded-[1.35rem]" contentClassName="relative max-h-[min(92vh,42rem)] overflow-y-auto px-4 pb-5 pt-4 sm:px-5" > -
-
- LEDGER -
-
泥点账单
- } - className="mt-3 bg-white/70" - > - {walletLedgerPresentation.balanceLabel} - -
+ } + className="bg-white/70" + > + {walletLedgerPresentation.balanceLabel} + + } + /> - {Array.from({ length: 5 }).map((_, index) => ( -
- ))} -
+ } isEmpty={entries.length === 0} emptyState={ @@ -105,8 +104,7 @@ export function PlatformProfileWalletLedgerModal({ >
{entries.map((entry) => ( -
- + ))}