396 lines
12 KiB
JavaScript
396 lines
12 KiB
JavaScript
// 主JavaScript文件 - 个人作品展示网站
|
||
|
||
// DOM加载完成后执行
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
console.log('个人作品展示网站已加载');
|
||
|
||
// 初始化所有模块
|
||
initNavigation();
|
||
initSmoothScroll();
|
||
initScrollEffects();
|
||
initContactForm();
|
||
initBackToTop();
|
||
initSkillAnimations();
|
||
|
||
// 性能监控
|
||
monitorPerformance();
|
||
});
|
||
|
||
// 导航功能
|
||
function initNavigation() {
|
||
const navbar = document.querySelector('.navbar');
|
||
const navbarToggle = document.querySelector('.navbar__toggle');
|
||
const navbarMenu = document.querySelector('.navbar__menu');
|
||
const navbarLinks = document.querySelectorAll('.navbar__link');
|
||
|
||
if (!navbarToggle || !navbarMenu) return;
|
||
|
||
// 切换移动端菜单
|
||
navbarToggle.addEventListener('click', function() {
|
||
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
||
this.setAttribute('aria-expanded', !isExpanded);
|
||
navbarMenu.classList.toggle('active');
|
||
|
||
// 更新汉堡菜单图标
|
||
const icons = this.querySelectorAll('.navbar__toggle-icon');
|
||
if (!isExpanded) {
|
||
icons[0].style.transform = 'rotate(45deg) translate(5px, 5px)';
|
||
icons[1].style.opacity = '0';
|
||
icons[2].style.transform = 'rotate(-45deg) translate(7px, -6px)';
|
||
} else {
|
||
icons[0].style.transform = 'none';
|
||
icons[1].style.opacity = '1';
|
||
icons[2].style.transform = 'none';
|
||
}
|
||
});
|
||
|
||
// 点击导航链接时关闭移动端菜单
|
||
navbarLinks.forEach(link => {
|
||
link.addEventListener('click', function() {
|
||
if (window.innerWidth <= 1023) {
|
||
navbarMenu.classList.remove('active');
|
||
navbarToggle.setAttribute('aria-expanded', 'false');
|
||
|
||
const icons = navbarToggle.querySelectorAll('.navbar__toggle-icon');
|
||
icons[0].style.transform = 'none';
|
||
icons[1].style.opacity = '1';
|
||
icons[2].style.transform = 'none';
|
||
}
|
||
|
||
// 更新活动链接
|
||
navbarLinks.forEach(l => l.classList.remove('active'));
|
||
this.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// 滚动时更新导航栏样式和活动链接
|
||
window.addEventListener('scroll', function() {
|
||
// 导航栏滚动效果
|
||
if (window.scrollY > 50) {
|
||
navbar.classList.add('scrolled');
|
||
} else {
|
||
navbar.classList.remove('scrolled');
|
||
}
|
||
|
||
// 更新活动导航链接
|
||
updateActiveNavLink();
|
||
});
|
||
|
||
// 点击外部区域关闭菜单
|
||
document.addEventListener('click', function(event) {
|
||
if (!navbar.contains(event.target) && navbarMenu.classList.contains('active')) {
|
||
navbarMenu.classList.remove('active');
|
||
navbarToggle.setAttribute('aria-expanded', 'false');
|
||
|
||
const icons = navbarToggle.querySelectorAll('.navbar__toggle-icon');
|
||
icons[0].style.transform = 'none';
|
||
icons[1].style.opacity = '1';
|
||
icons[2].style.transform = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 平滑滚动
|
||
function initSmoothScroll() {
|
||
// 使用CSS的scroll-behavior: smooth,这里作为后备方案
|
||
if (!('scrollBehavior' in document.documentElement.style)) {
|
||
const smoothScrollLinks = document.querySelectorAll('a[href^="#"]');
|
||
|
||
smoothScrollLinks.forEach(link => {
|
||
link.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
|
||
const targetId = this.getAttribute('href');
|
||
if (targetId === '#') return;
|
||
|
||
const targetElement = document.querySelector(targetId);
|
||
if (!targetElement) return;
|
||
|
||
const navbarHeight = document.querySelector('.navbar').offsetHeight;
|
||
const targetPosition = targetElement.offsetTop - navbarHeight;
|
||
|
||
window.scrollTo({
|
||
top: targetPosition,
|
||
behavior: 'smooth'
|
||
});
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// 滚动效果
|
||
function initScrollEffects() {
|
||
const observerOptions = {
|
||
root: null,
|
||
rootMargin: '0px',
|
||
threshold: 0.1
|
||
};
|
||
|
||
const observer = new IntersectionObserver(function(entries) {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add('animate-in');
|
||
}
|
||
});
|
||
}, observerOptions);
|
||
|
||
// 观察需要动画的元素
|
||
const animatedElements = document.querySelectorAll('.skill__progress, .project__card, .timeline__item');
|
||
animatedElements.forEach(element => {
|
||
observer.observe(element);
|
||
});
|
||
}
|
||
|
||
// 联系表单处理
|
||
function initContactForm() {
|
||
const contactForm = document.querySelector('.contact__form');
|
||
if (!contactForm) return;
|
||
|
||
const form = contactForm.querySelector('form');
|
||
if (!form) return;
|
||
|
||
const submitButton = form.querySelector('.form__button');
|
||
const messageDiv = form.querySelector('.form__message');
|
||
|
||
form.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
// 禁用提交按钮
|
||
submitButton.disabled = true;
|
||
submitButton.innerHTML = '<span class="loading"></span> 发送中...';
|
||
|
||
// 收集表单数据
|
||
const formData = new FormData(form);
|
||
const data = Object.fromEntries(formData);
|
||
|
||
try {
|
||
// 这里应该是实际的API调用
|
||
// 暂时使用模拟延迟
|
||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||
|
||
// 显示成功消息
|
||
showFormMessage('消息发送成功!我会尽快回复您。', 'success');
|
||
form.reset();
|
||
|
||
// 记录到控制台(实际项目中应该发送到服务器)
|
||
console.log('联系表单提交:', data);
|
||
|
||
} catch (error) {
|
||
console.error('表单提交错误:', error);
|
||
showFormMessage('发送失败,请稍后重试。', 'error');
|
||
} finally {
|
||
// 恢复提交按钮
|
||
submitButton.disabled = false;
|
||
submitButton.textContent = '发送消息';
|
||
}
|
||
});
|
||
|
||
// 显示表单消息
|
||
function showFormMessage(message, type) {
|
||
if (!messageDiv) return;
|
||
|
||
messageDiv.textContent = message;
|
||
messageDiv.className = `form__message form__message--${type}`;
|
||
messageDiv.style.display = 'block';
|
||
|
||
// 3秒后隐藏消息
|
||
setTimeout(() => {
|
||
messageDiv.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
// 表单验证
|
||
const inputs = form.querySelectorAll('.form__input, .form__textarea');
|
||
inputs.forEach(input => {
|
||
input.addEventListener('blur', function() {
|
||
validateField(this);
|
||
});
|
||
|
||
input.addEventListener('input', function() {
|
||
clearFieldError(this);
|
||
});
|
||
});
|
||
|
||
function validateField(field) {
|
||
const value = field.value.trim();
|
||
let isValid = true;
|
||
let errorMessage = '';
|
||
|
||
if (field.hasAttribute('required') && !value) {
|
||
isValid = false;
|
||
errorMessage = '此字段为必填项';
|
||
} else if (field.type === 'email' && value) {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(value)) {
|
||
isValid = false;
|
||
errorMessage = '请输入有效的邮箱地址';
|
||
}
|
||
}
|
||
|
||
if (!isValid) {
|
||
showFieldError(field, errorMessage);
|
||
} else {
|
||
clearFieldError(field);
|
||
}
|
||
|
||
return isValid;
|
||
}
|
||
|
||
function showFieldError(field, message) {
|
||
clearFieldError(field);
|
||
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.className = 'form__error';
|
||
errorDiv.textContent = message;
|
||
errorDiv.style.color = '#e74c3c';
|
||
errorDiv.style.fontSize = '0.875rem';
|
||
errorDiv.style.marginTop = '0.25rem';
|
||
|
||
field.parentNode.appendChild(errorDiv);
|
||
field.style.borderColor = '#e74c3c';
|
||
}
|
||
|
||
function clearFieldError(field) {
|
||
const errorDiv = field.parentNode.querySelector('.form__error');
|
||
if (errorDiv) {
|
||
errorDiv.remove();
|
||
}
|
||
field.style.borderColor = '';
|
||
}
|
||
}
|
||
|
||
// 返回顶部按钮
|
||
function initBackToTop() {
|
||
const backToTopButton = document.querySelector('.footer__back-to-top');
|
||
if (!backToTopButton) return;
|
||
|
||
window.addEventListener('scroll', function() {
|
||
if (window.scrollY > 500) {
|
||
backToTopButton.classList.add('visible');
|
||
} else {
|
||
backToTopButton.classList.remove('visible');
|
||
}
|
||
});
|
||
|
||
backToTopButton.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
window.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
});
|
||
});
|
||
}
|
||
|
||
// 技能进度条动画
|
||
function initSkillAnimations() {
|
||
const skillProgressBars = document.querySelectorAll('.skill__progress');
|
||
|
||
const observer = new IntersectionObserver(function(entries) {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
const progressBar = entry.target;
|
||
const width = progressBar.style.width;
|
||
|
||
// 重置宽度为0,然后动画到目标宽度
|
||
progressBar.style.width = '0%';
|
||
|
||
setTimeout(() => {
|
||
progressBar.style.width = width;
|
||
}, 100);
|
||
|
||
// 停止观察
|
||
observer.unobserve(progressBar);
|
||
}
|
||
});
|
||
}, {
|
||
threshold: 0.5
|
||
});
|
||
|
||
skillProgressBars.forEach(bar => {
|
||
observer.observe(bar);
|
||
});
|
||
}
|
||
|
||
// 更新活动导航链接
|
||
function updateActiveNavLink() {
|
||
const sections = document.querySelectorAll('section[id]');
|
||
const navLinks = document.querySelectorAll('.navbar__link');
|
||
|
||
let currentSection = '';
|
||
|
||
sections.forEach(section => {
|
||
const sectionTop = section.offsetTop;
|
||
const sectionHeight = section.clientHeight;
|
||
const navbarHeight = document.querySelector('.navbar').offsetHeight;
|
||
|
||
if (window.scrollY >= sectionTop - navbarHeight - 100) {
|
||
currentSection = section.getAttribute('id');
|
||
}
|
||
});
|
||
|
||
navLinks.forEach(link => {
|
||
link.classList.remove('active');
|
||
if (link.getAttribute('href') === `#${currentSection}`) {
|
||
link.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 性能监控
|
||
function monitorPerformance() {
|
||
// 记录页面加载时间
|
||
window.addEventListener('load', function() {
|
||
const timing = performance.timing;
|
||
const loadTime = timing.loadEventEnd - timing.navigationStart;
|
||
console.log(`页面加载时间: ${loadTime}ms`);
|
||
|
||
// 如果加载时间过长,记录警告
|
||
if (loadTime > 3000) {
|
||
console.warn('页面加载时间较长,建议优化');
|
||
}
|
||
});
|
||
|
||
// 监控资源加载
|
||
const resources = performance.getEntriesByType('resource');
|
||
resources.forEach(resource => {
|
||
if (resource.duration > 1000) {
|
||
console.warn(`资源加载较慢: ${resource.name} (${resource.duration}ms)`);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 工具函数
|
||
function debounce(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
}
|
||
|
||
function throttle(func, limit) {
|
||
let inThrottle;
|
||
return function() {
|
||
const args = arguments;
|
||
const context = this;
|
||
if (!inThrottle) {
|
||
func.apply(context, args);
|
||
inThrottle = true;
|
||
setTimeout(() => inThrottle = false, limit);
|
||
}
|
||
};
|
||
}
|
||
|
||
// 导出函数供其他模块使用
|
||
window.PortfolioApp = {
|
||
initNavigation,
|
||
initSmoothScroll,
|
||
initContactForm,
|
||
updateActiveNavLink,
|
||
debounce,
|
||
throttle
|
||
}; |