收口分类筛选与扫码弹窗壳层

RpgEntryHomeView 的分类筛选弹窗和扫码面板改用 UnifiedModal
补充扫码关闭清理与分类筛选关闭路径测试
更新 PlatformUiKit 收口计划与 .hermes 决策记录
This commit is contained in:
2026-06-10 17:24:11 +08:00
parent 701fd42777
commit 488c69aecc
4 changed files with 153 additions and 135 deletions

View File

@@ -56,6 +56,7 @@
- 2026-06-10 追加:`PlatformModalCloseButton variant="pixel"` 承接 `UnifiedModal variant="pixel"` 头部圆形关闭入口;`UnifiedModal` 只选择 `platformIcon / pixel` 变体并保留 closeDisabled、Backdrop、Escape 和 portal 语义,不再手写 X 图标、aria 和关闭按钮 class。验证命令`npm run test -- src/components/common/UnifiedModal.test.tsx src/components/common/PlatformModalCloseButton.test.tsx src/components/common/UnifiedConfirmDialog.test.tsx` - 2026-06-10 追加:`PlatformModalCloseButton variant="pixel"` 承接 `UnifiedModal variant="pixel"` 头部圆形关闭入口;`UnifiedModal` 只选择 `platformIcon / pixel` 变体并保留 closeDisabled、Backdrop、Escape 和 portal 语义,不再手写 X 图标、aria 和关闭按钮 class。验证命令`npm run test -- src/components/common/UnifiedModal.test.tsx src/components/common/PlatformModalCloseButton.test.tsx src/components/common/UnifiedConfirmDialog.test.tsx`
- 2026-06-10 追加:`UnifiedModal` 新增 `closeVariant``closeOnEscape``titleClassName``descriptionClassName`,用于在收口标准平台弹窗壳层时保留个人中心 `profile / profileCompact` 关闭按钮、原有标题层级和“不响应 Escape / backdrop”的交互语义RPG 首页个人中心里的昵称修改、账户充值、每日任务和兑换码弹窗已迁移到 `UnifiedModal`,支付结果 / 支付确认遮罩 / 泥点账单这类头部结构不同的弹窗继续保留专用实现。验证命令:`npm run test -- src/components/common/UnifiedModal.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx` - 2026-06-10 追加:`UnifiedModal` 新增 `closeVariant``closeOnEscape``titleClassName``descriptionClassName`,用于在收口标准平台弹窗壳层时保留个人中心 `profile / profileCompact` 关闭按钮、原有标题层级和“不响应 Escape / backdrop”的交互语义RPG 首页个人中心里的昵称修改、账户充值、每日任务和兑换码弹窗已迁移到 `UnifiedModal`,支付结果 / 支付确认遮罩 / 泥点账单这类头部结构不同的弹窗继续保留专用实现。验证命令:`npm run test -- src/components/common/UnifiedModal.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`
- 2026-06-10 追加:`UnifiedModal` 新增 `showHeader`,用于收口不需要标准头部但仍要保留 dialog 无障碍语义、遮罩和层级控制的轻量弹窗RPG 首页个人中心的支付结果提示与支付确认遮罩已迁移到 `showHeader={false}` 模式,业务页只保留 icon badge、文案与按钮不再手写 backdrop、aria 和白底壳层。个人中心移动端顶栏“扫码”“打开设置”入口统一使用 `PlatformIconButton`,并继续保留 `.platform-profile-header__icon-button` 局部 class 控制位置与主题色。验证命令:`npm run test -- src/components/common/UnifiedModal.test.tsx src/components/common/PlatformIconButton.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx` - 2026-06-10 追加:`UnifiedModal` 新增 `showHeader`,用于收口不需要标准头部但仍要保留 dialog 无障碍语义、遮罩和层级控制的轻量弹窗RPG 首页个人中心的支付结果提示与支付确认遮罩已迁移到 `showHeader={false}` 模式,业务页只保留 icon badge、文案与按钮不再手写 backdrop、aria 和白底壳层。个人中心移动端顶栏“扫码”“打开设置”入口统一使用 `PlatformIconButton`,并继续保留 `.platform-profile-header__icon-button` 局部 class 控制位置与主题色。验证命令:`npm run test -- src/components/common/UnifiedModal.test.tsx src/components/common/PlatformIconButton.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`
- 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-09 追加RPG 大编辑器暗色面板内的保存和角色槽动作继续走本地 `ActionButton`,不再混用白底平台 `platform-button` class平台白底动作收口和编辑器暗色动作收口保持两套视觉边界。 - 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 追加:`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` - 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

