This commit is contained in:
truewhile
2026-09-04 11:56:29 +08:00
parent cc40169616
commit 203abd106a
72 changed files with 1979 additions and 1155 deletions
@@ -202,7 +202,7 @@ function ManualScrapeCandidateRow({
<div className="flex flex-col gap-4 rounded-xl border border-sand-200 bg-white p-3 shadow-sm sm:flex-row">
<div className="h-28 w-20 shrink-0 overflow-hidden rounded-lg bg-sand-100">
{item.poster_url ? (
<img src={imageURL(item.poster_url)} alt={item.title} className="h-full w-full object-cover" referrerPolicy="no-referrer" />
<img src={imageURL(item.poster_url)} alt={item.title} loading="lazy" decoding="async" className="h-full w-full object-cover" referrerPolicy="no-referrer" />
) : (
<div className="flex h-full items-center justify-center text-xs text-sand-500">无海报</div>
)}
+23 -26
View File
@@ -1,12 +1,13 @@
import { useEffect, useRef, useState, type ReactNode } from 'react'
import { memo, useEffect, useRef, useState, type ReactNode } from 'react'
import { Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Film, Play, Layers, Star } from 'lucide-react'
import { imageURL } from '../api/client'
import type { Media } from '../types'
export const MediaCard = ({
media, progress, count, rating, linkTo, onClick, actions, compact,
// memo:父级状态变化(如轮播切图、其它卡片操作)不再级联重渲染所有卡片。
// 注意 actions/renderActions 必须引用稳定(用 renderActions 传函数)memo 才生效。
export const MediaCard = memo(function MediaCard({
media, progress, count, rating, linkTo, onClick, actions, renderActions, compact,
}: {
media: Media
progress?: number
@@ -15,25 +16,26 @@ export const MediaCard = ({
linkTo?: string
onClick?: () => void
actions?: ReactNode
renderActions?: (media: Media) => ReactNode
compact?: boolean
}) => {
}) {
const ref = useRef<HTMLDivElement>(null)
const href = linkTo ?? `/media/${media.id}`
const [posterFit, setPosterFit] = useState<'cover' | 'contain'>('cover')
const posterSrc = imageURL(media.poster_url, media.updated_at)
const displayRating = rating ?? media.rating
const versionCount = media.versions?.length ?? 0
// renderActions 延迟到卡片自身渲染时才调用,保证 memo 生效
const actionContent = actions ?? renderActions?.(media)
useEffect(() => {
setPosterFit('cover')
}, [media.poster_url, media.updated_at])
const card = (
<motion.div
<div
ref={ref}
whileHover={{ scale: 1.04, y: -6 }}
transition={{ type: 'spring', stiffness: 280, damping: 22 }}
className="relative overflow-hidden rounded-2xl border border-[var(--app-border)] bg-[var(--app-panel)] shadow-[0_1px_3px_rgba(0,0,0,0.01),0_1px_2px_rgba(0,0,0,0.015)] transition-all duration-300 hover:border-brand-500/40 hover:shadow-[0_12px_32px_var(--app-shadow)]"
className="relative overflow-hidden rounded-2xl border border-[var(--app-border)] bg-[var(--app-panel)] shadow-[0_1px_3px_rgba(0,0,0,0.01),0_1px_2px_rgba(0,0,0,0.015)] transition-all duration-300 hover:scale-[1.04] hover:-translate-y-1.5 hover:border-brand-500/40 hover:shadow-[0_12px_32px_var(--app-shadow)]"
>
{/* Poster Wrapper */}
<div className="relative aspect-[2/3] w-full overflow-hidden bg-[var(--app-panel-soft)]">
@@ -95,16 +97,11 @@ export const MediaCard = ({
</span>
)}
{/* Premium Hover Overlay */}
{/* Premium Hover Overlay(CSS 过渡替代 framer-motion 逐卡动画实例) */}
<div className={`absolute inset-0 bg-gradient-to-t from-[#111827]/90 via-[#111827]/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end ${
compact ? 'p-3' : 'p-4'
}`}>
<motion.div
initial={{ y: 15, opacity: 0 }}
whileHover={{ y: 0, opacity: 1 }}
transition={{ duration: 0.2 }}
className="space-y-2"
>
<div className="space-y-2 translate-y-4 transition-transform duration-200 group-hover:translate-y-0">
<span className={`inline-flex items-center gap-1.5 rounded-xl bg-brand-500 px-4 py-2 font-bold text-white shadow-md shadow-brand-500/20 ${
compact ? 'py-1.5 text-[11px]' : 'text-xs py-2'
}`}>
@@ -116,15 +113,15 @@ export const MediaCard = ({
{media.overview || "暂无简介内容"}
</p>
)}
</motion.div>
</div>
</div>
{/* Progress Bar overlay */}
{progress !== undefined && progress > 0 && progress < 1 && (
<div className="absolute inset-x-0 bottom-0 h-1.5 bg-[var(--app-hover)]">
<div
className="h-full bg-gradient-to-r from-brand-400 to-brand-500 rounded-r-full transition-all duration-300"
style={{ width: `${Math.round(progress * 100)}%` }}
<div
className="h-full bg-gradient-to-r from-brand-400 to-brand-500 rounded-r-full transition-all duration-300"
style={{ width: `${Math.round(progress * 100)}%` }}
/>
</div>
)}
@@ -148,7 +145,7 @@ export const MediaCard = ({
)}
</div>
</div>
</motion.div>
</div>
)
if (onClick) {
@@ -157,23 +154,23 @@ export const MediaCard = ({
<button type="button" onClick={onClick} className="block w-full text-left">
{card}
</button>
{actions && (
{actionContent && (
<div className="absolute right-2 top-2 z-20 flex flex-wrap justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
{actions}
{actionContent}
</div>
)}
</div>
)
}
if (actions) {
if (actionContent) {
return (
<div className="group relative block">
<Link to={href} className="block">
{card}
</Link>
<div className="absolute right-2 top-2 z-20 flex flex-wrap justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
{actions}
{actionContent}
</div>
</div>
)
@@ -184,4 +181,4 @@ export const MediaCard = ({
{card}
</Link>
)
}
})
-52
View File
@@ -1,52 +0,0 @@
import { type ReactNode } from 'react'
import { usePermissionStore } from '../stores/permissions'
import { useAuthStore } from '../stores/auth'
interface PermissionGuardProps {
permission: string
children: ReactNode
fallback?: ReactNode
requireSuperUser?: boolean
}
/**
* PermissionGuard 组件用于根据用户权限控制内容显示。
*
* @param permission - 需要的权限键
* @param children - 有权限时显示的内容
* @param fallback - 无权限时显示的内容(可选,默认不显示)
* @param requireSuperUser - 是否要求超级用户(admin/plus)绕过权限检查
*/
export function PermissionGuard({
permission,
children,
fallback = null,
requireSuperUser = false,
}: PermissionGuardProps) {
const { hasPermission, isSuper, permissions, isLoading } = usePermissionStore()
const tier = useAuthStore((state) => state.tier)
const role = useAuthStore((state) => state.user?.role)
// 超级用户(admin 或 plus)默认有所有权限
if (isSuper || tier === 'plus' || role === 'admin') {
return <>{children}</>
}
// 如果 requireSuperUser 为 true 且用户不是超级用户,则不显示
if (requireSuperUser && !isSuper) {
return <>{fallback}</>
}
// 加载中时显示 fallback
if (isLoading && Object.keys(permissions).length === 0) {
return <>{fallback}</>
}
// 检查具体权限
if (hasPermission(permission)) {
return <>{children}</>
}
return <>{fallback}</>
}