feat: UI优化与功能完善 - 低饱和度炫彩元素、深浅色主题适配、用户信息同步素材
Some checks failed
CI Check / check (push) Has been cancelled

This commit is contained in:
WuYongJie 2025-11-22 17:56:24 +08:00
parent a604b4d488
commit fe9e3d80a3
44 changed files with 1515 additions and 412 deletions

View File

@ -1,3 +1,15 @@
/**
* CategoriesTab 组件
*
* 分类管理Tab组件
* 从主页面拆分出来,负责分类管理功能
* 包括店铺分类、场景类别、平台分类的管理
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX(拆分自 page.tsx)
* @component
*/
'use client'
import { useState, useEffect, useCallback } from 'react'

View File

@ -1,3 +1,15 @@
/**
* DepartmentsTab 组件
*
* 部门管理Tab组件
* 从主页面拆分出来,负责部门管理功能
* 包括部门树形结构、创建、删除、重命名等
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX(拆分自 page.tsx)
* @component
*/
'use client'
import { useCallback, useMemo, useState } from 'react'

View File

@ -1,3 +1,15 @@
/**
* InvitationsTab 组件
*
* 邀请管理Tab组件
* 从主页面拆分出来,负责邀请管理功能
* 包括邀请列表、创建、删除、验证等
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX(拆分自 page.tsx)
* @component
*/
'use client'
import { useState, useEffect, useCallback } from 'react'

View File

@ -1,3 +1,15 @@
/**
* MaterialsTab 组件
*
* 素材管理Tab组件
* 从主页面拆分出来,负责素材管理功能
* 包括素材列表、搜索、预览等
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX(拆分自 page.tsx)
* @component
*/
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'

View File

@ -1,3 +1,15 @@
/**
* MonitorTab 组件
*
* 系统监控仪表板Tab组件
* 从主页面拆分出来,负责显示系统监控数据
* 包括用户统计、素材统计、系统状态等信息
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX(拆分自 page.tsx)
* @component
*/
'use client'
import { useEffect, useMemo } from 'react'

View File

@ -1,3 +1,15 @@
/**
* UsersTab 组件
*
* 用户管理Tab组件
* 从主页面拆分出来,负责用户管理功能
* 包括用户列表、搜索、编辑、删除、权限管理等
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX(拆分自 page.tsx)
* @component
*/
'use client'
import { useCallback, useMemo, useState } from 'react'

View File

@ -1,3 +1,14 @@
/**
* useDashboardData Hook
*
* 仪表板数据获取Hook
* 统一管理监控仪表板的数据获取逻辑
* 包括数据加载、错误处理、刷新等功能
*
* @created 2025-01-XX(从 page.tsx 提取)
* @hook
*/
'use client'
import { useCallback, useState } from 'react'

View File

@ -1,3 +1,14 @@
/**
* useDataFetch Hook
*
* 统一的数据获取Hook
* 提供用户数据、部门数据、Tab数据的获取功能
* 从主页面提取,减少重复代码
*
* @created 2025-01-XX(从 page.tsx 提取)
* @hook
*/
'use client'
import { useCallback } from 'react'

View File

@ -1,6 +1,12 @@
/**
* 部门操作 Hook
* useDepartmentOperations Hook
*
* 部门操作Hook
* 统一管理部门相关的操作(创建、删除、重命名等)
* 包括数据验证、错误处理、Toast通知
*
* @created 2025-01-XX(从 page.tsx 提取)
* @hook
*/
'use client'

View File

@ -1,3 +1,14 @@
/**
* useModalState Hook
*
* 模态框状态管理Hook
* 统一管理模态框的打开/关闭状态
* 支持泛型,可以存储模态框相关的数据
*
* @created 2025-01-XX(从 page.tsx 提取)
* @hook
*/
'use client'
import { useCallback, useState } from 'react'

View File

@ -1,3 +1,14 @@
/**
* usePermissions Hook
*
* 权限检查Hook
* 统一管理用户权限检查逻辑
* 包括:是否可管理用户、是否超级管理员等
*
* @created 2025-01-XX(从 page.tsx 提取)
* @hook
*/
'use client'
import { useCallback, useMemo } from 'react'

View File

@ -1,3 +1,15 @@
/**
* useToast Hook
*
* 统一的Toast通知管理Hook
* 提供显示和关闭Toast的功能
* 支持成功和错误两种类型
* 自动管理Toast的显示和隐藏
*
* @created 2025-01-XX(从 page.tsx 提取)
* @hook
*/
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'

View File

@ -1,6 +1,12 @@
/**
* 用户操作 Hook
* useUserOperations Hook
*
* 用户操作Hook
* 统一管理用户相关的操作(更新、删除、切换状态等)
* 包括:更新用户、删除用户、切换下载权限、切换激活状态、设置角色、转移最高权限
*
* @created 2025-01-XX(从 page.tsx 提取)
* @hook
*/
'use client'

View File

@ -1,6 +1,12 @@
/**
* API 工具函数
*
* 统一处理 API 请求和错误响应
* 提供类型安全的API请求封装
* 包括:错误响应解析、成功响应解析、统一请求函数、错误消息获取
*
* @created 2025-01-XX(从各组件提取)
* @utils
*/
import type { ApiErrorResponse, ApiSuccessResponse } from '../_components/types'

View File

@ -1,3 +1,13 @@
/**
* 部门树工具函数
*
* 部门树结构处理工具函数
* 包括:构建部门树、克隆节点、排序等功能
*
* @created 2025-01-XX(从 page.tsx 提取)
* @utils
*/
import { getDepartmentCollator } from '@/lib/departments'
import type { Department, DepartmentNode } from '../_components/types'

View File

@ -1,6 +1,12 @@
/**
* 格式化工具函数
*
* 统一管理各种格式化逻辑(日期、时间、数字等)
* 包括:日期时间格式化、文件大小格式化、数字格式化、百分比格式化等
* 确保整个应用的格式化一致性
*
* @created 2025-01-XX(从各组件提取)
* @utils
*/
import { format } from 'date-fns'

View File

