新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
196 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|