收敛工作台界面并完善安全分享入口
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 (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* 红色框框:顶部Header区域 */}
|
|
||||||
<section
|
<section
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative overflow-hidden rounded-3xl border mb-6 flex-shrink-0',
|
'rounded-lg border mb-6 flex-shrink-0',
|
||||||
theme === 'dark'
|
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-700 bg-slate-900/80 text-white'
|
||||||
: '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)]',
|
: 'border-slate-200 bg-white text-slate-900',
|
||||||
'transition-all duration-500 p-6',
|
'p-5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative z-10 space-y-3">
|
<div className="space-y-1.5">
|
||||||
<p className={cn('text-sm font-semibold tracking-[0.2em] uppercase', theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600')}>
|
<h2 className={cn('text-xl font-semibold', textTitle)}>模板分类管理</h2>
|
||||||
Category Management
|
<p className={cn('text-sm', textDesc)}>
|
||||||
</p>
|
|
||||||
<h2 className={cn('text-2xl font-semibold', textTitle)}>模板分类管理</h2>
|
|
||||||
<p className={cn('text-sm leading-relaxed', textDesc)}>
|
|
||||||
以下分类来自模板管理中勾选了“添加到分类”的字段,相同字段只显示一个卡片。可以直接添加、删除、重命名选项。
|
以下分类来自模板管理中勾选了“添加到分类”的字段,相同字段只显示一个卡片。可以直接添加、删除、重命名选项。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -564,11 +560,10 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
|
|||||||
<div className="w-[320px] flex-shrink-0 space-y-5">
|
<div className="w-[320px] flex-shrink-0 space-y-5">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
styles.cardStyles.base,
|
styles.cardStyles.base,
|
||||||
'p-5 backdrop-blur-sm flex flex-col',
|
'p-5 flex flex-col',
|
||||||
'sticky top-24 max-h-[calc(100vh-12rem)]',
|
'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>
|
</p>
|
||||||
{/* 按钮移动到底部,顶部仅保留标题 */}
|
{/* 按钮移动到底部,顶部仅保留标题 */}
|
||||||
@ -617,7 +612,7 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
|
|||||||
{isEditingFields && (
|
{isEditingFields && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<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 ? (
|
{orderedEnabledFields.length === 0 ? (
|
||||||
<div className={cn('text-xs text-slate-500 dark:text-slate-400')}>暂无启用字段</div>
|
<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
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={cn(
|
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',
|
'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 ? 'ring-2 ring-indigo-200 shadow-xl' : '',
|
isDragging ? 'border-indigo-300 bg-indigo-50/70 dark:border-indigo-700 dark:bg-indigo-950/30' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -702,7 +697,7 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div className="space-y-2">
|
||||||
{disabledFieldMeta.length === 0 && (
|
{disabledFieldMeta.length === 0 && (
|
||||||
<div className={cn('text-xs text-slate-500 dark:text-slate-400')}>暂无可添加字段</div>
|
<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) => (
|
{disabledFieldMeta.map((field) => (
|
||||||
<div
|
<div
|
||||||
key={`${field.key}-disabled`}
|
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>
|
<div>
|
||||||
<p className="text-sm font-semibold text-slate-900 dark:text-white">{field.label}</p>
|
<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}`}
|
key={`${category.fieldKey}-${option}-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
styles.cardStyles.flat,
|
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 ? (
|
{isRenaming ? (
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
`"${String(value ?? '').replace(/"/g, '""')}"`
|
`"${String(value ?? '').replace(/"/g, '""')}"`
|
||||||
try {
|
try {
|
||||||
const [materialsResponse, logsResponse] = await Promise.all([
|
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'),
|
fetch('/api/admin/material-logs?limit=200'),
|
||||||
])
|
])
|
||||||
if (!materialsResponse.ok) throw new Error('获取素材数据失败')
|
if (!materialsResponse.ok) throw new Error('获取素材数据失败')
|
||||||
@ -477,20 +477,17 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<section
|
<section
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative overflow-hidden rounded-3xl border mb-6 flex-shrink-0',
|
'rounded-lg border mb-6 flex-shrink-0',
|
||||||
theme === 'dark'
|
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-700 bg-slate-900/80 text-white'
|
||||||
: '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)]',
|
: 'border-slate-200 bg-white text-slate-900',
|
||||||
'transition-all duration-500 p-6',
|
'p-5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative z-10 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div className="space-y-3">
|
<div className="space-y-1.5">
|
||||||
<p className={cn('text-sm font-semibold tracking-[0.2em] uppercase', theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600')}>
|
<h2 className={cn('text-xl font-semibold', textTitle)}>系统监控概览</h2>
|
||||||
System Monitor
|
<p className={cn('text-sm', textDesc)}>最近更新:{formattedLastUpdated}</p>
|
||||||
</p>
|
|
||||||
<h2 className={cn('text-2xl font-semibold', textTitle)}>系统监控概览</h2>
|
|
||||||
<p className={cn('text-sm leading-relaxed', textDesc)}>最近更新:{formattedLastUpdated}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{refreshing && (
|
{refreshing && (
|
||||||
@ -555,19 +552,13 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{OVERVIEW_CARDS.map((card) => {
|
{OVERVIEW_CARDS.map((card) => {
|
||||||
const CardContent = (
|
const CardContent = (
|
||||||
<div className="p-6">
|
<div className="p-5">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex-shrink-0">
|
<span className={cn('h-10 w-1 rounded-full', card.accentClass)} aria-hidden="true" />
|
||||||
<div className={cn('w-12 h-12 rounded-full flex items-center justify-center shadow-sm', card.className)}>
|
<dl className="min-w-0 flex-1">
|
||||||
<span className="text-white text-xl font-medium">{card.icon}</span>
|
<dt className={cn('text-sm font-medium truncate', textLabel)}>{card.label}</dt>
|
||||||
</div>
|
<dd className={cn('text-2xl font-semibold mt-1', textValue)}>{card.getValue(data)}</dd>
|
||||||
</div>
|
</dl>
|
||||||
<div className="ml-5 w-0 flex-1">
|
|
||||||
<dl>
|
|
||||||
<dt className={cn('text-base 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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -578,9 +569,8 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
key={card.label}
|
key={card.label}
|
||||||
className={cn(
|
className={cn(
|
||||||
styles.cardStyles.base,
|
styles.cardStyles.base,
|
||||||
'overflow-hidden cursor-pointer transition-all',
|
'overflow-hidden cursor-pointer transition-colors',
|
||||||
'hover:shadow-lg hover:scale-[1.02]',
|
theme === 'dark' ? 'hover:border-slate-500 hover:bg-slate-800/80' : 'hover:border-slate-300 hover:bg-slate-50'
|
||||||
theme === 'dark' ? 'hover:border-slate-500' : 'hover:border-slate-300'
|
|
||||||
)}
|
)}
|
||||||
onClick={() => router.push(card.href!)}
|
onClick={() => router.push(card.href!)}
|
||||||
title={`点击查看${card.label}详情`}
|
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="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<div className={cn(styles.cardStyles.base, 'p-6')}>
|
<div className={cn(styles.cardStyles.base, 'p-6')}>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<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>
|
<span className={cn('text-sm', textDesc)}>累计 {currentMonthDailyData.reduce((sum, item) => sum + item.count, 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-64">
|
<div className="h-64">
|
||||||
@ -631,7 +621,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
|
|
||||||
<div className={cn(styles.cardStyles.base, 'p-6')}>
|
<div className={cn(styles.cardStyles.base, 'p-6')}>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<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 && (
|
{monthlyUploads.length > 0 && (
|
||||||
<div className={cn('text-sm font-medium', textValue)}>
|
<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="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
<div className={cn(styles.cardStyles.base)}>
|
<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">
|
<div className="space-y-2 h-48 overflow-y-auto pr-1">
|
||||||
{(() => {
|
{(() => {
|
||||||
// 只显示一级部门(没有"-"分隔符的部门)
|
// 只显示一级部门(没有"-"分隔符的部门)
|
||||||
@ -707,7 +697,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(styles.cardStyles.base)}>
|
<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="space-y-3">
|
||||||
{data.distribution.roles.length ? (
|
{data.distribution.roles.length ? (
|
||||||
data.distribution.roles.map((item, index) => (
|
data.distribution.roles.map((item, index) => (
|
||||||
@ -729,7 +719,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(styles.cardStyles.base)}>
|
<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="space-y-3">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className={cn('text-sm', textRow)}>站点名称</span>
|
<span className={cn('text-sm', textRow)}>站点名称</span>
|
||||||
@ -769,7 +759,7 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(styles.cardStyles.base)}>
|
<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">
|
<div className="overflow-x-auto">
|
||||||
<table className={cn('min-w-full divide-y', theme === 'dark' ? 'divide-slate-700' : 'divide-slate-200')}>
|
<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'}>
|
<thead className={theme === 'dark' ? 'bg-slate-700' : 'bg-slate-50'}>
|
||||||
@ -1013,40 +1003,33 @@ export default function MonitorTab({ showToast }: MonitorTabProps) {
|
|||||||
|
|
||||||
const OVERVIEW_CARDS: Array<{
|
const OVERVIEW_CARDS: Array<{
|
||||||
label: string
|
label: string
|
||||||
icon: string
|
accentClass: string
|
||||||
className: string
|
|
||||||
getValue: (data: DashboardData) => number
|
getValue: (data: DashboardData) => number
|
||||||
href?: string
|
href?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
label: '总用户数',
|
label: '总用户数',
|
||||||
icon: '👥',
|
accentClass: 'bg-slate-400 dark:bg-slate-500',
|
||||||
className: 'bg-blue-500 shadow-blue-500/40',
|
|
||||||
getValue: (data) => data.overview.totalUsers,
|
getValue: (data) => data.overview.totalUsers,
|
||||||
href: '/admin?tab=users',
|
href: '/admin?tab=users',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '活跃用户',
|
label: '活跃用户',
|
||||||
icon: '⚡',
|
accentClass: 'bg-emerald-500 dark:bg-emerald-400',
|
||||||
className: 'bg-green-500 shadow-green-500/40',
|
|
||||||
getValue: (data) => data.overview.activeUsers,
|
getValue: (data) => data.overview.activeUsers,
|
||||||
href: '/admin?tab=users',
|
href: '/admin?tab=users',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '总素材数',
|
label: '总素材数',
|
||||||
icon: '📦',
|
accentClass: 'bg-indigo-500 dark:bg-indigo-400',
|
||||||
className: 'bg-purple-500 shadow-purple-500/40',
|
|
||||||
getValue: (data) => data.overview.totalMaterials,
|
getValue: (data) => data.overview.totalMaterials,
|
||||||
href: '/',
|
href: '/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '本月新用户',
|
label: '本月新用户',
|
||||||
icon: '🆕',
|
accentClass: 'bg-amber-500 dark:bg-amber-400',
|
||||||
className: 'bg-yellow-500 shadow-yellow-500/40',
|
|
||||||
getValue: (data) => data.overview.newUsersThisMonth,
|
getValue: (data) => data.overview.newUsersThisMonth,
|
||||||
href: '/admin?tab=users',
|
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 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 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 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 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 textSuperAdmin = theme === 'dark' ? 'text-white' : 'text-slate-900'
|
||||||
const textNormal = theme === 'dark' ? 'text-white group-hover:text-slate-200' : 'text-slate-900 group-hover:text-slate-700'
|
const textNormal = theme === 'dark' ? 'text-white' : 'text-slate-900'
|
||||||
|
|
||||||
const userLookup = useMemo(() => {
|
const userLookup = useMemo(() => {
|
||||||
const map = new Map<string, User>()
|
const map = new Map<string, User>()
|
||||||
@ -423,35 +423,35 @@ export default function UsersTab({
|
|||||||
|
|
||||||
const renderUserRow = useCallback(
|
const renderUserRow = useCallback(
|
||||||
(user: User, isManager = false) => (
|
(user: User, isManager = false) => (
|
||||||
<div key={user.id} className={cn(styles.cardStyles.hover, 'group flex items-start gap-3 cursor-pointer')}>
|
<div key={user.id} className={cn(styles.cardStyles.hover, 'group flex items-start gap-3 cursor-pointer')}>
|
||||||
<div className="flex-1 space-y-1">
|
<div className="flex-1 space-y-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-all duration-200 text-sm',
|
'transition-colors duration-200 text-sm',
|
||||||
isManager || user.isSuperAdmin
|
isManager || user.isSuperAdmin
|
||||||
? cn('font-semibold', textSuperAdmin)
|
? cn('font-semibold', textSuperAdmin)
|
||||||
: cn('font-medium', textNormal),
|
: cn('font-medium', textNormal),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</span>
|
||||||
|
{user.isSuperAdmin && (
|
||||||
|
<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 && (
|
||||||
{user.username}
|
<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>
|
管理员
|
||||||
{user.isSuperAdmin && (
|
</span>
|
||||||
<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">
|
)}
|
||||||
最高权限
|
{renderStatusBadges(user)}
|
||||||
</span>
|
</div>
|
||||||
)}
|
<div className={cn('text-xs', textDesc)}>素材:{user._count.materials}</div>
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
{renderStatusBadges(user)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={cn('text-xs', textDesc)}>素材:{user._count.materials}</div>
|
{renderActionButtons(user)}
|
||||||
</div>
|
</div>
|
||||||
{renderActionButtons(user)}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
[renderActionButtons],
|
[renderActionButtons],
|
||||||
)
|
)
|
||||||
@ -462,115 +462,111 @@ export default function UsersTab({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cn(styles.spacingStyles.section)}>
|
<div className={cn(styles.spacingStyles.section)}>
|
||||||
<section
|
<section
|
||||||
className={cn(
|
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'
|
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-700 bg-slate-900/40 text-white'
|
||||||
: '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)]',
|
: 'border-slate-200 bg-white text-slate-900',
|
||||||
'transition-all duration-500 p-6',
|
'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="relative z-10 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className={cn('text-sm font-semibold tracking-[0.2em] uppercase', theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600')}>
|
<h2 className={cn('text-xl font-semibold', textTitle)}>成员目录</h2>
|
||||||
User Management
|
<p className={cn('text-sm leading-relaxed', textDesc)}>
|
||||||
</p>
|
结合组织树与搜索框,可按姓名、邮箱或部门快速定位管理员与成员。
|
||||||
<h2 className={cn('text-2xl font-semibold', textTitle)}>成员目录</h2>
|
</p>
|
||||||
<p className={cn('text-sm leading-relaxed', textDesc)}>
|
</div>
|
||||||
结合组织树与搜索框,可按姓名、邮箱或部门快速定位管理员与成员。
|
<div className="relative w-full md:w-[320px] lg:w-[360px]">
|
||||||
</p>
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索用户名、邮箱或部门..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className={cn(styles.inputStyles.base, 'pl-11 pr-10')}
|
||||||
|
/>
|
||||||
|
<svg className={cn('absolute left-4 top-3 w-5 h-5', theme === 'dark' ? 'text-slate-500' : 'text-slate-400')} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSearchTerm('')}
|
||||||
|
className={cn(
|
||||||
|
styles.buttonStyles.ghost,
|
||||||
|
styles.buttonModifiers.icon,
|
||||||
|
'absolute right-2 top-2',
|
||||||
|
theme === 'dark' ? 'text-slate-400 hover:text-slate-200' : 'text-slate-500 hover:text-slate-700',
|
||||||
|
)}
|
||||||
|
aria-label="清空成员搜索"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative w-full md:w-[320px] lg:w-[360px]">
|
</section>
|
||||||
<input
|
|
||||||
type="text"
|
<section className={cn(styles.cardStyles.base, 'overflow-hidden')}>
|
||||||
placeholder="搜索用户名、邮箱或部门..."
|
<div className="overflow-hidden p-4">
|
||||||
value={searchTerm}
|
{showSearchResults ? (
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
treeToRender.length > 0 ? (
|
||||||
className={cn(styles.inputStyles.base, 'pl-11 pr-10')}
|
<UserTree
|
||||||
/>
|
nodes={treeToRender}
|
||||||
<svg className={cn('absolute left-4 top-3 w-5 h-5', theme === 'dark' ? 'text-slate-500' : 'text-slate-400')} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
collapsedNodeIds={collapsedUserNodeIds}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
onToggleUserNode={toggleUserNodeCollapse}
|
||||||
</svg>
|
searchTerm={searchTerm}
|
||||||
{searchTerm && (
|
renderStatusBadges={renderStatusBadges}
|
||||||
<button
|
renderActionButtons={renderActionButtons}
|
||||||
type="button"
|
/>
|
||||||
onClick={() => setSearchTerm('')}
|
) : (
|
||||||
className={cn(
|
<div className={cn('text-center py-12', textDesc)}>暂无匹配的用户</div>
|
||||||
styles.buttonStyles.ghost,
|
)
|
||||||
styles.buttonModifiers.icon,
|
) : (
|
||||||
'absolute right-2 top-2',
|
<DepartmentHierarchy
|
||||||
theme === 'dark' ? 'text-slate-400 hover:text-slate-200' : 'text-slate-500 hover:text-slate-700',
|
departmentTree={departmentTree}
|
||||||
)}
|
departmentUsersMap={departmentUsersMap}
|
||||||
aria-label="清空成员搜索"
|
departmentMaterialCounts={departmentMaterialCounts}
|
||||||
>
|
collapsedDepartments={collapsedDepartments}
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
onToggleDepartmentCollapse={toggleDepartmentCollapse}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
renderUserRow={renderUserRow}
|
||||||
</svg>
|
totalUsers={filteredUsers.length}
|
||||||
</button>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
|
<UserEditModal
|
||||||
<section className={cn(styles.cardStyles.base, 'overflow-hidden')}>
|
isOpen={editingUserModal.isOpen}
|
||||||
<div className="overflow-hidden p-4">
|
user={editingUser}
|
||||||
{showSearchResults ? (
|
departments={departments}
|
||||||
treeToRender.length > 0 ? (
|
editingUsername={editingUsername}
|
||||||
<UserTree
|
selectedDepartment={selectedDepartment}
|
||||||
nodes={treeToRender}
|
managerNames={managerNames}
|
||||||
collapsedNodeIds={collapsedUserNodeIds}
|
showTransferButton={showTransferButton}
|
||||||
onToggleUserNode={toggleUserNodeCollapse}
|
canChangeProtectedFields={Boolean(currentIsSuperAdmin)}
|
||||||
searchTerm={searchTerm}
|
onChangeUsername={setEditingUsername}
|
||||||
renderStatusBadges={renderStatusBadges}
|
onChangeDepartment={setSelectedDepartment}
|
||||||
renderActionButtons={renderActionButtons}
|
onClose={handleCloseEditModal}
|
||||||
/>
|
onSave={handleSaveUser}
|
||||||
) : (
|
onToggleRole={handleModalRole}
|
||||||
<div className={cn('text-center py-12', textDesc)}>暂无匹配的用户</div>
|
onToggleActive={handleModalActive}
|
||||||
)
|
onToggleDownload={handleModalDownload}
|
||||||
) : (
|
onDelete={handleDeleteUserInternal}
|
||||||
<DepartmentHierarchy
|
onOpenTransferModal={handleOpenTransferModal}
|
||||||
departmentTree={departmentTree}
|
hasDepartments={hasDepartments}
|
||||||
departmentUsersMap={departmentUsersMap}
|
/>
|
||||||
departmentMaterialCounts={departmentMaterialCounts}
|
<TransferSuperAdminModal
|
||||||
collapsedDepartments={collapsedDepartments}
|
isOpen={passwordModal.isOpen}
|
||||||
onToggleDepartmentCollapse={toggleDepartmentCollapse}
|
targetUser={editingUser}
|
||||||
renderUserRow={renderUserRow}
|
password={transferPassword}
|
||||||
totalUsers={filteredUsers.length}
|
onClose={handleCloseTransferModal}
|
||||||
/>
|
onChangePassword={setTransferPassword}
|
||||||
)}
|
onConfirm={handleConfirmTransfer}
|
||||||
</div>
|
/>
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<UserEditModal
|
|
||||||
isOpen={editingUserModal.isOpen}
|
|
||||||
user={editingUser}
|
|
||||||
departments={departments}
|
|
||||||
editingUsername={editingUsername}
|
|
||||||
selectedDepartment={selectedDepartment}
|
|
||||||
managerNames={managerNames}
|
|
||||||
showTransferButton={showTransferButton}
|
|
||||||
canChangeProtectedFields={Boolean(currentIsSuperAdmin)}
|
|
||||||
onChangeUsername={setEditingUsername}
|
|
||||||
onChangeDepartment={setSelectedDepartment}
|
|
||||||
onClose={handleCloseEditModal}
|
|
||||||
onSave={handleSaveUser}
|
|
||||||
onToggleRole={handleModalRole}
|
|
||||||
onToggleActive={handleModalActive}
|
|
||||||
onToggleDownload={handleModalDownload}
|
|
||||||
onDelete={handleDeleteUserInternal}
|
|
||||||
onOpenTransferModal={handleOpenTransferModal}
|
|
||||||
hasDepartments={hasDepartments}
|
|
||||||
/>
|
|
||||||
<TransferSuperAdminModal
|
|
||||||
isOpen={passwordModal.isOpen}
|
|
||||||
targetUser={editingUser}
|
|
||||||
password={transferPassword}
|
|
||||||
onClose={handleCloseTransferModal}
|
|
||||||
onChangePassword={setTransferPassword}
|
|
||||||
onConfirm={handleConfirmTransfer}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ function DepartmentHierarchyComponent({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{managerCount > 0 && (
|
{managerCount > 0 && (
|
||||||
<div className="space-y-1">
|
<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 className="space-y-1">{entry!.managers.map((user) => renderUserRow(user, true))}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -77,16 +77,16 @@ function DepartmentHierarchyComponent({
|
|||||||
|
|
||||||
const depthContainerClasses =
|
const depthContainerClasses =
|
||||||
depthLevel === 0
|
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
|
: depthLevel === 1
|
||||||
? 'bg-indigo-50/70 dark:bg-indigo-900/30 border-indigo-300 dark:border-indigo-500/80'
|
? 'bg-white dark:bg-slate-900/30 border-slate-300 dark:border-slate-700'
|
||||||
: 'bg-indigo-50/50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-700/80'
|
: 'bg-white dark:bg-slate-900/20 border-slate-200 dark:border-slate-800'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={node.fullName} className="space-y-2">
|
<div key={node.fullName} className="space-y-2">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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,
|
depthContainerClasses,
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -99,7 +99,7 @@ function DepartmentHierarchyComponent({
|
|||||||
{canToggle ? (
|
{canToggle ? (
|
||||||
<button
|
<button
|
||||||
type="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 ? '展开' : '折叠'}
|
title={isCollapsed ? '展开' : '折叠'}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -108,7 +108,7 @@ function DepartmentHierarchyComponent({
|
|||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className={cn(
|
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',
|
!isCollapsed && 'rotate-90',
|
||||||
)}
|
)}
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -130,7 +130,7 @@ function DepartmentHierarchyComponent({
|
|||||||
{managerNames.map((name) => (
|
{managerNames.map((name) => (
|
||||||
<span
|
<span
|
||||||
key={name}
|
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}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
@ -167,17 +167,17 @@ function DepartmentHierarchyComponent({
|
|||||||
|
|
||||||
sections.push(
|
sections.push(
|
||||||
<div key="user-tree-root" className="space-y-2">
|
<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">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="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 ? '展开' : '折叠'}
|
title={rootCollapsed ? '展开' : '折叠'}
|
||||||
onClick={() => onToggleDepartmentCollapse('ROOT')}
|
onClick={() => onToggleDepartmentCollapse('ROOT')}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className={cn(
|
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',
|
!rootCollapsed && 'rotate-90',
|
||||||
)}
|
)}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|||||||
@ -16,8 +16,13 @@ export default function MonitorPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'unauthenticated') {
|
if (status === 'unauthenticated') {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}, [status, router])
|
|
||||||
|
if (status === 'authenticated' && (session.user as any)?.role !== 'ADMIN') {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}, [status, session, router])
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -78,7 +78,7 @@ interface AuthorMaterialsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthorPage() {
|
export default function AuthorPage() {
|
||||||
const { data: session } = useSession()
|
const { data: session, status } = useSession()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const styles = useThemeStyles()
|
const styles = useThemeStyles()
|
||||||
@ -98,6 +98,8 @@ export default function AuthorPage() {
|
|||||||
const pageSize = 20
|
const pageSize = 20
|
||||||
|
|
||||||
const fetchMaterials = useCallback(async () => {
|
const fetchMaterials = useCallback(async () => {
|
||||||
|
if (status !== 'authenticated') return
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@ -133,11 +135,18 @@ export default function AuthorPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [authorName, page])
|
}, [authorName, page, status])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMaterials()
|
if (status === 'unauthenticated') {
|
||||||
}, [fetchMaterials])
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'authenticated') {
|
||||||
|
fetchMaterials()
|
||||||
|
}
|
||||||
|
}, [fetchMaterials, router, status])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||||
@ -166,8 +175,8 @@ export default function AuthorPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
<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-white" />
|
<UserIcon className="w-10 h-10 text-slate-500 dark:text-slate-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|||||||
@ -34,17 +34,17 @@ body {
|
|||||||
rgb(var(--background-start-rgb));
|
rgb(var(--background-start-rgb));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apple-style button */
|
/* Legacy button utilities, kept visually neutral for the FLNXI workspace. */
|
||||||
.btn-apple {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
.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) */
|
/* Scrollbar styling (light & dark) */
|
||||||
@ -138,69 +138,37 @@ body {
|
|||||||
@keyframes modal-overlay-apple {
|
@keyframes modal-overlay-apple {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
backdrop-filter: blur(0px);
|
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
backdrop-filter: blur(25px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modal-content-apple {
|
@keyframes modal-content-apple {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(28px) scale(0.92);
|
transform: translateY(8px);
|
||||||
filter: blur(12px);
|
|
||||||
}
|
|
||||||
55% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(-4px) scale(1.02);
|
|
||||||
filter: blur(0px);
|
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0) scale(1);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-overlay-apple {
|
.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 {
|
.modal-content-apple {
|
||||||
position: relative;
|
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;
|
transform-origin: top center;
|
||||||
box-shadow:
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.16);
|
||||||
0 28px 80px rgba(15, 23, 42, 0.22),
|
|
||||||
0 6px 24px rgba(15, 23, 42, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .modal-content-apple {
|
.dark .modal-content-apple {
|
||||||
box-shadow:
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Material 预览弹窗现在重新使用统一 UI Modal 样式,
|
/* Material 预览弹窗现在重新使用统一 UI Modal 样式,
|
||||||
不再需要单独的溢出与尺寸覆盖规则。 */
|
不再需要单独的溢出与尺寸覆盖规则。 */
|
||||||
|
|
||||||
|
|||||||
726
app/page.tsx
726
app/page.tsx
File diff suppressed because it is too large
Load Diff
@ -68,7 +68,7 @@ interface Material {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status, update } = useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const styles = useThemeStyles()
|
const styles = useThemeStyles()
|
||||||
const { alert } = useDialog()
|
const { alert } = useDialog()
|
||||||
@ -323,8 +323,10 @@ export default function ProfilePage() {
|
|||||||
setShowAvatarModal(false)
|
setShowAvatarModal(false)
|
||||||
setAvatarFile(null)
|
setAvatarFile(null)
|
||||||
setAvatarPreview('')
|
setAvatarPreview('')
|
||||||
// 刷新页面以更新 session 中的头像(JWT callback 会从数据库获取最新头像)
|
// 触发 session 更新,JWT callback 会从数据库获取最新头像
|
||||||
// 由于 JWT callback 每5分钟自动刷新一次,也可以等待自动刷新
|
// 使用 update() 方法触发 trigger='update',立即刷新 token
|
||||||
|
await update()
|
||||||
|
// 刷新页面确保所有组件使用最新的 session
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} else {
|
} else {
|
||||||
const error = await res.json()
|
const error = await res.json()
|
||||||
|
|||||||
@ -4,6 +4,9 @@ import { useState, useEffect } from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
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() {
|
export default function RegisterPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -15,6 +18,11 @@ export default function RegisterPage() {
|
|||||||
const [confirmPassword, setConfirmPassword] = useState('')
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
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(() => {
|
useEffect(() => {
|
||||||
fetchDepartments()
|
fetchDepartments()
|
||||||
@ -41,8 +49,9 @@ export default function RegisterPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 6) {
|
const passwordValidation = validatePasswordStrength(password)
|
||||||
setError('密码长度至少为6个字符')
|
if (!passwordValidation.valid) {
|
||||||
|
setError(passwordValidation.error || '密码不符合安全要求')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,29 +82,36 @@ export default function RegisterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="max-w-md w-full">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<Image
|
{DEFAULT_LOGIN_LOGO && !logoError ? (
|
||||||
src={process.env.NEXT_PUBLIC_DEFAULT_LOGIN_LOGO_URL || ''}
|
<Image
|
||||||
alt="FLNXI素材库"
|
src={DEFAULT_LOGIN_LOGO}
|
||||||
width={80}
|
alt="FLNXI素材库"
|
||||||
height={80}
|
width={80}
|
||||||
unoptimized
|
height={80}
|
||||||
className="rounded-apple shadow-apple"
|
unoptimized
|
||||||
/>
|
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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">FLNXI素材库</h1>
|
<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">创建您的账号,开始分享3D素材</p>
|
<p className="text-slate-600 dark:text-slate-400 mt-2">申请加入素材库</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Register Form */}
|
{/* Register Form */}
|
||||||
<div className="card-apple p-8">
|
<div className={panelClass}>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -108,7 +124,7 @@ export default function RegisterPage() {
|
|||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="input-apple"
|
className={inputClass}
|
||||||
placeholder="your@email.com"
|
placeholder="your@email.com"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -122,7 +138,7 @@ export default function RegisterPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
className="input-apple"
|
className={inputClass}
|
||||||
placeholder="选择一个用户名"
|
placeholder="选择一个用户名"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -135,7 +151,7 @@ export default function RegisterPage() {
|
|||||||
<select
|
<select
|
||||||
value={department}
|
value={department}
|
||||||
onChange={(e) => setDepartment(e.target.value)}
|
onChange={(e) => setDepartment(e.target.value)}
|
||||||
className="input-apple"
|
className={inputClass}
|
||||||
>
|
>
|
||||||
<option value="">请选择部门</option>
|
<option value="">请选择部门</option>
|
||||||
{departments.map((dept) => (
|
{departments.map((dept) => (
|
||||||
@ -159,9 +175,10 @@ export default function RegisterPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="input-apple"
|
className={inputClass}
|
||||||
placeholder="••••••••"
|
placeholder={PASSWORD_POLICY_DESCRIPTION}
|
||||||
required
|
required
|
||||||
|
minLength={10}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -173,15 +190,16 @@ export default function RegisterPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
className="input-apple"
|
className={inputClass}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
|
minLength={10}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full btn-primary"
|
className={buttonClass}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? '注册中...' : '注册'}
|
{loading ? '注册中...' : '注册'}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ interface AudioVisualizerProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认封面图组件 - Apple Music 风格
|
// 默认音频封面图
|
||||||
function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
|
function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'w-full h-full',
|
sm: 'w-full h-full',
|
||||||
@ -30,19 +30,15 @@ function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
'relative overflow-hidden rounded-lg',
|
'relative overflow-hidden rounded-lg border',
|
||||||
'bg-gradient-to-br from-[#1a1a2e] via-[#16213e] to-[#0f3460]'
|
'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="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
|
<svg
|
||||||
className={cn(iconSizes[size], 'text-white/80')}
|
className={cn(iconSizes[size], 'text-slate-500 dark:text-slate-300')}
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
@ -54,7 +50,7 @@ function DefaultAudioCover({ size }: { size: 'sm' | 'md' | 'lg' }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 圆形放射状频谱可视化器 - 尖刺风格
|
// 圆形音频频谱可视化器
|
||||||
export default function AudioVisualizer({
|
export default function AudioVisualizer({
|
||||||
audioElement,
|
audioElement,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
@ -72,7 +68,7 @@ export default function AudioVisualizer({
|
|||||||
const [isVisualizerActive, setIsVisualizerActive] = useState(false)
|
const [isVisualizerActive, setIsVisualizerActive] = useState(false)
|
||||||
const [showCover, setShowCover] = useState(true)
|
const [showCover, setShowCover] = useState(true)
|
||||||
|
|
||||||
// 尺寸配置 - 增加频谱条数量以获得更密集的尖刺效果
|
// 尺寸配置
|
||||||
const sizeConfig = {
|
const sizeConfig = {
|
||||||
sm: { width: 160, height: 160, bars: 64, innerRadius: 25, maxBarHeight: 35 },
|
sm: { width: 160, height: 160, bars: 64, innerRadius: 25, maxBarHeight: 35 },
|
||||||
md: { width: 280, height: 280, bars: 96, innerRadius: 45, maxBarHeight: 65 },
|
md: { width: 280, height: 280, bars: 96, innerRadius: 45, maxBarHeight: 65 },
|
||||||
@ -125,7 +121,7 @@ export default function AudioVisualizer({
|
|||||||
}
|
}
|
||||||
}, [audioElement])
|
}, [audioElement])
|
||||||
|
|
||||||
// 绘制尖刺放射状频谱
|
// 绘制低调的放射状频谱
|
||||||
const draw = useCallback(() => {
|
const draw = useCallback(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
const analyser = analyserRef.current
|
const analyser = analyserRef.current
|
||||||
@ -141,21 +137,28 @@ export default function AudioVisualizer({
|
|||||||
const { width, height, bars, innerRadius, maxBarHeight } = config
|
const { width, height, bars, innerRadius, maxBarHeight } = config
|
||||||
const centerX = width / 2
|
const centerX = width / 2
|
||||||
const centerY = height / 2
|
const centerY = height / 2
|
||||||
|
const isDarkMode = document.documentElement.classList.contains('dark')
|
||||||
|
|
||||||
// 清除画布
|
// 清除画布
|
||||||
ctx.clearRect(0, 0, width, height)
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
// 绘制深色背景圆
|
// 绘制中性背景圆
|
||||||
const bgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius + maxBarHeight + 20)
|
const bgGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius + maxBarHeight + 20)
|
||||||
bgGradient.addColorStop(0, 'rgba(15, 23, 42, 0.95)')
|
if (isDarkMode) {
|
||||||
bgGradient.addColorStop(0.4, 'rgba(15, 23, 42, 0.9)')
|
bgGradient.addColorStop(0, 'rgba(30, 41, 59, 0.98)')
|
||||||
bgGradient.addColorStop(1, 'rgba(15, 23, 42, 0.7)')
|
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.fillStyle = bgGradient
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(centerX, centerY, innerRadius + maxBarHeight + 15, 0, Math.PI * 2)
|
ctx.arc(centerX, centerY, innerRadius + maxBarHeight + 15, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
|
||||||
// 绘制频谱尖刺
|
// 绘制频谱线条
|
||||||
const angleStep = (Math.PI * 2) / bars
|
const angleStep = (Math.PI * 2) / bars
|
||||||
const usableData = Math.floor(bufferLength * 0.7)
|
const usableData = Math.floor(bufferLength * 0.7)
|
||||||
|
|
||||||
@ -165,90 +168,55 @@ export default function AudioVisualizer({
|
|||||||
const nextIndex = Math.min(dataIndex + 1, usableData - 1)
|
const nextIndex = Math.min(dataIndex + 1, usableData - 1)
|
||||||
const value = (dataArray[dataIndex] + dataArray[nextIndex]) / 2
|
const value = (dataArray[dataIndex] + dataArray[nextIndex]) / 2
|
||||||
|
|
||||||
// 添加一些随机变化使效果更自然
|
const barHeight = Math.max(3, (value / 255) * maxBarHeight)
|
||||||
const randomFactor = 0.9 + Math.random() * 0.2
|
|
||||||
const barHeight = Math.max(3, (value / 255) * maxBarHeight * randomFactor)
|
|
||||||
|
|
||||||
const angle = i * angleStep - Math.PI / 2 // 从顶部开始
|
const angle = i * angleStep - Math.PI / 2 // 从顶部开始
|
||||||
|
|
||||||
// 计算尖刺的起点和终点
|
// 计算线条的起点和终点
|
||||||
const x1 = centerX + Math.cos(angle) * innerRadius
|
const x1 = centerX + Math.cos(angle) * innerRadius
|
||||||
const y1 = centerY + Math.sin(angle) * innerRadius
|
const y1 = centerY + Math.sin(angle) * innerRadius
|
||||||
const x2 = centerX + Math.cos(angle) * (innerRadius + barHeight)
|
const x2 = centerX + Math.cos(angle) * (innerRadius + barHeight)
|
||||||
const y2 = centerY + Math.sin(angle) * (innerRadius + barHeight)
|
const y2 = centerY + Math.sin(angle) * (innerRadius + barHeight)
|
||||||
|
|
||||||
// 创建渐变色 - 使用项目的蓝色/青色主题
|
|
||||||
const gradient = ctx.createLinearGradient(x1, y1, x2, y2)
|
|
||||||
const intensity = value / 255
|
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.beginPath()
|
||||||
ctx.strokeStyle = gradient
|
ctx.strokeStyle = isDarkMode
|
||||||
ctx.lineWidth = 2
|
? `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.lineCap = 'round'
|
||||||
ctx.moveTo(x1, y1)
|
ctx.moveTo(x1, y1)
|
||||||
ctx.lineTo(x2, y2)
|
ctx.lineTo(x2, y2)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// 在高强度时添加发光效果
|
|
||||||
if (intensity > 0.5) {
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.strokeStyle = `rgba(56, 189, 248, ${intensity * 0.3})`
|
|
||||||
ctx.lineWidth = 4
|
|
||||||
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)
|
const centerGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius - 5)
|
||||||
centerGradient.addColorStop(0, 'rgba(15, 23, 42, 1)')
|
if (isDarkMode) {
|
||||||
centerGradient.addColorStop(0.7, 'rgba(15, 23, 42, 0.95)')
|
centerGradient.addColorStop(0, 'rgba(15, 23, 42, 1)')
|
||||||
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.fillStyle = centerGradient
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
|
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
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.lineWidth = 1.5
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
|
ctx.arc(centerX, centerY, innerRadius - 5, 0, Math.PI * 2)
|
||||||
ctx.stroke()
|
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.font = `${innerRadius * 0.5}px sans-serif`
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.textBaseline = 'middle'
|
ctx.textBaseline = 'middle'
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import Modal from '@/components/ui/Modal'
|
|||||||
import Input from '@/components/ui/Input'
|
import Input from '@/components/ui/Input'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import { useThemeStyles, cn } from '@/lib/ui-styles'
|
import { useThemeStyles, cn } from '@/lib/ui-styles'
|
||||||
|
import { PASSWORD_POLICY_DESCRIPTION, validatePasswordStrength } from '@/lib/password-policy'
|
||||||
|
|
||||||
interface ChangePasswordModalProps {
|
interface ChangePasswordModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@ -48,8 +49,9 @@ export default function ChangePasswordModal({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
const passwordValidation = validatePasswordStrength(newPassword)
|
||||||
setError('新密码长度至少为6位')
|
if (!passwordValidation.valid) {
|
||||||
|
setError(passwordValidation.error || '新密码不符合安全要求')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,15 +63,16 @@ export default function ChangePasswordModal({
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const payload = isRequired
|
||||||
|
? { newPassword }
|
||||||
|
: { oldPassword, newPassword }
|
||||||
|
|
||||||
const response = await fetch('/api/user/change-password', {
|
const response = await fetch('/api/user/change-password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload),
|
||||||
oldPassword: isRequired ? '88888888' : oldPassword,
|
|
||||||
newPassword
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
@ -120,7 +123,7 @@ export default function ChangePasswordModal({
|
|||||||
'border-yellow-200 dark:border-yellow-800'
|
'border-yellow-200 dark:border-yellow-800'
|
||||||
)}>
|
)}>
|
||||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||||
⚠️ 您正在使用默认密码,为了账户安全,请立即修改密码。
|
⚠️ 您正在使用系统初始化密码,为了账户安全,请立即修改密码。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -150,9 +153,9 @@ export default function ChangePasswordModal({
|
|||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
placeholder="请输入新密码(至少6位)"
|
placeholder={PASSWORD_POLICY_DESCRIPTION}
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={10}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -167,7 +170,7 @@ export default function ChangePasswordModal({
|
|||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
placeholder="请再次输入新密码"
|
placeholder="请再次输入新密码"
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={10}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,7 +41,7 @@ const THEME_STORAGE_KEY = 'admin-theme-preference'
|
|||||||
|
|
||||||
// 默认导航栏 LOGO:优先使用构建时环境变量,否则使用内置静态资源
|
// 默认导航栏 LOGO:优先使用构建时环境变量,否则使用内置静态资源
|
||||||
const DEFAULT_HEADER_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) {
|
export default function Header({ session, compactMode }: HeaderProps) {
|
||||||
const styles = useThemeStyles()
|
const styles = useThemeStyles()
|
||||||
@ -751,4 +751,3 @@ export default function Header({ session, compactMode }: HeaderProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -133,8 +133,8 @@ describe('AudioVisualizer 组件', () => {
|
|||||||
const { container } = render(
|
const { container } = render(
|
||||||
<AudioVisualizer audioElement={null} isPlaying={false} />
|
<AudioVisualizer audioElement={null} isPlaying={false} />
|
||||||
)
|
)
|
||||||
// 检查是否有默认封面的渐变背景
|
// 检查是否有默认封面的中性背景
|
||||||
const defaultCover = container.querySelector('.bg-gradient-to-br')
|
const defaultCover = container.querySelector('.bg-slate-100')
|
||||||
expect(defaultCover).toBeInTheDocument()
|
expect(defaultCover).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -389,7 +389,7 @@ describe('AudioVisualizer 边界情况', () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
// 空字符串应该被视为 falsy,显示默认封面
|
// 空字符串应该被视为 falsy,显示默认封面
|
||||||
const defaultCover = container.querySelector('.bg-gradient-to-br')
|
const defaultCover = container.querySelector('.bg-slate-100')
|
||||||
expect(defaultCover).toBeInTheDocument()
|
expect(defaultCover).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -62,12 +62,12 @@ export default function Avatar({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full flex items-center justify-center font-semibold',
|
'rounded-full flex items-center justify-center font-semibold',
|
||||||
'border-2 shadow-md transition-all',
|
'border-2 shadow-sm transition-colors',
|
||||||
bgColor,
|
bgColor,
|
||||||
textColor,
|
textColor,
|
||||||
borderColor,
|
borderColor,
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
onClick && 'cursor-pointer hover:scale-105 hover:shadow-lg',
|
onClick && 'cursor-pointer',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Tag 组件
|
* Tag 组件
|
||||||
*
|
*
|
||||||
* 统一的标签组件,遵循三层架构设计,采用苹果美学风格
|
* 统一的标签组件,适合后台筛选与分类展示
|
||||||
* 用于显示分类、类型等标签信息
|
* 用于显示分类、类型等标签信息
|
||||||
* 支持多种颜色(blue、green、orange、purple等)和尺寸(sm、md、lg)
|
* 支持少量语义色和尺寸(xs、sm、md、lg)
|
||||||
* 支持点击事件,可用于筛选等功能
|
* 支持点击事件,可用于筛选等功能
|
||||||
*
|
*
|
||||||
* 苹果美学特点:
|
|
||||||
* - 圆角更大(rounded-full)
|
|
||||||
* - 背景色更柔和(低饱和度)
|
|
||||||
* - 选中状态使用更明显的视觉反馈
|
|
||||||
* - 过渡动画更流畅
|
|
||||||
*
|
|
||||||
* @created 2025-11-22
|
* @created 2025-11-22
|
||||||
* @updated 2025-01-22 - 重新设计为苹果美学风格
|
* @updated 2026-05-25 - 收敛为后台工作台风格
|
||||||
* @component
|
* @component
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -39,34 +33,30 @@ export default function Tag({
|
|||||||
onClick,
|
onClick,
|
||||||
selected = false,
|
selected = false,
|
||||||
}: TagProps) {
|
}: TagProps) {
|
||||||
// 使用主题样式钩子(保留以便将来扩展)
|
|
||||||
useThemeStyles()
|
useThemeStyles()
|
||||||
|
|
||||||
// 苹果美学风格的颜色配置(更柔和的背景,更清晰的文字)
|
const colorClasses: Record<NonNullable<TagProps['color']>, string> = {
|
||||||
// 修复:提高未选中状态的文字对比度,确保在素材数量为0时文字仍然可见
|
|
||||||
// 2025-12-13: 增强hover状态的文字颜色对比度,使用更深的文字颜色
|
|
||||||
const colorClasses = {
|
|
||||||
blue: selected
|
blue: selected
|
||||||
? 'bg-blue-500 dark:bg-blue-600 text-white border-blue-500 dark:border-blue-600'
|
? 'bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-950 border-slate-900 dark:border-slate-100'
|
||||||
: '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-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
|
green: selected
|
||||||
? 'bg-emerald-500 dark:bg-emerald-600 text-white border-emerald-500 dark:border-emerald-600'
|
? 'bg-emerald-700 dark:bg-emerald-500 text-white border-emerald-700 dark:border-emerald-500'
|
||||||
: '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-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
|
orange: selected
|
||||||
? 'bg-orange-500 dark:bg-orange-600 text-white border-orange-500 dark:border-orange-600'
|
? 'bg-amber-700 dark:bg-amber-500 text-white dark:text-slate-950 border-amber-700 dark:border-amber-500'
|
||||||
: '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-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
|
purple: selected
|
||||||
? 'bg-purple-500 dark:bg-purple-600 text-white border-purple-500 dark:border-purple-600'
|
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
|
||||||
: '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-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
|
indigo: selected
|
||||||
? 'bg-indigo-500 dark:bg-indigo-600 text-white border-indigo-500 dark:border-indigo-600'
|
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
|
||||||
: '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-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
|
pink: selected
|
||||||
? 'bg-pink-500 dark:bg-pink-600 text-white border-pink-500 dark:border-pink-600'
|
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
|
||||||
: '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-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
|
cyan: selected
|
||||||
? 'bg-cyan-500 dark:bg-cyan-600 text-white border-cyan-500 dark:border-cyan-600'
|
? 'bg-slate-700 dark:bg-slate-200 text-white dark:text-slate-950 border-slate-700 dark:border-slate-200'
|
||||||
: '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-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 = {
|
const sizeClasses = {
|
||||||
@ -76,15 +66,11 @@ export default function Tag({
|
|||||||
lg: 'px-5 py-2 text-base font-medium',
|
lg: 'px-5 py-2 text-base font-medium',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 苹果美学:圆角更大,过渡更流畅,选中状态有阴影
|
|
||||||
const baseClasses = cn(
|
const baseClasses = cn(
|
||||||
'inline-flex items-center justify-center',
|
'inline-flex items-center justify-center',
|
||||||
'rounded-full border',
|
'rounded-md border',
|
||||||
'transition-all duration-200 ease-out',
|
'transition-colors duration-200 ease-out',
|
||||||
'backdrop-blur-sm',
|
onClick && 'cursor-pointer'
|
||||||
selected && 'shadow-md scale-105',
|
|
||||||
!selected && onClick && 'hover:scale-105 hover:shadow-sm',
|
|
||||||
onClick && 'cursor-pointer active:scale-95'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Tag 组件单元测试
|
* 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
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -15,10 +11,9 @@ import { render, screen, fireEvent } from '@testing-library/react'
|
|||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
import Tag from '../Tag'
|
import Tag from '../Tag'
|
||||||
|
|
||||||
// Mock useThemeStyles hook
|
|
||||||
jest.mock('@/lib/ui-styles', () => ({
|
jest.mock('@/lib/ui-styles', () => ({
|
||||||
useThemeStyles: jest.fn(() => ({})),
|
useThemeStyles: jest.fn(() => ({})),
|
||||||
cn: (...classes: (string | boolean | undefined)[]) =>
|
cn: (...classes: (string | boolean | undefined)[]) =>
|
||||||
classes.filter(Boolean).join(' '),
|
classes.filter(Boolean).join(' '),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -29,10 +24,16 @@ describe('Tag 组件', () => {
|
|||||||
expect(screen.getByText('测试标签')).toBeInTheDocument()
|
expect(screen.getByText('测试标签')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('应该使用默认的 blue 颜色', () => {
|
it('默认样式应该是中性工作台标签', () => {
|
||||||
render(<Tag>测试</Tag>)
|
render(<Tag>测试</Tag>)
|
||||||
const tag = screen.getByText('测试')
|
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 尺寸', () => {
|
it('应该使用默认的 md 尺寸', () => {
|
||||||
@ -49,25 +50,38 @@ describe('Tag 组件', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('颜色变体', () => {
|
describe('颜色变体', () => {
|
||||||
const colors = ['blue', 'green', 'orange', 'purple', 'indigo', 'pink', 'cyan'] as const
|
it('blue、purple、indigo、pink、cyan 应该降级为中性色', () => {
|
||||||
// green 颜色实际使用 emerald 类名
|
const neutralColors = ['blue', 'purple', 'indigo', 'pink', 'cyan'] as const
|
||||||
const colorClassMap: Record<string, string> = {
|
|
||||||
blue: 'blue',
|
|
||||||
green: 'emerald',
|
|
||||||
orange: 'orange',
|
|
||||||
purple: 'purple',
|
|
||||||
indigo: 'indigo',
|
|
||||||
pink: 'pink',
|
|
||||||
cyan: 'cyan',
|
|
||||||
}
|
|
||||||
|
|
||||||
colors.forEach((color) => {
|
neutralColors.forEach((color) => {
|
||||||
it(`应该正确渲染 ${color} 颜色`, () => {
|
const { unmount } = render(<Tag color={color}>测试 {color}</Tag>)
|
||||||
render(<Tag color={color}>测试</Tag>)
|
const tag = screen.getByText(`测试 ${color}`)
|
||||||
const tag = screen.getByText('测试')
|
|
||||||
expect(tag.className).toContain(colorClassMap[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('尺寸变体', () => {
|
describe('尺寸变体', () => {
|
||||||
@ -97,26 +111,32 @@ describe('Tag 组件', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('选中状态', () => {
|
describe('选中状态', () => {
|
||||||
it('未选中时应该有正确的样式类', () => {
|
it('未选中时应该保持浅色背景和可读文字', () => {
|
||||||
render(<Tag selected={false}>测试</Tag>)
|
render(<Tag selected={false}>测试</Tag>)
|
||||||
const tag = screen.getByText('测试')
|
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('选中时应该有正确的样式类', () => {
|
it('选中时应该有明确但不夸张的强调', () => {
|
||||||
render(<Tag selected={true}>测试</Tag>)
|
render(<Tag selected>测试</Tag>)
|
||||||
const tag = screen.getByText('测试')
|
const tag = screen.getByText('测试')
|
||||||
// 选中状态应该有阴影和缩放
|
|
||||||
expect(tag.className).toContain('shadow-md')
|
|
||||||
expect(tag.className).toContain('scale-105')
|
|
||||||
expect(tag.className).toContain('bg-blue-500')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('选中时文字应该是白色', () => {
|
expect(tag.className).toContain('bg-slate-900')
|
||||||
render(<Tag selected={true}>测试</Tag>)
|
expect(tag.className).toContain('dark:bg-slate-100')
|
||||||
const tag = screen.getByText('测试')
|
|
||||||
expect(tag.className).toContain('text-white')
|
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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -125,7 +145,7 @@ describe('Tag 组件', () => {
|
|||||||
const handleClick = jest.fn()
|
const handleClick = jest.fn()
|
||||||
render(<Tag onClick={handleClick}>测试</Tag>)
|
render(<Tag onClick={handleClick}>测试</Tag>)
|
||||||
const tag = screen.getByText('测试')
|
const tag = screen.getByText('测试')
|
||||||
|
|
||||||
fireEvent.click(tag)
|
fireEvent.click(tag)
|
||||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
@ -143,170 +163,32 @@ describe('Tag 组件', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
describe('可读性', () => {
|
||||||
* Property 6: 标签文字可见性(零素材)
|
it('显示数量为 0 时文字应该可见', () => {
|
||||||
* *For any* 素材数量为0的标签按钮,文字应该保持可见
|
|
||||||
* **Feature: bugfix-dec-2025, Property 1: Tag组件零素材状态文字可见性**
|
|
||||||
* **Validates: Requirements 2.1, 2.2, 2.3**
|
|
||||||
*/
|
|
||||||
describe('Property 6: 标签文字可见性(零素材)', () => {
|
|
||||||
it('显示数量为0时文字应该可见', () => {
|
|
||||||
render(<Tag>3DMAX (0)</Tag>)
|
render(<Tag>3DMAX (0)</Tag>)
|
||||||
const tag = screen.getByText('3DMAX (0)')
|
const tag = screen.getByText('3DMAX (0)')
|
||||||
expect(tag).toBeVisible()
|
expect(tag).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('未选中状态下文字颜色应该有足够对比度', () => {
|
it('浅色模式未选中状态应使用深色文字', () => {
|
||||||
render(<Tag selected={false} color="blue">测试 (0)</Tag>)
|
render(<Tag selected={false}>测试 (0)</Tag>)
|
||||||
const tag = screen.getByText('测试 (0)')
|
const tag = screen.getByText('测试 (0)')
|
||||||
// 浅色模式:text-blue-700(深蓝色文字)
|
expect(tag.className).toContain('text-slate-700')
|
||||||
expect(tag.className).toContain('text-blue-700')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('深色模式下文字颜色应该足够亮', () => {
|
it('深色模式未选中状态应使用亮色文字', () => {
|
||||||
render(<Tag selected={false} color="blue">测试 (0)</Tag>)
|
render(<Tag selected={false}>测试 (0)</Tag>)
|
||||||
const tag = screen.getByText('测试 (0)')
|
const tag = screen.getByText('测试 (0)')
|
||||||
// 深色模式:dark:text-blue-100(亮蓝色文字)
|
expect(tag.className).toContain('dark:text-slate-200')
|
||||||
expect(tag.className).toContain('dark:text-blue-100')
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
it('hover 状态不应该依赖缩放或强阴影', () => {
|
||||||
* Property 7: Hover状态文字可见性
|
|
||||||
* *For any* 标签按钮,在hover状态下,文字的可见性不应该降低
|
|
||||||
* **Feature: bugfix-dec-2025, Property 1: Tag组件零素材状态文字可见性**
|
|
||||||
* **Validates: Requirements 2.2**
|
|
||||||
*/
|
|
||||||
describe('Property 7: Hover状态文字可见性', () => {
|
|
||||||
it('hover时背景变化应该增强文字颜色对比度', () => {
|
|
||||||
render(<Tag onClick={() => {}}>测试</Tag>)
|
render(<Tag onClick={() => {}}>测试</Tag>)
|
||||||
const tag = screen.getByText('测试')
|
const tag = screen.getByText('测试')
|
||||||
|
|
||||||
// hover 类改变背景色和文字颜色
|
|
||||||
expect(tag.className).toContain('hover:bg-blue-100')
|
|
||||||
// hover时文字颜色变得更深
|
|
||||||
expect(tag.className).toContain('hover:text-blue-900')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('深色模式hover时文字颜色应该变为白色', () => {
|
expect(tag.className).toContain('hover:bg-slate-50')
|
||||||
render(<Tag onClick={() => {}}>测试</Tag>)
|
expect(tag.className).not.toContain('hover:scale-105')
|
||||||
const tag = screen.getByText('测试')
|
expect(tag.className).not.toContain('hover:shadow')
|
||||||
|
|
||||||
// 深色模式 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')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 统一的动画工具类
|
* 统一的动画工具类
|
||||||
* 苹果风格 + 炫彩元素
|
* 面向后台工作台的低干扰动画与状态类
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 基础动画类
|
// 基础动画类
|
||||||
@ -8,15 +8,15 @@ export const animations = {
|
|||||||
// 淡入淡出
|
// 淡入淡出
|
||||||
fadeIn: 'animate-in fade-in duration-500',
|
fadeIn: 'animate-in fade-in duration-500',
|
||||||
fadeOut: 'animate-out fade-out duration-300',
|
fadeOut: 'animate-out fade-out duration-300',
|
||||||
fadeInUp: 'animate-in fade-in slide-in-from-bottom-4 duration-500',
|
fadeInUp: 'animate-in fade-in duration-300',
|
||||||
fadeInDown: 'animate-in fade-in slide-in-from-top-4 duration-500',
|
fadeInDown: 'animate-in fade-in duration-300',
|
||||||
fadeInLeft: 'animate-in fade-in slide-in-from-left-4 duration-500',
|
fadeInLeft: 'animate-in fade-in duration-300',
|
||||||
fadeInRight: 'animate-in fade-in slide-in-from-right-4 duration-500',
|
fadeInRight: 'animate-in fade-in duration-300',
|
||||||
|
|
||||||
// 缩放
|
// 缩放
|
||||||
scaleIn: 'animate-in zoom-in-95 duration-300',
|
scaleIn: 'animate-in zoom-in-95 duration-300',
|
||||||
scaleOut: 'animate-out zoom-out-95 duration-200',
|
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',
|
spin: 'animate-spin',
|
||||||
@ -28,13 +28,13 @@ export const animations = {
|
|||||||
|
|
||||||
// 弹跳
|
// 弹跳
|
||||||
bounce: 'animate-bounce',
|
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',
|
slideInFromLeft: 'animate-in fade-in duration-300',
|
||||||
slideInFromRight: 'animate-in slide-in-from-right duration-500',
|
slideInFromRight: 'animate-in fade-in duration-300',
|
||||||
slideInFromTop: 'animate-in slide-in-from-top duration-500',
|
slideInFromTop: 'animate-in fade-in duration-300',
|
||||||
slideInFromBottom: 'animate-in slide-in-from-bottom duration-500',
|
slideInFromBottom: 'animate-in fade-in duration-300',
|
||||||
|
|
||||||
// 交错动画延迟
|
// 交错动画延迟
|
||||||
stagger: {
|
stagger: {
|
||||||
@ -48,116 +48,96 @@ export const animations = {
|
|||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 炫彩渐变背景
|
// 背景与边框状态
|
||||||
export const gradientBackgrounds = {
|
export const gradientBackgrounds = {
|
||||||
// 苹果风格渐变(统一 Hero 卡片背景颜色)
|
appleLight: 'bg-slate-50 dark:bg-slate-900',
|
||||||
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-slate-900 dark:bg-slate-950',
|
||||||
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',
|
|
||||||
|
|
||||||
// Hero 卡片背景渐变(CSS linear-gradient 格式,用于内联样式)
|
// 兼容旧调用的普通后台背景(CSS linear-gradient 格式,用于内联样式)
|
||||||
heroBase: {
|
heroBase: {
|
||||||
light: 'linear-gradient(135deg, #f8fafc 0%, #eff6ff 50%, #eef2ff 100%)',
|
light: '#f8fafc',
|
||||||
dark: 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #1e1b4b 100%)',
|
dark: '#0f172a',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 炫彩渐变
|
rainbow: 'bg-slate-100 dark:bg-slate-800',
|
||||||
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-slate-50 dark:bg-slate-900',
|
||||||
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-slate-100 dark:bg-slate-800',
|
||||||
rainbowVibrant: 'bg-gradient-to-br from-pink-500/20 via-purple-500/20 via-indigo-500/20 to-blue-500/20',
|
|
||||||
|
neon: 'bg-slate-100 dark:bg-slate-800',
|
||||||
// 霓虹效果
|
neonSubtle: 'bg-slate-50 dark:bg-slate-900',
|
||||||
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',
|
metallic: 'bg-slate-100 dark:bg-slate-800',
|
||||||
|
|
||||||
// 金属质感
|
glass: 'bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700',
|
||||||
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',
|
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',
|
||||||
glass: 'bg-white/10 dark:bg-white/5 backdrop-blur-md border border-white/20',
|
glassNavDark: 'bg-slate-900 border-b border-slate-700',
|
||||||
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',
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 炫彩光晕效果
|
// 阴影效果
|
||||||
export const glowEffects = {
|
export const glowEffects = {
|
||||||
// 基础光晕
|
glow: 'shadow-sm',
|
||||||
glow: 'shadow-lg shadow-indigo-500/50',
|
glowStrong: 'shadow-md',
|
||||||
glowStrong: 'shadow-xl shadow-indigo-500/70',
|
glowRainbow: 'shadow-sm',
|
||||||
glowRainbow: 'shadow-xl shadow-[0_0_30px_rgba(139,92,246,0.5)]',
|
|
||||||
|
glowPink: 'shadow-sm',
|
||||||
// 彩色光晕
|
glowPurple: 'shadow-sm',
|
||||||
glowPink: 'shadow-lg shadow-pink-500/50',
|
glowBlue: 'shadow-sm',
|
||||||
glowPurple: 'shadow-lg shadow-purple-500/50',
|
glowCyan: 'shadow-sm',
|
||||||
glowBlue: 'shadow-lg shadow-blue-500/50',
|
|
||||||
glowCyan: 'shadow-lg shadow-cyan-500/50',
|
glowHover: 'hover:shadow-sm transition-shadow duration-150',
|
||||||
|
glowHoverRainbow: 'hover:shadow-sm transition-shadow duration-150',
|
||||||
// 悬停光晕
|
|
||||||
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',
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 边框动画
|
// 边框动画
|
||||||
export const borderAnimations = {
|
export const borderAnimations = {
|
||||||
// 渐变边框
|
gradientBorder: 'border border-slate-200 dark:border-slate-700',
|
||||||
gradientBorder: 'border-2 border-transparent bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 bg-clip-border',
|
|
||||||
|
|
||||||
// 闪烁边框
|
shimmer: 'relative overflow-hidden',
|
||||||
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',
|
|
||||||
|
|
||||||
// 霓虹边框
|
neon: 'border border-slate-300 dark:border-slate-600',
|
||||||
neon: 'border-2 border-cyan-400 shadow-[0_0_10px_rgba(34,211,238,0.5)]',
|
neonHover: 'hover:border-slate-400 dark:hover:border-slate-500 transition-colors duration-150',
|
||||||
neonHover: 'hover:border-cyan-300 hover:shadow-[0_0_20px_rgba(34,211,238,0.7)] transition-all duration-300',
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 文本动画
|
// 文本动画
|
||||||
export const textAnimations = {
|
export const textAnimations = {
|
||||||
// 渐变文本
|
gradientText: 'text-slate-900 dark:text-slate-100',
|
||||||
gradientText: 'bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent',
|
gradientTextRainbow: 'text-slate-900 dark:text-slate-100',
|
||||||
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',
|
|
||||||
|
|
||||||
// 闪烁文本
|
shimmerText: 'text-slate-500 dark:text-slate-400',
|
||||||
shimmerText: 'animate-[shimmer_2s_infinite] bg-gradient-to-r from-gray-400 via-white to-gray-400 bg-clip-text text-transparent',
|
|
||||||
|
|
||||||
// 发光文本
|
glowText: 'text-slate-700 dark:text-slate-200',
|
||||||
glowText: 'text-indigo-400 drop-shadow-[0_0_8px_rgba(99,102,241,0.8)]',
|
glowTextRainbow: 'text-slate-700 dark:text-slate-200',
|
||||||
glowTextRainbow: 'drop-shadow-[0_0_10px_rgba(139,92,246,0.6)]',
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 卡片动画
|
// 卡片动画
|
||||||
export const cardAnimations = {
|
export const cardAnimations = {
|
||||||
// 悬停提升
|
// 悬停提升
|
||||||
lift: 'transition-all duration-300 hover:-translate-y-1 hover:shadow-xl',
|
lift: 'transition-colors duration-150',
|
||||||
liftStrong: 'transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl',
|
liftStrong: 'transition-colors duration-150',
|
||||||
|
|
||||||
// 3D 倾斜效果
|
tilt: 'transition-colors duration-150',
|
||||||
tilt: 'transition-all duration-300 hover:rotate-1 hover:scale-105',
|
|
||||||
|
|
||||||
// 闪烁效果
|
// 闪烁效果
|
||||||
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
|
} as const
|
||||||
|
|
||||||
// 按钮动画
|
// 按钮动画
|
||||||
export const buttonAnimations = {
|
export const buttonAnimations = {
|
||||||
// 基础按钮动画
|
// 基础按钮动画
|
||||||
base: 'transition-all duration-200 active:scale-95',
|
base: 'transition-colors duration-150',
|
||||||
|
|
||||||
// 炫彩按钮
|
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',
|
||||||
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',
|
|
||||||
|
|
||||||
// 霓虹按钮
|
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',
|
||||||
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',
|
|
||||||
|
|
||||||
// 玻璃态按钮
|
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',
|
||||||
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',
|
|
||||||
|
|
||||||
// 脉冲按钮
|
pulse: 'transition-colors duration-150',
|
||||||
pulse: 'animate-pulse hover:animate-none',
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 组合动画工具函数
|
// 组合动画工具函数
|
||||||
@ -170,7 +150,7 @@ export function conditionalAnimation(condition: boolean, animation: string, fall
|
|||||||
return condition ? animation : fallback
|
return condition ? animation : fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// 苹果风格滚动动画
|
// 滚动动画
|
||||||
export const scrollAnimations = {
|
export const scrollAnimations = {
|
||||||
// 平滑滚动
|
// 平滑滚动
|
||||||
smoothScroll: 'scroll-smooth',
|
smoothScroll: 'scroll-smooth',
|
||||||
@ -184,16 +164,13 @@ export const scrollAnimations = {
|
|||||||
// 滚动时位移
|
// 滚动时位移
|
||||||
translateOnScroll: 'transition-transform duration-300 ease-out',
|
translateOnScroll: 'transition-transform duration-300 ease-out',
|
||||||
|
|
||||||
// 苹果风格的弹性动画
|
spring: 'transition-colors duration-150 ease-out',
|
||||||
spring: 'transition-all duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)]',
|
springSmooth: 'transition-colors duration-150 ease-out',
|
||||||
springSmooth: 'transition-all duration-400 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]',
|
|
||||||
|
|
||||||
// 滚动时隐藏/显示
|
// 滚动时隐藏/显示
|
||||||
hideOnScroll: 'transition-all duration-300 ease-out opacity-0 -translate-y-2',
|
hideOnScroll: 'transition-all duration-300 ease-out opacity-0 -translate-y-2',
|
||||||
showOnScroll: 'transition-all duration-300 ease-out opacity-100 translate-y-0',
|
showOnScroll: 'transition-all duration-300 ease-out opacity-100 translate-y-0',
|
||||||
|
|
||||||
// 苹果风格的弹性滚动(用于 scrollIntoView)
|
|
||||||
// 使用 CSS scroll-behavior: smooth 配合 JavaScript 实现弹性效果
|
|
||||||
elasticScroll: 'scroll-smooth',
|
elasticScroll: 'scroll-smooth',
|
||||||
} as const
|
} 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
|
return Math.pow(2, -6 * t) * Math.sin((t * 4 - 0.3) * c4) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 苹果风格的缓入缓出函数(ease-in-out-cubic)
|
|
||||||
function easeInOutCubic(t: number): number {
|
function easeInOutCubic(t: number): number {
|
||||||
return t < 0.5
|
return t < 0.5
|
||||||
? 4 * t * t * t
|
? 4 * t * t * t
|
||||||
: 1 - Math.pow(-2 * t + 2, 3) / 2
|
: 1 - Math.pow(-2 * t + 2, 3) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// 苹果风格的弹性滚动函数(用于 JavaScript 调用)
|
|
||||||
export function scrollToElementWithSpring(
|
export function scrollToElementWithSpring(
|
||||||
element: HTMLElement | null,
|
element: HTMLElement | null,
|
||||||
container: HTMLElement | null = null,
|
container: HTMLElement | null = null,
|
||||||
|
|||||||
@ -1,82 +1,82 @@
|
|||||||
/**
|
/**
|
||||||
* UI 样式基础配置
|
* UI 样式基础配置
|
||||||
* 包含所有非颜色相关的样式:布局、大小、动画、间距、圆角等
|
* 包含所有非颜色相关的样式:布局、大小、间距、圆角等。
|
||||||
* 这些样式在浅色和深色模式下完全相同
|
* 默认风格服务后台工作台,不做展示页式大圆角、毛玻璃、浮动光效。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 基础结构样式(不包含颜色)
|
// 基础结构样式(不包含颜色)
|
||||||
export const baseCardStyles = {
|
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',
|
||||||
|
|
||||||
// 炫彩卡片结构(带渐变背景)
|
// 兼容旧 variant,实际降级为普通卡片
|
||||||
rainbow: 'rounded-2xl border backdrop-blur-sm shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-300 p-6',
|
rainbow: 'rounded-lg border p-5 transition-colors duration-200',
|
||||||
|
|
||||||
// 玻璃态卡片结构
|
// 兼容旧 variant,实际降级为普通卡片
|
||||||
glass: 'rounded-2xl backdrop-blur-md border shadow-lg hover:shadow-xl transition-all duration-300 p-6',
|
glass: 'rounded-lg border p-5 transition-colors duration-200',
|
||||||
|
|
||||||
// 霓虹卡片结构
|
// 兼容旧 variant,实际降级为普通卡片
|
||||||
neon: 'rounded-2xl border-2 backdrop-blur-sm shadow-lg hover:shadow-xl transition-all duration-300 p-6',
|
neon: 'rounded-lg border p-5 transition-colors duration-200',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const baseButtonStyles = {
|
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 边框颜色在主题中定义)
|
// 次要按钮结构(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',
|
||||||
|
|
||||||
// 炫彩渐变按钮结构(标准尺寸)
|
// 兼容旧 variant,实际降级为普通主按钮
|
||||||
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',
|
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',
|
||||||
|
|
||||||
// 炫彩渐变按钮结构(紧凑尺寸,用于角标)
|
// 兼容旧 variant,实际降级为普通紧凑按钮
|
||||||
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',
|
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',
|
||||||
|
|
||||||
// 霓虹按钮结构
|
// 兼容旧 variant,实际降级为普通主按钮
|
||||||
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',
|
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',
|
||||||
|
|
||||||
// 玻璃态按钮结构
|
// 兼容旧 variant,实际降级为普通次按钮
|
||||||
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',
|
glass: 'px-4 py-2 rounded-lg border font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const baseInputStyles = {
|
export const baseInputStyles = {
|
||||||
// 基础输入框结构(focus 颜色在主题中定义)
|
// 基础输入框结构(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 颜色在主题中定义)
|
// 错误输入框结构(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
|
} as const
|
||||||
|
|
||||||
export const baseNavItemStyles = {
|
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',
|
||||||
|
|
||||||
// 炫彩导航项结构
|
// 兼容旧 variant,实际降级为普通导航项
|
||||||
rainbow: 'px-4 py-3 rounded-xl border backdrop-blur-sm hover:shadow-md transition-all duration-300',
|
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',
|
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 = {
|
export const baseButtonModifiers = {
|
||||||
compact: 'px-3 py-1.5 text-sm',
|
compact: 'px-3 py-1.5 text-sm',
|
||||||
compactXs: 'px-2 py-1 text-xs',
|
compactXs: 'px-2 py-1 text-xs',
|
||||||
icon: 'p-2 rounded-xl',
|
icon: 'p-2 rounded-lg',
|
||||||
block: 'w-full justify-center',
|
block: 'w-full justify-center',
|
||||||
pill: 'rounded-full',
|
pill: 'rounded-full',
|
||||||
} as const
|
} as const
|
||||||
@ -115,13 +115,13 @@ export const baseSpacingStyles = {
|
|||||||
// Modal 基础样式(不包含颜色)
|
// Modal 基础样式(不包含颜色)
|
||||||
export const baseModalStyles = {
|
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: {
|
sizes: {
|
||||||
@ -153,31 +153,31 @@ export const baseModalStyles = {
|
|||||||
// Section 显示动画样式(来自外观设置)
|
// Section 显示动画样式(来自外观设置)
|
||||||
export const baseSectionRevealStyles = {
|
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
|
} as const
|
||||||
|
|
||||||
// Hero 区域样式(来自外观设置)
|
// Hero 区域样式(保留兼容,实际按普通页面区块处理)
|
||||||
export const baseHeroStyles = {
|
export const baseHeroStyles = {
|
||||||
// Hero 卡片基础结构
|
// 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 标题区域
|
// Hero 标题区域
|
||||||
header: 'relative z-10 flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between',
|
header: 'relative z-10 flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between',
|
||||||
// Hero 内容区域
|
// Hero 内容区域
|
||||||
content: 'space-y-3 max-w-2xl',
|
content: 'space-y-3 max-w-2xl',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 浮动光球样式(来自外观设置)
|
// 浮动光球样式(保留兼容,默认隐藏)
|
||||||
export const baseFloatingOrbStyles = {
|
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
|
} as const
|
||||||
|
|
||||||
// 开关切换器样式(来自外观设置)
|
// 开关切换器样式(来自外观设置)
|
||||||
|
|||||||
@ -1,21 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* 统一的 UI 样式工具类
|
* 统一的 UI 样式工具类
|
||||||
* 苹果风格 + 炫彩元素
|
* 以私有化工作台为默认风格,保留浅色/深色两套配置
|
||||||
*
|
*
|
||||||
* ⚠️ 注意:此文件已重构为支持两套独立配置(浅色/深色)
|
|
||||||
*
|
|
||||||
* 推荐使用方式:
|
* 推荐使用方式:
|
||||||
* 1. 在新组件中使用 `useThemeStyles()` Hook(推荐)
|
* 1. 在新组件中使用 `useThemeStyles()` Hook(推荐)
|
||||||
* 2. 在服务端组件或需要向后兼容的地方,继续使用此文件的导出
|
* 2. 在服务端组件或需要向后兼容的地方,继续使用此文件的导出
|
||||||
*
|
|
||||||
* 迁移步骤:
|
|
||||||
* - 将组件改为客户端组件('use client')
|
|
||||||
* - 使用 `const styles = useThemeStyles()` 替代直接导入
|
|
||||||
* - 使用 `styles.cardStyles.base` 替代 `cardStyles.base`
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { gradientBackgrounds, glowEffects, cardAnimations } from './ui-animations'
|
|
||||||
|
|
||||||
// 导出主题 Hook(推荐使用)
|
// 导出主题 Hook(推荐使用)
|
||||||
export { useThemeStyles, useTheme } from './hooks/useThemeStyles'
|
export { useThemeStyles, useTheme } from './hooks/useThemeStyles'
|
||||||
|
|
||||||
@ -26,52 +17,52 @@ export { lightStyles, darkStyles } from './ui-styles-theme-exports'
|
|||||||
// ⚠️ 这些样式会在深色模式下正确工作,但代码较长
|
// ⚠️ 这些样式会在深色模式下正确工作,但代码较长
|
||||||
// 建议逐步迁移到 useThemeStyles Hook
|
// 建议逐步迁移到 useThemeStyles Hook
|
||||||
export const cardStyles = {
|
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
|
} as const
|
||||||
|
|
||||||
export const buttonStyles = {
|
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
|
} as const
|
||||||
|
|
||||||
export const buttonModifiers = {
|
export const buttonModifiers = {
|
||||||
@ -95,17 +86,17 @@ export const inputModifiers = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const navItemStyles = {
|
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',
|
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 = {
|
export const themeColors = {
|
||||||
// 卡片颜色
|
// 卡片颜色
|
||||||
card: {
|
card: {
|
||||||
border: 'border-slate-700/70',
|
border: 'border-slate-700',
|
||||||
bg: 'bg-slate-800',
|
bg: 'bg-slate-800',
|
||||||
shadow: 'shadow-black/50',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-indigo-500/30',
|
shadowHover: 'hover:border-slate-600',
|
||||||
shadowLarge: 'shadow-black/60',
|
shadowLarge: 'shadow-none',
|
||||||
shadowLargeHover: 'hover:shadow-indigo-500/35',
|
shadowLargeHover: 'hover:border-slate-600',
|
||||||
shadowRainbow: 'shadow-purple-500/30',
|
shadowRainbow: 'shadow-none',
|
||||||
shadowRainbowHover: 'hover:shadow-purple-500/40',
|
shadowRainbowHover: 'hover:border-slate-600',
|
||||||
shadowGlass: 'shadow-black/50',
|
shadowGlass: 'shadow-none',
|
||||||
shadowGlassHover: 'hover:shadow-indigo-500/30',
|
shadowGlassHover: 'hover:border-slate-600',
|
||||||
shadowNeon: 'shadow-cyan-500/40',
|
shadowNeon: 'shadow-none',
|
||||||
shadowNeonHover: 'hover:shadow-cyan-500/60',
|
shadowNeonHover: 'hover:border-slate-600',
|
||||||
borderNeon: 'border-cyan-400/70',
|
borderNeon: 'border-slate-700',
|
||||||
bgRainbow: 'bg-gradient-to-br from-pink-900/20 via-purple-900/20 via-indigo-900/20 to-blue-900/20',
|
bgRainbow: 'bg-slate-800',
|
||||||
bgGlass: 'bg-white/5',
|
bgGlass: 'bg-slate-800',
|
||||||
borderGlass: 'border-white/20',
|
borderGlass: 'border-slate-700',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 按钮颜色
|
// 按钮颜色
|
||||||
@ -29,8 +29,8 @@ export const themeColors = {
|
|||||||
primary: {
|
primary: {
|
||||||
bg: 'bg-indigo-500',
|
bg: 'bg-indigo-500',
|
||||||
hover: 'hover:bg-indigo-600',
|
hover: 'hover:bg-indigo-600',
|
||||||
shadow: 'shadow-indigo-500/50',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-indigo-500/60',
|
shadowHover: '',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
border: 'border-slate-600',
|
border: 'border-slate-600',
|
||||||
@ -45,19 +45,19 @@ export const themeColors = {
|
|||||||
danger: {
|
danger: {
|
||||||
bg: 'bg-red-500', // 与浅色模式保持一致
|
bg: 'bg-red-500', // 与浅色模式保持一致
|
||||||
hover: 'hover:bg-red-600',
|
hover: 'hover:bg-red-600',
|
||||||
shadow: 'shadow-red-500/40',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-red-500/50',
|
shadowHover: '',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
bg: 'bg-emerald-500',
|
bg: 'bg-emerald-500',
|
||||||
hover: 'hover:bg-emerald-600',
|
hover: 'hover:bg-emerald-600',
|
||||||
shadow: 'shadow-emerald-500/50',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-emerald-500/60',
|
shadowHover: '',
|
||||||
},
|
},
|
||||||
glass: {
|
glass: {
|
||||||
bg: 'bg-white/5',
|
bg: 'bg-slate-800',
|
||||||
hover: 'hover:bg-white/10',
|
hover: 'hover:bg-slate-700',
|
||||||
border: 'border-white/20',
|
border: 'border-slate-600',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -77,16 +77,16 @@ export const themeColors = {
|
|||||||
|
|
||||||
// 导航项颜色
|
// 导航项颜色
|
||||||
navItem: {
|
navItem: {
|
||||||
border: 'border-slate-700/70',
|
border: 'border-slate-700',
|
||||||
bg: 'bg-slate-800',
|
bg: 'bg-slate-800',
|
||||||
text: 'text-slate-200',
|
text: 'text-slate-200',
|
||||||
hoverBg: 'hover:bg-indigo-900/30',
|
hoverBg: 'hover:bg-slate-700',
|
||||||
hoverShadow: 'hover:shadow-indigo-500/30',
|
hoverShadow: 'hover:border-slate-600',
|
||||||
activeBg: 'bg-indigo-900/40',
|
activeBg: 'bg-slate-700',
|
||||||
activeText: 'text-indigo-200',
|
activeText: 'text-white',
|
||||||
activeShadow: 'shadow-indigo-500/40',
|
activeShadow: 'border-slate-100',
|
||||||
rainbowBg: 'bg-gradient-to-r from-pink-900/20 via-purple-900/20 to-indigo-900/20',
|
rainbowBg: 'bg-slate-800',
|
||||||
rainbowShadow: 'hover:shadow-purple-500/30',
|
rainbowShadow: 'hover:border-slate-600',
|
||||||
tabText: 'text-slate-400',
|
tabText: 'text-slate-400',
|
||||||
tabTextHover: 'hover:text-white',
|
tabTextHover: 'hover:text-white',
|
||||||
tabTextActive: 'text-white',
|
tabTextActive: 'text-white',
|
||||||
@ -112,69 +112,62 @@ export const themeColors = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// 低饱和度炫彩元素(用于标签、强调色等)
|
// 后台强调色(普通信息默认用中性色,状态再使用语义色)
|
||||||
accent: {
|
accent: {
|
||||||
// 粉紫色系(低饱和度)
|
|
||||||
pink: {
|
pink: {
|
||||||
bg: 'bg-pink-900/20',
|
bg: 'bg-slate-800',
|
||||||
text: 'text-pink-300',
|
text: 'text-slate-200',
|
||||||
border: 'border-pink-700/50',
|
border: 'border-slate-700',
|
||||||
hover: 'hover:bg-pink-900/30',
|
hover: 'hover:bg-slate-700',
|
||||||
},
|
},
|
||||||
// 紫色系(低饱和度)
|
|
||||||
purple: {
|
purple: {
|
||||||
bg: 'bg-purple-900/20',
|
bg: 'bg-slate-800',
|
||||||
text: 'text-purple-300',
|
text: 'text-slate-200',
|
||||||
border: 'border-purple-700/50',
|
border: 'border-slate-700',
|
||||||
hover: 'hover:bg-purple-900/30',
|
hover: 'hover:bg-slate-700',
|
||||||
},
|
},
|
||||||
// 蓝色系(低饱和度)
|
|
||||||
blue: {
|
blue: {
|
||||||
bg: 'bg-blue-900/20',
|
bg: 'bg-slate-800',
|
||||||
text: 'text-blue-300',
|
text: 'text-slate-200',
|
||||||
border: 'border-blue-700/50',
|
border: 'border-slate-700',
|
||||||
hover: 'hover:bg-blue-900/30',
|
hover: 'hover:bg-slate-700',
|
||||||
},
|
},
|
||||||
// 青色系(低饱和度)
|
|
||||||
cyan: {
|
cyan: {
|
||||||
bg: 'bg-cyan-900/20',
|
bg: 'bg-slate-800',
|
||||||
text: 'text-cyan-300',
|
text: 'text-slate-200',
|
||||||
border: 'border-cyan-700/50',
|
border: 'border-slate-700',
|
||||||
hover: 'hover:bg-cyan-900/30',
|
hover: 'hover:bg-slate-700',
|
||||||
},
|
},
|
||||||
// 绿色系(低饱和度)
|
|
||||||
emerald: {
|
emerald: {
|
||||||
bg: 'bg-emerald-900/20',
|
bg: 'bg-emerald-900/20',
|
||||||
text: 'text-emerald-300',
|
text: 'text-emerald-300',
|
||||||
border: 'border-emerald-700/50',
|
border: 'border-emerald-700/50',
|
||||||
hover: 'hover:bg-emerald-900/30',
|
hover: 'hover:bg-emerald-900/30',
|
||||||
},
|
},
|
||||||
// 橙色系(低饱和度)
|
|
||||||
orange: {
|
orange: {
|
||||||
bg: 'bg-orange-900/20',
|
bg: 'bg-amber-900/20',
|
||||||
text: 'text-orange-300',
|
text: 'text-amber-300',
|
||||||
border: 'border-orange-700/50',
|
border: 'border-amber-700/50',
|
||||||
hover: 'hover:bg-orange-900/30',
|
hover: 'hover:bg-amber-900/30',
|
||||||
},
|
},
|
||||||
// 渐变炫彩(低饱和度)
|
|
||||||
rainbow: {
|
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',
|
text: 'text-slate-200',
|
||||||
border: 'border-purple-700/50',
|
border: 'border-slate-700',
|
||||||
hover: 'hover:from-pink-900/30 hover:via-purple-900/30 hover:via-indigo-900/30 hover:to-blue-900/30',
|
hover: 'hover:bg-slate-700',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Modal 颜色
|
// Modal 颜色
|
||||||
modal: {
|
modal: {
|
||||||
overlay: 'bg-slate-950/70 backdrop-blur-xl',
|
overlay: 'bg-slate-950/70',
|
||||||
bg: 'bg-slate-900/80',
|
bg: 'bg-slate-900',
|
||||||
border: 'border-slate-800/60',
|
border: 'border-slate-800',
|
||||||
shadow: 'shadow-[0_45px_130px_rgba(0,0,0,0.75)] shadow-black/50',
|
shadow: 'shadow-lg shadow-black/30',
|
||||||
headerBorder: 'border-slate-800/60',
|
headerBorder: 'border-slate-800',
|
||||||
title: 'text-white',
|
title: 'text-white',
|
||||||
body: 'text-slate-200',
|
body: 'text-slate-200',
|
||||||
footerBorder: 'border-slate-800/60',
|
footerBorder: 'border-slate-800',
|
||||||
closeButton: 'text-slate-300 hover:text-white hover:bg-white/10',
|
closeButton: 'text-slate-300 hover:text-white hover:bg-white/10',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -6,22 +6,22 @@
|
|||||||
export const themeColors = {
|
export const themeColors = {
|
||||||
// 卡片颜色
|
// 卡片颜色
|
||||||
card: {
|
card: {
|
||||||
border: 'border-slate-200/70',
|
border: 'border-slate-200',
|
||||||
bg: 'bg-white',
|
bg: 'bg-white',
|
||||||
shadow: 'shadow-slate-200/50',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-indigo-500/20',
|
shadowHover: 'hover:border-slate-300',
|
||||||
shadowLarge: 'shadow-slate-200/60',
|
shadowLarge: 'shadow-none',
|
||||||
shadowLargeHover: 'hover:shadow-indigo-500/25',
|
shadowLargeHover: 'hover:border-slate-300',
|
||||||
shadowRainbow: 'shadow-purple-500/20',
|
shadowRainbow: 'shadow-none',
|
||||||
shadowRainbowHover: 'hover:shadow-purple-500/30',
|
shadowRainbowHover: 'hover:border-slate-300',
|
||||||
shadowGlass: 'shadow-slate-200/50',
|
shadowGlass: 'shadow-none',
|
||||||
shadowGlassHover: 'hover:shadow-indigo-500/20',
|
shadowGlassHover: 'hover:border-slate-300',
|
||||||
shadowNeon: 'shadow-cyan-500/30',
|
shadowNeon: 'shadow-none',
|
||||||
shadowNeonHover: 'hover:shadow-cyan-500/50',
|
shadowNeonHover: 'hover:border-slate-300',
|
||||||
borderNeon: 'border-cyan-400/50',
|
borderNeon: 'border-slate-200',
|
||||||
bgRainbow: 'bg-gradient-to-br from-pink-100 via-purple-100 via-indigo-100 to-blue-100',
|
bgRainbow: 'bg-white',
|
||||||
bgGlass: 'bg-white/10',
|
bgGlass: 'bg-white',
|
||||||
borderGlass: 'border-white/20',
|
borderGlass: 'border-slate-200',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 按钮颜色
|
// 按钮颜色
|
||||||
@ -29,8 +29,8 @@ export const themeColors = {
|
|||||||
primary: {
|
primary: {
|
||||||
bg: 'bg-indigo-600',
|
bg: 'bg-indigo-600',
|
||||||
hover: 'hover:bg-indigo-700',
|
hover: 'hover:bg-indigo-700',
|
||||||
shadow: 'shadow-indigo-500/40',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-indigo-500/50',
|
shadowHover: '',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
border: 'border-slate-300',
|
border: 'border-slate-300',
|
||||||
@ -45,19 +45,19 @@ export const themeColors = {
|
|||||||
danger: {
|
danger: {
|
||||||
bg: 'bg-red-500', // 使用适中的红色,确保可见性
|
bg: 'bg-red-500', // 使用适中的红色,确保可见性
|
||||||
hover: 'hover:bg-red-600',
|
hover: 'hover:bg-red-600',
|
||||||
shadow: 'shadow-red-500/50',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-red-500/60',
|
shadowHover: '',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
bg: 'bg-emerald-600',
|
bg: 'bg-emerald-600',
|
||||||
hover: 'hover:bg-emerald-700',
|
hover: 'hover:bg-emerald-700',
|
||||||
shadow: 'shadow-emerald-500/40',
|
shadow: 'shadow-none',
|
||||||
shadowHover: 'hover:shadow-emerald-500/50',
|
shadowHover: '',
|
||||||
},
|
},
|
||||||
glass: {
|
glass: {
|
||||||
bg: 'bg-white/10',
|
bg: 'bg-white',
|
||||||
hover: 'hover:bg-white/20',
|
hover: 'hover:bg-slate-50',
|
||||||
border: 'border-white/20',
|
border: 'border-slate-300',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -77,16 +77,16 @@ export const themeColors = {
|
|||||||
|
|
||||||
// 导航项颜色
|
// 导航项颜色
|
||||||
navItem: {
|
navItem: {
|
||||||
border: 'border-slate-200/70',
|
border: 'border-slate-200',
|
||||||
bg: 'bg-white',
|
bg: 'bg-white',
|
||||||
text: 'text-slate-700',
|
text: 'text-slate-700',
|
||||||
hoverBg: 'hover:bg-indigo-50',
|
hoverBg: 'hover:bg-slate-50',
|
||||||
hoverShadow: 'hover:shadow-indigo-500/20',
|
hoverShadow: 'hover:border-slate-300',
|
||||||
activeBg: 'bg-indigo-50',
|
activeBg: 'bg-slate-100',
|
||||||
activeText: 'text-indigo-700',
|
activeText: 'text-slate-900',
|
||||||
activeShadow: 'shadow-indigo-500/30',
|
activeShadow: 'border-slate-900',
|
||||||
rainbowBg: 'bg-gradient-to-r from-pink-50/50 via-purple-50/50 to-indigo-50/50',
|
rainbowBg: 'bg-white',
|
||||||
rainbowShadow: 'hover:shadow-purple-500/20',
|
rainbowShadow: 'hover:border-slate-300',
|
||||||
tabText: 'text-slate-600',
|
tabText: 'text-slate-600',
|
||||||
tabTextHover: 'hover:text-slate-900',
|
tabTextHover: 'hover:text-slate-900',
|
||||||
tabTextActive: 'text-slate-900',
|
tabTextActive: 'text-slate-900',
|
||||||
@ -112,70 +112,63 @@ export const themeColors = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// 低饱和度炫彩元素(用于标签、强调色等)
|
// 后台强调色(普通信息默认用中性色,状态再使用语义色)
|
||||||
accent: {
|
accent: {
|
||||||
// 粉紫色系(低饱和度)
|
|
||||||
pink: {
|
pink: {
|
||||||
bg: 'bg-pink-50',
|
bg: 'bg-slate-50',
|
||||||
text: 'text-pink-700',
|
text: 'text-slate-700',
|
||||||
border: 'border-pink-200',
|
border: 'border-slate-200',
|
||||||
hover: 'hover:bg-pink-100',
|
hover: 'hover:bg-slate-100',
|
||||||
},
|
},
|
||||||
// 紫色系(低饱和度)
|
|
||||||
purple: {
|
purple: {
|
||||||
bg: 'bg-purple-50',
|
bg: 'bg-slate-50',
|
||||||
text: 'text-purple-700',
|
text: 'text-slate-700',
|
||||||
border: 'border-purple-200',
|
border: 'border-slate-200',
|
||||||
hover: 'hover:bg-purple-100',
|
hover: 'hover:bg-slate-100',
|
||||||
},
|
},
|
||||||
// 蓝色系(低饱和度)
|
|
||||||
blue: {
|
blue: {
|
||||||
bg: 'bg-blue-50',
|
bg: 'bg-slate-50',
|
||||||
text: 'text-blue-700',
|
text: 'text-slate-700',
|
||||||
border: 'border-blue-200',
|
border: 'border-slate-200',
|
||||||
hover: 'hover:bg-blue-100',
|
hover: 'hover:bg-slate-100',
|
||||||
},
|
},
|
||||||
// 青色系(低饱和度)
|
|
||||||
cyan: {
|
cyan: {
|
||||||
bg: 'bg-cyan-50',
|
bg: 'bg-slate-50',
|
||||||
text: 'text-cyan-700',
|
text: 'text-slate-700',
|
||||||
border: 'border-cyan-200',
|
border: 'border-slate-200',
|
||||||
hover: 'hover:bg-cyan-100',
|
hover: 'hover:bg-slate-100',
|
||||||
},
|
},
|
||||||
// 绿色系(低饱和度)
|
|
||||||
emerald: {
|
emerald: {
|
||||||
bg: 'bg-emerald-50',
|
bg: 'bg-emerald-50',
|
||||||
text: 'text-emerald-700',
|
text: 'text-emerald-700',
|
||||||
border: 'border-emerald-200',
|
border: 'border-emerald-200',
|
||||||
hover: 'hover:bg-emerald-100',
|
hover: 'hover:bg-emerald-100',
|
||||||
},
|
},
|
||||||
// 橙色系(低饱和度)
|
|
||||||
orange: {
|
orange: {
|
||||||
bg: 'bg-orange-50',
|
bg: 'bg-amber-50',
|
||||||
text: 'text-orange-700',
|
text: 'text-amber-800',
|
||||||
border: 'border-orange-200',
|
border: 'border-amber-200',
|
||||||
hover: 'hover:bg-orange-100',
|
hover: 'hover:bg-amber-100',
|
||||||
},
|
},
|
||||||
// 渐变炫彩(低饱和度)
|
|
||||||
rainbow: {
|
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',
|
text: 'text-slate-700',
|
||||||
border: 'border-purple-200',
|
border: 'border-slate-200',
|
||||||
hover: 'hover:from-pink-100 hover:via-purple-100 hover:via-indigo-100 hover:to-blue-100',
|
hover: 'hover:bg-slate-100',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Modal 颜色
|
// Modal 颜色
|
||||||
modal: {
|
modal: {
|
||||||
overlay: 'bg-slate-900/35 backdrop-blur-xl',
|
overlay: 'bg-slate-900/45',
|
||||||
bg: 'bg-white/80',
|
bg: 'bg-white',
|
||||||
border: 'border-white/30',
|
border: 'border-slate-200',
|
||||||
shadow: 'shadow-[0_35px_110px_rgba(15,23,42,0.25)] shadow-slate-900/20',
|
shadow: 'shadow-lg shadow-slate-900/10',
|
||||||
headerBorder: 'border-white/30',
|
headerBorder: 'border-slate-200',
|
||||||
title: 'text-slate-900',
|
title: 'text-slate-900',
|
||||||
body: 'text-slate-600',
|
body: 'text-slate-600',
|
||||||
footerBorder: 'border-white/30',
|
footerBorder: 'border-slate-200',
|
||||||
closeButton: 'text-slate-500 hover:text-slate-700 hover:bg-white/40',
|
closeButton: 'text-slate-500 hover:text-slate-700 hover:bg-slate-100',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 开关切换器颜色
|
// 开关切换器颜色
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user