承接前两批,继续清理 Task #16 的 P0 硬违规: - brand-content (1) / methodology-content (3) / products-content-v3 (4) / solution-detail-content-v3 (4) / team-content-v3 (1) / detail-product-value (2) / service-value (1) / solution-value (3) / brand-visuals 入场处 (1) - 审计:P0 33 → 13,P1 163 / P2 28 持平 - 只改 duration,保留原 delay(与首批先例一致) 验证: - tsc --noEmit 0 error;eslint 9 文件 0 error(仅 methodology 既有 `any` 警告,非本次引入) - 视觉基线不变:前两批已证明时长改动在 reducedMotion+animations disabled 下不改变最终像素 剩余 13 处 P0 均为决策点,未在本批处理: - 3 处循环动画(detail-hero ×2、brand-visuals ×1,repeat:Infinity,违反「禁循环动画」) - 10 处死代码(src/lib/animations.tsx,全站零引用)
114 lines
5.3 KiB
TypeScript
114 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
|
|
|
// ===== 抽象几何装饰 - 用于各 section 背景 =====
|
|
export function GeometricDecoration({ variant = 'circles' }: { variant?: 'circles' | 'lines' | 'grid' | 'waves' }) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
if (variant === 'circles') {
|
|
return (
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
|
<svg className="absolute top-[10%] right-[5%] w-64 h-64 opacity-[0.04]" viewBox="0 0 200 200">
|
|
<circle cx="100" cy="100" r="80" fill="none" stroke="currentColor" className="text-[var(--color-brand)]" strokeWidth="0.5" />
|
|
<circle cx="100" cy="100" r="60" fill="none" stroke="currentColor" className="text-[var(--color-brand)]" strokeWidth="0.5" />
|
|
<circle cx="100" cy="100" r="40" fill="none" stroke="currentColor" className="text-[var(--color-brand)]" strokeWidth="0.5" />
|
|
{!shouldReduceMotion && (
|
|
<motion.circle
|
|
cx="100" cy="100" r="20"
|
|
fill="rgba(var(--color-brand-rgb, 196, 30, 58), 0.1)"
|
|
animate={{ r: [20, 35, 20], opacity: [0.1, 0.05, 0.1] }}
|
|
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
|
/>
|
|
)}
|
|
</svg>
|
|
<svg className="absolute bottom-[15%] left-[8%] w-48 h-48 opacity-[0.03]" viewBox="0 0 200 200">
|
|
<rect x="40" y="40" width="120" height="120" rx="20" fill="none" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" transform="rotate(15 100 100)" />
|
|
<rect x="60" y="60" width="80" height="80" rx="12" fill="none" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" transform="rotate(15 100 100)" />
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (variant === 'lines') {
|
|
return (
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
|
<svg className="absolute top-0 right-0 w-full h-full opacity-[0.02]" viewBox="0 0 1000 1000" preserveAspectRatio="none">
|
|
<line x1="800" y1="0" x2="1000" y2="400" stroke="currentColor" className="text-[var(--color-brand)]" strokeWidth="1" />
|
|
<line x1="850" y1="0" x2="1000" y2="300" stroke="currentColor" className="text-[var(--color-brand)]" strokeWidth="0.5" />
|
|
<line x1="900" y1="0" x2="1000" y2="200" stroke="currentColor" className="text-[var(--color-brand)]" strokeWidth="0.5" />
|
|
<line x1="0" y1="600" x2="300" y2="1000" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" />
|
|
<line x1="0" y1="700" x2="200" y2="1000" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" />
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ===== 数据可视化条 - 用于指标展示区域 =====
|
|
export function DataBar({ value, max = 100, label, color = 'brand' }: { value: number; max?: number; label: string; color?: 'brand' | 'blue' | 'purple' | 'cyan' }) {
|
|
const percentage = Math.min((value / max) * 100, 100);
|
|
const colorMap = {
|
|
brand: 'var(--color-brand)',
|
|
blue: 'var(--color-accent-blue)',
|
|
purple: 'var(--color-accent-purple)',
|
|
cyan: 'var(--color-accent-cyan)',
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="text-[var(--color-text-muted)]">{label}</span>
|
|
<span className="font-mono font-medium text-[var(--color-text-secondary)]">{value}%</span>
|
|
</div>
|
|
<div className="h-1.5 rounded-full bg-[var(--color-bg-tertiary)] overflow-hidden">
|
|
<motion.div
|
|
initial={{ width: 0 }}
|
|
whileInView={{ width: `${percentage}%` }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1], delay: 0.2 }}
|
|
className="h-full rounded-full"
|
|
style={{ backgroundColor: colorMap[color] }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ===== 品牌渐变分隔线 =====
|
|
export function GradientDivider() {
|
|
return (
|
|
<div className="flex items-center gap-4 py-2">
|
|
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[var(--color-border-primary)] to-transparent" />
|
|
<div className="w-1.5 h-1.5 rounded-full bg-[var(--color-brand)]" />
|
|
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[var(--color-border-primary)] to-transparent" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ===== 品牌徽章/印章效果 =====
|
|
export function BrandStamp({ children }: { children: React.ReactNode }) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
return (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { scale: 1.3, opacity: 0, rotate: -12 }}
|
|
whileInView={{ scale: 1, opacity: 1, rotate: -5 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
|
className="inline-block px-3 py-1.5 border-2 border-[var(--color-brand)] rounded-sm"
|
|
style={{
|
|
transform: 'rotate(-5deg)',
|
|
background: 'repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(var(--color-brand-rgb), 0.03) 2px, rgba(var(--color-brand-rgb), 0.03) 4px)',
|
|
}}
|
|
>
|
|
<span className="text-xs font-bold tracking-widest text-[var(--color-brand)] font-calligraphy">
|
|
{children}
|
|
</span>
|
|
</motion.div>
|
|
);
|
|
}
|