feat: 优化全局样式与组件视觉升级

- 更新 globals.css 全局样式
- 优化 not-found 404 页面
- 升级 footer, mega-dropdown, mobile-tab-bar 布局组件
- 改进 challenge, cta, hero 等核心 section 组件
- 优化 challenge-card, ink-glow-card, product-card 等 UI 组件
- 更新产品常量配置
This commit is contained in:
张翔
2026-05-19 16:35:10 +08:00
parent 7f6128a6ff
commit f7024b1cf4
17 changed files with 258 additions and 423 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import { COMPANY_INFO, NAVIGATION_V2, MEGA_DROPDOWN_DATA } from '@/lib/constants
export function Footer() {
return (
<footer className="bg-[var(--color-footer-bg)] text-white py-16" data-testid="footer" role="contentinfo">
<footer className="bg-[var(--color-footer-bg)] text-white py-12 md:py-16" data-testid="footer" role="contentinfo">
<div className="container-wide">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-10 lg:gap-8">
<div data-testid="card-brand" className="lg:col-span-2">
@@ -107,7 +107,7 @@ export function Footer() {
</div>
</div>
<div className="border-t border-[var(--color-footer-border)] mt-12 pt-8 pb-24 md:pb-8">
<div className="border-t border-[var(--color-footer-border)] mt-12 pt-8 pb-24 md:pb-8" style={{ paddingBottom: 'calc(5rem + env(safe-area-inset-bottom, 0px))' }}>
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-[var(--color-footer-text-muted)] text-sm">
© {new Date().getFullYear()} {COMPANY_INFO.name}. All rights reserved.
+8 -3
View File
@@ -74,10 +74,15 @@ export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }
<StaticLink
key={item.id}
href={item.href}
className="block p-4 rounded-lg border-l-[3px] border-l-[var(--color-brand-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors duration-200"
className="block p-4 rounded-lg border border-transparent hover:border-[var(--color-brand-primary)]/10 hover:bg-[var(--color-brand-primary-bg)] transition-all duration-200 group"
>
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{item.title}</div>
<div className="text-xs text-[var(--color-text-placeholder)] mt-1.5 leading-relaxed">{item.description}</div>
<div className="flex items-center gap-3">
<div className="w-1 h-8 rounded-full bg-[var(--color-border-primary)] group-hover:bg-[var(--color-brand-primary)] transition-colors duration-200 shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{item.title}</div>
<div className="text-xs text-[var(--text-placeholder)] mt-1.5 leading-relaxed">{item.description}</div>
</div>
</div>
</StaticLink>
))}
</div>
+1 -1
View File
@@ -37,7 +37,7 @@ export function MobileTabBar() {
};
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 md:hidden bg-[var(--color-bg-primary)]/95 backdrop-blur-xl border-t border-[var(--color-border-primary)] safe-area-inset-bottom">
<nav className="fixed bottom-0 left-0 right-0 z-50 md:hidden bg-[var(--color-bg-primary)]/95 backdrop-blur-xl border-t border-[var(--color-border-primary)]" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}>
<div className="flex items-center justify-around h-16">
{tabs.map((tab) => {
const Icon = tab.icon;
+7 -14
View File
@@ -56,21 +56,14 @@ export function ChallengeSection() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
{CHALLENGES.map((challenge, index) => (
<motion.div
<ChallengeCard
key={challenge.id}
initial={shouldReduceMotion ? {} : { opacity: 0, x: index === 0 ? -32 : index === 1 ? 0 : 32 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.55, delay: index * 0.12, ease: [0.16, 1, 0.3, 1] }}
>
<ChallengeCard
title={challenge.title}
description={challenge.description}
scenario={challenge.scenario}
href={challenge.href}
index={index}
/>
</motion.div>
title={challenge.title}
description={challenge.description}
scenario={challenge.scenario}
href={challenge.href}
index={index}
/>
))}
</div>
</div>
+11 -64
View File
@@ -55,64 +55,18 @@ export function CTASection({
style={{ backgroundColor: 'var(--color-cta-bg)' }}
>
<div className="pointer-events-none absolute inset-0">
<svg className="absolute inset-0 w-full h-full" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<filter id="cta-grain">
<feTurbulence
type="fractalNoise"
baseFrequency="0.75"
numOctaves="4"
stitchTiles="stitch"
result="noise"
/>
<feColorMatrix type="saturate" values="0" in="noise" result="desaturated" />
<feComponentTransfer in="desaturated" result="faded">
<feFuncA type="linear" slope="0.04" />
</feComponentTransfer>
</filter>
<rect width="100%" height="100%" filter="url(#cta-grain)" />
</svg>
{!shouldReduceMotion && (
<>
<motion.div
className="absolute inset-[-20%]"
style={{
background:
'radial-gradient(circle at center, rgba(196, 30, 58, 0.08) 0%, transparent 50%)',
x: glowX,
y: glowY,
opacity: isHovered ? 1 : 0.5,
}}
transition={{ opacity: { duration: 0.6 } }}
/>
<motion.div
animate={{
scale: [1, 1.15, 1],
opacity: [0.06, 0.12, 0.06],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: 'easeInOut',
}}
className="absolute left-[10%] top-[20%] w-64 h-64 rounded-full"
style={{ background: 'radial-gradient(circle, rgba(196,30,58,0.2), transparent 70%)' }}
/>
<motion.div
animate={{
scale: [1, 1.2, 1],
opacity: [0.05, 0.1, 0.05],
}}
transition={{
duration: 10,
repeat: Infinity,
ease: 'easeInOut',
delay: 2,
}}
className="absolute right-[5%] bottom-[25%] w-80 h-80 rounded-full"
style={{ background: 'radial-gradient(circle, rgba(196,30,58,0.15), transparent 70%)' }}
/>
</>
<motion.div
className="absolute inset-[-20%]"
style={{
background:
'radial-gradient(circle at center, rgba(196, 30, 58, 0.08) 0%, transparent 50%)',
x: glowX,
y: glowY,
opacity: isHovered ? 1 : 0.5,
}}
transition={{ opacity: { duration: 0.6 } }}
/>
)}
<div
@@ -122,13 +76,6 @@ export function CTASection({
'linear-gradient(to top, rgba(196, 30, 58, 0.06) 0%, transparent 40%), linear-gradient(135deg, transparent 40%, rgba(196, 30, 58, 0.03) 100%)',
}}
/>
<div
className="absolute inset-0"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23C41E3A' fill-opacity='0.02'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
}}
/>
</div>
<div className="container-wide relative z-10">
+35 -36
View File
@@ -54,61 +54,61 @@ export function HeroSectionV2() {
>
<HeroInkBackground />
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10 flex-1 flex items-center">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center w-full">
<div className="container-wide py-16 md:py-24 lg:py-32 relative z-10 flex-1 flex items-center">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16 items-center w-full">
<div>
<motion.div
{...fadeUp(0)}
className="mb-8"
className="mb-6"
>
<span className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--color-brand-primary-bg)] text-[var(--color-brand-primary)] text-sm font-medium border border-[var(--color-brand-primary)]/10">
<span className="inline-flex items-center gap-2 px-3 py-1.5 sm:px-4 sm:py-2 rounded-full bg-[var(--color-brand-primary-bg)] text-[var(--color-brand-primary)] text-xs sm:text-sm font-medium border border-[var(--color-brand-primary)]/10">
{COMPANY_INFO.slogan}
</span>
</motion.div>
<motion.h1
id="hero-heading"
{...fadeUp(0.1)}
className="text-6xl sm:text-7xl lg:text-8xl tracking-tight mb-6 font-brand"
style={{ fontWeight: 'normal' }}
>
{COMPANY_INFO.shortName}
</motion.h1>
<motion.p
{...fadeUp(0.2)}
className="text-xl sm:text-2xl text-[var(--color-text-primary)] mb-4"
{...fadeUp(0.1)}
className="text-lg sm:text-xl lg:text-2xl text-[var(--color-text-primary)] mb-3"
>
<span className="font-semibold text-[var(--color-brand-primary)] font-calligraphy">
</span>
</motion.p>
<motion.h1
id="hero-heading"
{...fadeUp(0.15)}
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-5 font-brand leading-[1.1]"
style={{ fontWeight: 'normal' }}
>
{COMPANY_INFO.shortName}
</motion.h1>
<motion.p
{...fadeUp(0.3)}
className="text-lg text-[var(--color-text-muted)] max-w-xl leading-relaxed mb-10"
{...fadeUp(0.25)}
className="text-base sm:text-lg text-[var(--color-text-muted)] max-w-lg leading-relaxed mb-8"
>
{COMPANY_INFO.description}
</motion.p>
<motion.div
{...fadeUp(0.4)}
{...fadeUp(0.35)}
className="flex flex-col sm:flex-row items-start gap-4"
>
<Button size="lg" asChild>
<Button size="lg" asChild className="min-h-[52px] px-8 text-base font-semibold shadow-lg shadow-[var(--color-brand-primary)]/20">
<StaticLink href="/contact">
<ArrowRight className="w-4 h-4 ml-2" />
</StaticLink>
</Button>
<Button size="lg" variant="outline" asChild>
<Button size="lg" variant="ghost" asChild className="text-[var(--color-text-muted)]">
<StaticLink href="/products"></StaticLink>
</Button>
</motion.div>
<motion.div
{...fadeUp(0.5)}
className="flex flex-wrap gap-3 mt-10 lg:hidden"
{...fadeUp(0.45)}
className="flex flex-wrap gap-2 sm:gap-3 mt-8"
>
{CAPABILITIES.map((cap) => {
const Icon = cap.icon;
@@ -129,7 +129,6 @@ export function HeroSectionV2() {
initial={shouldReduceMotion ? {} : { opacity: 0, y: 32 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5, ease: EASE }}
className="hidden lg:block"
>
<div
ref={cardRef}
@@ -152,35 +151,35 @@ export function HeroSectionV2() {
}}
/>
<div className="relative rounded-2xl bg-[var(--color-bg-primary)]/80 backdrop-blur-sm p-8 lg:p-10">
<div className="w-14 h-14 rounded-xl bg-[var(--color-brand-primary-bg)] flex items-center justify-center mb-6">
<Handshake className="w-6 h-6 text-[var(--color-brand-primary)]" strokeWidth={1.8} />
<div className="relative rounded-2xl bg-[var(--color-bg-primary)]/80 backdrop-blur-sm p-6 sm:p-8 lg:p-10">
<div className="w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-[var(--color-brand-primary-bg)] flex items-center justify-center mb-5 lg:mb-6">
<Handshake className="w-5 h-5 lg:w-6 lg:h-6 text-[var(--color-brand-primary)]" strokeWidth={1.8} />
</div>
<h3 className="text-xl font-semibold text-[var(--color-text-primary)] mb-2">
<h3 className="text-lg lg:text-xl font-semibold text-[var(--color-text-primary)] mb-2">
</h3>
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-8">
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-6 lg:mb-8">
</p>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3 lg:gap-4 lg:space-y-0 lg:block lg:space-y-4">
{JOURNEY_STEPS.map((step, idx) => {
const Icon = step.icon;
return (
<div
key={step.label}
className="flex items-center gap-4 p-3 rounded-lg bg-[var(--color-bg-section)]/60"
className="flex items-center gap-3 lg:gap-4 p-2.5 lg:p-3 rounded-lg bg-[var(--color-bg-section)]/60"
>
<div className="w-9 h-9 rounded-lg bg-[var(--color-brand-primary-bg)] flex items-center justify-center shrink-0">
<Icon className="w-4 h-4 text-[var(--color-brand-primary)]" strokeWidth={2.2} />
<div className="w-8 h-8 lg:w-9 lg:h-9 rounded-lg bg-[var(--color-brand-primary-bg)] flex items-center justify-center shrink-0">
<Icon className="w-3.5 h-3.5 lg:w-4 lg:h-4 text-[var(--color-brand-primary)]" strokeWidth={2.2} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-[var(--color-brand-primary)]">0{idx + 1}</span>
<span className="text-sm font-medium text-[var(--color-text-primary)]">{step.label}</span>
<div className="flex items-center gap-1.5 lg:gap-2">
<span className="text-[10px] lg:text-xs font-bold text-[var(--color-brand-primary)]">0{idx + 1}</span>
<span className="text-xs lg:text-sm font-medium text-[var(--color-text-primary)]">{step.label}</span>
</div>
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">{step.desc}</p>
<p className="text-[10px] lg:text-xs text-[var(--color-text-muted)] mt-0.5 hidden sm:block">{step.desc}</p>
</div>
</div>
);
@@ -7,12 +7,7 @@ import { METHODOLOGY } from '@/lib/constants/methodology';
import { CheckCircle2 } from 'lucide-react';
import { InkGlowCard } from '@/components/ui/ink-glow-card';
const phaseAccents: { rgb: string; glowStart: string; glowEnd: string }[] = [
{ rgb: '196, 30, 58', glowStart: 'var(--color-brand-primary)', glowEnd: 'var(--color-warning)' },
{ rgb: '217, 119, 6', glowStart: 'var(--color-warning)', glowEnd: 'var(--color-success)' },
{ rgb: '22, 163, 74', glowStart: 'var(--color-success)', glowEnd: 'var(--color-accent-cyan)' },
{ rgb: '37, 99, 235', glowStart: 'var(--color-accent-blue)', glowEnd: 'var(--color-accent-purple)' },
];
const phaseBgOpacities = [0.1, 0.08, 0.12, 0.06];
export function MethodologySection() {
const ref = useRef(null);
@@ -41,19 +36,16 @@ export function MethodologySection() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
{METHODOLOGY.map((phase, idx) => {
const accent = phaseAccents[idx % phaseAccents.length]!;
const bgOpacity = phaseBgOpacities[idx % phaseBgOpacities.length];
return (
<InkGlowCard
key={phase.id}
index={idx}
accentColorRgb={accent.rgb}
glowStart={accent.glowStart}
glowEnd={accent.glowEnd}
>
<div className="p-6 md:p-8">
<div
className="w-10 h-10 rounded-full flex items-center justify-center mb-5 text-sm font-bold"
style={{ backgroundColor: accent.glowStart, color: 'var(--color-bg-primary)' }}
style={{ backgroundColor: `rgba(var(--color-brand-primary-rgb), ${bgOpacity})`, color: 'var(--color-brand-primary)' }}
>
{phase.number}
</div>
@@ -33,21 +33,14 @@ export function ProductMatrixSection() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{PRODUCTS.map((product, index) => (
<motion.div
<ProductCard
key={product.id}
initial={shouldReduceMotion ? {} : { opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.5, delay: index * 0.08, ease: [0.16, 1, 0.3, 1] }}
>
<ProductCard
title={product.title}
description={product.description}
href={`/products/${product.id}`}
index={index}
status={product.status}
/>
</motion.div>
title={product.title}
description={product.description}
href={`/products/${product.id}`}
index={index}
status={product.status}
/>
))}
</div>
</div>
@@ -31,7 +31,7 @@ export function SocialProofSection() {
const shouldReduceMotion = useReducedMotion();
return (
<section id="social-proof" role="region" aria-labelledby="social-proof-heading" className="py-16 md:py-20 bg-[var(--color-bg-section)]">
<section id="social-proof" role="region" aria-labelledby="social-proof-heading" className="py-12 md:py-16 lg:py-20 bg-[var(--color-bg-section)]">
<div className="container-wide">
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
+6 -31
View File
@@ -14,34 +14,13 @@ interface ChallengeCardProps {
interface ScenarioConfig {
icon: LucideIcon;
accentColor: string;
accentColorRgb: string;
glowStart: string;
glowEnd: string;
iconBgOpacity: number;
}
const scenarioConfig: Record<string, ScenarioConfig> = {
isolation: {
icon: Lock,
accentColor: 'var(--color-brand-primary)',
accentColorRgb: '196, 30, 58',
glowStart: 'var(--color-brand-primary)',
glowEnd: 'var(--color-accent-purple)',
},
growth: {
icon: TrendingUp,
accentColor: 'var(--color-warning)',
accentColorRgb: '217, 119, 6',
glowStart: 'var(--color-warning)',
glowEnd: 'var(--color-success)',
},
compliance: {
icon: Shield,
accentColor: 'var(--color-success)',
accentColorRgb: '22, 163, 74',
glowStart: 'var(--color-success)',
glowEnd: 'var(--color-accent-cyan)',
},
isolation: { icon: Lock, iconBgOpacity: 0.06 },
growth: { icon: TrendingUp, iconBgOpacity: 0.08 },
compliance: { icon: Shield, iconBgOpacity: 0.05 },
};
export function ChallengeCard({ title, description, scenario, href, index }: ChallengeCardProps) {
@@ -52,19 +31,15 @@ export function ChallengeCard({ title, description, scenario, href, index }: Cha
<InkGlowCard
index={index}
href={href}
accentColorRgb={config.accentColorRgb}
glowStart={config.glowStart}
glowEnd={config.glowEnd}
>
<div className="p-6 md:p-8">
<div className="flex items-start justify-between mb-5">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center"
style={{ backgroundColor: `rgba(${config.accentColorRgb}, 0.06)` }}
style={{ backgroundColor: `rgba(var(--color-brand-primary-rgb), ${config.iconBgOpacity})` }}
>
<IconComponent
className="w-5 h-5"
style={{ color: config.accentColor }}
className="w-5 h-5 text-[var(--color-brand-primary)]"
strokeWidth={1.8}
/>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ import { useEffect, useRef, useMemo, useSyncExternalStore } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
const PARTICLE_COUNT = 24;
const PARTICLE_COUNT = 12;
interface Particle {
x: number;
+13 -19
View File
@@ -7,24 +7,16 @@ import { StaticLink } from '@/components/ui/static-link';
interface InkGlowCardProps {
children: ReactNode;
index?: number;
accentColorRgb?: string;
glowStart?: string;
glowEnd?: string;
className?: string;
href?: string;
onClick?: () => void;
}
const DEFAULT_ACCENT = '196, 30, 58';
const DEFAULT_GLOW_START = 'var(--color-brand-primary)';
const DEFAULT_GLOW_END = 'var(--color-warning)';
const DEFAULT_ACCENT_RGB = '196, 30, 58';
export function InkGlowCard({
children,
index = 0,
accentColorRgb = DEFAULT_ACCENT,
glowStart = DEFAULT_GLOW_START,
glowEnd = DEFAULT_GLOW_END,
className = '',
href,
onClick,
@@ -48,7 +40,7 @@ export function InkGlowCard({
className="absolute inset-0 pointer-events-none transition-opacity duration-500"
style={{
opacity: isHovered ? 1 : 0,
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, rgba(${accentColorRgb}, 0.04), transparent 40%)`,
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, rgba(${DEFAULT_ACCENT_RGB}, 0.04), transparent 40%)`,
}}
/>
<div className="relative z-10">{children}</div>
@@ -58,19 +50,21 @@ export function InkGlowCard({
return (
<motion.div
ref={cardRef}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
initial={{ opacity: 0, scale: 0.96, y: 16 }}
whileInView={{ opacity: 1, scale: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{
duration: 0.5,
delay: index * 0.08,
ease: [0.16, 1, 0.3, 1],
}}
className={`relative ink-glow-border rounded-2xl ${className}`}
style={{
'--glow-start': glowStart,
'--glow-end': glowEnd,
} as React.CSSProperties}
style={
{
'--glow-start': 'var(--color-brand-primary)',
'--glow-end': 'var(--color-warning)',
} as React.CSSProperties
}
>
{href ? (
<StaticLink
@@ -78,7 +72,7 @@ export function InkGlowCard({
className="relative block rounded-2xl bg-[var(--color-bg-primary)] overflow-hidden transition-all duration-500"
style={{
boxShadow: isHovered
? `0 20px 40px rgba(0,0,0,0.1), 0 0 0 1px rgba(${accentColorRgb}, 0.12)`
? `0 20px 40px rgba(0,0,0,0.1), 0 0 0 1px rgba(${DEFAULT_ACCENT_RGB}, 0.12)`
: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transform: isHovered ? 'translateY(-6px)' : 'translateY(0)',
}}
@@ -93,7 +87,7 @@ export function InkGlowCard({
className="relative rounded-2xl bg-[var(--color-bg-primary)] overflow-hidden transition-all duration-500"
style={{
boxShadow: isHovered
? `0 20px 40px rgba(0,0,0,0.1), 0 0 0 1px rgba(${accentColorRgb}, 0.12)`
? `0 20px 40px rgba(0,0,0,0.1), 0 0 0 1px rgba(${DEFAULT_ACCENT_RGB}, 0.12)`
: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transform: isHovered ? 'translateY(-6px)' : 'translateY(0)',
}}
+15 -23
View File
@@ -14,29 +14,25 @@ interface ProductCardProps {
interface ProductStyleConfig {
icon: LucideIcon;
accentColor: string;
accentColorRgb: string;
glowStart: string;
glowEnd: string;
iconBgOpacity: number;
}
const productConfig: ProductStyleConfig[] = [
{ icon: Database, accentColor: 'var(--color-brand-primary)', accentColorRgb: '196, 30, 58', glowStart: 'var(--color-brand-primary)', glowEnd: 'var(--color-warning)' },
{ icon: Users, accentColor: 'var(--color-warning)', accentColorRgb: '217, 119, 6', glowStart: 'var(--color-warning)', glowEnd: 'var(--color-success)' },
{ icon: FileText, accentColor: 'var(--color-accent-blue)', accentColorRgb: '37, 99, 235', glowStart: 'var(--color-accent-blue)', glowEnd: 'var(--color-accent-purple)' },
{ icon: BarChart3, accentColor: 'var(--color-success)', accentColorRgb: '22, 163, 74', glowStart: 'var(--color-success)', glowEnd: 'var(--color-accent-cyan)' },
{ icon: Truck, accentColor: 'var(--color-accent-purple)', accentColorRgb: '124, 58, 237', glowStart: 'var(--color-accent-purple)', glowEnd: 'var(--color-brand-primary)' },
{ icon: Building2, accentColor: 'var(--color-accent-cyan)', accentColorRgb: '8, 145, 178', glowStart: 'var(--color-accent-cyan)', glowEnd: 'var(--color-accent-blue)' },
];
const productIcons: LucideIcon[] = [Database, Users, FileText, BarChart3, Truck, Building2];
const productConfig: ProductStyleConfig[] = productIcons.map((icon, i) => ({
icon,
iconBgOpacity: 0.04 + i * 0.015,
}));
const defaultConfig: ProductStyleConfig = {
icon: Database, accentColor: 'var(--color-brand-primary)', accentColorRgb: '196, 30, 58', glowStart: 'var(--color-brand-primary)', glowEnd: 'var(--color-warning)',
icon: Database,
iconBgOpacity: 0.06,
};
const statusConfig = {
'研发中': { bg: 'rgba(var(--color-brand-primary-rgb), 0.08)', text: 'var(--color-brand-primary)', border: 'rgba(var(--color-brand-primary-rgb), 0.15)' },
'内测中': { bg: 'rgba(var(--color-warning-rgb), 0.08)', text: 'var(--color-warning)', border: 'rgba(var(--color-warning-rgb), 0.15)' },
'已发布': { bg: 'rgba(var(--color-success-rgb), 0.08)', text: 'var(--color-success)', border: 'rgba(var(--color-success-rgb), 0.15)' },
'研发中': { bg: 'rgba(var(--color-brand-primary-rgb), 0.06)', text: 'var(--color-brand-primary)', border: 'rgba(var(--color-brand-primary-rgb), 0.12)' },
'内测中': { bg: 'rgba(var(--color-warning-rgb), 0.06)', text: 'var(--color-warning)', border: 'rgba(var(--color-warning-rgb), 0.12)' },
'已发布': { bg: 'rgba(var(--color-success-rgb), 0.06)', text: 'var(--color-success)', border: 'rgba(var(--color-success-rgb), 0.12)' },
} as const;
export function ProductCard({ title, description, href, index, status }: ProductCardProps) {
@@ -48,19 +44,15 @@ export function ProductCard({ title, description, href, index, status }: Product
<InkGlowCard
index={index}
href={href}
accentColorRgb={config.accentColorRgb}
glowStart={config.glowStart}
glowEnd={config.glowEnd}
>
<div className="p-6 md:p-8">
<div className="flex items-start justify-between mb-5">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center"
style={{ backgroundColor: `rgba(${config.accentColorRgb}, 0.06)` }}
style={{ backgroundColor: `rgba(var(--color-brand-primary-rgb), ${config.iconBgOpacity})` }}
>
<IconComponent
className="w-5 h-5"
style={{ color: config.accentColor }}
className="w-5 h-5 text-[var(--color-brand-primary)]"
strokeWidth={1.8}
/>
</div>
@@ -91,7 +83,7 @@ export function ProductCard({ title, description, href, index, status }: Product
{description}
</p>
<div className="flex items-center gap-1.5 text-sm font-medium text-[var(--color-text-subtle)] group-hover:text-[var(--color-brand-primary)] transition-colors">
<div className="flex items-center gap-1.5 text-sm font-medium text-[var(--color-text-subtle)] group-hover:text-[var(--color-brand-primary)] transition-colors min-h-[44px]">
<span></span>
<ArrowUpRight className="w-4 h-4" strokeWidth={2} />
</div>
+7 -43
View File
@@ -28,58 +28,22 @@ export function ScrollProgress() {
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={shouldReduceMotion ? {} : { opacity: 0, y: -4 }}
transition={{ duration: 0.25 }}
className="fixed top-0 left-0 right-0 h-[3px] z-[100] bg-transparent"
initial={shouldReduceMotion ? {} : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={shouldReduceMotion ? {} : { opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed top-0 left-0 right-0 h-[2px] z-[100]"
role="progressbar"
aria-label="页面滚动进度"
aria-valuemin={0}
aria-valuemax={100}
>
<motion.div
className="h-full origin-left relative"
className="h-full origin-left"
style={{
scaleX,
background:
'linear-gradient(90deg, #C41E3A 0%, #E04A68 50%, #C41E3A 100%)',
backgroundSize: '200% 100%',
background: 'var(--color-brand-primary)',
}}
>
<motion.div
className="absolute inset-y-0 right-0 w-6 -mr-[2px]"
style={{
background: 'linear-gradient(90deg, transparent, rgba(196,30,58,0.6))',
filter: 'blur(4px)',
opacity: shouldReduceMotion ? 0 : 1,
}}
/>
{!shouldReduceMotion && (
<>
<motion.div
className="absolute top-1/2 -translate-y-1/2 right-0 w-2 h-2 rounded-full"
style={{ background: '#fff', boxShadow: '0 0 6px rgba(196,30,58,0.8)' }}
animate={{ scale: [1, 1.5, 1], opacity: [0.8, 1, 0.8] }}
transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
/>
<motion.div
className="absolute bottom-0 left-0 right-0 h-[1px]"
style={{
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent)',
}}
/>
</>
)}
</motion.div>
<div
className="absolute inset-0 pointer-events-none"
style={{
background: 'linear-gradient(90deg, transparent, rgba(196,30,58,0.08), transparent)',
filter: 'blur(8px)',
transform: `scaleX(${typeof scaleX === 'object' ? 'var(--progress, 0)' : 1})`,
}}
aria-hidden="true"
/>
</motion.div>
);