收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
195
src/components/common/PlatformUploadPreviewCard.tsx
Normal file
195
src/components/common/PlatformUploadPreviewCard.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user