重构核心模块并同步更新项目配置与相关实现

This commit is contained in:
truewhile
2026-09-13 20:36:14 +08:00
parent 386fc358d1
commit 9a68ef701f
38 changed files with 1587 additions and 678 deletions
+8 -6
View File
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { ArrowLeft, Film, LoaderCircle, Menu, Search, Star, X } from 'lucide-react'
@@ -132,12 +132,12 @@ function LayoutHeaderSearch() {
const locationKey = `${location.pathname}${location.search}`
const prevLocationKeyRef = useRef(locationKey)
const setSearchOpen = (open: boolean) => {
const setSearchOpen = useCallback((open: boolean) => {
isOpenRef.current = open
setIsOpen(open)
}
}, [])
const dismissSearch = (clearQuery: boolean) => {
const dismissSearch = useCallback((clearQuery: boolean) => {
suppressInputRef.current = true
setSearchOpen(false)
if (clearQuery) {
@@ -156,9 +156,11 @@ function LayoutHeaderSearch() {
window.setTimeout(() => {
suppressInputRef.current = false
}, 0)
}
}, [setSearchOpen])
const dismissSearchRef = useRef(dismissSearch)
dismissSearchRef.current = dismissSearch
useEffect(() => {
dismissSearchRef.current = dismissSearch
}, [dismissSearch])
useEffect(() => {
const trimmed = query.trim()
+16 -36
View File
@@ -1,5 +1,4 @@
import { Outlet } from 'react-router-dom'
import { AnimatePresence, motion } from 'framer-motion'
import clsx from 'clsx'
import { LayoutSidebarContent, type LayoutSidebarContentProps } from './LayoutSidebarContent'
@@ -50,29 +49,18 @@ export function LayoutDesktopSidebar({ children, isSidebarOpen }: LayoutSidebarP
}
export function LayoutMobileSidebar({ children, isOpen, onClose }: LayoutMobileSidebarProps) {
if (!isOpen) return null
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex lg:hidden">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/15 backdrop-blur-sm"
/>
<motion.div
initial={{ x: '-100%' }}
animate={{ x: 0 }}
exit={{ x: '-100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 220 }}
className="relative z-10 flex h-full min-h-0 w-64 max-w-xs flex-col overflow-hidden shadow-xl"
>
{children}
</motion.div>
</div>
)}
</AnimatePresence>
<div className="fixed inset-0 z-50 flex lg:hidden">
<div
onClick={onClose}
className="fixed inset-0 bg-black/15 backdrop-blur-sm animate-overlay-in"
/>
<div className="relative z-10 flex h-full min-h-0 w-64 max-w-xs flex-col overflow-hidden shadow-xl animate-drawer-in">
{children}
</div>
</div>
)
}
@@ -139,19 +127,11 @@ export function LayoutWorkspace({ routeKey, showMobileBottomNav = false }: Layou
return (
<main id="app-main-scroll" className={clsx('flex-1 overflow-y-auto [overflow-anchor:none] px-4 py-6 md:px-8 md:py-10', bottomPad)}>
<div className="max-w-7xl mx-auto">
<AnimatePresence mode="wait">
<motion.div
key={routeKey}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
>
<RouteErrorBoundary>
<Outlet />
</RouteErrorBoundary>
</motion.div>
</AnimatePresence>
<div key={routeKey} className="animate-page-in">
<RouteErrorBoundary>
<Outlet />
</RouteErrorBoundary>
</div>
</div>
</main>
)
+2 -7
View File
@@ -1,5 +1,4 @@
import { Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowLeft, Menu, X } from 'lucide-react'
import clsx from 'clsx'
@@ -77,13 +76,9 @@ function LayoutSidebarHeader({
className="h-10 w-10 shrink-0 rounded-xl object-contain shadow-sm"
/>
{sidebarExpanded && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="font-display text-lg font-extrabold tracking-tight text-[var(--app-text)]"
>
<span className="font-display text-lg font-extrabold tracking-tight text-[var(--app-text)] animate-fade-slide-in">
MeBox
</motion.span>
</span>
)}
</Link>
<SidebarIconButton className="hidden lg:block" onClick={onToggleSidebar}>
+9 -22
View File
@@ -1,6 +1,5 @@
import type { ReactNode } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronDown } from 'lucide-react'
import clsx from 'clsx'
@@ -52,21 +51,13 @@ export function SidebarGroup({ id, icon, label, children, collapsed, open, activ
</div>
)}
</button>
<AnimatePresence initial={false}>
{!collapsed && open && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
className="overflow-hidden"
>
<div className="space-y-1 pb-1 pl-3">
{children}
</div>
</motion.div>
)}
</AnimatePresence>
{!collapsed && open && (
<div className="overflow-hidden animate-accordion-in">
<div className="space-y-1 pb-1 pl-3">
{children}
</div>
</div>
)}
</div>
)
}
@@ -103,13 +94,9 @@ export function SidebarLink({ to, icon, label, end, collapsed, child }: SidebarL
{icon}
</span>
{!collapsed && (
<motion.span
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
className="truncate whitespace-nowrap"
>
<span className="truncate whitespace-nowrap">
{label}
</motion.span>
</span>
)}
{collapsed && (
<div className="pointer-events-none absolute left-full z-50 ml-3 whitespace-nowrap rounded-xl bg-[var(--app-tooltip-bg)] px-2.5 py-1.5 text-xs font-semibold text-[var(--app-tooltip-text)] opacity-0 shadow-lg transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
+7 -16
View File
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { Link, useLocation } from 'react-router-dom'
import { AnimatePresence, motion } from 'framer-motion'
import { Cast, ChevronDown, Clock, Heart, ListMusic, LogOut, Settings, Tv, UserCog } from 'lucide-react'
import clsx from 'clsx'
@@ -103,26 +102,18 @@ export function LayoutUserMenu({
const menuPortal = isOpen && menuPosition && typeof document !== 'undefined'
? createPortal(
<AnimatePresence>
<motion.div
<>
<div
key="layout-user-menu-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.12 }}
className="fixed inset-0 z-[120]"
className="fixed inset-0 z-[120] animate-overlay-in"
aria-hidden="true"
onPointerDown={onClose}
/>
<motion.div
<div
key="layout-user-menu-panel"
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.15 }}
role="menu"
style={{ top: menuPosition.top, right: menuPosition.right }}
className="fixed z-[121] w-56 origin-top-right rounded-2xl border border-[var(--app-border)] bg-[var(--app-panel)] p-2 shadow-xl"
className="fixed z-[121] w-56 origin-top-right rounded-2xl border border-[var(--app-border)] bg-[var(--app-panel)] p-2 shadow-xl animate-menu-in"
onPointerDown={(event) => event.stopPropagation()}
>
<UserMenuLink to="/profile" icon={<Settings size={16} />} label="设置" onNavigate={onClose} />
@@ -191,8 +182,8 @@ export function LayoutUserMenu({
<LogOut size={16} />
<span>安全登出系统</span>
</button>
</motion.div>
</AnimatePresence>,
</div>
</>,
document.body,
)
: null
+8 -3
View File
@@ -1,4 +1,4 @@
import { memo, useEffect, useRef, useState, type ReactNode } from 'react'
import { memo, useEffect, useRef, useState, type ReactNode } from 'react'
import { Link } from 'react-router-dom'
import { Film, Play, Layers, Star } from 'lucide-react'
import { imageURL } from '../api/client'
@@ -22,7 +22,12 @@ export const MediaCard = memo(function MediaCard({
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 posterSrc = imageURL(media.poster_url, media.updated_at, {
maxWidth: compact ? 320 : 480,
maxHeight: compact ? 480 : 600,
quality: 82,
})
const blurredPosterSrc = imageURL(media.poster_url, media.updated_at, { maxWidth: 160, quality: 60 })
const displayRating = rating ?? media.rating
const versionCount = media.versions?.length ?? 0
// renderActions 延迟到卡片自身渲染时才调用,保证 memo 生效
@@ -43,7 +48,7 @@ export const MediaCard = memo(function MediaCard({
<>
{posterFit === 'contain' && (
<img
src={posterSrc}
src={blurredPosterSrc}
alt=""
aria-hidden="true"
loading="lazy"