继续收口方洞图片槽弹窗壳层

方洞结果页图片槽弹窗复用 PlatformToolModalShell

保留上传、AI生成和历史素材选择业务语义

补充组件测试断言共享白底工具弹窗壳层

同步 PlatformUiKit 文档和 Hermes 决策记录
This commit is contained in:
2026-06-11 17:08:22 +08:00
parent a20d7b1655
commit 9b40f6b453
4 changed files with 86 additions and 108 deletions

View File

@@ -297,6 +297,9 @@ test('square hole image dialog reuses platform media frame', async () => {
const mediaFrame = image.closest('div.relative');
expect(dialog).toBeTruthy();
expect(dialog.className).toContain('platform-remap-surface');
expect(dialog.className).toContain('shadow-[0_24px_80px_rgba(0,0,0,0.55)]');
expect(screen.getByText('历史生成')).toBeTruthy();
expect(mediaFrame?.className).toContain('aspect-[4/3]');
expect(mediaFrame?.className).toContain('rounded-[1.35rem]');
expect(mediaFrame?.className).toContain(

View File

@@ -10,7 +10,6 @@ import {
Trash2,
} from 'lucide-react';
import { type ChangeEvent, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import type { SquareHoleResultDraft } from '../../../packages/shared/src/contracts/squareHoleAgent';
import type {
@@ -27,18 +26,17 @@ import {
type SquareHoleImageAssetKind,
updateSquareHoleWork,
} from '../../services/square-hole-works';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformBackActionButton } from '../common/PlatformBackActionButton';
import { PlatformAssetPickerGrid } from '../common/PlatformAssetPickerCard';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatGrid } from '../common/PlatformStatGrid';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformToolModalShell } from '../common/PlatformToolModalShell';
import {
PlatformSelectField,
PlatformTextField,
@@ -501,7 +499,6 @@ function SquareHoleImageSlotDialog({
event: ChangeEvent<HTMLInputElement>,
) => void;
}) {
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
const [assets, setAssets] = useState<SquareHoleHistoryAsset[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -537,115 +534,91 @@ function SquareHoleImageSlotDialog({
};
}, [slot.assetKind]);
if (typeof document === 'undefined') {
return null;
}
return createPortal(
<div
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[145] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
onClick={(event) => {
if (event.target === event.currentTarget) {
onClose();
}
}}
return (
<PlatformToolModalShell
open
title={slot.title}
ariaLabel={`${slot.title}查看`}
onClose={onClose}
closeLabel="关闭"
zIndexClassName="z-[145]"
size="xl"
panelClassName="!max-h-[min(92vh,46rem)]"
>
<div
role="dialog"
aria-modal="true"
aria-label={`${slot.title}查看`}
className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,46rem)] w-full max-w-5xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
{slot.title}
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(16rem,0.78fr)]">
<div className="space-y-3">
<PlatformMediaFrame
src={currentImageSrc}
alt={slot.title}
fallbackLabel={`${slot.title}占位图`}
fallbackContent={<Images className="h-10 w-10" />}
aspect="standard"
surface="plain"
className="rounded-[1.35rem]"
fallbackClassName="tracking-normal text-[var(--platform-text-soft)]"
/>
<div className="grid gap-2 sm:grid-cols-2">
<PlatformActionButton
asChild="label"
tone="ghost"
size="md"
aria-disabled={isBusy}
className={`min-h-11 cursor-pointer gap-2 px-4 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
>
<ImagePlus className="h-4 w-4" />
<input
type="file"
accept="image/*"
className="sr-only"
disabled={isBusy}
onChange={(event) => onUpload(slot, event)}
/>
</PlatformActionButton>
<PlatformActionButton
onClick={onRegenerateImages}
disabled={!canRegenerateImages || isBusy}
tone="secondary"
size="md"
className="min-h-11 gap-2 px-4"
>
{isRegeneratingImages ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
AI生成图片
</PlatformActionButton>
</div>
<PlatformModalCloseButton
onClick={onClose}
label="关闭"
variant="platformIcon"
</div>
<div className="min-h-0 space-y-3">
<PlatformFieldLabel
variant="section"
className="flex items-center gap-2"
>
<Images className="h-3.5 w-3.5" />
</PlatformFieldLabel>
<PlatformAssetPickerGrid
items={assets}
isLoading={isLoading}
error={error}
loadingLabel="读取中..."
emptyLabel="暂无历史图片"
disabled={isBusy}
getKey={(asset) => asset.assetObjectId}
getImageSrc={(asset) => asset.imageSrc}
getImageAlt={(asset) => asset.ownerLabel || '历史图片'}
getTitle={(asset) => asset.ownerLabel || '未记录账号'}
getSubtitle={(asset) => formatHistoryAssetDate(asset.createdAt)}
onSelect={onSelectHistory}
gridClassName="grid max-h-[24rem] grid-cols-2 gap-3 overflow-y-auto pr-1 sm:grid-cols-3 lg:grid-cols-2 xl:grid-cols-3"
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(16rem,0.78fr)]">
<div className="space-y-3">
<PlatformMediaFrame
src={currentImageSrc}
alt={slot.title}
fallbackLabel={`${slot.title}占位图`}
fallbackContent={<Images className="h-10 w-10" />}
aspect="standard"
surface="plain"
className="rounded-[1.35rem]"
fallbackClassName="tracking-normal text-[var(--platform-text-soft)]"
/>
<div className="grid gap-2 sm:grid-cols-2">
<PlatformActionButton
asChild="label"
tone="ghost"
size="md"
aria-disabled={isBusy}
className={`min-h-11 cursor-pointer gap-2 px-4 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
>
<ImagePlus className="h-4 w-4" />
<input
type="file"
accept="image/*"
className="sr-only"
disabled={isBusy}
onChange={(event) => onUpload(slot, event)}
/>
</PlatformActionButton>
<PlatformActionButton
onClick={onRegenerateImages}
disabled={!canRegenerateImages || isBusy}
tone="secondary"
size="md"
className="min-h-11 gap-2 px-4"
>
{isRegeneratingImages ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
AI生成图片
</PlatformActionButton>
</div>
</div>
<div className="min-h-0 space-y-3">
<PlatformFieldLabel
variant="section"
className="flex items-center gap-2"
>
<Images className="h-3.5 w-3.5" />
</PlatformFieldLabel>
<PlatformAssetPickerGrid
items={assets}
isLoading={isLoading}
error={error}
loadingLabel="读取中..."
emptyLabel="暂无历史图片"
disabled={isBusy}
getKey={(asset) => asset.assetObjectId}
getImageSrc={(asset) => asset.imageSrc}
getImageAlt={(asset) => asset.ownerLabel || '历史图片'}
getTitle={(asset) => asset.ownerLabel || '未记录账号'}
getSubtitle={(asset) => formatHistoryAssetDate(asset.createdAt)}
onSelect={onSelectHistory}
gridClassName="grid max-h-[24rem] grid-cols-2 gap-3 overflow-y-auto pr-1 sm:grid-cols-3 lg:grid-cols-2 xl:grid-cols-3"
/>
</div>
</div>
</div>
</div>
</div>,
document.body,
</PlatformToolModalShell>
);
}