Files
home/js/about.js
hehh 532cbafd8e feat(audio): 重构音频播放功能以支持iframe隔离播放
- 将音频播放器移至独立的iframe中以提升性能和隔离性
- 实现主页面与iframe之间的postMessage通信机制
- 添加音频播放状态同步和UI更新逻辑
- 支持自动播放控制和用户交互检测
- 实现播放状态持久化存储和恢复功能
- 优化移动端音频控制体验
- 添加版权信息更新和国际化支持
2025-11-30 16:42:00 +08:00

1284 lines
53 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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-v2';
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 ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'})[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 += `
<div class="repo-card" onclick="window.open('${url}', '_blank', 'noopener,noreferrer')">
<div class="repo-head">
<span class="gradient-text">${escapeHtml(repo.name)}</span>
<span>
<i class="ri-star-fill"></i> ${stars}
<i class="ri-git-branch-fill"></i> ${forks}
</span>
</div>
<div class="repo-desc">${escapeHtml(dShort)}</div>
</div>`;
});
// 使用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 += `
<div class="blog-item" onclick="window.open('${link}', '_blank', 'noopener,noreferrer')">
<div class="b-info">
<div class="b-title">${escapeHtml(post.title)}</div>
<div class="b-date">${escapeHtml(date)}</div>
</div>
<div class="b-cat">${escapeHtml(cat)}</div>
</div>`;
});
// 使用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.userInteracted = false;
this.audioPlayer = null;
this.audioIframe = null;
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(`<div style="text-align:center;color:#999;padding:20px;">${msg}</div>`);
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(`<div style="text-align:center;color:#999;padding:20px;">${msg}</div>`);
}
} else {
const msg = isZh ? '当前评论区已关闭' : 'Comments are closed';
$('#artalk-container').html(`<div style="text-align:center;color:#999;padding:20px;">${msg}</div>`);
}
}
// 重新加载 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';
// 音频播放状态需要通过iframe通信获取
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
// 请求当前播放状态
audioIframe.contentWindow.postMessage({
action: 'getCurrentState'
}, '*');
}
});
};
// 监听来自iframe的音频状态更新
window.addEventListener('message', (event) => {
const audioIframe = document.getElementById('audio-player-iframe');
if (!audioIframe || event.source !== audioIframe.contentWindow) return;
const data = event.data;
if (data.action === 'currentState') {
const fMusic = document.getElementById('fab-music');
if (fMusic) {
const lang = getStoredLanguage();
fMusic.querySelector('.fab-text').textContent =
lang === 'zh' ? (data.playing ? '暂停' : '播放') : (data.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', () => {
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
// 直接发送播放指令让iframe内部处理播放/暂停切换
audioIframe.contentWindow.postMessage({
action: 'play'
}, '*');
// 记录用户操作
this.setMusicPauseTime(); // 先记录暂停时间
}
// 延迟更新标签以避免阻塞
requestAnimationFrame(updateLabels);
});
// 延迟初始标签更新以避免阻塞
requestAnimationFrame(updateLabels);
}
initAudio() {
// 获取已存在的iframe或创建新的
let audioIframe = document.getElementById('audio-player-iframe');
if (!audioIframe) {
audioIframe = document.createElement('iframe');
audioIframe.src = 'audio-player.html';
audioIframe.style.display = 'none';
audioIframe.id = 'audio-player-iframe';
document.body.appendChild(audioIframe);
}
this.audioIframe = audioIframe;
const autoPlayer = () => {
// 检查是否在24小时内用户暂停过音乐
const shouldRemainPaused = this.shouldMusicRemainPaused();
// 如果不应该保持暂停状态,则尝试播放
if (!shouldRemainPaused) {
// 添加用户交互检查,避免浏览器阻止自动播放
const attemptAutoplay = () => {
// 检查是否已有用户交互
if (this.userInteracted) {
this.playAudio();
} else {
// 添加一次性用户交互监听器
const enableAudio = () => {
this.userInteracted = true;
this.playAudio();
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 });
}
};
setTimeout(attemptAutoplay, 500); // 稍微延迟以确保iframe加载完成
}
}
// 监听iframe发来的消息
const handleMessage = (event) => {
// 确保消息来自我们的iframe
if (event.source !== this.audioIframe.contentWindow) return;
const data = event.data;
switch (data.action) {
case 'playerReady':
// iframe准备就绪设置初始音频
this.setAudioTrack('data/至少做一件离谱的事-Kiri T_compressed.mp3');
autoPlayer();
break;
case 'stateChange':
// 音频状态改变更新UI
this.updateAudioUI(data.playing);
break;
case 'trackEnded':
// 曲目结束
this.updateAudioUI(false);
break;
case 'currentState':
// 当前状态响应
this.updateAudioUI(data.playing);
if (!data.playing) {
autoPlayer();
}
break;
}
};
window.addEventListener('message', handleMessage);
}
// 播放音频
playAudio() {
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
audioIframe.contentWindow.postMessage({
action: 'play'
}, '*');
}
}
// 暂停音频
pauseAudio() {
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
audioIframe.contentWindow.postMessage({
action: 'pause'
}, '*');
}
}
// 设置音频曲目
setAudioTrack(src) {
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
audioIframe.contentWindow.postMessage({
action: 'setTrack',
src: src
}, '*');
}
}
// 更新音频UI状态
updateAudioUI(playing) {
// 更新移动端fab按钮的文本
const fMusic = document.getElementById('fab-music');
if (fMusic) {
const lang = getStoredLanguage();
fMusic.querySelector('.fab-text').textContent =
lang === 'zh' ? (playing ? '暂停' : '播放') : (playing ? 'Pause' : 'Play');
}
}
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'}`);
});
}
}