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