refactor(link): 营销页站内链接统一 StaticLink(静态托管适配)+ 删除 0 引用死代码组件 + QA 页 client/server 拆分
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ScrollReveal } from '@/components/ui/scroll-reveal';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { BrandCalligraphyName } from '@/components/ui/brand-calligraphy-name';
|
||||
import {
|
||||
ArrowRight,
|
||||
@@ -355,20 +356,20 @@ export default function BrandContent() {
|
||||
从一次深入的业务诊断开始,开启您的数据驱动之旅。
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px]"
|
||||
>
|
||||
预约咨询
|
||||
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/cases"
|
||||
className="group inline-flex items-center justify-center gap-2 px-10 py-5 font-semibold text-base text-dark-text-primary border border-dark-border hover:border-dark-text-muted transition-all duration-300 min-h-[52px]"
|
||||
>
|
||||
查看客户案例
|
||||
<ChevronRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Building2, ArrowUpRight, Target, Lightbulb, Filter } from 'lucide-react';
|
||||
import {
|
||||
type CaseStudy,
|
||||
@@ -308,19 +309,19 @@ export default function CasesContentV3({
|
||||
让我们聊聊您的挑战,看看能创造什么样的成果。
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
预约咨询
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/services"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-white transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
了解服务
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Image from 'next/image';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { HeroProductVisual } from '@/components/sections/hero-product-visual';
|
||||
import { ArrowRight, Lightbulb, Database, Layers, Code, Puzzle, Server } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -335,13 +336,13 @@ function TrustSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex shrink-0 items-center gap-2 text-sm font-semibold text-ink hover:text-brand transition-colors"
|
||||
>
|
||||
<span>{earlyCtaLabel}</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</StaggerReveal>
|
||||
</div>
|
||||
@@ -398,7 +399,10 @@ function NarrativeSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
|
||||
<StaggerReveal key={act.number} staggerDelay={0.08}>
|
||||
<div className="group relative h-full bg-white p-8 sm:p-10 md:p-12 transition-colors duration-300 hover:bg-bg-secondary">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<span className="text-4xl sm:text-5xl font-black text-text-hint tabular-nums leading-none">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="text-4xl sm:text-5xl font-black text-text-hint tabular-nums leading-none"
|
||||
>
|
||||
{act.number}
|
||||
</span>
|
||||
</div>
|
||||
@@ -524,13 +528,13 @@ function CasesSection({ cases, pageCopy }: { cases?: Record<string, any>[]; page
|
||||
<p className="text-text-secondary leading-relaxed text-lg mb-8">
|
||||
{casesDesc}
|
||||
</p>
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center gap-3 text-ink font-semibold text-sm"
|
||||
>
|
||||
成为首批共创客户
|
||||
<ArrowRight className="w-4 h-5 transition-transform duration-300 group-hover:translate-x-2 text-text-secondary group-hover:text-brand" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.15}>
|
||||
<div className="bain-card p-10 sm:p-12 bg-bg-secondary">
|
||||
@@ -556,13 +560,13 @@ function CasesSection({ cases, pageCopy }: { cases?: Record<string, any>[]; page
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.1}>
|
||||
<a
|
||||
<StaticLink
|
||||
href="/cases"
|
||||
className="group inline-flex items-center gap-3 text-ink font-semibold text-sm"
|
||||
>
|
||||
查看全部案例
|
||||
<ArrowRight className="w-4 h-5 transition-transform duration-300 group-hover:translate-x-2 text-text-secondary group-hover:text-brand" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { EASE_OUT } from '@/components/ui/page-decoration';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, CheckCircle2 } from 'lucide-react';
|
||||
@@ -154,13 +155,13 @@ export function MethodologyContent({ data }: MethodologyContentProps) {
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
<Button size="xl" asChild>
|
||||
<a href="/services">
|
||||
<StaticLink href="/services">
|
||||
了解我们的服务
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/contact">联系我们</a>
|
||||
<StaticLink href="/contact">联系我们</StaticLink>
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -75,7 +75,7 @@ function NewsCard({ newsItem, index }: { newsItem: NewsItem; index: number }) {
|
||||
}
|
||||
|
||||
export default function NewsContentV3({ news, pageCopy }: { news?: NewsItem[]; pageCopy?: Record<string, unknown> | null }) {
|
||||
const NEWS = news ?? [];
|
||||
const NEWS = useMemo(() => news ?? [], [news]);
|
||||
const [selectedCategory, setSelectedCategory] = useState('全部');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -94,7 +94,7 @@ export default function NewsContentV3({ news, pageCopy }: { news?: NewsItem[]; p
|
||||
newsItem.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
}, [selectedCategory, searchQuery]);
|
||||
}, [selectedCategory, searchQuery, NEWS]);
|
||||
|
||||
const totalPages = Math.ceil(filteredNews.length / ITEMS_PER_PAGE);
|
||||
const paginatedNews = useMemo(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { ScrollReveal } from '@/components/ui/scroll-reveal';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Calendar, ArrowLeft, Newspaper, ArrowRight } from 'lucide-react';
|
||||
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
|
||||
@@ -63,9 +64,9 @@ export default function NewsDetailContentV3({ news, relatedNews = [] }: NewsDeta
|
||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
||||
<ScrollReveal>
|
||||
<nav aria-label="breadcrumb" className="flex items-center gap-2 text-sm text-text-muted mb-8 sm:mb-10">
|
||||
<a href="/news" className="hover:text-brand transition-colors duration-300">
|
||||
<StaticLink href="/news" className="hover:text-brand transition-colors duration-300">
|
||||
新闻动态
|
||||
</a>
|
||||
</StaticLink>
|
||||
<span>/</span>
|
||||
<span className="text-text-secondary line-clamp-1">{news.title}</span>
|
||||
</nav>
|
||||
@@ -145,20 +146,20 @@ export default function NewsDetailContentV3({ news, relatedNews = [] }: NewsDeta
|
||||
{/* Navigation */}
|
||||
<ScrollReveal>
|
||||
<div className="mt-16 sm:mt-20 flex flex-col sm:flex-row justify-center gap-4">
|
||||
<a
|
||||
<StaticLink
|
||||
href="/news"
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-4 font-medium text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回新闻列表
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-2 px-8 py-4 font-medium text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
|
||||
>
|
||||
联系我们
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
@@ -153,10 +154,10 @@ export default function ErpUpgradeContentV2({ item }: ErpUpgradeContentV2Props)
|
||||
<ScrollReveal delay={0.3}>
|
||||
<div className="mt-12 flex flex-wrap gap-4">
|
||||
<Button size="xl" asChild>
|
||||
<a href="/contact">
|
||||
<StaticLink href="/contact">
|
||||
免费诊断咨询
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="#features">了解功能模块</a>
|
||||
@@ -404,13 +405,13 @@ export default function ErpUpgradeContentV2({ item }: ErpUpgradeContentV2Props)
|
||||
|
||||
<div className="flex flex-wrap gap-6 justify-center">
|
||||
<Button size="xl" asChild>
|
||||
<a href="/contact">
|
||||
<StaticLink href="/contact">
|
||||
预约免费诊断
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/products/erp">查看产品详情</a>
|
||||
<StaticLink href="/products/erp">查看产品详情</StaticLink>
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, Package, Check, Settings, Users, FileText, Cpu, Shield, TrendingUp } from 'lucide-react';
|
||||
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
|
||||
@@ -89,13 +90,13 @@ function DetailHero({ product }: DetailHeroProps) {
|
||||
className="flex flex-wrap gap-6"
|
||||
>
|
||||
<Button size="xl" asChild>
|
||||
<a href="/contact">
|
||||
<StaticLink href="/contact">
|
||||
预约体验
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/products">查看更多产品</a>
|
||||
<StaticLink href="/products">查看更多产品</StaticLink>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -403,15 +404,29 @@ function CTASection({ product }: CTASectionProps) {
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
<Button size="xl" asChild>
|
||||
<a href="/contact">
|
||||
<StaticLink href="/contact">
|
||||
预约演示
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/products">查看更多产品</a>
|
||||
<StaticLink href="/products">查看更多产品</StaticLink>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 站内入口:ERP 升级专题(与 erp-upgrade-content-v2 的反向链接形成互链闭环) */}
|
||||
{product.id === 'erp' && (
|
||||
<div className="mt-10 inline-flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-sm text-text-secondary">
|
||||
正在使用旧版 ERP?
|
||||
<StaticLink
|
||||
href="/products/erp-upgrade"
|
||||
className="inline-flex items-center gap-1 font-medium text-brand hover:underline"
|
||||
>
|
||||
了解睿新 ERP 升级专题
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</StaticLink>
|
||||
</div>
|
||||
)}
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -136,13 +136,13 @@ function HeroSection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||
className="flex flex-wrap gap-6"
|
||||
>
|
||||
<Button size="xl" asChild>
|
||||
<a href="/services">
|
||||
<StaticLink href="/services">
|
||||
{ctaPrimary}
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/solutions">{ctaSecondary}</a>
|
||||
<StaticLink href="/solutions">{ctaSecondary}</StaticLink>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
@@ -425,7 +425,7 @@ function SuiteCombosSection({ products }: { products: Product[] }) {
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<a
|
||||
<StaticLink
|
||||
key={idx}
|
||||
href="/solutions"
|
||||
className="group p-10 lg:p-12 bg-white hover:bg-bg-secondary transition-all duration-500 overflow-hidden"
|
||||
@@ -465,7 +465,7 @@ function SuiteCombosSection({ products }: { products: Product[] }) {
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</a>
|
||||
</StaticLink>
|
||||
);
|
||||
})}
|
||||
</StaggerReveal>
|
||||
@@ -498,13 +498,13 @@ function CTASection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
<Button size="xl" asChild>
|
||||
<a href="/services">
|
||||
<StaticLink href="/services">
|
||||
了解我们的服务
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/contact">联系我们</a>
|
||||
<StaticLink href="/contact">联系我们</StaticLink>
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { getHeroTheme } from '@/lib/constants/hero-themes';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { DetailHero } from '@/components/detail/detail-hero';
|
||||
import { DetailTrustSection } from '@/components/detail/detail-trust-section';
|
||||
import { DetailCTASection } from '@/components/detail/detail-cta-section';
|
||||
@@ -112,19 +113,19 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-brand px-8 py-3 text-sm font-semibold text-white shadow-md transition-all hover:bg-brand/90 hover:shadow-lg"
|
||||
>
|
||||
获取上线通知
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/products"
|
||||
className="inline-flex items-center rounded-lg border border-border-primary px-6 py-3 text-sm font-semibold text-text-secondary transition-all hover:border-border-secondary hover:bg-bg-secondary"
|
||||
>
|
||||
查看其他产品
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { ArrowUpRight, CheckCircle2, Zap, Clock, Users, Award, TrendingUp, Shield } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import type { Service, CaseStudy } from '@/lib/constants/services';
|
||||
@@ -80,13 +81,13 @@ function DetailHero({ service }: { service: Service }) {
|
||||
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
|
||||
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
|
||||
>
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
免费咨询
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
<a
|
||||
href="#cases"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||
@@ -336,19 +337,19 @@ function CTASection() {
|
||||
首次咨询免费,没有任何销售压力。
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
预约免费咨询
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/services"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
返回服务列表
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -126,19 +126,19 @@ function HeroSection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
|
||||
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
|
||||
>
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
{ctaPrimary}
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/products"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
{ctaSecondary}
|
||||
</a>
|
||||
</StaticLink>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
@@ -364,19 +364,19 @@ function CTASection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||
))}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
预约免费咨询
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/team"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
了解我们的团队
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, Factory, ShoppingCart, Heart, GraduationCap, Lightbulb, Settings, Users, TrendingUp, Package, BookOpen, MessageSquare, Calendar, Smartphone, Route, Shield, Truck } from 'lucide-react';import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
|
||||
import type { Solution } from '@/lib/constants/solutions';
|
||||
@@ -83,13 +84,13 @@ function DetailHero({ solution }: DetailHeroProps) {
|
||||
className="flex flex-wrap gap-6"
|
||||
>
|
||||
<Button size="xl" asChild>
|
||||
<a href="/contact">
|
||||
<StaticLink href="/contact">
|
||||
获取方案详情
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/products">查看产品</a>
|
||||
<StaticLink href="/products">查看产品</StaticLink>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -491,13 +492,13 @@ function CTASection({ solution }: CTASectionProps) {
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
<Button size="xl" asChild>
|
||||
<a href="/contact">
|
||||
<StaticLink href="/contact">
|
||||
预约咨询
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</a>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button size="xl" variant="outline" asChild>
|
||||
<a href="/solutions">查看更多方案</a>
|
||||
<StaticLink href="/solutions">查看更多方案</StaticLink>
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -74,19 +74,19 @@ function HeroSection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
|
||||
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
|
||||
>
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
{ctaPrimary}
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/products"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
{ctaSecondary}
|
||||
</a>
|
||||
</StaticLink>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
@@ -240,19 +240,19 @@ function CTASection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||
{ctaDescription}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<a
|
||||
<StaticLink
|
||||
href="/contact"
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
获取定制方案
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
<a
|
||||
</StaticLink>
|
||||
<StaticLink
|
||||
href="/products"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||
>
|
||||
浏览产品
|
||||
</a>
|
||||
</StaticLink>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { trackError } from '@/lib/analytics';
|
||||
|
||||
export default function TestErrorTrackingClient() {
|
||||
const [testResults, setTestResults] = useState<string[]>([]);
|
||||
|
||||
const addResult = (message: string) => {
|
||||
setTestResults((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
|
||||
};
|
||||
|
||||
const testJavaScriptError = () => {
|
||||
try {
|
||||
addResult('🧪 测试 1: 触发 JavaScript 运行时错误...');
|
||||
throw new Error('测试错误:这是一个故意的 JavaScript 错误');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
trackError('javascript_error', errorMessage, false, {
|
||||
test_id: 'test_js_error',
|
||||
filename: 'test-error-page.tsx',
|
||||
lineno: 20,
|
||||
});
|
||||
addResult('✅ JavaScript 错误已发送到 GA4');
|
||||
}
|
||||
};
|
||||
|
||||
const testPromiseRejection = () => {
|
||||
addResult('🧪 测试 2: 触发 Promise 未捕获异常...');
|
||||
Promise.reject(new Error('测试错误:Promise 故意拒绝'));
|
||||
addResult('✅ Promise 异常已触发(由 GlobalErrorTracker 自动捕获)');
|
||||
};
|
||||
|
||||
const testReactError = () => {
|
||||
addResult('🧪 测试 3: 触发 React 渲染错误...');
|
||||
trackError('react_error', '组件渲染失败:测试故意错误', true, {
|
||||
component: 'TestComponent',
|
||||
stack_trace: 'Error: 测试错误\n at TestComponent...',
|
||||
});
|
||||
addResult('✅ React 错误已发送到 GA4(标记为 fatal)');
|
||||
};
|
||||
|
||||
const testNetworkError = () => {
|
||||
addResult('🧪 测试 4: 模拟网络请求错误...');
|
||||
trackError('network_error', 'Failed to fetch: https://api.example.com/data', false, {
|
||||
url: 'https://api.example.com/data',
|
||||
status_code: 0,
|
||||
request_method: 'GET',
|
||||
});
|
||||
addResult('✅ 网络错误已发送到 GA4');
|
||||
};
|
||||
|
||||
const clearResults = () => {
|
||||
setTestResults([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-8">
|
||||
🧪 GA4 错误监控测试面板
|
||||
</h1>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
||||
测试用例
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={testJavaScriptError}
|
||||
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
📛 测试 JavaScript 错误
|
||||
</button>
|
||||
<button
|
||||
onClick={testPromiseRejection}
|
||||
className="px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
⚠️ 测试 Promise 异常
|
||||
</button>
|
||||
<button
|
||||
onClick={testReactError}
|
||||
className="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
>
|
||||
💥 测试 React 致命错误
|
||||
</button>
|
||||
<button
|
||||
onClick={testNetworkError}
|
||||
className="px-6 py-3 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
|
||||
>
|
||||
🌐 测试网络错误
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={clearResults}
|
||||
className="mt-4 px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
🗑️ 清除日志
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{testResults.length > 0 && (
|
||||
<div className="bg-gray-900 rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold text-green-400 mb-4">
|
||||
📋 测试日志
|
||||
</h2>
|
||||
<div className="space-y-2 font-mono text-sm">
|
||||
{testResults.map((result, index) => (
|
||||
<div key={index} className="text-green-300">
|
||||
{result}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-yellow-800 mb-2">
|
||||
💡 如何验证错误是否成功发送到 GA4?
|
||||
</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-yellow-700">
|
||||
<li>点击上面的测试按钮</li>
|
||||
<li>
|
||||
打开{' '}
|
||||
<a
|
||||
href="https://analytics.google.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline font-semibold"
|
||||
>
|
||||
Google Analytics 后台
|
||||
</a>
|
||||
</li>
|
||||
<li>导航到:报告 → 实时</li>
|
||||
<li>在事件搜索框输入 <code className="bg-yellow-100 px-1">exception</code></li>
|
||||
<li>你应该能在 30 秒内看到错误事件出现!</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-blue-800 mb-2">
|
||||
🔧 开发者工具调试
|
||||
</h3>
|
||||
<p className="text-blue-700 mb-2">
|
||||
在开发模式下,打开浏览器控制台(F12),你会看到类似这样的日志:
|
||||
</p>
|
||||
<pre className="bg-blue-900 text-green-300 p-4 rounded-lg overflow-x-auto text-sm">
|
||||
{`[GA4] Error tracked: {
|
||||
description: "[javascript_error] 测试错误:这是一个故意的 JavaScript 错误",
|
||||
fatal: "false",
|
||||
url: "http://localhost:3000/test-error-tracking",
|
||||
timestamp: "2025-01-15T10:30:00.000Z",
|
||||
test_id: "test_js_error",
|
||||
filename: "test-error-page.tsx",
|
||||
lineno: 20
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,163 +1,12 @@
|
||||
'use client';
|
||||
import { Metadata } from 'next';
|
||||
import TestErrorTrackingClient from './page-client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { trackError } from '@/lib/analytics';
|
||||
// QA 内部测试页:禁止搜索引擎收录(robots.txt 未屏蔽,需 metadata 层 noindex)
|
||||
export const metadata: Metadata = {
|
||||
title: '错误监控测试面板(内部)',
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function TestErrorTrackingPage() {
|
||||
const [testResults, setTestResults] = useState<string[]>([]);
|
||||
|
||||
const addResult = (message: string) => {
|
||||
setTestResults((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
|
||||
};
|
||||
|
||||
const testJavaScriptError = () => {
|
||||
try {
|
||||
addResult('🧪 测试 1: 触发 JavaScript 运行时错误...');
|
||||
throw new Error('测试错误:这是一个故意的 JavaScript 错误');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
trackError('javascript_error', errorMessage, false, {
|
||||
test_id: 'test_js_error',
|
||||
filename: 'test-error-page.tsx',
|
||||
lineno: 20,
|
||||
});
|
||||
addResult('✅ JavaScript 错误已发送到 GA4');
|
||||
}
|
||||
};
|
||||
|
||||
const testPromiseRejection = () => {
|
||||
addResult('🧪 测试 2: 触发 Promise 未捕获异常...');
|
||||
Promise.reject(new Error('测试错误:Promise 故意拒绝'));
|
||||
addResult('✅ Promise 异常已触发(由 GlobalErrorTracker 自动捕获)');
|
||||
};
|
||||
|
||||
const testReactError = () => {
|
||||
addResult('🧪 测试 3: 触发 React 渲染错误...');
|
||||
trackError('react_error', '组件渲染失败:测试故意错误', true, {
|
||||
component: 'TestComponent',
|
||||
stack_trace: 'Error: 测试错误\n at TestComponent...',
|
||||
});
|
||||
addResult('✅ React 错误已发送到 GA4(标记为 fatal)');
|
||||
};
|
||||
|
||||
const testNetworkError = () => {
|
||||
addResult('🧪 测试 4: 模拟网络请求错误...');
|
||||
trackError('network_error', 'Failed to fetch: https://api.example.com/data', false, {
|
||||
url: 'https://api.example.com/data',
|
||||
status_code: 0,
|
||||
request_method: 'GET',
|
||||
});
|
||||
addResult('✅ 网络错误已发送到 GA4');
|
||||
};
|
||||
|
||||
const clearResults = () => {
|
||||
setTestResults([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-8">
|
||||
🧪 GA4 错误监控测试面板
|
||||
</h1>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
||||
测试用例
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={testJavaScriptError}
|
||||
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
📛 测试 JavaScript 错误
|
||||
</button>
|
||||
<button
|
||||
onClick={testPromiseRejection}
|
||||
className="px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
⚠️ 测试 Promise 异常
|
||||
</button>
|
||||
<button
|
||||
onClick={testReactError}
|
||||
className="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
>
|
||||
💥 测试 React 致命错误
|
||||
</button>
|
||||
<button
|
||||
onClick={testNetworkError}
|
||||
className="px-6 py-3 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
|
||||
>
|
||||
🌐 测试网络错误
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={clearResults}
|
||||
className="mt-4 px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
🗑️ 清除日志
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{testResults.length > 0 && (
|
||||
<div className="bg-gray-900 rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold text-green-400 mb-4">
|
||||
📋 测试日志
|
||||
</h2>
|
||||
<div className="space-y-2 font-mono text-sm">
|
||||
{testResults.map((result, index) => (
|
||||
<div key={index} className="text-green-300">
|
||||
{result}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-yellow-800 mb-2">
|
||||
💡 如何验证错误是否成功发送到 GA4?
|
||||
</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-yellow-700">
|
||||
<li>点击上面的测试按钮</li>
|
||||
<li>
|
||||
打开{' '}
|
||||
<a
|
||||
href="https://analytics.google.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline font-semibold"
|
||||
>
|
||||
Google Analytics 后台
|
||||
</a>
|
||||
</li>
|
||||
<li>导航到:报告 → 实时</li>
|
||||
<li>在事件搜索框输入 <code className="bg-yellow-100 px-1">exception</code></li>
|
||||
<li>你应该能在 30 秒内看到错误事件出现!</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-blue-800 mb-2">
|
||||
🔧 开发者工具调试
|
||||
</h3>
|
||||
<p className="text-blue-700 mb-2">
|
||||
在开发模式下,打开浏览器控制台(F12),你会看到类似这样的日志:
|
||||
</p>
|
||||
<pre className="bg-blue-900 text-green-300 p-4 rounded-lg overflow-x-auto text-sm">
|
||||
{`[GA4] Error tracked: {
|
||||
description: "[javascript_error] 测试错误:这是一个故意的 JavaScript 错误",
|
||||
fatal: "false",
|
||||
url: "http://localhost:3000/test-error-tracking",
|
||||
timestamp: "2025-01-15T10:30:00.000Z",
|
||||
test_id: "test_js_error",
|
||||
filename: "test-error-page.tsx",
|
||||
lineno: 20
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <TestErrorTrackingClient />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 引用)
|
||||
@@ -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 删除。
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user