gongxiangp2/components/BrandLogo.tsx
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

88 lines
2.4 KiB
TypeScript

'use client'
import Image, { type ImageProps } from 'next/image'
import { useEffect, useState, type SyntheticEvent } from 'react'
import {
BRAND_LOGO_FALLBACK_TEXT,
DEFAULT_HEADER_LOGO_URL,
} from '@/lib/brand-assets'
type BrandLogoProps = Omit<ImageProps, 'src' | 'alt' | 'onError'> & {
src?: string | null
alt?: string
fallbackSrc?: string
fallbackText?: string
onError?: (event: SyntheticEvent<HTMLImageElement>) => void
}
function resolveInitialSource(src: string | null | undefined, fallbackSrc: string) {
return typeof src === 'string' && src.trim() ? src.trim() : fallbackSrc
}
/**
* 品牌 Logo 的单次回退组件:远端失败后只切换一次本地资源;
* 如果本地资源也失败,则显示稳定的文字标记,避免 onError 无限循环。
*/
export default function BrandLogo({
src,
alt = '',
fallbackSrc = DEFAULT_HEADER_LOGO_URL,
fallbackText = BRAND_LOGO_FALLBACK_TEXT,
onError,
className,
style,
width,
height,
...imageProps
}: BrandLogoProps) {
const initialSource = resolveInitialSource(src, fallbackSrc)
const [currentSource, setCurrentSource] = useState(initialSource)
const [showTextFallback, setShowTextFallback] = useState(false)
useEffect(() => {
setCurrentSource(initialSource)
setShowTextFallback(false)
}, [initialSource])
const handleImageError = (event: SyntheticEvent<HTMLImageElement>) => {
onError?.(event)
if (currentSource !== fallbackSrc) {
setCurrentSource(fallbackSrc)
return
}
setShowTextFallback(true)
}
if (showTextFallback) {
return (
<span
data-brand-logo-fallback="text"
role={alt ? 'img' : undefined}
aria-label={alt || undefined}
aria-hidden={alt ? undefined : true}
className={`inline-flex shrink-0 items-center justify-center overflow-hidden bg-white text-center text-[9px] font-semibold leading-tight text-gray-900 ring-1 ring-black/10 ${className || ''}`}
style={{
width: typeof width === 'number' ? width : undefined,
height: typeof height === 'number' ? height : undefined,
...style,
}}
>
{fallbackText}
</span>
)
}
return (
<Image
{...imageProps}
src={currentSource}
alt={alt}
width={width}
height={height}
className={className}
style={style}
onError={handleImageError}
/>
)
}