feat: 添加连锁门店系统首页及核心UI组件
新增首页布局、导航栏、页脚及多个核心UI组件(按钮、卡片、表格等) 添加图片资源、工具函数和样式配置 实现响应式设计和主题支持 包含行业解决方案展示区块
This commit is contained in:
76
components/sections/about-section.tsx
Normal file
76
components/sections/about-section.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import {
|
||||
Zap,
|
||||
ShieldCheck,
|
||||
Layers,
|
||||
BarChart3,
|
||||
Headset,
|
||||
Cloud,
|
||||
} from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
"zap": <Zap className="w-5 h-5" />,
|
||||
"shield-check": <ShieldCheck className="w-5 h-5" />,
|
||||
"layers": <Layers className="w-5 h-5" />,
|
||||
"bar-chart-3": <BarChart3 className="w-5 h-5" />,
|
||||
"headset": <Headset className="w-5 h-5" />,
|
||||
"cloud": <Cloud className="w-5 h-5" />,
|
||||
}
|
||||
|
||||
export function AboutSection() {
|
||||
const { advantages } = homeData
|
||||
|
||||
return (
|
||||
<section id="advantages" className="py-20 lg:py-28 bg-neutral-50/80">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col items-center text-center gap-4 mb-14">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary/30 text-primary bg-primary-light px-4 py-1 text-sm font-medium"
|
||||
>
|
||||
{advantages.sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground text-balance">
|
||||
{advantages.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-base lg:text-lg max-w-2xl text-pretty">
|
||||
{advantages.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 3x2 grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{advantages.items.map((item, index) => (
|
||||
<Card
|
||||
key={item.title}
|
||||
className="group relative border border-border bg-background hover:border-primary/35 hover:shadow-lg hover:shadow-primary/8 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
{/* Top accent line */}
|
||||
<div className="absolute top-0 left-0 right-0 h-0.5 bg-primary scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left" />
|
||||
|
||||
<CardContent className="p-7 flex flex-col gap-4">
|
||||
{/* Icon */}
|
||||
<div className="w-11 h-11 rounded-xl bg-primary/10 text-primary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-all duration-300 shadow-sm">
|
||||
{iconMap[item.icon]}
|
||||
</div>
|
||||
|
||||
{/* Index number */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="text-base font-bold text-foreground leading-snug">{item.title}</h3>
|
||||
<span className="text-2xl font-black text-border group-hover:text-primary/20 transition-colors shrink-0 leading-none mt-0.5">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
90
components/sections/advantages-section.tsx
Normal file
90
components/sections/advantages-section.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Zap, ShieldCheck, Layers, BarChart3, Headset, Cloud, Settings2, CloudCog, Link2, TrendingUp,
|
||||
} from "lucide-react"
|
||||
import { SectionReveal } from "@/components/ui/section-reveal"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
"zap": <Zap className="w-5 h-5" />,
|
||||
"shield-check": <ShieldCheck className="w-5 h-5" />,
|
||||
"layers": <Layers className="w-5 h-5" />,
|
||||
"bar-chart-3": <BarChart3 className="w-5 h-5" />,
|
||||
"headset": <Headset className="w-5 h-5" />,
|
||||
"cloud": <Cloud className="w-5 h-5" />,
|
||||
"Zap": <Zap className="w-5 h-5" />,
|
||||
"Settings2": <Settings2 className="w-5 h-5" />,
|
||||
"CloudCog": <CloudCog className="w-5 h-5" />,
|
||||
"Headphones": <Headset className="w-5 h-5" />,
|
||||
"Link2": <Link2 className="w-5 h-5" />,
|
||||
"TrendingUp": <TrendingUp className="w-5 h-5" />,
|
||||
}
|
||||
|
||||
export function AdvantagesSection() {
|
||||
const { advantages } = homeData
|
||||
|
||||
return (
|
||||
<section
|
||||
id="advantages"
|
||||
className="section-panel overflow-hidden"
|
||||
style={{ background: "oklch(0.97 0.006 260)" }}
|
||||
>
|
||||
{/* Subtle background decoration */}
|
||||
<div
|
||||
className="absolute top-0 left-1/2 -translate-x-1/2 w-[900px] h-[400px] opacity-[0.06] pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
background: "radial-gradient(ellipse at center, oklch(0.46 0.22 264) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<SectionReveal className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative" delay={60}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center max-w-2xl mx-auto mb-16">
|
||||
<Badge variant="secondary" className="mb-4 text-primary border-primary/20 bg-primary/8 px-3 py-1">
|
||||
{advantages.sectionBadge ?? advantages.sectionTag}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-foreground text-balance mb-4 tracking-tight">
|
||||
{advantages.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed text-pretty">
|
||||
{advantages.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{advantages.items.map((item, i) => (
|
||||
<article
|
||||
key={item.title}
|
||||
className="group relative bg-white border border-border rounded-2xl p-7 hover:shadow-xl hover:shadow-primary/8 hover:-translate-y-1 transition-all duration-300"
|
||||
>
|
||||
{/* Number watermark */}
|
||||
<span
|
||||
className="absolute top-5 right-6 text-6xl font-black text-muted/20 leading-none select-none pointer-events-none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="w-12 h-12 rounded-2xl bg-primary/8 text-primary flex items-center justify-center mb-5 group-hover:bg-primary group-hover:text-white transition-colors duration-300">
|
||||
{iconMap[item.icon] ?? iconMap.zap}
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-foreground mb-2">{item.title}</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">{item.description}</p>
|
||||
|
||||
{/* Bottom accent line */}
|
||||
<div className="absolute bottom-0 left-6 right-6 h-px bg-primary scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left rounded-full" />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</SectionReveal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
90
components/sections/cases-section.tsx
Normal file
90
components/sections/cases-section.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { SectionReveal } from "@/components/ui/section-reveal"
|
||||
import { TrendingUp, Quote } from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
export function CasesSection() {
|
||||
const { cases } = homeData
|
||||
const sectionBadge = (cases as any).sectionBadge ?? (cases as any).sectionTag
|
||||
const caseItems = (Array.isArray((cases as any).items) ? (cases as any).items : []).map((item: any) => ({
|
||||
...item,
|
||||
result: item.result ?? item.solution ?? "",
|
||||
metrics: Array.isArray(item.metrics)
|
||||
? item.metrics
|
||||
: (Array.isArray(item.results) ? item.results.map((m: any) => ({ l: m.metric, v: m.value })) : []),
|
||||
}))
|
||||
|
||||
return (
|
||||
<section id="cases" className="section-panel bg-background">
|
||||
<SectionReveal className="w-full max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8" delay={120}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center max-w-3xl mx-auto mb-16">
|
||||
<Badge variant="secondary" className="mb-4 text-primary border-primary/20 bg-primary/8 px-3 py-1">
|
||||
{sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-foreground text-balance tracking-tight mb-4">
|
||||
{cases.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed text-pretty">
|
||||
{cases.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Case cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
||||
{caseItems.map((c) => (
|
||||
<article
|
||||
key={c.brand}
|
||||
className="group relative flex h-full flex-col lg:col-span-4 bg-white border border-border rounded-[2rem] p-8 hover:shadow-2xl hover:shadow-primary/10 hover:-translate-y-1 transition-all duration-300"
|
||||
>
|
||||
<div className="flex flex-col gap-6 flex-1">
|
||||
{/* Brand & scale */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<span className="inline-flex items-center rounded-full bg-primary/8 px-3 py-1 text-xs font-semibold text-primary mb-3">
|
||||
{c.industry}
|
||||
</span>
|
||||
<h3 className="font-bold text-foreground text-xl leading-tight mb-2">{c.brand}</h3>
|
||||
<span className="inline-flex text-xs text-muted-foreground bg-muted rounded-full px-3 py-1">
|
||||
{c.scale}
|
||||
</span>
|
||||
</div>
|
||||
<TrendingUp className="w-5 h-5 text-primary shrink-0 mt-0.5" />
|
||||
</div>
|
||||
|
||||
{/* Challenge */}
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-widest mb-1.5">遇到的挑战</p>
|
||||
<p className="text-sm text-foreground leading-relaxed">{c.challenge}</p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Result */}
|
||||
<div className="flex gap-2">
|
||||
<Quote className="w-4 h-4 text-primary shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-muted-foreground leading-relaxed italic">{c.result}</p>
|
||||
</div>
|
||||
|
||||
{/* Metrics */}
|
||||
<div className="grid grid-cols-3 gap-4 mt-auto pt-2">
|
||||
{c.metrics.map((m) => (
|
||||
<div key={m.l} className="flex flex-col items-center text-center bg-primary/6 rounded-2xl py-4 px-3">
|
||||
<span className="text-xl font-black text-primary leading-none mb-1">{m.v}</span>
|
||||
<span className="text-[10px] text-muted-foreground leading-tight">{m.l}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</SectionReveal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
154
components/sections/contact-section.tsx
Normal file
154
components/sections/contact-section.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { SectionReveal } from "@/components/ui/section-reveal"
|
||||
import { Phone, Mail, MapPin, Clock3, ArrowRight, CheckCircle2 } from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
phone: <Phone className="w-5 h-5" />,
|
||||
mail: <Mail className="w-5 h-5" />,
|
||||
mappin: <MapPin className="w-5 h-5" />,
|
||||
clock: <Clock3 className="w-5 h-5" />,
|
||||
}
|
||||
|
||||
export function ContactSection() {
|
||||
const { contact } = homeData
|
||||
const sectionBadge = (contact as any).sectionBadge ?? (contact as any).sectionTag
|
||||
const info = ((contact as any).info ??
|
||||
((contact as any).infoItems ?? []).map((item: any) => ({
|
||||
...item,
|
||||
icon: String(item.icon ?? "").toLowerCase(),
|
||||
sub: item.sub ?? "",
|
||||
}))) as Array<{ icon: string; label: string; value: string; sub?: string }>
|
||||
const submitLabel = (contact.form as any).submitLabel ?? (contact.form as any).submit
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSubmitted(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contact" className="section-panel bg-background">
|
||||
<SectionReveal className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" delay={140}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center max-w-2xl mx-auto mb-16">
|
||||
<Badge variant="secondary" className="mb-4 text-primary border-primary/20 bg-primary/8 px-3 py-1">
|
||||
{sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-foreground text-balance tracking-tight mb-4">
|
||||
{contact.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed text-pretty">
|
||||
{contact.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-5 gap-10 items-start">
|
||||
|
||||
{/* Left: info */}
|
||||
<div className="lg:col-span-2 flex flex-col gap-5">
|
||||
{info.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="flex items-start gap-4 p-5 bg-muted rounded-2xl border border-border hover:border-primary/30 transition-colors"
|
||||
>
|
||||
<div className="w-11 h-11 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0">
|
||||
{iconMap[item.icon] ?? iconMap.phone}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-widest mb-1">{item.label}</p>
|
||||
<p className="font-bold text-foreground text-sm">{item.value}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{item.sub}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Trust note */}
|
||||
<div className="bg-primary/6 border border-primary/15 rounded-2xl p-5">
|
||||
<p className="text-sm text-primary font-semibold mb-1.5">响应时效说明</p>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
工作时间内,专属顾问将在{" "}
|
||||
<strong className="text-foreground">2 小时内</strong>
|
||||
{" "}联系您;非工作时间提交的信息,将在次一工作日上午统一回复。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: form */}
|
||||
<div className="lg:col-span-3 bg-white border border-border rounded-3xl p-8 shadow-lg shadow-primary/5">
|
||||
{submitted ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 gap-5 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-green-50 border-2 border-green-200 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-8 h-8 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">信息已提交</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
工作时间内 2 小时响应,非工作时间次一工作日上午回复。
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setSubmitted(false)} className="mt-2">
|
||||
重新提交
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-5" noValidate>
|
||||
<h3 className="text-lg font-bold text-foreground">留下信息,先拿一版初步建议</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground" htmlFor="name">姓名</label>
|
||||
<Input id="name" placeholder={contact.form.namePlaceholder} required className="h-11" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground" htmlFor="company">公司 / 品牌</label>
|
||||
<Input id="company" placeholder={contact.form.companyPlaceholder} required className="h-11" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground" htmlFor="phone">电话</label>
|
||||
<Input id="phone" type="tel" placeholder={contact.form.phonePlaceholder} required className="h-11" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground" htmlFor="scale">门店规模</label>
|
||||
<Input id="scale" placeholder={contact.form.scalePlaceholder} className="h-11" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground" htmlFor="message">需求描述</label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder={contact.form.messagePlaceholder}
|
||||
rows={4}
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full bg-primary hover:bg-primary-dark text-white h-12 text-base font-semibold group shadow-lg shadow-primary/25"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{submitLabel}
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</span>
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
提交即视为同意{" "}
|
||||
<span className="text-primary cursor-pointer hover:underline">隐私政策</span>
|
||||
,我们承诺不向第三方共享您的信息。
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</SectionReveal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
66
components/sections/cta-banner.tsx
Normal file
66
components/sections/cta-banner.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowRight, Sparkles } from "lucide-react"
|
||||
|
||||
export function CTABanner() {
|
||||
return (
|
||||
<section className="bg-surface-dark py-16 lg:py-20 relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<div
|
||||
className="absolute -top-24 -right-24 w-96 h-96 rounded-full opacity-10"
|
||||
style={{ background: "radial-gradient(circle, oklch(0.46 0.22 264) 0%, transparent 70%)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-16 -left-16 w-72 h-72 rounded-full opacity-8"
|
||||
style={{ background: "radial-gradient(circle, oklch(0.55 0.18 280) 0%, transparent 70%)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(white 1px, transparent 1px), linear-gradient(90deg, white 1px, transparent 1px)`,
|
||||
backgroundSize: "64px 64px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-8 text-center lg:text-left">
|
||||
<div className="flex flex-col gap-3 max-w-2xl">
|
||||
<div className="flex items-center justify-center lg:justify-start gap-2 text-white/60">
|
||||
<Sparkles className="w-4 h-4 text-primary" />
|
||||
<span className="text-sm font-medium uppercase tracking-widest">开启门店智能化升级</span>
|
||||
</div>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white text-balance">
|
||||
准备好让您的门店更聪明了吗?
|
||||
</h2>
|
||||
<p className="text-white/55 text-base lg:text-lg text-pretty">
|
||||
立即联系智店软件,获取专属门店智能化改造方案,从智慧收银到智能监管,全程陪伴您的升级之路。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4 shrink-0">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary-dark text-white shadow-xl shadow-primary/40 px-8 h-12 text-base font-semibold"
|
||||
>
|
||||
<Link href="#contact" className="flex items-center gap-2">
|
||||
立即咨询
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-white/20 bg-transparent text-white hover:bg-white/10 hover:border-white/40 px-8 h-12 text-base"
|
||||
>
|
||||
<Link href="#products">查看产品</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
335
components/sections/hero-section.tsx
Normal file
335
components/sections/hero-section.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowRight, ChevronDown, Sparkles } from "lucide-react"
|
||||
import { useParallax, useScrollY } from "@/hooks/use-parallax"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
function parseStatValue(raw: string) {
|
||||
const match = raw.match(/-?\d[\d,.]*/)
|
||||
|
||||
if (!match || match.index === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const numericRaw = match[0].replace(/,/g, "")
|
||||
const value = Number(numericRaw)
|
||||
|
||||
if (Number.isNaN(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
prefix: raw.slice(0, match.index),
|
||||
suffix: raw.slice(match.index + match[0].length),
|
||||
value,
|
||||
decimals: (numericRaw.split(".")[1] ?? "").length,
|
||||
}
|
||||
}
|
||||
|
||||
function formatStatValue(parsed: NonNullable<ReturnType<typeof parseStatValue>>, progress: number) {
|
||||
const currentValue = parsed.value * progress
|
||||
|
||||
if (parsed.decimals > 0) {
|
||||
return `${parsed.prefix}${currentValue.toFixed(parsed.decimals)}${parsed.suffix}`
|
||||
}
|
||||
|
||||
return `${parsed.prefix}${Math.round(currentValue).toLocaleString()}${parsed.suffix}`
|
||||
}
|
||||
|
||||
function AnimatedStatValue({ value, start }: { value: string; start: boolean }) {
|
||||
const parsed = useMemo(() => parseStatValue(value), [value])
|
||||
const [displayValue, setDisplayValue] = useState(() =>
|
||||
parsed ? formatStatValue(parsed, 0) : value
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!parsed) {
|
||||
setDisplayValue(value)
|
||||
return
|
||||
}
|
||||
|
||||
if (!start) {
|
||||
setDisplayValue(formatStatValue(parsed, 0))
|
||||
return
|
||||
}
|
||||
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
setDisplayValue(value)
|
||||
return
|
||||
}
|
||||
|
||||
const duration = 1400
|
||||
const startAt = performance.now()
|
||||
let frame = 0
|
||||
|
||||
const animate = (now: number) => {
|
||||
const progress = Math.min((now - startAt) / duration, 1)
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
|
||||
setDisplayValue(formatStatValue(parsed, eased))
|
||||
|
||||
if (progress < 1) {
|
||||
frame = window.requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
frame = window.requestAnimationFrame(animate)
|
||||
|
||||
return () => window.cancelAnimationFrame(frame)
|
||||
}, [parsed, start, value])
|
||||
|
||||
return (
|
||||
<span
|
||||
className="inline-block tabular-nums [font-variant-numeric:tabular-nums]"
|
||||
style={{ minWidth: `${Math.max(value.length + 1, 7)}ch` }}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function HeroSection() {
|
||||
const { hero } = homeData
|
||||
const heroAny = hero as any
|
||||
const title = heroAny.title ?? heroAny.headline1 ?? ""
|
||||
const titleHighlight = heroAny.titleHighlight ?? heroAny.headline2 ?? ""
|
||||
const primaryCta = heroAny.primaryCta ?? {
|
||||
label: heroAny.ctaPrimary ?? "免费预约演示",
|
||||
href: "#contact",
|
||||
}
|
||||
const secondaryCta = heroAny.secondaryCta ?? {
|
||||
label: heroAny.ctaSecondary ?? "查看产品方案",
|
||||
href: "#products",
|
||||
}
|
||||
const stats = Array.isArray(heroAny.stats) ? heroAny.stats : []
|
||||
const tags = Array.isArray(heroAny.tags) ? heroAny.tags : []
|
||||
const { ref: bgRef, translateY: bgY } = useParallax(0.45)
|
||||
const { ref: floatRef, translateY: floatY } = useParallax(-0.15)
|
||||
const scrollY = useScrollY()
|
||||
const contentOpacity = Math.max(0, 1 - scrollY / 500)
|
||||
const statsRef = useRef<HTMLDivElement | null>(null)
|
||||
const [statsVisible, setStatsVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const node = statsRef.current
|
||||
|
||||
if (!node || statsVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setStatsVisible(true)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.35 }
|
||||
)
|
||||
|
||||
observer.observe(node)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [statsVisible])
|
||||
|
||||
return (
|
||||
<section
|
||||
id="home"
|
||||
className="relative min-h-[100svh] snap-start snap-always flex flex-col justify-center overflow-hidden"
|
||||
aria-label="首屏"
|
||||
>
|
||||
{/* ── 深色渐变背景(视差层) ── */}
|
||||
<div
|
||||
ref={bgRef}
|
||||
className="absolute inset-[-20%] will-change-transform"
|
||||
style={{ transform: `translateY(${bgY})` }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* 主背景 */}
|
||||
<div className="absolute inset-0 bg-surface-dark" />
|
||||
{/* 顶部蓝色光晕 */}
|
||||
<div
|
||||
className="absolute -top-32 -left-32 w-[800px] h-[800px] rounded-full opacity-30"
|
||||
style={{
|
||||
background: "radial-gradient(circle, oklch(0.46 0.22 264) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-32 -right-16 w-[600px] h-[600px] rounded-full opacity-20"
|
||||
style={{
|
||||
background: "radial-gradient(circle, oklch(0.55 0.18 280) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
{/* 网格线 */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.06]"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(oklch(1 0 0 / 1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, oklch(1 0 0 / 1) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "80px 80px",
|
||||
}}
|
||||
/>
|
||||
{/* 斜线装饰 */}
|
||||
<svg
|
||||
className="absolute top-0 right-0 w-1/2 h-full opacity-[0.04]"
|
||||
viewBox="0 0 600 900"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{Array.from({ length: 12 }).map((_, i) => (
|
||||
<line
|
||||
key={i}
|
||||
x1={-200 + i * 80}
|
||||
y1="0"
|
||||
x2={400 + i * 80}
|
||||
y2="900"
|
||||
stroke="white"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* ── 浮动几何装饰(反向视差) ── */}
|
||||
<div
|
||||
ref={floatRef}
|
||||
className="absolute inset-0 will-change-transform pointer-events-none"
|
||||
style={{ transform: `translateY(${floatY})` }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* 右上角大圆环 */}
|
||||
<div
|
||||
className="absolute -top-16 -right-16 w-[520px] h-[520px] rounded-full border opacity-10"
|
||||
style={{ borderColor: "oklch(0.46 0.22 264)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-20 right-20 w-[360px] h-[360px] rounded-full border opacity-8"
|
||||
style={{ borderColor: "oklch(0.65 0.18 264)" }}
|
||||
/>
|
||||
{/* 小光点 */}
|
||||
<div className="absolute top-1/3 right-1/4 w-2 h-2 rounded-full bg-primary opacity-60 animate-pulse" />
|
||||
<div className="absolute top-1/2 right-1/3 w-1.5 h-1.5 rounded-full bg-blue-300 opacity-40 animate-pulse [animation-delay:0.8s]" />
|
||||
<div className="absolute top-2/3 right-1/5 w-1 h-1 rounded-full bg-white opacity-30 animate-pulse [animation-delay:1.6s]" />
|
||||
|
||||
{/* 右侧产品亮点浮卡 */}
|
||||
<div className="absolute top-1/2 right-8 lg:right-16 xl:right-24 -translate-y-1/2 hidden xl:flex flex-col gap-3">
|
||||
{[
|
||||
{ label: "智慧收银", sub: "多端聚合支付" },
|
||||
{ label: "智能分账", sub: "实时自动结算" },
|
||||
{ label: "智能监管", sub: "AI 异常预警" },
|
||||
].map((card) => (
|
||||
<div
|
||||
key={card.label}
|
||||
className="flex items-center gap-3 bg-white/8 backdrop-blur-sm border border-white/15 rounded-xl px-4 py-3 min-w-[180px]"
|
||||
>
|
||||
<div className="w-2 h-2 rounded-full bg-primary shrink-0" />
|
||||
<div>
|
||||
<p className="text-white text-sm font-semibold leading-none mb-0.5">{card.label}</p>
|
||||
<p className="text-white/50 text-xs">{card.sub}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 主体内容 ── */}
|
||||
<div
|
||||
className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-28 pb-20"
|
||||
style={{ opacity: contentOpacity }}
|
||||
>
|
||||
<div className="max-w-2xl xl:max-w-3xl">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 bg-primary/20 border border-primary/40 rounded-full px-4 py-1.5 mb-7">
|
||||
<Sparkles className="w-3.5 h-3.5 text-primary" />
|
||||
<span className="text-white/90 text-sm font-medium">{hero.badge}</span>
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-extrabold text-white leading-[1.08] text-balance mb-6 tracking-tight">
|
||||
{title}
|
||||
<br />
|
||||
<span
|
||||
className="relative"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, oklch(0.75 0.16 220), oklch(0.65 0.2 264), oklch(0.55 0.22 280))",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
}}
|
||||
>
|
||||
{titleHighlight}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-lg text-white/65 leading-relaxed max-w-xl text-pretty mb-10">
|
||||
{hero.subtitle}
|
||||
</p>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-3 mb-10">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center rounded-full border border-white/12 bg-white/8 px-3.5 py-1.5 text-sm text-white/75 backdrop-blur-sm"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-16">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary-dark text-white shadow-xl shadow-primary/30 px-8 h-12 text-base font-semibold group"
|
||||
>
|
||||
<Link href={primaryCta.href ?? "#contact"} className="flex items-center gap-2">
|
||||
{primaryCta.label}
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-white/25 bg-white/8 backdrop-blur-sm text-white hover:bg-white/15 hover:border-white/40 px-8 h-12 text-base"
|
||||
>
|
||||
<Link href={secondaryCta.href ?? "#products"}>
|
||||
{secondaryCta.label}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div
|
||||
ref={statsRef}
|
||||
className="grid grid-cols-2 sm:grid-cols-4 gap-6 pt-10 border-t border-white/10"
|
||||
>
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="flex flex-col gap-1.5">
|
||||
<span className="text-3xl lg:text-4xl font-black text-white tracking-tight">
|
||||
<AnimatedStatValue value={stat.value} start={statsVisible} />
|
||||
</span>
|
||||
<span className="text-xs text-white/45 leading-tight">{stat.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll cue */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-10 flex flex-col items-center gap-1.5 animate-bounce">
|
||||
<span className="text-white/30 text-xs tracking-widest">SCROLL</span>
|
||||
<ChevronDown className="w-4 h-4 text-white/30" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
81
components/sections/news-section.tsx
Normal file
81
components/sections/news-section.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Building2, TrendingUp, Quote } from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
export function NewsSection() {
|
||||
const { cases } = homeData
|
||||
|
||||
return (
|
||||
<section id="cases" className="py-20 lg:py-28 bg-neutral-50/80">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col items-center text-center gap-4 mb-14">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary/30 text-primary bg-primary-light px-4 py-1 text-sm font-medium"
|
||||
>
|
||||
{cases.sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground text-balance">
|
||||
{cases.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-base lg:text-lg max-w-2xl text-pretty">
|
||||
{cases.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cases grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{cases.items.map((item, index) => (
|
||||
<Card
|
||||
key={item.brand}
|
||||
className="group border border-border bg-background hover:border-primary/35 hover:shadow-xl hover:shadow-primary/8 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
{/* Top accent bar */}
|
||||
<div className="h-1.5 bg-primary" style={{ opacity: 0.6 + index * 0.2 }} />
|
||||
|
||||
<CardContent className="p-7 flex flex-col gap-5">
|
||||
{/* Brand header */}
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center shrink-0 text-primary">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-sm text-foreground leading-snug">{item.brand}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{item.scale}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Challenge */}
|
||||
<div className="bg-neutral-50 rounded-xl p-4 border border-border">
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground mb-2 uppercase tracking-wide">
|
||||
<Quote className="w-3 h-3" />
|
||||
挑战
|
||||
</div>
|
||||
<p className="text-sm text-foreground leading-relaxed">{item.challenge}</p>
|
||||
</div>
|
||||
|
||||
{/* Result */}
|
||||
<div className="flex items-start gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-primary mt-0.5 shrink-0" />
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.result}</p>
|
||||
</div>
|
||||
|
||||
{/* Metrics */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-border">
|
||||
{item.metrics.map((m) => (
|
||||
<div key={m.l} className="flex flex-col items-center text-center gap-0.5">
|
||||
<span className="text-xl font-black text-primary leading-none">{m.v}</span>
|
||||
<span className="text-[11px] text-muted-foreground leading-tight">{m.l}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
71
components/sections/partners-section.tsx
Normal file
71
components/sections/partners-section.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
export function PartnersSection() {
|
||||
const { process } = homeData
|
||||
|
||||
return (
|
||||
<section id="process" className="py-20 lg:py-24 bg-primary relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 pointer-events-none" aria-hidden="true">
|
||||
<div
|
||||
className="absolute -top-40 -right-40 w-[600px] h-[600px] rounded-full opacity-10"
|
||||
style={{ background: "radial-gradient(circle, white 0%, transparent 70%)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-32 -left-32 w-[400px] h-[400px] rounded-full opacity-8"
|
||||
style={{ background: "radial-gradient(circle, white 0%, transparent 70%)" }}
|
||||
/>
|
||||
{/* Grid lines */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.04]"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(white 1px, transparent 1px), linear-gradient(90deg, white 1px, transparent 1px)`,
|
||||
backgroundSize: "60px 60px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col items-center text-center gap-4 mb-14">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-white/30 text-white bg-white/10 px-4 py-1 text-sm font-medium"
|
||||
>
|
||||
{process.sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white text-balance">
|
||||
{process.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8 relative">
|
||||
{/* Connector line (desktop only) */}
|
||||
<div className="hidden md:block absolute top-10 left-[calc(16.67%+2rem)] right-[calc(16.67%+2rem)] h-0.5 bg-white/20" aria-hidden="true" />
|
||||
|
||||
{process.steps.map((step, i) => (
|
||||
<div key={step.num} className="relative flex flex-col items-center text-center gap-5">
|
||||
{/* Step number circle */}
|
||||
<div className="relative w-20 h-20 rounded-full bg-white/12 border-2 border-white/25 flex items-center justify-center shrink-0 shadow-lg">
|
||||
<span className="text-2xl font-black text-white">{step.num}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="text-xl font-bold text-white">{step.title}</h3>
|
||||
<p className="text-white/65 text-sm leading-relaxed max-w-xs">{step.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Arrow between steps */}
|
||||
{i < process.steps.length - 1 && (
|
||||
<ArrowRight className="hidden md:block absolute top-8 -right-6 w-5 h-5 text-white/30" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
111
components/sections/process-section.tsx
Normal file
111
components/sections/process-section.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { SectionReveal } from "@/components/ui/section-reveal"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
import { useParallax } from "@/hooks/use-parallax"
|
||||
import homeData from "@/data/home.json"
|
||||
import Link from "next/link"
|
||||
|
||||
export function ProcessSection() {
|
||||
const homeDataAny = homeData as any
|
||||
const process = homeDataAny.process ?? homeDataAny.howItWorks
|
||||
const sectionBadge = process?.sectionBadge ?? process?.sectionTag
|
||||
const steps = (Array.isArray(process?.steps) ? process.steps : []).map((step: any) => ({
|
||||
...step,
|
||||
num: step.num ?? step.number,
|
||||
}))
|
||||
const { ref: bgRef, translateY: bgY } = useParallax(0.2)
|
||||
|
||||
if (!process) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="process"
|
||||
className="section-panel overflow-hidden"
|
||||
>
|
||||
{/* Parallax dark background */}
|
||||
<div
|
||||
ref={bgRef}
|
||||
className="absolute inset-[-15%] will-change-transform"
|
||||
style={{ transform: `translateY(${bgY})` }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 bg-surface-dark" />
|
||||
{/* Dot grid */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.07]"
|
||||
style={{
|
||||
backgroundImage: "radial-gradient(circle, oklch(1 0 0 / 1) 1px, transparent 1px)",
|
||||
backgroundSize: "32px 32px",
|
||||
}}
|
||||
/>
|
||||
{/* Blue glow */}
|
||||
<div
|
||||
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] opacity-20"
|
||||
style={{
|
||||
background: "radial-gradient(ellipse at center bottom, oklch(0.46 0.22 264) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionReveal className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" delay={100}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center max-w-xl mx-auto mb-16">
|
||||
<Badge className="mb-4 bg-primary/20 text-primary border-primary/30 px-3 py-1">
|
||||
{sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-white text-balance tracking-tight">
|
||||
{process.title}
|
||||
</h2>
|
||||
<p className="text-white/65 leading-relaxed text-pretty mt-4">
|
||||
{process.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="relative grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Connector line (desktop) */}
|
||||
<div
|
||||
className="hidden md:block absolute top-10 left-[calc(16.7%+1rem)] right-[calc(16.7%+1rem)] h-px border-t border-dashed border-white/20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{steps.map((step) => (
|
||||
<article key={step.num} className="relative flex flex-col items-center text-center gap-5">
|
||||
{/* Number circle */}
|
||||
<div className="relative z-10 w-20 h-20 rounded-full border-2 border-primary bg-surface-dark flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<span className="text-2xl font-black text-primary">{step.num}</span>
|
||||
</div>
|
||||
|
||||
{/* Content card */}
|
||||
<div className="bg-white/6 backdrop-blur-sm border border-white/10 rounded-2xl p-6 w-full">
|
||||
<h3 className="text-xl font-bold text-white mb-3">{step.title}</h3>
|
||||
<p className="text-white/60 text-sm leading-relaxed">{step.description}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="mt-14 flex justify-center">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary-dark text-white shadow-xl shadow-primary/30 px-8 h-12 text-base font-semibold group"
|
||||
>
|
||||
<Link href="#contact" className="flex items-center gap-2">
|
||||
预约方案诊断
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</SectionReveal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
232
components/sections/products-section.tsx
Normal file
232
components/sections/products-section.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Check, CreditCard, GitBranch, Monitor, ArrowRight } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { SectionReveal } from "@/components/ui/section-reveal"
|
||||
import homeData from "@/data/home.json"
|
||||
import Link from "next/link"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
pos: <CreditCard className="w-7 h-7" />,
|
||||
split: <GitBranch className="w-7 h-7" />,
|
||||
monitor: <Monitor className="w-7 h-7" />,
|
||||
}
|
||||
|
||||
const accentMap: Record<string, { bg: string; border: string; text: string; glow: string }> = {
|
||||
blue: {
|
||||
bg: "bg-primary",
|
||||
border: "border-primary/20",
|
||||
text: "text-primary",
|
||||
glow: "shadow-primary/20",
|
||||
},
|
||||
indigo: {
|
||||
bg: "bg-indigo-600",
|
||||
border: "border-indigo-500/20",
|
||||
text: "text-indigo-600",
|
||||
glow: "shadow-indigo-500/20",
|
||||
},
|
||||
violet: {
|
||||
bg: "bg-violet-600",
|
||||
border: "border-violet-500/20",
|
||||
text: "text-violet-600",
|
||||
glow: "shadow-violet-500/20",
|
||||
},
|
||||
}
|
||||
|
||||
export function ProductsSection() {
|
||||
const { products } = homeData
|
||||
const productsAny = products as any
|
||||
const sectionBadge = productsAny.sectionBadge ?? productsAny.sectionTag
|
||||
const normalizedItems = (Array.isArray(productsAny.items) ? productsAny.items : []).map((item: any) => {
|
||||
const id = item.id ?? ""
|
||||
const iconType = item.iconType ?? (id === "cashier" ? "pos" : id === "split" ? "split" : "monitor")
|
||||
const color = item.color ?? (id === "cashier" ? "blue" : id === "split" ? "indigo" : "violet")
|
||||
return {
|
||||
...item,
|
||||
iconType,
|
||||
color,
|
||||
title: item.title ?? item.name ?? item.tag,
|
||||
}
|
||||
})
|
||||
const [active, setActive] = useState(0)
|
||||
const current = normalizedItems[active] ?? normalizedItems[0]
|
||||
const accent = accentMap[current?.color] ?? accentMap.blue
|
||||
|
||||
if (!current) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="products" className="section-panel bg-background">
|
||||
<SectionReveal className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" delay={40}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center max-w-2xl mx-auto mb-16">
|
||||
<Badge variant="secondary" className="mb-4 text-primary border-primary/20 bg-primary/8 px-3 py-1">
|
||||
{sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-foreground text-balance mb-4 tracking-tight">
|
||||
{products.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed text-pretty">
|
||||
{products.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab switcher */}
|
||||
<div className="flex justify-center mb-12">
|
||||
<div className="inline-flex items-center gap-2 bg-muted p-1.5 rounded-2xl">
|
||||
{normalizedItems.map((item, i) => {
|
||||
const a = accentMap[item.color] ?? accentMap.blue
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActive(i)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold transition-all duration-200",
|
||||
active === i
|
||||
? `${a.bg} text-white shadow-md`
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
aria-pressed={active === i}
|
||||
>
|
||||
{iconMap[item.iconType] ?? iconMap.monitor}
|
||||
<span className="hidden sm:inline">{item.tag}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail panel */}
|
||||
<div
|
||||
key={current.id}
|
||||
className={cn(
|
||||
"grid lg:grid-cols-2 gap-10 items-center border rounded-3xl p-8 lg:p-12 shadow-xl",
|
||||
accent.border,
|
||||
accent.glow
|
||||
)}
|
||||
style={{ boxShadow: `0 20px 60px -10px var(--tw-shadow-color)` }}
|
||||
>
|
||||
{/* Left: info */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className={cn("inline-flex items-center gap-2 rounded-xl px-3 py-1.5 w-fit", `${accent.bg}/10`)}>
|
||||
<span className={accent.text}>{iconMap[current.iconType] ?? iconMap.monitor}</span>
|
||||
<span className={cn("text-sm font-semibold", accent.text)}>{current.tag}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl lg:text-3xl font-extrabold text-foreground mb-3 tracking-tight">
|
||||
{current.title}
|
||||
</h3>
|
||||
<p className={cn("text-sm font-semibold mb-3", accent.text)}>
|
||||
{current.tagline}
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed text-pretty">
|
||||
{current.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 max-w-lg">
|
||||
<div className={cn("rounded-2xl border px-4 py-3", accent.border, `${accent.bg}/5`)}>
|
||||
<p className={cn("text-lg font-black tracking-tight", accent.text)}>{current.highlight}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">关键性能指标</p>
|
||||
</div>
|
||||
<div className={cn("rounded-2xl border px-4 py-3", accent.border, `${accent.bg}/5`)}>
|
||||
<p className="text-sm font-bold text-foreground">{current.highlightSub}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">交付稳定性承诺</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{current.features.map((f) => (
|
||||
<li key={f} className="flex items-center gap-2.5 text-sm text-foreground">
|
||||
<span className={cn("w-5 h-5 rounded-full flex items-center justify-center shrink-0", `${accent.bg}/10`)}>
|
||||
<Check className={cn("w-3 h-3", accent.text)} />
|
||||
</span>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
className={cn("w-fit px-6 group", accent.bg, "hover:opacity-90 text-white")}
|
||||
>
|
||||
<Link href="#contact" className="flex items-center gap-2">
|
||||
了解详情
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Right: visual mock */}
|
||||
<div className="relative flex items-center justify-center min-h-[320px] lg:min-h-[380px]">
|
||||
{/* Dark card base */}
|
||||
<div className="relative w-full max-w-[420px] aspect-[4/3] rounded-2xl bg-surface-dark border border-white/8 shadow-2xl overflow-hidden">
|
||||
{/* Top bar */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-3 border-b border-white/8">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-red-500/70" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/70" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-green-500/70" />
|
||||
<div className="ml-3 h-4 bg-white/10 rounded flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Body: chart + cards */}
|
||||
<div className="p-5 flex flex-col gap-4">
|
||||
{/* Stat row */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{["日营业额", "交易笔数", "分账金额"].map((label, i) => (
|
||||
<div key={label} className="bg-white/6 rounded-xl p-3">
|
||||
<div className={cn("text-lg font-bold text-white mb-0.5", i === 0 && "text-primary")}>
|
||||
{i === 0 ? "¥48,260" : i === 1 ? "1,203" : "¥12,400"}
|
||||
</div>
|
||||
<div className="text-white/40 text-xs">{label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bar chart */}
|
||||
<div className="bg-white/6 rounded-xl p-3 flex-1">
|
||||
<p className="text-white/40 text-xs mb-3">近 7 日趋势</p>
|
||||
<div className="flex items-end gap-2 h-16">
|
||||
{[55, 70, 45, 88, 65, 92, 78].map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 rounded-sm transition-all"
|
||||
style={{
|
||||
height: `${h}%`,
|
||||
background: i === 5
|
||||
? `oklch(0.46 0.22 264)`
|
||||
: `oklch(0.46 0.22 264 / 0.35)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Glow overlay */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
background: `radial-gradient(ellipse at 80% 20%, oklch(0.46 0.22 264 / 0.12) 0%, transparent 60%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Floating badge */}
|
||||
<div className="absolute -bottom-4 -right-4 bg-white border border-border shadow-lg rounded-2xl px-4 py-2.5 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<span className="text-xs font-semibold text-foreground">系统运行正常</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</SectionReveal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
194
components/sections/services-section.tsx
Normal file
194
components/sections/services-section.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import {
|
||||
Monitor,
|
||||
SplitSquareVertical,
|
||||
BarChart3,
|
||||
Check,
|
||||
ArrowRight,
|
||||
Cpu,
|
||||
Network,
|
||||
Eye,
|
||||
} from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const productIcons: Record<string, React.ReactNode> = {
|
||||
pos: <Monitor className="w-7 h-7" />,
|
||||
split: <SplitSquareVertical className="w-7 h-7" />,
|
||||
monitor: <Eye className="w-7 h-7" />,
|
||||
}
|
||||
|
||||
const productBgSvg: Record<string, React.ReactNode> = {
|
||||
pos: (
|
||||
<svg className="absolute inset-0 w-full h-full opacity-[0.04]" viewBox="0 0 400 280" fill="none" aria-hidden="true">
|
||||
<rect x="60" y="40" width="280" height="160" rx="12" stroke="currentColor" strokeWidth="2" />
|
||||
<rect x="80" y="60" width="240" height="120" rx="6" stroke="currentColor" strokeWidth="1.5" />
|
||||
<line x1="160" y1="200" x2="240" y2="200" stroke="currentColor" strokeWidth="2" />
|
||||
<rect x="140" y="200" width="120" height="30" rx="4" stroke="currentColor" strokeWidth="1.5" />
|
||||
{[0,1,2,3].map(i => <rect key={i} x={90 + i * 56} y="70" width="44" height="28" rx="4" stroke="currentColor" strokeWidth="1" />)}
|
||||
</svg>
|
||||
),
|
||||
split: (
|
||||
<svg className="absolute inset-0 w-full h-full opacity-[0.04]" viewBox="0 0 400 280" fill="none" aria-hidden="true">
|
||||
<circle cx="200" cy="90" r="30" stroke="currentColor" strokeWidth="2" />
|
||||
<circle cx="100" cy="200" r="22" stroke="currentColor" strokeWidth="2" />
|
||||
<circle cx="200" cy="200" r="22" stroke="currentColor" strokeWidth="2" />
|
||||
<circle cx="300" cy="200" r="22" stroke="currentColor" strokeWidth="2" />
|
||||
<line x1="200" y1="120" x2="100" y2="178" stroke="currentColor" strokeWidth="1.5" />
|
||||
<line x1="200" y1="120" x2="200" y2="178" stroke="currentColor" strokeWidth="1.5" />
|
||||
<line x1="200" y1="120" x2="300" y2="178" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
),
|
||||
monitor: (
|
||||
<svg className="absolute inset-0 w-full h-full opacity-[0.04]" viewBox="0 0 400 280" fill="none" aria-hidden="true">
|
||||
{[0,1,2,3,4].map(i => <line key={i} x1="40" y1={60 + i * 40} x2="360" y2={60 + i * 40} stroke="currentColor" strokeWidth="1" />)}
|
||||
{[0,1,2,3,4,5,6,7].map(i => <line key={i} x1={40 + i * 46} y1="60" x2={40 + i * 46} y2="220" stroke="currentColor" strokeWidth="1" />)}
|
||||
<polyline points="40,200 120,140 200,160 280,100 360,120" stroke="currentColor" strokeWidth="2" fill="none" />
|
||||
<circle cx="280" cy="100" r="5" fill="currentColor" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
const colorMap: Record<string, { ring: string; badge: string; icon: string }> = {
|
||||
blue: { ring: "hover:border-primary/50", badge: "bg-primary/10 text-primary", icon: "bg-primary text-white" },
|
||||
indigo: { ring: "hover:border-indigo-400/50", badge: "bg-indigo-50 text-indigo-600", icon: "bg-indigo-600 text-white" },
|
||||
violet: { ring: "hover:border-violet-400/50", badge: "bg-violet-50 text-violet-600", icon: "bg-violet-600 text-white" },
|
||||
}
|
||||
|
||||
export function ServicesSection() {
|
||||
const { products } = homeData
|
||||
const [active, setActive] = useState(0)
|
||||
|
||||
return (
|
||||
<section id="products" className="py-20 lg:py-28 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col items-center text-center gap-4 mb-14">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary/30 text-primary bg-primary-light px-4 py-1 text-sm font-medium"
|
||||
>
|
||||
{products.sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground text-balance">
|
||||
{products.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-base lg:text-lg max-w-2xl text-pretty">
|
||||
{products.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab triggers */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mb-10">
|
||||
{products.items.map((item, i) => {
|
||||
const colors = colorMap[item.color]
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActive(i)}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 px-5 py-3 rounded-xl border text-sm font-semibold transition-all duration-200",
|
||||
active === i
|
||||
? "bg-primary border-primary text-white shadow-lg shadow-primary/25"
|
||||
: "bg-background border-border text-muted-foreground hover:border-primary/40 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<span className={cn("w-6 h-6 rounded-lg flex items-center justify-center", active === i ? "bg-white/20" : colors.icon)}>
|
||||
{item.iconType === "pos" && <Monitor className="w-3.5 h-3.5" />}
|
||||
{item.iconType === "split" && <Network className="w-3.5 h-3.5" />}
|
||||
{item.iconType === "monitor" && <Eye className="w-3.5 h-3.5" />}
|
||||
</span>
|
||||
{item.tag}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Active product detail */}
|
||||
{products.items.map((item, i) => {
|
||||
const colors = colorMap[item.color]
|
||||
if (i !== active) return null
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"border-2 overflow-hidden transition-all duration-300",
|
||||
active === i ? "border-primary/30 shadow-xl shadow-primary/10" : "border-border"
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||
{/* Left: text */}
|
||||
<div className="p-8 lg:p-10 flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn("w-12 h-12 rounded-2xl flex items-center justify-center shadow-sm", colors.icon)}>
|
||||
{productIcons[item.iconType]}
|
||||
</div>
|
||||
<div>
|
||||
<span className={cn("text-xs font-semibold px-2.5 py-1 rounded-full", colors.badge)}>
|
||||
{item.tag}
|
||||
</span>
|
||||
<h3 className="text-xl font-bold text-foreground mt-1">{item.title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground leading-relaxed text-pretty">{item.description}</p>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||
{item.features.map((f) => (
|
||||
<li key={f} className="flex items-center gap-2 text-sm text-foreground">
|
||||
<div className="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<Check className="w-3 h-3 text-primary" />
|
||||
</div>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a
|
||||
href="#contact"
|
||||
className="self-start flex items-center gap-1.5 text-sm font-semibold text-primary hover:gap-2.5 transition-all"
|
||||
>
|
||||
了解更多 <ArrowRight className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Right: visual mockup */}
|
||||
<div className={cn(
|
||||
"relative flex items-center justify-center min-h-[280px] lg:min-h-0 overflow-hidden",
|
||||
item.color === "blue" && "bg-primary/5",
|
||||
item.color === "indigo" && "bg-indigo-50",
|
||||
item.color === "violet" && "bg-violet-50",
|
||||
)}>
|
||||
{productBgSvg[item.iconType]}
|
||||
<div className="relative z-10 flex flex-col items-center gap-5 p-8">
|
||||
{/* Central icon */}
|
||||
<div className={cn("w-20 h-20 rounded-3xl flex items-center justify-center shadow-xl", colors.icon)}>
|
||||
{item.iconType === "pos" && <Monitor className="w-10 h-10" />}
|
||||
{item.iconType === "split" && <SplitSquareVertical className="w-10 h-10" />}
|
||||
{item.iconType === "monitor" && <BarChart3 className="w-10 h-10" />}
|
||||
</div>
|
||||
{/* Floating stat cards */}
|
||||
<div className="flex flex-wrap justify-center gap-3 max-w-xs">
|
||||
{item.features.slice(0, 3).map((f) => (
|
||||
<span key={f} className="bg-white border border-border text-xs text-foreground font-medium px-3 py-1.5 rounded-full shadow-sm">
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white border border-border rounded-xl px-4 py-2.5 shadow-sm">
|
||||
<Cpu className="w-4 h-4 text-primary" />
|
||||
<span className="text-xs font-semibold text-foreground">智店软件 · 智能引擎驱动</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
124
components/sections/solutions-section.tsx
Normal file
124
components/sections/solutions-section.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { SectionReveal } from "@/components/ui/section-reveal"
|
||||
import {
|
||||
ArrowRight,
|
||||
Car,
|
||||
Dumbbell,
|
||||
Scissors,
|
||||
ShoppingBag,
|
||||
Store,
|
||||
Utensils,
|
||||
} from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
utensils: <Utensils className="w-5 h-5" />,
|
||||
"shopping-bag": <ShoppingBag className="w-5 h-5" />,
|
||||
scissors: <Scissors className="w-5 h-5" />,
|
||||
dumbbell: <Dumbbell className="w-5 h-5" />,
|
||||
car: <Car className="w-5 h-5" />,
|
||||
store: <Store className="w-5 h-5" />,
|
||||
}
|
||||
|
||||
export function SolutionsSection() {
|
||||
const homeDataAny = homeData as any
|
||||
const solutions = homeDataAny.solutions ?? {
|
||||
sectionBadge: "行业方案",
|
||||
title: "覆盖多业态门店场景",
|
||||
subtitle: "面向餐饮、零售与综合商业体提供可落地的一站式智能化方案。",
|
||||
featured: {
|
||||
eyebrow: "方案设计思路",
|
||||
title: "不是卖一套通用软件,而是把关键流程接顺",
|
||||
description: "先梳理门店现有收银、结算、监管链路,再按行业共性与品牌个性组合能力模块。",
|
||||
points: [],
|
||||
metrics: [],
|
||||
},
|
||||
items: (Array.isArray(homeDataAny.cases?.items) ? homeDataAny.cases.items : []).map((item: any) => ({
|
||||
name: item.industry,
|
||||
desc: item.brand,
|
||||
icon: "store",
|
||||
focus: item.challenge,
|
||||
})),
|
||||
}
|
||||
|
||||
const featuredPoints = Array.isArray(solutions.featured?.points) ? solutions.featured.points : []
|
||||
const featuredMetrics = Array.isArray(solutions.featured?.metrics) ? solutions.featured.metrics : []
|
||||
|
||||
return (
|
||||
<section
|
||||
id="solutions"
|
||||
className="section-panel overflow-hidden lg:[padding-block:4.75rem_2.2rem]"
|
||||
style={{ background: "linear-gradient(180deg, oklch(0.985 0.004 260) 0%, oklch(0.97 0.008 260) 100%)" }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none opacity-55"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle at 10% 12%, oklch(0.78 0.08 240 / 0.16) 0%, transparent 24%), radial-gradient(circle at 88% 70%, oklch(0.62 0.14 270 / 0.10) 0%, transparent 22%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<SectionReveal className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" delay={80}>
|
||||
<div className="text-center max-w-4xl mx-auto mb-6 lg:mb-7">
|
||||
<Badge variant="secondary" className="mb-4 text-primary border-primary/20 bg-primary/8 px-3 py-1">
|
||||
{solutions.sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-[2.8rem] font-extrabold text-foreground text-balance tracking-tight mb-3">
|
||||
{solutions.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-base lg:text-lg leading-relaxed text-pretty max-w-3xl mx-auto">
|
||||
{solutions.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-5">
|
||||
{solutions.items.map((item: any, index: number) => (
|
||||
<article
|
||||
key={item.name}
|
||||
className="group relative overflow-hidden rounded-[1.75rem] border border-border/80 bg-white/96 p-5 shadow-sm transition-all duration-500 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1 hover:shadow-[0_18px_50px_-28px_rgba(34,84,186,0.35)]"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-24 opacity-70"
|
||||
aria-hidden="true"
|
||||
style={{ background: "linear-gradient(180deg, oklch(0.95 0.035 248) 0%, transparent 100%)" }}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full flex-col">
|
||||
<div className="flex items-start justify-between gap-3 mb-4">
|
||||
<div className="w-11 h-11 rounded-2xl bg-primary/8 text-primary flex items-center justify-center shadow-inner shadow-primary/10">
|
||||
{iconMap[item.icon] ?? iconMap.store}
|
||||
</div>
|
||||
<span className="text-[10px] font-semibold tracking-[0.24em] text-primary/45">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-bold text-foreground mb-2">{item.name}</h3>
|
||||
<p className="text-sm leading-6 text-muted-foreground min-h-[3rem]">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto rounded-[1.25rem] border border-primary/10 bg-primary/6 px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-3 mb-2.5">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary/80">
|
||||
重点场景
|
||||
</p>
|
||||
<ArrowRight className="w-3.5 h-3.5 text-primary/40 transition-all duration-500 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-1 group-hover:text-primary" />
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-foreground/85 min-h-[4.5rem]">
|
||||
{item.focus}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</SectionReveal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
65
components/sections/team-section.tsx
Normal file
65
components/sections/team-section.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Utensils,
|
||||
ShoppingBag,
|
||||
Scissors,
|
||||
Dumbbell,
|
||||
Car,
|
||||
Store,
|
||||
ArrowRight,
|
||||
} from "lucide-react"
|
||||
import homeData from "@/data/home.json"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
"utensils": <Utensils className="w-6 h-6" />,
|
||||
"shopping-bag": <ShoppingBag className="w-6 h-6" />,
|
||||
"scissors": <Scissors className="w-6 h-6" />,
|
||||
"dumbbell": <Dumbbell className="w-6 h-6" />,
|
||||
"car": <Car className="w-6 h-6" />,
|
||||
"store": <Store className="w-6 h-6" />,
|
||||
}
|
||||
|
||||
export function TeamSection() {
|
||||
const { solutions } = homeData
|
||||
|
||||
return (
|
||||
<section id="solutions" className="py-20 lg:py-28 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col items-center text-center gap-4 mb-14">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary/30 text-primary bg-primary-light px-4 py-1 text-sm font-medium"
|
||||
>
|
||||
{solutions.sectionBadge}
|
||||
</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground text-balance">
|
||||
{solutions.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-base lg:text-lg max-w-2xl text-pretty">
|
||||
{solutions.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Solutions grid */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{solutions.items.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="group flex flex-col items-center text-center gap-3 bg-background border border-border rounded-2xl p-6 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/8 hover:-translate-y-1 transition-all duration-300 cursor-pointer"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-primary/8 text-primary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-all duration-300">
|
||||
{iconMap[item.icon]}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-sm text-foreground">{item.name}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">{item.desc}</p>
|
||||
</div>
|
||||
<ArrowRight className="w-3.5 h-3.5 text-muted-foreground/40 group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user