portal/src/modules/markdown-editor/components/ui/AdBanner.tsx

198 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import * as React from "react"
import { X } from "lucide-react"
import { Button } from "./button"
import { Card } from "./card"
import { cn } from '../../lib/utils'
interface AdBannerProps {
id?: string // 广告ID用于本地存储
imageUrl?: string
title?: string
description?: string
linkUrl?: string
className?: string
onClose?: () => void
expireDays?: number // 关闭后多少天内不再显示默认7天
floating?: boolean // 是否浮动显示
}
const AdBanner = React.forwardRef<HTMLDivElement, AdBannerProps>(
({
id = "default-ad",
imageUrl = "/assets/img/neurapress-web-app.jpg",
title = "NeuraPress - 让 Markdown 编辑更简单",
description = "专业的 Markdown 编辑器,支持微信公众号、小红书等多平台格式导出",
linkUrl = "#",
className,
onClose,
expireDays = 7,
floating = false,
...props
}, ref) => {
const [isVisible, setIsVisible] = React.useState(true)
const storageKey = `ad_banner2_closed_${id}`
// 检查广告是否已被关闭且未过期
React.useEffect(() => {
try {
const closedData = localStorage.getItem(storageKey)
if (closedData) {
const { timestamp } = JSON.parse(closedData)
const expireTime = timestamp + (expireDays * 24 * 60 * 60 * 1000)
if (Date.now() < expireTime) {
setIsVisible(false)
return
} else {
// 过期了,清除存储
localStorage.removeItem(storageKey)
}
}
} catch (error) {
console.warn('AdBanner localStorage error:', error)
}
}, [storageKey, expireDays])
const handleClose = () => {
setIsVisible(false)
// 保存关闭状态到localStorage
try {
localStorage.setItem(storageKey, JSON.stringify({
timestamp: Date.now()
}))
} catch (error) {
console.warn('AdBanner localStorage save error:', error)
}
onClose?.()
}
const handleClick = () => {
if (linkUrl && linkUrl !== "#") {
window.open(linkUrl, "_blank")
}
}
if (!isVisible) return null
// 浮动模式的样式
const floatingStyles = floating ? {
position: 'fixed' as const,
bottom: '20px',
right: '20px',
zIndex: 1000,
width: '180px',
} : {}
return (
<Card
ref={ref}
style={floatingStyles}
className={cn(
floating
? "relative bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 shadow-2xl rounded-lg animate-in slide-in-from-bottom-2 duration-500"
: "relative overflow-hidden bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 border-blue-200 dark:border-blue-800 shadow-md hover:shadow-lg transition-all duration-300",
className
)}
{...props}
>
{/* 关闭按钮 */}
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 z-10 h-6 w-6 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
onClick={handleClose}
>
<X className="h-4 w-4" />
</Button>
<div
className={cn(
"cursor-pointer",
floating
? "p-4 pb-3"
: "flex items-center gap-4 p-4"
)}
onClick={handleClick}
>
{floating ? (
// 浮动模式:垂直布局,图片在上
<div className="space-y-4">
{/* 顶部图片 */}
<div className="flex justify-center pt-2">
<div className="w-25 h-25 rounded-lg overflow-hidden shadow-sm">
<img
src="/assets/img/qun.png"
alt={title}
className="w-full h-full object-cover"
onError={(e) => {
// 如果图片加载失败回退到默认的D图标
const target = e.currentTarget;
target.style.display = 'none';
const fallback = target.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = 'flex';
}}
/>
<div
className="w-full h-full bg-black dark:bg-white rounded-lg flex items-center justify-center"
style={{ display: 'none' }}
>
<span className="text-white dark:text-black font-bold text-2xl">D</span>
</div>
</div>
</div>
{/* 底部标识 */}
<div className="text-center text-xs text-gray-400 dark:text-gray-500 uppercase tracking-wide pt-2">
ADS VIA NEURAPRESS
</div>
</div>
) : (
// 非浮动模式:水平布局
<>
{/* 左侧图片 */}
<div className="flex-shrink-0">
<div className="w-16 h-16 rounded-lg overflow-hidden bg-white shadow-sm">
<img
src={imageUrl}
alt={title}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' fill='%23f3f4f6'/%3E%3Ctext x='32' y='32' text-anchor='middle' dy='0.3em' font-family='Arial' font-size='12' fill='%236b7280'%3EAD%3C/text%3E%3C/svg%3E"
}}
/>
</div>
</div>
{/* 右侧内容 */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-2">
<h3 className="font-medium text-gray-900 dark:text-gray-100 truncate">
{title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
{description}
</p>
</div>
</div>
</div>
</>
)}
</div>
{/* 装饰性渐变 - 仅非浮动模式显示 */}
{!floating && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent pointer-events-none" />
)}
</Card>
)
}
)
AdBanner.displayName = "AdBanner"
export { AdBanner, type AdBannerProps }