feat(about): 添加音乐播放器和移动端悬浮菜单功能
- 在 about.html 中引入 APlayer 样式与脚本依赖 - 新增音乐模块区域 `.area-music` 并实现黑胶唱片动画效果 - 添加移动端悬浮功能按钮(语言切换、主题切换、音乐控制) - 扩展 CSS 样式支持音乐模块布局及交互反馈 - 在 JavaScript 中初始化音乐播放器并集成 APlayer - 实现移动端功能菜单逻辑及其国际化支持 - 更新配置文件添加网易云歌单ID及相关默认设置 - 优化部分原有代码结构与可读性
This commit is contained in:
30
about.html
30
about.html
@@ -9,6 +9,7 @@
|
|||||||
<link href="https://cdn.bootcdn.net/ajax/libs/remixicon/3.5.0/remixicon.min.css" rel="stylesheet">
|
<link href="https://cdn.bootcdn.net/ajax/libs/remixicon/3.5.0/remixicon.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/about.css">
|
<link rel="stylesheet" href="css/about.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/aplayer/1.10.1/APlayer.min.css">
|
||||||
<!-- Artalk 评论样式 -->
|
<!-- Artalk 评论样式 -->
|
||||||
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.css">
|
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.css">
|
||||||
<link rel="icon" href="favicon.ico">
|
<link rel="icon" href="favicon.ico">
|
||||||
@@ -188,6 +189,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- [7] 音乐模块 (Music) -->
|
||||||
|
<div class="bento-card area-music">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="glow-title"><i class="ri-music-2-line"></i> <span data-i18n="music.title">Music</span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="vinyl-player">
|
||||||
|
<div class="vinyl-disc" id="vinyl-disc"></div>
|
||||||
|
<div class="vinyl-arm"></div>
|
||||||
|
<div class="vinyl-info">
|
||||||
|
<span class="vinyl-title" data-i18n="music.playlist">My Playlist</span>
|
||||||
|
<button id="music-toggle" class="link-btn" aria-label="Toggle Music">Play</button>
|
||||||
|
<span id="music-status" class="vinyl-status" style="display:none;" data-i18n="music.unavailable">Player under maintenance</span>
|
||||||
|
</div>
|
||||||
|
<div id="aplayer" class="aplayer" style="flex:1;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 移动端显示的社交栏 (6个链接) -->
|
<!-- 移动端显示的社交栏 (6个链接) -->
|
||||||
<div class="bento-card area-social-mobile mobile-social">
|
<div class="bento-card area-social-mobile mobile-social">
|
||||||
<a href="https://github.com/listener-He" class="ms-btn"><i class="ri-github-fill"></i></a>
|
<a href="https://github.com/listener-He" class="ms-btn"><i class="ri-github-fill"></i></a>
|
||||||
@@ -242,6 +260,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端悬浮功能按钮 -->
|
||||||
|
<div class="mobile-fab">
|
||||||
|
<button id="fab-main" class="fab-main" aria-label="Actions"><i class="ri-magic-line"></i><span class="fab-label">More</span></button>
|
||||||
|
<div class="fab-menu" id="fab-menu">
|
||||||
|
<button id="fab-lang" class="fab-item"><i class="ri-translate-2"></i><span class="fab-text">Lang</span></button>
|
||||||
|
<button id="fab-theme" class="fab-item"><i class="ri-moon-line"></i><span class="fab-text">Theme</span></button>
|
||||||
|
<button id="fab-music" class="fab-item"><i class="ri-music-2-line"></i><span class="fab-text">Music</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 微信弹窗 -->
|
<!-- 微信弹窗 -->
|
||||||
@@ -262,10 +289,11 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 脚本:BootCDN jQuery -->
|
<!-- 脚本:BootCDN jQuery / Artalk / APlayer -->
|
||||||
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||||
<script src="js/config.js"></script>
|
<script src="js/config.js"></script>
|
||||||
<script src="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.js"></script>
|
<script src="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.js"></script>
|
||||||
|
<script src="https://cdn.bootcdn.net/ajax/libs/aplayer/1.10.1/APlayer.min.js"></script>
|
||||||
<script src="js/about.js"></script>
|
<script src="js/about.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -321,11 +321,12 @@ body {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
/* PC: 3 Col, 2 Row Main */
|
/* PC: 3 Col, 2 Row Main */
|
||||||
grid-template-columns: 320px 1fr 260px;
|
grid-template-columns: 320px 1fr 260px;
|
||||||
grid-template-rows: 240px 220px auto;
|
grid-template-rows: 240px 220px auto auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"profile bio stats"
|
"profile bio stats"
|
||||||
"profile bio mbti"
|
"profile bio mbti"
|
||||||
"tech tech interests";
|
"tech tech interests"
|
||||||
|
"music music interests";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Areas --- */
|
/* --- Areas --- */
|
||||||
@@ -352,6 +353,7 @@ body {
|
|||||||
.area-interests {
|
.area-interests {
|
||||||
grid-area: interests;
|
grid-area: interests;
|
||||||
}
|
}
|
||||||
|
.area-music { grid-area: music; }
|
||||||
|
|
||||||
.mobile-social {
|
.mobile-social {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -1575,4 +1577,21 @@ body {
|
|||||||
-webkit-text-fill-color: initial;
|
-webkit-text-fill-color: initial;
|
||||||
}
|
}
|
||||||
.social-dock .s-icon { color: var(--text-primary); }
|
.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; }
|
.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; }
|
||||||
377
js/about.js
377
js/about.js
@@ -1,6 +1,6 @@
|
|||||||
/* about.js - Aurora Nexus Core */
|
/* about.js - Aurora Nexus Core */
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function () {
|
||||||
// 启动应用核心
|
// 启动应用核心
|
||||||
const app = new AppCore();
|
const app = new AppCore();
|
||||||
});
|
});
|
||||||
@@ -22,38 +22,90 @@ class I18nManager {
|
|||||||
this.lang = localStorage.getItem('lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
this.lang = localStorage.getItem('lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
||||||
this.dict = {
|
this.dict = {
|
||||||
zh: {
|
zh: {
|
||||||
"nav.home": "首页", "nav.about": "关于", "nav.blog": "博客",
|
"nav.home": "首页",
|
||||||
"status.online": "在线", "profile.name": "Honesty", "profile.role": "Java后端 & AI探索者", "profile.location": "上海, 中国",
|
"nav.about": "关于",
|
||||||
|
"nav.blog": "博客",
|
||||||
|
"status.online": "在线",
|
||||||
|
"profile.name": "Honesty",
|
||||||
|
"profile.role": "Java后端 & AI探索者",
|
||||||
|
"profile.location": "上海, 中国",
|
||||||
"bio.label": "关于我",
|
"bio.label": "关于我",
|
||||||
"bio.text": "我是一名充满热情的Java后端开发工程师,专注于AI技术的探索与应用。来自湖南,现在上海工作,享受在这座充满活力的城市中追求技术梦想。",
|
"bio.text": "我是一名充满热情的Java后端开发工程师,专注于AI技术的探索与应用。来自湖南,现在上海工作,享受在这座充满活力的城市中追求技术梦想。",
|
||||||
"bio.quote": "我追求技术的深度理解而非广度堆砌,每一项技术的学习都源于解决实际问题的内在驱动。作为INFJ人格类型,我善于深度思考,注重细节,喜欢通过代码创造有意义的产品。我相信技术的力量能够改变世界,也热衷于在开源社区中分享知识与经验。",
|
"bio.quote": "我追求技术的深度理解而非广度堆砌,每一项技术的学习都源于解决实际问题的内在驱动。作为INFJ人格类型,我善于深度思考,注重细节,喜欢通过代码创造有意义的产品。我相信技术的力量能够改变世界,也热衷于在开源社区中分享知识与经验。",
|
||||||
"stats.exp": "编程年限", "stats.repos": "开源项目", "stats.followers": "关注者",
|
"stats.exp": "编程年限",
|
||||||
"mbti.name": "提倡者", "mbti.desc": "提倡者人格类型的人非常稀少,只有不到1%的人口属于这种类型,但他们对世界的贡献不容忽视。",
|
"stats.repos": "开源项目",
|
||||||
"mbti.tag1": "理想主义与道德感", "mbti.tag2": "果断决绝的行动力", "mbti.tag3": "深度洞察与创意", "mbti.tag4": "关怀与同理心",
|
"stats.followers": "关注者",
|
||||||
"tech.title": "技术栈宇宙", "interest.title": "个人兴趣",
|
"mbti.name": "提倡者",
|
||||||
"interest.cycling": "骑行", "interest.cycling_desc": "享受在路上的自由感,用车轮丈量世界,在风景中寻找灵感",
|
"mbti.desc": "提倡者人格类型的人非常稀少,只有不到1%的人口属于这种类型,但他们对世界的贡献不容忽视。",
|
||||||
"interest.reading": "阅读", "interest.reading_desc": "通过文字记录思考轨迹,分享技术见解,用代码诠释创意",
|
"mbti.tag1": "理想主义与道德感",
|
||||||
"interest.opensource": "开源", "interest.opensource_desc": "热衷于开源项目,相信分享的力量,用代码连接世界",
|
"mbti.tag2": "果断决绝的行动力",
|
||||||
"interest.learning": "持续学习", "interest.learning_desc": "保持对新技术的好奇心,在变化中成长,在挑战中进步",
|
"mbti.tag3": "深度洞察与创意",
|
||||||
"github.title": "开源贡献", "blog.title": "最新文章", "blog.more": "查看更多",
|
"mbti.tag4": "关怀与同理心",
|
||||||
"comment.title": "留言板", "modal.wechat": "微信公众号", "modal.desc": "扫码关注获取干货"
|
"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: {
|
en: {
|
||||||
"nav.home": "Home", "nav.about": "About", "nav.blog": "Blog",
|
"nav.home": "Home",
|
||||||
"status.online": "Available", "profile.name": "Honesty", "profile.role": "Java Backend & AI Dev", "profile.location": "Shanghai, China",
|
"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.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.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.",
|
"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.exp": "Years Exp",
|
||||||
"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.",
|
"stats.repos": "Projects",
|
||||||
"mbti.tag1": "Idealism & Morality", "mbti.tag2": "Decisive Action", "mbti.tag3": "Deep Insight & Creativity", "mbti.tag4": "Care & Empathy",
|
"stats.followers": "Followers",
|
||||||
"tech.title": "Tech Universe", "interest.title": "Interests",
|
"mbti.name": "Advocate",
|
||||||
"interest.cycling": "Cycling", "interest.cycling_desc": "Enjoy the freedom on the road, measure the world with wheels, and find inspiration in the scenery",
|
"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.",
|
||||||
"interest.reading": "Reading", "interest.reading_desc": "Record thinking trajectories through text, share technical insights, and interpret creativity with code",
|
"mbti.tag1": "Idealism & Morality",
|
||||||
"interest.opensource": "Open Source", "interest.opensource_desc": "Passionate about open source projects, believing in the power of sharing, connecting the world with code",
|
"mbti.tag2": "Decisive Action",
|
||||||
"interest.learning": "Learning", "interest.learning_desc": "Maintain curiosity about new technologies, grow through change, and progress through challenges",
|
"mbti.tag3": "Deep Insight & Creativity",
|
||||||
"github.title": "Open Source", "blog.title": "Latest Posts", "blog.more": "View All",
|
"mbti.tag4": "Care & Empathy",
|
||||||
"comment.title": "Message Board", "modal.wechat": "WeChat Account", "modal.desc": "Scan for tech insights"
|
"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();
|
this.init();
|
||||||
@@ -73,9 +125,9 @@ class I18nManager {
|
|||||||
|
|
||||||
apply() {
|
apply() {
|
||||||
const t = this.dict[this.lang];
|
const t = this.dict[this.lang];
|
||||||
$('[data-i18n]').each(function() {
|
$('[data-i18n]').each(function () {
|
||||||
const k = $(this).data('i18n');
|
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';
|
const label = this.lang === 'zh' ? 'CN' : 'EN';
|
||||||
$('#lang-btn .btn-text').text(label);
|
$('#lang-btn .btn-text').text(label);
|
||||||
@@ -91,6 +143,7 @@ class ThemeManager {
|
|||||||
this.root = document.documentElement;
|
this.root = document.documentElement;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2';
|
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2';
|
||||||
const timeout = window.SiteConfig?.cacheKeys?.theme?.ttlMs || 360000;
|
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 prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
var night = hour >= 18 || prefersDark;
|
var night = hour >= 18 || prefersDark;
|
||||||
saved = night ? 'night' : 'day';
|
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').toggleClass('is-active', saved === 'night');
|
||||||
|
|
||||||
$('#theme-btn').on('click', () => {
|
$('#theme-btn').on('click', () => {
|
||||||
const curr = this.root.getAttribute('data-theme');
|
const curr = this.root.getAttribute('data-theme');
|
||||||
const next = curr === 'night' ? 'day' : 'night';
|
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');
|
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');
|
$('#theme-btn').toggleClass('is-active', next === 'night');
|
||||||
|
|
||||||
// 更新Artalk主题
|
// 更新Artalk主题
|
||||||
if (typeof Artalk !== 'undefined') {
|
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;
|
const timeout = (window.SiteConfig?.cacheKeys?.github?.ttlMs) || 3600000;
|
||||||
|
|
||||||
// Check Cache (1 hour)
|
// Check Cache (1 hour)
|
||||||
if(cached && (now - cached.time < timeout)) {
|
if (cached && (now - cached.time < timeout)) {
|
||||||
this.renderUser(cached.user);
|
this.renderUser(cached.user);
|
||||||
this.renderRepos(cached.repos);
|
this.renderRepos(cached.repos);
|
||||||
return;
|
return;
|
||||||
@@ -197,7 +255,7 @@ class DataManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderRepos(list) {
|
renderRepos(list) {
|
||||||
if(!Array.isArray(list)) list = window.SiteConfig?.defaults?.repos;
|
if (!Array.isArray(list)) list = window.SiteConfig?.defaults?.repos;
|
||||||
let html = '';
|
let html = '';
|
||||||
list.slice(0, 12).forEach(repo => {
|
list.slice(0, 12).forEach(repo => {
|
||||||
// Fix: API field compatibility
|
// Fix: API field compatibility
|
||||||
@@ -231,7 +289,7 @@ class DataManager {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Check Cache (1 hour)
|
// Check Cache (1 hour)
|
||||||
if(cached && (now - cached.time < timeout)) {
|
if (cached && (now - cached.time < timeout)) {
|
||||||
this.renderBlog(cached.posts);
|
this.renderBlog(cached.posts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -240,11 +298,11 @@ class DataManager {
|
|||||||
// 尝试从RSS获取
|
// 尝试从RSS获取
|
||||||
const response = await fetch(rssUrl);
|
const response = await fetch(rssUrl);
|
||||||
if (!response.ok) throw new Error('RSS fetch failed');
|
if (!response.ok) throw new Error('RSS fetch failed');
|
||||||
|
|
||||||
const xmlText = await response.text();
|
const xmlText = await response.text();
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const xmlDoc = parser.parseFromString(xmlText, "application/xml");
|
const xmlDoc = parser.parseFromString(xmlText, "application/xml");
|
||||||
|
|
||||||
if (xmlDoc.documentElement.nodeName === "parsererror") {
|
if (xmlDoc.documentElement.nodeName === "parsererror") {
|
||||||
throw new Error('RSS XML parse error');
|
throw new Error('RSS XML parse error');
|
||||||
}
|
}
|
||||||
@@ -252,7 +310,7 @@ class DataManager {
|
|||||||
// 解析RSS项目
|
// 解析RSS项目
|
||||||
const items = xmlDoc.getElementsByTagName("item");
|
const items = xmlDoc.getElementsByTagName("item");
|
||||||
const posts = [];
|
const posts = [];
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(items.length, 5); i++) {
|
for (let i = 0; i < Math.min(items.length, 5); i++) {
|
||||||
const item = items[i];
|
const item = items[i];
|
||||||
const title = item.querySelector("title")?.textContent || "无标题";
|
const title = item.querySelector("title")?.textContent || "无标题";
|
||||||
@@ -260,7 +318,7 @@ class DataManager {
|
|||||||
const pubDate = item.querySelector("pubDate")?.textContent || "";
|
const pubDate = item.querySelector("pubDate")?.textContent || "";
|
||||||
const categoryNodes = item.querySelectorAll("category");
|
const categoryNodes = item.querySelectorAll("category");
|
||||||
const categories = Array.from(categoryNodes).map(n => (n.textContent || '').trim()).filter(Boolean);
|
const categories = Array.from(categoryNodes).map(n => (n.textContent || '').trim()).filter(Boolean);
|
||||||
|
|
||||||
posts.push({
|
posts.push({
|
||||||
title,
|
title,
|
||||||
link,
|
link,
|
||||||
@@ -294,20 +352,21 @@ class DataManager {
|
|||||||
let html = '';
|
let html = '';
|
||||||
const lang = localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en');
|
const lang = localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en');
|
||||||
const fmtDate = (dStr) => {
|
const fmtDate = (dStr) => {
|
||||||
if(!dStr) return '';
|
if (!dStr) return '';
|
||||||
const d = new Date(dStr);
|
const d = new Date(dStr);
|
||||||
if(isNaN(d.getTime())) {
|
if (isNaN(d.getTime())) {
|
||||||
try {
|
try {
|
||||||
const parsed = new Date(Date.parse(dStr));
|
const parsed = new Date(Date.parse(dStr));
|
||||||
if(!isNaN(parsed.getTime())) return lang === 'zh'
|
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')}日`
|
||||||
: `${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(_) {}
|
} catch (_) {
|
||||||
|
}
|
||||||
return dStr;
|
return dStr;
|
||||||
}
|
}
|
||||||
return lang === 'zh'
|
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 => {
|
list.slice(0, 5).forEach(post => {
|
||||||
@@ -341,6 +400,8 @@ class UIManager {
|
|||||||
this.initBioToggle();
|
this.initBioToggle();
|
||||||
this.initNavInteraction();
|
this.initNavInteraction();
|
||||||
this.initProfileGradient();
|
this.initProfileGradient();
|
||||||
|
this.initMusic();
|
||||||
|
this.initFab();
|
||||||
let resizeTimer = null;
|
let resizeTimer = null;
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
clearTimeout(resizeTimer);
|
clearTimeout(resizeTimer);
|
||||||
@@ -351,16 +412,22 @@ class UIManager {
|
|||||||
initModal() {
|
initModal() {
|
||||||
window.toggleWechat = () => {
|
window.toggleWechat = () => {
|
||||||
const m = $('#wechat-modal');
|
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) {
|
$('#wechat-modal').on('click', function (e) {
|
||||||
if(e.target === this) toggleWechat();
|
if (e.target === this) toggleWechat();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initArtalk() {
|
initArtalk() {
|
||||||
// Safe initialization
|
// 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(`<div style="text-align:center;color:#999;padding:20px;">${(new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')]['comment.closed']}</div>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof Artalk !== 'undefined' && window.SiteConfig?.artalk) {
|
||||||
try {
|
try {
|
||||||
Artalk.init({
|
Artalk.init({
|
||||||
el: '#artalk-container',
|
el: '#artalk-container',
|
||||||
@@ -370,9 +437,12 @@ class UIManager {
|
|||||||
site: window.SiteConfig.artalk.site,
|
site: window.SiteConfig.artalk.site,
|
||||||
darkMode: document.documentElement.getAttribute('data-theme') === 'night'
|
darkMode: document.documentElement.getAttribute('data-theme') === 'night'
|
||||||
});
|
});
|
||||||
} catch(e) { console.error("Artalk Error", e); }
|
} catch (e) {
|
||||||
|
console.error("Artalk Error", e);
|
||||||
|
$('#artalk-container').html(`<div style="text-align:center;color:#999;padding:20px;">${(new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')]['comment.closed']}</div>`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$('#artalk-container').html('<div style="text-align:center;color:#999;padding:20px;">Message board loading...</div>');
|
$('#artalk-container').html(`<div style="text-align:center;color:#999;padding:20px;">${(new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')]['comment.closed']}</div>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,70 +450,72 @@ class UIManager {
|
|||||||
const el = document.querySelector('.bio-text');
|
const el = document.querySelector('.bio-text');
|
||||||
const btn = document.getElementById('bio-toggle');
|
const btn = document.getElementById('bio-toggle');
|
||||||
const qEl = document.querySelector('.quote-box p');
|
const qEl = document.querySelector('.quote-box p');
|
||||||
if(!el || !btn) return;
|
if (!el || !btn) return;
|
||||||
const assess = () => {
|
const assess = () => {
|
||||||
el.classList.add('collapsed');
|
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);
|
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';
|
btn.style.display = 'inline-block';
|
||||||
} else {
|
} else {
|
||||||
btn.style.display = 'none';
|
btn.style.display = 'none';
|
||||||
el.classList.remove('collapsed');
|
el.classList.remove('collapsed');
|
||||||
if(qEl) qEl.classList.remove('quote-collapsed');
|
if (qEl) qEl.classList.remove('quote-collapsed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
assess();
|
assess();
|
||||||
window.addEventListener('resize', () => assess(), { passive: true });
|
window.addEventListener('resize', () => assess(), {passive: true});
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
el.classList.toggle('collapsed');
|
el.classList.toggle('collapsed');
|
||||||
if(qEl) qEl.classList.toggle('quote-collapsed');
|
if (qEl) qEl.classList.toggle('quote-collapsed');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initNavInteraction() {
|
initNavInteraction() {
|
||||||
const nav = document.querySelector('.glass-nav');
|
const nav = document.querySelector('.glass-nav');
|
||||||
if(!nav) return;
|
if (!nav) return;
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
const y = window.scrollY || document.documentElement.scrollTop;
|
const y = window.scrollY || document.documentElement.scrollTop;
|
||||||
nav.classList.toggle('nav-scrolled', y > 30);
|
nav.classList.toggle('nav-scrolled', y > 30);
|
||||||
};
|
};
|
||||||
onScroll();
|
onScroll();
|
||||||
window.addEventListener('scroll', onScroll, { passive: true });
|
window.addEventListener('scroll', onScroll, {passive: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
initProfileGradient() {
|
initProfileGradient() {
|
||||||
const name = document.querySelector('.hero-name');
|
const name = document.querySelector('.hero-name');
|
||||||
const role = document.querySelector('.hero-role');
|
const role = document.querySelector('.hero-role');
|
||||||
const loc = document.querySelector('.location-tag');
|
const loc = document.querySelector('.location-tag');
|
||||||
if(name) name.classList.add('grad-text-1','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 (role) role.classList.add('grad-text-2', 'night-glow', 'glow-cycle');
|
||||||
if(loc) loc.classList.add('grad-text-3','night-glow','glow-cycle');
|
if (loc) loc.classList.add('grad-text-3', 'night-glow', 'glow-cycle');
|
||||||
}
|
}
|
||||||
|
|
||||||
initTechCloud() {
|
initTechCloud() {
|
||||||
const container = document.getElementById('tech-container');
|
const container = document.getElementById('tech-container');
|
||||||
if(!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const techStackRaw = window.SiteConfig?.techStack || [];
|
const techStackRaw = window.SiteConfig?.techStack || [];
|
||||||
const techStack = techStackRaw.map((item, idx) => {
|
const techStack = techStackRaw.map((item, idx) => {
|
||||||
const name = item.name || '';
|
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))
|
const gid = Number(item.gradientId) && Number.isFinite(Number(item.gradientId))
|
||||||
? Math.max(1, Math.min(10, Number(item.gradientId)))
|
? Math.max(1, Math.min(10, Number(item.gradientId)))
|
||||||
: (hash % 10) + 1;
|
: (hash % 10) + 1;
|
||||||
return { ...item, gradientId: gid };
|
return {...item, gradientId: gid};
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
if(isMobile) {
|
if (isMobile) {
|
||||||
// Mobile: 3-row seamless marquee
|
// Mobile: 3-row seamless marquee
|
||||||
container.classList.add('mobile-scroll');
|
container.classList.add('mobile-scroll');
|
||||||
const rows = 3;
|
const rows = 3;
|
||||||
const buckets = Array.from({ length: rows }, () => []);
|
const buckets = Array.from({length: rows}, () => []);
|
||||||
techStack.forEach((item, i) => { buckets[i % rows].push(item); });
|
techStack.forEach((item, i) => {
|
||||||
|
buckets[i % rows].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
const appendItem = (rowEl, item, idx) => {
|
const appendItem = (rowEl, item, idx) => {
|
||||||
const el = document.createElement('span');
|
const el = document.createElement('span');
|
||||||
@@ -457,8 +529,8 @@ class UIManager {
|
|||||||
|
|
||||||
buckets.forEach((items, rIdx) => {
|
buckets.forEach((items, rIdx) => {
|
||||||
const rowEl = document.createElement('div');
|
const rowEl = document.createElement('div');
|
||||||
rowEl.className = `tech-row row-${rIdx+1}`;
|
rowEl.className = `tech-row row-${rIdx + 1}`;
|
||||||
rowEl.style.gridRow = `${rIdx+1}`;
|
rowEl.style.gridRow = `${rIdx + 1}`;
|
||||||
items.forEach((item, idx) => appendItem(rowEl, item, idx));
|
items.forEach((item, idx) => appendItem(rowEl, item, idx));
|
||||||
items.forEach((item, idx) => appendItem(rowEl, item, idx + items.length));
|
items.forEach((item, idx) => appendItem(rowEl, item, idx + items.length));
|
||||||
container.appendChild(rowEl);
|
container.appendChild(rowEl);
|
||||||
@@ -477,61 +549,64 @@ class UIManager {
|
|||||||
el.innerText = item.name;
|
el.innerText = item.name;
|
||||||
el.style.border = 'none';
|
el.style.border = 'none';
|
||||||
container.appendChild(el);
|
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);
|
let radius = Math.max(160, Math.min(container.offsetWidth, container.offsetHeight) / 2 - 24);
|
||||||
const dtr = Math.PI/180;
|
const dtr = Math.PI / 180;
|
||||||
let lasta=1, lastb=1;
|
let lasta = 1, lastb = 1;
|
||||||
let active=false, mouseX=0, mouseY=0;
|
let active = false, mouseX = 0, mouseY = 0;
|
||||||
|
|
||||||
// Init positions
|
// Init positions
|
||||||
tags.forEach((tag, i) => {
|
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;
|
let theta = Math.sqrt(tags.length * Math.PI) * phi;
|
||||||
tag.x = radius * Math.cos(theta) * Math.sin(phi);
|
tag.x = radius * Math.cos(theta) * Math.sin(phi);
|
||||||
tag.y = radius * Math.sin(theta) * Math.sin(phi);
|
tag.y = radius * Math.sin(theta) * Math.sin(phi);
|
||||||
tag.z = radius * Math.cos(phi);
|
tag.z = radius * Math.cos(phi);
|
||||||
});
|
});
|
||||||
|
|
||||||
container.onmouseover = () => active=true;
|
container.onmouseover = () => active = true;
|
||||||
container.onmouseout = () => active=false;
|
container.onmouseout = () => active = false;
|
||||||
container.onmousemove = (e) => {
|
container.onmousemove = (e) => {
|
||||||
let rect = container.getBoundingClientRect();
|
let rect = container.getBoundingClientRect();
|
||||||
mouseX = (e.clientX - (rect.left + rect.width/2)) / 5;
|
mouseX = (e.clientX - (rect.left + rect.width / 2)) / 5;
|
||||||
mouseY = (e.clientY - (rect.top + rect.height/2)) / 5;
|
mouseY = (e.clientY - (rect.top + rect.height / 2)) / 5;
|
||||||
};
|
};
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
let a, b;
|
let a, b;
|
||||||
if(active) {
|
if (active) {
|
||||||
a = (-Math.min(Math.max(-mouseY, -200), 200)/radius) * 2;
|
a = (-Math.min(Math.max(-mouseY, -200), 200) / radius) * 2;
|
||||||
b = (Math.min(Math.max(-mouseX, -200), 200)/radius) * 2;
|
b = (Math.min(Math.max(-mouseX, -200), 200) / radius) * 2;
|
||||||
} else {
|
} else {
|
||||||
a = lasta * 0.98; // Auto rotate
|
a = lasta * 0.98; // Auto rotate
|
||||||
b = lastb * 0.98;
|
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 sa = Math.sin(a * dtr), ca = Math.cos(a * dtr);
|
||||||
let sb=Math.sin(b*dtr), cb=Math.cos(b*dtr);
|
let sb = Math.sin(b * dtr), cb = Math.cos(b * dtr);
|
||||||
|
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
let rx1=tag.x, ry1=tag.y*ca - tag.z*sa, rz1=tag.y*sa + tag.z*ca;
|
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;
|
let rx2 = rx1 * cb + rz1 * sb, ry2 = ry1, rz2 = rx1 * -sb + rz1 * cb;
|
||||||
tag.x=rx2; tag.y=ry2; tag.z=rz2;
|
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);
|
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.opacity = 1;
|
||||||
tag.el.style.zIndex = parseInt(scale*100);
|
tag.el.style.zIndex = parseInt(scale * 100);
|
||||||
let left = tag.x + container.offsetWidth/2 - tag.el.offsetWidth/2;
|
let left = tag.x + container.offsetWidth / 2 - tag.el.offsetWidth / 2;
|
||||||
let top = tag.y + container.offsetHeight/2 - tag.el.offsetHeight/2;
|
let top = tag.y + container.offsetHeight / 2 - tag.el.offsetHeight / 2;
|
||||||
tag.el.style.transform = `translate(${left}px, ${top}px) scale(${scale})`;
|
tag.el.style.transform = `translate(${left}px, ${top}px) scale(${scale})`;
|
||||||
});
|
});
|
||||||
requestAnimationFrame(update);
|
requestAnimationFrame(update);
|
||||||
@@ -539,4 +614,112 @@ class UIManager {
|
|||||||
update();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +89,11 @@ const SiteConfig = {
|
|||||||
{ name: 'Postgresql', category: 'data', weight: 1 }
|
{ 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不可用时使用)
|
// 默认数据(当API或RSS不可用时使用)
|
||||||
defaults: {
|
defaults: {
|
||||||
repos: [
|
repos: [
|
||||||
|
|||||||
Reference in New Issue
Block a user