feat: 更新营销页面组件与交互优化
- 删除 .impeccable.md - 新增 detail-swipe-nav, loading-state, skeleton, tooltip 组件 - 新增 use-keyboard-shortcuts, use-swipe-gesture hooks - 更新 contact, news, products, services, solutions 等页面 - 优化 header, mobile-menu, input, page-transition 组件 - 添加 terms 与错误追踪测试页面
This commit is contained in:
@@ -9,11 +9,12 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Toast } from '@/components/ui/toast';
|
||||
import { Mail, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2 } from 'lucide-react';
|
||||
import { Mail, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2, HelpCircle } from 'lucide-react';
|
||||
import { COMPANY_INFO } from '@/lib/constants';
|
||||
import { PageNav } from '@/components/layout/page-nav';
|
||||
import { trackContactForm, trackConversion } from '@/lib/analytics';
|
||||
import { BreadcrumbSchema } from '@/components/seo/structured-data';
|
||||
import { Tooltip } from '@/components/ui/tooltip';
|
||||
|
||||
const contactFormSchema = z.object({
|
||||
name: z.string().min(2, '姓名至少需要2个字符'),
|
||||
@@ -273,10 +274,18 @@ function ContactFormContent() {
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<input type="text" name="website" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" aria-hidden="true" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="relative">
|
||||
<Input
|
||||
name="name"
|
||||
data-testid="name-input"
|
||||
label="姓名"
|
||||
label={
|
||||
<span className="flex items-center gap-1.5">
|
||||
姓名
|
||||
<Tooltip content="用于正式沟通和方案报价">
|
||||
<HelpCircle className="w-3.5 h-3.5 text-[var(--color-text-subtle)] hover:text-[var(--color-brand-primary)] cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
id="name"
|
||||
placeholder="请输入您的姓名"
|
||||
required
|
||||
@@ -285,13 +294,22 @@ function ContactFormContent() {
|
||||
onBlur={(e) => handleBlur('name', e.target.value)}
|
||||
error={errors.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
name="phone"
|
||||
data-testid="phone-input"
|
||||
label="电话"
|
||||
label={
|
||||
<span className="flex items-center gap-1.5">
|
||||
电话
|
||||
<Tooltip content="接收项目进度通知和验证码,仅用于业务联系">
|
||||
<HelpCircle className="w-3.5 h-3.5 text-[var(--color-text-subtle)] hover:text-[var(--color-brand-primary)] cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="请输入您的电话"
|
||||
placeholder="请输入11位手机号"
|
||||
required
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleChange('phone', e.target.value)}
|
||||
@@ -299,6 +317,7 @@ function ContactFormContent() {
|
||||
error={errors.phone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
name="email"
|
||||
data-testid="email-input"
|
||||
@@ -341,14 +360,15 @@ function ContactFormContent() {
|
||||
type="submit"
|
||||
data-testid="submit-button"
|
||||
size="lg"
|
||||
className="w-full bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white min-h-13 md:min-h-0"
|
||||
className="w-full bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white min-h-13 md:min-h-0 relative overflow-hidden disabled:opacity-90"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
发送中...
|
||||
</>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>发送中...</span>
|
||||
<span className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ErrorBoundary } from '@/components/ui/error-boundary';
|
||||
import { Header } from '@/components/layout/header';
|
||||
import { Footer } from '@/components/layout/footer';
|
||||
import { PageTransition } from '@/components/ui/page-transition';
|
||||
|
||||
export default function MarketingLayout({
|
||||
children,
|
||||
@@ -11,10 +12,19 @@ export default function MarketingLayout({
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<a
|
||||
href="#main-content"
|
||||
data-skip-to-content
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[100] focus:px-6 focus:py-3 focus:bg-[var(--color-brand-primary)] focus:text-white focus:rounded-lg focus:shadow-lg focus:outline-none"
|
||||
>
|
||||
跳转到主要内容
|
||||
</a>
|
||||
<Header />
|
||||
<ErrorBoundary>
|
||||
<main id="main-content" className="flex-1 pt-16">
|
||||
{children}
|
||||
<main id="main-content" tabIndex={-1} className="flex-1 pt-16" role="main" aria-label="页面主体内容">
|
||||
<PageTransition>
|
||||
{children}
|
||||
</PageTransition>
|
||||
</main>
|
||||
</ErrorBoundary>
|
||||
<Footer />
|
||||
|
||||
@@ -70,7 +70,7 @@ export function NewsDetailClient({ news }: NewsDetailClientProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-l-4 border-[var(--color-brand-primary)] pl-6 mb-8">
|
||||
<div className="bg-[var(--color-brand-primary-bg)] rounded-xl p-6 mb-8">
|
||||
<p className="text-lg text-[var(--color-text-muted)] leading-relaxed">
|
||||
{news.excerpt}
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, ChangeEvent } from 'react';
|
||||
import { useState, useMemo, ChangeEvent, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import { NEWS, COMPANY_INFO } from '@/lib/constants';
|
||||
@@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { LoadingState } from '@/components/ui/loading-state';
|
||||
import { Search, Calendar, ChevronLeft, ChevronRight, ArrowRight, Newspaper } from 'lucide-react';
|
||||
import { PageNav } from '@/components/layout/page-nav';
|
||||
|
||||
@@ -20,6 +21,12 @@ export default function NewsListPage() {
|
||||
const [selectedCategory, setSelectedCategory] = useState('全部');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsLoading(false), 600);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const filteredNews = useMemo(() => {
|
||||
return NEWS.filter((newsItem) => {
|
||||
@@ -119,8 +126,9 @@ export default function NewsListPage() {
|
||||
<p className="text-lg text-[var(--color-text-muted)]">没有找到相关新闻</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<LoadingState isLoading={isLoading} variant="list">
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{paginatedNews.map((newsItem, index) => (
|
||||
<motion.div
|
||||
key={newsItem.id}
|
||||
@@ -205,6 +213,7 @@ export default function NewsListPage() {
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</LoadingState>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { StaticLink } from '@/components/ui/static-link';
|
||||
import { PRODUCTS, COMPANY_INFO } from '@/lib/constants';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PageNav } from '@/components/layout/page-nav';
|
||||
import { DetailSwipeNav } from '@/components/ui/detail-swipe-nav';
|
||||
import { CheckCircle2, Zap, Target, Layers, ArrowRight, FlaskConical } from 'lucide-react';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
@@ -35,6 +36,7 @@ export default async function ProductDetailPage({ params }: { params: Promise<{
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<DetailSwipeNav type="product" currentId={product.id} />
|
||||
<div className="pt-32 pb-16">
|
||||
<div className="container-wide">
|
||||
<PageNav items={[{ label: '产品', href: '/products' }, { label: product.title }]} />
|
||||
@@ -98,8 +100,9 @@ export default async function ProductDetailPage({ params }: { params: Promise<{
|
||||
{product.benefits.map((benefit, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-3 p-4 rounded-lg border-l-4 border-[var(--color-brand-primary)] bg-[var(--color-bg-section)]"
|
||||
className="flex items-start gap-3 p-4 rounded-lg bg-[var(--color-brand-primary-bg)]"
|
||||
>
|
||||
<div className="w-1.5 h-1.5 bg-[var(--color-brand-primary)] rounded-full mt-2 shrink-0" />
|
||||
<span className="text-[var(--color-text-primary)] font-medium text-sm">{benefit}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PRODUCTS } from '@/lib/constants';
|
||||
import { ProductCard } from '@/components/ui/product-card';
|
||||
import { motion } from 'framer-motion';
|
||||
@@ -7,10 +8,17 @@ import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import { PageNav } from '@/components/layout/page-nav';
|
||||
import { CTASection } from '@/components/sections/cta-section';
|
||||
import { BreadcrumbSchema } from '@/components/seo/structured-data';
|
||||
import { LoadingState } from '@/components/ui/loading-state';
|
||||
|
||||
export default function ProductsPage() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const fadeUp = shouldReduceMotion ? {} : { initial: { opacity: 0, y: 20 } };
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsLoading(false), 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<BreadcrumbSchema items={[{ name: '首页', href: '/' }, { name: '产品', href: '/products' }]} />
|
||||
@@ -36,18 +44,20 @@ export default function ProductsPage() {
|
||||
|
||||
<section className="pb-20">
|
||||
<div className="container-wide">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{PRODUCTS.map((product, index) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
title={product.title}
|
||||
description={product.description}
|
||||
href={`/products/${product.id}`}
|
||||
index={index}
|
||||
status={product.status}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<LoadingState isLoading={isLoading} variant="card">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{PRODUCTS.map((product, index) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
title={product.title}
|
||||
description={product.description}
|
||||
href={`/products/${product.id}`}
|
||||
index={index}
|
||||
status={product.status}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</LoadingState>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PageNav } from '@/components/layout/page-nav';
|
||||
import { DetailSwipeNav } from '@/components/ui/detail-swipe-nav';
|
||||
import {
|
||||
CheckCircle2,
|
||||
TrendingUp,
|
||||
@@ -73,6 +74,7 @@ export function ServiceDetailClient({ service }: ServiceDetailClientProps) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<DetailSwipeNav type="service" currentId={service.id} />
|
||||
<div className="pt-32 pb-16">
|
||||
<div className="container-wide">
|
||||
<PageNav items={[{ label: '服务', href: '/services' }, { label: service.title }]} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { SERVICES } from '@/lib/constants';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -7,10 +8,17 @@ import { ArrowRight, ArrowUpRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import { PageNav } from '@/components/layout/page-nav';
|
||||
import { LoadingState } from '@/components/ui/loading-state';
|
||||
|
||||
export default function ServicesPage() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const fadeUp = shouldReduceMotion ? {} : { initial: { opacity: 0, y: 20 } };
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsLoading(false), 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<section className="pt-32 pb-16">
|
||||
@@ -35,45 +43,47 @@ export default function ServicesPage() {
|
||||
|
||||
<section className="pb-20">
|
||||
<div className="container-wide">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{SERVICES.map((service, index) => (
|
||||
<motion.div
|
||||
key={service.id}
|
||||
{...fadeUp}
|
||||
whileInView={shouldReduceMotion ? {} : { opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<StaticLink
|
||||
href={`/services/${service.id}`}
|
||||
className="group block p-6 rounded-xl border border-[var(--color-border-primary)] bg-[var(--color-bg-primary)] hover:border-[var(--color-brand-primary)]/40 hover:shadow-lg transition-all duration-300 h-full"
|
||||
<LoadingState isLoading={isLoading} variant="card">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{SERVICES.map((service, index) => (
|
||||
<motion.div
|
||||
key={service.id}
|
||||
{...fadeUp}
|
||||
whileInView={shouldReduceMotion ? {} : { opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<span className="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-[var(--color-brand-primary-bg)] text-[var(--color-brand-primary)] text-sm font-bold">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<ArrowUpRight className="w-5 h-5 text-[var(--color-text-muted)] group-hover:text-[var(--color-brand-primary)] transition-colors" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-2 group-hover:text-[var(--color-brand-primary)] transition-colors">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-4">
|
||||
{service.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{service.features.slice(0, 3).map((feature, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex items-center text-xs px-2 py-1 bg-[var(--color-bg-tertiary)] text-[var(--color-text-muted)] rounded"
|
||||
>
|
||||
{feature.split(':')[0]}
|
||||
<StaticLink
|
||||
href={`/services/${service.id}`}
|
||||
className="group block p-6 rounded-xl border border-[var(--color-border-primary)] bg-[var(--color-bg-primary)] hover:border-[var(--color-brand-primary)]/40 hover:shadow-lg transition-all duration-300 h-full"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<span className="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-[var(--color-brand-primary-bg)] text-[var(--color-brand-primary)] text-sm font-bold">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</StaticLink>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
<ArrowUpRight className="w-5 h-5 text-[var(--color-text-muted)] group-hover:text-[var(--color-brand-primary)] transition-colors" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-2 group-hover:text-[var(--color-brand-primary)] transition-colors">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-4">
|
||||
{service.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{service.features.slice(0, 3).map((feature, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex items-center text-xs px-2 py-1 bg-[var(--color-bg-tertiary)] text-[var(--color-text-muted)] rounded"
|
||||
>
|
||||
{feature.split(':')[0]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</StaticLink>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</LoadingState>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -58,8 +58,9 @@ export function SolutionDetailClient({ solution, relatedProducts }: SolutionDeta
|
||||
{solution.challenges.map((challenge, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-3 p-4 rounded-lg border-l-4 border-[var(--color-brand-primary)] bg-[var(--color-bg-section)]"
|
||||
className="flex items-start gap-3 p-4 rounded-lg bg-[var(--color-brand-primary-bg)]"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 text-[var(--color-brand-primary)] mt-0.5 shrink-0" />
|
||||
<span className="text-[var(--color-text-primary)] text-sm">{challenge}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { ArrowRight, Lightbulb, Cpu, Users, CheckCircle2 } from 'lucide-react';
|
||||
import { ArrowRight, Lightbulb, Cpu, Users, CheckCircle2, ChevronDown } from 'lucide-react';
|
||||
import { MethodologySection } from '@/components/sections/methodology-section';
|
||||
import { CTASection } from '@/components/sections/cta-section';
|
||||
import { PageNav } from '@/components/layout/page-nav';
|
||||
import { BreadcrumbSchema } from '@/components/seo/structured-data';
|
||||
import { SwipeNavigation } from '@/hooks/use-swipe-gesture';
|
||||
|
||||
const modules = [
|
||||
{
|
||||
icon: Lightbulb,
|
||||
title: '数字化转型咨询 · 参谋伙伴',
|
||||
subtitle: '帮您看清前路,迈对第一步',
|
||||
title: '数字化转型咨询',
|
||||
subtitle: '参谋伙伴 — 帮您看清前路,迈对第一步',
|
||||
paragraphs: [
|
||||
'数字化转型最大的成本,是走错方向的成本。',
|
||||
'我们用行业智慧帮您洞察趋势,用理性分析帮您避开陷阱。',
|
||||
@@ -28,8 +30,8 @@ const modules = [
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: '信息技术解决方案 · 技术伙伴',
|
||||
subtitle: '让技术真正为业务服务',
|
||||
title: '信息技术解决方案',
|
||||
subtitle: '技术伙伴 — 让技术真正为业务服务',
|
||||
paragraphs: [
|
||||
'我们不追逐"最火"的技术,只选择"最对"的技术。',
|
||||
'将前沿技术深度融入您的业务场景,让每一行代码都产生业务价值。',
|
||||
@@ -44,8 +46,8 @@ const modules = [
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: '长期陪跑服务 · 同行伙伴',
|
||||
subtitle: '从需求理解到产品落地,陪伴才是常态',
|
||||
title: '长期陪跑服务',
|
||||
subtitle: '同行伙伴 — 从需求到落地,持续陪伴',
|
||||
paragraphs: [
|
||||
'当产品真正为您所用那天,才是我们成为伙伴的开始。',
|
||||
'我们建立长效服务机制,定期回访、持续优化、随时响应。',
|
||||
@@ -63,9 +65,28 @@ const modules = [
|
||||
export default function SolutionsPage() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const fadeUp = shouldReduceMotion ? {} : { initial: { opacity: 0, y: 20 } };
|
||||
const [expandedModules, setExpandedModules] = useState<Set<number>>(new Set([0]));
|
||||
|
||||
const toggleModule = (index: number) => {
|
||||
setExpandedModules(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)]">
|
||||
<BreadcrumbSchema items={[{ name: '首页', href: '/' }, { name: '解决方案', href: '/solutions' }]} />
|
||||
<SwipeNavigation
|
||||
prevRoute="/products"
|
||||
nextRoute="/services"
|
||||
prevLabel="产品方案"
|
||||
nextLabel="服务支持"
|
||||
/>
|
||||
<section className="pt-32 pb-16">
|
||||
<div className="container-wide">
|
||||
<PageNav items={[{ label: '解决方案' }]} />
|
||||
@@ -99,7 +120,7 @@ export default function SolutionsPage() {
|
||||
whileInView={shouldReduceMotion ? {} : { opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className={`p-8 md:p-12 rounded-xl border border-[var(--color-border-primary)] border-l-4 ${module.accentBorder} bg-[var(--color-bg-primary)] hover:border-[var(--color-border-primary)] transition-colors`}
|
||||
className={`p-8 md:p-12 rounded-xl border border-[var(--color-border-primary)] bg-[var(--color-bg-primary)] hover:border-[var(--color-border-primary)] transition-colors`}
|
||||
>
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center shrink-0" style={{ backgroundColor: module.accentBg }}>
|
||||
@@ -114,9 +135,29 @@ export default function SolutionsPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-8">
|
||||
{module.paragraphs.map((p, i) => (
|
||||
<p key={i} className="text-[var(--color-text-primary)] leading-relaxed">{p}</p>
|
||||
))}
|
||||
<button
|
||||
onClick={() => toggleModule(index)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-[var(--color-brand-primary)] hover:text-[var(--color-brand-primary-hover)] transition-colors group"
|
||||
aria-expanded={expandedModules.has(index)}
|
||||
>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${expandedModules.has(index) ? 'rotate-180' : ''}`} />
|
||||
{expandedModules.has(index) ? '收起详情' : '展开了解详情'}
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{expandedModules.has(index) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
{module.paragraphs.map((p, i) => (
|
||||
<p key={i} className="text-[var(--color-text-primary)] leading-relaxed mb-3 last:mb-0">{p}</p>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
|
||||
@@ -169,9 +169,12 @@ export default function TermsOfServicePage() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="bg-[var(--color-warning-bg)] p-6 rounded-lg border-l-4 border-[var(--color-brand-primary)]">
|
||||
<p className="text-[var(--color-text-primary)] font-medium mb-2">最后更新日期</p>
|
||||
<p className="text-[var(--color-text-muted)]">2026年2月26日</p>
|
||||
<section className="bg-[var(--color-warning-bg)] p-6 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-2 h-2 bg-[var(--color-brand-primary)] rounded-full" />
|
||||
<p className="text-[var(--color-text-primary)] font-medium">最后更新日期</p>
|
||||
</div>
|
||||
<p className="text-[var(--color-text-muted)] pl-4">2026年2月26日</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,8 @@ export default function TestErrorTrackingPage() {
|
||||
addResult('🧪 测试 1: 触发 JavaScript 运行时错误...');
|
||||
throw new Error('测试错误:这是一个故意的 JavaScript 错误');
|
||||
} catch (error) {
|
||||
trackError('javascript_error', error.message, false, {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user