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

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

@@ -3086,7 +3086,12 @@ test('profile scan action opens camera scanner instead of recharge panel', async
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(() => {
expect(getUserMedia).toHaveBeenCalledWith({
audio: false,
@@ -3094,6 +3099,14 @@ test('profile scan action opens camera scanner instead of recharge panel', async
});
});
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 () => {
@@ -5294,11 +5307,21 @@ test('mobile game category filter dialog filters by play type', async () => {
const filterDialog = await screen.findByRole('dialog', {
name: '分类筛选',
});
const closeButton = within(filterDialog).getByRole('button', {
name: '关闭分类筛选',
});
expect(closeButton.className).toContain('platform-icon-button');
await user.click(within(filterDialog).getByRole('button', { name: '抓鹅' }));
expect(screen.queryByRole('button', { name: //u })).toBeNull();
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 () => {

View File

@@ -2326,99 +2326,85 @@ function PlatformCategoryFilterDialog({
onClose: () => void;
}) {
return (
<div className="platform-modal-backdrop fixed inset-0 z-[90] flex items-end justify-center px-3 py-4 sm:items-center">
<button
type="button"
aria-label="关闭分类筛选"
className="absolute inset-0"
onClick={onClose}
/>
<div
role="dialog"
aria-modal="true"
aria-label="分类筛选"
className="platform-modal-shell platform-category-filter-dialog relative w-full max-w-md overflow-hidden rounded-[1.35rem]"
>
<div className="flex min-w-0 items-center justify-between gap-3 px-4 py-4">
<div className="min-w-0">
<div className="text-base font-black text-[var(--platform-text-strong)]">
</div>
<div className="mt-0.5 text-xs font-semibold text-[var(--platform-text-soft)]">
{resultCount}
</div>
</div>
<PlatformModalCloseButton
label="关闭"
onClick={onClose}
icon={<XCircle className="h-5 w-5" />}
/>
<UnifiedModal
open
title="分类筛选"
description={`${resultCount} 个作品`}
onClose={onClose}
closeOnBackdrop
closeOnEscape={false}
closeLabel="关闭分类筛选"
portal={false}
size="sm"
zIndexClassName="z-[90]"
overlayClassName="platform-modal-backdrop !px-3 !py-4"
panelClassName="platform-category-filter-dialog relative !rounded-[1.35rem]"
headerClassName="border-b-0 px-4 py-4"
titleClassName="text-base font-black"
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 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_KIND_FILTERS.map((option) => {
const active = option.id === kindFilter;
<div className="grid gap-4 px-4 pb-4">
<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_KIND_FILTERS.map((option) => {
const active = option.id === kindFilter;
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>
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>
<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]);
return (
<div
className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6"
role="dialog"
aria-modal="true"
aria-label="扫码"
<UnifiedModal
open
title="扫码"
onClose={onClose}
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="text-base font-black"></div>
<PlatformModalCloseButton
label="关闭扫码"
onClick={onClose}
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 className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div className="text-base font-black"></div>
<PlatformModalCloseButton
label="关闭扫码"
onClick={onClose}
icon="×"
/>
</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>
);
}