feat: 重构用户角色系统为管理员标识

- 将用户角色字段从role改为is_admin布尔值
- 更新相关API权限检查逻辑
- 修改数据库schema和迁移文件
- 调整前端用户显示逻辑
- 添加API响应工具函数
- 优化权限检查中间件
- 重构英雄组件为原子组件
This commit is contained in:
张翔
2026-03-12 20:45:43 +08:00
parent b207bfa7af
commit f357330ba8
22 changed files with 1078 additions and 552 deletions
+20
View File
@@ -0,0 +1,20 @@
export { DataParticleFlow } from './data-particle-flow';
export { SubtleDots } from './subtle-dots';
export { SubtleParticles } from './subtle-particles';
export { ParticleGalaxy } from './particle-galaxy';
export { MouseInteractiveParticles } from './mouse-interactive-particles';
export { GradientFlow } from './gradient-flow';
export { GradientAnimation } from './gradient-animation';
export { GradientOrbs } from './gradient-orbs';
export { GradientGrid } from './gradient-grid';
export { TechGridFlow } from './tech-grid-flow';
export { MeshGradient } from './mesh-gradient';
export { InkTechFusion } from './ink-tech-fusion';
export { GridLines } from './grid-lines';
export { GlowEffect } from './glow-effect';
export { GeometricShapes } from './geometric-shapes';
export { GeometricAbstract } from './geometric-abstract';
export { FluidWaveBackground } from './fluid-wave-background';
export { AdvancedFloatingEffects } from './advanced-floating-effects';
export { ParallaxEffect } from './parallax-effect';
export { SealAnimationEnhanced } from './seal-animation-enhanced';
@@ -0,0 +1,220 @@
'use client';
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
import { MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
import { COMPANY_INFO, STATS } from '@/lib/constants';
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
interface HeroContentProps {
isVisible: boolean;
}
const features = [
{ icon: Shield, text: '安全可靠' },
{ icon: Zap, text: '高效便捷' },
{ icon: Award, text: '专业服务' },
];
function scrollTo(id: string) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, id: string) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
scrollTo(id);
}
}
export function HeroContent({ isVisible }: HeroContentProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="mb-8"
>
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
</span>
</motion.div>
);
}
export function HeroTitle({ isVisible }: HeroContentProps) {
return (
<motion.h1
id="hero-heading"
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-calligraphy"
style={{
fontWeight: 'normal',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
textRendering: 'optimizeLegibility'
}}
>
{COMPANY_INFO.shortName}
</motion.h1>
);
}
export function HeroDescription(_props: HeroContentProps) {
return (
<div className="mb-10">
<BlurReveal delay={0.3}>
<p className="text-xl sm:text-2xl text-[#4A5568] mb-4">
<span className="font-semibold bg-gradient-to-r from-[#C41E3A] via-[#E04A68] to-[#C41E3A] bg-clip-text text-transparent">
</span>
</p>
</BlurReveal>
<BlurReveal delay={0.4}>
<p className="text-lg text-[#718096] max-w-2xl mx-auto leading-relaxed">
</p>
</BlurReveal>
</div>
);
}
export function HeroButtons({ isVisible }: HeroContentProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.3 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
>
<MagneticButton strength={0.4}>
<Link href="/contact">
<SealButton size="lg" className="min-w-45">
<ArrowRight className="w-4 h-4 ml-2" />
</SealButton>
</Link>
</MagneticButton>
<MagneticButton strength={0.4}>
<RippleButton
size="lg"
variant="outline"
onClick={() => scrollTo('about')}
onKeyDown={(e) => handleKeyDown(e, 'about')}
className="min-w-45"
>
</RippleButton>
</MagneticButton>
</motion.div>
);
}
export function HeroFeatures({ isVisible }: HeroContentProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.35 }}
className="flex flex-wrap gap-4 justify-center mb-16"
>
{features.map((feature, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.4, delay: 0.4 + index * 0.1 }}
whileHover={{ scale: 1.05, y: -2 }}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
>
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
<span className="text-sm text-[#3D3D3D]">{feature.text}</span>
</motion.div>
))}
</motion.div>
);
}
export function HeroStats() {
const [statsVisible, setStatsVisible] = useState(false);
useEffect(() => {
const statsEl = document.getElementById('stats-section');
if (!statsEl) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setStatsVisible(true);
}
},
{ threshold: 0.5 }
);
observer.observe(statsEl);
return () => observer.disconnect();
}, []);
return (
<motion.div
id="stats-section"
initial={{ opacity: 0, y: 20 }}
animate={statsVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 }}
className="pt-16 border-t border-[#E2E8F0]"
>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
{STATS.map((stat, index) => (
<HeroStatItem
key={stat.label}
stat={stat}
index={index}
shouldAnimate={statsVisible}
/>
))}
</div>
</motion.div>
);
}
function HeroStatItem({ stat, index, shouldAnimate }: {
stat: { value: string; label: string };
index: number;
shouldAnimate: boolean;
}) {
const numericValue = parseInt(stat.value.replace(/\D/g, ''));
const suffix = stat.value.replace(/[\d]/g, '');
return (
<motion.div
className="group cursor-default text-center"
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
transition={{ duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
whileHover={{ scale: 1.05, y: -5 }}
>
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
{shouldAnimate ? (
<CounterWithEffect
end={numericValue}
suffix={suffix}
effect="bounce"
duration={2000}
/>
) : (
<span className="text-[#CBD5E0]">0{suffix}</span>
)}
</div>
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
{stat.label}
</div>
</motion.div>
);
}
+7 -189
View File
@@ -1,13 +1,8 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import dynamic from 'next/dynamic';
import { motion } from 'framer-motion';
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
import { GradientText, MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
import { COMPANY_INFO, STATS } from '@/lib/constants';
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
import { HeroContent, HeroTitle, HeroDescription, HeroButtons, HeroFeatures, HeroStats } from './hero-section-atoms';
const InkBackground = dynamic(
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
@@ -24,16 +19,9 @@ const SubtleDots = dynamic(
{ ssr: false }
);
const features = [
{ icon: Shield, text: '安全可靠' },
{ icon: Zap, text: '高效便捷' },
{ icon: Award, text: '专业服务' },
];
export function HeroSection() {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLElement>(null);
const [statsVisible, setStatsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
@@ -52,37 +40,6 @@ export function HeroSection() {
return () => observer.disconnect();
}, []);
useEffect(() => {
const statsEl = document.getElementById('stats-section');
if (!statsEl) {return;}
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setStatsVisible(true);
}
},
{ threshold: 0.5 }
);
observer.observe(statsEl);
return () => observer.disconnect();
}, []);
const handleScrollTo = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, id: string) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleScrollTo(id);
}
};
return (
<section
id="home"
@@ -102,153 +59,14 @@ export function HeroSection() {
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10">
<div className="max-w-4xl mx-auto text-center">
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="mb-8"
>
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
</span>
</motion.div>
<motion.h1
id="hero-heading"
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-calligraphy"
style={{
fontWeight: 'normal',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
textRendering: 'optimizeLegibility'
}}
>
{COMPANY_INFO.shortName}
</motion.h1>
<BlurReveal delay={0.3}>
<p className="text-xl sm:text-2xl text-[#4A5568] mb-4">
<GradientText colors={['#C41E3A', '#E04A68', '#C41E3A']} duration={4}>
</GradientText>
</p>
</BlurReveal>
<BlurReveal delay={0.4}>
<p className="text-lg text-[#718096] mb-10 max-w-2xl mx-auto leading-relaxed">
</p>
</BlurReveal>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.3 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
>
<MagneticButton strength={0.4}>
<Link href="/contact">
<SealButton
size="lg"
className="min-w-45"
>
<ArrowRight className="w-4 h-4 ml-2" />
</SealButton>
</Link>
</MagneticButton>
<MagneticButton strength={0.4}>
<RippleButton
size="lg"
variant="outline"
onClick={() => handleScrollTo('about')}
onKeyDown={(e) => handleKeyDown(e, 'about')}
className="min-w-45"
>
</RippleButton>
</MagneticButton>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.35 }}
className="flex flex-wrap gap-4 justify-center mb-16"
>
{features.map((feature, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.4, delay: 0.4 + index * 0.1 }}
whileHover={{ scale: 1.05, y: -2 }}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
>
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
<span className="text-sm text-[#3D3D3D]">{feature.text}</span>
</motion.div>
))}
</motion.div>
<motion.div
id="stats-section"
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 }}
className="pt-16 border-t border-[#E2E8F0]"
>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
{STATS.map((stat, index) => (
<StatItem
key={stat.label}
stat={stat}
index={index}
shouldAnimate={statsVisible}
/>
))}
</div>
</motion.div>
<HeroContent isVisible={isVisible} />
<HeroTitle isVisible={isVisible} />
<HeroDescription isVisible={isVisible} />
<HeroButtons isVisible={isVisible} />
<HeroFeatures isVisible={isVisible} />
<HeroStats />
</div>
</div>
</section>
);
}
function StatItem({ stat, index, shouldAnimate }: {
stat: { value: string; label: string };
index: number;
shouldAnimate: boolean;
}) {
const numericValue = parseInt(stat.value.replace(/\D/g, ''));
const suffix = stat.value.replace(/[\d]/g, '');
return (
<motion.div
className="group cursor-default text-center"
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
transition={{ duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
whileHover={{ scale: 1.05, y: -5 }}
>
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
{shouldAnimate ? (
<CounterWithEffect
end={numericValue}
suffix={suffix}
effect="bounce"
duration={2000}
/>
) : (
<span className="text-[#CBD5E0]">0{suffix}</span>
)}
</div>
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
{stat.label}
</div>
</motion.div>
);
}