收敛工作台界面并完善安全分享入口
This commit is contained in:
parent
5b6dd8b487
commit
1a9921c9fe
@ -1 +1 @@
|
||||
1.0.0
|
||||
1.0.6
|
||||
|
||||
@ -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)}>
|
||||
以下分类来自模板管理中勾选了“添加到分类”的字段,相同字段只显示一个卡片。可以直接添加、删除、重命名选项。
|
||||
</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 ? (
|
||||
|
||||
@ -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',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -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({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 样式,
|
||||
不再需要单独的溢出与尺寸覆盖规则。 */
|
||||
|
||||
|
||||
344
app/page.tsx
344
app/page.tsx
@ -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}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 ? '注册中...' : '注册'}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
// 开关切换器样式(来自外观设置)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
// 开关切换器颜色
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user