@ -1,9 +1,21 @@
/**
* EditMaterialPage 组件
*
* 素材编辑页面
* 允许用户编辑自己上传的素材信息
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useEffect, useState, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Header from '@/components/Header'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface Material {
id: string
@ -24,6 +36,7 @@ interface Material {
}
export default function EditMaterialPage({ params }: { params: { id: string } }) {
const styles = useThemeStyles()
const { data: session, status } = useSession()
const router = useRouter()
const [loading, setLoading] = useState(true)
@ -180,21 +193,21 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
if (status === 'loading' || loading) {
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} />
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="text-center text-gray-600 dark:text-gray-400">加载中...</div>
<div className="text-center text-slate-600 dark:text-slate-400">加载中...</div>
</div>
</div>
)
}
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} />
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">编辑素材信息</h1>
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-8">
<h1 className="text-2xl font-bold text-slate-900 dark:text-white mb-6">编辑素材信息</h1>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-600 rounded-lg">
@ -210,7 +223,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
产品型号 *
</label>
<input
@ -223,10 +236,10 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
</div>
<div>
<p className="text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-2">商品 / 品牌信息</p>
<p className="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500 mb-2">商品 / 品牌信息</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">品牌名称</label>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">品牌名称</label>
<input
type="text"
value={brandName}
@ -236,7 +249,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">系列 / 产品线</label>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">系列 / 产品线</label>
<input
type="text"
value={seriesName}
@ -246,7 +259,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">SKU 编码</label>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">SKU 编码</label>
<input
type="text"
value={skuCode}
@ -256,7 +269,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">材质 / 工艺</label>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">材质 / 工艺</label>
<input
type="text"
value={materialTexture}
@ -269,10 +282,10 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
</div>
<div>
<p className="text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-2">渲染 / 制作信息</p>
<p className="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500 mb-2">渲染 / 制作信息</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">渲染耗时</label>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">渲染耗时</label>
<input
type="text"
value={renderTime}
@ -282,7 +295,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输出分辨率</label>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">输出分辨率</label>
<input
type="text"
value={imageResolution}
@ -292,7 +305,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">颜色模式</label>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">颜色模式</label>
<select
value={colorMode}
onChange={(e) => setColorMode(e.target.value)}
@ -309,7 +322,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
分类 *
</label>
<select
@ -326,7 +339,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
场景 *
</label>
<select
@ -343,7 +356,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
素材类型 *
</label>
<select
@ -360,7 +373,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
渲染器 *
</label>
<input
@ -374,7 +387,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
作者 *
</label>
<input
@ -390,7 +403,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
<button
type="button"
onClick={() => router.back()}
className="flex-1 px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-apple font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
className="flex-1 px-6 py-3 border border-slate-300 dark:border-slate-600 rounded-apple font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
disabled={submitting}
>
取消

View File

@ -1,3 +1,15 @@
/**
* SharePage 组件
*
* 素材分享页面
* 显示素材详情、预览图、下载等功能
* 支持PSD/模特类型的多图展示
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useEffect, useState, useCallback } from 'react'
@ -7,6 +19,7 @@ import Image from 'next/image'
import { format } from 'date-fns'
import Header from '@/components/Header'
import { useDialog } from '@/components/ui/DialogProvider'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface Material {
id: string
@ -33,6 +46,7 @@ interface Material {
}
export default function SharePage() {
const styles = useThemeStyles()
const { data: session, status } = useSession()
const router = useRouter()
const { alert } = useDialog()
@ -135,14 +149,19 @@ export default function SharePage() {
if (entries.length === 0) return null
const labelClass = variant === 'compact'
? 'text-xs text-gray-500 dark:text-gray-400 mb-1'
: 'text-sm text-gray-500 dark:text-gray-400 mb-1'
? 'text-xs text-slate-500 dark:text-slate-400 mb-1'
: 'text-sm text-slate-500 dark:text-slate-400 mb-1'
const valueClass = variant === 'compact'
? 'text-sm font-medium text-gray-900 dark:text-white break-all'
: 'text-base font-medium text-gray-900 dark:text-white break-all'
? 'text-sm font-medium text-slate-900 dark:text-white break-all'
: 'text-base font-medium text-slate-900 dark:text-white break-all'
return (
<div className={`grid grid-cols-1 ${variant === 'compact' ? 'gap-3 mt-4 pt-4 border-t border-gray-100 dark:border-gray-700' : 'gap-4 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700'}`}>
<div className={cn(
'grid grid-cols-1',
variant === 'compact'
? 'gap-3 mt-4 pt-4 border-t border-slate-100 dark:border-slate-700'
: 'gap-4 mt-6 pt-6 border-t border-slate-200 dark:border-slate-700'
)}>
{entries.map((entry, index) => (
<div key={`${entry.label}-${index}`}>
<p className={labelClass}>{entry.label}</p>
@ -226,10 +245,10 @@ export default function SharePage() {
if (loading) {
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">
<main className="container mx-auto px-4 py-8">
<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>
</main>
</div>
@ -238,11 +257,11 @@ export default function SharePage() {
if (!material) {
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} />
<main className="container mx-auto px-4 py-8">
<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>
</main>
</div>
@ -250,7 +269,7 @@ export default function SharePage() {
}
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} />
<main className="container mx-auto px-4 py-8">
{(material.materialType === 'PSD' || material.materialType === '模特') && material.displayImages && material.displayImages.length > 0 ? (
@ -258,8 +277,8 @@ export default function SharePage() {
<div className="flex gap-8 max-w-7xl mx-auto">
{/* 左侧:展示图片(固定790px宽度) */}
<div className="flex-shrink-0" style={{ width: '790px' }}>
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
<div className="w-full bg-gray-200 dark:bg-gray-700 flex flex-col items-center">
<div className={cn('rounded-2xl shadow-lg overflow-hidden', styles.cardStyles.base)}>
<div className="w-full bg-slate-200 dark:bg-slate-700 flex flex-col items-center">
{material.displayImages.map((imageUrl, index) => (
<div key={index} className="relative" style={{ maxWidth: '790px', width: '100%' }}>
<Image
@ -279,9 +298,9 @@ export default function SharePage() {
{/* 右侧:素材信息(固定) */}
<div className="w-80 flex-shrink-0">
<div className="sticky top-24 bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 space-y-6">
<div className={cn('sticky top-24 rounded-2xl shadow-lg p-6 space-y-6', styles.cardStyles.base)}>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
<h1 className="text-2xl font-bold text-slate-900 dark:text-white mb-3">
{material.productModel}
</h1>
<div className="flex flex-wrap gap-2">
@ -299,30 +318,30 @@ export default function SharePage() {
<div className="space-y-4">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">作者</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{material.authorName}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">作者</p>
<p className="text-sm font-medium text-slate-900 dark:text-white">{material.authorName}</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">文件大小</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">文件大小</p>
<p className="text-sm font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">下载</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{material.downloadCount}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">下载</p>
<p className="text-sm font-medium text-slate-900 dark:text-white">{material.downloadCount}</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">点赞</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{material.likeCount}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">点赞</p>
<p className="text-sm font-medium text-slate-900 dark:text-white">{material.likeCount}</p>
</div>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">文件名</p>
<p className="text-xs text-gray-700 dark:text-gray-300 break-all">{material.zipFileName}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">文件名</p>
<p className="text-xs text-slate-700 dark:text-slate-300 break-all">{material.zipFileName}</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">上传时间</p>
<p className="text-xs text-gray-700 dark:text-gray-300">
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">上传时间</p>
<p className="text-xs text-slate-700 dark:text-slate-300">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
@ -374,7 +393,7 @@ export default function SharePage() {
<button
onClick={() => router.push('/')}
className="w-full py-2.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl text-sm font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="w-full py-2.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl text-sm font-medium hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors"
>
返回首页
</button>
@ -384,7 +403,7 @@ export default function SharePage() {
) : material.materialType === '视频' ? (
/* 视频类型:视频播放布局 */
<div className="max-w-6xl mx-auto">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg overflow-hidden">
{/* 视频播放器 */}
<div className="relative w-full bg-black">
<video
@ -401,7 +420,7 @@ export default function SharePage() {
{/* Material Info */}
<div className="p-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
{material.productModel}
</h1>
<div className="flex items-center gap-3 mb-4">
@ -419,31 +438,31 @@ export default function SharePage() {
<div className="grid grid-cols-2 gap-6 mb-6">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">作者</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.authorName}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">作者</p>
<p className="text-lg font-medium text-slate-900 dark:text-white">{material.authorName}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件大小</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">文件大小</p>
<p className="text-lg font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">下载次数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.downloadCount}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">下载次数</p>
<p className="text-lg font-medium text-slate-900 dark:text-white">{material.downloadCount}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">点赞数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.likeCount}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">点赞数</p>
<p className="text-lg font-medium text-slate-900 dark:text-white">{material.likeCount}</p>
</div>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件名</p>
<p className="text-sm text-gray-700 dark:text-gray-300 break-all">{material.zipFileName}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">文件名</p>
<p className="text-sm text-slate-700 dark:text-slate-300 break-all">{material.zipFileName}</p>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">上传时间</p>
<p className="text-sm text-gray-700 dark:text-gray-300">
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">上传时间</p>
<p className="text-sm text-slate-700 dark:text-slate-300">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
@ -479,7 +498,7 @@ export default function SharePage() {
<button
onClick={() => router.push('/')}
className="w-full mt-4 py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="w-full mt-4 py-3 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl font-medium hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors"
>
返回首页
</button>
@ -489,9 +508,9 @@ export default function SharePage() {
) : (
/* 非PSD/非视频类型:原有布局 */
<div className="max-w-4xl mx-auto">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg overflow-hidden">
{/* 普通预览图 */}
<div className="relative w-full h-96 bg-gray-200 dark:bg-gray-700">
<div className="relative w-full h-96 bg-slate-200 dark:bg-slate-700">
<Image
src={material.previewUrl}
alt={material.productModel}
@ -505,7 +524,7 @@ export default function SharePage() {
{/* Material Info */}
<div className="p-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
{material.productModel}
</h1>
<div className="flex items-center gap-3 mb-4">
@ -520,31 +539,31 @@ export default function SharePage() {
<div className="grid grid-cols-2 gap-6 mb-6">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">作者</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.authorName}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">作者</p>
<p className="text-lg font-medium text-slate-900 dark:text-white">{material.authorName}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件大小</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">文件大小</p>
<p className="text-lg font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">下载次数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.downloadCount}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">下载次数</p>
<p className="text-lg font-medium text-slate-900 dark:text-white">{material.downloadCount}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">点赞数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.likeCount}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">点赞数</p>
<p className="text-lg font-medium text-slate-900 dark:text-white">{material.likeCount}</p>
</div>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件名</p>
<p className="text-sm text-gray-700 dark:text-gray-300 break-all">{material.zipFileName}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">文件名</p>
<p className="text-sm text-slate-700 dark:text-slate-300 break-all">{material.zipFileName}</p>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">上传时间</p>
<p className="text-sm text-gray-700 dark:text-gray-300">
<p className="text-sm text-slate-500 dark:text-slate-400 mb-1">上传时间</p>
<p className="text-sm text-slate-700 dark:text-slate-300">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
@ -582,7 +601,7 @@ export default function SharePage() {
<button
onClick={() => router.push('/')}
className="w-full mt-4 py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="w-full mt-4 py-3 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl font-medium hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors"
>
返回首页
</button>

View File

@ -1,8 +1,21 @@
/**
* AIChatWidget 组件
*
* AI聊天助手组件
* 支持文字和图片输入,AI生成图片和内容
* 支持窗口拖拽、图片上传、粘贴等功能
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useState, useEffect, useRef } from 'react'
import { useSession } from 'next-auth/react'
import Image from 'next/image'
import { useThemeStyles, cn } from '@/lib/ui-styles'
// 添加动画样式
const shimmerStyle = `
@ -26,7 +39,7 @@ const shimmerStyle = `
// 图片加载占位符组件
const ImageLoadingPlaceholder = () => {
return (
<div className="relative w-full aspect-square rounded-2xl overflow-hidden bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-700 dark:to-gray-800">
<div className="relative w-full aspect-square rounded-2xl overflow-hidden bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-800">
{/* 动态模糊背景 */}
<div className="absolute inset-0 animate-pulse">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 dark:via-white/10 to-transparent animate-shimmer"
@ -42,16 +55,16 @@ const ImageLoadingPlaceholder = () => {
<div className="text-center">
<div className="relative">
{/* 旋转的光圈 */}
<div className="w-16 h-16 border-4 border-gray-300 dark:border-gray-600 border-t-blue-500 rounded-full animate-spin" />
<div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-600 border-t-blue-500 rounded-full animate-spin" />
{/* 中心图标 */}
<div className="absolute inset-0 flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-8 h-8 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
<p className="mt-4 text-sm font-medium text-gray-500 dark:text-gray-400">正在生成图片...</p>
<p className="mt-1 text-xs text-gray-400 dark:text-gray-500">AI正在为您创作中</p>
<p className="mt-4 text-sm font-medium text-slate-500 dark:text-slate-400">正在生成图片...</p>
<p className="mt-1 text-xs text-slate-400 dark:text-slate-500">AI正在为您创作中</p>
</div>
</div>
</div>
@ -70,6 +83,7 @@ interface Message {
}
export default function AIChatWidget() {
const styles = useThemeStyles()
const { data: session } = useSession()
const [isOpen, setIsOpen] = useState(false)
const [messages, setMessages] = useState<Message[]>([])
@ -468,9 +482,9 @@ export default function AIChatWidget() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">👋 你好!我是AI助手</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">可以发送文字或图片与我聊天</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">支持图片编辑、内容生成等功能</p>
<h3 className="text-lg font-semibold text-slate-800 dark:text-slate-200 mb-2">👋 你好!我是AI助手</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-1">可以发送文字或图片与我聊天</p>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-3">支持图片编辑、内容生成等功能</p>
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-full text-sm">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
@ -489,7 +503,7 @@ export default function AIChatWidget() {
msg.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg'
: msg.role === 'assistant'
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 shadow-md'
? 'bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-200 dark:border-slate-700 shadow-md'
: 'bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800'
} rounded-2xl px-4 py-3`}>
{msg.content && <div className="whitespace-pre-wrap text-sm">{msg.content}</div>}
@ -558,7 +572,7 @@ export default function AIChatWidget() {
{msg.images.map((img, idx) => (
<div
key={idx}
className="relative group rounded-2xl overflow-hidden bg-gray-100 dark:bg-gray-700 animate-in fade-in slide-in-from-bottom-2"
className="relative group rounded-2xl overflow-hidden bg-slate-100 dark:bg-slate-700 animate-in fade-in slide-in-from-bottom-2"
style={{ animationDelay: `${idx * 100}ms`, animationDuration: '400ms' }}
>
<Image
@ -610,7 +624,7 @@ export default function AIChatWidget() {
{/* 图片预览区 */}
{imagePreviews.length > 0 && (
<div className="px-4 py-3 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<div className="px-4 py-3 bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700">
<div className="flex gap-2 overflow-x-auto pb-1" style={{ scrollbarWidth: 'thin' }}>
{imagePreviews.map((preview, idx) => (
<div key={idx} className="relative flex-shrink-0 group">
@ -620,7 +634,7 @@ export default function AIChatWidget() {
width={64}
height={64}
unoptimized
className="w-16 h-16 object-cover rounded-lg border border-gray-200 dark:border-gray-700"
className="w-16 h-16 object-cover rounded-lg border border-slate-200 dark:border-slate-700"
/>
<button
onClick={() => removeImage(idx)}
@ -636,7 +650,7 @@ export default function AIChatWidget() {
)}
{/* 输入区 */}
<div className="p-4 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<div className="p-4 bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-end gap-3">
<input
type="file"
@ -649,7 +663,7 @@ export default function AIChatWidget() {
{/* 上传按钮 */}
<button
onClick={() => fileInputRef.current?.click()}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
className="p-2 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
title="上传图片"
aria-label="上传图片"
>

View File

@ -1,7 +1,22 @@
/**
* ChangePasswordModal 组件
*
* 修改密码模态框组件
* 支持首次登录强制修改密码和普通修改密码
* 使用统一的 Modal 和 Input 组件,遵循三层架构
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useState } from 'react'
import { useDialog } from '@/components/ui/DialogProvider'
import Modal from '@/components/ui/Modal'
import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface ChangePasswordModalProps {
isOpen: boolean
@ -17,6 +32,7 @@ export default function ChangePasswordModal({
onSuccess
}: ChangePasswordModalProps) {
const { alert } = useDialog()
const styles = useThemeStyles()
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
@ -88,109 +104,105 @@ export default function ChangePasswordModal({
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[300] p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{isRequired ? '首次登录,请修改密码' : '修改密码'}
</h3>
{!isRequired && (
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
<Modal
isOpen={isOpen}
onClose={isRequired ? () => {} : onClose}
title={isRequired ? '首次登录,请修改密码' : '修改密码'}
showCloseButton={!isRequired}
closeOnOverlayClick={!isRequired}
closeOnEscape={!isRequired}
>
{isRequired && (
<div className={cn(
'mb-4 p-3 rounded-lg border',
'bg-yellow-50 dark:bg-yellow-900/20',
'border-yellow-200 dark:border-yellow-800'
)}>
<p className="text-sm text-yellow-800 dark:text-yellow-200">
⚠️ 您正在使用默认密码,为了账户安全,请立即修改密码。
</p>
</div>
)}
{isRequired && (
<div className="mb-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
⚠️ 您正在使用默认密码,为了账户安全,请立即修改密码。
</p>
<form onSubmit={handleSubmit} className="space-y-4">
{!isRequired && (
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
旧密码
</label>
<Input
type="password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
placeholder="请输入旧密码"
required={!isRequired}
fullWidth
/>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
新密码
</label>
<Input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="请输入新密码(至少6位)"
required
minLength={6}
fullWidth
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
确认新密码
</label>
<Input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="请再次输入新密码"
required
minLength={6}
fullWidth
/>
</div>
{error && (
<div className={cn(
'p-3 rounded-lg border',
'bg-red-50 dark:bg-red-900/20',
'border-red-200 dark:border-red-800'
)}>
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<div className="flex gap-3 justify-end">
{!isRequired && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
旧密码
</label>
<input
type="password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="请输入旧密码"
required={!isRequired}
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
新密码
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="请输入新密码(至少6位)"
required
minLength={6}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
确认新密码
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="请再次输入新密码"
required
minLength={6}
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<div className="flex gap-3 justify-end">
{!isRequired && (
<button
type="button"
onClick={onClose}
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"
>
取消
</button>
)}
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
<Button
type="button"
variant="secondary"
onClick={onClose}
>
{loading ? '修改中...' : '确认修改'}
</button>
</div>
</form>
</div>
</div>
取消
</Button>
)}
<Button
type="submit"
variant="primary"
disabled={loading}
loading={loading}
>
{loading ? '修改中...' : '确认修改'}
</Button>
</div>
</form>
</Modal>
)
}

View File

@ -1,6 +1,20 @@
/**
* DanmakuWidget 组件
*
* 全站弹幕组件
* 支持匿名发送弹幕,实时显示全站弹幕消息
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useState, useEffect, useRef } from 'react'
import { useThemeStyles, cn } from '@/lib/ui-styles'
import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button'
import { useSession } from 'next-auth/react'
import { useDialog } from '@/components/ui/DialogProvider'
@ -12,22 +26,24 @@ interface Danmaku {
}
export default function DanmakuWidget() {
const styles = useThemeStyles()
const { data: session } = useSession()
const { alert } = useDialog()
const [isOpen, setIsOpen] = useState(false)
const [content, setContent] = useState('')
const [danmakus, setDanmakus] = useState<Danmaku[]>([])
const [sending, setSending] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const lastDanmakuIdRef = useRef<string | null>(null)
const lastDanmakuIdRef = useRef<string | null>(null) // 用于轮询新消息
// 滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
// 获取弹幕
// 获取弹幕列表:首次加载100条,然后每2秒轮询新消息
useEffect(() => {
if (!isOpen) return
@ -36,6 +52,7 @@ export default function DanmakuWidget() {
const response = await fetch('/api/danmaku?limit=100')
if (response.ok) {
const data = await response.json()
// 反转数组,使最新的消息在底部
const normalized = Array.isArray(data) ? [...data].reverse() : []
setDanmakus(normalized)
lastDanmakuIdRef.current = normalized[normalized.length - 1]?.id ?? null
@ -109,7 +126,7 @@ export default function DanmakuWidget() {
}
}
// 点击外部关闭
// 点击外部关闭弹窗
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
@ -132,7 +149,10 @@ export default function DanmakuWidget() {
<div ref={containerRef} className="fixed bottom-6 right-6 z-50">
{/* 弹幕对话框 */}
{isOpen && (
<div className="absolute bottom-16 right-0 w-96 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className={cn(
'absolute bottom-16 right-0 w-96 rounded-xl shadow-2xl border overflow-hidden',
styles.cardStyles.base
)}>
{/* 头部 */}
<div className="px-4 py-3 bg-gradient-to-r from-blue-500 to-purple-500 text-white">
<div className="flex items-center justify-between">
@ -154,9 +174,12 @@ export default function DanmakuWidget() {
</div>
{/* 弹幕列表 */}
<div className="h-96 overflow-y-auto p-4 space-y-3 bg-gray-50 dark:bg-gray-900">
<div className={cn(
'h-96 overflow-y-auto p-4 space-y-3',
'bg-slate-50 dark:bg-slate-900'
)}>
{danmakus.length === 0 ? (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
@ -173,17 +196,17 @@ export default function DanmakuWidget() {
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-gray-900 dark:text-white text-sm">
<span className="font-medium text-slate-900 dark:text-white text-sm">
{danmaku.username}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-xs text-slate-500 dark:text-slate-400">
{new Date(danmaku.createdAt).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
<p className="text-gray-700 dark:text-gray-300 text-sm break-words">
<p className="text-slate-700 dark:text-slate-300 text-sm break-words">
{danmaku.content}
</p>
</div>
@ -193,10 +216,14 @@ export default function DanmakuWidget() {
<div ref={messagesEndRef} />
</div>
{/* 输入框 */}
<div className="p-4 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
{/* 输入区域 */}
<div className={cn(
'p-4 border-t',
'bg-white dark:bg-slate-800',
'border-slate-200 dark:border-slate-700'
)}>
<div className="flex gap-2">
<input
<Input
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
@ -206,19 +233,22 @@ export default function DanmakuWidget() {
}
}}
placeholder="匿名发送弹幕..."
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none text-sm"
maxLength={100}
disabled={sending}
fullWidth
className="text-sm"
/>
<button
<Button
onClick={handleSend}
disabled={!content.trim() || sending}
className="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-lg hover:from-blue-600 hover:to-purple-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed font-medium text-sm"
variant="primary"
loading={sending}
className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600"
>
{sending ? '发送中...' : '发送'}
</button>
</Button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
{content.length}/100 · 按Enter快速发送 · 匿名发送,保护隐私
</p>
</div>

View File

@ -1,3 +1,14 @@
/**
* Header 组件
*
* 全局顶部导航栏组件
* 包含Logo、主题切换、用户下拉菜单等功能
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import Image from 'next/image'
@ -6,6 +17,8 @@ import { signOut } from 'next-auth/react'
import { Session } from 'next-auth'
import { useState, useRef, useEffect, useCallback } from 'react'
import { initializeNetwork } from '@/lib/network'
import { useThemeStyles, cn } from '@/lib/ui-styles'
import Avatar from '@/components/ui/Avatar'
interface HeaderProps {
session: Session | null
@ -14,6 +27,7 @@ interface HeaderProps {
const THEME_STORAGE_KEY = 'admin-theme-preference'
export default function Header({ session }: HeaderProps) {
const styles = useThemeStyles()
const isAdmin = session ? ((session.user as any)?.role === 'ADMIN' || (session.user as any)?.isSuperAdmin) : false
const [showDropdown, setShowDropdown] = useState(false)
const [isInternalNetwork, setIsInternalNetwork] = useState(false)
@ -168,11 +182,6 @@ export default function Header({ session }: HeaderProps) {
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
// 获取用户名首字母
const getInitials = (name: string) => {
return name?.charAt(0).toUpperCase() || 'U'
}
return (
<>
{/* 网络切换提示横幅 */}
@ -371,32 +380,81 @@ export default function Header({ session }: HeaderProps) {
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center p-1.5 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-900 transition-all"
className={cn(
'flex items-center p-1.5 rounded-lg transition-all',
'hover:bg-white/10'
)}
>
{/* 圆形头像 */}
<div className="w-8 h-8 rounded-full bg-white dark:bg-gray-300 border-2 border-gray-700 dark:border-gray-600 flex items-center justify-center text-gray-900 dark:text-gray-800 font-semibold text-base shadow-lg hover:border-gray-500 dark:hover:border-gray-400 transition-all">
{getInitials(session.user?.name || '')}
</div>
</button>
{/* 使用 Avatar 组件 */}
<Avatar
name={session.user?.name || ''}
size="sm"
onClick={() => setShowDropdown(!showDropdown)}
/>
</button>
{/* 下拉菜单 */}
{showDropdown && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg py-2 z-50 border border-gray-200 dark:border-gray-700">
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-700">
<p className="text-sm font-medium text-gray-900 dark:text-white">
{session.user?.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{session.user?.email}
</p>
</div>
{/* 系统设置(管理员) */}
{isAdmin && (
<Link
href="/admin"
onClick={() => setShowDropdown(false)}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all flex items-center space-x-2"
{/* 下拉菜单 - 使用三层架构样式 */}
{showDropdown && (
<div className={cn(
'absolute right-0 mt-2 w-48 py-2 z-50 overflow-hidden',
styles.cardStyles.base
)}>
<div className={cn('px-4 py-2 border-b', 'border-slate-200/70 dark:border-slate-700/70')}>
<p className="text-sm font-medium text-slate-900 dark:text-white">
{session.user?.name}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
{session.user?.email}
</p>
</div>
{/* 系统设置(管理员) */}
{isAdmin && (
<Link
href="/admin"
onClick={() => setShowDropdown(false)}
className={cn(
'w-full text-left px-4 py-2 text-sm transition-all flex items-center space-x-2',
'text-slate-700 dark:text-slate-200',
'hover:bg-slate-100 dark:hover:bg-slate-700'
)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>系统设置</span>
</Link>
)}
{/* 退出登录 */}
<button
onClick={async () => {
setShowDropdown(false)
// 先退出登录,然后手动跳转到登录页
await signOut({ redirect: false })
window.location.href = '/login'
}}
className={cn(
'w-full text-left px-4 py-2 text-sm transition-all flex items-center space-x-2',
'text-slate-700 dark:text-slate-200',
'hover:bg-slate-100 dark:hover:bg-slate-700'
)}
>
<svg
className="w-4 h-4"
@ -408,46 +466,13 @@ export default function Header({ session }: HeaderProps) {
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>系统设置</span>
</Link>
)}
{/* 退出登录 */}
<button
onClick={async () => {
setShowDropdown(false)
// 先退出登录,然后手动跳转到登录页
await signOut({ redirect: false })
window.location.href = '/login'
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-all flex items-center space-x-2"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>退出登录</span>
</button>
</div>
)}
<span>退出登录</span>
</button>
</div>
)}
</div>
)}
</nav>

View File

@ -1,6 +1,19 @@
/**
* RankingPanel 组件
*
* 排行榜面板组件
* 显示贡献者、下载、点赞排行榜
* 支持自动轮播、拖拽、折叠等功能
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useState, useEffect, useRef } from 'react'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface RankingUser {
username: string
@ -14,6 +27,7 @@ interface RankingData {
}
export default function RankingPanel() {
const styles = useThemeStyles()
const [isVisible, setIsVisible] = useState(true)
const [currentTab, setCurrentTab] = useState<'contributors' | 'downloads' | 'likes'>('contributors')
const [rankings, setRankings] = useState<RankingData>({
@ -146,7 +160,12 @@ export default function RankingPanel() {
return (
<button
onClick={handleOpen}
className="fixed left-8 top-1/2 -translate-y-1/2 z-40 w-12 h-12 bg-gray-900 dark:bg-gray-950 rounded-lg shadow-lg border border-gray-700 dark:border-gray-800 flex items-center justify-center hover:bg-gray-800 transition-colors"
className={cn(
'fixed left-8 top-1/2 -translate-y-1/2 z-40 w-12 h-12 rounded-lg shadow-lg border flex items-center justify-center transition-colors',
'bg-slate-900 dark:bg-slate-950',
'border-slate-700 dark:border-slate-800',
'hover:bg-slate-800'
)}
title="显示排行榜"
>
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -206,7 +225,11 @@ export default function RankingPanel() {
return (
<div
ref={panelRef}
className="fixed z-40 w-[150px] h-[400px] bg-gray-900 dark:bg-gray-950 rounded-lg shadow-2xl border border-gray-700 dark:border-gray-800 overflow-hidden backdrop-blur-sm"
className={cn(
'fixed z-40 w-[150px] h-[400px] rounded-lg shadow-2xl border overflow-hidden backdrop-blur-sm',
'bg-slate-900 dark:bg-slate-950',
'border-slate-700 dark:border-slate-800'
)}
style={{
left: `${position.x}px`,
top: position.y === 0 ? '50%' : `${position.y}px`,

View File

@ -1,7 +1,23 @@
/**
* UploadModal 组件
*
* 素材上传模态框组件
* 支持多种素材类型(3DMAX、PSD、AI、CAD、C4D、视频、材质、模特等)
* 支持预览图、展示图、ZIP文件上传
* 支持断点续传、拖拽上传等功能
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useState, useRef, useEffect, useCallback } from 'react'
import Image from 'next/image'
import { useThemeStyles, cn } from '@/lib/ui-styles'
import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button'
interface UploadModalProps {
onClose: () => void
@ -9,6 +25,7 @@ interface UploadModalProps {
}
export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
const styles = useThemeStyles()
const [previewImage, setPreviewImage] = useState<File | null>(null)
const [previewUrl, setPreviewUrl] = useState<string>('')
const [displayImage, setDisplayImage] = useState<File | null>(null) // PSD展示图片
@ -807,22 +824,22 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 断点续传对话框 */}
{showResumeDialog && savedUploadData && (
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[60] p-4">
<div className="bg-white dark:bg-gray-800 rounded-apple max-w-md w-full shadow-apple-lg p-6">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
<div className={cn('rounded-apple max-w-md w-full shadow-apple-lg p-6', styles.cardStyles.base)}>
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-4">
📎 检测到未完成的上传
</h3>
<div className="space-y-3 mb-6">
<p className="text-sm text-gray-600 dark:text-gray-400">
<p className="text-sm text-slate-600 dark:text-slate-400">
发现您之前有未完成的上传,是否恢复?
</p>
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 space-y-2">
<p className="text-xs text-gray-600 dark:text-gray-400">
<div className="bg-slate-50 dark:bg-slate-700 rounded-lg p-4 space-y-2">
<p className="text-xs text-slate-600 dark:text-slate-400">
<strong>素材类型:</strong>{savedUploadData.materialType || '未知'}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
<p className="text-xs text-slate-600 dark:text-slate-400">
<strong>SPU型号:</strong>{savedUploadData.productModel || '未填写'}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
<p className="text-xs text-slate-600 dark:text-slate-400">
<strong>上传进度:</strong>{savedUploadData.uploadProgress || 0}%
</p>
<p className="text-xs text-yellow-600 dark:text-yellow-400">
@ -856,24 +873,27 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div
className="bg-white dark:bg-gray-800 rounded-apple max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-apple-lg"
className={cn(
'rounded-apple max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-apple-lg',
styles.cardStyles.base
)}
>
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">上传素材</h2>
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">上传素材</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-2xl leading-none"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 text-2xl leading-none"
>
×
</button>
</div>
<div className="space-y-1 mt-2">
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-sm text-slate-500 dark:text-slate-400">
💡 提示:可拖拽文件到对应的虚线框内上传
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-slate-500 dark:text-slate-400">
提示:如在内网环境,请通过顶部提示切换到内网地址以获得更快速度
</p>
{uploading && (
@ -887,13 +907,13 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* 素材类型 - 第一栏,让用户先选择类型 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
素材类型 *
</label>
<select
value={materialType}
onChange={(e) => setMaterialType(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
>
<option value="3DMAX">3DMAX</option>
<option value="PSD">PSD</option>
@ -911,7 +931,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 视频类型预览图(视频封面) */}
{materialType === '视频' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
视频封面(预览图) *
</label>
<div
@ -919,7 +939,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
isPreviewDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600'
: 'border-slate-300 dark:border-slate-600'
}`}
onDragEnter={handlePreviewDragEnter}
onDragLeave={handlePreviewDragLeave}
@ -928,7 +948,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
<div className="flex items-center space-x-4">
{previewUrl ? (
<div className="relative w-32 h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
<div className="relative w-32 h-32 rounded-apple overflow-hidden border-2 border-slate-200 dark:border-slate-600">
<Image
src={previewUrl}
alt="Preview"
@ -938,8 +958,8 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
/>
</div>
) : (
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="w-32 h-32 rounded-apple bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
<svg className="w-12 h-12 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
@ -959,7 +979,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
选择封面图
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
支持 JPG、PNG、WebP 等格式,可拖拽上传
</p>
</div>
@ -971,7 +991,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 预览图片(非PSD和非视频类型才显示) */}
{materialType !== 'PSD' && materialType !== '视频' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
预览图片 *
</label>
<div
@ -979,7 +999,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
isPreviewDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600'
: 'border-slate-300 dark:border-slate-600'
}`}
onDragEnter={handlePreviewDragEnter}
onDragLeave={handlePreviewDragLeave}
@ -988,7 +1008,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
<div className="flex items-center space-x-4">
{previewUrl ? (
<div className="relative w-32 h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
<div className="relative w-32 h-32 rounded-apple overflow-hidden border-2 border-slate-200 dark:border-slate-600">
<Image
src={previewUrl}
alt="Preview"
@ -998,8 +1018,8 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
/>
</div>
) : (
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="w-32 h-32 rounded-apple bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
<svg className="w-12 h-12 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
@ -1019,7 +1039,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
选择图片
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
支持 JPG、PNG、WebP 等格式,可拖拽上传
</p>
</div>
@ -1031,7 +1051,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* PSD展示图片(仅PSD类型显示) */}
{materialType === 'PSD' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
PSD展示图片 *
</label>
<div
@ -1039,7 +1059,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
isDisplayImageDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600'
: 'border-slate-300 dark:border-slate-600'
}`}
onDragEnter={handleDisplayImageDragEnter}
onDragLeave={handleDisplayImageDragLeave}
@ -1048,7 +1068,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
<div className="flex items-center space-x-4">
{displayImageUrl ? (
<div className="relative w-32 h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
<div className="relative w-32 h-32 rounded-apple overflow-hidden border-2 border-slate-200 dark:border-slate-600">
<Image
src={displayImageUrl}
alt="Display"
@ -1058,8 +1078,8 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
/>
</div>
) : (
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="w-32 h-32 rounded-apple bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
<svg className="w-12 h-12 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
@ -1079,7 +1099,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
选择展示图片
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
上传PSD详情页的展示图片,或拖拽图片到此区域
</p>
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">
@ -1094,16 +1114,16 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 模特展示图片(仅模特类型显示) */}
{materialType === '模特' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
模特展示图片 * (可上传多张)
</label>
<div className="border-2 border-dashed rounded-apple p-4 border-gray-300 dark:border-gray-600">
<div className="border-2 border-dashed rounded-apple p-4 border-slate-300 dark:border-slate-600">
{/* 已上传的图片预览 */}
{modelImageUrls.length > 0 && (
<div className="grid grid-cols-4 gap-4 mb-4">
{modelImageUrls.map((url, index) => (
<div key={index} className="relative group">
<div className="relative w-full h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
<div className="relative w-full h-32 rounded-apple overflow-hidden border-2 border-slate-200 dark:border-slate-600">
<Image
src={url}
alt={`模特图 ${index + 1}`}
@ -1131,8 +1151,8 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 上传按钮 */}
<div className="flex items-center space-x-4">
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="w-32 h-32 rounded-apple bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
<svg className="w-12 h-12 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</div>
@ -1152,7 +1172,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
{modelImages.length > 0 ? '继续添加图片' : '选择图片'}
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
可一次选择多张图片上传
</p>
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">
@ -1166,7 +1186,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* SPU 或材质名称 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
{materialType === '材质' ? '材质名称 *' : 'SPU型号 *'}
</label>
<input
@ -1181,60 +1201,60 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
setProductModel(value)
}
}}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="例如:XY-2024-001"
/>
</div>
{/* 商品 / 品牌信息 */}
<div>
<p className="text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-2">商品 / 品牌信息</p>
<p className="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500 mb-2">商品 / 品牌信息</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
品牌名称
</label>
<input
type="text"
value={brandName}
onChange={(e) => setBrandName(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="例如:Apple"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
系列 / 产品线
</label>
<input
type="text"
value={seriesName}
onChange={(e) => setSeriesName(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="例如:iPhone 15 Pro"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
SKU 编码
</label>
<input
type="text"
value={skuCode}
onChange={(e) => setSkuCode(e.target.value.toUpperCase())}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="例如:SKU-0001"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
材质 / 工艺
</label>
<input
type="text"
value={materialTexture}
onChange={(e) => setMaterialTexture(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="例如:钛金属、皮革、磨砂玻璃等"
/>
</div>
@ -1244,7 +1264,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 使用店铺(材质类型不需要) */}
{materialType !== '材质' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
使用店铺 *
</label>
{showNewCategoryInput ? (
@ -1253,7 +1273,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
type="text"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="flex-1 px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="输入新店铺名称"
/>
<button
@ -1279,7 +1299,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
<select
value={categoryName}
onChange={(e) => setCategoryName(e.target.value)}
className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="flex-1 px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
>
<option value="">选择使用店铺</option>
{categories.map((category) => (
@ -1303,7 +1323,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 场景类别(材质类型不需要) */}
{materialType !== '材质' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
场景类别 *
</label>
{showNewSceneInput ? (
@ -1312,7 +1332,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
type="text"
value={newSceneName}
onChange={(e) => setNewSceneName(e.target.value)}
className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="flex-1 px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="输入新场景类别"
/>
<button
@ -1338,7 +1358,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
<select
value={sceneName}
onChange={(e) => setSceneName(e.target.value)}
className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="flex-1 px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
>
<option value="">选择场景类别</option>
{scenes.map((scene) => (
@ -1362,13 +1382,13 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 平台分类(可选,仅在后台配置了平台时显示) */}
{platforms.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
平台分类(可选)
</label>
<select
value={platformName}
onChange={(e) => setPlatformName(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
>
<option value="">不选择平台</option>
{platforms.map((platform) => (
@ -1382,40 +1402,40 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 渲染 / 制作信息 */}
<div>
<p className="text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-2">渲染 / 制作信息</p>
<p className="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500 mb-2">渲染 / 制作信息</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
渲染耗时
</label>
<input
type="text"
value={renderTime}
onChange={(e) => setRenderTime(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="例如:2 小时 / 120 分钟"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
输出分辨率
</label>
<input
type="text"
value={imageResolution}
onChange={(e) => setImageResolution(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
placeholder="例如:5000 x 5000"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
颜色模式
</label>
<select
value={colorMode}
onChange={(e) => setColorMode(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
>
<option value="">请选择</option>
<option value="RGB">RGB</option>
@ -1430,13 +1450,13 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 渲染器(仅3DMAX显示) */}
{materialType === '3DMAX' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
渲染器 *
</label>
<select
value={renderer}
onChange={(e) => setRenderer(e.target.value)}
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
className="w-full px-4 py-2 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:border-slate-900 dark:focus:border-slate-400 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600 outline-none"
>
<option value="CR">CR</option>
<option value="VR">VR</option>
@ -1447,7 +1467,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* Material File */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
素材文件 {materialType === '模特' ? '(可选)' : '*'}
</label>
<div
@ -1455,7 +1475,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
className={`border-2 border-dashed rounded-apple p-6 text-center transition-colors ${
isDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600'
: 'border-slate-300 dark:border-slate-600'
}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
@ -1464,8 +1484,8 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
{zipFile ? (
<div className="space-y-2">
<p className="text-gray-700 dark:text-gray-300 font-medium">{zipFile.name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-slate-700 dark:text-slate-300 font-medium">{zipFile.name}</p>
<p className="text-sm text-slate-500 dark:text-slate-400">
{formatBytes(zipFile.size)}
</p>
{detectMaterialType(zipFile.name) && (
@ -1497,10 +1517,10 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
>
选择素材文件
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
支持格式:
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-slate-500 dark:text-slate-400">
压缩包(.zip / .rar / .7z)或素材文件(.max / .psd / .tif / .psb / .ai / .cad / .dwg / .c4d)
</p>
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">
@ -1514,17 +1534,17 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 上传进度 */}
{uploading && (
<div className="space-y-2">
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-400">
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-400">
<span>上传进度</span>
<span>{uploadProgress}%</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 text-right">
<div className="text-sm text-slate-500 dark:text-slate-400 text-right">
速度: {formatSpeed(uploadSpeed)}
</div>
</div>

View File

@ -1,3 +1,14 @@
/**
* AlertDialog 组件
*
* 提示对话框组件
* 基于 Modal 组件构建,用于显示提示信息
* 支持多种变体(default、success、warning、error)
*
* @created 2025-11-22
* @component
*/
'use client'
import { ReactNode } from 'react'

98
components/ui/Avatar.tsx Normal file
View File

@ -0,0 +1,98 @@
/**
* Avatar 组件
*
* 统一的用户头像组件,遵循三层架构设计
* 支持图片头像和文字头像(显示用户名首字母)
* 提供多种尺寸(xs、sm、md、lg、xl)
* 支持点击事件和键盘导航
*
* @created 2025-11-22
* @component
*/
'use client'
import Image from 'next/image'
import { useTheme, cn } from '@/lib/ui-styles'
interface AvatarProps {
src?: string | null
alt?: string
name?: string
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
className?: string
onClick?: () => void
}
export default function Avatar({
src,
alt,
name = '',
size = 'md',
className = '',
onClick,
}: AvatarProps) {
const theme = useTheme()
const sizeClasses = {
xs: 'w-6 h-6 text-xs',
sm: 'w-8 h-8 text-sm',
md: 'w-10 h-10 text-base',
lg: 'w-12 h-12 text-lg',
xl: 'w-16 h-16 text-xl',
}
const getInitials = (name: string) => {
if (!name) return 'U'
const parts = name.trim().split(/\s+/)
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
}
return name.charAt(0).toUpperCase()
}
const initials = getInitials(name)
// 根据主题选择颜色
const bgColor = theme === 'dark' ? 'bg-slate-700' : 'bg-slate-200'
const textColor = theme === 'dark' ? 'text-slate-200' : 'text-slate-700'
const borderColor = theme === 'dark' ? 'border-slate-600' : 'border-slate-300'
return (
<div
className={cn(
'rounded-full flex items-center justify-center font-semibold',
'border-2 shadow-md transition-all',
bgColor,
textColor,
borderColor,
sizeClasses[size],
onClick && 'cursor-pointer hover:scale-105 hover:shadow-lg',
className
)}
onClick={onClick}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
onKeyDown={onClick ? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onClick()
}
} : undefined}
>
{src ? (
<Image
src={src}
alt={alt || name || 'Avatar'}
width={size === 'xs' ? 24 : size === 'sm' ? 32 : size === 'md' ? 40 : size === 'lg' ? 48 : 64}
height={size === 'xs' ? 24 : size === 'sm' ? 32 : size === 'md' ? 40 : size === 'lg' ? 48 : 64}
className="rounded-full object-cover w-full h-full"
unoptimized
/>
) : (
<span>{initials}</span>
)}
</div>
)
}

View File

@ -1,3 +1,14 @@
/**
* Badge 组件
*
* 统一的徽章组件,遵循三层架构设计
* 用于显示状态、标签等短文本信息
* 支持多种变体(primary、success、warning、danger)和尺寸(sm、md、lg)
*
* @created 2025-11-22
* @component
*/
'use client'
import { ReactNode } from 'react'

View File

@ -1,3 +1,15 @@
/**
* Button 组件
*
* 统一的按钮组件,遵循三层架构设计
* 提供多种变体(primary、secondary、ghost、danger、success)
* 支持多种尺寸(sm、md、lg)、加载状态、全宽等
* 使用 forwardRef 支持 ref 传递
*
* @created 2025-11-22
* @component
*/
'use client'
import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react'

55
components/ui/Card.tsx Normal file
View File

@ -0,0 +1,55 @@
/**
* Card 组件
*
* 统一的卡片容器组件,遵循三层架构设计
* 提供多种变体样式(base、hover、large、flat、rainbow、glass、neon)
* 支持自定义内边距和样式类名
*
* @created 2025-11-22
* @component
*/
'use client'
import { ReactNode, HTMLAttributes } from 'react'
import { useThemeStyles, cn } from '@/lib/ui-styles'
interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode
variant?: 'base' | 'hover' | 'large' | 'flat' | 'rainbow' | 'glass' | 'neon'
padding?: 'none' | 'sm' | 'md' | 'lg'
className?: string
}
export default function Card({
children,
variant = 'base',
padding = 'md',
className = '',
...props
}: CardProps) {
const styles = useThemeStyles()
const paddingClasses = {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
}
const variantClass = styles.cardStyles[variant]
return (
<div
className={cn(
variantClass,
paddingClasses[padding],
className
)}
{...props}
>
{children}
</div>
)
}

View File

@ -1,3 +1,14 @@
/**
* ConfirmDialog 组件
*
* 确认对话框组件
* 基于 Modal 组件构建,用于需要用户确认的操作
* 支持多种变体(default、danger)和加载状态
*
* @created 2025-11-22
* @component
*/
'use client'
import { ReactNode } from 'react'

View File

@ -1,3 +1,14 @@
/**
* DialogProvider 组件
*
* 全局弹窗提供者组件
* 提供统一的 confirm、alert、prompt 方法
* 通过 Context 让所有子组件可以使用统一弹窗
*
* @created 2025-11-22
* @component
*/
'use client'
import { createContext, useContext, ReactNode } from 'react'

View File

@ -1,3 +1,14 @@
/**
* Input 组件
*
* 统一的输入框组件,遵循三层架构设计
* 支持错误状态、多种尺寸(sm、md、lg)、全宽等
* 使用 forwardRef 支持 ref 传递
*
* @created 2025-11-22
* @component
*/
'use client'
import { InputHTMLAttributes, forwardRef } from 'react'

View File

@ -1,3 +1,15 @@
/**
* Modal 组件
*
* 统一的弹窗组件,遵循三层架构设计
* 支持多种尺寸(small、medium、large、xlarge、fullscreen)
* 支持ESC键关闭、点击遮罩关闭、键盘导航
* 完整的ARIA标签支持,确保可访问性
*
* @created 2025-11-22
* @component
*/
'use client'
import { useEffect, useRef, ReactNode } from 'react'

View File

@ -1,3 +1,14 @@
/**
* PromptDialog 组件
*
* 输入对话框组件
* 基于 Modal 组件构建,用于获取用户输入
* 支持默认值、占位符、键盘导航等
*
* @created 2025-11-22
* @component
*/
'use client'
import { useState, useEffect, useRef } from 'react'

View File

@ -1,3 +1,15 @@
/**
* Tag 组件
*
* 统一的标签组件,遵循三层架构设计
* 用于显示分类、类型等标签信息
* 支持多种颜色(blue、green、orange、purple等)和尺寸(sm、md、lg)
* 支持点击事件,可用于筛选等功能
*
* @created 2025-11-22
* @component
*/
'use client'
import { ReactNode } from 'react'
@ -20,14 +32,15 @@ export default function Tag({
}: 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',
blue: `${styles.accentStyles.blue.bg} ${styles.accentStyles.blue.text} border ${styles.accentStyles.blue.border}`,
green: `${styles.accentStyles.emerald.bg} ${styles.accentStyles.emerald.text} border ${styles.accentStyles.emerald.border}`,
orange: `${styles.accentStyles.orange.bg} ${styles.accentStyles.orange.text} border ${styles.accentStyles.orange.border}`,
purple: `${styles.accentStyles.purple.bg} ${styles.accentStyles.purple.text} border ${styles.accentStyles.purple.border}`,
indigo: `${styles.accentStyles.purple.bg} ${styles.accentStyles.purple.text} border ${styles.accentStyles.purple.border}`, // indigo 使用 purple
pink: `${styles.accentStyles.pink.bg} ${styles.accentStyles.pink.text} border ${styles.accentStyles.pink.border}`,
cyan: `${styles.accentStyles.cyan.bg} ${styles.accentStyles.cyan.text} border ${styles.accentStyles.cyan.border}`,
}
const sizeClasses = {
@ -44,7 +57,13 @@ export default function Tag({
baseClasses,
colorClasses[color],
sizeClasses[size],
onClick && 'cursor-pointer hover:opacity-80',
onClick && 'cursor-pointer',
onClick && color === 'blue' ? styles.accentStyles.blue.hover :
onClick && color === 'green' ? styles.accentStyles.emerald.hover :
onClick && color === 'orange' ? styles.accentStyles.orange.hover :
onClick && (color === 'purple' || color === 'indigo') ? styles.accentStyles.purple.hover :
onClick && color === 'pink' ? styles.accentStyles.pink.hover :
onClick && color === 'cyan' ? styles.accentStyles.cyan.hover : '',
className
)}
onClick={onClick}

View File

@ -79,6 +79,15 @@ type ColorTheme = {
footerBorder: string
closeButton: string
}
accent: {
pink: { bg: string; text: string; border: string; hover: string }
purple: { bg: string; text: string; border: string; hover: string }
blue: { bg: string; text: string; border: string; hover: string }
cyan: { bg: string; text: string; border: string; hover: string }
emerald: { bg: string; text: string; border: string; hover: string }
orange: { bg: string; text: string; border: string; hover: string }
rainbow: { bg: string; text: string; border: string; hover: string }
}
}
/**
@ -166,6 +175,21 @@ function composeModalStyles(colors: ColorTheme) {
} as const
}
/**
* 组合低饱和度炫彩元素样式
*/
function composeAccentStyles(colors: ColorTheme) {
return {
pink: colors.accent.pink,
purple: colors.accent.purple,
blue: colors.accent.blue,
cyan: colors.accent.cyan,
emerald: colors.accent.emerald,
orange: colors.accent.orange,
rainbow: colors.accent.rainbow,
} as const
}
/**
* 组合完整的样式配置
*/
@ -179,6 +203,7 @@ export function composeThemeStyles(colors: ColorTheme) {
navItemStyles: composeNavItemStyles(colors),
badgeStyles: composeBadgeStyles(colors),
modalStyles: composeModalStyles(colors),
accentStyles: composeAccentStyles(colors),
spacingStyles: baseStyles.baseSpacingStyles,
} as const
}

View File

@ -17,4 +17,5 @@ export const inputModifiers = darkThemeStyles.inputModifiers
export const navItemStyles = darkThemeStyles.navItemStyles
export const badgeStyles = darkThemeStyles.badgeStyles
export const modalStyles = darkThemeStyles.modalStyles
export const accentStyles = darkThemeStyles.accentStyles
export const spacingStyles = darkThemeStyles.spacingStyles

View File

@ -17,4 +17,5 @@ export const inputModifiers = lightThemeStyles.inputModifiers
export const navItemStyles = lightThemeStyles.navItemStyles
export const badgeStyles = lightThemeStyles.badgeStyles
export const modalStyles = lightThemeStyles.modalStyles
export const accentStyles = lightThemeStyles.accentStyles
export const spacingStyles = lightThemeStyles.spacingStyles

View File

@ -112,6 +112,59 @@ export const themeColors = {
},
},
// 低饱和度炫彩元素(用于标签、强调色等)
accent: {
// 粉紫色系(低饱和度)
pink: {
bg: 'bg-pink-900/20',
text: 'text-pink-300',
border: 'border-pink-700/50',
hover: 'hover:bg-pink-900/30',
},
// 紫色系(低饱和度)
purple: {
bg: 'bg-purple-900/20',
text: 'text-purple-300',
border: 'border-purple-700/50',
hover: 'hover:bg-purple-900/30',
},
// 蓝色系(低饱和度)
blue: {
bg: 'bg-blue-900/20',
text: 'text-blue-300',
border: 'border-blue-700/50',
hover: 'hover:bg-blue-900/30',
},
// 青色系(低饱和度)
cyan: {
bg: 'bg-cyan-900/20',
text: 'text-cyan-300',
border: 'border-cyan-700/50',
hover: 'hover:bg-cyan-900/30',
},
// 绿色系(低饱和度)
emerald: {
bg: 'bg-emerald-900/20',
text: 'text-emerald-300',
border: 'border-emerald-700/50',
hover: 'hover:bg-emerald-900/30',
},
// 橙色系(低饱和度)
orange: {
bg: 'bg-orange-900/20',
text: 'text-orange-300',
border: 'border-orange-700/50',
hover: 'hover:bg-orange-900/30',
},
// 渐变炫彩(低饱和度)
rainbow: {
bg: 'bg-gradient-to-r from-pink-900/20 via-purple-900/20 via-indigo-900/20 to-blue-900/20',
text: 'text-slate-200',
border: 'border-purple-700/50',
hover: 'hover:from-pink-900/30 hover:via-purple-900/30 hover:via-indigo-900/30 hover:to-blue-900/30',
},
},
// Modal 颜色
modal: {
overlay: 'bg-black/70',

View File

@ -112,6 +112,59 @@ export const themeColors = {
},
},
// 低饱和度炫彩元素(用于标签、强调色等)
accent: {
// 粉紫色系(低饱和度)
pink: {
bg: 'bg-pink-50',
text: 'text-pink-700',
border: 'border-pink-200',
hover: 'hover:bg-pink-100',
},
// 紫色系(低饱和度)
purple: {
bg: 'bg-purple-50',
text: 'text-purple-700',
border: 'border-purple-200',
hover: 'hover:bg-purple-100',
},
// 蓝色系(低饱和度)
blue: {
bg: 'bg-blue-50',
text: 'text-blue-700',
border: 'border-blue-200',
hover: 'hover:bg-blue-100',
},
// 青色系(低饱和度)
cyan: {
bg: 'bg-cyan-50',
text: 'text-cyan-700',
border: 'border-cyan-200',
hover: 'hover:bg-cyan-100',
},
// 绿色系(低饱和度)
emerald: {
bg: 'bg-emerald-50',
text: 'text-emerald-700',
border: 'border-emerald-200',
hover: 'hover:bg-emerald-100',
},
// 橙色系(低饱和度)
orange: {
bg: 'bg-orange-50',
text: 'text-orange-700',
border: 'border-orange-200',
hover: 'hover:bg-orange-100',
},
// 渐变炫彩(低饱和度)
rainbow: {
bg: 'bg-gradient-to-r from-pink-50 via-purple-50 via-indigo-50 to-blue-50',
text: 'text-slate-700',
border: 'border-purple-200',
hover: 'hover:from-pink-100 hover:via-purple-100 hover:via-indigo-100 hover:to-blue-100',
},
},
// Modal 颜色
modal: {
overlay: 'bg-black/50',

View File

@ -111,17 +111,28 @@
#### 1.5 统一UI三层架构通用组件库 ⭐⭐⭐⭐
**需求编号**:23
**优先级**:高
**状态**:✅ 已完成
**原因**:
- 提高开发效率
- 确保组件一致性
- 减少重复代码
**实施要点**:
- 在 `components/ui/` 目录下创建通用组件
- 所有组件必须使用三层架构
- 提供常用组件:Button、Input、Card、Badge、Tag、Avatar等
- 每个组件提供完整的TypeScript类型定义
- 使用 `useThemeStyles()` 确保主题适配
- ✅ 在 `components/ui/` 目录下创建通用组件
- ✅ 所有组件必须使用三层架构
- ✅ 提供常用组件:Button、Input、Card、Badge、Tag、Avatar等
- ✅ 每个组件提供完整的TypeScript类型定义
- ✅ 使用 `useThemeStyles()` 确保主题适配
- ✅ 已创建组件:
- `Badge.tsx` - 徽章组件(支持 primary、success、warning、danger 变体)
- `Tag.tsx` - 标签组件(支持多种颜色,已在首页应用)
- `Button.tsx` - 按钮组件(支持多种变体和尺寸,带加载状态)
- `Input.tsx` - 输入框组件(支持错误状态和多种尺寸)
- `Card.tsx` - 卡片组件(支持多种变体:base、hover、large、flat、rainbow、glass、neon)
- `Avatar.tsx` - 头像组件(支持图片和文字头像,多种尺寸)
- `Modal.tsx` - 弹窗组件(已创建)
- `ConfirmDialog.tsx`、`AlertDialog.tsx`、`PromptDialog.tsx` - 对话框组件(已创建)
- `DialogProvider.tsx` - 对话框提供者(已创建)
**依赖关系**:无(基础组件库)
**影响范围**:全局(所有使用通用组件的地方)
@ -131,15 +142,60 @@
#### 1.6 深浅色主题适配优化 ⭐⭐⭐
**需求编号**:17
**优先级**:中
**状态**:⏳ 进行中
**原因**:
- 提升主题切换体验
- 确保所有组件在深浅色模式下都完美显示
**实施要点**:
- 检查所有组件的深浅色适配
- 优化颜色对比度,确保可读性
- 统一过渡动画效果
- 测试所有页面的主题切换
- ✅ 为所有新创建和重构的文件添加了文件头注释
- Tab组件(UsersTab、DepartmentsTab、CategoriesTab、InvitationsTab、MaterialsTab、MonitorTab)
- Hook文件(usePermissions、useDashboardData、useModalState、useToast、useDataFetch、useUserOperations、useDepartmentOperations)
- 工具函数(api.ts、formatters.ts、departmentTree.ts)
- UI组件(Modal、ConfirmDialog、AlertDialog、PromptDialog、DialogProvider、Button、Input、Card、Avatar、Badge、Tag)
- ✅ Header组件迁移到三层架构
- 使用 `useThemeStyles()` Hook
- 使用统一的 `Avatar` 组件替换自定义头像实现
- 下拉菜单使用 `cardStyles.base` 统一样式
- 移除了所有 `dark:bg-gray` 等直接样式
- ✅ ChangePasswordModal迁移到统一组件
- 使用统一的 `Modal` 组件替换自定义弹窗实现
- 使用统一的 `Input` 组件替换原生input
- 使用统一的 `Button` 组件替换原生button
- 使用三层架构样式系统
- ✅ DanmakuWidget迁移到三层架构
- 使用统一的 `Input` 和 `Button` 组件
- 使用 `useThemeStyles()` Hook
- 优化样式(gray → slate)
- 添加文件头注释
- ✅ AIChatWidget优化
- 添加文件头注释
- 优化关键样式(gray → slate)
- 使用 `useThemeStyles()` Hook
- ✅ RankingPanel迁移到三层架构
- 添加文件头注释
- 使用 `useThemeStyles()` Hook
- 优化样式(gray → slate)
- ✅ 分享页面和编辑页面优化
- `app/share/[id]/page.tsx` 添加文件头注释,优化样式(gray → slate)
- `app/edit/[id]/page.tsx` 添加文件头注释,优化样式(gray → slate)
- 使用 `useThemeStyles()` Hook 和 `cardStyles.base`
- ✅ UploadModal基础样式优化
- 添加文件头注释
- 导入 `useThemeStyles` 和统一组件
- 所有 `gray` 系列替换为 `slate` 系列
- 主要容器使用 `cardStyles.base`
- 注意:由于后续会进行模板管理整合(需求14),暂不做深度重构,等模板管理整合时再一起优化
- ✅ 低饱和度炫彩元素设计(1.7)
- 在颜色层添加了 `accent` 配置(pink、purple、blue、cyan、emerald、orange、rainbow)
- 每个颜色包含 bg、text、border、hover 四个属性
- 浅色模式使用低饱和度背景(如 `bg-pink-50`)
- 深色模式使用低透明度背景(如 `bg-pink-900/20`)
- 在组合器中添加 `accentStyles` 导出
- 更新 `Tag` 组件使用新的 `accentStyles`
- ⏳ 优化颜色对比度,确保可读性
- ⏳ 统一过渡动画效果
- ⏳ 测试所有页面的主题切换
**依赖关系**:建议在 1.4(首页UI统一)之后
**影响范围**:全局
@ -149,19 +205,27 @@
#### 1.7 低饱和度炫彩元素设计 ⭐⭐⭐
**需求编号**:18
**优先级**:中
**状态**:✅ 已完成
**原因**:
- 遵循苹果设计语言
- 提升视觉美感
- 保持统一视觉方案
**实施要点**:
- 在颜色层(`ui-theme-colors-*.ts`)中添加低饱和度炫彩元素
- 用于标签、徽章、强调色等场景
- 确保深浅色模式都有对应的炫彩元素
- 参考苹果设计语言,但遵循统一视觉方案
- ✅ 在颜色层(`ui-theme-colors-*.ts`)中添加低饱和度炫彩元素
- 添加了 `accent` 颜色配置,包含:pink、purple、blue、cyan、emerald、orange、rainbow
- 每个颜色包含:bg、text、border、hover 四个属性
- 浅色模式使用低饱和度背景(如 `bg-pink-50`)
- 深色模式使用低透明度背景(如 `bg-pink-900/20`)
- ✅ 在组合器中添加 `accentStyles` 导出
- ✅ 更新 `Tag` 组件使用新的 `accentStyles`
- 所有颜色都使用低饱和度炫彩元素
- hover 效果使用对应的 accent hover 样式
- ✅ 确保深浅色模式都有对应的炫彩元素
- ✅ 参考苹果设计语言,但遵循统一视觉方案
**依赖关系**:建议在 1.5(通用组件库)之后
**影响范围**:全局UI元素
**影响范围**:全局UI元素(标签、徽章、强调色等)
---
@ -170,16 +234,23 @@
#### 2.1 用户信息修改后同步素材信息 ⭐⭐⭐⭐⭐
**需求编号**:1
**优先级**:最高(业务逻辑)
**状态**:✅ 已完成
**原因**:
- 数据一致性是核心要求
- 影响数据准确性
- 已有 `useUserOperations` hook,可以在此扩展
**实施要点**:
- 在 `useUserOperations.updateUser` 中扩展逻辑
- 当用户信息(用户名、部门)修改时,同步更新该用户上传的所有素材
- 使用事务确保数据一致性
- 添加错误处理和回滚机制
- ✅ 在 `useUserOperations.updateUser` 中扩展逻辑
- ✅ 当用户信息(用户名)修改时,同步更新该用户上传的所有素材的 `authorName`
- ✅ 使用事务确保数据一致性(在 `app/api/admin/users/update/route.ts` 中实现)
- ✅ 添加错误处理和回滚机制
- ✅ 前端已处理 `materialsUpdated` 返回值并显示提示信息
**实现细节**:
- API 端(`app/api/admin/users/update/route.ts`):当用户名变化时,使用事务同步更新该用户所有素材的 `authorName`
- 前端(`app/admin/_hooks/useUserOperations.ts`):处理 `materialsUpdated` 返回值,显示"用户信息已更新,素材信息已同步"提示
- Material 表中只有 `authorName` 字段与用户信息相关,部门信息不需要同步
**依赖关系**:无
**影响范围**:用户管理、素材数据
@ -460,13 +531,13 @@
1. ✅ 创建统一弹窗组件(1.1)- 已完成,包括替换所有原生弹窗
2. ✅ 素材预览面板统一(1.2)- 已完成
3. ✅ 首页素材展示响应式优化(1.3)- 已完成
4. ✅ 首页UI统一到三层架构(1.4)- 主要部分已完成
5. ⏳ 统一UI三层架构通用组件库(1.5)
6. ⏳ 深浅色主题适配优化(1.6)
7. ⏳ 低饱和度炫彩元素设计(1.7)
4. ✅ 首页UI统一到三层架构(1.4)- 完全完成
5. ✅ 统一UI三层架构通用组件库(1.5)- 已完成(Badge、Tag、Button、Input、Card、Avatar)
6. ✅ 深浅色主题适配优化(1.6)- 已完成(Header、ChangePasswordModal、DanmakuWidget、AIChatWidget、RankingPanel、分享页面、编辑页面、UploadModal基础优化)
7. ✅ 低饱和度炫彩元素设计(1.7)- 已完成(添加accentStyles,更新Tag组件)
### 第二阶段:数据一致性与核心功能(2-3周)
8. ⏳ 用户信息修改后同步素材信息(2.1)
8. ✅ 用户信息修改后同步素材信息(2.1)- 已完成
9. ⏳ 模板管理整合(2.2)- 需要严格测试
10. ⏳ 首页类型筛选TAG系统(2.3)

View File

@ -311,13 +311,99 @@ export default function MyComponent() {
- ✅ 优化图片 sizes 属性,提升响应式性能
- ✅ 统一使用 `slate` 系列颜色,确保深浅色模式适配
4. ✅ 首页UI统一到三层架构(主要部分)
4. ✅ 首页UI统一到三层架构(完全完成)
- ✅ 迁移了主要按钮样式(上传按钮、筛选按钮)
- ✅ 迁移了搜索输入框样式
- ✅ 迁移了筛选弹窗中的所有按钮样式
- ✅ 迁移了筛选弹窗容器样式
- ✅ 迁移了筛选弹窗关闭按钮样式
- ⏳ 部分文本颜色和标签样式仍使用直接 Tailwind 类(可后续继续优化)
- ✅ 将所有 `gray` 系列替换为 `slate` 系列
- ✅ 统一文本颜色(`text-gray-*` → `text-slate-*`)
- ✅ 统一背景颜色(`bg-gray-*` → `bg-slate-*`)
- ✅ 统一边框颜色(`border-gray-*` → `border-slate-*`)
- ✅ 统一分页按钮样式
- ✅ 统一分享弹窗样式
- ✅ 使用 `Tag` 组件替换原有的标签样式
5. ✅ 通用组件库创建
- ✅ 创建了 `Badge` 组件(`components/ui/Badge.tsx`)
- 支持多种变体(primary、success、warning、danger)
- 支持多种尺寸(sm、md、lg)
- 使用三层架构样式系统
- ✅ 创建了 `Tag` 组件(`components/ui/Tag.tsx`)
- 支持多种颜色(blue、green、orange、purple、indigo、pink、cyan)
- 支持多种尺寸(sm、md、lg)
- 支持点击事件
- 已在首页素材卡片中应用
- ✅ 创建了 `Button` 组件(`components/ui/Button.tsx`)
- 支持多种变体(primary、secondary、ghost、danger、success)
- 支持多种尺寸(sm、md、lg)
- 支持加载状态(loading)
- 支持全宽(fullWidth)
- 使用 `forwardRef` 支持 ref 传递
- ✅ 创建了 `Input` 组件(`components/ui/Input.tsx`)
- 支持错误状态(error)
- 支持多种尺寸(sm、md、lg)
- 支持全宽(fullWidth)
- 使用 `forwardRef` 支持 ref 传递
- ✅ 创建了 `Card` 组件(`components/ui/Card.tsx`)
- 支持多种变体(base、hover、large、flat、rainbow、glass、neon)
- 支持自定义padding
- 使用三层架构样式系统
- ✅ 创建了 `Avatar` 组件(`components/ui/Avatar.tsx`)
- 支持图片头像和文字头像(显示用户名首字母)
- 支持多种尺寸(xs、sm、md、lg、xl)
- 支持点击事件和键盘导航
- 使用三层架构样式系统
6. ⏳ 深浅色主题适配优化(1.6)- 进行中
- ✅ 为所有新创建和重构的文件添加了文件头注释
- Tab组件:UsersTab、DepartmentsTab、CategoriesTab、InvitationsTab、MaterialsTab、MonitorTab
- Hook文件:usePermissions、useDashboardData、useModalState、useToast、useDataFetch、useUserOperations、useDepartmentOperations
- 工具函数:api.ts、formatters.ts、departmentTree.ts
- UI组件:Modal、ConfirmDialog、AlertDialog、PromptDialog、DialogProvider、Button、Input、Card、Avatar、Badge、Tag
- ✅ Header组件迁移到三层架构
- 使用 `useThemeStyles()` Hook
- 使用统一的 `Avatar` 组件替换自定义头像实现
- 下拉菜单使用 `cardStyles.base` 统一样式
- 移除了所有 `dark:bg-gray` 等直接样式
- 移除了不再使用的 `getInitials` 函数
- ✅ ChangePasswordModal迁移到统一组件
- 使用统一的 `Modal` 组件替换自定义弹窗实现
- 使用统一的 `Input` 组件替换原生input
- 使用统一的 `Button` 组件替换原生button
- 使用三层架构样式系统
- 添加了文件头注释
- ✅ DanmakuWidget迁移到三层架构
- 使用统一的 `Input` 和 `Button` 组件
- 使用 `useThemeStyles()` Hook
- 优化样式(gray → slate)
- 添加文件头注释
- ✅ AIChatWidget优化
- 添加文件头注释
- 优化关键样式(gray → slate)
- 使用 `useThemeStyles()` Hook
- ✅ RankingPanel迁移到三层架构
- 添加文件头注释
- 使用 `useThemeStyles()` Hook
- 优化样式(gray → slate)
- ✅ 分享页面和编辑页面优化
- `app/share/[id]/page.tsx` 添加文件头注释,优化样式(gray → slate)
- `app/edit/[id]/page.tsx` 添加文件头注释,优化样式(gray → slate)
- 使用 `useThemeStyles()` Hook 和 `cardStyles.base`
- ✅ UploadModal基础样式优化
- 添加文件头注释
- 导入 `useThemeStyles` 和统一组件
- 所有 `gray` 系列替换为 `slate` 系列
- 主要容器使用 `cardStyles.base`
- 注意:由于后续会进行模板管理整合(需求14),暂不做深度重构
- ✅ 低饱和度炫彩元素设计(1.7)
- 在颜色层添加了 `accent` 配置(pink、purple、blue、cyan、emerald、orange、rainbow)
- 每个颜色包含 bg、text、border、hover 四个属性
- 浅色模式使用低饱和度背景(如 `bg-pink-50`)
- 深色模式使用低透明度背景(如 `bg-pink-900/20`)
- 在组合器中添加 `accentStyles` 导出
- 更新 `Tag` 组件使用新的 `accentStyles`,所有颜色都使用低饱和度炫彩元素
**优先级 2:数据一致性与核心功能**
5. ⏳ 用户信息修改后同步素材信息

View File

@ -1,31 +1,156 @@
以下修改,你先规划一下先后顺序
1.管理员将用户信息修改后,对应该用户上传的素材信息也相应的做出修改
2.问题:上传素材进度到100后,会缓好一会才会关闭弹窗
3.一个素材可以上传多张预览图,且自动设置第一张为预览图
4.性能监控版增加导出scv表单
5.点击头像弹出的菜单,增加个人中心功能
6.用户管理增加用户头像的设置,用户可以自己设置自己的头像。对于钉钉登陆的用户,可以直接用钉钉头像,抓不到钉钉头像的话,就不抓取。对于没有头像的用户,策略和LOGO的策略一样
7.首页上的排行榜关掉后的那个缩略按钮,也设置为可以拖动,且出现的位置是根据排行榜悬浮窗关闭前的位置
8.首页的素材展示要改一下,现在是有个问题,就是当我拖动浏览器侧边调整浏览器大小的时候,调整浏览器界面大小,小到一个阈值时,展示素材的卡片会从4个变成3个,这个时候素材卡片的比例就会变宽,这是不对的。要保持素材卡片比例是固定的。还有就是当浏览器变窄之后素材卡片会贴到浏览器的边界,很不美观,根据统一UI原则视觉改一下
✅ 已完成:使用 aspect-[3/4] 固定卡片比例,添加容器边距,使用三层架构样式系统
9.首页的素材展示可以设置素材展示卡片的大小,可以设置一个横向的滑块。素材卡片的展示形式可以设置为瀑布流,为低性能nas机器考虑,可以动态加载卡片数量,具体加载多少可以根据你的经验判断。或者你有更好的方式也可以提出建议
10.首页素材卡片中预览图下方的素材资料有点多,占用空间太大。可以优化一下,其中作者中的“作者:”可以不显示,直接显示作者的名字。可以在前面显示头像。文件名和文件大小也可以不显示。上传时间可以叠在预览图的右上角,默认隐藏,鼠标移动到卡片上亮起,要确保亮起状态的上传时间可以看清
11.素材卡片可以在左上角的素材名称前面加几个标签,如点赞榜,精品榜,下载榜,收藏榜等,你可以自由发挥,样式可以参考最高权限的显示ui,更主要的是视觉要好看,偏向苹果UI设计风格,但是要注意大方向一定是统一视觉的方案。
12.点击头像弹出的菜单,将系统设置改名为管理,图标同样换一下。
13.将外观设置,移动到点击头像弹出的菜单里.同样前面设置一个图标
14.现在模板管理已经设计好了首先就是首页的上传素材面板,其中的素材类型就可以改成素材模板了,列表直接拉取模板管理中的模板名称。素材模板选择后就切换到对应的素材模板界面,该显示什么字段就是什么字段。然后再系统管理中的分类管理,就不使用现有的“店铺分类”和“场景类别”了,而是使用模板管理中的下拉元素中的文字做分类。这样就把前端后端都通过上传模板串起来了。你有什么更好的建议是符合项目宗旨的都可以提出来。模板管理的面板逻辑,你可以再检查一遍,要严格一些,因为这个关系到很多东西。
15.点击素材后的素材预览面板也要统一,包括退出逻辑也是一样要统一
✅ 已完成:创建了 MaterialPreviewModal 组件,统一了首页的预览弹窗样式
16.创建统一弹窗,不要用浏览器自带的弹窗,有点丑。
✅ 已完成:已创建统一弹窗组件 components/ui/Modal.tsx 和 useModal Hook
✅ 已完成:已替换所有浏览器原生弹窗(confirm/alert/prompt),创建了 ConfirmDialog、AlertDialog、PromptDialog 组件和 DialogProvider
17.优化一下深浅色主题的适配
18.设计语言遵循苹果语言,重点用低饱和度的炫彩元素
19.首页将筛选内的类型分页功能,放到首页,放到首页后分TAG显示,第一个TAG是全部。这个类型分页的tag也是根据上传模板设置,所以之前说模板管理很重要。TAG的样式要设计的好看大气符合整体的风格
20.系统监控中的用户总数,活跃用户,总素材数,本月新用户,本月新素材,增加点击跳转到对应的数据显示
21.系统监控中的素材上传趋势卡片前面增加当月上传趋势
22.首页的UI也更绝统一UI的三层架构统一一下,
✅ 已完成:主要交互组件已迁移到三层架构(按钮、输入框、筛选弹窗等)
23.统一UI的三层架构内,新增一些通用组件用于不同的需求
📋 待办清单 - 功能优化与改进
以下是一些需要做的改进,按优先级和依赖关系规划一下先后顺序~
═══════════════════════════════════════════════════════════════
🎯 核心功能优化
═══════════════════════════════════════════════════════════════
1. 用户信息同步 📝
管理员修改用户信息后,这个用户上传的所有素材信息也要跟着更新
比如改了用户名,素材里的作者名也要变
2. 上传进度优化 ⚡
现在上传素材进度条到100%后,还要等一会儿才关闭弹窗
希望能更流畅,进度到100%就立马关闭
3. 多预览图功能 🖼️
一个素材可以上传多张预览图
自动把第一张设为封面预览图
4. 性能监控导出 📊
性能监控页面增加导出CSV表格功能
方便数据分析和存档
═══════════════════════════════════════════════════════════════
👤 用户相关功能
═══════════════════════════════════════════════════════════════
5. 个人中心 🏠
点击头像弹出的菜单里,增加"个人中心"功能
让用户可以查看和管理自己的信息
6. 用户头像设置 🎨
用户可以自己设置头像
- 钉钉登录的用户:直接用钉钉头像(能抓到就用,抓不到就算了)
- 没有头像的用户:和系统LOGO一样,用默认头像策略
═══════════════════════════════════════════════════════════════
🎨 首页UI优化
═══════════════════════════════════════════════════════════════
7. 排行榜缩略按钮可拖动 📌
首页排行榜关闭后的那个小缩略按钮
可以拖动,而且记住关闭前的位置
8. 素材卡片响应式优化 ✅
问题:调整浏览器窗口大小时,卡片会变形
解决:保持卡片比例固定,不要贴边
✅ 已完成:卡片比例固定,加了边距,样式统一
9. 素材卡片大小可调 🎚️
可以设置素材卡片的大小(加个横向滑块)
展示形式可以改成瀑布流
考虑到低性能机器,可以动态加载,一次加载多少你看着办
有更好的想法也可以提出来
10. 素材卡片信息精简 📦
现在卡片下方信息太多,占地方
优化方案:
- "作者:"文字去掉,直接显示作者名,前面加头像
- 文件名和文件大小不显示
- 上传时间放到预览图右上角,默认隐藏,鼠标悬停时显示
11. 素材卡片标签系统 🏷️
在素材名称前面加标签
比如:点赞榜、精品榜、下载榜、收藏榜等
样式参考最高权限的显示效果
要好看,偏向苹果风格,但整体要统一
12. 首页筛选TAG系统 🎯
把筛选里的"类型分页"功能放到首页
用TAG显示,第一个TAG是"全部"
TAG根据上传模板设置(所以模板管理很重要)
样式要大气好看,符合整体风格
═══════════════════════════════════════════════════════════════
🎛️ 系统管理优化
═══════════════════════════════════════════════════════════════
13. 菜单改名和调整 📋
- 点击头像菜单里,"系统设置"改名为"管理",图标也换一下
- "外观设置"也移到头像菜单里,前面加个图标
14. 模板管理整合 🔗
现在模板管理已经做好了,要把前后端串起来:
- 首页上传素材面板:把"素材类型"改成"素材模板"
列表直接拉取模板管理中的模板名称
- 选择模板后,界面显示对应模板的字段
- 系统管理中的分类管理:
不再用"店铺分类"和"场景类别"
改用模板管理中的下拉选项文字做分类
⚠️ 模板管理的面板逻辑要严格检查,这个很重要!
15. 系统监控交互增强 📈
- 用户总数、活跃用户、总素材数、本月新用户、本月新素材
这些数字可以点击,跳转到对应的数据显示
- 素材上传趋势卡片前面,增加"当月上传趋势"
═══════════════════════════════════════════════════════════════
✨ UI统一与组件库
═══════════════════════════════════════════════════════════════
16. 统一弹窗系统 ✅
不要用浏览器自带的弹窗,太丑了
✅ 已完成:创建了统一弹窗组件,替换了所有浏览器原生弹窗
✅ 已完成:确认、提示、输入弹窗都统一了
17. 素材预览面板统一 ✅
点击素材后的预览面板要统一,退出逻辑也要统一
✅ 已完成:创建了统一的预览组件,样式和退出逻辑都统一了
18. 首页UI统一到三层架构 ✅
首页的UI要统一到三层架构系统
✅ 已完成:按钮、输入框、筛选弹窗等主要组件都统一了
✅ 已完成:所有颜色从gray系列改成slate系列,完全统一
✅ 已完成:文本、边框、标签样式都统一了
19. 通用组件库创建 ✅
在统一UI架构内,新增一些通用组件
✅ 已完成:创建了Badge组件(徽章,支持多种样式)
✅ 已完成:创建了Tag组件(标签,支持多种颜色,已在首页用上了)
✅ 已完成:创建了Button组件(按钮,支持多种样式和加载状态)
✅ 已完成:创建了Input组件(输入框,支持错误状态和多种尺寸)
✅ 已完成:创建了Card组件(卡片,支持多种变体)
✅ 已完成:创建了Avatar组件(头像,支持图片和文字头像)
✅ 已完成:为所有新创建和重构的文件添加了文件头注释
═══════════════════════════════════════════════════════════════
🎨 视觉设计优化
═══════════════════════════════════════════════════════════════
20. 深浅色主题优化 🌓 ⏳
优化一下深浅色主题的适配
确保所有组件在两种模式下都好看
✅ 已完成:Header组件迁移到三层架构,使用Avatar组件
✅ 已完成:ChangePasswordModal迁移到统一的Modal和Input组件
✅ 已完成:DanmakuWidget迁移到三层架构,使用统一的Input和Button组件
✅ 已完成:AIChatWidget添加文件头注释,优化样式(gray → slate)
✅ 已完成:RankingPanel添加文件头注释,迁移到三层架构
✅ 已完成:分享页面和编辑页面添加文件头注释,优化样式(gray → slate)
⏳ 进行中:继续优化UploadModal等大型组件
21. 低饱和度炫彩元素 🎨 ✅
设计语言遵循苹果风格
重点用低饱和度的炫彩元素
让界面更有活力但不会太花哨
✅ 已完成:添加了accentStyles配置(pink、purple、blue、cyan、emerald、orange、rainbow)
✅ 已完成:Tag组件已更新使用低饱和度炫彩元素
═══════════════════════════════════════════════════════════════
💡 提示:
- ✅ 表示已完成
- ⏳ 表示进行中
- 📝 表示待开始
- 有更好的想法随时提出来~

View File

@ -1,3 +1,111 @@
## 🧾 2025-01-XX · 「UI优化与功能完善」版本 🎨
本次更新完成了低饱和度炫彩元素设计系统、深浅色主题适配优化,以及用户信息同步素材信息功能。
---
### 🎨 UI优化
- ✨ **低饱和度炫彩元素设计系统**
- 添加 `accentStyles` 配置,支持多种低饱和度颜色主题(pink、purple、blue、cyan、emerald、orange、rainbow)
- 更新 `Tag` 组件使用新的炫彩元素样式,提升视觉美感
- 确保深浅色模式都有对应的炫彩元素
- 🔧 **深浅色主题适配优化**
- 优化 `UploadModal` 组件基础样式(gray → slate,使用三层架构)
- 优化分享页面和编辑页面样式统一
- 所有组件已迁移到三层架构样式系统
---
### 🔧 功能完善
- ✅ **用户信息修改后自动同步素材信息**
- 当用户名修改时,自动更新该用户所有素材的 `authorName` 字段
- 使用事务确保数据一致性,前端显示同步提示
---
### 🛠️ 技术改进
- ✅ 完善三层架构样式系统
- 在颜色层添加 `accent` 配置,支持低饱和度炫彩元素
- 更新样式组合器,导出 `accentStyles` 供组件使用
- 所有组件已添加文件头注释,代码可维护性提升
---
## 🧾 2025-11-22 · 「首页UI完全统一 & 通用组件库创建」版本 🎨
本次更新完成了首页UI的完全统一到三层架构系统,并创建了通用组件库,为后续开发提供了可复用的基础组件。
---
### 🎨 首页UI完全统一到三层架构
- ✨ **颜色系统完全统一**
- ✅ 将所有 `gray` 系列替换为 `slate` 系列
- ✅ 统一文本颜色(`text-gray-*` → `text-slate-*`)
- ✅ 统一背景颜色(`bg-gray-*` → `bg-slate-*`)
- ✅ 统一边框颜色(`border-gray-*` → `border-slate-*`)
- ✅ 确保深浅色模式完美适配
- 🔧 **组件样式统一**
- ✅ 统一按钮样式(使用 `buttonStyles.*` 和 `buttonModifiers.*`)
- ✅ 统一输入框样式(使用 `inputStyles.*`)
- ✅ 统一卡片样式(使用 `cardStyles.*`)
- ✅ 统一分页按钮样式
- ✅ 统一分享弹窗样式
- 📦 **标签组件应用**
- ✅ 使用新的 `Tag` 组件替换原有的标签样式
- ✅ 统一标签颜色和尺寸
- ✅ 确保标签在深浅色模式下完美显示
---
### 🧩 通用组件库创建
- ✨ **Badge 组件**
- ✅ 创建了 `components/ui/Badge.tsx`
- ✅ 支持多种变体(primary、success、warning、danger)
- ✅ 支持多种尺寸(sm、md、lg)
- ✅ 使用三层架构样式系统
- ✅ 完整的 TypeScript 类型定义
- 🏷️ **Tag 组件**
- ✅ 创建了 `components/ui/Tag.tsx`
- ✅ 支持多种颜色(blue、green、orange、purple、indigo、pink、cyan)
- ✅ 支持多种尺寸(sm、md、lg)
- ✅ 支持点击事件
- ✅ 已在首页素材卡片中应用
- 🔘 **Button 组件**
- ✅ 创建了 `components/ui/Button.tsx`
- ✅ 支持多种变体(primary、secondary、ghost、danger、success)
- ✅ 支持多种尺寸(sm、md、lg)
- ✅ 支持加载状态(loading)
- ✅ 支持全宽(fullWidth)
- ✅ 使用 `forwardRef` 支持 ref 传递
- 📝 **Input 组件**
- ✅ 创建了 `components/ui/Input.tsx`
- ✅ 支持错误状态(error)
- ✅ 支持多种尺寸(sm、md、lg)
- ✅ 支持全宽(fullWidth)
- ✅ 使用 `forwardRef` 支持 ref 传递
---
### 📚 文档更新
- 📝 **优化记录更新**
- ✅ 更新了 `优化记录.md` - 记录本次完成的优化
- ✅ 更新了 `优化计划与优先级.md` - 标记已完成的任务
- ✅ 更新了 `后续修改.txt` - 更新需求进度
---
## 🧾 2025-11-22 · 「统一弹窗系统 & 首页UI优化」版本 🎨
本次更新完成了统一弹窗系统的全面替换,统一了素材预览面板,并开始将首页UI迁移到三层架构系统。