@@ -226,6 +226,7 @@
19.3.2. 个人中心昵称修改、账户充值、每日任务和兑换码四类标准头部弹窗迁移到 `UnifiedModal``UnifiedModal` 新增 `closeVariant``closeOnEscape``titleClassName``descriptionClassName`,用于在收口弹窗壳层时保留个人中心 `profile / profileCompact` 关闭按钮、居中浮层布局和标题层级。上述弹窗统一通过 `closeOnBackdrop={false}``closeOnEscape={false}` 保持原有交互语义,不把 backdrop / Escape 关闭行为悄悄带进个人中心;泥点账单这类头部结构和 footer 语义明显不同的弹窗继续保留专用实现,等出现更多同构 case 再扩充 `UnifiedModal` 能力。 19.3.2. 个人中心昵称修改、账户充值、每日任务和兑换码四类标准头部弹窗迁移到 `UnifiedModal``UnifiedModal` 新增 `closeVariant``closeOnEscape``titleClassName``descriptionClassName`,用于在收口弹窗壳层时保留个人中心 `profile / profileCompact` 关闭按钮、居中浮层布局和标题层级。上述弹窗统一通过 `closeOnBackdrop={false}``closeOnEscape={false}` 保持原有交互语义,不把 backdrop / Escape 关闭行为悄悄带进个人中心;泥点账单这类头部结构和 footer 语义明显不同的弹窗继续保留专用实现,等出现更多同构 case 再扩充 `UnifiedModal` 能力。
19.3.3. 个人中心支付结果提示和支付确认遮罩迁移到 `UnifiedModal` 的 headerless 模式;`UnifiedModal` 新增 `showHeader`,用于在保留 `role="dialog"`、可访问名称、遮罩和 z-index 语义的同时,允许业务页自己排版 icon badge、轻量标题和正文。支付结果提示与确认遮罩统一使用 `showHeader={false}``showCloseButton={false}``closeOnBackdrop={false}``closeOnEscape={false}`,继续保持阻断式确认语义;业务页只保留图标、文案和按钮,不再手写 backdrop、dialog aria 和面板壳层。 19.3.3. 个人中心支付结果提示和支付确认遮罩迁移到 `UnifiedModal` 的 headerless 模式;`UnifiedModal` 新增 `showHeader`,用于在保留 `role="dialog"`、可访问名称、遮罩和 z-index 语义的同时,允许业务页自己排版 icon badge、轻量标题和正文。支付结果提示与确认遮罩统一使用 `showHeader={false}``showCloseButton={false}``closeOnBackdrop={false}``closeOnEscape={false}`,继续保持阻断式确认语义;业务页只保留图标、文案和按钮,不再手写 backdrop、dialog aria 和面板壳层。
19.3.4. 个人中心移动端顶栏的“扫码”“打开设置”入口迁移到 `PlatformIconButton`;页面继续保留 `.platform-profile-header__icon-button` 局部 class 控制位置、尺寸和主题色,交互语义与可访问名称统一由共享按钮承接,不再在 `RpgEntryHomeView` 里手写图标按钮的 `type``aria-label` 和基础 chrome。 19.3.4. 个人中心移动端顶栏的“扫码”“打开设置”入口迁移到 `PlatformIconButton`;页面继续保留 `.platform-profile-header__icon-button` 局部 class 控制位置、尺寸和主题色,交互语义与可访问名称统一由共享按钮承接,不再在 `RpgEntryHomeView` 里手写图标按钮的 `type``aria-label` 和基础 chrome。
19.3.5. 发现页分类筛选弹窗与个人中心扫码面板迁移到 `UnifiedModal`;分类筛选继续复用本地选项栅格和底部动作区样式,但 backdrop、dialog 语义、头部关闭入口和 `closeOnEscape={false}` 统一收口到共享壳层。扫码面板复用 `showHeader={false}` 模式保留深色自定义头部、摄像头 viewport 和状态提示,同时显式保持 `closeOnBackdrop={false}``closeOnEscape={false}`,确保不会把扫码中的资源清理语义改散到页面外层。
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 顶栏按钮壳。
20. 平台方形上传入口和紧凑虚线新增入口迁移到 `PlatformUploadTile`,上传后的图片预览迁移到 `PlatformUploadPreviewCard`;反馈页上传凭证入口 / 预览、敲木鱼工作台新增功德词条入口、通用创作图片面板的提示词参考图缩略图、抓大鹅封面编辑参考图缩略图、通用输入 Composer 已选参考图条、creation-agent 已选参考图条和拼图结果页关卡引用图横条已先迁移。方形缩略图使用默认 `layout="square"`,横向“已选择参考图 / 文件名 / 素材名 / 移除”条使用 `layout="inline"`;只读引用图条不传 `onRemove`,避免公共组件额外渲染删除入口。后续继续收口结果页素材上传、工作台参考图上传、紧凑虚线新增入口等上传 / 动作块时,业务页只保留文件选择、预览数组、预览回调、删除回调、校验逻辑或新增回调,上传方块外观、主副文案、缩略图壳、预览按钮、标题行、横向已选条、移除按钮和禁用态统一由 Module 承接;工具栏中的小图标上传仍继续使用 `PlatformIconButton asChild="label"` 20. 平台方形上传入口和紧凑虚线新增入口迁移到 `PlatformUploadTile`,上传后的图片预览迁移到 `PlatformUploadPreviewCard`;反馈页上传凭证入口 / 预览、敲木鱼工作台新增功德词条入口、通用创作图片面板的提示词参考图缩略图、抓大鹅封面编辑参考图缩略图、通用输入 Composer 已选参考图条、creation-agent 已选参考图条和拼图结果页关卡引用图横条已先迁移。方形缩略图使用默认 `layout="square"`,横向“已选择参考图 / 文件名 / 素材名 / 移除”条使用 `layout="inline"`;只读引用图条不传 `onRemove`,避免公共组件额外渲染删除入口。后续继续收口结果页素材上传、工作台参考图上传、紧凑虚线新增入口等上传 / 动作块时,业务页只保留文件选择、预览数组、预览回调、删除回调、校验逻辑或新增回调,上传方块外观、主副文案、缩略图壳、预览按钮、标题行、横向已选条、移除按钮和禁用态统一由 Module 承接;工具栏中的小图标上传仍继续使用 `PlatformIconButton asChild="label"`

