继续收口认证入口弹窗壳层
新增 PlatformAuthModalShell 统一认证白底弹窗壳层 登录入口和邀请码弹窗复用共享认证壳层 补充认证壳层和 AuthGate 接入测试 同步 PlatformUiKit 文档和 Hermes 决策记录
This commit is contained in:
@@ -2101,6 +2101,7 @@
|
|||||||
- 决策:`PlatformToolModalShell` 继续承接 RPG 结果页发布检查弹窗;`RpgCreationResultActionBar.tsx` 只保留发布检查、封面预览、封面设置和发布动作语义,不再直接维护 `createPortal`、平台主题 overlay、白底 remap panel、header close、body/footer spacing 和遮罩关闭逻辑。后续结果页 / 工具页里同形态的白底 portal 弹窗优先迁移到 `PlatformToolModalShell`;编辑器大壳、暗色 runtime overlay 和需要专属布局的面板继续保留局部 shell。
|
- 决策:`PlatformToolModalShell` 继续承接 RPG 结果页发布检查弹窗;`RpgCreationResultActionBar.tsx` 只保留发布检查、封面预览、封面设置和发布动作语义,不再直接维护 `createPortal`、平台主题 overlay、白底 remap panel、header close、body/footer spacing 和遮罩关闭逻辑。后续结果页 / 工具页里同形态的白底 portal 弹窗优先迁移到 `PlatformToolModalShell`;编辑器大壳、暗色 runtime overlay 和需要专属布局的面板继续保留局部 shell。
|
||||||
- 决策:`PlatformToolModalShell` 继续承接方洞结果页图片槽弹窗;`SquareHoleResultView.tsx` 的封面 / 背景 / 形状 / 洞口图片查看与历史选择弹窗只保留当前图、上传、AI 生成和历史素材选择语义,不再直接维护 `createPortal`、主题 overlay、白底 remap panel、header close 和滚动 body。该弹窗使用 `ariaLabel` 保持“封面图查看 / 背景图查看”等固定可访问名称,历史生成区继续由 `PlatformAssetPickerGrid` 承接读取、错误和空态。
|
- 决策:`PlatformToolModalShell` 继续承接方洞结果页图片槽弹窗;`SquareHoleResultView.tsx` 的封面 / 背景 / 形状 / 洞口图片查看与历史选择弹窗只保留当前图、上传、AI 生成和历史素材选择语义,不再直接维护 `createPortal`、主题 overlay、白底 remap panel、header close 和滚动 body。该弹窗使用 `ariaLabel` 保持“封面图查看 / 背景图查看”等固定可访问名称,历史生成区继续由 `PlatformAssetPickerGrid` 承接读取、错误和空态。
|
||||||
- 决策:`PlatformToolModalShell` 继续承接视觉小说结果页素材选择弹窗;`VisualNovelAssetPickerDialog` 只保留本地上传、AI 图片生成、历史素材读取、错误提示和素材选择回调,不再直接维护 `createPortal`、平台主题 overlay、白底 remap panel、header close 和滚动 body。视觉小说音频生成弹窗需要保留生成中禁止关闭,实体编辑器弹窗需要保留编辑 footer,后续逐个迁移并补对应交互测试。
|
- 决策:`PlatformToolModalShell` 继续承接视觉小说结果页素材选择弹窗;`VisualNovelAssetPickerDialog` 只保留本地上传、AI 图片生成、历史素材读取、错误提示和素材选择回调,不再直接维护 `createPortal`、平台主题 overlay、白底 remap panel、header close 和滚动 body。视觉小说音频生成弹窗需要保留生成中禁止关闭,实体编辑器弹窗需要保留编辑 footer,后续逐个迁移并补对应交互测试。
|
||||||
|
- 决策:认证入口白底弹窗壳层收口到 `src/components/auth/PlatformAuthModalShell.tsx`;该壳层只承接平台主题 overlay、`platform-auth-card`、标准标题栏、关闭按钮、点击遮罩关闭和禁用 Escape 的认证弹窗策略,不持有短信 / 密码登录、重置密码、邀请码规范化、法律协议或错误状态。`LoginScreen.tsx` 与 `RegistrationInviteModal.tsx` 只保留各自表单状态和提交流程。
|
||||||
- 决策:NPC dark modal footer 和暗色明细空态也继续纳入同一条收口线。`NpcModals.tsx` 里的交易 / 赠礼 / 招募弹窗 footer 按钮和物品详情“关闭”按钮都改为委托 `PlatformActionButton surface="editorDark"`,交易右侧“请选择一件物品”提示改为 `PlatformEmptyState surface="editorDark"`;`CharacterInfoShared.tsx` 的 `BuildContributionDetailPanel` 空明细也改为 `PlatformEmptyState surface="editorDark"`。数量 stepper、赠礼 / 招募 option card、标签强度按钮这类带独立业务语义的控件继续保留局部实现。
|
- 决策:NPC dark modal footer 和暗色明细空态也继续纳入同一条收口线。`NpcModals.tsx` 里的交易 / 赠礼 / 招募弹窗 footer 按钮和物品详情“关闭”按钮都改为委托 `PlatformActionButton surface="editorDark"`,交易右侧“请选择一件物品”提示改为 `PlatformEmptyState surface="editorDark"`;`CharacterInfoShared.tsx` 的 `BuildContributionDetailPanel` 空明细也改为 `PlatformEmptyState surface="editorDark"`。数量 stepper、赠礼 / 招募 option card、标签强度按钮这类带独立业务语义的控件继续保留局部实现。
|
||||||
- 决策:详情页头部动作组合统一收口到 `src/components/common/PlatformDetailTopbar.tsx` 与 `src/components/common/PlatformDetailShareActions.tsx`。`PlatformDetailTopbar` 只负责返回按钮、标题居中槽位和右侧动作槽位的布局,可在 `pill` / `icon` 返回入口之间切换;`PlatformDetailShareActions` 只负责“前置 badge 区块 + 作品号复制 + 分享复制”这组稳定动作,并允许按页面关闭复制或分享其中一项。`RpgEntryWorldDetailView.tsx` 已接入 overlay 版完整动作组,`PlatformWorkDetailView.tsx` 已接入 icon topbar 与 solid 版作品号复制动作,同时继续保留公开详情页自己的顶部 icon 分享入口和分享反馈提示。后续详情页若只是复用返回、标题、作品号复制或分享动作排列,优先组合这两个薄组件,不把作者、摘要、封面、轮播或业务 CTA 塞进共享配置对象。
|
- 决策:详情页头部动作组合统一收口到 `src/components/common/PlatformDetailTopbar.tsx` 与 `src/components/common/PlatformDetailShareActions.tsx`。`PlatformDetailTopbar` 只负责返回按钮、标题居中槽位和右侧动作槽位的布局,可在 `pill` / `icon` 返回入口之间切换;`PlatformDetailShareActions` 只负责“前置 badge 区块 + 作品号复制 + 分享复制”这组稳定动作,并允许按页面关闭复制或分享其中一项。`RpgEntryWorldDetailView.tsx` 已接入 overlay 版完整动作组,`PlatformWorkDetailView.tsx` 已接入 icon topbar 与 solid 版作品号复制动作,同时继续保留公开详情页自己的顶部 icon 分享入口和分享反馈提示。后续详情页若只是复用返回、标题、作品号复制或分享动作排列,优先组合这两个薄组件,不把作者、摘要、封面、轮播或业务 CTA 塞进共享配置对象。
|
||||||
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||||
|
|||||||
@@ -280,6 +280,7 @@
|
|||||||
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.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.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.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.53. 认证入口白底弹窗壳层收口到 `src/components/auth/PlatformAuthModalShell.tsx`;该 Module 只承接平台主题 overlay、`platform-auth-card`、标准标题栏、关闭按钮、点击遮罩关闭和禁用 Escape 的认证弹窗策略,不持有短信 / 密码登录、重置密码、邀请码规范化、法律协议或错误状态。`LoginScreen.tsx` 与 `RegistrationInviteModal.tsx` 已接入,业务组件只保留表单状态与提交流程。后续认证域新增同形态白底弹窗时优先复用该壳层;账号安全详情和绑定手机号这类布局差异较大的卡片先独立评估,不把 auth shell 扩成万能认证容器。验证命令:`npx vitest run src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.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.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. 大鱼吃小鱼结果页 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`。
|
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`。
|
||||||
|
|||||||
@@ -592,6 +592,8 @@ test('auth gate hides register entry and opens invite modal for new sms account'
|
|||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: '进入作品' }));
|
await user.click(screen.getByRole('button', { name: '进入作品' }));
|
||||||
const dialog = await screen.findByRole('dialog', { name: '账号入口' });
|
const dialog = await screen.findByRole('dialog', { name: '账号入口' });
|
||||||
|
expect(dialog.className).toContain('platform-auth-card');
|
||||||
|
expect(dialog.className).toContain('platform-modal-shell');
|
||||||
expect(within(dialog).queryByRole('tab', { name: '注册' })).toBeNull();
|
expect(within(dialog).queryByRole('tab', { name: '注册' })).toBeNull();
|
||||||
expect(within(dialog).queryByLabelText('邀请码')).toBeNull();
|
expect(within(dialog).queryByLabelText('邀请码')).toBeNull();
|
||||||
await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
|
await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
|
||||||
@@ -609,6 +611,8 @@ test('auth gate hides register entry and opens invite modal for new sms account'
|
|||||||
const inviteDialog = await screen.findByRole('dialog', {
|
const inviteDialog = await screen.findByRole('dialog', {
|
||||||
name: '请填写邀请码',
|
name: '请填写邀请码',
|
||||||
});
|
});
|
||||||
|
expect(inviteDialog.className).toContain('platform-auth-card');
|
||||||
|
expect(inviteDialog.className).toContain('platform-modal-shell');
|
||||||
const inviteCodeInput = within(inviteDialog).getByLabelText(
|
const inviteCodeInput = within(inviteDialog).getByLabelText(
|
||||||
'邀请码',
|
'邀请码',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
@@ -799,6 +803,8 @@ test('login modal resets draft state every time it is reopened', async () => {
|
|||||||
await user.click(await screen.findByRole('button', { name: '进入作品' }));
|
await user.click(await screen.findByRole('button', { name: '进入作品' }));
|
||||||
|
|
||||||
const firstDialog = screen.getByRole('dialog', { name: '账号入口' });
|
const firstDialog = screen.getByRole('dialog', { name: '账号入口' });
|
||||||
|
expect(firstDialog.className).toContain('platform-auth-card');
|
||||||
|
expect(firstDialog.className).toContain('platform-modal-shell');
|
||||||
await user.type(within(firstDialog).getByLabelText('手机号'), '13800000000');
|
await user.type(within(firstDialog).getByLabelText('手机号'), '13800000000');
|
||||||
await user.click(
|
await user.click(
|
||||||
within(firstDialog).getByRole('button', { name: '获取验证码' }),
|
within(firstDialog).getByRole('button', { name: '获取验证码' }),
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import {
|
|||||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
|
||||||
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
||||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||||
import { PlatformTextField } from '../common/PlatformTextField';
|
import { PlatformTextField } from '../common/PlatformTextField';
|
||||||
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||||
|
import { PlatformAuthModalShell } from './PlatformAuthModalShell';
|
||||||
|
|
||||||
type SmsScene = 'login' | 'reset_password';
|
type SmsScene = 'login' | 'reset_password';
|
||||||
type LoginTab = 'phone' | 'password';
|
type LoginTab = 'phone' | 'password';
|
||||||
@@ -191,201 +191,181 @@ export function LoginScreen({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<PlatformAuthModalShell
|
||||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[120] flex items-end justify-center px-3 py-4 text-[var(--platform-text-strong)] backdrop-blur-sm sm:items-center sm:p-4`}
|
title={isResetPanelOpen ? '重置密码' : '账号入口'}
|
||||||
onClick={onClose}
|
platformTheme={platformTheme}
|
||||||
|
onClose={onClose}
|
||||||
|
closeLabel="关闭登录弹窗"
|
||||||
|
panelClassName="!max-w-md"
|
||||||
>
|
>
|
||||||
<div
|
{isResetPanelOpen ? (
|
||||||
role="dialog"
|
<PasswordResetPanel
|
||||||
aria-modal="true"
|
phone={resetPhone}
|
||||||
aria-labelledby="auth-login-dialog-title"
|
code={resetCode}
|
||||||
className="platform-auth-card w-full max-w-md overflow-hidden rounded-[2rem]"
|
password={resetPasswordValue}
|
||||||
onClick={(event) => event.stopPropagation()}
|
sendingCode={sendingCode}
|
||||||
>
|
loggingIn={loggingIn}
|
||||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
cooldownSeconds={resetCooldownSeconds}
|
||||||
<div
|
error={error}
|
||||||
id="auth-login-dialog-title"
|
onPhoneChange={setResetPhone}
|
||||||
className="text-lg font-semibold text-[var(--platform-text-strong)]"
|
onCodeChange={setResetCode}
|
||||||
>
|
onPasswordChange={setResetPasswordValue}
|
||||||
{isResetPanelOpen ? '重置密码' : '账号入口'}
|
onBack={() => setIsResetPanelOpen(false)}
|
||||||
</div>
|
onSendCode={async () => {
|
||||||
<PlatformModalCloseButton
|
const result = await onSendCode(resetPhone, 'reset_password');
|
||||||
onClick={onClose}
|
setResetCooldownSeconds(result.cooldownSeconds);
|
||||||
label="关闭登录弹窗"
|
}}
|
||||||
variant="platformIcon"
|
onSubmit={() =>
|
||||||
className="p-2"
|
onResetPassword(resetPhone, resetCode, resetPasswordValue)
|
||||||
/>
|
}
|
||||||
</div>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-5 px-5 py-5">
|
||||||
|
{phoneLoginEnabled ? (
|
||||||
|
<PlatformSegmentedTabs
|
||||||
|
items={
|
||||||
|
passwordLoginEnabled
|
||||||
|
? LOGIN_TAB_ITEMS
|
||||||
|
: LOGIN_TAB_ITEMS.slice(0, 1)
|
||||||
|
}
|
||||||
|
activeId={activeLoginTab}
|
||||||
|
onChange={setActiveLoginTab}
|
||||||
|
columns={passwordLoginEnabled ? 'two' : 'one'}
|
||||||
|
frame="bare"
|
||||||
|
surface="transparent"
|
||||||
|
tone="underline"
|
||||||
|
size="tab"
|
||||||
|
semantics="tabs"
|
||||||
|
ariaLabel="登录方式"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isResetPanelOpen ? (
|
{passwordLoginEnabled && activeLoginTab === 'password' ? (
|
||||||
<PasswordResetPanel
|
<form
|
||||||
phone={resetPhone}
|
className="flex flex-col gap-4"
|
||||||
code={resetCode}
|
onSubmit={(event) => {
|
||||||
password={resetPasswordValue}
|
event.preventDefault();
|
||||||
sendingCode={sendingCode}
|
if (
|
||||||
loggingIn={loggingIn}
|
submitDisabled ||
|
||||||
cooldownSeconds={resetCooldownSeconds}
|
!phone.trim() ||
|
||||||
error={error}
|
!password.trim() ||
|
||||||
onPhoneChange={setResetPhone}
|
!legalConsentChecked
|
||||||
onCodeChange={setResetCode}
|
) {
|
||||||
onPasswordChange={setResetPasswordValue}
|
return;
|
||||||
onBack={() => setIsResetPanelOpen(false)}
|
|
||||||
onSendCode={async () => {
|
|
||||||
const result = await onSendCode(resetPhone, 'reset_password');
|
|
||||||
setResetCooldownSeconds(result.cooldownSeconds);
|
|
||||||
}}
|
|
||||||
onSubmit={() =>
|
|
||||||
onResetPassword(resetPhone, resetCode, resetPasswordValue)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-5 px-5 py-5">
|
|
||||||
{phoneLoginEnabled ? (
|
|
||||||
<PlatformSegmentedTabs
|
|
||||||
items={
|
|
||||||
passwordLoginEnabled
|
|
||||||
? LOGIN_TAB_ITEMS
|
|
||||||
: LOGIN_TAB_ITEMS.slice(0, 1)
|
|
||||||
}
|
}
|
||||||
activeId={activeLoginTab}
|
void onPasswordSubmit(phone, password);
|
||||||
onChange={setActiveLoginTab}
|
}}
|
||||||
columns={passwordLoginEnabled ? 'two' : 'one'}
|
>
|
||||||
frame="bare"
|
<label className="grid gap-2">
|
||||||
surface="transparent"
|
<PlatformFieldLabel variant="form" className="mb-0">
|
||||||
tone="underline"
|
手机号
|
||||||
size="tab"
|
</PlatformFieldLabel>
|
||||||
semantics="tabs"
|
<PlatformTextField
|
||||||
ariaLabel="登录方式"
|
autoComplete="tel"
|
||||||
/>
|
inputMode="numeric"
|
||||||
) : null}
|
value={phone}
|
||||||
|
onChange={(event) => setPhone(event.target.value)}
|
||||||
|
placeholder="13800000000"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="grid gap-2">
|
||||||
|
<PlatformFieldLabel variant="form" className="mb-0">
|
||||||
|
密码
|
||||||
|
</PlatformFieldLabel>
|
||||||
|
<PlatformTextField
|
||||||
|
autoComplete="current-password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
placeholder="输入密码"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
{passwordLoginEnabled && activeLoginTab === 'password' ? (
|
{error ? <ErrorBanner message={error} /> : null}
|
||||||
<form
|
{legalConsentRow}
|
||||||
className="flex flex-col gap-4"
|
|
||||||
onSubmit={(event) => {
|
<div className="flex flex-col gap-2">
|
||||||
event.preventDefault();
|
<PlatformActionButton
|
||||||
if (
|
type="submit"
|
||||||
|
disabled={
|
||||||
submitDisabled ||
|
submitDisabled ||
|
||||||
!phone.trim() ||
|
!phone.trim() ||
|
||||||
!password.trim() ||
|
!password.trim() ||
|
||||||
!legalConsentChecked
|
!legalConsentChecked
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
void onPasswordSubmit(phone, password);
|
size="lg"
|
||||||
}}
|
>
|
||||||
>
|
{loggingIn ? '登录中' : '登录'}
|
||||||
<label className="grid gap-2">
|
</PlatformActionButton>
|
||||||
<PlatformFieldLabel variant="form" className="mb-0">
|
<button
|
||||||
手机号
|
type="button"
|
||||||
</PlatformFieldLabel>
|
className="self-end text-sm text-[var(--platform-accent)]"
|
||||||
<PlatformTextField
|
onClick={() => setIsResetPanelOpen(true)}
|
||||||
autoComplete="tel"
|
>
|
||||||
inputMode="numeric"
|
忘记密码
|
||||||
value={phone}
|
</button>
|
||||||
onChange={(event) => setPhone(event.target.value)}
|
</div>
|
||||||
placeholder="13800000000"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="grid gap-2">
|
|
||||||
<PlatformFieldLabel variant="form" className="mb-0">
|
|
||||||
密码
|
|
||||||
</PlatformFieldLabel>
|
|
||||||
<PlatformTextField
|
|
||||||
autoComplete="current-password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(event) => setPassword(event.target.value)}
|
|
||||||
placeholder="输入密码"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{error ? <ErrorBanner message={error} /> : null}
|
{wechatLoginEnabled && !miniProgramRuntime ? (
|
||||||
{legalConsentRow}
|
<WechatButton
|
||||||
|
loading={wechatLoading}
|
||||||
|
disabled={submitDisabled}
|
||||||
|
onClick={onStartWechatLogin}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
{phoneLoginEnabled && activeLoginTab === 'phone' ? (
|
||||||
<PlatformActionButton
|
<PhoneCodeForm
|
||||||
type="submit"
|
phone={phone}
|
||||||
disabled={
|
code={code}
|
||||||
submitDisabled ||
|
captchaAnswer={captchaAnswer}
|
||||||
!phone.trim() ||
|
captchaChallenge={captchaChallenge}
|
||||||
!password.trim() ||
|
cooldownSeconds={cooldownSeconds}
|
||||||
!legalConsentChecked
|
sendingCode={sendingCode}
|
||||||
}
|
loggingIn={loggingIn}
|
||||||
size="lg"
|
error={error}
|
||||||
>
|
hint={hint}
|
||||||
{loggingIn ? '登录中' : '登录'}
|
submitLabel="登录"
|
||||||
</PlatformActionButton>
|
enabled={phoneLoginEnabled}
|
||||||
<button
|
legalConsentChecked={legalConsentChecked}
|
||||||
type="button"
|
legalConsentNode={legalConsentRow}
|
||||||
className="self-end text-sm text-[var(--platform-accent)]"
|
showPhoneField
|
||||||
onClick={() => setIsResetPanelOpen(true)}
|
onPhoneChange={setPhone}
|
||||||
>
|
onCodeChange={setCode}
|
||||||
忘记密码
|
onCaptchaAnswerChange={setCaptchaAnswer}
|
||||||
</button>
|
onSendCode={async () => {
|
||||||
</div>
|
setHint('');
|
||||||
|
const result = await onSendCode(phone, 'login', {
|
||||||
|
challengeId: captchaChallenge?.challengeId,
|
||||||
|
answer: captchaAnswer,
|
||||||
|
});
|
||||||
|
setCooldownSeconds(result.cooldownSeconds);
|
||||||
|
setHint(
|
||||||
|
`短信请求已提交,验证码有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
|
||||||
|
);
|
||||||
|
setCaptchaAnswer('');
|
||||||
|
}}
|
||||||
|
onSubmit={() => onPhoneSubmit(phone, code)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{wechatLoginEnabled && !miniProgramRuntime ? (
|
{!passwordLoginEnabled &&
|
||||||
<WechatButton
|
!phoneLoginEnabled &&
|
||||||
loading={wechatLoading}
|
!wechatLoginEnabled &&
|
||||||
disabled={submitDisabled}
|
!miniProgramRuntime ? (
|
||||||
onClick={onStartWechatLogin}
|
<PlatformEmptyState
|
||||||
/>
|
surface="subpanel"
|
||||||
) : null}
|
size="compact"
|
||||||
</form>
|
className="px-4 py-4"
|
||||||
) : null}
|
>
|
||||||
|
当前登录入口暂不可用。
|
||||||
{phoneLoginEnabled && activeLoginTab === 'phone' ? (
|
</PlatformEmptyState>
|
||||||
<PhoneCodeForm
|
) : null}
|
||||||
phone={phone}
|
</div>
|
||||||
code={code}
|
)}
|
||||||
captchaAnswer={captchaAnswer}
|
</PlatformAuthModalShell>
|
||||||
captchaChallenge={captchaChallenge}
|
|
||||||
cooldownSeconds={cooldownSeconds}
|
|
||||||
sendingCode={sendingCode}
|
|
||||||
loggingIn={loggingIn}
|
|
||||||
error={error}
|
|
||||||
hint={hint}
|
|
||||||
submitLabel="登录"
|
|
||||||
enabled={phoneLoginEnabled}
|
|
||||||
legalConsentChecked={legalConsentChecked}
|
|
||||||
legalConsentNode={legalConsentRow}
|
|
||||||
showPhoneField
|
|
||||||
onPhoneChange={setPhone}
|
|
||||||
onCodeChange={setCode}
|
|
||||||
onCaptchaAnswerChange={setCaptchaAnswer}
|
|
||||||
onSendCode={async () => {
|
|
||||||
setHint('');
|
|
||||||
const result = await onSendCode(phone, 'login', {
|
|
||||||
challengeId: captchaChallenge?.challengeId,
|
|
||||||
answer: captchaAnswer,
|
|
||||||
});
|
|
||||||
setCooldownSeconds(result.cooldownSeconds);
|
|
||||||
setHint(
|
|
||||||
`短信请求已提交,验证码有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
|
|
||||||
);
|
|
||||||
setCaptchaAnswer('');
|
|
||||||
}}
|
|
||||||
onSubmit={() => onPhoneSubmit(phone, code)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!passwordLoginEnabled &&
|
|
||||||
!phoneLoginEnabled &&
|
|
||||||
!wechatLoginEnabled &&
|
|
||||||
!miniProgramRuntime ? (
|
|
||||||
<PlatformEmptyState
|
|
||||||
surface="subpanel"
|
|
||||||
size="compact"
|
|
||||||
className="px-4 py-4"
|
|
||||||
>
|
|
||||||
当前登录入口暂不可用。
|
|
||||||
</PlatformEmptyState>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<LegalDocumentModal
|
<LegalDocumentModal
|
||||||
document={activeLegalDocument}
|
document={activeLegalDocument}
|
||||||
open={Boolean(activeLegalDocument)}
|
open={Boolean(activeLegalDocument)}
|
||||||
|
|||||||
54
src/components/auth/PlatformAuthModalShell.test.tsx
Normal file
54
src/components/auth/PlatformAuthModalShell.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||||
|
import { expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { PlatformAuthModalShell } from './PlatformAuthModalShell';
|
||||||
|
|
||||||
|
test('renders auth modal shell with platform theme and auth card chrome', () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<PlatformAuthModalShell
|
||||||
|
title="账号入口"
|
||||||
|
platformTheme="light"
|
||||||
|
onClose={onClose}
|
||||||
|
closeLabel="关闭登录弹窗"
|
||||||
|
panelClassName="!max-w-md"
|
||||||
|
>
|
||||||
|
<div>登录表单</div>
|
||||||
|
</PlatformAuthModalShell>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialog = screen.getByRole('dialog', { name: '账号入口' });
|
||||||
|
|
||||||
|
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||||
|
expect(dialog.className).toContain('platform-modal-shell');
|
||||||
|
expect(dialog.className).toContain('platform-auth-card');
|
||||||
|
expect(dialog.className).toContain('!max-w-md');
|
||||||
|
expect(within(dialog).getByText('登录表单')).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(dialog.parentElement as HTMLElement);
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps escape disabled for auth flows', () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<PlatformAuthModalShell
|
||||||
|
title="请填写邀请码"
|
||||||
|
platformTheme="dark"
|
||||||
|
onClose={onClose}
|
||||||
|
closeLabel="取消填写邀请码"
|
||||||
|
zIndexClassName="z-[130]"
|
||||||
|
>
|
||||||
|
<div>邀请码表单</div>
|
||||||
|
</PlatformAuthModalShell>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.keyDown(window, { key: 'Escape' });
|
||||||
|
|
||||||
|
expect(onClose).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByRole('button', { name: '取消填写邀请码' })).toBeTruthy();
|
||||||
|
});
|
||||||
59
src/components/auth/PlatformAuthModalShell.tsx
Normal file
59
src/components/auth/PlatformAuthModalShell.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import { UnifiedModal } from '../common/UnifiedModal';
|
||||||
|
|
||||||
|
type PlatformAuthModalShellProps = {
|
||||||
|
title: string;
|
||||||
|
platformTheme: PlatformTheme;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
closeLabel: string;
|
||||||
|
zIndexClassName?: string;
|
||||||
|
panelClassName?: string;
|
||||||
|
bodyClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function joinClassNames(...classNames: Array<string | false | null | undefined>) {
|
||||||
|
return classNames.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证入口弹窗共享壳层。
|
||||||
|
* 这里只统一主题遮罩、auth card、标题栏和关闭按钮,登录 / 邀请码表单状态仍留在各自业务组件。
|
||||||
|
*/
|
||||||
|
export function PlatformAuthModalShell({
|
||||||
|
title,
|
||||||
|
platformTheme,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
closeLabel,
|
||||||
|
zIndexClassName = 'z-[120]',
|
||||||
|
panelClassName,
|
||||||
|
bodyClassName = '!p-0',
|
||||||
|
}: PlatformAuthModalShellProps) {
|
||||||
|
return (
|
||||||
|
<UnifiedModal
|
||||||
|
open
|
||||||
|
title={title}
|
||||||
|
onClose={onClose}
|
||||||
|
closeLabel={closeLabel}
|
||||||
|
closeVariant="platformIcon"
|
||||||
|
closeOnBackdrop
|
||||||
|
closeOnEscape={false}
|
||||||
|
portal={false}
|
||||||
|
size="sm"
|
||||||
|
zIndexClassName={zIndexClassName}
|
||||||
|
overlayClassName={`platform-theme platform-theme--${platformTheme} !px-3 !py-4 text-[var(--platform-text-strong)] sm:!p-4`}
|
||||||
|
panelClassName={joinClassNames(
|
||||||
|
'platform-auth-card !rounded-[2rem] sm:!rounded-[2rem]',
|
||||||
|
panelClassName,
|
||||||
|
)}
|
||||||
|
headerClassName="!items-center !px-5 !py-4"
|
||||||
|
titleClassName="text-lg font-semibold text-[var(--platform-text-strong)]"
|
||||||
|
bodyClassName={bodyClassName}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</UnifiedModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
|
||||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||||
import { PlatformTextField } from '../common/PlatformTextField';
|
import { PlatformTextField } from '../common/PlatformTextField';
|
||||||
|
import { PlatformAuthModalShell } from './PlatformAuthModalShell';
|
||||||
|
|
||||||
type RegistrationInviteModalProps = {
|
type RegistrationInviteModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -49,70 +49,48 @@ export function RegistrationInviteModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PlatformAuthModalShell
|
||||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[130] flex items-end justify-center px-3 py-4 text-[var(--platform-text-strong)] backdrop-blur-sm sm:items-center sm:p-4`}
|
title="请填写邀请码"
|
||||||
onClick={onClose}
|
platformTheme={platformTheme}
|
||||||
|
onClose={onClose}
|
||||||
|
closeLabel="取消填写邀请码"
|
||||||
|
zIndexClassName="z-[130]"
|
||||||
|
panelClassName="!max-w-sm"
|
||||||
>
|
>
|
||||||
<div
|
<form
|
||||||
role="dialog"
|
className="flex flex-col gap-4 px-5 py-5"
|
||||||
aria-modal="true"
|
onSubmit={(event) => {
|
||||||
aria-labelledby="registration-invite-dialog-title"
|
event.preventDefault();
|
||||||
className="platform-auth-card w-full max-w-sm overflow-hidden rounded-[2rem]"
|
if (!normalizedInviteCode) {
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSubmit(normalizedInviteCode);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
<label className="grid gap-2">
|
||||||
<div
|
<PlatformFieldLabel variant="form" className="mb-0">
|
||||||
id="registration-invite-dialog-title"
|
邀请码
|
||||||
className="text-lg font-semibold text-[var(--platform-text-strong)]"
|
</PlatformFieldLabel>
|
||||||
>
|
<PlatformTextField
|
||||||
请填写邀请码
|
autoComplete="off"
|
||||||
</div>
|
value={inviteCode}
|
||||||
<PlatformModalCloseButton
|
onChange={(event) => setInviteCode(event.target.value)}
|
||||||
onClick={onClose}
|
placeholder="邀请码"
|
||||||
label="取消填写邀请码"
|
|
||||||
variant="platformIcon"
|
|
||||||
className="p-2"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</label>
|
||||||
<form
|
|
||||||
className="flex flex-col gap-4 px-5 py-5"
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!normalizedInviteCode) {
|
|
||||||
onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onSubmit(normalizedInviteCode);
|
{error ? (
|
||||||
}}
|
<PlatformStatusMessage tone="error" surface="profile">
|
||||||
>
|
{error}
|
||||||
<label className="grid gap-2">
|
</PlatformStatusMessage>
|
||||||
<PlatformFieldLabel variant="form" className="mb-0">
|
) : null}
|
||||||
邀请码
|
|
||||||
</PlatformFieldLabel>
|
|
||||||
<PlatformTextField
|
|
||||||
autoComplete="off"
|
|
||||||
value={inviteCode}
|
|
||||||
onChange={(event) => setInviteCode(event.target.value)}
|
|
||||||
placeholder="邀请码"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{error ? (
|
<PlatformActionButton type="submit" disabled={submitting} size="lg">
|
||||||
<PlatformStatusMessage tone="error" surface="profile">
|
{submitting ? '提交中' : normalizedInviteCode ? '提交' : '跳过'}
|
||||||
{error}
|
</PlatformActionButton>
|
||||||
</PlatformStatusMessage>
|
</form>
|
||||||
) : null}
|
</PlatformAuthModalShell>
|
||||||
|
|
||||||
<PlatformActionButton
|
|
||||||
type="submit"
|
|
||||||
disabled={submitting}
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{submitting ? '提交中' : normalizedInviteCode ? '提交' : '跳过'}
|
|
||||||
</PlatformActionButton>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user