收敛工作台界面并完善安全分享入口

This commit is contained in:
WuYongJie 2026-05-25 17:30:11 +08:00
parent 5b6dd8b487
commit 1a9921c9fe
23 changed files with 1159 additions and 1201 deletions

View File

@ -1 +1 @@
1.0.0
1.0.6

View File

@ -537,22 +537,18 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
return (
<div className="flex flex-col h-full">
{/* 红色框框:顶部Header区域 */}
<section
className={cn(
'relative overflow-hidden rounded-3xl border mb-6 flex-shrink-0',
'rounded-lg border mb-6 flex-shrink-0',
theme === 'dark'
? 'border-slate-600/60 text-white shadow-[0_28px_60px_rgba(139,92,246,0.2)] hover:shadow-[0_35px_65px_rgba(139,92,246,0.3)]'
: 'border-slate-200/70 text-slate-900 shadow-[0_28px_60px_rgba(15,23,42,0.28)] hover:shadow-[0_35px_65px_rgba(15,23,42,0.35)]',
'transition-all duration-500 p-6',
? 'border-slate-700 bg-slate-900/80 text-white'
: 'border-slate-200 bg-white text-slate-900',
'p-5',
)}
>
<div className="relative z-10 space-y-3">
<p className={cn('text-sm font-semibold tracking-[0.2em] uppercase', theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600')}>
Category Management
</p>
<h2 className={cn('text-2xl font-semibold', textTitle)}>模板分类管理</h2>
<p className={cn('text-sm leading-relaxed', textDesc)}>
<div className="space-y-1.5">
<h2 className={cn('text-xl font-semibold', textTitle)}>模板分类管理</h2>
<p className={cn('text-sm', textDesc)}>
以下分类来自模板管理中勾选了&ldquo;添加到分类&rdquo;的字段,相同字段只显示一个卡片。可以直接添加、删除、重命名选项。
</p>
</div>
@ -564,11 +560,10 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
<div className="w-[320px] flex-shrink-0 space-y-5">
<div className={cn(
styles.cardStyles.base,
'p-5 backdrop-blur-sm flex flex-col',
'p-5 flex flex-col',
'sticky top-24 max-h-[calc(100vh-12rem)]',
cardAnimations.lift
)}>
<p className={cn('text-xs font-semibold tracking-[0.3em] uppercase mb-4 flex-shrink-0', theme === 'dark' ? 'text-slate-200' : 'text-slate-500')}>
<p className={cn('text-sm font-medium mb-4 flex-shrink-0', theme === 'dark' ? 'text-slate-200' : 'text-slate-600')}>
分类列表
</p>
{/* 按钮移动到底部,顶部仅保留标题 */}
@ -617,7 +612,7 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
{isEditingFields && (
<>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-indigo-500 mb-2">已启用字段</p>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">已启用字段</p>
{orderedEnabledFields.length === 0 ? (
<div className={cn('text-xs text-slate-500 dark:text-slate-400')}>暂无启用字段</div>
) : (
@ -648,8 +643,8 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
<div
ref={setNodeRef}
className={cn(
'rounded-2xl border border-slate-200 dark:border-slate-700 px-4 py-3 flex items-center gap-3 bg-white/90 dark:bg-slate-900/40',
isDragging ? 'ring-2 ring-indigo-200 shadow-xl' : '',
'rounded-lg border border-slate-200 dark:border-slate-700 px-4 py-3 flex items-center gap-3 bg-white dark:bg-slate-900/60',
isDragging ? 'border-indigo-300 bg-indigo-50/70 dark:border-indigo-700 dark:bg-indigo-950/30' : '',
)}
>
<button
@ -702,7 +697,7 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
)}
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-slate-400 mb-2">可添加字段</p>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">可添加字段</p>
<div className="space-y-2">
{disabledFieldMeta.length === 0 && (
<div className={cn('text-xs text-slate-500 dark:text-slate-400')}>暂无可添加字段</div>
@ -710,7 +705,7 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
{disabledFieldMeta.map((field) => (
<div
key={`${field.key}-disabled`}
className="rounded-2xl border border-dashed border-indigo-300 px-4 py-3 flex items-center justify-between gap-3"
className="rounded-lg border border-dashed border-slate-300 dark:border-slate-700 px-4 py-3 flex items-center justify-between gap-3"
>
<div>
<p className="text-sm font-semibold text-slate-900 dark:text-white">{field.label}</p>
@ -807,7 +802,7 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
key={`${category.fieldKey}-${option}-${index}`}
className={cn(
styles.cardStyles.flat,
'p-2 space-y-1.5 hover:shadow-md transition-all duration-300 hover:-translate-y-0.5',
'p-2 space-y-1.5 transition-colors',
)}
>
{isRenaming ? (

View File

@ -128,7 +128,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
`"${String(value ?? '').replace(/"/g, '""')}"`
try {
const [materialsResponse, logsResponse] = await Promise.all([
fetch('/api/materials?pageSize=10000'),
fetch('/api/admin/materials/export?limit=10000'),
fetch('/api/admin/material-logs?limit=200'),
])
if (!materialsResponse.ok) throw new Error('获取素材数据失败')
@ -477,20 +477,17 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
<div className="space-y-8">
<section
className={cn(
'relative overflow-hidden rounded-3xl border mb-6 flex-shrink-0',
'rounded-lg border mb-6 flex-shrink-0',
theme === 'dark'
? 'border-slate-600/60 text-white shadow-[0_28px_60px_rgba(139,92,246,0.2)] hover:shadow-[0_35px_65px_rgba(139,92,246,0.3)]'
: 'border-slate-200/70 text-slate-900 shadow-[0_28px_60px_rgba(15,23,42,0.28)] hover:shadow-[0_35px_65px_rgba(15,23,42,0.35)]',
'transition-all duration-500 p-6',
? 'border-slate-700 bg-slate-900/80 text-white'
: 'border-slate-200 bg-white text-slate-900',
'p-5',
)}
>
<div className="relative z-10 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-3">
<p className={cn('text-sm font-semibold tracking-[0.2em] uppercase', theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600')}>
System Monitor
</p>
<h2 className={cn('text-2xl font-semibold', textTitle)}>系统监控概览</h2>
<p className={cn('text-sm leading-relaxed', textDesc)}>最近更新:{formattedLastUpdated}</p>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1.5">
<h2 className={cn('text-xl font-semibold', textTitle)}>系统监控概览</h2>
<p className={cn('text-sm', textDesc)}>最近更新:{formattedLastUpdated}</p>
</div>
<div className="flex items-center gap-3">
{refreshing && (
@ -555,21 +552,15 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{OVERVIEW_CARDS.map((card) => {
const CardContent = (
<div className="p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className={cn('w-12 h-12 rounded-full flex items-center justify-center shadow-sm', card.className)}>
<span className="text-white text-xl font-medium">{card.icon}</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className={cn('text-base font-medium truncate', textLabel)}>{card.label}</dt>
<div className="p-5">
<div className="flex items-center gap-4">
<span className={cn('h-10 w-1 rounded-full', card.accentClass)} aria-hidden="true" />
<dl className="min-w-0 flex-1">
<dt className={cn('text-sm font-medium truncate', textLabel)}>{card.label}</dt>
<dd className={cn('text-2xl font-semibold mt-1', textValue)}>{card.getValue(data)}</dd>
</dl>
</div>
</div>
</div>
)
if (card.href) {
@ -578,9 +569,8 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
key={card.label}
className={cn(
styles.cardStyles.base,
'overflow-hidden cursor-pointer transition-all',
'hover:shadow-lg hover:scale-[1.02]',
theme === 'dark' ? 'hover:border-slate-500' : 'hover:border-slate-300'
'overflow-hidden cursor-pointer transition-colors',
theme === 'dark' ? 'hover:border-slate-500 hover:bg-slate-800/80' : 'hover:border-slate-300 hover:bg-slate-50'
)}
onClick={() => router.push(card.href!)}
title={`点击查看${card.label}详情`}
@ -601,7 +591,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className={cn(styles.cardStyles.base, 'p-6')}>
<div className="mb-4 flex items-center justify-between">
<h3 className={cn('text-lg font-semibold', textTitle)}>🆕 本月新素材(每日上传)</h3>
<h3 className={cn('text-lg font-semibold', textTitle)}>本月新素材(每日上传)</h3>
<span className={cn('text-sm', textDesc)}>累计 {currentMonthDailyData.reduce((sum, item) => sum + item.count, 0)}</span>
</div>
<div className="h-64">
@ -631,7 +621,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
<div className={cn(styles.cardStyles.base, 'p-6')}>
<div className="mb-4 flex items-center justify-between">
<h3 className={cn('text-lg font-semibold', textTitle)}>📊 素材上传趋势(最近12个月)</h3>
<h3 className={cn('text-lg font-semibold', textTitle)}>素材上传趋势(最近12个月)</h3>
{monthlyUploads.length > 0 && (
<div className={cn('text-sm font-medium', textValue)}>
最近月份上传:
@ -669,7 +659,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className={cn(styles.cardStyles.base)}>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>🏢 架构分布(一级架构)</h3>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>架构分布(一级架构)</h3>
<div className="space-y-2 h-48 overflow-y-auto pr-1">
{(() => {
// 只显示一级部门(没有"-"分隔符的部门)
@ -707,7 +697,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
</div>
<div className={cn(styles.cardStyles.base)}>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>🛡️ 权限分布</h3>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>权限分布</h3>
<div className="space-y-3">
{data.distribution.roles.length ? (
data.distribution.roles.map((item, index) => (
@ -729,7 +719,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
</div>
<div className={cn(styles.cardStyles.base)}>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>⚙️ 系统信息</h3>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>系统信息</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className={cn('text-sm', textRow)}>站点名称</span>
@ -769,7 +759,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
</div>
<div className={cn(styles.cardStyles.base)}>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>🕒 最近活动</h3>
<h3 className={cn('text-lg font-semibold mb-4', textTitle)}>最近活动</h3>
<div className="overflow-x-auto">
<table className={cn('min-w-full divide-y', theme === 'dark' ? 'divide-slate-700' : 'divide-slate-200')}>
<thead className={theme === 'dark' ? 'bg-slate-700' : 'bg-slate-50'}>
@ -1013,40 +1003,33 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
const OVERVIEW_CARDS: Array<{
label: string
icon: string
className: string
accentClass: string
getValue: (data: DashboardData) => number
href?: string
onClick?: () => void
}> = [
{
label: '总用户数',
icon: '👥',
className: 'bg-blue-500 shadow-blue-500/40',
accentClass: 'bg-slate-400 dark:bg-slate-500',
getValue: (data) => data.overview.totalUsers,
href: '/admin?tab=users',
},
{
label: '活跃用户',
icon: '⚡',
className: 'bg-green-500 shadow-green-500/40',
accentClass: 'bg-emerald-500 dark:bg-emerald-400',
getValue: (data) => data.overview.activeUsers,
href: '/admin?tab=users',
},
{
label: '总素材数',
icon: '📦',
className: 'bg-purple-500 shadow-purple-500/40',
accentClass: 'bg-indigo-500 dark:bg-indigo-400',
getValue: (data) => data.overview.totalMaterials,
href: '/',
},
{
label: '本月新用户',
icon: '🆕',
className: 'bg-yellow-500 shadow-yellow-500/40',
accentClass: 'bg-amber-500 dark:bg-amber-400',
getValue: (data) => data.overview.newUsersThisMonth,
href: '/admin?tab=users',
},
]

View File

@ -67,11 +67,11 @@ export default function UsersTab({
// 根据主题选择颜色类名
const badgeActive = theme === 'dark' ? 'bg-green-900/30 text-green-300' : 'bg-green-100 text-green-700'
const badgeInactive = theme === 'dark' ? 'bg-rose-900/30 text-rose-300' : 'bg-rose-100 text-rose-700'
const badgeRole = theme === 'dark' ? 'bg-blue-900/30 text-blue-300' : 'bg-blue-100 text-blue-700'
const badgeRole = theme === 'dark' ? 'bg-slate-800 text-slate-200' : 'bg-slate-100 text-slate-700'
const badgeNoRole = theme === 'dark' ? 'bg-slate-800 text-slate-200' : 'bg-slate-200 text-slate-700'
const textDesc = theme === 'dark' ? 'text-slate-400' : 'text-slate-500'
const textSuperAdmin = theme === 'dark' ? 'text-indigo-100 group-hover:text-indigo-200' : 'text-indigo-900 group-hover:text-indigo-700'
const textNormal = theme === 'dark' ? 'text-white group-hover:text-slate-200' : 'text-slate-900 group-hover:text-slate-700'
const textSuperAdmin = theme === 'dark' ? 'text-white' : 'text-slate-900'
const textNormal = theme === 'dark' ? 'text-white' : 'text-slate-900'
const userLookup = useMemo(() => {
const map = new Map<string, User>()
@ -428,7 +428,7 @@ export default function UsersTab({
<div className="flex flex-wrap items-center gap-2">
<span
className={cn(
'transition-all duration-200 text-sm',
'transition-colors duration-200 text-sm',
isManager || user.isSuperAdmin
? cn('font-semibold', textSuperAdmin)
: cn('font-medium', textNormal),
@ -437,12 +437,12 @@ export default function UsersTab({
{user.username}
</span>
{user.isSuperAdmin && (
<span className="px-2 py-0.5 bg-gradient-to-r from-purple-600 to-pink-600 text-white text-xs font-semibold rounded-full shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105">
<span className="px-2 py-0.5 border border-slate-300 dark:border-slate-600 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 text-xs font-semibold rounded-md">
最高权限
</span>
)}
{isManager && !user.isSuperAdmin && (
<span className="px-2 py-0.5 bg-gradient-to-r from-purple-500 to-indigo-600 text-white text-xs font-semibold rounded-full shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105">
<span className="px-2 py-0.5 border border-slate-300 dark:border-slate-600 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 text-xs font-semibold rounded-md">
管理员
</span>
)}
@ -464,19 +464,16 @@ export default function UsersTab({
<div className={cn(styles.spacingStyles.section)}>
<section
className={cn(
'relative overflow-hidden rounded-3xl border mb-6 flex-shrink-0',
'relative overflow-hidden rounded-lg border mb-6 flex-shrink-0',
theme === 'dark'
? 'border-slate-600/60 text-white shadow-[0_28px_60px_rgba(139,92,246,0.2)] hover:shadow-[0_35px_65px_rgba(139,92,246,0.3)]'
: 'border-slate-200/70 text-slate-900 shadow-[0_28px_60px_rgba(15,23,42,0.28)] hover:shadow-[0_35px_65px_rgba(15,23,42,0.35)]',
'transition-all duration-500 p-6',
? 'border-slate-700 bg-slate-900/40 text-white'
: 'border-slate-200 bg-white text-slate-900',
'transition-colors duration-200 p-5',
)}
>
<div className="relative z-10 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-3">
<p className={cn('text-sm font-semibold tracking-[0.2em] uppercase', theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600')}>
User Management
</p>
<h2 className={cn('text-2xl font-semibold', textTitle)}>成员目录</h2>
<h2 className={cn('text-xl font-semibold', textTitle)}>成员目录</h2>
<p className={cn('text-sm leading-relaxed', textDesc)}>
结合组织树与搜索框,可按姓名、邮箱或部门快速定位管理员与成员。
</p>
@ -573,4 +570,3 @@ export default function UsersTab({
</>
)
}

View File

@ -48,7 +48,7 @@ function DepartmentHierarchyComponent({
<div className="space-y-3">
{managerCount > 0 && (
<div className="space-y-1">
<div className="text-xs font-semibold text-indigo-600 dark:text-indigo-300">管理员</div>
<div className="text-xs font-semibold text-slate-600 dark:text-slate-300">管理员</div>
<div className="space-y-1">{entry!.managers.map((user) => renderUserRow(user, true))}</div>
</div>
)}
@ -77,16 +77,16 @@ function DepartmentHierarchyComponent({
const depthContainerClasses =
depthLevel === 0
? 'bg-indigo-50/90 dark:bg-indigo-900/50 border-indigo-400 dark:border-indigo-400'
? 'bg-slate-50 dark:bg-slate-900/50 border-slate-400 dark:border-slate-600'
: depthLevel === 1
? 'bg-indigo-50/70 dark:bg-indigo-900/30 border-indigo-300 dark:border-indigo-500/80'
: 'bg-indigo-50/50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-700/80'
? 'bg-white dark:bg-slate-900/30 border-slate-300 dark:border-slate-700'
: 'bg-white dark:bg-slate-900/20 border-slate-200 dark:border-slate-800'
return (
<div key={node.fullName} className="space-y-2">
<div
className={cn(
'group flex items-center justify-between px-4 py-2.5 border-l-[3px] rounded-xl shadow-[0_1px_3px_rgba(15,23,42,0.06)] dark:shadow-[0_1px_3px_rgba(15,23,42,0.55)] transition-all duration-300 ease-out hover:shadow-md hover:scale-[1.01]',
'group flex items-center justify-between px-4 py-2.5 border-l-[3px] rounded-lg transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800',
depthContainerClasses,
)}
onClick={() => {
@ -99,7 +99,7 @@ function DepartmentHierarchyComponent({
{canToggle ? (
<button
type="button"
className="flex-shrink-0 w-6 h-6 flex items-center justify-center rounded hover:bg-indigo-200/80 dark:hover:bg-indigo-700/80 transition-all duration-200 hover:scale-110"
className="flex-shrink-0 w-6 h-6 flex items-center justify-center rounded hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors duration-200"
title={isCollapsed ? '展开' : '折叠'}
onClick={(e) => {
e.stopPropagation()
@ -108,7 +108,7 @@ function DepartmentHierarchyComponent({
>
<svg
className={cn(
'w-4 h-4 transform transition-transform duration-300 ease-out text-indigo-600 dark:text-indigo-300',
'w-4 h-4 transform transition-transform duration-200 ease-out text-slate-600 dark:text-slate-300',
!isCollapsed && 'rotate-90',
)}
fill="none"
@ -130,7 +130,7 @@ function DepartmentHierarchyComponent({
{managerNames.map((name) => (
<span
key={name}
className="px-2 py-0.5 bg-gradient-to-r from-purple-500 to-indigo-600 text-white text-xs font-semibold rounded-full shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105"
className="px-2 py-0.5 border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 text-xs font-semibold rounded-md"
>
{name}
</span>
@ -167,17 +167,17 @@ function DepartmentHierarchyComponent({
sections.push(
<div key="user-tree-root" className="space-y-2">
<div className="group flex items-center justify-between px-4 py-2.5 bg-gradient-to-r from-indigo-50/90 to-purple-50/90 dark:from-indigo-900/50 dark:to-purple-900/50 border-l-4 border-indigo-500 dark:border-indigo-400 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 ease-out hover:scale-[1.01]">
<div className="group flex items-center justify-between px-4 py-2.5 bg-slate-50 dark:bg-slate-900/50 border-l-4 border-slate-500 dark:border-slate-500 rounded-lg transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800">
<div className="flex items-center gap-3">
<button
type="button"
className="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-xl hover:bg-indigo-200/80 dark:hover:bg-indigo-700/80 transition-all duration-200 hover:scale-110 active:scale-95"
className="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-lg hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors duration-200"
title={rootCollapsed ? '展开' : '折叠'}
onClick={() => onToggleDepartmentCollapse('ROOT')}
>
<svg
className={cn(
'w-5 h-5 transform transition-transform duration-200 text-indigo-600 dark:text-indigo-300',
'w-5 h-5 transform transition-transform duration-200 text-slate-600 dark:text-slate-300',
!rootCollapsed && 'rotate-90',
)}
fill="none"

View File

@ -16,8 +16,13 @@ export default function MonitorPage() {
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login')
return
}
}, [status, router])
if (status === 'authenticated' && (session.user as any)?.role !== 'ADMIN') {
router.push('/')
}
}, [status, session, router])
if (status === 'loading') {
return (

View File

@ -78,7 +78,7 @@ interface AuthorMaterialsResponse {
}
export default function AuthorPage() {
const { data: session } = useSession()
const { data: session, status } = useSession()
const params = useParams()
const router = useRouter()
const styles = useThemeStyles()
@ -98,6 +98,8 @@ export default function AuthorPage() {
const pageSize = 20
const fetchMaterials = useCallback(async () => {
if (status !== 'authenticated') return
setLoading(true)
try {
const res = await fetch(
@ -133,11 +135,18 @@ export default function AuthorPage() {
} finally {
setLoading(false)
}
}, [authorName, page])
}, [authorName, page, status])
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login')
return
}
if (status === 'authenticated') {
fetchMaterials()
}, [fetchMaterials])
}
}, [fetchMaterials, router, status])
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
@ -166,8 +175,8 @@ export default function AuthorPage() {
/>
</div>
) : (
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<UserIcon className="w-10 h-10 text-white" />
<div className="w-20 h-20 rounded-full border border-slate-200 bg-slate-100 dark:border-slate-700 dark:bg-slate-800 flex items-center justify-center">
<UserIcon className="w-10 h-10 text-slate-500 dark:text-slate-300" />
</div>
)}
<div className="flex-1">

View File

@ -34,17 +34,17 @@ body {
rgb(var(--background-start-rgb));
}
/* Apple-style button */
/* Legacy button utilities, kept visually neutral for the FLNXI workspace. */
.btn-apple {
@apply px-6 py-2.5 rounded-apple font-medium transition-all duration-200 shadow-apple hover:shadow-apple-lg;
@apply px-6 py-2.5 rounded-lg font-medium transition-colors duration-150 shadow-none;
}
.btn-primary {
@apply btn-apple bg-slate-900 dark:bg-slate-700 text-white hover:bg-slate-800 dark:hover:bg-slate-600 active:scale-95;
@apply btn-apple bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 hover:bg-slate-800 dark:hover:bg-white disabled:opacity-60;
}
.btn-secondary {
@apply btn-apple bg-slate-200 dark:bg-slate-700 text-slate-900 dark:text-slate-200 hover:bg-slate-300 dark:hover:bg-slate-600 active:scale-95;
@apply btn-apple bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700 disabled:opacity-60;
}
/* 滑块样式 */
@ -82,17 +82,17 @@ body {
}
.btn-danger {
@apply btn-apple bg-red-600 dark:bg-red-700 text-white hover:bg-red-700 dark:hover:bg-red-600 active:scale-95;
@apply btn-apple bg-red-600 dark:bg-red-700 text-white hover:bg-red-700 dark:hover:bg-red-600 disabled:opacity-60;
}
/* Apple-style input */
/* Legacy input utility, now matched to the restrained admin controls. */
.input-apple {
@apply w-full px-4 py-3 rounded-apple border border-slate-300 dark:border-slate-600 dark:bg-slate-800 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 transition-all;
@apply w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:border-slate-900 dark:focus:border-slate-500 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 outline-none transition-colors;
}
/* Apple-style card */
/* Legacy card utility, no hover lift or showcase shadow. */
.card-apple {
@apply bg-white dark:bg-slate-800 rounded-apple shadow-apple hover:shadow-apple-lg transition-all duration-300 border border-transparent dark:border-slate-700;
@apply bg-white dark:bg-slate-900 rounded-lg shadow-sm border border-slate-200 dark:border-slate-800;
}
/* Scrollbar styling (light & dark) */
@ -138,69 +138,37 @@ body {
@keyframes modal-overlay-apple {
0% {
opacity: 0;
backdrop-filter: blur(0px);
}
100% {
opacity: 1;
backdrop-filter: blur(25px);
}
}
@keyframes modal-content-apple {
0% {
opacity: 0;
transform: translateY(28px) scale(0.92);
filter: blur(12px);
}
55% {
opacity: 1;
transform: translateY(-4px) scale(1.02);
filter: blur(0px);
transform: translateY(8px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
transform: translateY(0);
}
}
.modal-overlay-apple {
animation: modal-overlay-apple 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
animation: modal-overlay-apple 0.15s ease-out forwards;
}
.modal-content-apple {
position: relative;
animation: modal-content-apple 0.45s cubic-bezier(0.16, 1, 0.3, 1) forwards;
animation: modal-content-apple 0.18s ease-out forwards;
transform-origin: top center;
box-shadow:
0 28px 80px rgba(15, 23, 42, 0.22),
0 6px 24px rgba(15, 23, 42, 0.12);
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.16);
}
.dark .modal-content-apple {
box-shadow:
0 32px 110px rgba(0, 0, 0, 0.8),
0 10px 30px rgba(0, 0, 0, 0.5);
}
.modal-content-apple::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
border: 1px solid rgba(255, 255, 255, 0.65);
box-shadow:
inset 0 0 0 0.5px rgba(255, 255, 255, 0.35),
0 1px 0 rgba(15, 23, 42, 0.08);
pointer-events: none;
}
.dark .modal-content-apple::after {
border-color: rgba(255, 255, 255, 0.18);
box-shadow:
inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
0 1px 0 rgba(0, 0, 0, 0.4);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
}
/* Material 预览弹窗现在重新使用统一 UI Modal 样式,
不再需要单独的溢出与尺寸覆盖规则。 */

View File

@ -8,8 +8,6 @@ import { format } from 'date-fns'
import UploadModal from '@/components/UploadModal'
import AudioVisualizer from '@/components/AudioVisualizer'
import Header from '@/components/Header'
import RankingPanel from '@/components/RankingPanel'
import AIChatWidget from '@/components/AIChatWidget'
import ChangePasswordModal from '@/components/ChangePasswordModal'
import MaterialPreviewModal from '@/components/MaterialPreviewModal'
import Modal from '@/components/ui/Modal'
@ -20,11 +18,15 @@ import MaterialActionButtons from '@/components/MaterialActionButtons'
import EditMaterialModal from '@/components/EditMaterialModal'
import { useThemeStyles, cn } from '@/lib/ui-styles'
import { useDialog } from '@/components/ui/DialogProvider'
import {
BUILTIN_TEMPLATE_NAMES as BUILTIN_TEMPLATE_NAMES_MAP,
BUILTIN_TEMPLATE_TYPES as BUILTIN_TEMPLATE_TYPES_MAP,
} from '@/lib/upload-template-presets'
const TAG_COLOR_PALETTE = ['blue', 'green', 'orange', 'purple', 'indigo', 'pink', 'cyan'] as const
type TagColorToken = (typeof TAG_COLOR_PALETTE)[number]
const FALLBACK_MATERIAL_TYPES = ['3DMAX', 'PSD', 'AI 工作流', '视频素材模板', '模特素材模板', '其他素材模板', 'MD']
const FALLBACK_MATERIAL_TYPES = Array.from(new Set(Object.values(BUILTIN_TEMPLATE_TYPES_MAP)))
const MONTH_KEYS = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'] as const
const VIDEO_STATUS_META: Record<'pending' | 'processing' | 'done' | 'failed', { label: string; className: string }> = {
@ -92,12 +94,7 @@ interface Material {
// 内置预制模板名称(用于区分“自定义模板”与“预制模板”)
const BUILTIN_TEMPLATE_NAMES = new Set([
'3DMAX 素材模板',
'PSD 素材模板',
'AI 工作流',
'视频素材模板',
'模特素材模板',
'其他素材模板',
...Object.values(BUILTIN_TEMPLATE_NAMES_MAP),
])
// 计算素材卡片上用于区分模板的标签文案:
@ -111,7 +108,7 @@ const getMaterialTemplateLabel = (material: Material): string => {
const isBuiltin =
BUILTIN_TEMPLATE_NAMES.has(templateName) ||
BUILTIN_TEMPLATE_NAMES.has(stripped + '素材模板') ||
['3DMAX', 'PSD', 'AI 工作流', '视频', '模特', '其他'].includes(stripped)
Object.values(BUILTIN_TEMPLATE_TYPES_MAP).includes(stripped)
// 非预制模板:认为是“自定义模板”,展示模板名称
if (!isBuiltin) {
@ -125,7 +122,6 @@ const getMaterialTemplateLabel = (material: Material): string => {
type RankingBadge = {
label: string
gradient: string
}
type TemplateFieldMeta = {
@ -151,6 +147,17 @@ type HomeFilterFieldConfig = {
totalCount?: number
}
type ShareTokenItem = {
id: string
materialId: string
createdByUserId: string
createdByName: string
createdAt: string
expiresAt: string
revokedAt: string | null
status: 'active' | 'expired' | 'revoked'
}
export default function Home() {
const { data: session, status } = useSession()
const router = useRouter()
@ -183,6 +190,10 @@ export default function Home() {
const [totalMaterialsCount, setTotalMaterialsCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [shareUrl, setShareUrl] = useState<string | null>(null)
const [shareMaterialId, setShareMaterialId] = useState<string | null>(null)
const [shareTokens, setShareTokens] = useState<ShareTokenItem[]>([])
const [shareTokensLoading, setShareTokensLoading] = useState(false)
const [shareTokensError, setShareTokensError] = useState<string | null>(null)
const [showShareModal, setShowShareModal] = useState(false)
const [showShareNotification, setShowShareNotification] = useState(false)
const searchDebounceRef = useRef<number | null>(null)
@ -399,13 +410,13 @@ export default function Home() {
const likeIndex = materialRankings.likes.indexOf(materialId)
if (premiumIncluded) {
badges.push({ label: '精品', gradient: 'from-amber-400 via-orange-500 to-pink-500' })
badges.push({ label: '精品' })
}
if (downloadIndex > -1 && downloadIndex < 10) {
badges.push({ label: '下载榜', gradient: 'from-sky-500 via-blue-500 to-indigo-600' })
badges.push({ label: '下载榜' })
}
if (likeIndex > -1 && likeIndex < 10) {
badges.push({ label: '点赞榜', gradient: 'from-rose-500 via-fuchsia-500 to-purple-600' })
badges.push({ label: '点赞榜' })
}
return badges
},
@ -418,42 +429,25 @@ export default function Home() {
}
}, [status, router])
// 系统初始化检查和默认密码检查
// 初始管理员强制改密检查
useEffect(() => {
const checkSystemInit = async () => {
const checkInitialAdminPassword = async () => {
if (status !== 'authenticated') return
try {
// 1. 检查系统是否已初始化
const initResponse = await fetch('/api/system/init')
const initData = await initResponse.json()
if (!initData.initialized) {
// 系统未初始化,执行初始化
const initResult = await fetch('/api/system/init', {
method: 'POST'
})
const initResultData = await initResult.json()
if (initResultData.success) {
console.log('系统初始化成功')
}
}
// 检查初始管理员账号是否使用默认密码(只针对 admin@flnxi.com)
const passwordCheck = await fetch('/api/user/check-default-password')
const passwordData = await passwordCheck.json()
if (passwordData.isDefaultPassword) {
console.log('检测到初始管理员账号使用默认密码,强制修改密码')
console.log('检测到系统初始化账号仍需首次改密,强制修改密码')
setIsDefaultPassword(true)
setShowChangePasswordModal(true)
}
} catch (error) {
console.error('系统初始化检查失败:', error)
console.error('初始管理员改密检查失败:', error)
}
}
checkSystemInit()
checkInitialAdminPassword()
}, [status, session])
// 检查是否有未完成的上传(断点续传)
@ -1086,17 +1080,86 @@ export default function Home() {
return 'pending'
}
const formatShareDateTime = (value?: string | null) => {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '-'
return format(date, 'yyyy-MM-dd HH:mm')
}
const getShareTokenStatusMeta = (status: ShareTokenItem['status']) => {
if (status === 'active') {
return {
label: '有效',
className: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-300',
}
}
if (status === 'expired') {
return {
label: '已过期',
className: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-300',
}
}
return {
label: '已撤销',
className: 'border-slate-200 bg-slate-100 text-slate-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400',
}
}
const fetchShareTokens = useCallback(async (materialId: string) => {
setShareTokensLoading(true)
setShareTokensError(null)
try {
const response = await fetch(`/api/materials/share/tokens?materialId=${encodeURIComponent(materialId)}`, {
cache: 'no-store',
})
const data = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(data?.error || '获取分享链接失败')
}
setShareTokens(Array.isArray(data?.tokens) ? data.tokens : [])
} catch (error) {
console.error('[Share] Fetch share tokens failed:', error)
setShareTokens([])
setShareTokensError(error instanceof Error ? error.message : '获取分享链接失败')
} finally {
setShareTokensLoading(false)
}
}, [])
// 分享功能
const handleShare = async (materialId: string) => {
try {
const response = await fetch('/api/materials/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ materialId }),
})
const data = await response.json().catch(() => ({}))
if (!response.ok || !data?.shareToken) {
throw new Error(data?.error || '创建分享链接失败')
}
const base = process.env.NEXT_PUBLIC_PORTAL_BASE_URL || window.location.origin
const url = `${base}/share/${materialId}`
const url = `${base}/share/${materialId}?token=${encodeURIComponent(data.shareToken)}`
setShareUrl(url)
setShareMaterialId(materialId)
setShowShareModal(true)
void fetchShareTokens(materialId)
const copied = await copyTextToClipboard(url)
if (copied) {
setShowShareNotification(true)
setTimeout(() => setShowShareNotification(false), 2000)
}
} catch (error) {
console.error('[Share] Create share token failed:', error)
alert({
title: '分享失败',
message: error instanceof Error ? error.message : '创建分享链接失败,请稍后重试',
variant: 'error',
})
}
}
// 实时搜索:输入时更新 searchQuery,但不做整页刷新,只重新请求列表数据
@ -1124,7 +1187,6 @@ export default function Home() {
message: '链接已复制,可直接分享给同事',
variant: 'success',
})
setShowShareModal(false)
return
}
alert({
@ -1135,6 +1197,45 @@ export default function Home() {
}
}
const handleRevokeShareToken = async (tokenId: string) => {
const confirmed = await confirm({
title: '撤销分享链接',
message: '撤销后,持有该链接的人将不能继续访问这个素材。确定要撤销吗?',
confirmText: '撤销',
cancelText: '取消',
variant: 'danger',
})
if (!confirmed) return
try {
const response = await fetch(`/api/materials/share/tokens/${encodeURIComponent(tokenId)}`, {
method: 'DELETE',
})
const data = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(data?.error || '撤销分享链接失败')
}
if (shareMaterialId) {
await fetchShareTokens(shareMaterialId)
}
alert({
title: '已撤销',
message: '该分享链接已失效',
variant: 'success',
})
} catch (error) {
console.error('[Share] Revoke share token failed:', error)
alert({
title: '撤销失败',
message: error instanceof Error ? error.message : '撤销分享链接失败,请稍后重试',
variant: 'error',
})
}
}
// TAG 按钮区域不再做宽度动画,改为简单左对齐,避免任何因测量/过渡导致的整体闪动
const renderMaterialTypeChips = () => {
@ -1692,8 +1793,6 @@ export default function Home() {
}}
/>
<RankingPanel />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header Section */}
<div className="mb-8 space-y-6">
@ -1885,7 +1984,7 @@ export default function Home() {
return (
<div
key={material.id}
className="w-full rounded-2xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-900 shadow-lg shadow-slate-200/60 dark:shadow-black/40 overflow-hidden flex flex-col transition-transform duration-300 ease-out hover:-translate-y-1 hover:shadow-xl"
className="w-full rounded-2xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-900 shadow-sm overflow-hidden flex flex-col transition-colors duration-200 hover:border-slate-300 dark:hover:border-slate-600"
>
<div
className="relative w-full aspect-[3/4] bg-slate-100 dark:bg-slate-800 cursor-pointer group overflow-hidden rounded-t-2xl"
@ -1895,6 +1994,14 @@ export default function Home() {
rankingBadges: getRankingBadges(material.id, { isPremium: material.isPremium }),
user: material.user, // 确保包含user信息用于头像显示
})
// 记录浏览历史
if (session?.user) {
fetch('/api/user/history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ materialId: material.id }),
}).catch((err) => console.error('记录浏览历史失败:', err))
}
}}
onMouseEnter={() => {
if (canPlayClip) {
@ -1919,7 +2026,7 @@ export default function Home() {
}
}}
poster={videoCover || material.previewUrl}
className="object-cover rounded-t-2xl w-full h-full transition-transform duration-500 group-hover:scale-[1.03] will-change-transform"
className="object-cover rounded-t-2xl w-full h-full will-change-transform"
muted
loop
playsInline
@ -1955,17 +2062,19 @@ export default function Home() {
)
) : isAudioMaterial ? (
// 音频素材卡片 - 支持卡片内播放 + 圆形频谱可视化
<div className="absolute inset-0 flex flex-col items-center justify-center bg-gradient-to-b from-slate-800 to-slate-900 overflow-hidden">
{/* 圆形频谱可视化背景 */}
<div className="absolute inset-0 flex flex-col items-center justify-center bg-slate-900 overflow-hidden">
{/* 圆形频谱可视化背景 - 只在播放时显示 */}
{playingAudioId === material.id && (
<div className="absolute inset-0 flex items-center justify-center">
<AudioVisualizer
audioElement={playingAudioId === material.id ? audioRef.current : null}
isPlaying={playingAudioId === material.id}
audioElement={audioRef.current}
isPlaying={true}
size="sm"
coverImage={material.previewUrl?.startsWith('data:') ? null : material.previewUrl}
showCoverOnPause={true}
coverImage={null}
showCoverOnPause={false}
/>
</div>
)}
{/* 播放进度条(底部) */}
{playingAudioId === material.id && (
<div className="absolute bottom-0 left-0 right-0 h-1 bg-slate-700 z-20">
@ -1975,9 +2084,9 @@ export default function Home() {
/>
</div>
)}
{/* 信息覆盖层(不阻止点击穿透到父级打开预览) */}
{/* 信息覆盖层 */}
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center pointer-events-none">
{/* 播放/暂停按钮 - 只有这个按钮可以点击播放 */}
{/* 播放/暂停按钮 */}
<button
type="button"
onClick={(e) => {
@ -1985,30 +2094,31 @@ export default function Home() {
handleAudioPlay(material.id, audioUrl)
}}
className={cn(
"w-14 h-14 mb-2 rounded-full backdrop-blur-sm flex items-center justify-center border transition-all pointer-events-auto",
"w-14 h-14 rounded-full flex items-center justify-center border transition-colors pointer-events-auto",
playingAudioId === material.id
? "bg-white/30 border-white/50 hover:bg-white/40"
: "bg-white/20 border-white/30 group-hover:scale-110 hover:bg-white/30"
: "bg-white/20 border-white/30 hover:bg-white/30"
)}
>
{playingAudioId === material.id ? (
// 暂停图标
<svg className="w-7 h-7 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
</svg>
) : (
// 播放图标
<svg className="w-7 h-7 text-white ml-1" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
)}
</button>
{/* 音频名称 */}
<p className="text-white text-sm font-medium text-center px-4 line-clamp-1 drop-shadow-lg">{material.productModel}</p>
{/* 时长 */}
{/* 音频名称 - 只在未播放时显示 */}
{playingAudioId !== material.id && (
<>
<p className="text-white text-sm font-medium text-center px-4 line-clamp-1 drop-shadow-lg mt-3">{material.productModel}</p>
{material.audioDuration && (
<p className="text-white/80 text-xs mt-1 drop-shadow">{material.audioDuration}</p>
)}
</>
)}
</div>
</div>
) : (
@ -2017,7 +2127,7 @@ export default function Home() {
alt={material.sceneName}
fill
unoptimized
className="object-cover rounded-t-2xl transition-transform duration-500 group-hover:scale-[1.03] will-change-transform"
className="object-cover rounded-t-2xl will-change-transform"
sizes="(max-width: 640px) 90vw, 320px"
priority={false}
/>
@ -2047,15 +2157,15 @@ export default function Home() {
<span>{videoStatusMeta?.label || '预览生成中'}</span>
</div>
)}
<div className="absolute top-3 left-3 flex flex-col items-start gap-1.5">
{/* 榜单角标和SPU型号 - 使用z-20确保在音频/视频覆盖层之上 */}
<div className="absolute top-3 left-3 flex flex-col items-start gap-1.5 z-20">
{rankingBadges.length > 0 && (
<div className="flex flex-wrap gap-1">
{rankingBadges.map((badge) => (
<span
key={`card-badge-${material.id}-${badge.label}`}
className={cn(
'px-2.5 py-0.5 rounded-full bg-gradient-to-r text-[11px] font-semibold text-white shadow-sm',
badge.gradient
'px-2.5 py-0.5 rounded-full bg-slate-900/80 text-[11px] font-semibold text-white shadow-sm'
)}
>
{badge.label}
@ -2063,11 +2173,12 @@ export default function Home() {
))}
</div>
)}
{/* 视频可预览角标按需求移除,避免视觉信息过载 */}
<div className="rounded-full bg-black/70 text-white px-2.5 py-1 text-xs font-semibold backdrop-blur">
{/* SPU型号 */}
<div className="rounded-full bg-slate-900/80 text-white px-2.5 py-1 text-xs font-semibold">
{material.productModel}
</div>
</div>
{/* 点赞按钮 - 使用z-20确保在音频/视频覆盖层之上,已点赞时始终显示 */}
{!isOwnMaterial && currentUserId && (
<button
type="button"
@ -2076,7 +2187,7 @@ export default function Home() {
handleLike(material.id)
}}
className={cn(
'absolute top-3 right-3 w-8 h-8 rounded-full flex items-center justify-center border transition-all duration-300 shadow-sm backdrop-blur',
'absolute top-3 right-3 w-8 h-8 rounded-full flex items-center justify-center border transition-colors shadow-sm z-20',
material.isLiked
? 'opacity-100 bg-white text-rose-500 border-rose-200 shadow-rose-200/60'
: 'opacity-0 group-hover:opacity-100 bg-white/85 text-slate-600 border-white/60 hover:text-rose-500'
@ -2139,9 +2250,10 @@ export default function Home() {
<div className="flex items-center justify-between mt-3 gap-3">
<div className="flex items-center gap-2">
<Avatar name={material.authorName} src={material.user?.avatar || undefined} size="xs" />
{/* 显示上传者信息,而不是素材内容的作者(如音频歌手) */}
<Avatar name={material.user?.username || material.authorName} src={material.user?.avatar || undefined} size="xs" />
<span className="text-sm text-slate-700 dark:text-slate-200 font-medium">
{material.authorName}
{material.user?.username || material.authorName}
</span>
</div>
<div className="flex items-center gap-3 text-xs text-slate-500 dark:text-slate-400">
@ -2554,26 +2666,36 @@ export default function Home() {
<Modal
isOpen={showShareModal}
onClose={() => setShowShareModal(false)}
size="small"
onClose={() => {
setShowShareModal(false)
setShareTokensError(null)
}}
size="medium"
title="分享素材链接"
showCloseButton
closeOnOverlayClick
closeOnEscape
footer={
<div className="flex justify-end">
<Button variant="secondary" size="md" onClick={() => setShowShareModal(false)}>
<Button
variant="secondary"
size="md"
onClick={() => {
setShowShareModal(false)
setShareTokensError(null)
}}
>
关闭
</Button>
</div>
}
>
<div className="space-y-4">
<div className="space-y-5">
<p className="text-sm text-slate-600 dark:text-slate-300">
复制下方链接发送给同事,对方即可直接访问该素材的分享页面。
新生成的链接会自动复制。下方列表用于查看已有链接状态,必要时可撤销。
</p>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 rounded-2xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/40 px-3 py-2">
<div className="flex items-center gap-2 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/40 px-3 py-2">
<input
readOnly
value={shareUrl ?? ''}
@ -2584,9 +2706,81 @@ export default function Home() {
</Button>
</div>
<p className="text-[11px] text-slate-500 dark:text-slate-400">
如浏览器拦截自动复制,可手动选中上方链接后使用 Ctrl+C(或 ⌘+C)
分享链接默认 7 天有效。如浏览器拦截自动复制,可手动选中上方链接后使用 Ctrl+C(或 ⌘+C)
</p>
</div>
<div className="border-t border-slate-200 dark:border-slate-700 pt-4">
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-slate-900 dark:text-slate-100">已有分享链接</h3>
{shareMaterialId && (
<Button
variant="ghost"
size="sm"
onClick={() => fetchShareTokens(shareMaterialId)}
disabled={shareTokensLoading}
>
刷新
</Button>
)}
</div>
{shareTokensError && (
<div className="mb-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300">
{shareTokensError}
</div>
)}
{shareTokensLoading ? (
<div className="rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-4 text-sm text-slate-500 dark:text-slate-400">
正在加载分享链接...
</div>
) : shareTokens.length === 0 ? (
<div className="rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-4 text-sm text-slate-500 dark:text-slate-400">
暂无历史分享链接。
</div>
) : (
<div className="space-y-2">
{shareTokens.map((token) => {
const statusMeta = getShareTokenStatusMeta(token.status)
const canRevoke = token.status === 'active'
return (
<div
key={token.id}
className="rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 py-3"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className={cn('inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium', statusMeta.className)}>
{statusMeta.label}
</span>
<span className="text-xs text-slate-500 dark:text-slate-400">
创建人:{token.createdByName || '未知'}
</span>
</div>
<div className="grid gap-1 text-xs text-slate-500 dark:text-slate-400 sm:grid-cols-2">
<span>创建:{formatShareDateTime(token.createdAt)}</span>
<span>到期:{formatShareDateTime(token.expiresAt)}</span>
{token.revokedAt && (
<span className="sm:col-span-2">撤销:{formatShareDateTime(token.revokedAt)}</span>
)}
</div>
</div>
{canRevoke && (
<Button
variant="danger"
size="sm"
onClick={() => handleRevokeShareToken(token.id)}
className="shrink-0"
>
撤销
</Button>
)}
</div>
</div>
)
})}
</div>
)}
</div>
</div>
</Modal>
@ -2610,6 +2804,7 @@ export default function Home() {
imageResolution: editingMaterial.imageResolution,
colorMode: editingMaterial.colorMode,
templateId: (editingMaterial as any).templateId,
templateFields: (editingMaterial as any).templateFields,
platformName: (editingMaterial as any).platformName,
// 音频相关字段
audioDuration: (editingMaterial as any).audioDuration,
@ -2624,9 +2819,6 @@ export default function Home() {
/>
)}
{/* AI聊天助手 */}
<AIChatWidget />
{/* 修改密码弹窗 */}
<ChangePasswordModal
isOpen={showChangePasswordModal}

View File

@ -68,7 +68,7 @@ interface Material {
}
export default function ProfilePage() {
const { data: session, status } = useSession()
const { data: session, status, update } = useSession()
const router = useRouter()
const styles = useThemeStyles()
const { alert } = useDialog()
@ -323,8 +323,10 @@ export default function ProfilePage() {
setShowAvatarModal(false)
setAvatarFile(null)
setAvatarPreview('')
// 刷新页面以更新 session 中的头像(JWT callback 会从数据库获取最新头像)
// 由于 JWT callback 每5分钟自动刷新一次,也可以等待自动刷新
// 触发 session 更新,JWT callback 会从数据库获取最新头像
// 使用 update() 方法触发 trigger='update',立即刷新 token
await update()
// 刷新页面确保所有组件使用最新的 session
window.location.reload()
} else {
const error = await res.json()

View File

@ -4,6 +4,9 @@ import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import Link from 'next/link'
import { PASSWORD_POLICY_DESCRIPTION, validatePasswordStrength } from '@/lib/password-policy'
const DEFAULT_LOGIN_LOGO = process.env.NEXT_PUBLIC_DEFAULT_LOGIN_LOGO_URL || ''
export default function RegisterPage() {
const router = useRouter()
@ -15,6 +18,11 @@ export default function RegisterPage() {
const [confirmPassword, setConfirmPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [logoError, setLogoError] = useState(false)
const pageClass = 'min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-950 p-4'
const panelClass = 'w-full max-w-md rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm p-6'
const inputClass = 'w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 focus:border-slate-900 dark:focus:border-slate-500 focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 outline-none transition-colors'
const buttonClass = 'w-full py-3 rounded-xl bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 hover:bg-slate-800 dark:hover:bg-white font-medium transition-colors disabled:opacity-60'
useEffect(() => {
fetchDepartments()
@ -41,8 +49,9 @@ export default function RegisterPage() {
return
}
if (password.length < 6) {
setError('密码长度至少为6个字符')
const passwordValidation = validatePasswordStrength(password)
if (!passwordValidation.valid) {
setError(passwordValidation.error || '密码不符合安全要求')
return
}
@ -73,29 +82,36 @@ export default function RegisterPage() {
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100 p-4">
<div className={pageClass}>
<div className="max-w-md w-full">
{/* Logo */}
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
{DEFAULT_LOGIN_LOGO && !logoError ? (
<Image
src={process.env.NEXT_PUBLIC_DEFAULT_LOGIN_LOGO_URL || ''}
src={DEFAULT_LOGIN_LOGO}
alt="FLNXI素材库"
width={80}
height={80}
unoptimized
className="rounded-apple shadow-apple"
className="rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm"
onError={() => setLogoError(true)}
/>
) : (
<div className="w-20 h-20 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm bg-slate-900 dark:bg-slate-700 flex items-center justify-center">
<span className="text-3xl font-bold text-white">F</span>
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">FLNXI素材库</h1>
<p className="text-slate-600 dark:text-slate-400 mt-2">创建您的账号,开始分享3D素材</p>
)}
</div>
<h1 className="text-2xl font-semibold text-slate-900 dark:text-slate-100">FLNXI素材库</h1>
<p className="text-slate-600 dark:text-slate-400 mt-2">申请加入素材库</p>
</div>
{/* Register Form */}
<div className="card-apple p-8">
<div className={panelClass}>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-apple text-sm">
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-xl text-sm">
{error}
</div>
)}
@ -108,7 +124,7 @@ export default function RegisterPage() {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input-apple"
className={inputClass}
placeholder="your@email.com"
required
/>
@ -122,7 +138,7 @@ export default function RegisterPage() {
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="input-apple"
className={inputClass}
placeholder="选择一个用户名"
required
/>
@ -135,7 +151,7 @@ export default function RegisterPage() {
<select
value={department}
onChange={(e) => setDepartment(e.target.value)}
className="input-apple"
className={inputClass}
>
<option value="">请选择部门</option>
{departments.map((dept) => (
@ -159,9 +175,10 @@ export default function RegisterPage() {
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input-apple"
placeholder="••••••••"
className={inputClass}
placeholder={PASSWORD_POLICY_DESCRIPTION}
required
minLength={10}
/>
</div>
@ -173,15 +190,16 @@ export default function RegisterPage() {
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="input-apple"
className={inputClass}
placeholder="••••••••"
required
minLength={10}
/>
</div>
<button
type="submit"
className="w-full btn-primary"
className={buttonClass}
disabled={loading}
>
{loading ? '注册中...' : '注册'}

View File

@ -12,7 +12,7 @@ interface AudioVisualizerProps {
className?: string
}
// 默认封面图组件 - Apple Music 风格
// 默认音频封面图
function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
const sizeClasses = {
sm: 'w-full h-full',
@ -30,19 +30,15 @@ function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
<div
className={cn(
sizeClasses[size],
'relative overflow-hidden rounded-lg',
'bg-gradient-to-br from-[#1a1a2e] via-[#16213e] to-[#0f3460]'
'relative overflow-hidden rounded-lg border',
'bg-slate-100 border-slate-200 dark:bg-slate-800 dark:border-slate-700'
)}
>
{/* 装饰光晕 */}
<div className="absolute top-1/4 left-1/4 w-1/2 h-1/2 bg-blue-500/20 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-1/3 h-1/3 bg-purple-500/20 rounded-full blur-2xl" />
{/* 中心音符图标 */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="p-4 rounded-full bg-white/10 backdrop-blur-sm border border-white/20">
<div className="p-4 rounded-md bg-white border border-slate-200 dark:bg-slate-900 dark:border-slate-700">
<svg
className={cn(iconSizes[size], 'text-white/80')}
className={cn(iconSizes[size], 'text-slate-500 dark:text-slate-300')}
fill="currentColor"
viewBox="0 0 24 24"
>
@ -54,7 +50,7 @@ function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
)
}
// 圆形放射状频谱可视化器 - 尖刺风格
// 圆形音频频谱可视化器
export default function AudioVisualizer({
audioElement,
isPlaying,
@ -72,7 +68,7 @@ export default function AudioVisualizer({
const [isVisualizerActive, setIsVisualizerActive] = useState(false)
const [showCover, setShowCover] = useState(true)
// 尺寸配置 - 增加频谱条数量以获得更密集的尖刺效果
// 尺寸配置
const sizeConfig = {
sm: { width: 160, height: 160, bars: 64, innerRadius: 25, maxBarHeight: 35 },
md: { width: 280, height: 280, bars: 96, innerRadius: 45, maxBarHeight: 65 },
@ -125,7 +121,7 @@ export default function AudioVisualizer({
}
}, [audioElement])
// 绘制尖刺放射状频谱
// 绘制低调的放射状频谱
const draw = useCallback(() => {
const canvas = canvasRef.current
const analyser = analyserRef.current
@ -141,21 +137,28 @@ export default function AudioVisualizer({
const { width, height, bars, innerRadius, maxBarHeight } = config
const centerX = width / 2
const centerY = height / 2
const isDarkMode = document.documentElement.classList.contains('dark')
// 清除画布
ctx.clearRect(0, 0, width, height)
// 绘制深色背景圆
// 绘制中性背景圆
const bgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius + maxBarHeight + 20)
bgGradient.addColorStop(0, 'rgba(15, 23, 42, 0.95)')
bgGradient.addColorStop(0.4, 'rgba(15, 23, 42, 0.9)')
bgGradient.addColorStop(1, 'rgba(15, 23, 42, 0.7)')
if (isDarkMode) {
bgGradient.addColorStop(0, 'rgba(30, 41, 59, 0.98)')
bgGradient.addColorStop(0.7, 'rgba(15, 23, 42, 0.96)')
bgGradient.addColorStop(1, 'rgba(15, 23, 42, 0.9)')
} else {
bgGradient.addColorStop(0, 'rgba(248, 250, 252, 0.98)')
bgGradient.addColorStop(0.7, 'rgba(241, 245, 249, 0.94)')
bgGradient.addColorStop(1, 'rgba(226, 232, 240, 0.9)')
}
ctx.fillStyle = bgGradient
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius + maxBarHeight + 15, 0, Math.PI * 2)
ctx.fill()
// 绘制频谱尖刺
// 绘制频谱线条
const angleStep = (Math.PI * 2) / bars
const usableData = Math.floor(bufferLength * 0.7)
@ -165,90 +168,55 @@ export default function AudioVisualizer({
const nextIndex = Math.min(dataIndex + 1, usableData - 1)
const value = (dataArray[dataIndex] + dataArray[nextIndex]) / 2
// 添加一些随机变化使效果更自然
const randomFactor = 0.9 + Math.random() * 0.2
const barHeight = Math.max(3, (value / 255) * maxBarHeight * randomFactor)
const barHeight = Math.max(3, (value / 255) * maxBarHeight)
const angle = i * angleStep - Math.PI / 2 // 从顶部开始
// 计算尖刺的起点和终点
// 计算线条的起点和终点
const x1 = centerX + Math.cos(angle) * innerRadius
const y1 = centerY + Math.sin(angle) * innerRadius
const x2 = centerX + Math.cos(angle) * (innerRadius + barHeight)
const y2 = centerY + Math.sin(angle) * (innerRadius + barHeight)
// 创建渐变色 - 使用项目的蓝色/青色主题
const gradient = ctx.createLinearGradient(x1, y1, x2, y2)
const intensity = value / 255
// 根据强度调整颜色
if (intensity > 0.7) {
// 高强度 - 亮青色到白色
gradient.addColorStop(0, 'rgba(34, 211, 238, 0.9)')
gradient.addColorStop(0.5, 'rgba(56, 189, 248, 1)')
gradient.addColorStop(1, 'rgba(255, 255, 255, 1)')
} else if (intensity > 0.4) {
// 中等强度 - 蓝色到青色
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.8)')
gradient.addColorStop(0.5, 'rgba(34, 211, 238, 0.9)')
gradient.addColorStop(1, 'rgba(56, 189, 248, 1)')
} else {
// 低强度 - 深蓝到蓝色
gradient.addColorStop(0, 'rgba(30, 64, 175, 0.6)')
gradient.addColorStop(0.5, 'rgba(59, 130, 246, 0.7)')
gradient.addColorStop(1, 'rgba(96, 165, 250, 0.8)')
}
// 绘制尖刺线条
// 绘制线条
ctx.beginPath()
ctx.strokeStyle = gradient
ctx.lineWidth = 2
ctx.lineCap = 'round'
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
// 在高强度时添加发光效果
if (intensity > 0.5) {
ctx.beginPath()
ctx.strokeStyle = `rgba(56, 189, 248, ${intensity * 0.3})`
ctx.lineWidth = 4
ctx.strokeStyle = isDarkMode
? `rgba(148, 163, 184, ${0.34 + intensity * 0.42})`
: `rgba(37, 99, 235, ${0.28 + intensity * 0.42})`
ctx.lineWidth = 1.5
ctx.lineCap = 'round'
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
}
}
// 绘制内圈发光效果
const innerGlow = ctx.createRadialGradient(centerX, centerY, innerRadius - 10, centerX, centerY, innerRadius + 5)
innerGlow.addColorStop(0, 'rgba(56, 189, 248, 0.1)')
innerGlow.addColorStop(0.5, 'rgba(34, 211, 238, 0.15)')
innerGlow.addColorStop(1, 'rgba(34, 211, 238, 0)')
ctx.fillStyle = innerGlow
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius + 5, 0, Math.PI * 2)
ctx.fill()
// 绘制中心圆 - 深色背景
// 绘制中心圆
const centerGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius - 5)
if (isDarkMode) {
centerGradient.addColorStop(0, 'rgba(15, 23, 42, 1)')
centerGradient.addColorStop(0.7, 'rgba(15, 23, 42, 0.95)')
centerGradient.addColorStop(1, 'rgba(30, 41, 59, 0.9)')
centerGradient.addColorStop(0.7, 'rgba(30, 41, 59, 1)')
centerGradient.addColorStop(1, 'rgba(51, 65, 85, 0.96)')
} else {
centerGradient.addColorStop(0, 'rgba(255, 255, 255, 1)')
centerGradient.addColorStop(0.7, 'rgba(248, 250, 252, 1)')
centerGradient.addColorStop(1, 'rgba(226, 232, 240, 0.96)')
}
ctx.fillStyle = centerGradient
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
ctx.fill()
// 绘制中心圆边框
ctx.strokeStyle = 'rgba(56, 189, 248, 0.3)'
ctx.strokeStyle = isDarkMode ? 'rgba(100, 116, 139, 0.75)' : 'rgba(148, 163, 184, 0.55)'
ctx.lineWidth = 1.5
ctx.beginPath()
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
ctx.stroke()
// 绘制中心音符图标
ctx.fillStyle = 'rgba(56, 189, 248, 0.9)'
ctx.fillStyle = isDarkMode ? 'rgba(203, 213, 225, 0.9)' : 'rgba(71, 85, 105, 0.9)'
ctx.font = `${innerRadius * 0.5}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'

View File

@ -17,6 +17,7 @@ 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'
import { PASSWORD_POLICY_DESCRIPTION, validatePasswordStrength } from '@/lib/password-policy'
interface ChangePasswordModalProps {
isOpen: boolean
@ -48,8 +49,9 @@ export default function ChangePasswordModal({
return
}
if (newPassword.length < 6) {
setError('新密码长度至少为6位')
const passwordValidation = validatePasswordStrength(newPassword)
if (!passwordValidation.valid) {
setError(passwordValidation.error || '新密码不符合安全要求')
return
}
@ -61,15 +63,16 @@ export default function ChangePasswordModal({
setLoading(true)
try {
const payload = isRequired
? { newPassword }
: { oldPassword, newPassword }
const response = await fetch('/api/user/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
oldPassword: isRequired ? '88888888' : oldPassword,
newPassword
}),
body: JSON.stringify(payload),
})
const data = await response.json()
@ -120,7 +123,7 @@ export default function ChangePasswordModal({
'border-yellow-200 dark:border-yellow-800'
)}>
<p className="text-sm text-yellow-800 dark:text-yellow-200">
⚠️ 您正在使用默认密码,为了账户安全,请立即修改密码。
⚠️ 您正在使用系统初始化密码,为了账户安全,请立即修改密码。
</p>
</div>
)}
@ -150,9 +153,9 @@ export default function ChangePasswordModal({
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="请输入新密码(至少6位)"
placeholder={PASSWORD_POLICY_DESCRIPTION}
required
minLength={6}
minLength={10}
fullWidth
/>
</div>
@ -167,7 +170,7 @@ export default function ChangePasswordModal({
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="请再次输入新密码"
required
minLength={6}
minLength={10}
fullWidth
/>
</div>

View File

@ -41,7 +41,7 @@ const THEME_STORAGE_KEY = 'admin-theme-preference'
// 默认导航栏 LOGO:优先使用构建时环境变量,否则使用内置静态资源
const DEFAULT_HEADER_LOGO =
process.env.NEXT_PUBLIC_DEFAULT_HEADER_LOGO_URL || '/logo/logo1.png'
process.env.NEXT_PUBLIC_DEFAULT_HEADER_LOGO_URL || ''
export default function Header({ session, compactMode }: HeaderProps) {
const styles = useThemeStyles()
@ -751,4 +751,3 @@ export default function Header({ session, compactMode }: HeaderProps) {
)
}

View File

@ -133,8 +133,8 @@ describe('AudioVisualizer 组件', () => {
const { container } = render(
<AudioVisualizer audioElement={null} isPlaying={false} />
)
// 检查是否有默认封面的渐变背景
const defaultCover = container.querySelector('.bg-gradient-to-br')
// 检查是否有默认封面的中性背景
const defaultCover = container.querySelector('.bg-slate-100')
expect(defaultCover).toBeInTheDocument()
})
@ -389,7 +389,7 @@ describe('AudioVisualizer 边界情况', () => {
/>
)
// 空字符串应该被视为 falsy,显示默认封面
const defaultCover = container.querySelector('.bg-gradient-to-br')
const defaultCover = container.querySelector('.bg-slate-100')
expect(defaultCover).toBeInTheDocument()
})

View File

@ -62,12 +62,12 @@ export default function Avatar({
<div
className={cn(
'rounded-full flex items-center justify-center font-semibold',
'border-2 shadow-md transition-all',
'border-2 shadow-sm transition-colors',
bgColor,
textColor,
borderColor,
sizeClasses[size],
onClick && 'cursor-pointer hover:scale-105 hover:shadow-lg',
onClick && 'cursor-pointer',
className
)}
onClick={onClick}

View File

@ -1,19 +1,13 @@
/**
* Tag 组件
*
* 统一的标签组件,遵循三层架构设计,采用苹果美学风格
* 统一的标签组件,适合后台筛选与分类展示
* 用于显示分类、类型等标签信息
* 支持多种颜色(blue、green、orange、purple等)和尺寸(sm、md、lg)
* 支持少量语义色和尺寸(xs、sm、md、lg)
* 支持点击事件,可用于筛选等功能
*
* 苹果美学特点:
* - 圆角更大(rounded-full)
* - 背景色更柔和(低饱和度)
* - 选中状态使用更明显的视觉反馈
* - 过渡动画更流畅
*
* @created 2025-11-22
* @updated 2025-01-22 - 重新设计为苹果美学风格
* @updated 2026-05-25 - 收敛为后台工作台风格
* @component
*/
@ -39,34 +33,30 @@ export default function Tag({
onClick,
selected = false,
}: TagProps) {
// 使用主题样式钩子(保留以便将来扩展)
useThemeStyles()
// 苹果美学风格的颜色配置(更柔和的背景,更清晰的文字)
// 修复:提高未选中状态的文字对比度,确保在素材数量为0时文字仍然可见
// 2025-12-13: 增强hover状态的文字颜色对比度,使用更深的文字颜色
const colorClasses = {
const colorClasses: Record<NonNullable<TagProps['color']>, string> = {
blue: selected
? 'bg-blue-500 dark:bg-blue-600 text-white border-blue-500 dark:border-blue-600'
: 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-700/70 hover:bg-blue-100 dark:hover:bg-blue-800/60 hover:text-blue-900 dark:hover:text-white',
? 'bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-950 border-slate-900 dark:border-slate-100'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-200 border-slate-300 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800',
green: selected
? 'bg-emerald-500 dark:bg-emerald-600 text-white border-emerald-500 dark:border-emerald-600'
: 'bg-emerald-50 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-100 border-emerald-200 dark:border-emerald-700/70 hover:bg-emerald-100 dark:hover:bg-emerald-800/60 hover:text-emerald-900 dark:hover:text-white',
? 'bg-emerald-700 dark:bg-emerald-500 text-white border-emerald-700 dark:border-emerald-500'
: 'bg-emerald-50 dark:bg-emerald-950/30 text-emerald-800 dark:text-emerald-200 border-emerald-200 dark:border-emerald-800 hover:bg-emerald-100 dark:hover:bg-emerald-950/50',
orange: selected
? 'bg-orange-500 dark:bg-orange-600 text-white border-orange-500 dark:border-orange-600'
: 'bg-orange-50 dark:bg-orange-900/50 text-orange-700 dark:text-orange-100 border-orange-200 dark:border-orange-700/70 hover:bg-orange-100 dark:hover:bg-orange-800/60 hover:text-orange-900 dark:hover:text-white',
? 'bg-amber-700 dark:bg-amber-500 text-white dark:text-slate-950 border-amber-700 dark:border-amber-500'
: 'bg-amber-50 dark:bg-amber-950/30 text-amber-800 dark:text-amber-200 border-amber-200 dark:border-amber-800 hover:bg-amber-100 dark:hover:bg-amber-950/50',
purple: selected
? 'bg-purple-500 dark:bg-purple-600 text-white border-purple-500 dark:border-purple-600'
: 'bg-purple-50 dark:bg-purple-900/50 text-purple-700 dark:text-purple-100 border-purple-200 dark:border-purple-700/70 hover:bg-purple-100 dark:hover:bg-purple-800/60 hover:text-purple-900 dark:hover:text-white',
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
: 'bg-slate-50 dark:bg-slate-900 text-slate-700 dark:text-slate-200 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800',
indigo: selected
? 'bg-indigo-500 dark:bg-indigo-600 text-white border-indigo-500 dark:border-indigo-600'
: 'bg-indigo-50 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-100 border-indigo-200 dark:border-indigo-700/70 hover:bg-indigo-100 dark:hover:bg-indigo-800/60 hover:text-indigo-900 dark:hover:text-white',
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
: 'bg-slate-50 dark:bg-slate-900 text-slate-700 dark:text-slate-200 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800',
pink: selected
? 'bg-pink-500 dark:bg-pink-600 text-white border-pink-500 dark:border-pink-600'
: 'bg-pink-50 dark:bg-pink-900/50 text-pink-700 dark:text-pink-100 border-pink-200 dark:border-pink-700/70 hover:bg-pink-100 dark:hover:bg-pink-800/60 hover:text-pink-900 dark:hover:text-white',
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
: 'bg-slate-50 dark:bg-slate-900 text-slate-700 dark:text-slate-200 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800',
cyan: selected
? 'bg-cyan-500 dark:bg-cyan-600 text-white border-cyan-500 dark:border-cyan-600'
: 'bg-cyan-50 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-100 border-cyan-200 dark:border-cyan-700/70 hover:bg-cyan-100 dark:hover:bg-cyan-800/60 hover:text-cyan-900 dark:hover:text-white',
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
: 'bg-slate-50 dark:bg-slate-900 text-slate-700 dark:text-slate-200 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800',
}
const sizeClasses = {
@ -76,15 +66,11 @@ export default function Tag({
lg: 'px-5 py-2 text-base font-medium',
} as const
// 苹果美学:圆角更大,过渡更流畅,选中状态有阴影
const baseClasses = cn(
'inline-flex items-center justify-center',
'rounded-full border',
'transition-all duration-200 ease-out',
'backdrop-blur-sm',
selected && 'shadow-md scale-105',
!selected && onClick && 'hover:scale-105 hover:shadow-sm',
onClick && 'cursor-pointer active:scale-95'
'rounded-md border',
'transition-colors duration-200 ease-out',
onClick && 'cursor-pointer'
)
return (

View File

@ -1,11 +1,7 @@
/**
* Tag 组件单元测试
*
* 测试 Tag 组件的样式和行为
* 验证文字可见性、颜色对比度、hover状态等
*
* **Feature: video-template-and-tag-ui-fix**
* **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
* 当前视觉口径:后台工作台风格,标签应保持可读、紧凑、低干扰。
*
* @jest-environment jsdom
*/
@ -15,7 +11,6 @@ import { render, screen, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import Tag from '../Tag'
// Mock useThemeStyles hook
jest.mock('@/lib/ui-styles', () => ({
useThemeStyles: jest.fn(() => ({})),
cn: (...classes: (string | boolean | undefined)[]) =>
@ -29,10 +24,16 @@ describe('Tag 组件', () => {
expect(screen.getByText('测试标签')).toBeInTheDocument()
})
it('应该使用默认的 blue 颜色', () => {
it('默认样式应该是中性工作台标签', () => {
render(<Tag>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('blue')
expect(tag.className).toContain('rounded-md')
expect(tag.className).toContain('border')
expect(tag.className).toContain('transition-colors')
expect(tag.className).toContain('bg-white')
expect(tag.className).toContain('text-slate-700')
expect(tag.className).toContain('dark:text-slate-200')
})
it('应该使用默认的 md 尺寸', () => {
@ -49,25 +50,38 @@ describe('Tag 组件', () => {
})
describe('颜色变体', () => {
const colors = ['blue', 'green', 'orange', 'purple', 'indigo', 'pink', 'cyan'] as const
// green 颜色实际使用 emerald 类名
const colorClassMap: Record<string, string> = {
blue: 'blue',
green: 'emerald',
orange: 'orange',
purple: 'purple',
indigo: 'indigo',
pink: 'pink',
cyan: 'cyan',
}
it('blue、purple、indigo、pink、cyan 应该降级为中性色', () => {
const neutralColors = ['blue', 'purple', 'indigo', 'pink', 'cyan'] as const
colors.forEach((color) => {
it(`应该正确渲染 ${color} 颜色`, () => {
render(<Tag color={color}>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain(colorClassMap[color])
neutralColors.forEach((color) => {
const { unmount } = render(<Tag color={color}>测试 {color}</Tag>)
const tag = screen.getByText(`测试 ${color}`)
expect(tag.className).toContain('text-slate-700')
expect(tag.className).toContain('dark:text-slate-200')
expect(tag.className).toContain('border-slate-')
unmount()
})
})
it('green 保留成功语义色', () => {
render(<Tag color="green">测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('bg-emerald-50')
expect(tag.className).toContain('text-emerald-800')
expect(tag.className).toContain('dark:text-emerald-200')
})
it('orange 使用 amber 告警语义色', () => {
render(<Tag color="orange">测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('bg-amber-50')
expect(tag.className).toContain('text-amber-800')
expect(tag.className).toContain('dark:text-amber-200')
})
})
describe('尺寸变体', () => {
@ -97,26 +111,32 @@ describe('Tag 组件', () => {
})
describe('选中状态', () => {
it('未选中时应该有正确的样式类', () => {
it('未选中时应该保持浅色背景和可读文字', () => {
render(<Tag selected={false}>测试</Tag>)
const tag = screen.getByText('测试')
// 未选中状态应该有 hover 效果类
expect(tag.className).toContain('bg-blue-50')
expect(tag.className).toContain('bg-white')
expect(tag.className).toContain('text-slate-700')
expect(tag.className).toContain('hover:bg-slate-50')
})
it('选中时应该有正确的样式类', () => {
render(<Tag selected={true}>测试</Tag>)
it('选中时应该有明确但不夸张的强调', () => {
render(<Tag selected>测试</Tag>)
const tag = screen.getByText('测试')
// 选中状态应该有阴影和缩放
expect(tag.className).toContain('shadow-md')
expect(tag.className).toContain('scale-105')
expect(tag.className).toContain('bg-blue-500')
})
it('选中时文字应该是白色', () => {
render(<Tag selected={true}>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('bg-slate-900')
expect(tag.className).toContain('dark:bg-slate-100')
expect(tag.className).toContain('text-white')
expect(tag.className).not.toContain('scale-105')
expect(tag.className).not.toContain('shadow-md')
})
it('选中时文字应该可读', () => {
render(<Tag selected>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('text-white')
expect(tag.className).toContain('dark:text-slate-950')
})
})
@ -143,170 +163,32 @@ describe('Tag 组件', () => {
})
})
/**
* Property 6: 标签文字可见性(零素材)
* *For any* 素材数量为0的标签按钮,文字应该保持可见
* **Feature: bugfix-dec-2025, Property 1: Tag组件零素材状态文字可见性**
* **Validates: Requirements 2.1, 2.2, 2.3**
*/
describe('Property 6: 标签文字可见性(零素材)', () => {
it('显示数量为0时文字应该可见', () => {
describe('可读性', () => {
it('显示数量为 0 时文字应该可见', () => {
render(<Tag>3DMAX (0)</Tag>)
const tag = screen.getByText('3DMAX (0)')
expect(tag).toBeVisible()
})
it('未选中状态下文字颜色应该有足够对比度', () => {
render(<Tag selected={false} color="blue">测试 (0)</Tag>)
it('浅色模式未选中状态应使用深色文字', () => {
render(<Tag selected={false}>测试 (0)</Tag>)
const tag = screen.getByText('测试 (0)')
// 浅色模式:text-blue-700(深蓝色文字)
expect(tag.className).toContain('text-blue-700')
expect(tag.className).toContain('text-slate-700')
})
it('深色模式下文字颜色应该足够亮', () => {
render(<Tag selected={false} color="blue">测试 (0)</Tag>)
it('深色模式未选中状态应使用亮色文字', () => {
render(<Tag selected={false}>测试 (0)</Tag>)
const tag = screen.getByText('测试 (0)')
// 深色模式:dark:text-blue-100(亮蓝色文字)
expect(tag.className).toContain('dark:text-blue-100')
})
expect(tag.className).toContain('dark:text-slate-200')
})
/**
* Property 7: Hover状态文字可见性
* *For any* 标签按钮,在hover状态下,文字的可见性不应该降低
* **Feature: bugfix-dec-2025, Property 1: Tag组件零素材状态文字可见性**
* **Validates: Requirements 2.2**
*/
describe('Property 7: Hover状态文字可见性', () => {
it('hover时背景变化应该增强文字颜色对比度', () => {
it('hover 状态不应该依赖缩放或强阴影', () => {
render(<Tag onClick={() => {}}>测试</Tag>)
const tag = screen.getByText('测试')
// hover 类改变背景色和文字颜色
expect(tag.className).toContain('hover:bg-blue-100')
// hover时文字颜色变得更深
expect(tag.className).toContain('hover:text-blue-900')
})
it('深色模式hover时文字颜色应该变为白色', () => {
render(<Tag onClick={() => {}}>测试</Tag>)
const tag = screen.getByText('测试')
// 深色模式 hover 背景
expect(tag.className).toContain('dark:hover:bg-blue-800/60')
// 深色模式hover时文字变为白色
expect(tag.className).toContain('dark:hover:text-white')
})
})
/**
* Property 8: 未选中状态对比度
* *For any* 未选中状态的标签按钮,文字与背景的对比度应该足够
* **Feature: bugfix-dec-2025, Property 1: Tag组件零素材状态文字可见性**
* **Validates: Requirements 2.3**
*/
describe('Property 8: 未选中状态对比度', () => {
const colors = ['blue', 'green', 'orange', 'purple', 'indigo', 'pink', 'cyan'] as const
colors.forEach((color) => {
it(`${color} 颜色未选中时应该有深色文字`, () => {
render(<Tag color={color} selected={false}>测试</Tag>)
const tag = screen.getByText('测试')
// 浅色模式下应该有 -700 的深色文字
expect(tag.className).toContain(`text-${color === 'green' ? 'emerald' : color}-700`)
})
})
colors.forEach((color) => {
it(`${color} 颜色未选中时深色模式应该有亮色文字`, () => {
render(<Tag color={color} selected={false}>测试</Tag>)
const tag = screen.getByText('测试')
// 深色模式下应该有 -100 的亮色文字
expect(tag.className).toContain(`dark:text-${color === 'green' ? 'emerald' : color}-100`)
})
})
})
/**
* Property 9: 深色模式文字可见性
* *For any* 在深色模式下显示的标签按钮,文字颜色应该足够亮
* **Feature: bugfix-dec-2025, Property 1: Tag组件零素材状态文字可见性**
* **Validates: Requirements 2.4**
*/
describe('Property 9: 深色模式文字可见性', () => {
it('深色模式未选中状态应该使用亮色文字', () => {
render(<Tag selected={false}>测试</Tag>)
const tag = screen.getByText('测试')
// dark:text-blue-100 是亮蓝色
expect(tag.className).toContain('dark:text-blue-100')
})
it('深色模式选中状态应该使用白色文字', () => {
render(<Tag selected={true}>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('text-white')
})
it('深色模式背景应该有足够的不透明度', () => {
render(<Tag selected={false}>测试</Tag>)
const tag = screen.getByText('测试')
// dark:bg-blue-900/50 - 50% 不透明度
expect(tag.className).toContain('dark:bg-blue-900/50')
})
})
/**
* Property 10: 浅色模式文字可见性
* *For any* 在浅色模式下显示的标签按钮,文字颜色应该足够深
* **Feature: bugfix-dec-2025, Property 1: Tag组件零素材状态文字可见性**
* **Validates: Requirements 2.5**
*/
describe('Property 10: 浅色模式文字可见性', () => {
it('浅色模式未选中状态应该使用深色文字', () => {
render(<Tag selected={false}>测试</Tag>)
const tag = screen.getByText('测试')
// text-blue-700 是深蓝色
expect(tag.className).toContain('text-blue-700')
})
it('浅色模式选中状态应该使用白色文字', () => {
render(<Tag selected={true}>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('text-white')
})
it('浅色模式背景应该是浅色', () => {
render(<Tag selected={false}>测试</Tag>)
const tag = screen.getByText('测试')
// bg-blue-50 是浅蓝色背景
expect(tag.className).toContain('bg-blue-50')
})
})
describe('苹果美学样式', () => {
it('应该有圆角样式', () => {
render(<Tag>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('rounded-full')
})
it('应该有边框', () => {
render(<Tag>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('border')
})
it('应该有过渡动画', () => {
render(<Tag>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('transition-all')
expect(tag.className).toContain('duration-200')
})
it('应该有背景模糊效果', () => {
render(<Tag>测试</Tag>)
const tag = screen.getByText('测试')
expect(tag.className).toContain('backdrop-blur-sm')
expect(tag.className).toContain('hover:bg-slate-50')
expect(tag.className).not.toContain('hover:scale-105')
expect(tag.className).not.toContain('hover:shadow')
})
})
})

View File

@ -1,6 +1,6 @@
/**
* 统一的动画工具类
* 苹果风格 + 炫彩元素
* 面向后台工作台的低干扰动画与状态类
*/
// 基础动画类
@ -8,15 +8,15 @@ export const animations = {
// 淡入淡出
fadeIn: 'animate-in fade-in duration-500',
fadeOut: 'animate-out fade-out duration-300',
fadeInUp: 'animate-in fade-in slide-in-from-bottom-4 duration-500',
fadeInDown: 'animate-in fade-in slide-in-from-top-4 duration-500',
fadeInLeft: 'animate-in fade-in slide-in-from-left-4 duration-500',
fadeInRight: 'animate-in fade-in slide-in-from-right-4 duration-500',
fadeInUp: 'animate-in fade-in duration-300',
fadeInDown: 'animate-in fade-in duration-300',
fadeInLeft: 'animate-in fade-in duration-300',
fadeInRight: 'animate-in fade-in duration-300',
// 缩放
scaleIn: 'animate-in zoom-in-95 duration-300',
scaleOut: 'animate-out zoom-out-95 duration-200',
scaleUp: 'hover:scale-105 active:scale-95 transition-transform duration-200',
scaleUp: 'active:scale-[0.99] transition-transform duration-150',
// 旋转
spin: 'animate-spin',
@ -28,13 +28,13 @@ export const animations = {
// 弹跳
bounce: 'animate-bounce',
bounceIn: 'animate-in bounce-in duration-500',
bounceIn: 'animate-in fade-in duration-300',
// 滑动
slideInFromLeft: 'animate-in slide-in-from-left duration-500',
slideInFromRight: 'animate-in slide-in-from-right duration-500',
slideInFromTop: 'animate-in slide-in-from-top duration-500',
slideInFromBottom: 'animate-in slide-in-from-bottom duration-500',
slideInFromLeft: 'animate-in fade-in duration-300',
slideInFromRight: 'animate-in fade-in duration-300',
slideInFromTop: 'animate-in fade-in duration-300',
slideInFromBottom: 'animate-in fade-in duration-300',
// 交错动画延迟
stagger: {
@ -48,116 +48,96 @@ export const animations = {
},
} as const
// 炫彩渐变背景
// 背景与边框状态
export const gradientBackgrounds = {
// 苹果风格渐变(统一 Hero 卡片背景颜色)
appleLight: 'bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900',
appleDark: 'bg-gradient-to-br from-slate-900 via-indigo-900 to-purple-900 dark:from-slate-950 dark:via-indigo-950 dark:to-purple-950',
appleLight: 'bg-slate-50 dark:bg-slate-900',
appleDark: 'bg-slate-900 dark:bg-slate-950',
// Hero 卡片背景渐变(CSS linear-gradient 格式,用于内联样式)
// 兼容旧调用的普通后台背景(CSS linear-gradient 格式,用于内联样式)
heroBase: {
light: 'linear-gradient(135deg, #f8fafc 0%, #eff6ff 50%, #eef2ff 100%)',
dark: 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #1e1b4b 100%)',
light: '#f8fafc',
dark: '#0f172a',
},
// 炫彩渐变
rainbow: 'bg-gradient-to-r from-pink-500 via-red-500 via-yellow-500 via-green-500 via-blue-500 via-indigo-500 to-purple-500',
rainbowSubtle: 'bg-gradient-to-br from-pink-100 via-purple-100 via-indigo-100 to-blue-100 dark:from-pink-900/20 dark:via-purple-900/20 dark:via-indigo-900/20 dark:to-blue-900/20',
rainbowVibrant: 'bg-gradient-to-br from-pink-500/20 via-purple-500/20 via-indigo-500/20 to-blue-500/20',
rainbow: 'bg-slate-100 dark:bg-slate-800',
rainbowSubtle: 'bg-slate-50 dark:bg-slate-900',
rainbowVibrant: 'bg-slate-100 dark:bg-slate-800',
// 霓虹效果
neon: 'bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-500',
neonSubtle: 'bg-gradient-to-br from-cyan-100 via-blue-100 to-purple-100 dark:from-cyan-900/30 dark:via-blue-900/30 dark:to-purple-900/30',
neon: 'bg-slate-100 dark:bg-slate-800',
neonSubtle: 'bg-slate-50 dark:bg-slate-900',
// 金属质感
metallic: 'bg-gradient-to-br from-gray-200 via-gray-300 to-gray-400 dark:from-gray-700 dark:via-gray-600 dark:to-gray-500',
metallic: 'bg-slate-100 dark:bg-slate-800',
// 玻璃态
glass: 'bg-white/10 dark:bg-white/5 backdrop-blur-md border border-white/20',
glassStrong: 'bg-white/20 dark:bg-white/10 backdrop-blur-lg border border-white/30',
// 毛玻璃半透明(用于顶部栏)
glassNav: 'bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border-b border-white/20 dark:border-slate-700/30',
glassNavLight: 'bg-white/70 backdrop-blur-xl border-b border-slate-200/50',
glassNavDark: 'bg-slate-900/70 backdrop-blur-xl border-b border-slate-700/50',
glass: 'bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700',
glassStrong: 'bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700',
glassNav: 'bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700',
glassNavLight: 'bg-white border-b border-slate-200',
glassNavDark: 'bg-slate-900 border-b border-slate-700',
} as const
// 炫彩光晕效果
// 阴影效果
export const glowEffects = {
// 基础光晕
glow: 'shadow-lg shadow-indigo-500/50',
glowStrong: 'shadow-xl shadow-indigo-500/70',
glowRainbow: 'shadow-xl shadow-[0_0_30px_rgba(139,92,246,0.5)]',
glow: 'shadow-sm',
glowStrong: 'shadow-md',
glowRainbow: 'shadow-sm',
// 彩色光晕
glowPink: 'shadow-lg shadow-pink-500/50',
glowPurple: 'shadow-lg shadow-purple-500/50',
glowBlue: 'shadow-lg shadow-blue-500/50',
glowCyan: 'shadow-lg shadow-cyan-500/50',
glowPink: 'shadow-sm',
glowPurple: 'shadow-sm',
glowBlue: 'shadow-sm',
glowCyan: 'shadow-sm',
// 悬停光晕
glowHover: 'hover:shadow-xl hover:shadow-indigo-500/60 transition-shadow duration-300',
glowHoverRainbow: 'hover:shadow-2xl hover:shadow-[0_0_40px_rgba(139,92,246,0.6)] transition-shadow duration-300',
glowHover: 'hover:shadow-sm transition-shadow duration-150',
glowHoverRainbow: 'hover:shadow-sm transition-shadow duration-150',
} as const
// 边框动画
export const borderAnimations = {
// 渐变边框
gradientBorder: 'border-2 border-transparent bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 bg-clip-border',
gradientBorder: 'border border-slate-200 dark:border-slate-700',
// 闪烁边框
shimmer: 'relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent',
shimmer: 'relative overflow-hidden',
// 霓虹边框
neon: 'border-2 border-cyan-400 shadow-[0_0_10px_rgba(34,211,238,0.5)]',
neonHover: 'hover:border-cyan-300 hover:shadow-[0_0_20px_rgba(34,211,238,0.7)] transition-all duration-300',
neon: 'border border-slate-300 dark:border-slate-600',
neonHover: 'hover:border-slate-400 dark:hover:border-slate-500 transition-colors duration-150',
} as const
// 文本动画
export const textAnimations = {
// 渐变文本
gradientText: 'bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent',
gradientTextRainbow: 'bg-gradient-to-r from-pink-500 via-red-500 via-yellow-500 via-green-500 via-blue-500 via-indigo-500 to-purple-500 bg-clip-text text-transparent',
gradientText: 'text-slate-900 dark:text-slate-100',
gradientTextRainbow: 'text-slate-900 dark:text-slate-100',
// 闪烁文本
shimmerText: 'animate-[shimmer_2s_infinite] bg-gradient-to-r from-gray-400 via-white to-gray-400 bg-clip-text text-transparent',
shimmerText: 'text-slate-500 dark:text-slate-400',
// 发光文本
glowText: 'text-indigo-400 drop-shadow-[0_0_8px_rgba(99,102,241,0.8)]',
glowTextRainbow: 'drop-shadow-[0_0_10px_rgba(139,92,246,0.6)]',
glowText: 'text-slate-700 dark:text-slate-200',
glowTextRainbow: 'text-slate-700 dark:text-slate-200',
} as const
// 卡片动画
export const cardAnimations = {
// 悬停提升
lift: 'transition-all duration-300 hover:-translate-y-1 hover:shadow-xl',
liftStrong: 'transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl',
lift: 'transition-colors duration-150',
liftStrong: 'transition-colors duration-150',
// 3D 倾斜效果
tilt: 'transition-all duration-300 hover:rotate-1 hover:scale-105',
tilt: 'transition-colors duration-150',
// 闪烁效果
shimmer: 'relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent',
shimmer: 'relative overflow-hidden',
// 光晕跟随鼠标
glowFollow: 'transition-all duration-500',
glowFollow: 'transition-colors duration-150',
} as const
// 按钮动画
export const buttonAnimations = {
// 基础按钮动画
base: 'transition-all duration-200 active:scale-95',
base: 'transition-colors duration-150',
// 炫彩按钮
rainbow: 'bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 hover:from-pink-600 hover:via-purple-600 hover:to-indigo-600 transition-all duration-300',
rainbow: 'bg-slate-900 text-white hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white transition-colors duration-150',
// 霓虹按钮
neon: 'bg-cyan-500 hover:bg-cyan-400 shadow-lg shadow-cyan-500/50 hover:shadow-xl hover:shadow-cyan-500/70 transition-all duration-300',
neon: 'bg-slate-900 text-white hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white transition-colors duration-150',
// 玻璃态按钮
glass: 'bg-white/10 dark:bg-white/5 backdrop-blur-md border border-white/20 hover:bg-white/20 dark:hover:bg-white/10 transition-all duration-300',
glass: 'bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors duration-150',
// 脉冲按钮
pulse: 'animate-pulse hover:animate-none',
pulse: 'transition-colors duration-150',
} as const
// 组合动画工具函数
@ -170,7 +150,7 @@ export function conditionalAnimation(condition: boolean, animation: string, fall
return condition ? animation : fallback
}
// 苹果风格滚动动画
// 滚动动画
export const scrollAnimations = {
// 平滑滚动
smoothScroll: 'scroll-smooth',
@ -184,16 +164,13 @@ export const scrollAnimations = {
// 滚动时位移
translateOnScroll: 'transition-transform duration-300 ease-out',
// 苹果风格的弹性动画
spring: 'transition-all duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)]',
springSmooth: 'transition-all duration-400 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]',
spring: 'transition-colors duration-150 ease-out',
springSmooth: 'transition-colors duration-150 ease-out',
// 滚动时隐藏/显示
hideOnScroll: 'transition-all duration-300 ease-out opacity-0 -translate-y-2',
showOnScroll: 'transition-all duration-300 ease-out opacity-100 translate-y-0',
// 苹果风格的弹性滚动(用于 scrollIntoView)
// 使用 CSS scroll-behavior: smooth 配合 JavaScript 实现弹性效果
elasticScroll: 'scroll-smooth',
} as const
@ -208,14 +185,12 @@ function easeOutElastic(t: number): number {
return Math.pow(2, -6 * t) * Math.sin((t * 4 - 0.3) * c4) + 1
}
// 苹果风格的缓入缓出函数(ease-in-out-cubic)
function easeInOutCubic(t: number): number {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2
}
// 苹果风格的弹性滚动函数(用于 JavaScript 调用)
export function scrollToElementWithSpring(
element: HTMLElement | null,
container: HTMLElement | null = null,

View File

@ -1,82 +1,82 @@
/**
* UI 样式基础配置
* 包含所有非颜色相关的样式:布局、大小、动画、间距、圆角等
* 这些样式在浅色和深色模式下完全相同
* 包含所有非颜色相关的样式:布局、大小、间距、圆角等。
* 默认风格服务后台工作台,不做展示页式大圆角、毛玻璃、浮动光效。
*/
// 基础结构样式(不包含颜色)
export const baseCardStyles = {
// 基础卡片结构
base: 'rounded-2xl border backdrop-blur-sm shadow-lg p-6 transition-all duration-300',
base: 'rounded-lg border p-5 transition-colors duration-200',
// 悬停卡片结构(带提升效果)
hover: 'rounded-2xl border backdrop-blur-sm shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-300 p-6',
// 悬停卡片结构
hover: 'rounded-lg border p-5 transition-colors duration-200',
// 大卡片结构(带炫彩光晕)
large: 'rounded-3xl border backdrop-blur-md shadow-xl hover:shadow-2xl transition-all duration-500 p-8',
// 大卡片结构
large: 'rounded-lg border p-6 transition-colors duration-200',
// 扁平卡片结构
flat: 'rounded-2xl border backdrop-blur-sm p-6 transition-all duration-300',
flat: 'rounded-lg border p-5 transition-colors duration-200',
// 炫彩卡片结构(带渐变背景)
rainbow: 'rounded-2xl border backdrop-blur-sm shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-300 p-6',
// 兼容旧 variant,实际降级为普通卡片
rainbow: 'rounded-lg border p-5 transition-colors duration-200',
// 玻璃态卡片结构
glass: 'rounded-2xl backdrop-blur-md border shadow-lg hover:shadow-xl transition-all duration-300 p-6',
// 兼容旧 variant,实际降级为普通卡片
glass: 'rounded-lg border p-5 transition-colors duration-200',
// 霓虹卡片结构
neon: 'rounded-2xl border-2 backdrop-blur-sm shadow-lg hover:shadow-xl transition-all duration-300 p-6',
// 兼容旧 variant,实际降级为普通卡片
neon: 'rounded-lg border p-5 transition-colors duration-200',
} as const
export const baseButtonStyles = {
// 主要按钮结构
primary: 'px-6 py-3 rounded-xl text-white font-medium shadow-lg hover:shadow-xl active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
primary: 'px-4 py-2 rounded-lg text-white font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 次要按钮结构(hover 边框颜色在主题中定义)
secondary: 'px-6 py-3 rounded-xl border backdrop-blur-sm font-medium active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
secondary: 'px-4 py-2 rounded-lg border font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 幽灵按钮结构
ghost: 'px-6 py-3 rounded-xl font-medium active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
ghost: 'px-4 py-2 rounded-lg font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 危险按钮结构(不包含颜色,颜色由主题提供)
danger: 'px-6 py-3 rounded-xl text-white font-medium shadow-lg hover:shadow-xl active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
danger: 'px-4 py-2 rounded-lg text-white font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 成功按钮结构
success: 'px-6 py-3 rounded-xl text-white font-medium shadow-lg hover:shadow-xl active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
success: 'px-4 py-2 rounded-lg text-white font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 炫彩渐变按钮结构(标准尺寸)
rainbow: 'px-6 py-3 rounded-xl bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 text-white font-medium shadow-lg hover:from-pink-600 hover:via-purple-600 hover:to-indigo-600 hover:shadow-xl active:scale-95 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed',
// 兼容旧 variant,实际降级为普通主按钮
rainbow: 'px-4 py-2 rounded-lg bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 炫彩渐变按钮结构(紧凑尺寸,用于角标)
rainbowCompact: 'px-2 py-1 text-xs rounded-xl bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 text-white font-medium shadow-lg hover:from-pink-600 hover:via-purple-600 hover:to-indigo-600 hover:shadow-xl active:scale-95 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed',
// 兼容旧 variant,实际降级为普通紧凑按钮
rainbowCompact: 'px-2 py-1 text-xs rounded-md bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 霓虹按钮结构
neon: 'px-6 py-3 rounded-xl bg-cyan-500 text-white font-medium shadow-lg hover:bg-cyan-400 hover:shadow-xl active:scale-95 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed',
// 兼容旧 variant,实际降级为普通主按钮
neon: 'px-4 py-2 rounded-lg bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 玻璃态按钮结构
glass: 'px-6 py-3 rounded-xl backdrop-blur-md border text-white font-medium hover:border-white/30 active:scale-95 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed',
// 兼容旧 variant,实际降级为普通次按钮
glass: 'px-4 py-2 rounded-lg border font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
} as const
export const baseInputStyles = {
// 基础输入框结构(focus 颜色在主题中定义)
base: 'w-full px-4 py-3 rounded-xl border focus:ring-2 outline-none transition-all',
base: 'w-full px-3 py-2 rounded-lg border focus:ring-2 outline-none transition-colors',
// 错误输入框结构(focus 颜色在主题中定义)
error: 'w-full px-4 py-3 rounded-xl border-2 focus:ring-2 outline-none transition-all',
error: 'w-full px-3 py-2 rounded-lg border-2 focus:ring-2 outline-none transition-colors',
} as const
export const baseNavItemStyles = {
// 基础导航项结构
base: 'px-4 py-3 rounded-xl border backdrop-blur-sm transition-all duration-300',
base: 'px-3 py-2 rounded-lg border transition-colors duration-200',
// 悬停导航项结构(带光晕效果,hover 边框和阴影颜色在主题中定义)
hover: 'px-4 py-3 rounded-xl border backdrop-blur-sm hover:shadow-md transition-all duration-300',
// 悬停导航项结构
hover: 'px-3 py-2 rounded-lg border transition-colors duration-200',
// 激活导航项结构(带炫彩光晕,边框和阴影颜色在主题中定义)
active: 'px-4 py-3 rounded-xl border-2 backdrop-blur-sm shadow-lg transition-all duration-300',
// 激活导航项结构
active: 'px-3 py-2 rounded-lg border transition-colors duration-200',
// 炫彩导航项结构
rainbow: 'px-4 py-3 rounded-xl border backdrop-blur-sm hover:shadow-md transition-all duration-300',
// 兼容旧 variant,实际降级为普通导航项
rainbow: 'px-3 py-2 rounded-lg border transition-colors duration-200',
// 标签页导航项结构
tabBase: 'relative pb-4 px-5 font-medium transition-all duration-300 ease-out border-none bg-transparent',
@ -95,7 +95,7 @@ export const baseBadgeStyles = {
export const baseButtonModifiers = {
compact: 'px-3 py-1.5 text-sm',
compactXs: 'px-2 py-1 text-xs',
icon: 'p-2 rounded-xl',
icon: 'p-2 rounded-lg',
block: 'w-full justify-center',
pill: 'rounded-full',
} as const
@ -115,13 +115,13 @@ export const baseSpacingStyles = {
// Modal 基础样式(不包含颜色)
export const baseModalStyles = {
// 遮罩层
overlay: 'fixed inset-0 z-50 flex items-center justify-center px-4 py-8 sm:px-6 sm:py-12 lg:px-10 lg:py-16 bg-slate-950/55 dark:bg-black/75 backdrop-blur-2xl transition-opacity duration-300 modal-overlay-apple',
overlay: 'fixed inset-0 z-50 flex items-center justify-center px-4 py-8 sm:px-6 sm:py-12 lg:px-10 lg:py-16 bg-slate-950/45 dark:bg-black/65 transition-opacity duration-200',
// 弹窗容器
container: 'relative w-auto max-w-full max-h-[80vh] sm:max-h-[75vh] overflow-hidden transition-all duration-300 flex-none rounded-[32px]',
container: 'relative w-auto max-w-full max-h-[80vh] sm:max-h-[75vh] overflow-hidden transition-opacity duration-200 flex-none rounded-lg',
// 弹窗内容(阴影在主题颜色中定义,这里不重复定义)
content: 'w-full rounded-[32px] bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl overflow-hidden transition-all duration-300 modal-content-apple',
content: 'w-full rounded-lg bg-white dark:bg-slate-900 overflow-hidden transition-opacity duration-200',
// 弹窗尺寸
sizes: {
@ -153,31 +153,31 @@ export const baseModalStyles = {
// Section 显示动画样式(来自外观设置)
export const baseSectionRevealStyles = {
// 基础显示动画类
base: 'transition-all duration-700 ease-out transform',
base: 'transition-opacity duration-300 ease-out',
// 可见状态
visible: 'opacity-100 translate-y-0 scale-100',
visible: 'opacity-100',
// 隐藏状态
hidden: 'opacity-0 translate-y-6 scale-95',
hidden: 'opacity-0',
} as const
// Hero 区域样式(来自外观设置)
// Hero 区域样式(保留兼容,实际按普通页面区块处理)
export const baseHeroStyles = {
// Hero 卡片基础结构
card: 'relative overflow-hidden rounded-3xl border transition-all duration-500 p-6',
card: 'relative overflow-hidden rounded-lg border transition-colors duration-200 p-5',
// Hero 标题区域
header: 'relative z-10 flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between',
// Hero 内容区域
content: 'space-y-3 max-w-2xl',
} as const
// 浮动光球样式(来自外观设置)
// 浮动光球样式(保留兼容,默认隐藏)
export const baseFloatingOrbStyles = {
// 光球基础样式
base: 'absolute rounded-full blur-3xl pointer-events-none',
base: 'hidden',
// 右上角光球
topRight: '-top-8 -right-8 h-24 w-24 animate-pulse-slow',
topRight: '',
// 左下角光球
bottomLeft: '-bottom-8 -left-8 h-20 w-20 animate-[pulse_8s_ease-in-out_infinite]',
bottomLeft: '',
} as const
// 开关切换器样式(来自外观设置)

View File

@ -1,21 +1,12 @@
/**
* 统一的 UI 样式工具类
* 苹果风格 + 炫彩元素
*
* ⚠️ 注意:此文件已重构为支持两套独立配置(浅色/深色)
* 以私有化工作台为默认风格,保留浅色/深色两套配置
*
* 推荐使用方式:
* 1. 在新组件中使用 `useThemeStyles()` Hook(推荐)
* 2. 在服务端组件或需要向后兼容的地方,继续使用此文件的导出
*
* 迁移步骤:
* - 将组件改为客户端组件('use client')
* - 使用 `const styles = useThemeStyles()` 替代直接导入
* - 使用 `styles.cardStyles.base` 替代 `cardStyles.base`
*/
import { gradientBackgrounds, glowEffects, cardAnimations } from './ui-animations'
// 导出主题 Hook(推荐使用)
export { useThemeStyles, useTheme } from './hooks/useThemeStyles'
@ -26,52 +17,52 @@ export { lightStyles, darkStyles } from './ui-styles-theme-exports'
// ⚠️ 这些样式会在深色模式下正确工作,但代码较长
// 建议逐步迁移到 useThemeStyles Hook
export const cardStyles = {
// 基础卡片 - 确保深色模式完全适配(兼容模式)
base: 'rounded-2xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-800 backdrop-blur-sm shadow-lg shadow-slate-200/50 dark:shadow-black/50 p-6 transition-all duration-300',
// 基础卡片
base: 'rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 p-6 transition-colors duration-200',
// 悬停卡片(带提升效果)- 确保深色模式完全适配
hover: 'rounded-2xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-800 backdrop-blur-sm shadow-lg shadow-slate-200/50 dark:shadow-black/50 hover:shadow-xl hover:shadow-indigo-500/20 dark:hover:shadow-indigo-500/30 hover:-translate-y-1 transition-all duration-300 p-6',
// 悬停卡片
hover: 'rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 hover:border-slate-300 dark:hover:border-slate-600 transition-colors duration-200 p-6',
// 大卡片(带炫彩光晕)- 确保深色模式完全适配
large: 'rounded-3xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-800 backdrop-blur-md shadow-xl shadow-slate-200/60 dark:shadow-black/60 hover:shadow-2xl hover:shadow-indigo-500/25 dark:hover:shadow-indigo-500/35 transition-all duration-500 p-8',
// 大卡片
large: 'rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 transition-colors duration-200 p-8',
// 扁平卡片 - 确保深色模式完全适配
flat: 'rounded-2xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-800 backdrop-blur-sm p-6 transition-all duration-300',
// 扁平卡片
flat: 'rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 p-6 transition-colors duration-200',
// 炫彩卡片(带渐变背景)- 确保深色模式完全适配
rainbow: `rounded-2xl border border-slate-200/70 dark:border-slate-700/70 ${gradientBackgrounds.rainbowSubtle} backdrop-blur-sm shadow-lg shadow-purple-500/20 dark:shadow-purple-500/30 hover:shadow-xl hover:shadow-purple-500/30 dark:hover:shadow-purple-500/40 hover:-translate-y-1 transition-all duration-300 p-6`,
// 保留兼容字段,但降级为普通卡片
rainbow: 'rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 p-6 transition-colors duration-200',
// 玻璃态卡片 - 确保深色模式完全适配
glass: `rounded-2xl ${gradientBackgrounds.glass} shadow-lg shadow-slate-200/50 dark:shadow-black/50 hover:shadow-xl hover:shadow-indigo-500/20 dark:hover:shadow-indigo-500/30 transition-all duration-300 p-6`,
// 保留兼容字段,但降级为普通卡片
glass: 'rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 p-6 transition-colors duration-200',
// 霓虹卡片 - 确保深色模式完全适配
neon: 'rounded-2xl border-2 border-cyan-400/50 dark:border-cyan-400/70 bg-white dark:bg-slate-800 backdrop-blur-sm shadow-lg shadow-cyan-500/30 dark:shadow-cyan-500/40 hover:shadow-xl hover:shadow-cyan-500/50 dark:hover:shadow-cyan-500/60 transition-all duration-300 p-6',
// 保留兼容字段,但降级为普通卡片
neon: 'rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 p-6 transition-colors duration-200',
} as const
export const buttonStyles = {
// 主要按钮(带炫彩光晕)- 确保深色模式完全适配
primary: 'px-6 py-3 rounded-xl bg-indigo-600 dark:bg-indigo-500 text-white font-medium shadow-lg shadow-indigo-500/40 dark:shadow-indigo-500/50 hover:bg-indigo-700 dark:hover:bg-indigo-600 hover:shadow-xl hover:shadow-indigo-500/50 dark:hover:shadow-indigo-500/60 active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 主要按钮
primary: 'px-6 py-3 rounded-xl bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-medium hover:bg-slate-800 dark:hover:bg-white transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 次要按钮 - 确保深色模式完全适配
secondary: 'px-6 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 backdrop-blur-sm text-slate-700 dark:text-slate-200 font-medium hover:bg-slate-50 dark:hover:bg-slate-700 hover:border-indigo-400 dark:hover:border-indigo-400 active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 次要按钮
secondary: 'px-6 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 font-medium hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 幽灵按钮 - 确保深色模式完全适配
ghost: 'px-6 py-3 rounded-xl text-slate-700 dark:text-slate-200 font-medium hover:bg-slate-100 dark:hover:bg-slate-800 active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 幽灵按钮
ghost: 'px-6 py-3 rounded-xl text-slate-700 dark:text-slate-200 font-medium hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 危险按钮 - 确保深色模式完全适配(浅色模式使用更深的红色以增强对比度)
danger: 'px-6 py-3 rounded-xl bg-red-700 dark:bg-red-500 text-white font-medium shadow-lg shadow-red-600/70 dark:shadow-red-500/50 hover:bg-red-800 dark:hover:bg-red-600 hover:shadow-xl hover:shadow-red-600/80 dark:hover:shadow-red-500/60 active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 危险按钮
danger: 'px-6 py-3 rounded-xl bg-red-600 dark:bg-red-500 text-white font-medium hover:bg-red-700 dark:hover:bg-red-600 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 成功按钮 - 确保深色模式完全适配
success: 'px-6 py-3 rounded-xl bg-emerald-600 dark:bg-emerald-500 text-white font-medium shadow-lg shadow-emerald-500/40 dark:shadow-emerald-500/50 hover:bg-emerald-700 dark:hover:bg-emerald-600 hover:shadow-xl hover:shadow-emerald-500/50 dark:hover:shadow-emerald-500/60 active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 成功按钮
success: 'px-6 py-3 rounded-xl bg-emerald-600 dark:bg-emerald-500 text-white font-medium hover:bg-emerald-700 dark:hover:bg-emerald-600 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 炫彩渐变按钮
rainbow: 'px-6 py-3 rounded-xl bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 text-white font-medium shadow-lg shadow-purple-500/40 hover:from-pink-600 hover:via-purple-600 hover:to-indigo-600 hover:shadow-xl hover:shadow-purple-500/50 active:scale-95 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed',
// 保留兼容字段,但降级为普通按钮
rainbow: 'px-6 py-3 rounded-xl bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-medium hover:bg-slate-800 dark:hover:bg-white transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 霓虹按钮
neon: 'px-6 py-3 rounded-xl bg-cyan-500 text-white font-medium shadow-lg shadow-cyan-500/50 hover:bg-cyan-400 hover:shadow-xl hover:shadow-cyan-500/70 active:scale-95 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed',
// 保留兼容字段,但降级为普通按钮
neon: 'px-6 py-3 rounded-xl bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-medium hover:bg-slate-800 dark:hover:bg-white transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
// 玻璃态按钮
glass: 'px-6 py-3 rounded-xl bg-white/10 dark:bg-white/5 backdrop-blur-md border border-white/20 text-white font-medium hover:bg-white/20 dark:hover:bg-white/10 hover:border-white/30 active:scale-95 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed',
// 保留兼容字段,但降级为普通按钮
glass: 'px-6 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 font-medium hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
} as const
export const buttonModifiers = {
@ -95,17 +86,17 @@ export const inputModifiers = {
} as const
export const navItemStyles = {
// 基础导航项 - 确保深色模式完全适配
base: 'px-4 py-3 rounded-xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-800 backdrop-blur-sm text-slate-700 dark:text-slate-200 transition-all duration-300',
// 基础导航项
base: 'px-4 py-3 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 transition-colors duration-200',
// 悬停导航项(带光晕效果)- 确保深色模式完全适配
hover: 'px-4 py-3 rounded-xl border border-slate-200/70 dark:border-slate-700/70 bg-white dark:bg-slate-800 backdrop-blur-sm hover:border-indigo-400 dark:hover:border-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 hover:shadow-md hover:shadow-indigo-500/20 dark:hover:shadow-indigo-500/30 text-slate-700 dark:text-slate-200 transition-all duration-300',
// 悬停导航项
hover: 'px-4 py-3 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 hover:border-slate-300 dark:hover:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors duration-200',
// 激活导航项(带炫彩光晕)- 确保深色模式完全适配
active: 'px-4 py-3 rounded-xl border-2 border-indigo-500 dark:border-indigo-400 bg-indigo-50 dark:bg-indigo-900/40 backdrop-blur-sm text-indigo-700 dark:text-indigo-200 shadow-lg shadow-indigo-500/30 dark:shadow-indigo-500/40 transition-all duration-300',
// 激活导航项
active: 'px-4 py-3 rounded-xl border border-slate-900 dark:border-slate-100 bg-slate-100 dark:bg-slate-700 text-slate-900 dark:text-white transition-colors duration-200',
// 炫彩导航项 - 确保深色模式完全适配
rainbow: 'px-4 py-3 rounded-xl border border-slate-200/70 dark:border-slate-700/70 bg-gradient-to-r from-pink-50/50 via-purple-50/50 to-indigo-50/50 dark:from-pink-900/20 dark:via-purple-900/20 dark:to-indigo-900/20 backdrop-blur-sm hover:shadow-md hover:shadow-purple-500/20 dark:hover:shadow-purple-500/30 text-slate-700 dark:text-slate-200 transition-all duration-300',
// 保留兼容字段,但降级为普通导航项
rainbow: 'px-4 py-3 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 transition-colors duration-200',
// 标签页导航项 - 确保深色模式完全适配
tabBase: 'relative pb-4 px-5 font-medium transition-all duration-300 ease-out text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white',

View File

@ -6,22 +6,22 @@
export const themeColors = {
// 卡片颜色
card: {
border: 'border-slate-700/70',
border: 'border-slate-700',
bg: 'bg-slate-800',
shadow: 'shadow-black/50',
shadowHover: 'hover:shadow-indigo-500/30',
shadowLarge: 'shadow-black/60',
shadowLargeHover: 'hover:shadow-indigo-500/35',
shadowRainbow: 'shadow-purple-500/30',
shadowRainbowHover: 'hover:shadow-purple-500/40',
shadowGlass: 'shadow-black/50',
shadowGlassHover: 'hover:shadow-indigo-500/30',
shadowNeon: 'shadow-cyan-500/40',
shadowNeonHover: 'hover:shadow-cyan-500/60',
borderNeon: 'border-cyan-400/70',
bgRainbow: 'bg-gradient-to-br from-pink-900/20 via-purple-900/20 via-indigo-900/20 to-blue-900/20',
bgGlass: 'bg-white/5',
borderGlass: 'border-white/20',
shadow: 'shadow-none',
shadowHover: 'hover:border-slate-600',
shadowLarge: 'shadow-none',
shadowLargeHover: 'hover:border-slate-600',
shadowRainbow: 'shadow-none',
shadowRainbowHover: 'hover:border-slate-600',
shadowGlass: 'shadow-none',
shadowGlassHover: 'hover:border-slate-600',
shadowNeon: 'shadow-none',
shadowNeonHover: 'hover:border-slate-600',
borderNeon: 'border-slate-700',
bgRainbow: 'bg-slate-800',
bgGlass: 'bg-slate-800',
borderGlass: 'border-slate-700',
},
// 按钮颜色
@ -29,8 +29,8 @@ export const themeColors = {
primary: {
bg: 'bg-indigo-500',
hover: 'hover:bg-indigo-600',
shadow: 'shadow-indigo-500/50',
shadowHover: 'hover:shadow-indigo-500/60',
shadow: 'shadow-none',
shadowHover: '',
},
secondary: {
border: 'border-slate-600',
@ -45,19 +45,19 @@ export const themeColors = {
danger: {
bg: 'bg-red-500', // 与浅色模式保持一致
hover: 'hover:bg-red-600',
shadow: 'shadow-red-500/40',
shadowHover: 'hover:shadow-red-500/50',
shadow: 'shadow-none',
shadowHover: '',
},
success: {
bg: 'bg-emerald-500',
hover: 'hover:bg-emerald-600',
shadow: 'shadow-emerald-500/50',
shadowHover: 'hover:shadow-emerald-500/60',
shadow: 'shadow-none',
shadowHover: '',
},
glass: {
bg: 'bg-white/5',
hover: 'hover:bg-white/10',
border: 'border-white/20',
bg: 'bg-slate-800',
hover: 'hover:bg-slate-700',
border: 'border-slate-600',
},
},
@ -77,16 +77,16 @@ export const themeColors = {
// 导航项颜色
navItem: {
border: 'border-slate-700/70',
border: 'border-slate-700',
bg: 'bg-slate-800',
text: 'text-slate-200',
hoverBg: 'hover:bg-indigo-900/30',
hoverShadow: 'hover:shadow-indigo-500/30',
activeBg: 'bg-indigo-900/40',
activeText: 'text-indigo-200',
activeShadow: 'shadow-indigo-500/40',
rainbowBg: 'bg-gradient-to-r from-pink-900/20 via-purple-900/20 to-indigo-900/20',
rainbowShadow: 'hover:shadow-purple-500/30',
hoverBg: 'hover:bg-slate-700',
hoverShadow: 'hover:border-slate-600',
activeBg: 'bg-slate-700',
activeText: 'text-white',
activeShadow: 'border-slate-100',
rainbowBg: 'bg-slate-800',
rainbowShadow: 'hover:border-slate-600',
tabText: 'text-slate-400',
tabTextHover: 'hover:text-white',
tabTextActive: 'text-white',
@ -112,69 +112,62 @@ 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',
bg: 'bg-slate-800',
text: 'text-slate-200',
border: 'border-slate-700',
hover: 'hover:bg-slate-700',
},
// 紫色系(低饱和度)
purple: {
bg: 'bg-purple-900/20',
text: 'text-purple-300',
border: 'border-purple-700/50',
hover: 'hover:bg-purple-900/30',
bg: 'bg-slate-800',
text: 'text-slate-200',
border: 'border-slate-700',
hover: 'hover:bg-slate-700',
},
// 蓝色系(低饱和度)
blue: {
bg: 'bg-blue-900/20',
text: 'text-blue-300',
border: 'border-blue-700/50',
hover: 'hover:bg-blue-900/30',
bg: 'bg-slate-800',
text: 'text-slate-200',
border: 'border-slate-700',
hover: 'hover:bg-slate-700',
},
// 青色系(低饱和度)
cyan: {
bg: 'bg-cyan-900/20',
text: 'text-cyan-300',
border: 'border-cyan-700/50',
hover: 'hover:bg-cyan-900/30',
bg: 'bg-slate-800',
text: 'text-slate-200',
border: 'border-slate-700',
hover: 'hover:bg-slate-700',
},
// 绿色系(低饱和度)
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',
bg: 'bg-amber-900/20',
text: 'text-amber-300',
border: 'border-amber-700/50',
hover: 'hover:bg-amber-900/30',
},
// 渐变炫彩(低饱和度)
rainbow: {
bg: 'bg-gradient-to-r from-pink-900/20 via-purple-900/20 via-indigo-900/20 to-blue-900/20',
bg: 'bg-slate-800',
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',
border: 'border-slate-700',
hover: 'hover:bg-slate-700',
},
},
// Modal 颜色
modal: {
overlay: 'bg-slate-950/70 backdrop-blur-xl',
bg: 'bg-slate-900/80',
border: 'border-slate-800/60',
shadow: 'shadow-[0_45px_130px_rgba(0,0,0,0.75)] shadow-black/50',
headerBorder: 'border-slate-800/60',
overlay: 'bg-slate-950/70',
bg: 'bg-slate-900',
border: 'border-slate-800',
shadow: 'shadow-lg shadow-black/30',
headerBorder: 'border-slate-800',
title: 'text-white',
body: 'text-slate-200',
footerBorder: 'border-slate-800/60',
footerBorder: 'border-slate-800',
closeButton: 'text-slate-300 hover:text-white hover:bg-white/10',
},

View File

@ -6,22 +6,22 @@
export const themeColors = {
// 卡片颜色
card: {
border: 'border-slate-200/70',
border: 'border-slate-200',
bg: 'bg-white',
shadow: 'shadow-slate-200/50',
shadowHover: 'hover:shadow-indigo-500/20',
shadowLarge: 'shadow-slate-200/60',
shadowLargeHover: 'hover:shadow-indigo-500/25',
shadowRainbow: 'shadow-purple-500/20',
shadowRainbowHover: 'hover:shadow-purple-500/30',
shadowGlass: 'shadow-slate-200/50',
shadowGlassHover: 'hover:shadow-indigo-500/20',
shadowNeon: 'shadow-cyan-500/30',
shadowNeonHover: 'hover:shadow-cyan-500/50',
borderNeon: 'border-cyan-400/50',
bgRainbow: 'bg-gradient-to-br from-pink-100 via-purple-100 via-indigo-100 to-blue-100',
bgGlass: 'bg-white/10',
borderGlass: 'border-white/20',
shadow: 'shadow-none',
shadowHover: 'hover:border-slate-300',
shadowLarge: 'shadow-none',
shadowLargeHover: 'hover:border-slate-300',
shadowRainbow: 'shadow-none',
shadowRainbowHover: 'hover:border-slate-300',
shadowGlass: 'shadow-none',
shadowGlassHover: 'hover:border-slate-300',
shadowNeon: 'shadow-none',
shadowNeonHover: 'hover:border-slate-300',
borderNeon: 'border-slate-200',
bgRainbow: 'bg-white',
bgGlass: 'bg-white',
borderGlass: 'border-slate-200',
},
// 按钮颜色
@ -29,8 +29,8 @@ export const themeColors = {
primary: {
bg: 'bg-indigo-600',
hover: 'hover:bg-indigo-700',
shadow: 'shadow-indigo-500/40',
shadowHover: 'hover:shadow-indigo-500/50',
shadow: 'shadow-none',
shadowHover: '',
},
secondary: {
border: 'border-slate-300',
@ -45,19 +45,19 @@ export const themeColors = {
danger: {
bg: 'bg-red-500', // 使用适中的红色,确保可见性
hover: 'hover:bg-red-600',
shadow: 'shadow-red-500/50',
shadowHover: 'hover:shadow-red-500/60',
shadow: 'shadow-none',
shadowHover: '',
},
success: {
bg: 'bg-emerald-600',
hover: 'hover:bg-emerald-700',
shadow: 'shadow-emerald-500/40',
shadowHover: 'hover:shadow-emerald-500/50',
shadow: 'shadow-none',
shadowHover: '',
},
glass: {
bg: 'bg-white/10',
hover: 'hover:bg-white/20',
border: 'border-white/20',
bg: 'bg-white',
hover: 'hover:bg-slate-50',
border: 'border-slate-300',
},
},
@ -77,16 +77,16 @@ export const themeColors = {
// 导航项颜色
navItem: {
border: 'border-slate-200/70',
border: 'border-slate-200',
bg: 'bg-white',
text: 'text-slate-700',
hoverBg: 'hover:bg-indigo-50',
hoverShadow: 'hover:shadow-indigo-500/20',
activeBg: 'bg-indigo-50',
activeText: 'text-indigo-700',
activeShadow: 'shadow-indigo-500/30',
rainbowBg: 'bg-gradient-to-r from-pink-50/50 via-purple-50/50 to-indigo-50/50',
rainbowShadow: 'hover:shadow-purple-500/20',
hoverBg: 'hover:bg-slate-50',
hoverShadow: 'hover:border-slate-300',
activeBg: 'bg-slate-100',
activeText: 'text-slate-900',
activeShadow: 'border-slate-900',
rainbowBg: 'bg-white',
rainbowShadow: 'hover:border-slate-300',
tabText: 'text-slate-600',
tabTextHover: 'hover:text-slate-900',
tabTextActive: 'text-slate-900',
@ -112,70 +112,63 @@ export const themeColors = {
},
},
// 低饱和度炫彩元素(用于标签、强调色等)
// 后台强调色(普通信息默认用中性色,状态再使用语义色)
accent: {
// 粉紫色系(低饱和度)
pink: {
bg: 'bg-pink-50',
text: 'text-pink-700',
border: 'border-pink-200',
hover: 'hover:bg-pink-100',
bg: 'bg-slate-50',
text: 'text-slate-700',
border: 'border-slate-200',
hover: 'hover:bg-slate-100',
},
// 紫色系(低饱和度)
purple: {
bg: 'bg-purple-50',
text: 'text-purple-700',
border: 'border-purple-200',
hover: 'hover:bg-purple-100',
bg: 'bg-slate-50',
text: 'text-slate-700',
border: 'border-slate-200',
hover: 'hover:bg-slate-100',
},
// 蓝色系(低饱和度)
blue: {
bg: 'bg-blue-50',
text: 'text-blue-700',
border: 'border-blue-200',
hover: 'hover:bg-blue-100',
bg: 'bg-slate-50',
text: 'text-slate-700',
border: 'border-slate-200',
hover: 'hover:bg-slate-100',
},
// 青色系(低饱和度)
cyan: {
bg: 'bg-cyan-50',
text: 'text-cyan-700',
border: 'border-cyan-200',
hover: 'hover:bg-cyan-100',
bg: 'bg-slate-50',
text: 'text-slate-700',
border: 'border-slate-200',
hover: 'hover:bg-slate-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',
bg: 'bg-amber-50',
text: 'text-amber-800',
border: 'border-amber-200',
hover: 'hover:bg-amber-100',
},
// 渐变炫彩(低饱和度)
rainbow: {
bg: 'bg-gradient-to-r from-pink-50 via-purple-50 via-indigo-50 to-blue-50',
bg: 'bg-slate-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',
border: 'border-slate-200',
hover: 'hover:bg-slate-100',
},
},
// Modal 颜色
modal: {
overlay: 'bg-slate-900/35 backdrop-blur-xl',
bg: 'bg-white/80',
border: 'border-white/30',
shadow: 'shadow-[0_35px_110px_rgba(15,23,42,0.25)] shadow-slate-900/20',
headerBorder: 'border-white/30',
overlay: 'bg-slate-900/45',
bg: 'bg-white',
border: 'border-slate-200',
shadow: 'shadow-lg shadow-slate-900/10',
headerBorder: 'border-slate-200',
title: 'text-slate-900',
body: 'text-slate-600',
footerBorder: 'border-white/30',
closeButton: 'text-slate-500 hover:text-slate-700 hover:bg-white/40',
footerBorder: 'border-slate-200',
closeButton: 'text-slate-500 hover:text-slate-700 hover:bg-slate-100',
},
// 开关切换器颜色