View File

@@ -3086,7 +3086,12 @@ test('profile scan action opens camera scanner instead of recharge panel', async
within(topbar as HTMLElement).getByRole('button', { name: '扫码' }), within(topbar as HTMLElement).getByRole('button', { name: '扫码' }),
); );
expect(await screen.findByRole('dialog', { name: '扫码' })).toBeTruthy(); const qrScannerDialog = await screen.findByRole('dialog', { name: '扫码' });
expect(qrScannerDialog).toBeTruthy();
expect(
within(qrScannerDialog).getByRole('button', { name: '关闭扫码' }),
).toBeTruthy();
await waitFor(() => { await waitFor(() => {
expect(getUserMedia).toHaveBeenCalledWith({ expect(getUserMedia).toHaveBeenCalledWith({
audio: false, audio: false,
@@ -3094,6 +3099,14 @@ test('profile scan action opens camera scanner instead of recharge panel', async
}); });
}); });
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled(); expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
expect(screen.queryByText('账户充值')).toBeNull();
await user.click(within(qrScannerDialog).getByRole('button', { name: '关闭扫码' }));
await waitFor(() => {
expect(screen.queryByRole('dialog', { name: '扫码' })).toBeNull();
});
expect(stopTrack).toHaveBeenCalledTimes(1);
}); });
test('desktop account entry uses saved avatar image when available', async () => { test('desktop account entry uses saved avatar image when available', async () => {
@@ -5294,11 +5307,21 @@ test('mobile game category filter dialog filters by play type', async () => {
const filterDialog = await screen.findByRole('dialog', { const filterDialog = await screen.findByRole('dialog', {
name: '分类筛选', name: '分类筛选',
}); });
const closeButton = within(filterDialog).getByRole('button', {
name: '关闭分类筛选',
});
expect(closeButton.className).toContain('platform-icon-button');
await user.click(within(filterDialog).getByRole('button', { name: '抓鹅' })); await user.click(within(filterDialog).getByRole('button', { name: '抓鹅' }));
expect(screen.queryByRole('button', { name: //u })).toBeNull(); expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.getByRole('button', { name: //u })).toBeTruthy(); expect(screen.getByRole('button', { name: //u })).toBeTruthy();
await user.click(closeButton);
await waitFor(() => {
expect(screen.queryByRole('dialog', { name: '分类筛选' })).toBeNull();
});
}); });
test('bottom category tab becomes ranking and switches ranking metrics', async () => { test('bottom category tab becomes ranking and switches ranking metrics', async () => {

View File

@@ -2326,99 +2326,85 @@ function PlatformCategoryFilterDialog({
onClose: () => void; onClose: () => void;
}) { }) {
return ( return (
<div className="platform-modal-backdrop fixed inset-0 z-[90] flex items-end justify-center px-3 py-4 sm:items-center"> <UnifiedModal
<button open
type="button" title="分类筛选"
aria-label="关闭分类筛选" description={`${resultCount} 个作品`}
className="absolute inset-0" onClose={onClose}
onClick={onClose} closeOnBackdrop
/> closeOnEscape={false}
<div closeLabel="关闭分类筛选"
role="dialog" portal={false}
aria-modal="true" size="sm"
aria-label="分类筛选" zIndexClassName="z-[90]"
className="platform-modal-shell platform-category-filter-dialog relative w-full max-w-md overflow-hidden rounded-[1.35rem]" overlayClassName="platform-modal-backdrop !px-3 !py-4"
> panelClassName="platform-category-filter-dialog relative !rounded-[1.35rem]"
<div className="flex min-w-0 items-center justify-between gap-3 px-4 py-4"> headerClassName="border-b-0 px-4 py-4"
<div className="min-w-0"> titleClassName="text-base font-black"
<div className="text-base font-black text-[var(--platform-text-strong)]"> descriptionClassName="mt-0.5 text-xs font-semibold text-[var(--platform-text-soft)]"
bodyClassName="grid gap-4 !px-4 !pb-4 !pt-0 sm:!px-4 sm:!pb-4 sm:!pt-0"
</div> >
<div className="mt-0.5 text-xs font-semibold text-[var(--platform-text-soft)]"> <div className="grid gap-2">
{resultCount} <div className="text-xs font-black text-[var(--platform-text-soft)]">
</div>
</div>
<PlatformModalCloseButton
label="关闭"
onClick={onClose}
icon={<XCircle className="h-5 w-5" />}
/>
</div> </div>
<div className="platform-category-filter-dialog__options">
{PLATFORM_CATEGORY_KIND_FILTERS.map((option) => {
const active = option.id === kindFilter;
<div className="grid gap-4 px-4 pb-4"> return (
<div className="grid gap-2"> <button
<div className="text-xs font-black text-[var(--platform-text-soft)]"> key={option.id}
type="button"
</div> onClick={() => onKindFilterChange(option.id)}
<div className="platform-category-filter-dialog__options"> className={`platform-category-filter-dialog__option ${active ? 'platform-category-filter-dialog__option--active' : ''}`}
{PLATFORM_CATEGORY_KIND_FILTERS.map((option) => { >
const active = option.id === kindFilter; {option.label}
</button>
return ( );
<button })}
key={option.id}
type="button"
onClick={() => onKindFilterChange(option.id)}
className={`platform-category-filter-dialog__option ${active ? 'platform-category-filter-dialog__option--active' : ''}`}
>
{option.label}
</button>
);
})}
</div>
</div>
<div className="grid gap-2">
<div className="text-xs font-black text-[var(--platform-text-soft)]">
</div>
<div className="platform-category-filter-dialog__options">
{PLATFORM_CATEGORY_SORT_OPTIONS.map((option) => {
const active = option.id === sortMode;
return (
<button
key={option.id}
type="button"
onClick={() => onSortModeChange(option.id)}
className={`platform-category-filter-dialog__option ${active ? 'platform-category-filter-dialog__option--active' : ''}`}
>
{option.label}
</button>
);
})}
</div>
</div>
</div>
<div className="platform-category-filter-dialog__actions">
<button
type="button"
onClick={onReset}
className="platform-category-filter-dialog__action platform-category-filter-dialog__action--secondary"
>
</button>
<button
type="button"
onClick={onClose}
className="platform-category-filter-dialog__action platform-category-filter-dialog__action--primary"
>
</button>
</div> </div>
</div> </div>
</div>
<div className="grid gap-2">
<div className="text-xs font-black text-[var(--platform-text-soft)]">
</div>
<div className="platform-category-filter-dialog__options">
{PLATFORM_CATEGORY_SORT_OPTIONS.map((option) => {
const active = option.id === sortMode;
return (
<button
key={option.id}
type="button"
onClick={() => onSortModeChange(option.id)}
className={`platform-category-filter-dialog__option ${active ? 'platform-category-filter-dialog__option--active' : ''}`}
>
{option.label}
</button>
);
})}
</div>
</div>
<div className="platform-category-filter-dialog__actions">
<button
type="button"
onClick={onReset}
className="platform-category-filter-dialog__action platform-category-filter-dialog__action--secondary"
>
</button>
<button
type="button"
onClick={onClose}
className="platform-category-filter-dialog__action platform-category-filter-dialog__action--primary"
>
</button>
</div>
</UnifiedModal>
); );
} }
@@ -3866,53 +3852,60 @@ function ProfileQrScannerModal({
}, [onError, onResult]); }, [onError, onResult]);
return ( return (
<div <UnifiedModal
className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6" open
role="dialog" title="扫码"
aria-modal="true" onClose={onClose}
aria-label="扫码" showHeader={false}
showCloseButton={false}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[80]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-qr-scanner-modal !max-w-sm rounded-[1.4rem]"
bodyClassName="!p-0"
> >
<div className="platform-qr-scanner-modal w-full max-w-sm overflow-hidden rounded-[1.4rem]"> <div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4"> <div className="text-base font-black"></div>
<div className="text-base font-black"></div> <PlatformModalCloseButton
<PlatformModalCloseButton label="关闭扫码"
label="关闭扫码" onClick={onClose}
onClick={onClose} icon="×"
icon="×" />
/>
</div>
<div className="space-y-3 px-5 py-5">
<div className="platform-qr-scanner-modal__viewport">
<video
ref={videoRef}
className="h-full w-full object-cover"
playsInline
muted
/>
<span className="platform-qr-scanner-modal__frame" />
</div>
{result ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{result}
</PlatformStatusMessage>
) : error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</div> </div>
</div> <div className="space-y-3 px-5 py-5">
<div className="platform-qr-scanner-modal__viewport">
<video
ref={videoRef}
className="h-full w-full object-cover"
playsInline
muted
/>
<span className="platform-qr-scanner-modal__frame" />
</div>
{result ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{result}
</PlatformStatusMessage>
) : error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</UnifiedModal>
); );
} }