收口个人中心状态弹层与扫码组件
新增 PlatformStatusDialog 统一支付结果与确认中状态弹层 新增 PlatformProfileQrScannerModal 统一个人中心扫码面板 RpgEntryHomeView 改用共享组件并删除内联支付与扫码实现 更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
67
src/components/common/PlatformStatusDialog.test.tsx
Normal file
67
src/components/common/PlatformStatusDialog.test.tsx
Normal 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();
|
||||
});
|
||||
154
src/components/common/PlatformStatusDialog.tsx
Normal file
154
src/components/common/PlatformStatusDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user