style(theme): 更新网站主题色彩方案与字体配置
- 调整主色调从 #1C1C1C 至 #1A1A1A,优化视觉层次 - 更新背景色系为暖白色调 (#FAFAF7, #F5F4F0 等) - 配置中文字体栈,添加 serif 字体支持 - 优化文本颜色梯度,提升可读性 - 调整边框颜色,统一水墨风格 - 添加 Google Search Console 验证码配置项 - 新增桌面应用架构专家代理配置文件 - 重构 E2E 测试等待策略,提升稳定性 - 添加回归测试脚本,增强质量保障
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
'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-primary)]" strokeWidth="0.5" />
|
||||
<circle cx="100" cy="100" r="60" fill="none" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
<circle cx="100" cy="100" r="40" fill="none" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
{!shouldReduceMotion && (
|
||||
<motion.circle
|
||||
cx="100" cy="100" r="20"
|
||||
fill="rgba(var(--color-brand-primary-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-primary)]" strokeWidth="1" />
|
||||
<line x1="850" y1="0" x2="1000" y2="300" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
<line x1="900" y1="0" x2="1000" y2="200" stroke="currentColor" className="text-[var(--color-brand-primary)]" 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-primary)',
|
||||
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: 1, 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-primary)]" />
|
||||
<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-primary)] rounded-sm"
|
||||
style={{
|
||||
transform: 'rotate(-5deg)',
|
||||
background: 'repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(var(--color-brand-primary-rgb), 0.03) 2px, rgba(var(--color-brand-primary-rgb), 0.03) 4px)',
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-bold tracking-widest text-[var(--color-brand-primary)] font-calligraphy">
|
||||
{children}
|
||||
</span>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { type ReactNode, useRef } from 'react';
|
||||
import { useMouseGlow } from '@/hooks/use-mouse-glow';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface InkCardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Enable ink-glow border on hover */
|
||||
glow?: boolean;
|
||||
/** Enable mouse follow glow */
|
||||
mouseFollow?: boolean;
|
||||
/** Card padding variant */
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
/** Interactive - adds hover elevation */
|
||||
interactive?: boolean;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const paddingMap = {
|
||||
none: '',
|
||||
sm: 'p-4 sm:p-5',
|
||||
md: 'p-5 sm:p-6 md:p-7',
|
||||
lg: 'p-6 sm:p-8 md:p-10',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 水墨雅致统一卡片组件
|
||||
* 融合 ink-glow-border + mouse-follow-glow + 宣纸底色
|
||||
*/
|
||||
export function InkCard({
|
||||
children,
|
||||
className,
|
||||
glow = true,
|
||||
mouseFollow = true,
|
||||
padding = 'md',
|
||||
interactive = true,
|
||||
onClick,
|
||||
href,
|
||||
}: InkCardProps) {
|
||||
const { glowStyle, handlers } = useMouseGlow({ radius: 350, opacity: 0.05 });
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const cardClasses = cn(
|
||||
'relative rounded-2xl',
|
||||
'bg-[var(--color-bg-primary)]',
|
||||
'border border-[var(--color-border-primary)]',
|
||||
glow && 'ink-glow-border',
|
||||
interactive && 'transition-all duration-300 hover:border-[rgba(var(--color-brand-primary-rgb),0.2)] hover:shadow-lg',
|
||||
paddingMap[padding],
|
||||
className,
|
||||
);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{/* Mouse follow glow overlay */}
|
||||
{mouseFollow && <div style={glowStyle} />}
|
||||
{/* Card content */}
|
||||
<div className="relative z-[1]">{children}</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
||||
const handleLinkClick = !isExternal
|
||||
? (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// 阻止 Next.js 客户端路由拦截(output: 'export' 模式下 RSC payload 不存在)
|
||||
e.preventDefault();
|
||||
window.location.href = href;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={anchorRef}
|
||||
href={href}
|
||||
className={cardClasses}
|
||||
onClick={handleLinkClick}
|
||||
{...handlers}
|
||||
style={glow ? { '--glow-start': 'var(--color-text-muted)', '--glow-end': 'var(--color-border-primary)' } as React.CSSProperties : undefined}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={cardClasses}
|
||||
onClick={onClick}
|
||||
{...handlers}
|
||||
style={glow ? { '--glow-start': 'var(--color-text-muted)', '--glow-end': 'var(--color-border-primary)' } as React.CSSProperties : undefined}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
interface InkWashBackgroundProps {
|
||||
/** Background variant */
|
||||
variant?: 'hero' | 'section' | 'subtle';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 水墨晕散背景组件
|
||||
* 用于 Hero 和各大 section 的水墨晕散效果
|
||||
*/
|
||||
export function InkWashBackground({ variant = 'section', className }: InkWashBackgroundProps) {
|
||||
|
||||
if (variant === 'hero') {
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
|
||||
{/* 主墨晕 */}
|
||||
<div
|
||||
className="absolute top-[10%] left-[15%] w-[600px] h-[600px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(var(--color-primary-rgb), 0.04) 0%, transparent 60%)',
|
||||
}}
|
||||
/>
|
||||
{/* 品牌红点缀 */}
|
||||
<div
|
||||
className="absolute bottom-[20%] right-[10%] w-[400px] h-[400px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(var(--color-brand-primary-rgb), 0.025) 0%, transparent 55%)',
|
||||
}}
|
||||
/>
|
||||
{/* 宣纸纹理 */}
|
||||
<div className="absolute inset-0 bg-paper-texture opacity-50" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'subtle') {
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
|
||||
<div className="absolute inset-0 bg-paper-texture opacity-30" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: section
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
|
||||
<div
|
||||
className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px]"
|
||||
style={{
|
||||
background: 'radial-gradient(ellipse at 50% 0%, rgba(var(--color-primary-rgb), 0.025) 0%, transparent 65%)',
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-paper-texture opacity-30" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { InkCard } from './InkCard';
|
||||
export { InkWashBackground } from './InkWashBackground';
|
||||
@@ -12,21 +12,25 @@ interface ProductCardProps {
|
||||
status?: '研发中' | '内测中' | '已发布';
|
||||
}
|
||||
|
||||
interface ProductStyleConfig {
|
||||
icon: LucideIcon;
|
||||
iconBgOpacity: number;
|
||||
}
|
||||
|
||||
const productIcons: LucideIcon[] = [Database, Users, FileText, BarChart3, Truck, Building2];
|
||||
|
||||
const productConfig: ProductStyleConfig[] = productIcons.map((icon, i) => ({
|
||||
const PRODUCT_COLORS = [
|
||||
{ bg: 'rgba(var(--color-brand-primary-rgb), 0.08)', text: 'var(--color-brand-primary)', border: 'rgba(var(--color-brand-primary-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-blue-rgb), 0.08)', text: 'var(--color-accent-blue)', border: 'rgba(var(--color-accent-blue-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-purple-rgb), 0.08)', text: 'var(--color-accent-purple)', border: 'rgba(var(--color-accent-purple-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-cyan-rgb), 0.08)', text: 'var(--color-accent-cyan)', border: 'rgba(var(--color-accent-cyan-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-brand-primary-rgb), 0.08)', text: 'var(--color-brand-primary)', border: 'rgba(var(--color-brand-primary-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-blue-rgb), 0.08)', text: 'var(--color-accent-blue)', border: 'rgba(var(--color-accent-blue-rgb), 0.12)' },
|
||||
] as const;
|
||||
|
||||
const productConfig: { icon: LucideIcon; color: { bg: string; text: string; border: string } }[] = productIcons.map((icon, i) => ({
|
||||
icon,
|
||||
iconBgOpacity: 0.04 + i * 0.015,
|
||||
color: PRODUCT_COLORS[i % PRODUCT_COLORS.length] ?? PRODUCT_COLORS[0],
|
||||
}));
|
||||
|
||||
const defaultConfig: ProductStyleConfig = {
|
||||
const defaultConfig = {
|
||||
icon: Database,
|
||||
iconBgOpacity: 0.06,
|
||||
color: PRODUCT_COLORS[0],
|
||||
};
|
||||
|
||||
const statusConfig = {
|
||||
@@ -49,10 +53,11 @@ export function ProductCard({ title, description, href, index, status }: Product
|
||||
<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(var(--color-brand-primary-rgb), ${config.iconBgOpacity})` }}
|
||||
style={{ backgroundColor: config.color.bg }}
|
||||
>
|
||||
<IconComponent
|
||||
className="w-5 h-5 text-[var(--color-brand-primary)]"
|
||||
className="w-5 h-5"
|
||||
style={{ color: config.color.text }}
|
||||
strokeWidth={1.8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, useScroll, useSpring } from 'framer-motion';
|
||||
import { motion, useScroll, useSpring, useTransform } from 'framer-motion';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
export function ScrollProgress() {
|
||||
@@ -21,6 +21,8 @@ export function ScrollProgress() {
|
||||
restDelta: 0.001,
|
||||
});
|
||||
|
||||
const glowLeft = useTransform(glowX, (v) => `calc(${v * 100}% - 30px)`);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsVisible(window.scrollY > 80);
|
||||
@@ -55,14 +57,11 @@ export function ScrollProgress() {
|
||||
<motion.div
|
||||
className="absolute top-1/2 -translate-y-1/2 w-[60px] h-[3px] -mt-[0.5px]"
|
||||
style={{
|
||||
left: 'calc(var(--glow-x) * (100% - 60px))',
|
||||
left: glowLeft,
|
||||
background: 'linear-gradient(90deg, transparent, var(--color-brand-primary), transparent)',
|
||||
filter: 'blur(3px)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
animate={{
|
||||
'--glow-x': glowX,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user