feat: 添加连锁门店系统首页及核心UI组件

新增首页布局、导航栏、页脚及多个核心UI组件(按钮、卡片、表格等)
添加图片资源、工具函数和样式配置
实现响应式设计和主题支持
包含行业解决方案展示区块
This commit is contained in:
JanYork
2026-03-18 10:50:42 +08:00
commit 6a68a96287
110 changed files with 12842 additions and 0 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}