监听滑动动画

This commit is contained in:
小潘
2026-06-08 17:36:30 +08:00
parent ec6222d183
commit 92c84e9dab
7 changed files with 224 additions and 155 deletions
+40
View File
@@ -11,4 +11,44 @@
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
<script>
window.difyChatbotConfig = {
token: 'DJdSTFBWmkh0OOAb',
baseUrl: 'https://szdify.2009xc.com',
inputs: {
// You can define the inputs from the Start node here
// key is the variable name
// e.g.
// name: "NAME"
},
systemVariables: {
// user_id: 'YOU CAN DEFINE USER ID HERE',
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
},
userVariables: {
// avatar_url: 'YOU CAN DEFINE USER AVATAR URL HERE',
// name: 'YOU CAN DEFINE USER NAME HERE',
},
}
</script>
<script src="https://szdify.2009xc.com/embed.min.js" id="DJdSTFBWmkh0OOAb" defer>
</script>
<style>
#dify-chatbot-bubble-button {
background-color: #043b8f !important;
position: fixed !important;
bottom: 36px !important;
right: 24px !important;
z-index: 9999 !important;
}
#dify-chatbot-bubble-window {
width: 24rem !important;
height: 40rem !important;
position: fixed !important;
bottom: 80px !important;
right: 24px !important;
z-index: 9999 !important;
}
</style>
</html>
+73 -86
View File
@@ -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 <canvas ref={canvasRef} className="absolute inset-0 w-full h-full z-[1]" />;
}
// return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full z-[1] border-amber-500 " />;
// }
export default function HeroSection() {
return (
@@ -108,7 +108,7 @@ export default function HeroSection() {
style={{ animation: 'pulse-slow 12s ease-in-out 3s infinite' }} />
</div>
<ParticleNetwork />
{/* Top & bottom edge lines */}
<div className="absolute top-0 left-0 right-0 z-10 h-px bg-gradient-to-r from-transparent via-white/[0.06] to-transparent" />
@@ -230,20 +230,7 @@ export default function HeroSection() {
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-10"
style={{ animation: 'fadeIn 1s ease-out 1.8s both' }}>
<a href="#data" className="flex flex-col items-center gap-3 group">
<span className="text-[9px] font-extralight text-white/15 group-hover:text-white/30 transition-colors duration-700"
style={{ letterSpacing: '0.6em' }}>
SCROLL
</span>
<div className="w-px h-10 bg-white/[0.06] relative overflow-hidden rounded-full">
<div className="absolute w-full h-4 bg-gradient-to-b from-[#004ec4]/60 to-transparent rounded-full"
style={{ animation: 'scrollFlow 2.5s ease-in-out infinite' }} />
</div>
</a>
</div>
</section>
);
}
+40 -15
View File
@@ -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)`,
+43
View File
@@ -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 <span ref={ref}>{count}{suffix}</span>
}
+12 -12
View File
@@ -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: '项目→模块→任务三级结构,权重灵活分配,实训教学内容标准化、可复用' },
+12 -41
View File
@@ -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 <span ref={ref}>{count}{suffix}</span>
}
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 && (
<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
<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'
}`}>
}`}>
{t}
</span>
))}
@@ -372,7 +343,7 @@ export default function Home() {
</div>
</section>
{/* ─── 7. 客户认可区(白底) ─── */}
<section className="py-20 px-4 bg-[#f8fafc]">
+4 -1
View File
@@ -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) => (
<div key={i} className={`flex flex-col items-center justify-center px-6 md:px-10 py-5 ${i < product.stats.length - 1 ? 'border-r border-white/10' : ''}`}>
<div className="text-2xl md:text-3xl font-extrabold tracking-tight text-white">{s.num}</div>
<div className="text-2xl md:text-3xl font-extrabold tracking-tight text-white">
<StatNumber target={s.num} suffix={s.suffix} />
</div>
<div className="text-xs text-white/50 mt-2 font-medium">{s.label}</div>
</div>
))}