diff --git a/website/index.html b/website/index.html index cd759bd..d3826c4 100644 --- a/website/index.html +++ b/website/index.html @@ -11,4 +11,44 @@
+ + + diff --git a/website/src/components/HeroSection.jsx b/website/src/components/HeroSection.jsx index 11f471d..a4ebf98 100644 --- a/website/src/components/HeroSection.jsx +++ b/website/src/components/HeroSection.jsx @@ -1,85 +1,85 @@ import { ArrowRight } from 'lucide-react'; -import { useEffect, useRef } from 'react'; +// import { useEffect, useRef } from 'react'; -function ParticleNetwork() { - const canvasRef = useRef(null); +// function ParticleNetwork() { +// const canvasRef = useRef(null); - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - let animId; - let particles = []; - const COUNT = 35; - const CONNECT_DIST = 150; +// useEffect(() => { +// const canvas = canvasRef.current; +// if (!canvas) return; +// const ctx = canvas.getContext('2d'); +// let animId; +// let particles = []; +// const COUNT = 35; +// const CONNECT_DIST = 150; - function resize() { - canvas.width = canvas.offsetWidth * window.devicePixelRatio; - canvas.height = canvas.offsetHeight * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } +// function resize() { +// canvas.width = canvas.offsetWidth * window.devicePixelRatio; +// canvas.height = canvas.offsetHeight * window.devicePixelRatio; +// ctx.scale(window.devicePixelRatio, window.devicePixelRatio); +// } - function init() { - resize(); - particles = []; - for (let i = 0; i < COUNT; i++) { - particles.push({ - x: Math.random() * canvas.offsetWidth, - y: Math.random() * canvas.offsetHeight, - vx: (Math.random() - 0.5) * 0.15, - vy: (Math.random() - 0.5) * 0.15, - r: Math.random() * 1 + 0.3, - color: Math.random() > 0.8 ? '#f08519' : '#004ec4', - }); - } - } +// function init() { +// resize(); +// particles = []; +// for (let i = 0; i < COUNT; i++) { +// particles.push({ +// x: Math.random() * canvas.offsetWidth, +// y: Math.random() * canvas.offsetHeight, +// vx: (Math.random() - 0.5) * 0.15, +// vy: (Math.random() - 0.5) * 0.15, +// r: Math.random() * 1 + 0.3, +// color: Math.random() > 0.8 ? '#f08519' : '#004ec4', +// }); +// } +// } - function draw() { - const w = canvas.offsetWidth; - const h = canvas.offsetHeight; - ctx.clearRect(0, 0, w, h); +// function draw() { +// const w = canvas.offsetWidth; +// const h = canvas.offsetHeight; +// ctx.clearRect(0, 0, w, h); - for (let i = 0; i < particles.length; i++) { - const p = particles[i]; - p.x += p.vx; - p.y += p.vy; - if (p.x < 0 || p.x > w) p.vx *= -1; - if (p.y < 0 || p.y > h) p.vy *= -1; +// for (let i = 0; i < particles.length; i++) { +// const p = particles[i]; +// p.x += p.vx; +// p.y += p.vy; +// if (p.x < 0 || p.x > w) p.vx *= -1; +// if (p.y < 0 || p.y > h) p.vy *= -1; - ctx.beginPath(); - ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); - ctx.fillStyle = p.color; - ctx.globalAlpha = 0.15; - ctx.fill(); +// ctx.beginPath(); +// ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); +// ctx.fillStyle = p.color; +// ctx.globalAlpha = 0.15; +// ctx.fill(); - for (let j = i + 1; j < particles.length; j++) { - const p2 = particles[j]; - const dx = p.x - p2.x; - const dy = p.y - p2.y; - const dist = Math.sqrt(dx * dx + dy * dy); - if (dist < CONNECT_DIST) { - ctx.beginPath(); - ctx.moveTo(p.x, p.y); - ctx.lineTo(p2.x, p2.y); - ctx.strokeStyle = '#004ec4'; - ctx.globalAlpha = 0.03 * (1 - dist / CONNECT_DIST); - ctx.lineWidth = 0.5; - ctx.stroke(); - } - } - } - ctx.globalAlpha = 1; - animId = requestAnimationFrame(draw); - } +// for (let j = i + 1; j < particles.length; j++) { +// const p2 = particles[j]; +// const dx = p.x - p2.x; +// const dy = p.y - p2.y; +// const dist = Math.sqrt(dx * dx + dy * dy); +// if (dist < CONNECT_DIST) { +// ctx.beginPath(); +// ctx.moveTo(p.x, p.y); +// ctx.lineTo(p2.x, p2.y); +// ctx.strokeStyle = '#004ec4'; +// ctx.globalAlpha = 0.03 * (1 - dist / CONNECT_DIST); +// ctx.lineWidth = 0.5; +// ctx.stroke(); +// } +// } +// } +// ctx.globalAlpha = 1; +// animId = requestAnimationFrame(draw); +// } - init(); - draw(); - window.addEventListener('resize', () => { resize(); }); - return () => cancelAnimationFrame(animId); - }, []); +// init(); +// draw(); +// window.addEventListener('resize', () => { resize(); }); +// return () => cancelAnimationFrame(animId); +// }, []); - return ; -} +// return ; +// } export default function HeroSection() { return ( @@ -108,7 +108,7 @@ export default function HeroSection() { style={{ animation: 'pulse-slow 12s ease-in-out 3s infinite' }} /> - + {/* Top & bottom edge lines */}
@@ -230,20 +230,7 @@ export default function HeroSection() {
- {/* Scroll indicator */} -
- - - SCROLL - -
-
-
-
-
+ ); } diff --git a/website/src/components/ScrollReveal.jsx b/website/src/components/ScrollReveal.jsx index 646f594..ba80014 100644 --- a/website/src/components/ScrollReveal.jsx +++ b/website/src/components/ScrollReveal.jsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; - +import { useLocation } from 'react-router-dom' export default function ScrollReveal({ children, className = '', @@ -14,26 +14,51 @@ export default function ScrollReveal({ }) { const ref = useRef(null); const [visible, setVisible] = useState(false); + const { pathname } = useLocation() + + + useEffect(() => { + const timer = setTimeout(() => { + setVisible(false); + }, 0); + return () => { + clearTimeout(timer); + }; + }, [pathname]); + + useEffect(() => { const el = ref.current; if (!el) return; - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - setVisible(true); - if (once) observer.unobserve(el); - } else if (!once) { - setVisible(false); - } - }, - { threshold, rootMargin: '0px 0px -60px 0px' } - ); + let observer; + let cancelled = false; - observer.observe(el); - return () => observer.disconnect(); - }, [once, threshold]); + const timer = setTimeout(() => { + if (cancelled) return; + + observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + if (once) observer.unobserve(el); + } else if (!once) { + setVisible(false); + } + }, + { threshold, rootMargin: '0px 0px -60px 0px' } + ); + + observer.observe(el); + }, 0); + + return () => { + cancelled = true; + clearTimeout(timer); + if (observer) observer.disconnect(); + }; + }, [once, threshold, pathname]); const transforms = { up: `translateY(${distance}px)`, diff --git a/website/src/components/StatNumber.jsx b/website/src/components/StatNumber.jsx new file mode 100644 index 0000000..30f4d79 --- /dev/null +++ b/website/src/components/StatNumber.jsx @@ -0,0 +1,43 @@ + +import { useState, useEffect, useRef } from 'react' +import { useLocation } from 'react-router-dom' +/* ─── Animated counter ─── */ +export default function StatNumber({ target, suffix }) { + const [count, setCount] = useState(0) + const ref = useRef(null) + const started = useRef(false) + const { pathname } = useLocation() + + useEffect(() => { + started.current = false + setTimeout(() => setCount(0), 0) + }, [pathname]) + + + useEffect(() => { + const el = ref.current + if (!el) return + const obs = new IntersectionObserver(([e]) => { + if (e.isIntersecting && !started.current) { + started.current = true + const dur = 2000 + const start = performance.now() + const num = parseInt(target) + function tick(now) { + const p = Math.min((now - start) / dur, 1) + setCount(Math.round((1 - Math.pow(1 - p, 3)) * num)) + if (p < 1) requestAnimationFrame(tick) + } + requestAnimationFrame(tick) + obs.unobserve(el) + } + }, { threshold: 0.3 }) + obs.observe(el) + return () => obs.disconnect() + }, [target, pathname]) + + + + + return {count}{suffix} +} diff --git a/website/src/data/products.js b/website/src/data/products.js index 55c44cc..53ead4d 100644 --- a/website/src/data/products.js +++ b/website/src/data/products.js @@ -15,9 +15,9 @@ export const products = [ brief: '面向职业院校实习就业管理的专业化平台,覆盖实习计划制定、过程跟踪、数据采集、鉴定评价全流程,已服务广西多所职业院校,帮助学校轻松应对教育厅数据上报要求。', tags: ['实习计划管理', 'GPS签到+电子围栏', '问卷式日志', '多维鉴定评价'], stats: [ - { num: '20+所', label: '覆盖学校' }, - { num: '50,000+', label: '服务学生' }, - { num: '100万+', label: '累计签到' }, + { num: '20', suffix: '+所', label: '覆盖学校' }, + { num: '50,000', suffix: '+', label: '服务学生' }, + { num: '100', suffix: '万+', label: '累计签到' }, ], features: [ { emoji: '📋', title: '实习计划管理', desc: '按学期、专业、班级灵活制定实习计划,绑定不同实习规则,实现多批次差异化管理' }, @@ -53,9 +53,9 @@ export const products = [ brief: '面向高校及科研院所的全生命周期科研业务管理平台。覆盖项目申报、评审立项、经费管控、成果登记到数据统计的全流程数字化,已帮助多所高校实现科研管理从纸质流转到"一网通办"的跨越。', tags: ['在线申报审批', '智能查重防重复', '经费精细管控', '成果自动赋分'], stats: [ - { num: '10+所', label: '服务高校' }, - { num: '3,000+', label: '管理项目' }, - { num: '8,000+', label: '登记成果' }, + { num: '10', suffix: '+所', label: '服务高校' }, + { num: '3000', suffix: '+', label: '管理项目' }, + { num: '8000', suffix: '+', label: '登记成果' }, ], features: [ { emoji: '📝', title: '在线申报审批', desc: '项目申报、审核、立项全流程在线办理,智能校验引擎自动检查相似度和限项规则' }, @@ -93,9 +93,9 @@ export const products = [ brief: '面向高校及职业院校各级党组织的智慧党建管理系统。以"党业融合、指标驱动、全程留痕"为核心理念,将三会一课、党员活动、工作记录等日常工作自动关联考核指标,实现年终考核"零重复填报"。', tags: ['三级考核指标', '三会一课数字化', '万能工作记录', '实时进度看板'], stats: [ - { num: '150+', label: '服务支部' }, - { num: '5,000+', label: '管理党员' }, - { num: '200+', label: '考核指标' }, + { num: '150', suffix: '+所', label: '服务支部' }, + { num: '5000', suffix: '+', label: '管理党员' }, + { num: '200', suffix: '+', label: '考核指标' }, ], features: [ { emoji: '📊', title: '三级考核指标', desc: '灵活构建校—院—支部三级指标树,分值自动汇总,考核部门与执行部门逐一对应' }, @@ -141,9 +141,9 @@ export const products = [ brief: '专为职业院校商科专业群打造的实训项目管理系统。以"项目—模块—任务"三级结构为骨架,以"咖豆积分+技能画像"为引擎,将抽象的五级进阶培养模式变成可量化、可追踪、可激励的线上实战平台。', tags: ['三级精细管理', '咖豆积分引擎', '五级自动晋级', '动态技能画像'], stats: [ - { num: '500+', label: '实训项目库' }, - { num: '15+', label: '覆盖专业' }, - { num: '200万+', label: '累计咖豆' }, + { num: '500', suffix: '+', label: '实训项目库' }, + { num: '15', suffix: '+', label: '覆盖专业' }, + { num: '200', suffix: '万+', label: '累计咖豆' }, ], features: [ { emoji: '📦', title: '三级精细管理', desc: '项目→模块→任务三级结构,权重灵活分配,实训教学内容标准化、可复用' }, diff --git a/website/src/pages/Home.jsx b/website/src/pages/Home.jsx index e368c4d..e17792f 100644 --- a/website/src/pages/Home.jsx +++ b/website/src/pages/Home.jsx @@ -5,7 +5,8 @@ import ScrollReveal from '../components/ScrollReveal' import { products } from '../data/products' import RosePieChart from '../components/RosePieChart' import hunBg from '../assets/images/hun.jpg' -import {ArrowRight, Play, Users, Building2} from 'lucide-react' +import { ArrowRight, Play, Users, Building2 } from 'lucide-react' +import StatNumber from '../components/StatNumber' /* ─── Section 3: 数据亮点 ─── */ const stats = [ @@ -47,70 +48,41 @@ const customerCards = [ type: 'school', icon: Building2, title: 'ISO 9001 质量管理体系', - + }, { type: 'video', icon: Play, title: 'ISO 14001 环境管理体系', - + }, { type: 'satisfaction', icon: Users, title: 'ISO 45001 职业健康安全管理体系', - + }, - + { type: 'satisfaction', icon: Users, title: 'ISO 27001 信息安全管理体系', - + }, { type: 'satisfaction', icon: Users, title: 'ISO 20000 信息技术服务管理体系', - + }, ] -/* ─── Animated counter ─── */ -function StatNumber({ target, suffix }) { - const [count, setCount] = useState(0) - const ref = useRef(null) - const started = useRef(false) - useEffect(() => { - const el = ref.current - if (!el) return - const obs = new IntersectionObserver(([e]) => { - if (e.isIntersecting && !started.current) { - started.current = true - const dur = 2000 - const start = performance.now() - const num = parseInt(target) - function tick(now) { - const p = Math.min((now - start) / dur, 1) - setCount(Math.round((1 - Math.pow(1 - p, 3)) * num)) - if (p < 1) requestAnimationFrame(tick) - } - requestAnimationFrame(tick) - obs.unobserve(el) - } - }, { threshold: 0.3 }) - obs.observe(el) - return () => obs.disconnect() - }, [target]) - return {count}{suffix} -} - -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect } from 'react' export default function Home() { const [activeCapability, setActiveCapability] = useState(0) @@ -226,11 +198,10 @@ export default function Home() { {c.tip && (
{c.tip.split('、').map((t, ti) => ( - + }`}> {t} ))} @@ -372,7 +343,7 @@ export default function Home() {
- + {/* ─── 7. 客户认可区(白底) ─── */}
diff --git a/website/src/pages/ProductPage.jsx b/website/src/pages/ProductPage.jsx index 33217cd..fcceae9 100644 --- a/website/src/pages/ProductPage.jsx +++ b/website/src/pages/ProductPage.jsx @@ -2,6 +2,7 @@ import { useParams, Link } from 'react-router-dom' import { ChevronDown, ArrowRight, Shield, Clock, Headphones, AlertCircle } from 'lucide-react' import { getProduct } from '../data/products' import ScrollReveal from '../components/ScrollReveal' +import StatNumber from '../components/StatNumber' export default function ProductPage() { const { slug } = useParams() @@ -54,7 +55,9 @@ export default function ProductPage() { style={{ backgroundColor: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(8px)' }}> {product.stats.map((s, i) => (
-
{s.num}
+
+ +
{s.label}
))}