继续收口视觉小说素材选择弹窗

视觉小说素材选择弹窗复用 PlatformToolModalShell

保留上传、AI生成、历史素材读取和选择回调

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

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

View File

@@ -113,6 +113,11 @@ test('visual novel upload-only asset picker uses PlatformEmptyState chrome', asy
const uploadOnlyEmptyState =
within(pickerDialog).getByText('选择本地文件上传');
expect(pickerDialog.className).toContain('platform-remap-surface');
expect(pickerDialog.className).toContain(
'shadow-[0_24px_80px_rgba(0,0,0,0.55)]',
);
expect(pickerDialog.className).toContain('!max-w-4xl');
expect(uploadOnlyEmptyState.className).toContain('border-dashed');
expect(uploadOnlyEmptyState.className).toContain('rounded-[1.35rem]');
expect(uploadOnlyEmptyState.className).toContain('min-h-24');

View File

@@ -51,6 +51,7 @@ import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformToolModalShell } from '../common/PlatformToolModalShell';
import {
PlatformSelectField,
PlatformTextField,
@@ -409,7 +410,6 @@ function VisualNovelAssetPickerDialog({
onSelect: (asset: VisualNovelAssetReference) => void;
}) {
const authUi = useAuthUi();
const platformTheme = authUi?.platformTheme ?? 'light';
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [historyAssets, setHistoryAssets] = useState<
VisualNovelAssetReference[]
@@ -527,126 +527,100 @@ function VisualNovelAssetPickerDialog({
}
};
if (typeof document === 'undefined') {
return null;
}
return createPortal(
<div
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[170] 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={config.title}
onClose={onClose}
closeLabel="关闭"
zIndexClassName="z-[170]"
size="xl"
panelClassName="!max-h-[min(92vh,42rem)] !max-w-4xl"
>
<section
role="dialog"
aria-modal="true"
aria-label={config.title}
className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,42rem)] w-full max-w-4xl flex-col overflow-hidden rounded-t-[1.45rem] sm:rounded-[1.45rem]"
onClick={(event) => event.stopPropagation()}
>
<header className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-4 py-3">
<h2 className="min-w-0 truncate text-base font-black text-[var(--platform-text-strong)]">
{config.title}
</h2>
<PlatformModalCloseButton
label="关闭"
variant="platformIcon"
className="h-9 w-9"
onClick={onClose}
title="关闭"
/>
</header>
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
<div className="mb-4 flex flex-wrap gap-2">
<PlatformActionButton
tone="secondary"
disabled={disabled || isUploading || isGeneratingImage}
onClick={() => fileInputRef.current?.click()}
className="min-h-10"
>
{isUploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Upload className="h-4 w-4" />
)}
</PlatformActionButton>
{config.imageGeneratorConfig && config.previewTone === 'image' ? (
<PlatformActionButton
disabled={disabled || isUploading || isGeneratingImage}
onClick={() => {
void handleGenerateImage();
}}
className="min-h-10"
>
{isGeneratingImage ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4" />
)}
AI生成
</PlatformActionButton>
) : null}
<input
ref={fileInputRef}
type="file"
accept={config.accept}
disabled={disabled || isUploading || isGeneratingImage}
onChange={(event) => {
void handleUpload(event);
}}
className="hidden"
aria-label={`上传${config.title}文件`}
/>
</div>
<div className="mb-4 flex flex-wrap gap-2">
<PlatformActionButton
tone="secondary"
disabled={disabled || isUploading || isGeneratingImage}
onClick={() => fileInputRef.current?.click()}
className="min-h-10"
>
{isUploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Upload className="h-4 w-4" />
)}
</PlatformActionButton>
{config.imageGeneratorConfig && config.previewTone === 'image' ? (
<PlatformActionButton
disabled={disabled || isUploading || isGeneratingImage}
onClick={() => {
void handleGenerateImage();
}}
className="min-h-10"
>
{isGeneratingImage ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4" />
)}
AI生成
</PlatformActionButton>
) : null}
<input
ref={fileInputRef}
type="file"
accept={config.accept}
disabled={disabled || isUploading || isGeneratingImage}
onChange={(event) => {
void handleUpload(event);
}}
className="hidden"
aria-label={`上传${config.title}文件`}
/>
</div>
{!config.historyKind ? (
<PlatformEmptyState
surface="dashed"
size="inline"
tone="base"
className="mb-4 min-h-24"
>
</PlatformEmptyState>
) : null}
{!config.historyKind ? (
<PlatformEmptyState
surface="dashed"
size="inline"
tone="base"
className="mb-4 min-h-24"
>
</PlatformEmptyState>
) : null}
{error ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="md"
className="mb-4 rounded-2xl"
>
{error}
</PlatformStatusMessage>
) : null}
{error ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="md"
className="mb-4 rounded-2xl"
>
{error}
</PlatformStatusMessage>
) : null}
{config.historyKind ? (
<PlatformAssetPickerGrid
items={historyAssets}
isLoading={isLoadingHistory}
error={null}
loadingLabel="读取中..."
emptyLabel="暂无历史素材"
disabled={disabled}
getKey={(asset) => asset.assetObjectId}
getImageSrc={(asset) => asset.imageSrc}
getImageAlt={(asset) => asset.ownerLabel || '历史素材'}
getTitle={(asset) => asset.ownerLabel || '未记录账号'}
getSubtitle={(asset) => formatHistoryAssetDate(asset.createdAt)}
onSelect={onSelect}
gridClassName="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4"
emptyClassName="min-h-40 rounded-[1.15rem] bg-white/56"
/>
) : null}
</div>
</section>
</div>,
document.body,
{config.historyKind ? (
<PlatformAssetPickerGrid
items={historyAssets}
isLoading={isLoadingHistory}
error={null}
loadingLabel="读取中..."
emptyLabel="暂无历史素材"
disabled={disabled}
getKey={(asset) => asset.assetObjectId}
getImageSrc={(asset) => asset.imageSrc}
getImageAlt={(asset) => asset.ownerLabel || '历史素材'}
getTitle={(asset) => asset.ownerLabel || '未记录账号'}
getSubtitle={(asset) => formatHistoryAssetDate(asset.createdAt)}
onSelect={onSelect}
gridClassName="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4"
emptyClassName="min-h-40 rounded-[1.15rem] bg-white/56"
/>
) : null}
</PlatformToolModalShell>
);
}