/* about.js - Aurora Nexus Core */ // 公共方法:设置本地存储的语言设置 function setStoredLanguage(lang) { localStorage.setItem('lang', lang); } // 公共方法:获取本地存储的语言设置 function getStoredLanguage() { return localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en'); } // 公共方法:设置本地存储的主题设置 function setStoredTheme(theme) { const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme'; localStorage.setItem(cacheKey, JSON.stringify({ value: theme, time: new Date().getTime() })); } // 公共方法:获取本地存储的主题设置 function getStoredTheme() { const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2'; const timeout = window.SiteConfig?.cacheKeys?.theme?.ttlMs || 360000; const cacheJson = localStorage.getItem(cacheKey); const saved = cacheJson ? JSON.parse(cacheJson) : null; let theme = 'day'; if (saved == null || new Date().getTime() - timeout > saved.time) { const hour = new Date().getHours(); const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const night = hour >= 18 || prefersDark; theme = night ? 'night' : 'day'; setStoredTheme(theme) } else if (saved.value) { theme = saved.value; } return theme; } $(document).ready(function () { const app = new AppCore(); }); function escapeHtml(s) { if (s == null) return ''; return String(s).replace(/[&<>\"]/g, function (c) { return ({'&': '&', '<': '<', '>': '>', '"': '"'})[c]; }); } class AppCore { constructor() { this.i18n = new I18nManager(); this.theme = new ThemeManager(); this.ui = new UIManager(); this.data = new DataManager(); } } /* =========================== 1. I18n (Language) =========================== */ class I18nManager { constructor() { // 获取当前请求参数有无 lang 参数 try { this.query = new URLSearchParams(window.location.search); if (this.query.has('lang')) { let lang = this.query.get('lang'); if (lang === 'zh' || lang === 'en') { this.lang = lang; setStoredLanguage(lang); } } } catch (e) { console.error(e); } if (!this.lang) { this.lang = getStoredLanguage(); } this.dict = { zh: { "nav.home": "首页", "nav.about": "关于", "nav.blog": "博客", "status.online": "活跃", "profile.name": "Honesty", "profile.role": "Java后端 & AI探索者", "profile.location": "上海, 中国", "bio.label": "关于我", "bio.text": "我是一名充满热情的Java后端开发工程师,专注于AI技术的探索与应用。来自湖南,现在上海工作,享受在这座充满活力的城市中追求技术梦想。", "bio.quote": "我追求技术的深度理解而非广度堆砌,每一项技术的学习都源于解决实际问题的内在驱动。作为INFJ人格类型,我善于深度思考,注重细节,喜欢通过代码创造有意义的产品。我相信技术的力量能够改变世界,也热衷于在开源社区中分享知识与经验。", "stats.exp": "编程年限", "stats.repos": "开源项目", "stats.followers": "关注者", "stats.visitors": "访客数", "stats.visitNum": "访问量", "mbti.name": "提倡者", "mbti.desc": "提倡者人格类型的人非常稀少,只有不到1%的人口属于这种类型,但他们对世界的贡献不容忽视。", "mbti.tag1": "理想主义与道德感", "mbti.tag2": "果断决绝的行动力", "mbti.tag3": "深度洞察与创意", "mbti.tag4": "关怀与同理心", "tech.title": "技术栈宇宙", "interest.title": "兴趣", "interest.subtitle": "INFJ · 创造者 · 探索者", "interest.cycling": "骑行", "interest.cycling_desc": "享受在路上的自由感,用车轮丈量世界,在风景中寻找灵感", "interest.reading": "阅读", "interest.reading_desc": "通过文字记录思考轨迹,分享技术见解,用代码诠释创意", "interest.opensource": "开源", "interest.opensource_desc": "热衷于开源项目,相信分享的力量,用代码连接世界", "interest.learning": "持续学习", "interest.learning_desc": "保持对新技术的好奇心,在变化中成长,在挑战中进步", "github.title": "开源贡献", "blog.title": "最新文章", "blog.more": "查看更多", "comment.title": "留言板", "modal.wechat": "微信公众号", "modal.desc": "扫码关注获取干货", "comment.closed": "当前评论区已关闭" }, en: { "nav.home": "Home", "nav.about": "About", "nav.blog": "Blog", "status.online": "Available", "profile.name": "Honesty", "profile.role": "Java Backend & AI Dev", "profile.location": "Shanghai, China", "bio.label": "About Me", "bio.text": "I am a passionate Java Backend Engineer focused on AI exploration. Based in Shanghai, originally from Hunan, I enjoy pursuing my technical dreams in this vibrant city.", "bio.quote": "I pursue a deep understanding of technology rather than a broad accumulation, and the learning of every technology stems from the intrinsic drive to solve practical problems. As an INFJ personality type, I am good at deep thinking, pay attention to details, and enjoy creating meaningful products through code. I believe in the power of technology to change the world, and I am passionate about sharing knowledge and experience in the open source community.", "stats.exp": "Years Exp", "stats.repos": "Projects", "stats.followers": "Followers", "stats.visitors": "Access User", "stats.visitNum": "Visitors", "mbti.name": "Advocate", "mbti.desc": "Advocates of this personality type are very rare, with less than 1% of the population belonging to this type, but their contributions to the world cannot be ignored.", "mbti.tag1": "Idealism & Morality", "mbti.tag2": "Decisive Action", "mbti.tag3": "Deep Insight & Creativity", "mbti.tag4": "Care & Empathy", "tech.title": "Tech Universe", "interest.title": "Interests", "interest.subtitle": "INFJ · Creator · Explorer", "interest.cycling": "Cycling", "interest.cycling_desc": "Enjoy the freedom on the road, measure the world with wheels, and find inspiration in the scenery", "interest.reading": "Reading", "interest.reading_desc": "Record thinking trajectories through text, share technical insights, and interpret creativity with code", "interest.opensource": "Open Source", "interest.opensource_desc": "Passionate about open source projects, believing in the power of sharing, connecting the world with code", "interest.learning": "Learning", "interest.learning_desc": "Maintain curiosity about new technologies, grow through change, and progress through challenges", "github.title": "Open Source", "blog.title": "Latest Posts", "blog.more": "View All", "comment.title": "Message Board", "modal.wechat": "WeChat Account", "modal.desc": "Scan for tech insights", "comment.closed": "Comments are closed" } }; this.init(); } init() { this.apply(); $('#lang-btn').on('click', () => { this.lang = this.lang === 'zh' ? 'en' : 'zh'; setStoredLanguage(this.lang); this.apply(); const label = this.lang === 'zh' ? 'EN' : 'CN'; $('#lang-btn .btn-text').text(label); $('#lang-btn').attr('title', label); }); } apply() { const t = this.dict[this.lang]; document.documentElement.setAttribute('data-lang', this.lang) $('[data-i18n]').each(function () { const k = $(this).data('i18n'); if (t[k]) $(this).text(t[k]); }); const label = this.lang === 'zh' ? 'EN' : 'CN'; $('#lang-btn .btn-text').text(label); $('#lang-btn').attr('title', label); } } /* =========================== 2. Theme Manager =========================== */ class ThemeManager { constructor() { this.root = document.documentElement; this.init(); } init() { let theme = getStoredTheme(); if (theme) this.root.setAttribute('data-theme', theme); const themeBtn = $('#theme-btn'); themeBtn.toggleClass('is-active', theme === 'night'); const langForTitle = getStoredLanguage(); const titleText = theme === 'night' ? (langForTitle === 'zh' ? '白天模式' : 'Day') : (langForTitle === 'zh' ? '黑夜模式' : 'Night'); themeBtn.attr('title', titleText); themeBtn.on('click', () => { const curr = this.root.getAttribute('data-theme'); const next = curr === 'night' ? 'day' : 'night'; if (next) this.root.setAttribute('data-theme', next); else this.root.removeAttribute('data-theme'); setStoredTheme(next) themeBtn.toggleClass('is-active', next === 'night'); const lang = getStoredLanguage(); const t = next === 'night' ? (lang === 'zh' ? '白天模式' : 'Day') : (lang === 'zh' ? '黑夜模式' : 'Night'); themeBtn.attr('title', t); // 更新Artalk主题 if (typeof Artalk !== 'undefined') { try { Artalk.reload(); } catch (e) { } } }); } } /* =========================== 3. Data Manager (Robust) =========================== */ class DataManager { constructor() { this.init(); } init() { // 使用requestIdleCallback或setTimeout优化初始化调用 if ('requestIdleCallback' in window) { requestIdleCallback(() => this.fetchGithub(), { timeout: 2000 }); requestIdleCallback(() => this.fetchBlog(), { timeout: 2000 }); } else { // 降级到setTimeout,但稍后执行以避免阻塞 setTimeout(() => this.fetchGithub(), 200); setTimeout(() => this.fetchBlog(), 300); } } // 创建带超时的fetch函数 async fetchWithTimeout(url, options = {}) { const { timeout = 5000 } = options; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(id); return response; } // 优先缓存 -> API -> 默认值 async fetchGithub() { const user = (window.SiteConfig?.github?.username) || 'listener-He'; const cacheKey = (window.SiteConfig?.cacheKeys?.github?.key) || 'gh_data_v2'; const cached = JSON.parse(localStorage.getItem(cacheKey)); const now = Date.now(); const timeout = (window.SiteConfig?.cacheKeys?.github?.ttlMs) || 3600000; // Check Cache (1 hour) if (cached && (now - cached.time < timeout)) { this.renderUser(cached.user); this.renderRepos(cached.repos); return; } try { // Parallel Fetch with timeout let userData, repoData; try { const uRes = await this.fetchWithTimeout(`https://api.github.com/users/${user}`, { timeout: 1000 }); if (uRes.ok) { userData = await uRes.json(); } else { const fallbackUser = await this.fetchWithTimeout("./data/github_user.json", { timeout: 200 }); userData = await fallbackUser.json(); } } catch (err) { // Handle abort errors and other fetch errors if (err.name === 'AbortError') { console.warn("GitHub user fetch aborted, using fallback data"); } const fallbackUser = await this.fetchWithTimeout("./data/github_user.json", { timeout: 200 }); userData = await fallbackUser.json(); } try { let allRepos = []; let page = 1; const perPage = 100; while (page <= 50) { // 最多抓取500条,直到满足条件或为空 const rRes = await this.fetchWithTimeout(`https://api.github.com/users/${user}/repos?sort=pushed&direction=desc&per_page=${perPage}&page=${page}`, { timeout: 3000 }); if (!rRes.ok) break; const repos = await rRes.json(); if (!Array.isArray(repos) || repos.length === 0) break; allRepos = allRepos.concat(repos); if (repos.length < perPage || allRepos.length >= 500) break; // 足量 page++; } if (allRepos.length) { repoData = allRepos; } else { const fallbackRepos = await this.fetchWithTimeout("./data/github_repos.json", { timeout: 300 }); repoData = await fallbackRepos.json(); } } catch (err) { // Handle abort errors and other fetch errors if (err.name === 'AbortError') { console.warn("GitHub repos fetch aborted, using fallback data"); } const fallbackRepos = await this.fetchWithTimeout("./data/github_repos.json", { timeout: 300 }); repoData = await fallbackRepos.json(); } // 过滤掉fork项目并按星数排序 if (Array.isArray(repoData)) { repoData = repoData .filter(repo => !repo.fork && (repo.stargazers_count > 0 || repo.forks_count > 0)) .sort((a, b) => (b.stargazers_count || 0) - (a.stargazers_count || 0)) .slice(0, 16); // 只取前16个 } const slimUser = { created_at: userData.created_at || (window.SiteConfig?.defaults?.user?.created), public_repos: userData.public_repos || (window.SiteConfig?.defaults?.user?.repos), followers: userData.followers || (window.SiteConfig?.defaults?.user?.followers) }; const slimRepos = Array.isArray(repoData) ? repoData.map(r => ({ name: r.name || '', stargazers_count: (r.stargazers_count !== undefined ? r.stargazers_count : (r.stars || 0)), forks_count: (r.forks_count !== undefined ? r.forks_count : (r.forks || 0)), description: r.description || r.desc || '', html_url: r.html_url || r.url || '#' })) : []; localStorage.setItem(cacheKey, JSON.stringify({ user: slimUser, repos: slimRepos, time: now })); this.renderUser(slimUser); this.renderRepos(slimRepos); } catch (e) { console.warn("GH API Fail", e); this.renderUser(window.SiteConfig?.defaults?.user); this.renderRepos(window.SiteConfig?.defaults?.repos); } } renderUser(data) { // 使用requestAnimationFrame避免强制重排 requestAnimationFrame(() => { const years = new Date().getFullYear() - new Date(data.created_at || (window.SiteConfig?.defaults?.user?.created)).getFullYear(); $('#coding-years').text(years + "+"); $('#github-repos').text(data.public_repos || (window.SiteConfig?.defaults?.user?.repos)); $('#github-followers').text(data.followers || (window.SiteConfig?.defaults?.user?.followers)); }); } renderRepos(list) { if (!Array.isArray(list)) list = window.SiteConfig?.defaults?.repos; let html = ''; list.forEach(repo => { // Fix: API field compatibility const stars = repo.stargazers_count !== undefined ? repo.stargazers_count : (repo.stars || 0); const forks = repo.forks_count !== undefined ? repo.forks_count : (repo.forks || 0); const descRaw = repo.description || repo.desc || 'No description.'; const url = repo.html_url || repo.url || '#'; const dShort = (descRaw || '').length > 120 ? (descRaw.slice(0, 117) + '...') : descRaw; html += `
${escapeHtml(repo.name)} ${stars} ${forks}
${escapeHtml(dShort)}
`; }); // 使用requestAnimationFrame避免强制重排 requestAnimationFrame(() => { const pc = $('#projects-container'); pc.removeClass('fade-in'); requestAnimationFrame(() => { pc.html(html); pc.addClass('fade-in'); }); }); } // 从RSS获取博客文章 async fetchBlog() { const rssUrl = window.SiteConfig?.blog?.rssUrl || 'https://blog.hehouhui.cn/api/rss'; const cacheKey = (window.SiteConfig?.cacheKeys?.blog?.key) || 'blog_data_v2'; const timeout = (window.SiteConfig?.cacheKeys?.blog?.ttlMs) || 3600000; const cached = JSON.parse(localStorage.getItem(cacheKey)); const now = Date.now(); // Check Cache (1 hour) if (cached && (now - cached.time < timeout)) { this.renderBlog(cached.posts); return; } try { // 尝试从RSS获取,带超时设置 const response = await this.fetchWithTimeout(rssUrl, { timeout: 5000 }); if (!response.ok) throw new Error('RSS fetch failed'); const xmlText = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "application/xml"); if (xmlDoc.documentElement.nodeName === "parsererror") { throw new Error('RSS XML parse error'); } // 解析RSS项目 const items = xmlDoc.getElementsByTagName("item"); const posts = []; for (let i = 0; i < Math.min(items.length, 5); i++) { const item = items[i]; const title = item.querySelector("title")?.textContent || "无标题"; const link = item.querySelector("link")?.textContent || "#"; const pubDate = item.querySelector("pubDate")?.textContent || ""; const categoryNodes = item.querySelectorAll("category"); const categories = Array.from(categoryNodes).map(n => (n.textContent || '').trim()).filter(Boolean); posts.push({ title, link, date: pubDate, cats: categories.length ? categories : [""] }); } // Cache & Render localStorage.setItem(cacheKey, JSON.stringify({ posts, time: now })); this.renderBlog(posts); } catch (e) { console.warn("RSS API Fail", e); // 降级到本地JSON文件 try { const response = await this.fetchWithTimeout('data/articles.json', { timeout: 3000 }); const data = await response.json(); this.renderBlog(data); } catch (e2) { console.warn("Local JSON Fail", e2); this.renderBlog(window.SiteConfig?.defaults?.posts); } } } renderBlog(list) { let html = ''; const lang = getStoredLanguage(); const fmtDate = (dStr) => { if (!dStr) return ''; const d = new Date(dStr); if (isNaN(d.getTime())) { try { const parsed = new Date(Date.parse(dStr)); if (!isNaN(parsed.getTime())) return lang === 'zh' ? `${parsed.getFullYear()}年${String(parsed.getMonth() + 1).padStart(2, '0')}月${String(parsed.getDate()).padStart(2, '0')}日` : `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}-${String(parsed.getDate()).padStart(2, '0')}`; } catch (_) { } return dStr; } return lang === 'zh' ? `${d.getFullYear()}年${String(d.getMonth() + 1).padStart(2, '0')}月${String(d.getDate()).padStart(2, '0')}日` : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; list.slice(0, 5).forEach(post => { const dateRaw = post.pubDate || post.date || ''; const date = fmtDate(dateRaw); const catsArr = post.categories || post.cats || (post.category ? [post.category] : (post.cat ? [post.cat] : ['Tech'])); const cat = Array.isArray(catsArr) ? (lang === 'zh' ? catsArr.join('、') : catsArr.join(', ')) : (catsArr || 'Tech'); const link = post.link || post.url || '#'; html += `
${escapeHtml(post.title)}
${escapeHtml(date)}
${escapeHtml(cat)}
`; }); // 使用requestAnimationFrame避免强制重排 requestAnimationFrame(() => { const bc = $('#blog-container'); bc.removeClass('fade-in'); requestAnimationFrame(() => { bc.html(html); bc.addClass('fade-in'); }); }); } } /* =========================== 4. UI Manager (Visuals) =========================== */ class UIManager { constructor() { this.initTechCloud(); this.initModal(); this.initArtalk(); this.initAudio(); this.initFab(); } // 公共方法:获取音乐暂停时间 getMusicPauseTime() { const pauseTime = localStorage.getItem('musicPauseTime'); return pauseTime ? parseInt(pauseTime) : null; } // 公共方法:检查音乐是否应在24小时内保持暂停状态 shouldMusicRemainPaused() { const pauseTime = this.getMusicPauseTime(); if (!pauseTime) return false; const now = new Date().getTime(); return (now - pauseTime) < 24 * 60 * 60 * 1000; } // 公共方法:设置音乐暂停时间 setMusicPauseTime() { localStorage.setItem('musicPauseTime', new Date().getTime().toString()); } // 公共方法:清除音乐暂停时间 clearMusicPauseTime() { localStorage.removeItem('musicPauseTime'); } initModal() { window.toggleWechat = () => { const m = $('#wechat-modal'); m.is(':visible') ? m.fadeOut(200) : m.css('display', 'flex').hide().fadeIn(200); }; $('#wechat-modal').on('click', function (e) { if (e.target === this) toggleWechat(); }); } initArtalk() { const isHttps = location.protocol === 'https:'; const isLocal = !!(window.SiteConfig?.dev?.isLocal); const lang = getStoredLanguage(); const isZh = lang === 'zh'; if (!isHttps || isLocal) { const msg = isZh ? '当前评论区已关闭' : 'Comments are closed'; $('#artalk-container').html(`
${msg}
`); return; } if (typeof Artalk !== 'undefined' && window.SiteConfig?.artalk) { try { const artalkConfig = { el: '#artalk-container', pageKey: '/about.html', pageTitle: '关于我 - Honesty', server: window.SiteConfig.artalk.server, site: window.SiteConfig.artalk.site, // 多语言支持 locale: isZh ? 'zh-CN' : 'en', // 自定义占位符(支持多语言) placeholder: isZh ? '说点什么吧...支持 Markdown 语法,可 @用户、发送表情' : 'Leave a comment... Supports Markdown, @mentions, and Send 😊', // 无评论时显示 noComment: isZh ? '快来成为第一个评论的人吧~' : 'Be the first to leave a comment~😍', // 发送按钮文字(多语言) sendBtn: isZh ? '发送' : 'Send', loginBtn: isZh ? '发送' : 'Send', // 主题支持 darkMode: document.documentElement.getAttribute('data-theme') === 'night', // 编辑器增强配置 editor: { // 启用 Markdown markdown: true, emoticons: "https://emoticons.hzchu.top/json/artalk/zaoandandandeyouyongquan.json", // 启用 @ 用户提醒功能 mention: true, // 自动聚焦(仅桌面端) autoFocus: !('ontouchstart' in window), // 限制编辑器高度 maxHeight: 200, // 工具栏 toolbar: [ 'bold', 'italic', 'strike', 'link', 'blockquote', 'code', 'codeblock', 'ol', 'ul', 'hr', 'emoji', 'mention' ] }, // 评论格式化函数 commentFormatter: (comment) => { // 美化时间显示 const formatTime = (dateStr) => { const date = new Date(dateStr); const now = new Date(); const diffSec = Math.floor((now - date) / 1000); if (diffSec < 60) return isZh ? '刚刚' : 'Just now'; if (diffSec < 3600) return isZh ? `${Math.floor(diffSec / 60)}分钟前` : `${Math.floor(diffSec / 60)} minutes ago`; if (diffSec < 86400) return isZh ? `${Math.floor(diffSec / 3600)}小时前` : `${Math.floor(diffSec / 3600)} hours ago`; const isToday = date.toDateString() === now.toDateString(); if (isToday) return isZh ? `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` : `Today ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; const isYesterday = new Date(now - 86400000).toDateString() === date.toDateString(); if (isYesterday) return isZh ? `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` : `Yesterday ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; return date.toLocaleString(isZh ? 'zh-CN' : 'en', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/\//g, '-'); }; // 如果是管理员,添加徽章 if (comment.is_admin) { comment.nick = `👑${comment.nick}`; } // 更新显示时间 comment.create_date_formatted = formatTime(comment.date || comment.created_at || comment.create_date); return comment; } }; let artalkRef = Artalk.init(artalkConfig); this.enhanceArtalkUI(artalkRef); } catch (e) { console.error("Artalk Error", e); const msg = isZh ? '当前评论区已关闭' : 'Comments are closed'; $('#artalk-container').html(`
${msg}
`); } } else { const msg = isZh ? '当前评论区已关闭' : 'Comments are closed'; $('#artalk-container').html(`
${msg}
`); } } // 重新加载 Artalk 评论组件 reloadArtalk() { // 销毁现有的 Artalk 实例 if (typeof Artalk !== 'undefined' && Artalk.instances) { try { Artalk.destroy(); } catch (e) { console.error("Artalk destroy Error", e); } } // 清空容器 const container = document.getElementById('artalk-container'); if (container) { container.innerHTML = ''; } // 重新初始化 this.initArtalk(); } enhanceArtalkUI(artalkRef) { const container = document.getElementById('artalk-container'); if (!container) return; // 检测是否为移动端 const isMobile = window.matchMedia('(max-width: 768px)').matches; container.classList.toggle('atk-mobile', isMobile); container.classList.toggle('atk-desktop', !isMobile); // 获取当前语言 const lang = getStoredLanguage(); // 获取当前主题 const currentTheme = document.documentElement.getAttribute('data-theme'); // 移动端增强功能 if (isMobile) { this.enhanceMobileArtalk(container, lang); } // 监听主题/语言变化 const themeObserver = new MutationObserver(() => { const newTheme = document.documentElement.getAttribute('data-theme'); const newLang = document.documentElement.getAttribute('data-lang'); console.log('Theme/Language changed:', newTheme, newLang); if (newLang && newLang !== lang) { // 延迟执行 setTimeout(() => { // 重新加载整个评论组件 this.reloadArtalk(); }, 300); } else if (newTheme && newTheme !== currentTheme) { try { artalkRef.ui.setDarkMode(newTheme === 'night') } catch (e) { setTimeout(() => { // 重新加载整个评论组件 this.reloadArtalk(); }, 300); } } }); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'data-lang'] }); // 初始化自定义样式 this.updateCustomStyles(container, currentTheme); } enhanceMobileArtalk(container, lang) { const applyMobileStyles = () => { container.querySelectorAll('.atk-comment-wrap .atk-content').forEach(el => { // 检查是否已经处理过 if (el.dataset.mobileProcessed) return; // 检查内容是否超过3行才添加展开收起功能 const lineHeight = parseInt(window.getComputedStyle(el).lineHeight); const paddingTop = parseInt(window.getComputedStyle(el).paddingTop); const paddingBottom = parseInt(window.getComputedStyle(el).paddingBottom); const actualHeight = el.clientHeight - paddingTop - paddingBottom; // 如果内容高度超过3倍行高,则添加展开收起功能 if (actualHeight > lineHeight * 3) { // 添加移动端内容截断 el.classList.add('clamped'); el.style.setProperty('--max-lines', '3'); // 创建展开/收起按钮 const btn = document.createElement('button'); btn.className = 'atk-expand-btn'; const expandText = lang === 'zh' ? '展开' : 'Expand'; const collapseText = lang === 'zh' ? '收起' : 'Collapse'; btn.textContent = expandText; btn.addEventListener('click', (e) => { e.stopPropagation(); const isClamped = el.classList.toggle('clamped'); btn.textContent = isClamped ? expandText : collapseText; }); // 将按钮插入到适当位置 const actionsElement = el.closest('.atk-comment').querySelector('.atk-actions'); if (actionsElement) { actionsElement.parentNode.insertBefore(btn, actionsElement); } else { el.parentNode.appendChild(btn); } } // 标记为已处理 el.dataset.mobileProcessed = '1'; }); }; // 初始应用 applyMobileStyles(); // 创建观察器以处理动态添加的评论 const observer = new MutationObserver(applyMobileStyles); observer.observe(container, { childList: true, subtree: true, attributes: false }); // 添加移动端特定的样式类 const theme = document.documentElement.getAttribute('data-theme'); container.classList.add(`atk-theme-${theme || 'day'}`); } initTechCloud() { const container = document.getElementById('tech-container'); if (!container) return; const techStackRaw = window.SiteConfig?.techStack || []; const techStack = techStackRaw.map((item, idx) => { //const name = item.name || ''; //const hash = Array.from(name).reduce((a, c) => a + c.charCodeAt(0), 0); const gid = Number(item.gradientId) && Number.isFinite(Number(item.gradientId)) ? Math.max(1, Math.min(25, Number(item.gradientId))) : (idx % 25) + 1; return {...item, gradientId: gid}; }); const currentState = window.matchMedia('(max-width: 768px)').matches ? 'mobile' : 'desktop'; // 检查是否已保存状态到 sessionStorage const savedState = sessionStorage.getItem('techCloudState_' + currentState); if (savedState) { const parsedState = JSON.parse(savedState); // 如果当前状态与保存的状态一致,直接使用保存的内容 if (parsedState.type === currentState) { container.innerHTML = parsedState.html; if (currentState === 'mobile') { container.classList.add('mobile-scroll'); } if (currentState === 'desktop') { container.classList.remove('mobile-scroll'); // 重新初始化3D球体动画 this.init3DSphereAnimation(container, techStack); } // 监听窗口大小变化 this.setupResizeListener(container, techStack, currentState); return; } } // 清空容器并重新生成 this.generateTechCloud(container, techStack, currentState); // 监听窗口大小变化 this.setupResizeListener(container, techStack, currentState); } // 保存技术标签云状态到 sessionStorage saveTechCloudState(container, type) { const state = { type: type, html: container.innerHTML }; sessionStorage.setItem('techCloudState_' + type, JSON.stringify(state)); } // 生成技术标签云 generateTechCloud(container, techStack, type) { container.innerHTML = ''; if (type === 'mobile') { // Mobile: 3-row seamless marquee container.classList.add('mobile-scroll'); const rows = 3; const buckets = Array.from({length: rows}, () => []); techStack.forEach((item, i) => { buckets[i % rows].push(item); }); const appendItem = (rowEl, item, idx) => { const el = document.createElement('span'); el.className = 'tech-tag-mobile'; const colorClass = `tag-color-${item.gradientId || ((idx % 25) + 1)}`; el.classList.add(colorClass); el.innerText = item.name; el.style.border = 'none'; rowEl.appendChild(el); }; buckets.forEach((items, rIdx) => { const rowEl = document.createElement('div'); rowEl.className = `tech-row row-${rIdx + 1}`; rowEl.style.gridRow = `${rIdx + 1}`; items.forEach((item, idx) => appendItem(rowEl, item, idx)); items.forEach((item, idx) => appendItem(rowEl, item, idx + items.length)); container.appendChild(rowEl); }); } else { // PC: 3D Sphere container.classList.remove('mobile-scroll'); this.init3DSphereAnimation(container, techStack); } // 保存当前状态 this.saveTechCloudState(container, type); } // 初始化3D球体动画 init3DSphereAnimation(container, techStack) { // 清除之前的动画 if (container.__animToken) { cancelAnimationFrame(container.__animToken); } // 清空容器 container.innerHTML = ''; const tags = []; techStack.forEach((item, index) => { const el = document.createElement('a'); el.className = 'tech-tag-3d'; const colorClass = `tag-color-${item.gradientId || ((index % 25) + 1)}`; el.classList.add(colorClass); el.innerText = item.name; el.style.border = 'none'; container.appendChild(el); tags.push({el, x: 0, y: 0, z: 0}); }); // 动态半径,避免容器溢出,使用防抖优化 let radius = Math.max(160, Math.min(container.offsetWidth, container.offsetHeight) / 2 - 24); const dtr = Math.PI / 180; let lasta = 1, lastb = 1; let active = false, mouseX = 0, mouseY = 0; // 初始化位置 tags.forEach((tag, i) => { let phi = Math.acos(-1 + (2 * i + 1) / tags.length); let theta = Math.sqrt(tags.length * Math.PI) * phi; tag.x = radius * Math.cos(theta) * Math.sin(phi); tag.y = radius * Math.sin(theta) * Math.sin(phi); tag.z = radius * Math.cos(phi); }); container.onmouseover = () => active = true; container.onmouseout = () => active = false; container.onmousemove = (e) => { // 使用requestAnimationFrame处理鼠标移动事件,避免强制重排 requestAnimationFrame(() => { let rect = container.getBoundingClientRect(); mouseX = (e.clientX - (rect.left + rect.width / 2)) / 5; mouseY = (e.clientY - (rect.top + rect.height / 2)) / 5; }); }; const update = () => { let a, b; if (active) { a = (-Math.min(Math.max(-mouseY, -200), 200) / radius) * 2; b = (Math.min(Math.max(-mouseX, -200), 200) / radius) * 2; } else { a = lasta * 0.98; // Auto rotate b = lastb * 0.98; } lasta = a; lastb = b; if (Math.abs(a) <= 0.01 && Math.abs(b) <= 0.01 && !active) a = 0.5; // Keep spinning slowly let sa = Math.sin(a * dtr), ca = Math.cos(a * dtr); let sb = Math.sin(b * dtr), cb = Math.cos(b * dtr); // 批量更新样式以减少重排 // 先收集所有需要更新的样式信息 const updates = []; tags.forEach(tag => { let rx1 = tag.x, ry1 = tag.y * ca - tag.z * sa, rz1 = tag.y * sa + tag.z * ca; let ry2 = ry1, rz2 = rx1 * -sb + rz1 * cb; tag.x = rx1 * cb + rz1 * sb; tag.y = ry2; tag.z = rz2; let scale = (tag.z + radius) / (2 * radius) + 0.45; scale = Math.min(Math.max(scale, 0.7), 1.15); const opacity = (tag.z + radius) / (2 * radius) + 0.2; const zIndex = parseInt(scale * 100); const left = tag.x + container.offsetWidth / 2 - tag.el.offsetWidth / 2; const top = tag.y + container.offsetHeight / 2 - tag.el.offsetHeight / 2; updates.push({ el: tag.el, transform: `translate(${left}px, ${top}px) scale(${scale})`, opacity: opacity, zIndex: zIndex }); }); updates.forEach(update => { update.el.style.transform = update.transform; update.el.style.opacity = update.opacity; update.el.style.zIndex = update.zIndex; }); container.__animToken = requestAnimationFrame(update); }; container.__animToken = requestAnimationFrame(update); } // 设置窗口大小变化监听器 setupResizeListener(container, techStack, currentType) { // 使用防抖优化尺寸计算 let resizeTimeout; let windowRef = currentType; const handleResize = () => { if (resizeTimeout) { clearTimeout(resizeTimeout); } resizeTimeout = setTimeout(() => { const isMobile = window.matchMedia('(max-width: 768px)').matches; const currentState = isMobile ? 'mobile' : 'desktop'; if (windowRef === currentState) { return } windowRef = currentState; // 检查 sessionStorage 中是否有对应状态的内容 const savedState = sessionStorage.getItem('techCloudState_' + currentState); if (savedState) { // 直接使用保存的内容 const parsedState = JSON.parse(savedState); container.innerHTML = parsedState.html; if (currentState === 'mobile') { container.classList.add('mobile-scroll'); } if (currentState === 'desktop') { container.classList.remove('mobile-scroll'); this.init3DSphereAnimation(container, techStack); } } else { // 重新生成 container.innerHTML = ''; this.generateTechCloud(container, techStack, currentState); } }, 100); }; // 监听窗口大小变化 window.addEventListener('resize', handleResize); } initFab() { const main = document.getElementById('fab-main'); const menu = document.getElementById('fab-menu'); const fLang = document.getElementById('fab-lang'); const fTheme = document.getElementById('fab-theme'); const fMusic = document.getElementById('fab-music'); if (!main || !menu || !fLang || !fTheme || !fMusic) return; const updateLabels = () => { // 使用requestAnimationFrame避免强制重排 requestAnimationFrame(() => { const lang = getStoredLanguage(); const theme = getStoredTheme(); fLang.querySelector('.fab-text').textContent = lang === 'zh' ? 'English' : '中文'; fTheme.querySelector('.fab-text').textContent = theme === 'night' ? 'Day' : 'Night'; const playing = (this.audio && !this.audio.paused); fMusic.querySelector('.fab-text').textContent = lang === 'zh' ? (playing ? '暂停' : '播放') : (playing ? 'Pause' : 'Play'); }); }; main.addEventListener('click', () => { menu.classList.toggle('open'); main.setAttribute('aria-expanded', menu.classList.contains('open') ? 'true' : 'false'); // 延迟更新标签以避免阻塞 requestAnimationFrame(updateLabels); }); fLang.addEventListener('click', () => { document.getElementById('lang-btn')?.click(); // 延迟更新标签以避免阻塞 requestAnimationFrame(updateLabels); }); fTheme.addEventListener('click', () => { document.getElementById('theme-btn')?.click(); // 延迟更新标签以避免阻塞 requestAnimationFrame(updateLabels); }); fMusic.addEventListener('click', () => { if (this.audio) { if (this.audio.paused) { this.audio.play().catch(() => { }); // 清除暂停时间记录,允许下次自动播放 this.clearMusicPauseTime(); } else { this.audio.pause(); // 记录暂停时间 this.setMusicPauseTime(); } } // 延迟更新标签以避免阻塞 requestAnimationFrame(updateLabels); }); // 延迟初始标签更新以避免阻塞 requestAnimationFrame(updateLabels); } initAudio() { const el = document.getElementById('site-audio'); if (!el) return; this.audio = el; this.audio.loop = true; // 页面加载完成后根据条件决定是否播放 window.addEventListener('load', () => { // 检查是否在24小时内用户暂停过音乐 const shouldRemainPaused = this.shouldMusicRemainPaused(); // 如果不应该保持暂停状态,则尝试播放 if (!shouldRemainPaused) { let userInteracted = true; this.audio.play().catch(() => { // 静默处理播放失败 userInteracted = false; }); // 添加用户交互检查,避免浏览器阻止自动播放 const attemptAutoplay = () => { // 检查是否已有用户交互 if (this.userInteracted === false) { // 添加一次性用户交互监听器 const enableAudio = () => { this.userInteracted = true; setTimeout(() => { this.audio.play().catch(() => { }); }, 1000); document.removeEventListener('click', enableAudio); document.removeEventListener('touchstart', enableAudio); document.removeEventListener('keydown', enableAudio); document.removeEventListener('mousemove', enableAudio); }; document.addEventListener('click', enableAudio, { once: true }); document.addEventListener('touchstart', enableAudio, { once: true }); document.addEventListener('keydown', enableAudio, { once: true }); document.addEventListener('mousemove', enableAudio, { once: true }); } }; requestAnimationFrame(attemptAutoplay); } }); } updateCustomStyles(container, theme) { // 确保容器具有正确的主题类 container.classList.remove('atk-theme-day', 'atk-theme-night'); container.classList.add(`atk-theme-${theme || 'day'}`); // 更新自定义元素的主题样式 const customElements = container.querySelectorAll('.atk-expand-btn, .atk-pagination .atk-page-item'); customElements.forEach(el => { el.classList.remove('atk-theme-day', 'atk-theme-night'); el.classList.add(`atk-theme-${theme || 'day'}`); }); } }