GEO 优化

This commit is contained in:
小潘
2026-06-11 16:53:18 +08:00
parent 40fb2ab22e
commit a33d132289
7 changed files with 183 additions and 101 deletions
+2 -2
View File
@@ -5,7 +5,7 @@
## 核心页面
- [首页](https://products.2009xc.com/): 企业概览、核心数据、四大产品展示、资质荣誉
- [产品详情](https://products.2009xc.com/product/smart-campus): 各产品详情页(痛点、功能、优势、FAQ),通过 `/product/{slug}` 动态路由访问
- [产品详情](https://products.2009xc.com/product/smart-campus): 各产品详情页(痛点、功能、优势、FAQ),通过 /product/{slug} 动态路由访问
- [合作咨询](https://products.2009xc.com/contact): 联系方式、代理合作权益、在线咨询表单
## 核心产品
@@ -50,7 +50,7 @@
### 售后服务和技术支持是怎样的?
提供 7×24 小时技术支持,包括远程运维、定期巡检、系统升级、用户培训等。
提供 7x24 小时技术支持,包括远程运维、定期巡检、系统升级、用户培训等。
### 数据安全如何保障?
+65 -36
View File
@@ -1,22 +1,34 @@
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import { X } from 'lucide-react'
export default function Carousel({ images, interval = 3000, themeColor }) {
export default function Carousel({ images, interval = 4000, themeColor }) {
const [current, setCurrent] = useState(0)
const [transitioning, setTransitioning] = useState(false)
const [preview, setPreview] = useState(null) // 预览图片的索引
const [preview, setPreview] = useState(null)
const timerRef = useRef(null)
const goTo = useCallback((index) => {
setTransitioning(true)
setTimeout(() => {
setCurrent(index)
setTimeout(() => setTransitioning(false), 50)
}, 350)
}, [])
const next = useCallback(() => {
if (transitioning) return
setTransitioning(true)
setCurrent(prev => (prev + 1) % images.length)
setTimeout(() => setTransitioning(false), 500)
}, [images.length, transitioning])
goTo((current + 1) % images.length)
}, [current, images.length, transitioning, goTo])
const prev = useCallback(() => {
if (transitioning) return
goTo((current - 1 + images.length) % images.length)
}, [current, images.length, transitioning, goTo])
useEffect(() => {
if (preview !== null) return // 预览时不自动轮播
const timer = setInterval(next, interval)
return () => clearInterval(timer)
if (preview !== null) return
timerRef.current = setInterval(next, interval)
return () => clearInterval(timerRef.current)
}, [next, interval, preview])
const getIndex = (offset) => {
@@ -39,49 +51,66 @@ export default function Carousel({ images, interval = 3000, themeColor }) {
<div className="flex items-center justify-center">
{/* Left - prev */}
<div
className={`relative z-10 w-80 h-44 md:w-96 md:h-52 bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-lg -rotate-6 transition-all duration-500 cursor-pointer hover:opacity-80 ${
transitioning ? 'opacity-70' : 'opacity-90'
}`}
onClick={() => handleImageClick(prevIdx)}
className="relative z-10 w-80 h-44 md:w-96 md:h-52 bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-lg -rotate-6 cursor-pointer hover:opacity-80 transition-all duration-700 ease-[cubic-bezier(0.4,0,0.2,1)]"
style={{
opacity: transitioning ? 0.4 : 0.9,
transform: transitioning ? 'rotate(-6deg) translateX(24px) scale(0.95)' : 'rotate(-6deg)',
}}
onClick={() => { handleImageClick(prevIdx); prev() }}
>
<img
src={images[prevIdx]}
alt=""
className="w-full h-full object-contain"
/>
<img src={images[prevIdx]} alt="" className="w-full h-full object-contain" />
</div>
{/* Center - current */}
<div
className={`relative z-30 w-96 h-52 md:w-[500px] md:h-[280px] -mx-10 md:-mx-14 bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-2xl transition-all duration-500 cursor-pointer ${
transitioning ? 'scale-95' : 'scale-100'
}`}
className="relative z-30 w-96 h-52 md:w-[500px] md:h-[280px] -mx-10 md:-mx-14 bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-2xl cursor-pointer transition-all duration-700 ease-[cubic-bezier(0.4,0,0.2,1)]"
style={{
transform: transitioning ? 'scale(0.92)' : 'scale(1)',
opacity: transitioning ? 0.7 : 1,
}}
onClick={() => handleImageClick(current)}
>
<div className="absolute top-0 left-0 right-0 h-[2px]"
style={{ background: `linear-gradient(90deg, transparent, ${themeColor}, transparent)` }} />
<img
src={images[current]}
alt=""
className="w-full h-full object-contain"
/>
<img src={images[current]} alt="" className="w-full h-full object-contain" />
</div>
{/* Right - next */}
<div
className={`relative z-10 w-80 h-44 md:w-96 md:h-52 bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-lg rotate-6 transition-all duration-500 cursor-pointer hover:opacity-80 ${
transitioning ? 'opacity-70' : 'opacity-90'
}`}
onClick={() => handleImageClick(nextIdx)}
className="relative z-10 w-80 h-44 md:w-96 md:h-52 bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-lg rotate-6 cursor-pointer hover:opacity-80 transition-all duration-700 ease-[cubic-bezier(0.4,0,0.2,1)]"
style={{
opacity: transitioning ? 0.4 : 0.9,
transform: transitioning ? 'rotate(6deg) translateX(-24px) scale(0.95)' : 'rotate(6deg)',
}}
onClick={() => { handleImageClick(nextIdx); next() }}
>
<img
src={images[nextIdx]}
alt=""
className="w-full h-full object-contain"
/>
<img src={images[nextIdx]} alt="" className="w-full h-full object-contain" />
</div>
</div>
{/* Dot indicators */}
<div className="flex justify-center gap-2 mt-6">
{images.map((_, i) => (
<button
key={i}
onClick={() => {
if (i === current || transitioning) return
setTransitioning(true)
setTimeout(() => {
setCurrent(i)
setTimeout(() => setTransitioning(false), 50)
}, 350)
}}
className="rounded-full transition-all duration-500"
style={{
width: i === current ? '24px' : '8px',
height: '8px',
backgroundColor: i === current ? themeColor : '#d1d5db',
}}
/>
))}
</div>
{/* Preview Modal */}
{preview !== null && (
<div
+5 -5
View File
@@ -52,9 +52,9 @@ export default function Navbar() {
left: 10%;
right: 10%;
height: 3px;
background: linear-gradient(90deg, transparent, var(--nav-theme, #3b82f6), transparent);
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
border-radius: 2px;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.5), 0 0 24px rgba(59, 130, 246, 0.2);
box-shadow: 0 0 12px rgba(255,255,255,0.5), 0 0 24px rgba(255,255,255,0.2);
}
.nav-item {
position: relative;
@@ -75,8 +75,8 @@ export default function Navbar() {
}
`}</style>
<nav
className={`sticky top-0 z-50 transition-all duration-500 backdrop-blur-xl ${scrolled ? 'bg-[#0f172a]/95 shadow-[0_4px_30px_rgba(0,0,0,0.2)] border-b border-white/[0.06]' : 'bg-gradient-to-b from-black/40 to-transparent'}`}
style={scrolled && themeColor ? { '--nav-theme': themeColor, backgroundColor: `${themeColor}ee` } : undefined}
className={`sticky top-0 z-50 transition-all duration-500 backdrop-blur-xl ${scrolled ? 'shadow-[0_4px_30px_rgba(0,0,0,0.2)] border-b border-white/[0.06]' : 'bg-gradient-to-b from-black/40 to-transparent'}`}
style={scrolled ? { backgroundColor: themeColor ? `${themeColor}f0` : '#0f172af2' } : undefined}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center h-[80px] overflow-visible">
@@ -139,7 +139,7 @@ export default function Navbar() {
mobileOpen ? 'max-h-[800px]' : 'max-h-0'
}`}
>
<div className="bg-[#0a1628]/95 backdrop-blur-2xl border-t border-white/[0.06] px-4 pb-6 pt-3 space-y-1">
<div className="bg-[#0a1628]/95 backdrop-blur-2xl border-t border-white/[0.06] px-4 pb-6 pt-3 space-y-1" style={scrolled && themeColor ? { backgroundColor: `${themeColor}f0` } : undefined}>
{navItems.map((item) => (
<Link
key={item.to}
+16 -7
View File
@@ -8,8 +8,6 @@ const chartData = [
{ value: 25, name: '实训管理平台' },
];
const color = '#043b8f';
const rotationNeeded = [45, -45, -135, -225];
function normalizeAngle(a) {
@@ -25,12 +23,23 @@ function shortestRotation(from, to) {
return from + diff;
}
export default function RosePieChart({ onSelect, activeIndex }) {
/** 将 hex 颜色转为带透明度的 rgba */
function hexToRgba(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r},${g},${b},${alpha})`;
}
export default function RosePieChart({ onSelect, activeIndex, colors }) {
const containerRef = useRef(null);
const chartRef = useRef(null);
const rotationRef = useRef(0);
const [rotation, setRotation] = useState(0);
// 默认回退颜色
const safeColors = colors?.length === 4 ? colors : ['#60ce93', '#0c86cf', '#aa3130', '#8b50ee'];
useEffect(() => {
if (!containerRef.current) return;
@@ -73,7 +82,7 @@ export default function RosePieChart({ onSelect, activeIndex }) {
},
data: chartData.map((d, i) => ({
...d,
itemStyle: { color, opacity: 1, borderRadius: 6, borderWidth: 0 },
itemStyle: { color: safeColors[i], opacity: 1, borderRadius: 6, borderWidth: 0 },
})),
animationType: 'scale',
animationEasing: 'elasticOut',
@@ -106,9 +115,9 @@ export default function RosePieChart({ onSelect, activeIndex }) {
series: [{
data: chartData.map((d, i) => ({
...d,
label: { color: isActive && activeIndex === i ? '#fff' : '#043b8f' },
label: { color: isActive && activeIndex === i ? '#fff' : safeColors[i] },
itemStyle: {
color: isActive && activeIndex !== i ? color + '30' : color,
color: isActive && activeIndex !== i ? hexToRgba(safeColors[i], 0.2) : safeColors[i],
opacity: 1,
borderRadius: 6,
borderColor: 'rgba(15,23,42,0.8)',
@@ -140,7 +149,7 @@ export default function RosePieChart({ onSelect, activeIndex }) {
transition: 'transform 1s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
<div ref={containerRef} className="w-full h-[420px] md:h-[480px]" />
<div ref={containerRef} className="w-full h-[520px] md:h-[600px]" />
</div>
);
}
+71 -41
View File
@@ -1,3 +1,4 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom';
import HeroSection from '../components/HeroSection'
@@ -82,11 +83,10 @@ const customerCards = [
import { useState, useEffect } from 'react'
export default function Home() {
const [activeCapability, setActiveCapability] = useState(0)
const [tagRadius, setTagRadius] = useState(68)
const activeColor = products[activeCapability]?.themeColor || '#043b8f'
useEffect(() => {
const updateRadius = () => {
@@ -141,19 +141,25 @@ export default function Home() {
{/* Tab bar */}
<ScrollReveal>
<div className="flex justify-center gap-1 mb-12 flex-wrap">
{products.map((p, i) => (
{products.map((p, i) => {
const isActive = activeCapability === i
return (
<button
key={p.slug}
onClick={() => setActiveCapability(i)}
className={`flex items-center gap-1.5 xs:gap-2 px-3 xs:px-4 sm:px-6 py-2 xs:py-3 rounded-lg text-xs xs:text-sm font-medium transition-all border ${activeCapability === i
? 'text-[#043b8f] border-[#043b8f]'
: 'text-[#475569] border-gray-200 hover:text-[#043b8f] hover:border-[#043b8f]'
}`}
className="flex items-center gap-1.5 xs:gap-2 px-3 xs:px-4 sm:px-6 py-2 xs:py-3 rounded-lg text-xs xs:text-sm font-medium transition-all border"
style={{
color: isActive ? p.themeColor : '#475569',
borderColor: isActive ? p.themeColor : '#e5e7eb',
}}
onMouseEnter={(e) => { if (!isActive) { e.target.style.color = p.themeColor; e.target.style.borderColor = p.themeColor; } }}
onMouseLeave={(e) => { if (!isActive) { e.target.style.color = '#475569'; e.target.style.borderColor = '#e5e7eb'; } }}
>
<p.icon className="w-4 h-4 xs:w-5 xs:h-5" style={{ color: activeCapability === i ? '#043b8f' : p.themeColor }} />
<p.icon className="w-4 h-4 xs:w-5 xs:h-5" style={{ color: isActive ? p.themeColor : p.themeColor }} />
{p.name}
</button>
))}
)
})}
</div>
</ScrollReveal>
@@ -162,58 +168,82 @@ export default function Home() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center mb-12">
{/* Left: visual */}
<div className="flex justify-center">
<RosePieChart onSelect={setActiveCapability} activeIndex={activeCapability} />
<RosePieChart onSelect={setActiveCapability} activeIndex={activeCapability} colors={products.map(p => p.themeColor)} />
</div>
{/* Right: text */}
<div className="space-y-4">
{(activeCapability !== null ? [capabilities[activeCapability]] : capabilities).map((c, i) => {
const idx = activeCapability !== null ? activeCapability : i
const color = products[idx]?.themeColor || '#043b8f'
const isActive = activeCapability === idx
return (
<div key={idx} className={`relative p-8 rounded-2xl transition-all duration-500 cursor-pointer group overflow-hidden ${activeCapability === idx
? 'bg-white border border-[#043b8f]/20 shadow-[0_4px_24px_rgba(4,59,199,0.12)]'
: 'hover:bg-gray-50/80 border border-gray-100'
}`} onClick={() => setActiveCapability(idx)}>
{activeCapability === idx && (
<div className="absolute top-0 left-0 right-0 h-[2px]"
style={{ background: 'linear-gradient(90deg, transparent, #043b8f, transparent)' }} />
<div key={idx} className="relative p-8 rounded-2xl transition-all duration-500 cursor-pointer group overflow-hidden"
style={isActive
? { backgroundColor: '#fff', borderColor: color + '33', borderWidth: 1, boxShadow: `0 8px 32px ${color}14, 0 2px 8px ${color}08` }
: {}}
onClick={() => setActiveCapability(idx)}>
{/* Left accent line */}
{isActive && (
<div className="absolute top-6 left-0 bottom-6 w-[3px] rounded-r-full opacity-60"
style={{ background: `linear-gradient(180deg, transparent, ${color}, transparent)` }} />
)}
<div className="absolute top-0 right-0 w-24 h-24 bg-[#043b8f]/[0.03] rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="absolute bottom-0 left-0 w-16 h-16 border border-[#043b8f]/[0.06] rounded-tr-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="flex items-center gap-3 mb-3">
<span className={`flex items-center justify-center w-7 h-7 rounded-md text-[11px] font-black transition-colors duration-300 ${activeCapability === idx
? 'bg-[#043b8f]/[0.08] text-[#043b8f]'
: 'bg-gray-100 text-[#94a3b8]'
}`}>0{idx + 1}</span>
<div className={`h-px flex-1 transition-colors duration-300 ${activeCapability === idx
? 'bg-gradient-to-r from-[#043b8f]/15 to-transparent'
: 'bg-gray-100'
}`} />
{/* Decorative corner blobs */}
<div className="absolute top-0 right-0 w-32 h-32 rounded-bl-full opacity-0 group-hover:opacity-100 transition-all duration-700"
style={{ backgroundColor: color + '06' }} />
<div className="absolute bottom-0 left-0 w-20 h-20 border rounded-tr-3xl opacity-0 group-hover:opacity-100 transition-all duration-700"
style={{ borderColor: color + '0d' }} />
{/* Number badge row */}
<div className="flex items-center gap-3 mb-4">
<span className="flex items-center justify-center w-8 h-8 rounded-lg text-xs font-black transition-all duration-300"
style={isActive
? { backgroundColor: color + '14', color: color, boxShadow: `0 2px 8px ${color}14` }
: { backgroundColor: '#f1f5f9', color: '#94a3b8' }}>0{idx + 1}</span>
<div className="h-px flex-1 transition-all duration-300"
style={isActive
? { background: `linear-gradient(90deg, ${color}30, ${color}08, transparent)` }
: { backgroundColor: '#f1f5f9' }} />
{/* Product icon */}
{React.createElement(products[idx].icon, {
className: "w-4 h-4 transition-all duration-300",
style: { color: isActive ? color : '#cbd5e1' }
})}
</div>
<h3 className={`text-lg xs:text-xl sm:text-2xl font-bold transition-colors duration-300 ${activeCapability === idx ? 'text-[#043b8f]' : 'text-[#0f172a]'
}`}>{c.title}</h3>
<p className={`text-sm xs:text-base mt-1.5 font-medium transition-colors duration-300 ${activeCapability === idx ? 'text-[#043b8f]/60' : 'text-[#94a3b8]'
}`}>{c.sub}</p>
<p className="text-sm xs:text-base text-[#334155] leading-[2] mt-4">{c.desc}</p>
{/* Title */}
<h3 className="text-xl xs:text-2xl sm:text-3xl font-bold transition-all duration-300 tracking-tight"
style={{ color: isActive ? color : '#0f172a' }}>{c.title}</h3>
{/* Subtitle */}
<p className="text-sm xs:text-base mt-1.5 font-medium transition-all duration-300"
style={{ color: isActive ? color + '99' : '#94a3b8' }}>{c.sub}</p>
{/* Divider */}
<div className="my-4 h-px w-12 transition-all duration-500"
style={{ background: isActive ? `linear-gradient(90deg, ${color}40, transparent)` : '#f1f5f9' }} />
{/* Description */}
<p className="text-sm xs:text-base text-[#475569] leading-[1.9]">{c.desc}</p>
{/* Tags */}
{c.tip && (
<div className="mt-5 flex flex-wrap gap-2">
{c.tip.split('、').map((t, ti) => (
<span key={ti} className={`px-3 py-1 xs:px-4 xs:py-1.5 text-xs xs:text-sm font-medium rounded-md transition-all duration-200 ${activeCapability === idx
? 'text-[#043b8f] bg-[#043b8f]/[0.08]'
: 'text-[#94a3b8] bg-gray-50'
}`}>
<span key={ti} className="px-3 py-1.5 text-xs font-medium rounded-full transition-all duration-300"
style={isActive
? { color: color, backgroundColor: color + '0f', borderColor: color + '1a', borderWidth: 1 }
: { color: '#94a3b8', backgroundColor: '#f8fafc', borderColor: '#e2e8f0', borderWidth: 1 }}>
{t}
</span>
))}
</div>
)}
{activeCapability === idx && (
<div className="mt-5 flex justify-end">
{/* View more link */}
{isActive && (
<div className="mt-6 flex justify-end">
<Link to={`/product/${c.slug}`}
onClick={(e) => e.stopPropagation()}
className="text-sm text-[#043b8f]/60 hover:text-[#043b8f] hover:underline flex items-center gap-1 transition-colors duration-200">
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300"
style={{ color: color, backgroundColor: color + '0a' }}
onMouseEnter={(e) => { e.target.style.backgroundColor = color + '1a'; }}
onMouseLeave={(e) => { e.target.style.backgroundColor = color + '0a'; }}>
查看全部
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" /></svg>
<svg className="w-3.5 h-3.5 transition-transform duration-300 group-hover:translate-x-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" /></svg>
</Link>
</div>
)}
+8 -8
View File
@@ -161,19 +161,19 @@ export default function ProductPage() {
<div className="space-y-5">
{product.painPoints.map((item, i) => (
<ScrollReveal key={i} delay={i * 0.08}>
<div className="relative bg-white rounded-2xl p-10 border border-gray-100 hover:shadow-lg hover:shadow-gray-200/50 transition-all duration-300 group h-full">
<div className="relative bg-white rounded-2xl p-6 xs:p-8 md:p-10 border border-gray-100 hover:shadow-lg hover:shadow-gray-200/50 transition-all duration-300 group h-full">
<div className="absolute top-0 left-0 right-0 h-1 rounded-t-2 bg-[var(--theme)] opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
{/* Pain point */}
<div className="flex items-start gap-3 mb-5 pb-5 border-b border-dashed border-gray-200">
<div className="shrink-0 w-9 h-9 rounded-full bg-red-50 flex items-center justify-center">
<AlertCircle className="w-5 h-5 text-red-500" />
<div className="flex items-start gap-2 xs:gap-3 mb-4 xs:mb-5 pb-4 xs:pb-5 border-b border-dashed border-gray-200">
<div className="shrink-0 w-7 h-7 xs:w-9 xs:h-9 rounded-full bg-red-50 flex items-center justify-center">
<AlertCircle className="w-4 h-4 xs:w-5 xs:h-5 text-red-500" />
</div>
<h3 className="text-lg font-bold text-[#0f172a] leading-relaxed">{item.title}</h3>
<h3 className="text-base xs:text-lg font-bold text-[#0f172a] leading-relaxed">{item.title}</h3>
</div>
{/* Solution */}
<div className="ml-11 pl-5 border-l-2 border-[var(--theme)] bg-[rgba(var(--theme-r),var(--theme-g),var(--theme-b),0.03)] rounded-r-lg py-4 pr-4">
<span className="inline-block text-sm font-semibold text-white bg-[var(--theme)] rounded px-3 py-1.5 mb-3">解决方案</span>
<p className="text-[#334155] text-xl leading-[1.9]">{item.desc}</p>
<div className="ml-0 sm:ml-6 md:ml-11 pl-3 xs:pl-5 border-l-2 border-[var(--theme)] bg-[rgba(var(--theme-r),var(--theme-g),var(--theme-b),0.03)] rounded-r-lg py-3 xs:py-4 pr-3 xs:pr-4">
<span className="inline-block text-xs xs:text-sm font-semibold text-white bg-[var(--theme)] rounded px-2 xs:px-3 py-1 xs:py-1.5 mb-2 xs:mb-3">解决方案</span>
<p className="text-[#334155] text-sm xs:text-base md:text-lg xl:text-xl leading-[1.7] xs:leading-[1.9]">{item.desc}</p>
</div>
</div>
</ScrollReveal>
+16 -2
View File
@@ -3,6 +3,20 @@ import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins: [
react(),
tailwindcss(),
{
name: 'txt-charset',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url.endsWith('.txt')) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
}
next()
})
},
},
],
base: '/',
})
})