refactor(link): 营销页站内链接统一 StaticLink(静态托管适配)+ 删除 0 引用死代码组件 + QA 页 client/server 拆分

This commit is contained in:
2026-08-31 12:03:12 +08:00
parent 77b455c04e
commit 08a020105f
26 changed files with 297 additions and 571 deletions
-145
View File
@@ -1,145 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { type LucideIcon } from 'lucide-react';
import type { ReactNode } from 'react';
interface BrandSealProps {
text?: string;
variant?: 'red' | 'gold' | 'blue';
size?: 'sm' | 'md' | 'lg';
}
export function BrandSeal({ text = '旗舰', variant = 'red', size = 'md' }: BrandSealProps) {
const sizes = {
sm: 'w-14 h-14 text-xs',
md: 'w-20 h-20 text-sm',
lg: 'w-28 h-28 text-base',
};
const colors = {
red: {
border: '#C41E3A',
bg: 'rgba(196, 30, 58, 0.06)',
text: '#C41E3A',
shadow: 'rgba(196, 30, 58, 0.2)',
},
gold: {
border: '#d97706',
bg: 'rgba(217, 119, 6, 0.06)',
text: '#d97706',
shadow: 'rgba(217, 119, 6, 0.2)',
},
blue: {
border: '#2563eb',
bg: 'rgba(37, 99, 235, 0.06)',
text: '#2563eb',
shadow: 'rgba(37, 99, 235, 0.2)',
},
};
const color = colors[variant];
return (
<motion.div
initial={{ opacity: 0, scale: 0.5, rotate: -15 }}
whileInView={{ opacity: 1, scale: 1, rotate: -12 }}
viewport={{ once: true }}
transition={{
duration: 0.8,
delay: 0.5,
ease: [0.34, 1.56, 0.64, 1] as const,
}}
className={`relative ${sizes[size]} flex items-center justify-center rounded-lg border-2 font-bold select-none`}
style={{
borderColor: color.border,
backgroundColor: color.bg,
color: color.text,
boxShadow: `0 4px 16px ${color.shadow}`,
fontFamily: "'Noto Serif SC', serif",
}}
>
<div className="absolute inset-[4px] border border-current opacity-20 rounded-md" />
<span
className="relative z-10 tracking-widest"
style={{ writingMode: 'vertical-rl' }}
>
{text}
</span>
<div
className="absolute inset-0 rounded-lg opacity-0 hover:opacity-100 transition-opacity duration-500"
style={{
background: `conic-gradient(from 0deg, transparent 0%, ${color.border}10 50%, transparent 100%)`,
animation: 'rotate 8s linear infinite',
}}
/>
</motion.div>
);
}
interface CalligraphyTextProps {
children: ReactNode;
className?: string;
as?: 'h1' | 'h2' | 'h3' | 'span';
}
export function CalligraphyText({
children,
className = '',
as: Tag = 'span',
}: CalligraphyTextProps) {
return (
<Tag
className={`font-calligraphy ${className}`}
style={{
fontFamily: "'Noto Serif SC', 'STKaiti', 'KaiTi', serif",
fontWeight: 700,
}}
>
{children}
</Tag>
);
}
interface SectionHeaderProps {
badge?: string;
title: ReactNode;
subtitle?: string;
align?: 'left' | 'center';
icon?: LucideIcon;
}
export function SectionHeader({
badge,
title,
subtitle,
align = 'center',
icon: Icon,
}: SectionHeaderProps) {
return (
<motion.div
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.75, ease: [0.22, 1, 0.36, 1] as const }}
className={`mb-16 ${align === 'center' ? 'text-center max-w-3xl mx-auto' : ''}`}
>
{(badge || Icon) && (
<div className={`inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-red-50 text-brand text-sm font-bold tracking-wider uppercase mb-5 ${
align === 'center' ? '' : ''
}`}>
{Icon && <Icon className="w-4 h-4" />}
{badge}
</div>
)}
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900 mb-4">
{title}
</h2>
{subtitle && (
<p className="text-lg text-gray-600 leading-relaxed">{subtitle}</p>
)}
</motion.div>
);
}
@@ -1,106 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowRight, Package, Lightbulb, Wrench } from 'lucide-react';
import type { CrossReference } from '@/lib/constants/cross-references';
interface CrossRecommendGridProps {
items: CrossReference[];
title?: string;
}
const typeConfig = {
product: {
icon: Package,
label: '产品',
color: '#3b82f6',
bg: '#eff6ff',
},
solution: {
icon: Lightbulb,
label: '方案',
color: '#C41E3A',
bg: '#FEF2F4',
},
service: {
icon: Wrench,
label: '服务',
color: '#7c3aed',
bg: '#f5f3ff',
},
};
export function CrossRecommendGrid({ items, title = '相关推荐' }: CrossRecommendGridProps) {
if (items.length === 0) return null;
return (
<section className="bg-bg-secondary py-16 sm:py-20 md:py-28">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="mb-12"
>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
{title}
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-black text-ink tracking-tight leading-[0.95]">
探索更多可能
</h2>
<p className="text-text-secondary mt-3">
发现与您需求相关的产品、方案和服务
</p>
</motion.div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item, index) => {
const config = typeConfig[item.type];
const Icon = config.icon;
return (
<motion.a
key={item.id}
href={item.href}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.08 }}
className="bain-card flex items-start gap-4 p-5 group"
>
<div
className="flex h-10 w-10 shrink-0 items-center justify-center"
style={{ backgroundColor: config.bg, color: config.color }}
>
<Icon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2">
<span
className="rounded-full px-2 py-0.5 text-xs font-medium"
style={{
color: config.color,
backgroundColor: `${config.color}15`,
}}
>
{config.label}
</span>
<ArrowRight className="h-3 w-3 text-text-muted opacity-0 transition-opacity group-hover:opacity-100" />
</div>
<h3 className="text-sm font-semibold text-ink">{item.title}</h3>
<p className="mt-1 text-xs leading-relaxed text-text-muted">
{item.reason}
</p>
</div>
</motion.a>
);
})}
</div>
</div>
</section>
);
}
+1 -61
View File
@@ -261,12 +261,6 @@ const mockProduct = {
],
};
const mockCrossRefItems = [
{ id: 's1', title: '智能制造方案', type: 'solution' as const, href: '/solutions/s1', reason: 'ERP 是核心组件' },
{ id: 'p2', title: 'CRM 系统', type: 'product' as const, href: '/products/crm', reason: '常与 ERP 一起使用' },
{ id: 'sv1', title: '软件定制开发', type: 'service' as const, href: '/services/sv1', reason: '配套服务' },
];
// ─── Tests ───────────────────────────────────────────────────────────────
describe('DetailHero', () => {
@@ -573,58 +567,4 @@ describe('DetailCTASection', () => {
});
});
describe('CrossRecommendGrid', () => {
it('should render nothing when items is empty', async () => {
const { CrossRecommendGrid } = await import('./detail-cross-recommend');
const { container } = render(<CrossRecommendGrid items={[]} />);
expect(container.innerHTML).toBe('');
});
it('should render default title', async () => {
const { CrossRecommendGrid } = await import('./detail-cross-recommend');
render(<CrossRecommendGrid items={mockCrossRefItems} />);
expect(screen.getByText('相关推荐')).toBeInTheDocument();
});
it('should render custom title', async () => {
const { CrossRecommendGrid } = await import('./detail-cross-recommend');
render(<CrossRecommendGrid items={mockCrossRefItems} title="更多推荐" />);
expect(screen.getByText('更多推荐')).toBeInTheDocument();
});
it('should render all items', async () => {
const { CrossRecommendGrid } = await import('./detail-cross-recommend');
render(<CrossRecommendGrid items={mockCrossRefItems} />);
expect(screen.getByText('智能制造方案')).toBeInTheDocument();
expect(screen.getByText('CRM 系统')).toBeInTheDocument();
expect(screen.getByText('软件定制开发')).toBeInTheDocument();
});
it('should render reason for each item', async () => {
const { CrossRecommendGrid } = await import('./detail-cross-recommend');
render(<CrossRecommendGrid items={mockCrossRefItems} />);
expect(screen.getByText('ERP 是核心组件')).toBeInTheDocument();
expect(screen.getByText('常与 ERP 一起使用')).toBeInTheDocument();
expect(screen.getByText('配套服务')).toBeInTheDocument();
});
it('should render correct type labels', async () => {
const { CrossRecommendGrid } = await import('./detail-cross-recommend');
render(<CrossRecommendGrid items={mockCrossRefItems} />);
// Type labels: product=产品, solution=方案, service=服务
expect(screen.getByText('产品')).toBeInTheDocument();
// There might be multiple "方案" texts in the page, so use getAllByText
const solutionLabels = screen.getAllByText('方案');
expect(solutionLabels.length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('服务')).toBeInTheDocument();
});
it('should render links for items', async () => {
const { CrossRecommendGrid } = await import('./detail-cross-recommend');
render(<CrossRecommendGrid items={mockCrossRefItems} />);
const solutionLink = screen.getByText('智能制造方案').closest('a');
expect(solutionLink).toHaveAttribute('href', '/solutions/s1');
const productLink = screen.getByText('CRM 系统').closest('a');
expect(productLink).toHaveAttribute('href', '/products/crm');
});
});
// 注:CrossRecommendGrid 相关测试已随组件删除(2026-08-31,组件生产 0 引用)
+2 -2
View File
@@ -8,7 +8,6 @@ export { DetailCTASection } from './detail-cta-section';
export { CaseStudyCard } from './detail-case-study';
export { CertificationList } from './detail-certification';
export { CrossRecommendGrid } from './detail-cross-recommend';
export { SolutionValueSection } from './solution-value';
export { ServiceValueSection } from './service-value';
@@ -19,4 +18,5 @@ export { SolutionCard } from './solution-service-card';
export { TiltCard, PressableButton, HoverLink } from './micro-interactions';
export { BrandSeal, CalligraphyText, SectionHeader } from './brand-elements';
// 注:brand-elements.tsx(BrandSeal/CalligraphyText/SectionHeader)与
// detail-cross-recommend.tsx(CrossRecommendGrid)此前 0 消费者,已于 2026-08-31 删除。
+1
View File
@@ -36,6 +36,7 @@ export function ProductCard({ product }: ProductCardProps) {
src={product.image}
alt={product.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover group-hover:scale-105 transition-transform duration-500"
/>
+5 -8
View File
@@ -48,14 +48,11 @@ export function CaseCard({
>
{/* Background image */}
{image && (
<div className="absolute inset-0 transition-opacity duration-500 opacity-[0.02] group-hover:opacity-[0.05]">
<img
src={image}
alt=""
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
<div
aria-hidden="true"
style={{ backgroundImage: `url(${image})` }}
className="absolute inset-0 bg-cover bg-center transition-opacity duration-500 opacity-[0.02] group-hover:opacity-[0.05]"
/>
)}
<div className="relative z-10 flex flex-col flex-1">
+6 -5
View File
@@ -3,6 +3,7 @@
import { motion, useScroll, useTransform } from 'framer-motion';
import { useRef, useState, useEffect } from 'react';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { StaticLink } from '@/components/ui/static-link';
import { SectionHeader } from '@/components/sections/section-header';
import { ServiceCard } from '@/components/sections/service-card';
import { ScrollProgress } from '@/components/ui/scroll-progress';
@@ -175,10 +176,10 @@ function CaseDetailHero({ data }: CaseDetailHeroProps) {
transition={{ duration: 0.8, delay: 0.8, ease: EASE }}
>
<Button size="lg" asChild>
<a href="/contact">
<StaticLink href="/contact">
咨询类似方案
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</StaticLink>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="#solution">查看方案</a>
@@ -512,13 +513,13 @@ function CTASection() {
</p>
<div className="flex flex-col sm:flex-row justify-center gap-4">
<Button size="lg" asChild>
<a href="/contact">
<StaticLink href="/contact">
免费咨询
<ArrowRight className="w-4 h-4 ml-2" />
</a>
</StaticLink>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/cases">查看更多案例</a>
<StaticLink href="/cases">查看更多案例</StaticLink>
</Button>
</div>
</ScrollReveal>
+4 -5
View File
@@ -38,11 +38,10 @@ export function InsightCard({
>
{/* Background image */}
{image && (
<img
src={image}
alt=""
className="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 opacity-10"
loading="lazy"
<div
aria-hidden="true"
style={{ backgroundImage: `url(${image})` }}
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105 opacity-10"
/>
)}
+1 -1
View File
@@ -174,7 +174,7 @@ export function WhyUsSection() {
{tech.abbr}
</span>
</div>
<span className="text-[10px] text-[var(--color-text-hint)]">{tech.name}</span>
<span className="text-[10px] text-[var(--color-text-muted)]">{tech.name}</span>
</div>
))}
</div>
+2 -2
View File
@@ -35,8 +35,8 @@ const defaultConfig = {
const statusConfig = {
'研发中': { bg: 'rgba(var(--color-brand-rgb), 0.06)', text: 'var(--color-brand)', border: 'rgba(var(--color-brand-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)' },
'内测中': { bg: 'rgba(var(--color-warning-rgb), 0.06)', text: 'var(--color-warning-text)', border: 'rgba(var(--color-warning-rgb), 0.12)' },
'已发布': { bg: 'rgba(var(--color-success-rgb), 0.06)', text: 'var(--color-success-text)', border: 'rgba(var(--color-success-rgb), 0.12)' },
} as const;
export function ProductCard({ title, description, href, index, status }: ProductCardProps) {