feat: 首页UI完全统一到三层架构 & 创建通用组件库
Some checks failed
CI Check / check (push) Has been cancelled

- 首页UI统一到三层架构
  - 将所有 gray 系列替换为 slate 系列
  - 统一按钮、输入框、边框颜色样式
  - 使用 useThemeStyles() Hook

- 创建通用组件库
  - Badge 组件(支持 primary、success、warning、danger 变体)
  - Tag 组件(支持多种颜色,已在首页应用)
  - Button 组件(支持多种变体和尺寸,带加载状态)
  - Input 组件(支持错误状态和多种尺寸)
This commit is contained in:
WuYongJie 2025-11-22 16:32:52 +08:00
parent 7dc8d2a528
commit a604b4d488
5 changed files with 315 additions and 89 deletions

View File

@ -11,6 +11,7 @@ import RankingPanel from '@/components/RankingPanel'
import AIChatWidget from '@/components/AIChatWidget'
import ChangePasswordModal from '@/components/ChangePasswordModal'
import MaterialPreviewModal from '@/components/MaterialPreviewModal'
import Tag from '@/components/ui/Tag'
import { useThemeStyles } from '@/lib/ui-styles'
import { useDialog } from '@/components/ui/DialogProvider'
@ -513,8 +514,8 @@ export default function Home() {
if (status === 'loading' || loading) {
return (
<div className="min-h-screen flex items-center justify-center dark:bg-gray-900">
<div className="text-xl text-gray-600 dark:text-gray-400">加载中...</div>
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900">
<div className="text-xl text-slate-600 dark:text-slate-400">加载中...</div>
</div>
)
}
@ -524,7 +525,7 @@ export default function Home() {
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<Header session={session} />
{/* 内网切换提示 */}
@ -594,7 +595,7 @@ export default function Home() {
className={`${styles.inputStyles.base} pl-12 pr-4`}
/>
<svg
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400"
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -609,7 +610,7 @@ export default function Home() {
{searchKeyword && (
<button
onClick={() => setSearchKeyword('')}
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@ -640,9 +641,9 @@ export default function Home() {
<>
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">类型分页</h2>
<h2 className="text-sm font-medium text-slate-700 dark:text-slate-300">类型分页</h2>
{selectedMaterialType && (
<span className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-xs text-slate-500 dark:text-slate-400">
{materials.length} 个素材
</span>
)}
@ -653,7 +654,7 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMaterialType === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedMaterialType === '' ? { backgroundColor: themeColor } : {}}
>
@ -666,7 +667,7 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMaterialType === type.value
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedMaterialType === type.value ? { backgroundColor: themeColor } : {}}
>
@ -679,9 +680,9 @@ export default function Home() {
{/* Category Filter (使用店铺) */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">使用店铺</h2>
<h2 className="text-sm font-medium text-slate-700 dark:text-slate-300">使用店铺</h2>
{selectedCategory && (
<span className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-xs text-slate-500 dark:text-slate-400">
{materials.length} 个素材
</span>
)}
@ -692,15 +693,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedCategory === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedCategory === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({totalMaterialsCount})
</span>
@ -712,15 +713,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === category.name
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedCategory === category.name ? { backgroundColor: themeColor } : {}}
>
{category.name}
<span className={`ml-1.5 text-xs ${
selectedCategory === category.name
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({category.count || 0})
</span>
@ -732,9 +733,9 @@ export default function Home() {
{/* Scene Filter (场景类别) */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">场景类别</h2>
<h2 className="text-sm font-medium text-slate-700 dark:text-slate-300">场景类别</h2>
{selectedScene && (
<span className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-xs text-slate-500 dark:text-slate-400">
{materials.length} 个素材
</span>
)}
@ -745,15 +746,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedScene === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedScene === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedScene === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({totalMaterialsCount})
</span>
@ -765,15 +766,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedScene === scene.name
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedScene === scene.name ? { backgroundColor: themeColor } : {}}
>
{scene.name}
<span className={`ml-1.5 text-xs ${
selectedScene === scene.name
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({scene.count || 0})
</span>
@ -786,9 +787,9 @@ export default function Home() {
{platforms.length > 0 && (
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">平台分类</h2>
<h2 className="text-sm font-medium text-slate-700 dark:text-slate-300">平台分类</h2>
{selectedPlatform && (
<span className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-xs text-slate-500 dark:text-slate-400">
{materials.length} 个素材
</span>
)}
@ -799,15 +800,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedPlatform === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedPlatform === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedPlatform === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({totalMaterialsCount})
</span>
@ -819,15 +820,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedPlatform === platform.name
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedPlatform === platform.name ? { backgroundColor: themeColor } : {}}
>
{platform.name}
<span className={`ml-1.5 text-xs ${
selectedPlatform === platform.name
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({platform.count || 0})
</span>
@ -840,7 +841,7 @@ export default function Home() {
{/* Month Filter (上传月份) */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">上传月份</h2>
<h2 className="text-sm font-medium text-slate-700 dark:text-slate-300">上传月份</h2>
</div>
<div className="flex flex-wrap gap-2">
<button
@ -848,15 +849,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMonth === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedMonth === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedMonth === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({totalMaterialsCount})
</span>
@ -868,15 +869,15 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMonth === month.value
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={selectedMonth === month.value ? { backgroundColor: themeColor } : {}}
>
{month.label}
<span className={`ml-1.5 text-xs ${
selectedMonth === month.value
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
? 'text-slate-300'
: 'text-slate-500 dark:text-slate-400'
}`}>
({month.count})
</span>
@ -891,7 +892,7 @@ export default function Home() {
{/* Sort Options (排序方式) - 保留排序选项 */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">排序方式</h2>
<h2 className="text-sm font-medium text-slate-700 dark:text-slate-300">排序方式</h2>
</div>
<div className="flex flex-wrap gap-2">
<button
@ -899,7 +900,7 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
sortBy === 'createdAt'
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={sortBy === 'createdAt' ? { backgroundColor: themeColor } : {}}
>
@ -910,7 +911,7 @@ export default function Home() {
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
sortBy === 'downloadCount'
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={sortBy === 'downloadCount' ? { backgroundColor: themeColor } : {}}
>
@ -922,69 +923,69 @@ export default function Home() {
{/* Active Filters Display */}
{(searchKeyword || selectedCategory || selectedScene || selectedMaterialType || selectedPlatform || selectedMonth) && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-apple p-3 mb-4">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 flex-wrap">
<div className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400 flex-wrap">
<span className="font-medium text-blue-600 dark:text-blue-400">当前筛选:</span>
{searchKeyword && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-slate-100 dark:bg-slate-800 rounded-full">
搜索: {searchKeyword}
<button
onClick={() => setSearchKeyword('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
>
×
</button>
</span>
)}
{selectedCategory && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-slate-100 dark:bg-slate-800 rounded-full">
店铺: {selectedCategory}
<button
onClick={() => setSelectedCategory('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
>
×
</button>
</span>
)}
{selectedScene && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-slate-100 dark:bg-slate-800 rounded-full">
场景类别: {selectedScene}
<button
onClick={() => setSelectedScene('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
>
×
</button>
</span>
)}
{selectedMaterialType && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-slate-100 dark:bg-slate-800 rounded-full">
素材类型: {selectedMaterialType}
<button
onClick={() => setSelectedMaterialType('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
>
×
</button>
</span>
)}
{selectedPlatform && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-slate-100 dark:bg-slate-800 rounded-full">
平台分类: {selectedPlatform}
<button
onClick={() => setSelectedPlatform('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
>
×
</button>
</span>
)}
{selectedMonth && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-slate-100 dark:bg-slate-800 rounded-full">
月份: {months.find(m => m.value === selectedMonth)?.label || selectedMonth}
<button
onClick={() => setSelectedMonth('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
className="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
>
×
</button>
@ -1004,7 +1005,7 @@ export default function Home() {
清除所有筛选
</button>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<div className="text-xs text-slate-500 dark:text-slate-400 mt-2">
💡 提示:可以组合使用多个筛选条件,找到更精确的素材
</div>
</div>
@ -1014,11 +1015,11 @@ export default function Home() {
{/* Materials Grid */}
{loading ? (
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">加载中...</p>
<p className="text-slate-500 dark:text-slate-400 text-lg">加载中...</p>
</div>
) : materials.length === 0 ? (
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">
<p className="text-slate-500 dark:text-slate-400 text-lg">
{searchKeyword || selectedCategory || selectedScene || selectedMaterialType || selectedPlatform ? '没有找到匹配的素材' : '还没有素材,快来上传第一个吧!'}
</p>
</div>
@ -1064,24 +1065,16 @@ export default function Home() {
<div className="p-4">
{/* 店铺、场景、素材类型和渲染器 */}
<div className="flex items-center gap-2 mb-3 flex-wrap">
<span className="inline-flex items-center px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded text-xs font-medium">
{material.categoryName}
</span>
<span className="inline-flex items-center px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded text-xs font-medium">
{material.sceneName}
</span>
<span className="inline-flex items-center px-2 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded text-xs font-medium">
{material.materialType}
</span>
<Tag color="blue">{material.categoryName}</Tag>
<Tag color="green">{material.sceneName}</Tag>
<Tag color="orange">{material.materialType}</Tag>
{material.materialType === '3DMAX' && (
<span className="inline-flex items-center px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium">
{material.renderer}
</span>
<Tag color="purple">{material.renderer}</Tag>
)}
</div>
{/* 作者和时间 */}
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400 mb-3">
<div className="space-y-1 text-sm text-slate-600 dark:text-slate-400 mb-3">
<p className="truncate">
<span className="font-medium">作者:</span>
{material.authorName}
@ -1094,13 +1087,13 @@ export default function Home() {
<span className="font-medium">大小:</span>
<span className="text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</span>
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
<p className="text-xs text-slate-500 dark:text-slate-500">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
{/* 统计数据 */}
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 mb-3 pb-3 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -1135,7 +1128,7 @@ export default function Home() {
)}
<button
onClick={() => handleShare(material.id)}
className="flex-none px-4 py-2 rounded-apple font-medium text-sm transition-all bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact} flex-none`}
title="分享"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -1189,7 +1182,7 @@ export default function Home() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === 1}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact} disabled:opacity-50 disabled:cursor-not-allowed`}
>
首页
</button>
@ -1201,7 +1194,7 @@ export default function Home() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === 1}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
@ -1232,7 +1225,7 @@ export default function Home() {
className={`px-4 py-2 rounded-lg transition-all ${
currentPage === pageNum
? 'text-white font-medium'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'
}`}
style={currentPage === pageNum ? { backgroundColor: themeColor } : {}}
>
@ -1249,7 +1242,7 @@ export default function Home() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === totalPages}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
@ -1263,7 +1256,7 @@ export default function Home() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === totalPages}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact} disabled:opacity-50 disabled:cursor-not-allowed`}
>
末页
</button>
@ -1271,11 +1264,11 @@ export default function Home() {
{/* 页码跳转和信息 */}
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600 dark:text-gray-400">
<span className="text-sm text-slate-600 dark:text-slate-400">
第 {currentPage} / {totalPages} 页,共 {totalMaterialsCount} 条
</span>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">跳至</span>
<span className="text-sm text-slate-600 dark:text-slate-400">跳至</span>
<input
type="number"
min="1"
@ -1292,10 +1285,10 @@ export default function Home() {
}
}
}}
className="w-16 px-2 py-1 text-center rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
className={`${styles.inputStyles.base} w-16 px-2 py-1 text-center`}
placeholder=""
/>
<span className="text-sm text-gray-600 dark:text-gray-400">页</span>
<span className="text-sm text-slate-600 dark:text-slate-400">页</span>
<button
onClick={() => {
const page = parseInt(jumpPage)
@ -1544,7 +1537,7 @@ export default function Home() {
setSelectedPlatform('')
setSelectedMonth('')
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact}`}
>
清除全部
</button>
@ -1567,11 +1560,11 @@ export default function Home() {
onClick={() => setShowShareModal(false)}
>
<div
className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-lg w-full mx-4 shadow-2xl"
className={`${styles.cardStyles.base} max-w-lg w-full mx-4`}
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">分享链接</h3>
<div className="bg-gray-100 dark:bg-gray-700 p-3 rounded-lg mb-4 break-all text-sm text-gray-800 dark:text-gray-200">
<h3 className="text-lg font-semibold mb-4 text-slate-900 dark:text-white">分享链接</h3>
<div className="bg-slate-100 dark:bg-slate-700 p-3 rounded-lg mb-4 break-all text-sm text-slate-800 dark:text-slate-200">
{shareUrl}
</div>
<div className="flex gap-2">
@ -1584,7 +1577,7 @@ export default function Home() {
</button>
<button
onClick={() => setShowShareModal(false)}
className="flex-1 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-800 dark:text-white px-4 py-2 rounded-lg transition-colors"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact} flex-1`}
>
关闭
</button>

40
components/ui/Badge.tsx Normal file
View File

@ -0,0 +1,40 @@
'use client'
import { ReactNode } from 'react'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface BadgeProps {
children: ReactNode
variant?: 'primary' | 'success' | 'warning' | 'danger'
size?: 'sm' | 'md' | 'lg'
className?: string
}
export default function Badge({
children,
variant = 'primary',
size = 'md',
className = '',
}: BadgeProps) {
const styles = useThemeStyles()
const sizeClasses = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
}
return (
<span
className={cn(
styles.badgeStyles[variant],
sizeClasses[size],
'inline-flex items-center font-medium rounded',
className
)}
>
{children}
</span>
)
}

86
components/ui/Button.tsx Normal file
View File

@ -0,0 +1,86 @@
'use client'
import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'success'
size?: 'sm' | 'md' | 'lg'
fullWidth?: boolean
loading?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = 'primary',
size = 'md',
fullWidth = false,
loading = false,
disabled,
className = '',
...props
},
ref
) => {
const styles = useThemeStyles()
const sizeClasses = {
sm: styles.buttonModifiers.compactXs,
md: '',
lg: 'px-8 py-4 text-base',
}
const variantClass = styles.buttonStyles[variant]
return (
<button
ref={ref}
disabled={disabled || loading}
className={cn(
variantClass,
sizeClasses[size],
fullWidth && styles.buttonModifiers.block,
loading && 'opacity-75 cursor-wait',
className
)}
{...props}
>
{loading ? (
<span className="flex items-center gap-2">
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<span>加载中...</span>
</span>
) : (
children
)}
</button>
)
}
)
Button.displayName = 'Button'
export default Button

51
components/ui/Input.tsx Normal file
View File

@ -0,0 +1,51 @@
'use client'
import { InputHTMLAttributes, forwardRef } from 'react'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
error?: boolean
size?: 'sm' | 'md' | 'lg'
fullWidth?: boolean
}
const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
error = false,
size = 'md',
fullWidth = true,
className = '',
...props
},
ref
) => {
const styles = useThemeStyles()
const sizeClasses = {
sm: styles.inputModifiers.compactXs,
md: '',
lg: 'px-5 py-4 text-base',
}
const inputClass = error ? styles.inputStyles.error : styles.inputStyles.base
return (
<input
ref={ref}
className={cn(
inputClass,
sizeClasses[size],
!fullWidth && 'w-auto',
className
)}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export default Input

56
components/ui/Tag.tsx Normal file
View File

@ -0,0 +1,56 @@
'use client'
import { ReactNode } from 'react'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface TagProps {
children: ReactNode
color?: 'blue' | 'green' | 'orange' | 'purple' | 'indigo' | 'pink' | 'cyan'
size?: 'sm' | 'md' | 'lg'
className?: string
onClick?: () => void
}
export default function Tag({
children,
color = 'blue',
size = 'md',
className = '',
onClick,
}: TagProps) {
const styles = useThemeStyles()
const colorClasses = {
blue: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
green: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
orange: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
purple: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
indigo: 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300',
pink: 'bg-pink-100 dark:bg-pink-900/30 text-pink-700 dark:text-pink-300',
cyan: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-700 dark:text-cyan-300',
}
const sizeClasses = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
}
const baseClasses = 'inline-flex items-center font-medium rounded transition-colors'
return (
<span
className={cn(
baseClasses,
colorClasses[color],
sizeClasses[size],
onClick && 'cursor-pointer hover:opacity-80',
className
)}
onClick={onClick}
>
{children}
</span>
)
}