收口个人中心状态弹层与扫码组件

新增 PlatformStatusDialog 统一支付结果与确认中状态弹层
新增 PlatformProfileQrScannerModal 统一个人中心扫码面板
RpgEntryHomeView 改用共享组件并删除内联支付与扫码实现
更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-10 20:24:09 +08:00
parent 914b74ce8e
commit f54c3ee936
7 changed files with 583 additions and 260 deletions

View File

@@ -0,0 +1,67 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from '@testing-library/react';
import { expect, test, vi } from 'vitest';
import { PlatformStatusDialog } from './PlatformStatusDialog';
test('renders result state with description and primary action', () => {
const onClose = vi.fn();
const onAction = vi.fn();
render(
<PlatformStatusDialog
status="success"
title="支付成功"
description="账户状态已刷新"
onClose={onClose}
action={{ label: '知道了', onClick: onAction }}
/>,
);
const dialog = screen.getByRole('dialog', { name: '支付成功' });
const badge = dialog.querySelector('.platform-icon-badge');
const action = screen.getByRole('button', { name: '知道了' });
const visibleDescription = dialog.querySelector(
'.mt-3.text-sm.font-semibold.leading-6.text-\\[var\\(--platform-text-soft\\)\\]',
);
expect(dialog).toBeTruthy();
expect(visibleDescription?.textContent).toBe('账户状态已刷新');
expect(badge?.className).toContain('text-[var(--platform-success-text)]');
expect(action.className).toContain('platform-primary-button');
fireEvent.click(action);
expect(onAction).toHaveBeenCalledTimes(1);
});
test('supports blocking confirming state without close action', () => {
const onClose = vi.fn();
render(
<PlatformStatusDialog
status="confirming"
title="正在确认支付"
description="订单 A100 正在同步到账状态,请先停留在当前页面。"
onClose={onClose}
closeDisabled
zIndexClassName="z-[95]"
/>,
);
const dialog = screen.getByRole('dialog', { name: '正在确认支付' });
const overlay = dialog.parentElement as HTMLElement;
const spinner = dialog.querySelector('.platform-icon-badge svg');
expect(overlay.className).toContain('z-[95]');
expect(spinner?.getAttribute('class')).toContain('animate-spin');
expect(
screen.queryByRole('button', { name: '关闭' }) ||
screen.queryByRole('button', { name: '知道了' }),
).toBeNull();
fireEvent.click(overlay);
fireEvent.keyDown(window, { key: 'Escape' });
expect(onClose).not.toHaveBeenCalled();
});

View File

@@ -0,0 +1,154 @@
import type { ReactNode } from 'react';
import {
AlertCircle,
CheckCircle2,
Loader2,
XCircle,
} from 'lucide-react';
import { PlatformActionButton } from './PlatformActionButton';
import { PlatformIconBadge } from './PlatformIconBadge';
import { UnifiedModal } from './UnifiedModal';
export type PlatformStatusDialogStatus =
| 'success'
| 'cancel'
| 'error'
| 'loading'
| 'confirming';
type PlatformStatusDialogAction = {
label: string;
onClick: () => void;
disabled?: boolean;
};
type PlatformStatusDialogProps = {
open?: boolean;
status: PlatformStatusDialogStatus;
title: string;
description?: ReactNode;
onClose: () => void;
action?: PlatformStatusDialogAction;
closeDisabled?: boolean;
zIndexClassName?: string;
overlayClassName?: string;
panelClassName?: string;
bodyClassName?: string;
iconClassName?: string;
};
type PlatformStatusVisualConfig = {
icon: ReactNode;
iconClassName: string;
};
const DEFAULT_OVERLAY_CLASS =
'platform-profile-modal-overlay bg-slate-950/72 backdrop-blur-xl';
const DEFAULT_PANEL_CLASS =
'platform-remap-surface !max-w-sm rounded-[1.4rem]';
const DEFAULT_BODY_CLASS = 'px-5 pb-5 pt-6 text-center';
function getStatusVisualConfig(
status: PlatformStatusDialogStatus,
): PlatformStatusVisualConfig {
switch (status) {
case 'success':
return {
icon: <CheckCircle2 className="h-8 w-8" aria-hidden="true" />,
iconClassName: 'text-[var(--platform-success-text)]',
};
case 'cancel':
return {
icon: <XCircle className="h-8 w-8" aria-hidden="true" />,
iconClassName: 'text-[var(--platform-text-soft)]',
};
case 'error':
return {
icon: <AlertCircle className="h-8 w-8" aria-hidden="true" />,
iconClassName: 'text-[var(--platform-button-danger-text)]',
};
case 'loading':
case 'confirming':
return {
icon: (
<Loader2 className="h-8 w-8 animate-spin" aria-hidden="true" />
),
iconClassName: 'text-[var(--platform-accent)]',
};
}
}
/**
* 平台状态弹窗。
* 收口个人中心这类“状态图标 + 标题正文 + 可选主动作”的无头弹窗模式。
*/
export function PlatformStatusDialog({
open = true,
status,
title,
description,
onClose,
action,
closeDisabled = false,
zIndexClassName = 'z-[90]',
overlayClassName = DEFAULT_OVERLAY_CLASS,
panelClassName = DEFAULT_PANEL_CLASS,
bodyClassName = DEFAULT_BODY_CLASS,
iconClassName,
}: PlatformStatusDialogProps) {
const visualConfig = getStatusVisualConfig(status);
return (
<UnifiedModal
open={open}
title={title}
onClose={onClose}
showHeader={false}
showCloseButton={false}
closeDisabled={closeDisabled}
closeOnBackdrop={false}
closeOnEscape={false}
portal={false}
size="sm"
zIndexClassName={zIndexClassName}
overlayClassName={overlayClassName}
panelClassName={panelClassName}
bodyClassName={bodyClassName}
>
<PlatformIconBadge
icon={visualConfig.icon}
size="xl"
tone="neutral"
className={[
'mx-auto bg-white/10',
visualConfig.iconClassName,
iconClassName,
]
.filter(Boolean)
.join(' ')}
/>
<div className="mt-4 text-xl font-black text-[var(--platform-text-strong)]">
{title}
</div>
{description ? (
<div className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-soft)]">
{description}
</div>
) : null}
{action ? (
<PlatformActionButton
surface="profile"
fullWidth
size="md"
className="mt-5"
onClick={action.onClick}
disabled={action.disabled}
>
{action.label}
</PlatformActionButton>
) : null}
</UnifiedModal>
);
}