监听滑动动画
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)`,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
@@ -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]">
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user