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

164 lines
4.1 KiB
TypeScript

import {
forwardRef,
type HTMLAttributes,
type ImgHTMLAttributes,
type ReactNode,
} from 'react';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
type PlatformMediaFrameAspect =
| 'auto'
| 'square'
| 'standard'
| 'landscape'
| 'wide'
| 'portrait'
| 'video';
type PlatformMediaFrameSurface =
| 'warm'
| 'editorDark'
| 'plain'
| 'soft'
| 'bright'
| 'none'
| 'bare';
type PlatformMediaFrameProps = Omit<
HTMLAttributes<HTMLDivElement>,
'children' | 'className'
> & {
src?: string | null;
fallbackSrc?: string | null;
alt: string;
fallbackLabel: string;
aspect?: PlatformMediaFrameAspect;
surface?: PlatformMediaFrameSurface;
loading?: 'eager' | 'lazy';
refreshKey?: string | number | null;
imageClassName?: string;
imageProps?: Omit<
ImgHTMLAttributes<HTMLImageElement>,
'alt' | 'className' | 'loading' | 'src'
>;
className?: string;
fallbackClassName?: string;
fallbackShellClassName?: string;
fallbackContent?: ReactNode;
children?: ReactNode;
previewOverlay?: ReactNode;
overlayInteractive?: boolean;
};
const PLATFORM_MEDIA_FRAME_ASPECT_CLASS: Record<
PlatformMediaFrameAspect,
string
> = {
auto: '',
square: 'aspect-square',
standard: 'aspect-[4/3]',
landscape: 'aspect-[16/9]',
wide: 'aspect-[9/5]',
portrait: 'aspect-[9/16]',
video: 'aspect-video',
};
const PLATFORM_MEDIA_FRAME_SURFACE_CLASS: Record<
PlatformMediaFrameSurface,
string
> = {
warm: 'border border-[var(--platform-subpanel-border)] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.22),transparent_42%),linear-gradient(180deg,rgba(204,117,76,0.9),rgba(223,127,64,0.82))]',
editorDark:
'border border-white/10 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.16),transparent_48%),linear-gradient(180deg,rgba(19,24,39,0.95),rgba(8,10,17,0.92))]',
plain:
'border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)]',
soft: 'border border-[var(--platform-subpanel-border)] bg-white/68',
bright: 'border border-[var(--platform-subpanel-border)] bg-white/82',
none: '',
bare: 'bg-[var(--platform-subpanel-fill)]',
};
/**
* 平台媒体预览框。
* 统一承接图片预览、固定比例、fallback 文案和可选 overlay。
*/
export const PlatformMediaFrame = forwardRef<
HTMLDivElement,
PlatformMediaFrameProps
>(function PlatformMediaFrame(
{
src,
fallbackSrc,
alt,
fallbackLabel,
aspect = 'square',
surface = 'warm',
loading,
refreshKey,
imageClassName = 'h-full w-full object-cover',
imageProps,
className,
fallbackClassName,
fallbackShellClassName,
fallbackContent,
children,
previewOverlay,
overlayInteractive = false,
...containerProps
},
ref,
) {
const imageSrc = src?.trim() || fallbackSrc?.trim() || '';
const hasOverlay = Boolean(children) || Boolean(previewOverlay);
return (
<div
{...containerProps}
ref={ref}
className={[
'platform-media-frame relative overflow-hidden rounded-2xl',
PLATFORM_MEDIA_FRAME_SURFACE_CLASS[surface],
PLATFORM_MEDIA_FRAME_ASPECT_CLASS[aspect],
className,
]
.filter(Boolean)
.join(' ')}
>
{imageSrc ? (
<ResolvedAssetImage
{...imageProps}
src={src?.trim() || undefined}
fallbackSrc={fallbackSrc?.trim() || undefined}
alt={alt}
loading={loading}
refreshKey={refreshKey}
className={imageClassName}
/>
) : (
<div
className={[
'flex h-full w-full items-center justify-center px-4 text-center text-sm font-semibold tracking-[0.18em] text-zinc-400',
fallbackShellClassName,
fallbackClassName,
]
.filter(Boolean)
.join(' ')}
>
{fallbackContent ?? fallbackLabel}
</div>
)}
{hasOverlay ? (
<div
className={[
overlayInteractive ? 'pointer-events-auto' : 'pointer-events-none',
'absolute inset-0',
].join(' ')}
>
{previewOverlay}
{children}
</div>
) : null}
</div>
);
});