收口轻量支付弹窗与个人中心图标按钮

UnifiedModal 新增无头部模式并补齐对应可访问性测试
RpgEntryHomeView 的支付结果提示、支付确认遮罩与个人中心顶栏图标按钮改用共享组件
更新 PlatformUiKit 收口计划与 .hermes 决策记录
This commit is contained in:
2026-06-10 17:15:43 +08:00
parent ba5f84d963
commit 701fd42777
6 changed files with 147 additions and 100 deletions

View File

@@ -63,6 +63,28 @@ test('supports disabling escape close while keeping the custom close button chro
expect(screen.getByRole('dialog', { name: '个人中心弹窗' })).toBeTruthy();
});
test('supports headerless dialogs while preserving the accessible name', () => {
render(
<UnifiedModal
open
title="支付成功"
description="账户状态已刷新"
onClose={() => {}}
showHeader={false}
showCloseButton={false}
portal={false}
>
<div></div>
</UnifiedModal>,
);
const dialog = screen.getByRole('dialog', { name: '支付成功' });
expect(dialog).toBeTruthy();
expect(screen.queryByRole('button', { name: '关闭' })).toBeNull();
expect(screen.getByText('窗口内容')).toBeTruthy();
});
test('respects closeDisabled for every default close path', () => {
const onClose = vi.fn();
render(

View File

@@ -25,6 +25,7 @@ type UnifiedModalProps = {
onClose: () => void;
variant?: UnifiedModalVariant;
size?: UnifiedModalSize;
showHeader?: boolean;
closeDisabled?: boolean;
closeOnBackdrop?: boolean;
closeOnEscape?: boolean;
@@ -86,6 +87,7 @@ function UnifiedModalContent({
onClose,
variant = 'platform',
size = 'md',
showHeader = true,
closeDisabled = false,
closeOnBackdrop = true,
closeOnEscape = true,
@@ -173,42 +175,51 @@ function UnifiedModalContent({
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-labelledby={showHeader ? titleId : undefined}
aria-label={showHeader ? undefined : title}
aria-describedby={description ? descriptionId : undefined}
className={joinClassNames(panelClasses, sizeClassName, panelClassName)}
style={getPanelStyle(variant, panelStyle)}
onClick={(event) => event.stopPropagation()}
>
<div className={joinClassNames(headerClasses, headerClassName)}>
<div className="min-w-0">
<div
id={titleId}
className={joinClassNames(titleClasses, titleClassName)}
>
{title}
</div>
{description ? (
{showHeader ? (
<div className={joinClassNames(headerClasses, headerClassName)}>
<div className="min-w-0">
<div
id={descriptionId}
className={joinClassNames(
descriptionClasses,
descriptionClassName,
)}
id={titleId}
className={joinClassNames(titleClasses, titleClassName)}
>
{description}
{title}
</div>
{description ? (
<div
id={descriptionId}
className={joinClassNames(
descriptionClasses,
descriptionClassName,
)}
>
{description}
</div>
) : null}
</div>
{showCloseButton ? (
<PlatformModalCloseButton
label={closeLabel}
onClick={onClose}
disabled={closeDisabled}
variant={closeVariant ?? (isPixel ? 'pixel' : 'platformIcon')}
/>
) : null}
</div>
{showCloseButton ? (
<PlatformModalCloseButton
label={closeLabel}
onClick={onClose}
disabled={closeDisabled}
variant={closeVariant ?? (isPixel ? 'pixel' : 'platformIcon')}
/>
) : null}
</div>
) : null}
<div className={joinClassNames(bodyClasses, bodyClassName)}>
{/* 无头部弹窗仍需要把描述挂到 aria-describedby避免只剩可访问名称没有上下文。 */}
{!showHeader && description ? (
<div id={descriptionId} className="sr-only">
{description}
</div>
) : null}
{children}
</div>
{footer ? (

View File

@@ -2903,12 +2903,20 @@ test('mobile profile page matches the reference layout sections', async () => {
const topbar = container.querySelector('.platform-mobile-topbar');
expect(topbar).toBeTruthy();
expect(
within(topbar as HTMLElement).getByRole('button', { name: '扫码' }),
).toBeTruthy();
expect(
within(topbar as HTMLElement).getByRole('button', { name: '打开设置' }),
).toBeTruthy();
const scanButton = within(topbar as HTMLElement).getByRole('button', {
name: '扫码',
});
const settingsButton = within(topbar as HTMLElement).getByRole('button', {
name: '打开设置',
});
expect(scanButton).toBeTruthy();
expect(settingsButton).toBeTruthy();
expect(scanButton.className).toContain('platform-icon-button');
expect(scanButton.className).toContain('platform-profile-header__icon-button');
expect(settingsButton.className).toContain('platform-icon-button');
expect(settingsButton.className).toContain(
'platform-profile-header__icon-button',
);
expect(
within(topbar as HTMLElement).queryByRole('button', {
name: //u,

View File

@@ -3337,69 +3337,76 @@ function RechargePaymentResultModal({
: 'text-[var(--platform-button-danger-text)]';
return (
<div className="platform-modal-backdrop fixed inset-0 z-[90] flex items-center justify-center px-4 py-6">
<div
role="dialog"
aria-modal="true"
aria-labelledby="recharge-payment-result-title"
className="platform-modal-shell platform-remap-surface w-full max-w-sm overflow-hidden rounded-[1.4rem]"
>
<div className="px-5 pb-5 pt-6 text-center">
<PlatformIconBadge
icon={<Icon className="h-8 w-8" aria-hidden="true" />}
size="xl"
tone="neutral"
className={`mx-auto bg-white/10 ${iconClass}`}
/>
<div
id="recharge-payment-result-title"
className="mt-4 text-xl font-black text-[var(--platform-text-strong)]"
>
{result.title}
</div>
<div className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-soft)]">
{result.message}
</div>
<PlatformActionButton
surface="profile"
fullWidth
size="md"
className="mt-5"
onClick={onClose}
>
</PlatformActionButton>
</div>
<UnifiedModal
open
title={result.title}
onClose={onClose}
showHeader={false}
showCloseButton={false}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[90]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-remap-surface !max-w-sm rounded-[1.4rem]"
bodyClassName="px-5 pb-5 pt-6 text-center"
>
<PlatformIconBadge
icon={<Icon className="h-8 w-8" aria-hidden="true" />}
size="xl"
tone="neutral"
className={`mx-auto bg-white/10 ${iconClass}`}
/>
<div className="mt-4 text-xl font-black text-[var(--platform-text-strong)]">
{result.title}
</div>
</div>
<div className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-soft)]">
{result.message}
</div>
<PlatformActionButton
surface="profile"
fullWidth
size="md"
className="mt-5"
onClick={onClose}
>
</PlatformActionButton>
</UnifiedModal>
);
}
function RechargePaymentConfirmationMask({ orderId }: { orderId: string }) {
return (
<div className="platform-modal-backdrop fixed inset-0 z-[95] flex items-center justify-center px-4 py-6">
<div
role="dialog"
aria-modal="true"
aria-label="正在确认支付"
className="platform-modal-shell platform-remap-surface w-full max-w-sm overflow-hidden rounded-[1.4rem]"
>
<div className="px-5 pb-5 pt-6 text-center">
<PlatformIconBadge
icon={<Loader2 className="h-8 w-8 animate-spin" aria-hidden="true" />}
size="xl"
tone="neutral"
className="mx-auto bg-white/10 text-[var(--platform-accent)]"
/>
<div className="mt-4 text-xl font-black text-[var(--platform-text-strong)]">
</div>
<div className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-soft)]">
{orderId}
</div>
</div>
<UnifiedModal
open
title="正在确认支付"
onClose={() => undefined}
showHeader={false}
showCloseButton={false}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName="z-[95]"
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
panelClassName="platform-remap-surface !max-w-sm rounded-[1.4rem]"
bodyClassName="px-5 pb-5 pt-6 text-center"
>
<PlatformIconBadge
icon={<Loader2 className="h-8 w-8 animate-spin" aria-hidden="true" />}
size="xl"
tone="neutral"
className="mx-auto bg-white/10 text-[var(--platform-accent)]"
/>
<div className="mt-4 text-xl font-black text-[var(--platform-text-strong)]">
</div>
</div>
<div className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-soft)]">
{orderId}
</div>
</UnifiedModal>
);
}
@@ -7402,22 +7409,18 @@ export function RpgEntryHomeView({
<RpgEntryBrandLogo />
{isAuthenticated && activeTab === 'profile' ? (
<div className="flex items-center gap-2.5">
<button
type="button"
<PlatformIconButton
label="扫码"
icon={<ScanLine className="h-5 w-5" />}
onClick={openQrScannerPanel}
className="platform-profile-header__icon-button"
aria-label="扫码"
>
<ScanLine className="h-5 w-5" />
</button>
<button
type="button"
/>
<PlatformIconButton
label="打开设置"
icon={<Settings className="h-5 w-5" />}
onClick={() => authUi?.openSettingsModal()}
className="platform-profile-header__icon-button"
aria-label="打开设置"
>
<Settings className="h-5 w-5" />
</button>
/>
</div>
) : isAuthenticated &&
(activeTab === 'create' || activeTab === 'saves') ? (