Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
88 lines
2.4 KiB
TypeScript
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}
|
|
/>
|
|
)
|
|
}
|