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