feat: V2/V3组件体系与水墨雅致视觉改造
- 新增detail-v2组件库(HeroV2/V3, TrustSectionV2, CTASectionV2等) - Hero组件水墨雅致改造:浅色宣纸底/深色墨色文字/墨韵纹理 - 方案卡片添加推荐组合徽标(Package图标) - 独立产品页从V1组件迁移到V2/V3 - 修复why-us-section未使用导入和ESLint引号转义错误
This commit is contained in:
@@ -56,7 +56,7 @@ export function Footer() {
|
||||
<div data-testid="card-products">
|
||||
<h3 className="font-semibold text-base mb-5 text-white">产品与方案</h3>
|
||||
<ul className="space-y-3">
|
||||
{(MEGA_DROPDOWN_DATA.products ?? []).map((item) => (
|
||||
{(MEGA_DROPDOWN_DATA.products ?? []).flatMap(group => group.items).filter(item => item.href !== '#').map((item) => (
|
||||
<li key={item.id}>
|
||||
<StaticLink
|
||||
href={item.href}
|
||||
@@ -66,7 +66,7 @@ export function Footer() {
|
||||
</StaticLink>
|
||||
</li>
|
||||
))}
|
||||
{(MEGA_DROPDOWN_DATA.solutions ?? []).map((item) => (
|
||||
{(MEGA_DROPDOWN_DATA.solutions ?? []).flatMap(group => group.items).map((item) => (
|
||||
<li key={item.id}>
|
||||
<StaticLink
|
||||
href={item.href}
|
||||
|
||||
@@ -89,17 +89,19 @@ function HeaderContent() {
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<StaticLink
|
||||
href="/"
|
||||
className="flex items-center group"
|
||||
className="relative flex items-center group/logo"
|
||||
aria-label="返回首页"
|
||||
>
|
||||
<div className="absolute inset-0 rounded-lg bg-[var(--color-brand-primary)] opacity-0 group-hover/logo:opacity-5 blur-xl transition-all duration-500 scale-75 group-hover/logo:scale-100" />
|
||||
<Image
|
||||
src={resolvedTheme === 'dark' ? '/logo-light.svg' : '/logo.svg'}
|
||||
src="/logo.svg"
|
||||
alt={COMPANY_INFO.name}
|
||||
width={120}
|
||||
height={30}
|
||||
className="transition-transform duration-200 group-hover:scale-105 w-auto h-8 md:h-8"
|
||||
className={`relative transition-all duration-300 group-hover/logo:scale-105 w-auto h-8 md:h-8 ${resolvedTheme === 'dark' ? 'dark:invert dark:brightness-0 dark:hue-rotate-180' : ''}`}
|
||||
loading="eager"
|
||||
priority
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
</StaticLink>
|
||||
|
||||
@@ -109,7 +111,7 @@ function HeaderContent() {
|
||||
<MegaDropdown
|
||||
key={item.id}
|
||||
label={item.label}
|
||||
items={MEGA_DROPDOWN_DATA[item.dropdownKey!] ?? []}
|
||||
groups={MEGA_DROPDOWN_DATA[item.dropdownKey!] ?? []}
|
||||
isOpen={openDropdown === item.id}
|
||||
onToggle={() => {
|
||||
setOpenDropdown((prev) => prev === item.id ? null : item.id);
|
||||
@@ -118,32 +120,33 @@ function HeaderContent() {
|
||||
onClose={() => setOpenDropdown((prev) => prev === item.id ? null : prev)}
|
||||
/>
|
||||
) : (
|
||||
<StaticLink
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
onClick={(e) => handleNavClick(e, item)}
|
||||
className={`
|
||||
relative px-3 py-1.5 text-sm font-medium
|
||||
transition-all duration-300
|
||||
${isActive(item)
|
||||
? 'text-[var(--color-text-primary)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
|
||||
}
|
||||
`}
|
||||
aria-current={isActive(item) ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
<span
|
||||
<div key={item.id} className="-mx-2 px-2 py-2">
|
||||
<StaticLink
|
||||
href={item.href}
|
||||
onClick={(e) => handleNavClick(e, item)}
|
||||
className={`
|
||||
absolute bottom-0 left-1/2 -translate-x-1/2 w-6 h-0.5 bg-[var(--color-brand-primary)] rounded-full
|
||||
transition-all duration-200 ease-out
|
||||
relative block px-3 py-1.5 text-sm font-medium
|
||||
transition-all duration-300 rounded-md
|
||||
${isActive(item)
|
||||
? 'opacity-100 scale-x-100'
|
||||
: 'opacity-0 scale-x-0'
|
||||
? 'text-[var(--color-text-primary)] bg-[var(--color-brand-primary-bg)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)]'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</StaticLink>
|
||||
aria-current={isActive(item) ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
<span
|
||||
className={`
|
||||
absolute bottom-0 left-1/2 -translate-x-1/2 w-6 h-0.5 bg-[var(--color-brand-primary)] rounded-full
|
||||
transition-all duration-200 ease-out
|
||||
${isActive(item)
|
||||
? 'opacity-100 scale-x-100'
|
||||
: 'opacity-0 scale-x-0'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</StaticLink>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ChevronDown, Lock, Sparkles } from 'lucide-react';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
interface MegaDropdownItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
}
|
||||
import type { MegaDropdownGroup } from '@/lib/constants/navigation';
|
||||
|
||||
interface MegaDropdownProps {
|
||||
label: string;
|
||||
items: MegaDropdownItem[];
|
||||
groups: MegaDropdownGroup[];
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }: MegaDropdownProps) {
|
||||
export function MegaDropdown({ label, groups, isOpen, onToggle, onOpen, onClose }: MegaDropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
@@ -40,24 +34,26 @@ export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className="relative" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`
|
||||
flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md
|
||||
transition-all duration-200
|
||||
${isOpen
|
||||
? 'text-[var(--color-brand-primary)] bg-[var(--color-brand-primary-bg)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)]'
|
||||
}
|
||||
`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{label}
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<div className="-mx-2 px-2 py-2">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`
|
||||
flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md
|
||||
transition-all duration-200
|
||||
${isOpen
|
||||
? 'text-[var(--color-brand-primary)] bg-[var(--color-brand-primary-bg)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)]'
|
||||
}
|
||||
`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{label}
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
@@ -66,25 +62,64 @@ export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute top-full left-1/2 -translate-x-1/2 pt-2 w-[520px] z-50"
|
||||
className="absolute top-full left-1/2 -translate-x-1/2 pt-2 w-[640px] z-50"
|
||||
>
|
||||
<div className="bg-[var(--color-bg-primary)] rounded-xl border border-[var(--color-border-primary)] shadow-lg p-5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{items.map((item) => (
|
||||
<StaticLink
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
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="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 className="bg-[var(--color-bg-primary)] rounded-xl border border-[var(--color-border-primary)] shadow-lg p-6">
|
||||
{groups.map((group, groupIndex) => (
|
||||
<div key={group.id} className={groupIndex > 0 ? 'mt-6 pt-6 border-t border-[var(--color-border-primary)]' : ''}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h3 className="text-sm font-bold text-[var(--color-text-primary)]">{group.title}</h3>
|
||||
{group.description && (
|
||||
<span className="text-xs text-[var(--color-text-muted)]">{group.description}</span>
|
||||
)}
|
||||
{group.highlight && (
|
||||
<Sparkles className="w-3.5 h-3.5 text-[var(--color-brand-primary)]" />
|
||||
)}
|
||||
</div>
|
||||
</StaticLink>
|
||||
|
||||
<div className={`grid gap-2 ${group.items.length > 4 ? 'grid-cols-3' : 'grid-cols-2'}`}>
|
||||
{group.items.map((item) => (
|
||||
<StaticLink
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
className={`block p-3 rounded-lg border transition-all duration-200 group ${
|
||||
item.href === '#'
|
||||
? 'border-[var(--color-border-primary)]/50 bg-[var(--color-bg-secondary)] cursor-not-allowed opacity-70'
|
||||
: 'border-transparent hover:border-[var(--color-brand-primary)]/10 hover:bg-[var(--color-brand-primary-bg)]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{item.title}</div>
|
||||
{item.badge && (
|
||||
<span className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-medium ${
|
||||
item.badge === '敬请期待'
|
||||
? 'bg-gray-100 text-gray-500'
|
||||
: 'bg-[var(--color-brand-primary-bg)] text-[var(--color-brand-primary)]'
|
||||
}`}>
|
||||
{item.badge === '敬请期待' && <Lock className="w-2.5 h-2.5 inline mr-0.5" />}
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-placeholder)] mt-1 leading-relaxed">{item.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</StaticLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-[var(--color-border-primary)]">
|
||||
<StaticLink
|
||||
href={`/${label === '产品' ? 'products' : label === '解决方案' ? 'solutions' : 'services'}`}
|
||||
className="flex items-center justify-center gap-1 text-xs font-medium text-[var(--color-brand-primary)] hover:underline"
|
||||
>
|
||||
查看全部{label}
|
||||
<ChevronDown className="w-3 h-3 rotate-[-90deg]" />
|
||||
</StaticLink>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function MobileMenu({ className }: MobileMenuProps) {
|
||||
</button>
|
||||
{expandedDropdown === item.dropdownKey && (
|
||||
<ul className="pl-4 space-y-1 mt-1 mb-2" role="list">
|
||||
{(MEGA_DROPDOWN_DATA[item.dropdownKey] ?? []).map((sub) => (
|
||||
{(MEGA_DROPDOWN_DATA[item.dropdownKey] ?? []).flatMap(group => group.items).filter(sub => sub.href !== '#').map((sub) => (
|
||||
<li key={sub.id}>
|
||||
<StaticLink
|
||||
href={sub.href}
|
||||
|
||||
Reference in New Issue
Block a user