2025-09-08 14:05:26 +08:00
|
|
|
|
// 2024长三角国际新能源汽车与智能交通产业博览会 - 交互脚本
|
|
|
|
|
|
|
|
|
|
|
|
// 页面加载完成后初始化
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
// 隐藏页面加载器
|
|
|
|
|
|
hidePageLoader();
|
|
|
|
|
|
|
|
|
|
|
|
initNavbar();
|
|
|
|
|
|
initAnimations();
|
|
|
|
|
|
initCounters();
|
|
|
|
|
|
initScrollEffects();
|
|
|
|
|
|
initRippleEffect();
|
|
|
|
|
|
initLazyLoading();
|
|
|
|
|
|
initFormValidation();
|
|
|
|
|
|
initCharts();
|
|
|
|
|
|
initTimeline();
|
|
|
|
|
|
initInteractiveElements();
|
|
|
|
|
|
handleImageErrors();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-10 02:35:16 +08:00
|
|
|
|
// 隐藏页面加载器(兼容新的加载器)
|
2025-09-08 14:05:26 +08:00
|
|
|
|
function hidePageLoader() {
|
2025-09-10 02:35:16 +08:00
|
|
|
|
const loader = document.getElementById('pageLoader') || document.getElementById('page-loader');
|
2025-09-08 14:05:26 +08:00
|
|
|
|
if (loader) {
|
2025-09-10 02:35:16 +08:00
|
|
|
|
// 如果是新的加载器,使用其自带的隐藏方法
|
|
|
|
|
|
if (loader.id === 'page-loader' && window.PageLoader) {
|
|
|
|
|
|
window.PageLoader.hideLoader();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 旧的加载器逻辑
|
2025-09-08 14:05:26 +08:00
|
|
|
|
loader.style.opacity = '0';
|
|
|
|
|
|
loader.style.transition = 'opacity 0.5s ease-out';
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
loader.style.display = 'none';
|
|
|
|
|
|
document.body.style.overflow = 'auto';
|
|
|
|
|
|
document.body.classList.remove('loading');
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有找到加载器,也要确保恢复滚动
|
|
|
|
|
|
document.body.style.overflow = 'auto';
|
|
|
|
|
|
document.body.classList.remove('loading');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果页面加载超过3秒,强制隐藏加载器
|
|
|
|
|
|
window.addEventListener('load', function() {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
hidePageLoader();
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 导航栏交互
|
|
|
|
|
|
function initNavbar() {
|
|
|
|
|
|
const navbar = document.querySelector('.navbar');
|
|
|
|
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
|
|
|
|
|
const mobileMenuBtn = document.querySelector('.mobile-menu-btn');
|
|
|
|
|
|
const mobileMenu = document.querySelector('.mobile-menu');
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动时改变导航栏样式
|
|
|
|
|
|
window.addEventListener('scroll', () => {
|
|
|
|
|
|
if (window.scrollY > 100) {
|
|
|
|
|
|
navbar?.classList.add('scrolled');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
navbar?.classList.remove('scrolled');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 高亮当前页面链接
|
|
|
|
|
|
const currentPath = window.location.pathname;
|
|
|
|
|
|
navLinks.forEach(link => {
|
|
|
|
|
|
if (link.getAttribute('href') === currentPath) {
|
|
|
|
|
|
link.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 移动端菜单切换
|
|
|
|
|
|
mobileMenuBtn?.addEventListener('click', () => {
|
|
|
|
|
|
mobileMenu?.classList.toggle('open');
|
|
|
|
|
|
mobileMenuBtn.classList.toggle('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 平滑滚动到锚点
|
|
|
|
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
|
|
|
|
anchor.addEventListener('click', function(e) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const target = document.querySelector(this.getAttribute('href'));
|
|
|
|
|
|
if (target) {
|
|
|
|
|
|
target.scrollIntoView({
|
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
|
block: 'start'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 动画初始化
|
|
|
|
|
|
function initAnimations() {
|
|
|
|
|
|
// Intersection Observer for fade-in animations
|
|
|
|
|
|
const observerOptions = {
|
|
|
|
|
|
threshold: 0.1,
|
|
|
|
|
|
rootMargin: '0px 0px -50px 0px'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
|
entry.target.classList.add('animate-in');
|
|
|
|
|
|
observer.unobserve(entry.target);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, observerOptions);
|
|
|
|
|
|
|
|
|
|
|
|
// 观察所有需要动画的元素
|
|
|
|
|
|
document.querySelectorAll('.fade-in, .slide-in-left, .slide-in-right, .grid-item').forEach(el => {
|
|
|
|
|
|
observer.observe(el);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 数字计数器动画
|
|
|
|
|
|
function initCounters() {
|
|
|
|
|
|
const counters = document.querySelectorAll('.counter');
|
|
|
|
|
|
const speed = 200; // 动画速度
|
|
|
|
|
|
|
|
|
|
|
|
const countUp = (counter) => {
|
|
|
|
|
|
const target = +counter.getAttribute('data-target');
|
|
|
|
|
|
const increment = target / speed;
|
|
|
|
|
|
|
|
|
|
|
|
const updateCount = () => {
|
|
|
|
|
|
const count = +counter.innerText.replace(/[^0-9]/g, '');
|
|
|
|
|
|
|
|
|
|
|
|
if (count < target) {
|
|
|
|
|
|
counter.innerText = Math.ceil(count + increment).toLocaleString();
|
|
|
|
|
|
setTimeout(updateCount, 1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
counter.innerText = target.toLocaleString();
|
|
|
|
|
|
|
|
|
|
|
|
// 添加单位
|
|
|
|
|
|
const unit = counter.getAttribute('data-unit');
|
|
|
|
|
|
if (unit) {
|
|
|
|
|
|
counter.innerText += unit;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
updateCount();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Intersection Observer 触发计数
|
|
|
|
|
|
const counterObserver = new IntersectionObserver((entries) => {
|
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
|
countUp(entry.target);
|
|
|
|
|
|
counterObserver.unobserve(entry.target);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, { threshold: 0.5 });
|
|
|
|
|
|
|
|
|
|
|
|
counters.forEach(counter => {
|
|
|
|
|
|
counter.innerText = '0';
|
|
|
|
|
|
counterObserver.observe(counter);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动效果
|
|
|
|
|
|
function initScrollEffects() {
|
|
|
|
|
|
let lastScrollTop = 0;
|
|
|
|
|
|
const header = document.querySelector('header');
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('scroll', () => {
|
|
|
|
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏/显示导航栏
|
|
|
|
|
|
if (scrollTop > lastScrollTop && scrollTop > 300) {
|
|
|
|
|
|
header?.classList.add('hide');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
header?.classList.remove('hide');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
|
|
|
|
|
|
|
|
|
|
|
|
// 视差效果
|
|
|
|
|
|
const parallaxElements = document.querySelectorAll('.parallax');
|
|
|
|
|
|
parallaxElements.forEach(el => {
|
|
|
|
|
|
const speed = el.getAttribute('data-speed') || 0.5;
|
|
|
|
|
|
const yPos = -(scrollTop * speed);
|
|
|
|
|
|
el.style.transform = `translateY(${yPos}px)`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 进度条
|
|
|
|
|
|
const progressBar = document.querySelector('.progress-bar');
|
|
|
|
|
|
if (progressBar) {
|
|
|
|
|
|
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
|
|
|
|
|
|
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
|
|
|
|
|
const scrolled = (winScroll / height) * 100;
|
|
|
|
|
|
progressBar.style.width = scrolled + '%';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 波纹效果
|
|
|
|
|
|
function initRippleEffect() {
|
|
|
|
|
|
document.querySelectorAll('.btn, .card').forEach(element => {
|
|
|
|
|
|
element.addEventListener('click', function(e) {
|
|
|
|
|
|
const ripple = document.createElement('span');
|
|
|
|
|
|
ripple.classList.add('ripple');
|
|
|
|
|
|
|
|
|
|
|
|
const rect = this.getBoundingClientRect();
|
|
|
|
|
|
const size = Math.max(rect.width, rect.height);
|
|
|
|
|
|
const x = e.clientX - rect.left - size / 2;
|
|
|
|
|
|
const y = e.clientY - rect.top - size / 2;
|
|
|
|
|
|
|
|
|
|
|
|
ripple.style.width = ripple.style.height = size + 'px';
|
|
|
|
|
|
ripple.style.left = x + 'px';
|
|
|
|
|
|
ripple.style.top = y + 'px';
|
|
|
|
|
|
|
|
|
|
|
|
this.appendChild(ripple);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
ripple.remove();
|
|
|
|
|
|
}, 600);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 懒加载
|
|
|
|
|
|
function initLazyLoading() {
|
|
|
|
|
|
const images = document.querySelectorAll('img[data-src]');
|
|
|
|
|
|
|
|
|
|
|
|
const imageObserver = new IntersectionObserver((entries) => {
|
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
|
const img = entry.target;
|
|
|
|
|
|
img.src = img.dataset.src;
|
|
|
|
|
|
img.removeAttribute('data-src');
|
|
|
|
|
|
imageObserver.unobserve(img);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加加载完成动画
|
|
|
|
|
|
img.addEventListener('load', () => {
|
|
|
|
|
|
img.classList.add('loaded');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, {
|
|
|
|
|
|
rootMargin: '50px 0px'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
images.forEach(img => imageObserver.observe(img));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 表单验证
|
|
|
|
|
|
function initFormValidation() {
|
|
|
|
|
|
const forms = document.querySelectorAll('form');
|
|
|
|
|
|
|
|
|
|
|
|
forms.forEach(form => {
|
|
|
|
|
|
form.addEventListener('submit', function(e) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
let isValid = true;
|
|
|
|
|
|
const inputs = form.querySelectorAll('input[required], textarea[required]');
|
|
|
|
|
|
|
|
|
|
|
|
inputs.forEach(input => {
|
|
|
|
|
|
if (!input.value.trim()) {
|
|
|
|
|
|
isValid = false;
|
|
|
|
|
|
input.classList.add('error');
|
|
|
|
|
|
showError(input, '此字段为必填项');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
input.classList.remove('error');
|
|
|
|
|
|
clearError(input);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 邮箱验证
|
|
|
|
|
|
if (input.type === 'email' && input.value) {
|
|
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
|
|
if (!emailRegex.test(input.value)) {
|
|
|
|
|
|
isValid = false;
|
|
|
|
|
|
input.classList.add('error');
|
|
|
|
|
|
showError(input, '请输入有效的邮箱地址');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 电话验证
|
|
|
|
|
|
if (input.type === 'tel' && input.value) {
|
|
|
|
|
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
|
|
|
|
|
if (!phoneRegex.test(input.value)) {
|
|
|
|
|
|
isValid = false;
|
|
|
|
|
|
input.classList.add('error');
|
|
|
|
|
|
showError(input, '请输入有效的手机号码');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (isValid) {
|
|
|
|
|
|
// 显示成功消息
|
|
|
|
|
|
showSuccess('表单提交成功!');
|
|
|
|
|
|
form.reset();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 实时验证
|
|
|
|
|
|
form.querySelectorAll('input, textarea').forEach(input => {
|
|
|
|
|
|
input.addEventListener('blur', function() {
|
|
|
|
|
|
validateInput(this);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
input.addEventListener('input', function() {
|
|
|
|
|
|
if (this.classList.contains('error')) {
|
|
|
|
|
|
validateInput(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示错误信息
|
|
|
|
|
|
function showError(input, message) {
|
|
|
|
|
|
const errorEl = input.nextElementSibling;
|
|
|
|
|
|
if (errorEl && errorEl.classList.contains('error-message')) {
|
|
|
|
|
|
errorEl.textContent = message;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const error = document.createElement('span');
|
|
|
|
|
|
error.classList.add('error-message');
|
|
|
|
|
|
error.textContent = message;
|
|
|
|
|
|
input.parentNode.insertBefore(error, input.nextSibling);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除错误信息
|
|
|
|
|
|
function clearError(input) {
|
|
|
|
|
|
const errorEl = input.nextElementSibling;
|
|
|
|
|
|
if (errorEl && errorEl.classList.contains('error-message')) {
|
|
|
|
|
|
errorEl.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证输入
|
|
|
|
|
|
function validateInput(input) {
|
|
|
|
|
|
if (input.hasAttribute('required') && !input.value.trim()) {
|
|
|
|
|
|
input.classList.add('error');
|
|
|
|
|
|
showError(input, '此字段为必填项');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
input.classList.remove('error');
|
|
|
|
|
|
clearError(input);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示成功消息
|
|
|
|
|
|
function showSuccess(message) {
|
|
|
|
|
|
const toast = document.createElement('div');
|
|
|
|
|
|
toast.classList.add('toast', 'success');
|
|
|
|
|
|
toast.textContent = message;
|
|
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
toast.classList.add('show');
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
toast.classList.remove('show');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
toast.remove();
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化图表
|
|
|
|
|
|
function initCharts() {
|
|
|
|
|
|
// 预算分配饼图
|
|
|
|
|
|
const budgetChart = document.getElementById('budgetChart');
|
|
|
|
|
|
if (budgetChart) {
|
|
|
|
|
|
const data = [
|
|
|
|
|
|
{ label: '场地租赁', value: 35, color: '#10b981' },
|
|
|
|
|
|
{ label: '营销推广', value: 25, color: '#3b82f6' },
|
|
|
|
|
|
{ label: '运营服务', value: 20, color: '#8b5cf6' },
|
|
|
|
|
|
{ label: '人员成本', value: 15, color: '#f59e0b' },
|
|
|
|
|
|
{ label: '其他费用', value: 5, color: '#ef4444' }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
drawPieChart(budgetChart, data);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 参展商类别分布
|
|
|
|
|
|
const exhibitorChart = document.getElementById('exhibitorChart');
|
|
|
|
|
|
if (exhibitorChart) {
|
|
|
|
|
|
const data = [
|
|
|
|
|
|
{ label: '整车制造', value: 40 },
|
|
|
|
|
|
{ label: '零部件', value: 30 },
|
|
|
|
|
|
{ label: '充电设施', value: 15 },
|
|
|
|
|
|
{ label: '智能驾驶', value: 10 },
|
|
|
|
|
|
{ label: '其他', value: 5 }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
drawBarChart(exhibitorChart, data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制饼图
|
|
|
|
|
|
function drawPieChart(canvas, data) {
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
const centerX = canvas.width / 2;
|
|
|
|
|
|
const centerY = canvas.height / 2;
|
|
|
|
|
|
const radius = Math.min(centerX, centerY) - 20;
|
|
|
|
|
|
|
|
|
|
|
|
let currentAngle = -Math.PI / 2;
|
|
|
|
|
|
const total = data.reduce((sum, item) => sum + item.value, 0);
|
|
|
|
|
|
|
|
|
|
|
|
data.forEach(item => {
|
|
|
|
|
|
const sliceAngle = (item.value / total) * 2 * Math.PI;
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制扇形
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle);
|
|
|
|
|
|
ctx.lineTo(centerX, centerY);
|
|
|
|
|
|
ctx.fillStyle = item.color;
|
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制标签
|
|
|
|
|
|
const labelX = centerX + Math.cos(currentAngle + sliceAngle / 2) * (radius * 0.7);
|
|
|
|
|
|
const labelY = centerY + Math.sin(currentAngle + sliceAngle / 2) * (radius * 0.7);
|
|
|
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#fff';
|
|
|
|
|
|
ctx.font = '14px sans-serif';
|
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
|
ctx.fillText(item.value + '%', labelX, labelY);
|
|
|
|
|
|
|
|
|
|
|
|
currentAngle += sliceAngle;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制柱状图
|
|
|
|
|
|
function drawBarChart(canvas, data) {
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
const barWidth = canvas.width / (data.length * 2);
|
|
|
|
|
|
const maxValue = Math.max(...data.map(item => item.value));
|
|
|
|
|
|
const chartHeight = canvas.height - 40;
|
|
|
|
|
|
|
|
|
|
|
|
data.forEach((item, index) => {
|
|
|
|
|
|
const barHeight = (item.value / maxValue) * chartHeight;
|
|
|
|
|
|
const x = (index * 2 + 0.5) * barWidth;
|
|
|
|
|
|
const y = canvas.height - barHeight - 20;
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制柱子
|
|
|
|
|
|
const gradient = ctx.createLinearGradient(x, y, x, y + barHeight);
|
|
|
|
|
|
gradient.addColorStop(0, '#10b981');
|
|
|
|
|
|
gradient.addColorStop(1, '#3b82f6');
|
|
|
|
|
|
|
|
|
|
|
|
ctx.fillStyle = gradient;
|
|
|
|
|
|
ctx.fillRect(x, y, barWidth, barHeight);
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制数值
|
|
|
|
|
|
ctx.fillStyle = '#333';
|
|
|
|
|
|
ctx.font = '12px sans-serif';
|
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
|
ctx.fillText(item.value + '%', x + barWidth / 2, y - 5);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化时间线
|
|
|
|
|
|
function initTimeline() {
|
|
|
|
|
|
const timelineItems = document.querySelectorAll('.timeline-item');
|
|
|
|
|
|
|
|
|
|
|
|
const timelineObserver = new IntersectionObserver((entries) => {
|
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
|
entry.target.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, { threshold: 0.5 });
|
|
|
|
|
|
|
|
|
|
|
|
timelineItems.forEach(item => {
|
|
|
|
|
|
timelineObserver.observe(item);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 交互元素初始化
|
|
|
|
|
|
function initInteractiveElements() {
|
|
|
|
|
|
// 标签页切换
|
|
|
|
|
|
const tabs = document.querySelectorAll('.tab');
|
|
|
|
|
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
|
|
|
|
|
|
|
|
|
|
tabs.forEach(tab => {
|
|
|
|
|
|
tab.addEventListener('click', () => {
|
|
|
|
|
|
const target = tab.dataset.tab;
|
|
|
|
|
|
|
|
|
|
|
|
// 切换标签状态
|
|
|
|
|
|
tabs.forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
tab.classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
// 切换内容显示
|
|
|
|
|
|
tabContents.forEach(content => {
|
|
|
|
|
|
if (content.id === target) {
|
|
|
|
|
|
content.classList.add('active');
|
|
|
|
|
|
content.style.display = 'block';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
content.classList.remove('active');
|
|
|
|
|
|
content.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 手风琴效果
|
|
|
|
|
|
const accordionHeaders = document.querySelectorAll('.accordion-header');
|
|
|
|
|
|
|
|
|
|
|
|
accordionHeaders.forEach(header => {
|
|
|
|
|
|
header.addEventListener('click', () => {
|
|
|
|
|
|
const content = header.nextElementSibling;
|
|
|
|
|
|
const isOpen = header.classList.contains('active');
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭其他项
|
|
|
|
|
|
accordionHeaders.forEach(h => {
|
|
|
|
|
|
h.classList.remove('active');
|
|
|
|
|
|
h.nextElementSibling.style.maxHeight = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 切换当前项
|
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
|
header.classList.add('active');
|
|
|
|
|
|
content.style.maxHeight = content.scrollHeight + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 模态框
|
|
|
|
|
|
const modalTriggers = document.querySelectorAll('[data-modal]');
|
|
|
|
|
|
const modals = document.querySelectorAll('.modal');
|
|
|
|
|
|
const modalCloses = document.querySelectorAll('.modal-close');
|
|
|
|
|
|
|
|
|
|
|
|
modalTriggers.forEach(trigger => {
|
|
|
|
|
|
trigger.addEventListener('click', () => {
|
|
|
|
|
|
const modalId = trigger.dataset.modal;
|
|
|
|
|
|
const modal = document.getElementById(modalId);
|
|
|
|
|
|
if (modal) {
|
|
|
|
|
|
modal.classList.add('open');
|
|
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
modalCloses.forEach(close => {
|
|
|
|
|
|
close.addEventListener('click', () => {
|
|
|
|
|
|
const modal = close.closest('.modal');
|
|
|
|
|
|
modal.classList.remove('open');
|
|
|
|
|
|
document.body.style.overflow = '';
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 点击背景关闭模态框
|
|
|
|
|
|
modals.forEach(modal => {
|
|
|
|
|
|
modal.addEventListener('click', (e) => {
|
|
|
|
|
|
if (e.target === modal) {
|
|
|
|
|
|
modal.classList.remove('open');
|
|
|
|
|
|
document.body.style.overflow = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 工具提示
|
|
|
|
|
|
const tooltips = document.querySelectorAll('[data-tooltip]');
|
|
|
|
|
|
|
|
|
|
|
|
tooltips.forEach(element => {
|
|
|
|
|
|
const tooltip = document.createElement('div');
|
|
|
|
|
|
tooltip.classList.add('tooltip');
|
|
|
|
|
|
tooltip.textContent = element.dataset.tooltip;
|
|
|
|
|
|
|
|
|
|
|
|
element.addEventListener('mouseenter', () => {
|
|
|
|
|
|
document.body.appendChild(tooltip);
|
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
|
tooltip.style.left = rect.left + rect.width / 2 - tooltip.offsetWidth / 2 + 'px';
|
|
|
|
|
|
tooltip.style.top = rect.top - tooltip.offsetHeight - 10 + 'px';
|
|
|
|
|
|
tooltip.classList.add('show');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
element.addEventListener('mouseleave', () => {
|
|
|
|
|
|
tooltip.classList.remove('show');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
tooltip.remove();
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 复制到剪贴板
|
|
|
|
|
|
function copyToClipboard(text) {
|
|
|
|
|
|
const textarea = document.createElement('textarea');
|
|
|
|
|
|
textarea.value = text;
|
|
|
|
|
|
document.body.appendChild(textarea);
|
|
|
|
|
|
textarea.select();
|
|
|
|
|
|
document.execCommand('copy');
|
|
|
|
|
|
document.body.removeChild(textarea);
|
|
|
|
|
|
showSuccess('已复制到剪贴板');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分享功能
|
|
|
|
|
|
function shareContent(platform) {
|
|
|
|
|
|
const url = encodeURIComponent(window.location.href);
|
|
|
|
|
|
const title = encodeURIComponent(document.title);
|
|
|
|
|
|
let shareUrl = '';
|
|
|
|
|
|
|
|
|
|
|
|
switch(platform) {
|
|
|
|
|
|
case 'wechat':
|
|
|
|
|
|
// 生成微信分享二维码
|
|
|
|
|
|
generateQRCode(window.location.href);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'weibo':
|
|
|
|
|
|
shareUrl = `https://service.weibo.com/share/share.php?url=${url}&title=${title}`;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'linkedin':
|
|
|
|
|
|
shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${url}`;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'twitter':
|
|
|
|
|
|
shareUrl = `https://twitter.com/intent/tweet?url=${url}&text=${title}`;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shareUrl) {
|
|
|
|
|
|
window.open(shareUrl, '_blank', 'width=600,height=400');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成二维码
|
|
|
|
|
|
function generateQRCode(text) {
|
|
|
|
|
|
const modal = document.createElement('div');
|
|
|
|
|
|
modal.classList.add('qr-modal');
|
|
|
|
|
|
modal.innerHTML = `
|
|
|
|
|
|
<div class="qr-content">
|
|
|
|
|
|
<h3>微信扫码分享</h3>
|
|
|
|
|
|
<div id="qrcode"></div>
|
|
|
|
|
|
<button class="btn btn-secondary" onclick="this.parentElement.parentElement.remove()">关闭</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
|
|
|
|
|
|
// 这里可以集成实际的二维码生成库
|
|
|
|
|
|
document.getElementById('qrcode').innerHTML = '二维码生成区域';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 主题切换
|
|
|
|
|
|
function initThemeToggle() {
|
|
|
|
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
|
|
|
|
const currentTheme = localStorage.getItem('theme') || 'light';
|
|
|
|
|
|
|
|
|
|
|
|
document.documentElement.setAttribute('data-theme', currentTheme);
|
|
|
|
|
|
|
|
|
|
|
|
themeToggle?.addEventListener('click', () => {
|
|
|
|
|
|
const theme = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
|
|
|
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
|
|
|
|
localStorage.setItem('theme', theme);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 导出为 PDF
|
|
|
|
|
|
function exportToPDF() {
|
|
|
|
|
|
window.print();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 性能监控
|
|
|
|
|
|
function initPerformanceMonitoring() {
|
|
|
|
|
|
// 页面加载时间
|
|
|
|
|
|
window.addEventListener('load', () => {
|
|
|
|
|
|
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
|
|
|
|
|
|
console.log(`页面加载时间: ${loadTime}ms`);
|
|
|
|
|
|
|
|
|
|
|
|
// 发送到分析服务
|
|
|
|
|
|
if (window.gtag) {
|
|
|
|
|
|
gtag('event', 'page_load_time', {
|
|
|
|
|
|
value: loadTime,
|
|
|
|
|
|
page_path: window.location.pathname
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监控长任务
|
|
|
|
|
|
if ('PerformanceObserver' in window) {
|
|
|
|
|
|
const observer = new PerformanceObserver((list) => {
|
|
|
|
|
|
for (const entry of list.getEntries()) {
|
|
|
|
|
|
console.log('Long Task detected:', entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
observer.observe({ entryTypes: ['longtask'] });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理图片加载错误
|
|
|
|
|
|
function handleImageErrors() {
|
|
|
|
|
|
const images = document.querySelectorAll('img');
|
|
|
|
|
|
const defaultImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZTJlOGYwIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzk0YTNiOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWbvueJh+WKoOi9veS4rTwvdGV4dD48L3N2Zz4=';
|
|
|
|
|
|
|
|
|
|
|
|
images.forEach(img => {
|
|
|
|
|
|
// 添加错误处理
|
|
|
|
|
|
img.addEventListener('error', function() {
|
|
|
|
|
|
console.warn('图片加载失败:', this.src);
|
|
|
|
|
|
// 设置默认占位图
|
|
|
|
|
|
this.src = defaultImage;
|
|
|
|
|
|
this.alt = '图片加载失败';
|
|
|
|
|
|
this.classList.add('image-error');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 添加加载成功处理
|
|
|
|
|
|
img.addEventListener('load', function() {
|
|
|
|
|
|
this.classList.add('image-loaded');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化性能监控
|
|
|
|
|
|
initPerformanceMonitoring();
|
|
|
|
|
|
initThemeToggle();
|