WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

281 lines
11 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import { useParams, useRouter } from 'next/navigation'
import Image from 'next/image'
import Link from 'next/link'
import Header from '@/components/Header'
import { useThemeStyles, cn } from '@/lib/ui-styles'
// 内联SVG图标组件
const UserIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
)
const ArrowLeftIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
)
const DownloadIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
)
const HeartIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
)
const ChevronLeftIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
)
const ChevronRightIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)
interface Material {
id: string
productModel: string
previewUrl: string
materialType: string | null
categoryName: string
sceneName: string
downloadCount: number
likeCount: number
createdAt: string
user?: {
username: string
avatar: string | null
}
}
interface AuthorMaterialsResponse {
materials: Material[]
pagination: {
page: number
pageSize: number
total: number
totalPages: number
}
author: {
name: string
totalMaterials: number
totalDownloads: number
totalLikes: number
}
}
export default function AuthorPage() {
const { data: session } = useSession()
const params = useParams()
const router = useRouter()
const styles = useThemeStyles()
const authorName = decodeURIComponent(params.authorName as string)
const [materials, setMaterials] = useState<Material[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [authorStats, setAuthorStats] = useState({
name: authorName,
totalMaterials: 0,
totalDownloads: 0,
totalLikes: 0,
avatar: null as string | null,
})
const pageSize = 20
const fetchMaterials = useCallback(async () => {
setLoading(true)
try {
const res = await fetch(
`/api/materials?authorName=${encodeURIComponent(authorName)}&page=${page}&pageSize=${pageSize}`
)
if (res.ok) {
const data = await res.json()
setMaterials(data.materials || [])
setTotalPages(data.pagination?.totalPages || 1)
// 计算作者统计
if (data.materials?.length > 0) {
const totalDownloads = data.materials.reduce(
(sum: number, m: Material) => sum + m.downloadCount,
0
)
const totalLikes = data.materials.reduce(
(sum: number, m: Material) => sum + m.likeCount,
0
)
// 获取作者头像(从第一个素材的用户信息中获取)
const authorAvatar = data.materials[0]?.user?.avatar || null
setAuthorStats({
name: authorName,
totalMaterials: data.pagination?.total || data.materials.length,
totalDownloads,
totalLikes,
avatar: authorAvatar,
})
}
}
} catch (error) {
console.error('Failed to fetch author materials:', error)
} finally {
setLoading(false)
}
}, [authorName, page])
useEffect(() => {
fetchMaterials()
}, [fetchMaterials])
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<Header session={session} />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* 返回按钮 */}
<button
onClick={() => router.back()}
className="flex items-center gap-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white mb-6 transition-colors"
>
<ArrowLeftIcon className="w-5 h-5" />
返回
</button>
{/* 作者信息卡片 */}
<div className={cn(styles.cardStyles.base, 'mb-8')}>
<div className="flex items-center gap-6">
{authorStats.avatar ? (
<div className="w-20 h-20 rounded-full overflow-hidden">
<Image
src={authorStats.avatar}
alt={authorStats.name}
width={80}
height={80}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<UserIcon className="w-10 h-10 text-white" />
</div>
)}
<div className="flex-1">
<h1 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
{authorStats.name}
</h1>
<div className="flex flex-wrap gap-6 text-sm text-slate-600 dark:text-slate-400">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900 dark:text-white">
{authorStats.totalMaterials}
</span>
<span>作品</span>
</div>
<div className="flex items-center gap-2">
<DownloadIcon className="w-4 h-4" />
<span className="font-medium text-slate-900 dark:text-white">
{authorStats.totalDownloads}
</span>
<span>下载</span>
</div>
<div className="flex items-center gap-2">
<HeartIcon className="w-4 h-4" />
<span className="font-medium text-slate-900 dark:text-white">
{authorStats.totalLikes}
</span>
<span>点赞</span>
</div>
</div>
</div>
</div>
</div>
{/* 作品列表 */}
{loading ? (
<div className="flex items-center justify-center py-16">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
) : materials.length === 0 ? (
<div className="text-center py-16">
<UserIcon className="w-16 h-16 mx-auto text-slate-300 dark:text-slate-600 mb-4" />
<p className="text-slate-500 dark:text-slate-400">该作者暂无作品</p>
</div>
) : (
<>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{materials.map((material) => (
<Link
key={material.id}
href={`/share/${material.id}`}
className="group relative bg-white dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
>
<div className="aspect-[4/3] relative bg-slate-100 dark:bg-slate-700">
<Image
src={material.previewUrl}
alt={material.productModel}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
/>
</div>
<div className="p-3">
<h3 className="text-sm font-medium text-slate-900 dark:text-white truncate">
{material.productModel}
</h3>
<div className="flex items-center justify-between mt-2 text-xs text-slate-500 dark:text-slate-400">
<span>{material.materialType || material.categoryName}</span>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
<DownloadIcon className="w-3 h-3" />
{material.downloadCount}
</span>
<span className="flex items-center gap-1">
<HeartIcon className="w-3 h-3" />
{material.likeCount}
</span>
</div>
</div>
</div>
</Link>
))}
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 mt-8">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeftIcon className="w-5 h-5" />
</button>
<span className="text-sm text-slate-600 dark:text-slate-400">
{page} / {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRightIcon className="w-5 h-5" />
</button>
</div>
)}
</>
)}
</div>
</div>
)
}