Files
Genarrative/src/components/common/PlatformUploadPreviewCard.tsx
kdletters 1ad25e30f8 收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
2026-06-10 10:24:18 +08:00

196 lines
5.5 KiB
TypeScript

import { X } from 'lucide-react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { PlatformIconButton } from './PlatformIconButton';
import { PlatformSubpanel } from './PlatformSubpanel';
type PlatformUploadPreviewCardProps = {
imageSrc: string;
imageAlt: string;
imageRefreshKey?: string | number | null;
removeLabel: string;
layout?: 'square' | 'inline';
surface?: 'platform' | 'editorDark';
caption?: ReactNode;
previewLabel?: string;
onPreview?: () => void;
onRemove?: () => void;
disabled?: boolean;
resolveAsset?: boolean;
className?: string;
imageClassName?: string;
imageShellClassName?: string;
captionClassName?: string;
removeIcon?: ReactNode;
previewButtonProps?: Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'aria-label' | 'children' | 'disabled' | 'onClick' | 'type'
>;
removeButtonProps?: Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'aria-label' | 'children' | 'disabled' | 'onClick' | 'type'
>;
};
/**
* 平台上传预览卡片。
* 统一承载上传后缩略图、预览壳和右上角移除按钮。
*/
export function PlatformUploadPreviewCard({
imageSrc,
imageAlt,
imageRefreshKey = null,
removeLabel,
layout = 'square',
surface = 'platform',
caption,
previewLabel,
onPreview,
onRemove,
disabled = false,
resolveAsset = false,
className,
imageClassName,
imageShellClassName,
captionClassName,
removeIcon = <X className="h-3 w-3" />,
previewButtonProps,
removeButtonProps,
}: PlatformUploadPreviewCardProps) {
const { className: previewButtonClassName, ...restPreviewButtonProps } =
previewButtonProps ?? {};
const { className: removeButtonClassName, ...restRemoveButtonProps } =
removeButtonProps ?? {};
const inline = layout === 'inline';
const editorDark = surface === 'editorDark';
const imageClassNames = ['h-full w-full object-cover', imageClassName]
.filter(Boolean)
.join(' ');
const imageShellClassNames = [
inline
? [
'h-12 w-12 shrink-0 overflow-hidden rounded-[0.8rem]',
editorDark
? 'border border-white/10 bg-black/30'
: 'bg-[var(--platform-track-fill)]',
].join(' ')
: caption
? 'aspect-square overflow-hidden'
: 'h-full w-full',
imageShellClassName,
]
.filter(Boolean)
.join(' ');
const captionClassNames = [
'truncate px-2 py-2 pr-8 text-[11px] font-semibold',
editorDark ? 'text-zinc-300' : 'text-[var(--platform-text-base)]',
captionClassName,
]
.filter(Boolean)
.join(' ');
const previewActionLabel = previewLabel ?? imageAlt;
const imageElement = resolveAsset ? (
<ResolvedAssetImage
src={imageSrc}
refreshKey={imageRefreshKey}
alt={imageAlt}
className={imageClassNames}
/>
) : (
<img src={imageSrc} alt={imageAlt} className={imageClassNames} />
);
const imageContent =
caption || imageShellClassName || inline ? (
<div className={imageShellClassNames}>{imageElement}</div>
) : (
imageElement
);
const squareRootClassName =
surface === 'editorDark'
? 'relative h-[5.75rem] w-[5.75rem] overflow-hidden rounded-xl border border-white/10 bg-black/25'
: 'relative h-[5.75rem] w-[5.75rem] overflow-hidden rounded-xl border border-[var(--platform-subpanel-border)] bg-[var(--platform-input-fill)]';
const cardContent = (
<>
{onPreview ? (
<button
{...restPreviewButtonProps}
type="button"
aria-label={previewActionLabel}
title={restPreviewButtonProps.title ?? previewActionLabel}
disabled={disabled}
onClick={onPreview}
className={[
caption ? 'block w-full' : 'block h-full w-full',
'disabled:cursor-not-allowed disabled:opacity-55',
previewButtonClassName,
]
.filter(Boolean)
.join(' ')}
>
{imageContent}
</button>
) : (
imageContent
)}
{caption ? (
<div
className={
inline
? [
'min-w-0 flex-1 truncate text-sm font-semibold',
editorDark
? 'text-zinc-300'
: 'text-[var(--platform-text-strong)]',
captionClassName,
]
.filter(Boolean)
.join(' ')
: captionClassNames
}
>
{caption}
</div>
) : null}
{onRemove ? (
<PlatformIconButton
{...restRemoveButtonProps}
label={removeLabel}
icon={removeIcon}
variant={inline && !editorDark ? 'platformIcon' : 'darkMini'}
disabled={disabled}
onClick={onRemove}
className={[
inline ? 'h-9 w-9 shrink-0' : 'absolute right-1 top-1 h-5 w-5',
removeButtonClassName,
]
.filter(Boolean)
.join(' ')}
/>
) : null}
</>
);
if (inline) {
return (
<PlatformSubpanel
as="div"
surface={editorDark ? 'dark' : 'soft'}
radius="sm"
padding="row"
className={['flex items-center gap-3', className]
.filter(Boolean)
.join(' ')}
>
{cardContent}
</PlatformSubpanel>
);
}
return (
<div className={[squareRootClassName, className].filter(Boolean).join(' ')}>
{cardContent}
</div>
);
}