diff --git a/about.html b/about.html index f95c89a..8971533 100644 --- a/about.html +++ b/about.html @@ -9,6 +9,7 @@ + @@ -188,6 +189,23 @@ + +
+
+

Music

+
+
+
+
+
+ My Playlist + + +
+
+
+
+
@@ -242,6 +260,15 @@
+ +
+ +
+ + + +
+
@@ -262,10 +289,11 @@ - + + \ No newline at end of file diff --git a/css/about.css b/css/about.css index cb3db3f..bd6b553 100644 --- a/css/about.css +++ b/css/about.css @@ -321,11 +321,12 @@ body { gap: 24px; /* PC: 3 Col, 2 Row Main */ grid-template-columns: 320px 1fr 260px; - grid-template-rows: 240px 220px auto; + grid-template-rows: 240px 220px auto auto; grid-template-areas: "profile bio stats" "profile bio mbti" - "tech tech interests"; + "tech tech interests" + "music music interests"; } /* --- Areas --- */ @@ -352,6 +353,7 @@ body { .area-interests { grid-area: interests; } +.area-music { grid-area: music; } .mobile-social { display: none; @@ -1575,4 +1577,21 @@ body { -webkit-text-fill-color: initial; } .social-dock .s-icon { color: var(--text-primary); } -.social-dock .s-icon i { background: none !important; -webkit-background-clip: initial !important; background-clip: initial !important; -webkit-text-fill-color: initial !important; color: currentColor !important; text-shadow: none !important; } \ No newline at end of file +.social-dock .s-icon i { background: none !important; -webkit-background-clip: initial !important; background-clip: initial !important; -webkit-text-fill-color: initial !important; color: currentColor !important; text-shadow: none !important; } +.area-music { padding: 20px; display: flex; flex-direction: column; } +.vinyl-player { position: relative; display: flex; align-items: center; gap: 16px; } +.vinyl-disc { width: 140px; height: 140px; border-radius: 50%; background: radial-gradient(#222 0%, #111 30%, #000 60%, #111 100%); box-shadow: inset 0 0 20px rgba(0,0,0,0.6), 0 8px 20px rgba(0,0,0,0.2); position: relative; } +.vinyl-disc::after { content: ""; position: absolute; inset: 50% auto auto 50%; width: 32px; height: 32px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent-glow); } +.vinyl-arm { width: 80px; height: 8px; background: #888; border-radius: 8px; transform-origin: left center; transform: rotate(12deg); box-shadow: 0 2px 8px rgba(0,0,0,0.2); } +.vinyl-info { display: flex; flex-direction: column; gap: 6px; } +.vinyl-title { font-weight: 700; background: var(--gradient-4); -webkit-background-clip: text; background-clip: text; color: transparent; } +.vinyl-status { font-size: 0.85rem; color: var(--text-secondary); } +.netease-embed { position: absolute; right: 20px; bottom: 10px; width: 340px; height: 86px; border-radius: 12px; overflow: hidden; } +.spinning { animation: spinDisc 8s linear infinite; } +@keyframes spinDisc { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } +.mobile-fab { position: fixed; right: 16px; bottom: 88px; z-index: 1100; } +.fab-main { display: flex; align-items: center; gap: 6px; background: var(--accent); color: #fff; border: none; border-radius: 22px; padding: 10px 14px; box-shadow: 0 8px 18px rgba(0,0,0,0.25); } +.fab-label { font-size: 12px; } +.fab-menu { display: none; flex-direction: column; gap: 10px; margin-top: 10px; } +.fab-item { display: flex; align-items: center; gap: 6px; background: var(--glass-bg); border-radius: 18px; padding: 8px 12px; border: var(--glass-border); box-shadow: var(--glass-shadow); } +.fab-menu.open { display: flex; } \ No newline at end of file diff --git a/js/about.js b/js/about.js index c28ff3c..90c8068 100644 --- a/js/about.js +++ b/js/about.js @@ -1,6 +1,6 @@ /* about.js - Aurora Nexus Core */ -$(document).ready(function() { +$(document).ready(function () { // 启动应用核心 const app = new AppCore(); }); @@ -22,38 +22,90 @@ class I18nManager { this.lang = localStorage.getItem('lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en'); this.dict = { zh: { - "nav.home": "首页", "nav.about": "关于", "nav.blog": "博客", - "status.online": "在线", "profile.name": "Honesty", "profile.role": "Java后端 & AI探索者", "profile.location": "上海, 中国", + "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": "关注者", - "mbti.name": "提倡者", "mbti.desc": "提倡者人格类型的人非常稀少,只有不到1%的人口属于这种类型,但他们对世界的贡献不容忽视。", - "mbti.tag1": "理想主义与道德感", "mbti.tag2": "果断决绝的行动力", "mbti.tag3": "深度洞察与创意", "mbti.tag4": "关怀与同理心", - "tech.title": "技术栈宇宙", "interest.title": "个人兴趣", - "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": "扫码关注获取干货" + "stats.exp": "编程年限", + "stats.repos": "开源项目", + "stats.followers": "关注者", + "mbti.name": "提倡者", + "mbti.desc": "提倡者人格类型的人非常稀少,只有不到1%的人口属于这种类型,但他们对世界的贡献不容忽视。", + "mbti.tag1": "理想主义与道德感", + "mbti.tag2": "果断决绝的行动力", + "mbti.tag3": "深度洞察与创意", + "mbti.tag4": "关怀与同理心", + "tech.title": "技术栈宇宙", + "interest.title": "个人兴趣", + "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": "扫码关注获取干货", + "music.title": "音乐", + "music.playlist": "我的歌单", + "music.play": "播放", + "music.pause": "暂停", + "music.unavailable": "播放器维修中", + "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", + "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", - "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.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" + "stats.exp": "Years Exp", + "stats.repos": "Projects", + "stats.followers": "Followers", + "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.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", + "music.title": "Music", + "music.playlist": "My Playlist", + "music.play": "Play", + "music.pause": "Pause", + "music.unavailable": "Player under maintenance", + "comment.closed": "Comments are closed" } }; this.init(); @@ -73,9 +125,9 @@ class I18nManager { apply() { const t = this.dict[this.lang]; - $('[data-i18n]').each(function() { + $('[data-i18n]').each(function () { const k = $(this).data('i18n'); - if(t[k]) $(this).text(t[k]); + if (t[k]) $(this).text(t[k]); }); const label = this.lang === 'zh' ? 'CN' : 'EN'; $('#lang-btn .btn-text').text(label); @@ -91,6 +143,7 @@ class ThemeManager { this.root = document.documentElement; this.init(); } + init() { const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2'; const timeout = window.SiteConfig?.cacheKeys?.theme?.ttlMs || 360000; @@ -100,22 +153,27 @@ class ThemeManager { var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; var night = hour >= 18 || prefersDark; saved = night ? 'night' : 'day'; - localStorage.setItem(cacheKey, { time: new Date().getTime(), value: saved }); + localStorage.setItem(cacheKey, {time: new Date().getTime(), value: saved}); } - if(saved === 'night') this.root.setAttribute('data-theme', 'night'); + if (saved === 'night') this.root.setAttribute('data-theme', 'night'); $('#theme-btn').toggleClass('is-active', saved === 'night'); $('#theme-btn').on('click', () => { const curr = this.root.getAttribute('data-theme'); const next = curr === 'night' ? 'day' : 'night'; - if(next === 'night') this.root.setAttribute('data-theme', 'night'); + if (next === 'night') this.root.setAttribute('data-theme', 'night'); else this.root.removeAttribute('data-theme'); - localStorage.setItem(cacheKey, { time: new Date().getTime(), value: next }); + localStorage.setItem(cacheKey, {time: new Date().getTime(), value: next}); $('#theme-btn').toggleClass('is-active', next === 'night'); - + // 更新Artalk主题 if (typeof Artalk !== 'undefined') { - Artalk.reload(); + try { + Artalk.reload(); + } catch (e) { + console.error('重新加载评论组件异常:', e); + } + } }); } @@ -143,7 +201,7 @@ class DataManager { const timeout = (window.SiteConfig?.cacheKeys?.github?.ttlMs) || 3600000; // Check Cache (1 hour) - if(cached && (now - cached.time < timeout)) { + if (cached && (now - cached.time < timeout)) { this.renderUser(cached.user); this.renderRepos(cached.repos); return; @@ -197,7 +255,7 @@ class DataManager { } renderRepos(list) { - if(!Array.isArray(list)) list = window.SiteConfig?.defaults?.repos; + if (!Array.isArray(list)) list = window.SiteConfig?.defaults?.repos; let html = ''; list.slice(0, 12).forEach(repo => { // Fix: API field compatibility @@ -231,7 +289,7 @@ class DataManager { const now = Date.now(); // Check Cache (1 hour) - if(cached && (now - cached.time < timeout)) { + if (cached && (now - cached.time < timeout)) { this.renderBlog(cached.posts); return; } @@ -240,11 +298,11 @@ class DataManager { // 尝试从RSS获取 const response = await fetch(rssUrl); 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'); } @@ -252,7 +310,7 @@ class DataManager { // 解析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 || "无标题"; @@ -260,7 +318,7 @@ class DataManager { 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, @@ -294,20 +352,21 @@ class DataManager { let html = ''; const lang = localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en'); const fmtDate = (dStr) => { - if(!dStr) return ''; + if (!dStr) return ''; const d = new Date(dStr); - if(isNaN(d.getTime())) { + 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(_) {} + 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')}`; + ? `${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 => { @@ -341,6 +400,8 @@ class UIManager { this.initBioToggle(); this.initNavInteraction(); this.initProfileGradient(); + this.initMusic(); + this.initFab(); let resizeTimer = null; window.addEventListener('resize', () => { clearTimeout(resizeTimer); @@ -351,16 +412,22 @@ class UIManager { initModal() { window.toggleWechat = () => { const m = $('#wechat-modal'); - m.is(':visible') ? m.fadeOut(200) : m.css('display','flex').hide().fadeIn(200); + m.is(':visible') ? m.fadeOut(200) : m.css('display', 'flex').hide().fadeIn(200); }; - $('#wechat-modal').on('click', function(e) { - if(e.target === this) toggleWechat(); + $('#wechat-modal').on('click', function (e) { + if (e.target === this) toggleWechat(); }); } initArtalk() { // Safe initialization - if(typeof Artalk !== 'undefined' && window.SiteConfig?.artalk) { + const isHttps = location.protocol === 'https:'; + const isLocal = !!(window.SiteConfig?.dev?.isLocal); + if(!isHttps || isLocal) { + $('#artalk-container').html(`
${(new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')]['comment.closed']}
`); + return; + } + if (typeof Artalk !== 'undefined' && window.SiteConfig?.artalk) { try { Artalk.init({ el: '#artalk-container', @@ -370,9 +437,12 @@ class UIManager { site: window.SiteConfig.artalk.site, darkMode: document.documentElement.getAttribute('data-theme') === 'night' }); - } catch(e) { console.error("Artalk Error", e); } + } catch (e) { + console.error("Artalk Error", e); + $('#artalk-container').html(`
${(new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')]['comment.closed']}
`); + } } else { - $('#artalk-container').html('
Message board loading...
'); + $('#artalk-container').html(`
${(new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')]['comment.closed']}
`); } } @@ -380,70 +450,72 @@ class UIManager { const el = document.querySelector('.bio-text'); const btn = document.getElementById('bio-toggle'); const qEl = document.querySelector('.quote-box p'); - if(!el || !btn) return; + if (!el || !btn) return; const assess = () => { el.classList.add('collapsed'); - if(qEl) qEl.classList.add('quote-collapsed'); + if (qEl) qEl.classList.add('quote-collapsed'); const needsToggle = (el.scrollHeight > el.clientHeight) || (qEl && qEl.scrollHeight > qEl.clientHeight) || (window.innerWidth < 480 && ((el.textContent || '').length + (qEl?.textContent?.length || 0)) > 120); - if(needsToggle) { + if (needsToggle) { btn.style.display = 'inline-block'; } else { btn.style.display = 'none'; el.classList.remove('collapsed'); - if(qEl) qEl.classList.remove('quote-collapsed'); + if (qEl) qEl.classList.remove('quote-collapsed'); } }; assess(); - window.addEventListener('resize', () => assess(), { passive: true }); + window.addEventListener('resize', () => assess(), {passive: true}); btn.addEventListener('click', () => { el.classList.toggle('collapsed'); - if(qEl) qEl.classList.toggle('quote-collapsed'); + if (qEl) qEl.classList.toggle('quote-collapsed'); }); } initNavInteraction() { const nav = document.querySelector('.glass-nav'); - if(!nav) return; + if (!nav) return; const onScroll = () => { const y = window.scrollY || document.documentElement.scrollTop; nav.classList.toggle('nav-scrolled', y > 30); }; onScroll(); - window.addEventListener('scroll', onScroll, { passive: true }); + window.addEventListener('scroll', onScroll, {passive: true}); } initProfileGradient() { const name = document.querySelector('.hero-name'); const role = document.querySelector('.hero-role'); const loc = document.querySelector('.location-tag'); - if(name) name.classList.add('grad-text-1','night-glow','glow-cycle'); - if(role) role.classList.add('grad-text-2','night-glow','glow-cycle'); - if(loc) loc.classList.add('grad-text-3','night-glow','glow-cycle'); + if (name) name.classList.add('grad-text-1', 'night-glow', 'glow-cycle'); + if (role) role.classList.add('grad-text-2', 'night-glow', 'glow-cycle'); + if (loc) loc.classList.add('grad-text-3', 'night-glow', 'glow-cycle'); } initTechCloud() { const container = document.getElementById('tech-container'); - if(!container) return; + 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 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(10, Number(item.gradientId))) : (hash % 10) + 1; - return { ...item, gradientId: gid }; + return {...item, gradientId: gid}; }); const isMobile = window.matchMedia('(max-width: 768px)').matches; container.innerHTML = ''; - if(isMobile) { + if (isMobile) { // 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 buckets = Array.from({length: rows}, () => []); + techStack.forEach((item, i) => { + buckets[i % rows].push(item); + }); const appendItem = (rowEl, item, idx) => { const el = document.createElement('span'); @@ -457,8 +529,8 @@ class UIManager { buckets.forEach((items, rIdx) => { const rowEl = document.createElement('div'); - rowEl.className = `tech-row row-${rIdx+1}`; - rowEl.style.gridRow = `${rIdx+1}`; + 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); @@ -477,61 +549,64 @@ class UIManager { el.innerText = item.name; el.style.border = 'none'; container.appendChild(el); - tags.push({ el, x:0, y:0, z:0 }); + 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; + const dtr = Math.PI / 180; + let lasta = 1, lastb = 1; + let active = false, mouseX = 0, mouseY = 0; // Init positions tags.forEach((tag, i) => { - let phi = Math.acos(-1 + (2*i+1)/tags.length); + 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.onmouseover = () => active = true; + container.onmouseout = () => active = false; container.onmousemove = (e) => { let rect = container.getBoundingClientRect(); - mouseX = (e.clientX - (rect.left + rect.width/2)) / 5; - mouseY = (e.clientY - (rect.top + rect.height/2)) / 5; + 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; + 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; + lasta = a; + lastb = b; - if(Math.abs(a)<=0.01 && Math.abs(b)<=0.01 && !active) a=0.5; // Keep spinning slowly + 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); + let sa = Math.sin(a * dtr), ca = Math.cos(a * dtr); + let sb = Math.sin(b * dtr), cb = Math.cos(b * dtr); tags.forEach(tag => { - let rx1=tag.x, ry1=tag.y*ca - tag.z*sa, rz1=tag.y*sa + tag.z*ca; - let rx2=rx1*cb + rz1*sb, ry2=ry1, rz2=rx1*-sb + rz1*cb; - tag.x=rx2; tag.y=ry2; tag.z=rz2; + let rx1 = tag.x, ry1 = tag.y * ca - tag.z * sa, rz1 = tag.y * sa + tag.z * ca; + let rx2 = rx1 * cb + rz1 * sb, ry2 = ry1, rz2 = rx1 * -sb + rz1 * cb; + tag.x = rx2; + tag.y = ry2; + tag.z = rz2; - let scale = (tag.z + radius)/(2*radius) + 0.45; + let scale = (tag.z + radius) / (2 * radius) + 0.45; scale = Math.min(Math.max(scale, 0.7), 1.15); - let opacity = (tag.z + radius)/(2*radius) + 0.2; + let opacity = (tag.z + radius) / (2 * radius) + 0.2; tag.el.style.opacity = 1; - tag.el.style.zIndex = parseInt(scale*100); - let left = tag.x + container.offsetWidth/2 - tag.el.offsetWidth/2; - let top = tag.y + container.offsetHeight/2 - tag.el.offsetHeight/2; + tag.el.style.zIndex = parseInt(scale * 100); + let left = tag.x + container.offsetWidth / 2 - tag.el.offsetWidth / 2; + let top = tag.y + container.offsetHeight / 2 - tag.el.offsetHeight / 2; tag.el.style.transform = `translate(${left}px, ${top}px) scale(${scale})`; }); requestAnimationFrame(update); @@ -539,4 +614,112 @@ class UIManager { update(); } } + + initMusic() { + const embed = null; // 不再使用 iframe 嵌入 + const disc = document.getElementById('vinyl-disc'); + const btn = document.getElementById('music-toggle'); + const statusEl = document.getElementById('music-status'); + if (!disc || !btn) return; + let playing = true; + let loaded = false; + const fail = () => { + playing = false; + embed.style.display = 'none'; + if (statusEl) statusEl.style.display = 'inline'; + updateUI(); + }; + const updateUI = () => { + if (playing) { + disc.classList.add('spinning'); + btn.textContent = this._t('music.pause') || 'Pause'; + } else { + disc.classList.remove('spinning'); + btn.textContent = this._t('music.play') || 'Play'; + } + }; + btn.addEventListener('click', () => { + playing = !playing; + updateUI(); + }); + const api = `https://api.injahow.cn/meting/?type=playlist&id=${(window.SiteConfig?.music?.playlistId) || 5131713379}&server=netease`; + try { + fetch(api).then(r => r.json()).then(list => { + if (!Array.isArray(list) || !window.APlayer) throw new Error('load fail'); + const audio = list.map(i => ({name: i.title, artist: i.author, url: i.url, cover: i.pic, lrc: i.lrc})); + this.aplayer = new APlayer({ + container: document.getElementById('aplayer'), + theme: getComputedStyle(document.documentElement).getPropertyValue('--accent') || '#6c5ce7', + autoplay: true, + listFolded: true, + audio + }); + this.aplayer.on('play', () => { + playing = true; + updateUI(); + }); + this.aplayer.on('pause', () => { + playing = false; + updateUI(); + }); + btn.addEventListener('click', () => { + this.aplayer.toggle(); + }); + loaded = true; + }).catch(() => fail()); + setTimeout(() => { + if (!loaded) fail(); + }, 6000); + } catch (_) { + fail(); + } + updateUI(); + } + + 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 = () => { + const lang = localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en'); + const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2'; + const themeBean = localStorage.getItem(cacheKey); + let theme = 'day'; + if (themeBean != null && themeBean.value != null) { + theme = themeBean.value; + } + fLang.querySelector('.fab-text').textContent = lang === 'zh' ? '中文' : 'English'; + fTheme.querySelector('.fab-text').textContent = theme === 'night' ? 'Night' : 'Day'; + const mt = document.getElementById('music-toggle'); + fMusic.querySelector('.fab-text').textContent = mt ? mt.textContent : 'Music'; + }; + main.addEventListener('click', () => { + menu.classList.toggle('open'); + updateLabels(); + }); + fLang.addEventListener('click', () => { + document.getElementById('lang-btn')?.click(); + updateLabels(); + }); + fTheme.addEventListener('click', () => { + document.getElementById('theme-btn')?.click(); + updateLabels(); + }); + fMusic.addEventListener('click', () => { + document.getElementById('music-toggle')?.click(); + updateLabels(); + }); + updateLabels(); + } + + _t(key) { + try { + return (new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')][key]; + } catch (_) { + return null; + } + } } \ No newline at end of file diff --git a/js/config.js b/js/config.js index 614a3c3..be2f208 100644 --- a/js/config.js +++ b/js/config.js @@ -89,6 +89,11 @@ const SiteConfig = { { name: 'Postgresql', category: 'data', weight: 1 } ], + music: { + playlistId: 5131713379, + embedUrl: 'https://music.163.com/outchain/player?type=0&id=5131713379&auto=1&height=66' + }, + // 默认数据(当API或RSS不可用时使用) defaults: { repos: [