feat: UI优化与功能完善 - 低饱和度炫彩元素、深浅色主题适配、用户信息同步素材
Some checks failed
CI Check / check (push) Has been cancelled
Some checks failed
CI Check / check (push) Has been cancelled
This commit is contained in:
parent
a604b4d488
commit
fe9e3d80a3
@ -1,3 +1,15 @@
|
||||
/**
|
||||
* CategoriesTab 组件
|
||||
*
|
||||
* 分类管理Tab组件
|
||||
* 从主页面拆分出来,负责分类管理功能
|
||||
* 包括店铺分类、场景类别、平台分类的管理
|
||||
* 使用三层架构样式系统,支持深浅色主题
|
||||
*
|
||||
* @created 2025-01-XX(拆分自 page.tsx)
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
/**
|
||||
* DepartmentsTab 组件
|
||||
*
|
||||
* 部门管理Tab组件
|
||||
* 从主页面拆分出来,负责部门管理功能
|
||||
* 包括部门树形结构、创建、删除、重命名等
|
||||
* 使用三层架构样式系统,支持深浅色主题
|
||||
*
|
||||
* @created 2025-01-XX(拆分自 page.tsx)
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
/**
|
||||
* InvitationsTab 组件
|
||||
*
|
||||
* 邀请管理Tab组件
|
||||
* 从主页面拆分出来,负责邀请管理功能
|
||||
* 包括邀请列表、创建、删除、验证等
|
||||
* 使用三层架构样式系统,支持深浅色主题
|
||||
*
|
||||
* @created 2025-01-XX(拆分自 page.tsx)
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
/**
|
||||
* MaterialsTab 组件
|
||||
*
|
||||
* 素材管理Tab组件
|
||||
* 从主页面拆分出来,负责素材管理功能
|
||||
* 包括素材列表、搜索、预览等
|
||||
* 使用三层架构样式系统,支持深浅色主题
|
||||
*
|
||||
* @created 2025-01-XX(拆分自 page.tsx)
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
/**
|
||||
* MonitorTab 组件
|
||||
*
|
||||
* 系统监控仪表板Tab组件
|
||||
* 从主页面拆分出来,负责显示系统监控数据
|
||||
* 包括用户统计、素材统计、系统状态等信息
|
||||
* 使用三层架构样式系统,支持深浅色主题
|
||||
*
|
||||
* @created 2025-01-XX(拆分自 page.tsx)
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
/**
|
||||
* UsersTab 组件
|
||||
*
|
||||
* 用户管理Tab组件
|
||||
* 从主页面拆分出来,负责用户管理功能
|
||||
* 包括用户列表、搜索、编辑、删除、权限管理等
|
||||
* 使用三层架构样式系统,支持深浅色主题
|
||||
*
|
||||
* @created 2025-01-XX(拆分自 page.tsx)
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* useDashboardData Hook
|
||||
*
|
||||
* 仪表板数据获取Hook
|
||||
* 统一管理监控仪表板的数据获取逻辑
|
||||
* 包括数据加载、错误处理、刷新等功能
|
||||
*
|
||||
* @created 2025-01-XX(从 page.tsx 提取)
|
||||
* @hook
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* useDataFetch Hook
|
||||
*
|
||||
* 统一的数据获取Hook
|
||||
* 提供用户数据、部门数据、Tab数据的获取功能
|
||||
* 从主页面提取,减少重复代码
|
||||
*
|
||||
* @created 2025-01-XX(从 page.tsx 提取)
|
||||
* @hook
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
/**
|
||||
* 部门操作 Hook
|
||||
* useDepartmentOperations Hook
|
||||
*
|
||||
* 部门操作Hook
|
||||
* 统一管理部门相关的操作(创建、删除、重命名等)
|
||||
* 包括数据验证、错误处理、Toast通知
|
||||
*
|
||||
* @created 2025-01-XX(从 page.tsx 提取)
|
||||
* @hook
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* useModalState Hook
|
||||
*
|
||||
* 模态框状态管理Hook
|
||||
* 统一管理模态框的打开/关闭状态
|
||||
* 支持泛型,可以存储模态框相关的数据
|
||||
*
|
||||
* @created 2025-01-XX(从 page.tsx 提取)
|
||||
* @hook
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* usePermissions Hook
|
||||
*
|
||||
* 权限检查Hook
|
||||
* 统一管理用户权限检查逻辑
|
||||
* 包括:是否可管理用户、是否超级管理员等
|
||||
*
|
||||
* @created 2025-01-XX(从 page.tsx 提取)
|
||||
* @hook
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
/**
|
||||
* 用户操作 Hook
|
||||
* useUserOperations Hook
|
||||
*
|
||||
* 用户操作Hook
|
||||
* 统一管理用户相关的操作(更新、删除、切换状态等)
|
||||
* 包括:更新用户、删除用户、切换下载权限、切换激活状态、设置角色、转移最高权限
|
||||
*
|
||||
* @created 2025-01-XX(从 page.tsx 提取)
|
||||
* @hook
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
/**
|
||||
* API 工具函数
|
||||
*
|
||||
* 统一处理 API 请求和错误响应
|
||||
* 提供类型安全的API请求封装
|
||||
* 包括:错误响应解析、成功响应解析、统一请求函数、错误消息获取
|
||||
*
|
||||
* @created 2025-01-XX(从各组件提取)
|
||||
* @utils
|
||||
*/
|
||||
|
||||
import type { ApiErrorResponse, ApiSuccessResponse } from '../_components/types'
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
/**
|
||||
* 部门树工具函数
|
||||
*
|
||||
* 部门树结构处理工具函数
|
||||
* 包括:构建部门树、克隆节点、排序等功能
|
||||
*
|
||||
* @created 2025-01-XX(从 page.tsx 提取)
|
||||
* @utils
|
||||
*/
|
||||
|
||||
import { getDepartmentCollator } from '@/lib/departments'
|
||||
import type { Department, DepartmentNode } from '../_components/types'
|
||||
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
/**
|
||||
* 格式化工具函数
|
||||
*
|
||||
* 统一管理各种格式化逻辑(日期、时间、数字等)
|
||||
* 包括:日期时间格式化、文件大小格式化、数字格式化、百分比格式化等
|
||||
* 确保整个应用的格式化一致性
|
||||
*
|
||||
* @created 2025-01-XX(从各组件提取)
|
||||
* @utils
|
||||
*/
|
||||
|
||||
import { format } from 'date-fns'
|
||||
|
||||
@ -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}
|
||||
>
|
||||
取消
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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="上传图片"
|
||||
>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
98
components/ui/Avatar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* Badge 组件
|
||||
*
|
||||
* 统一的徽章组件,遵循三层架构设计
|
||||
* 用于显示状态、标签等短文本信息
|
||||
* 支持多种变体(primary、success、warning、danger)和尺寸(sm、md、lg)
|
||||
*
|
||||
* @created 2025-11-22
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
@ -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
55
components/ui/Card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* ConfirmDialog 组件
|
||||
*
|
||||
* 确认对话框组件
|
||||
* 基于 Modal 组件构建,用于需要用户确认的操作
|
||||
* 支持多种变体(default、danger)和加载状态
|
||||
*
|
||||
* @created 2025-11-22
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* DialogProvider 组件
|
||||
*
|
||||
* 全局弹窗提供者组件
|
||||
* 提供统一的 confirm、alert、prompt 方法
|
||||
* 通过 Context 让所有子组件可以使用统一弹窗
|
||||
*
|
||||
* @created 2025-11-22
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, ReactNode } from 'react'
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* Input 组件
|
||||
*
|
||||
* 统一的输入框组件,遵循三层架构设计
|
||||
* 支持错误状态、多种尺寸(sm、md、lg)、全宽等
|
||||
* 使用 forwardRef 支持 ref 传递
|
||||
*
|
||||
* @created 2025-11-22
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { InputHTMLAttributes, forwardRef } from 'react'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
/**
|
||||
* PromptDialog 组件
|
||||
*
|
||||
* 输入对话框组件
|
||||
* 基于 Modal 组件构建,用于获取用户输入
|
||||
* 支持默认值、占位符、键盘导航等
|
||||
*
|
||||
* @created 2025-11-22
|
||||
* @component
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
117
优化计划与优先级.md
117
优化计划与优先级.md
@ -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)
|
||||
|
||||
|
||||
90
优化记录.md
90
优化记录.md
@ -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. ⏳ 用户信息修改后同步素材信息
|
||||
|
||||
183
后续修改.txt
183
后续修改.txt
@ -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组件已更新使用低饱和度炫彩元素
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
💡 提示:
|
||||
- ✅ 表示已完成
|
||||
- ⏳ 表示进行中
|
||||
- 📝 表示待开始
|
||||
- 有更好的想法随时提出来~
|
||||
|
||||
108
更新日志.md
108
更新日志.md
@ -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迁移到三层架构系统。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user