feat(about): 增强安全性和可访问性

- 移除调试用的 console.log 输出
- 添加 escapeHtml 函数防止 XSS 攻击
- 使用 getStoredLanguage 统一语言获取逻辑
- 为外部链接添加 noopener 和 noreferrer 属性
- 在动态内容插入时使用 escapeHtml 进行转义
- 更新评论区关闭提示的多语言支持
- 为菜单按钮添加 aria-expanded 属性提升可访问性
- 优化拖拽功能的边界计算逻辑
- 设置 will-change 提升拖拽性能
This commit is contained in:
hehh
2025-11-24 02:00:15 +08:00
parent ab883a9cc0
commit d344b1a844

View File

@@ -17,7 +17,6 @@ function setStoredTheme(theme) {
localStorage.setItem(cacheKey, JSON.stringify({ localStorage.setItem(cacheKey, JSON.stringify({
value: theme, time: new Date().getTime() value: theme, time: new Date().getTime()
})); }));
console.log("已保存主题设置:", theme);
} }
// 公共方法:获取本地存储的主题设置 // 公共方法:获取本地存储的主题设置
@@ -33,7 +32,6 @@ function getStoredTheme() {
const night = hour >= 18 || prefersDark; const night = hour >= 18 || prefersDark;
theme = night ? 'night' : 'day'; theme = night ? 'night' : 'day';
setStoredTheme(theme) setStoredTheme(theme)
console.log("已初始化主题设置:", theme);
} else if (saved.value) { } else if (saved.value) {
theme = saved.value; theme = saved.value;
} }
@@ -41,10 +39,16 @@ function getStoredTheme() {
} }
$(document).ready(function () { $(document).ready(function () {
// 启动应用核心
const app = new AppCore(); 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 { class AppCore {
constructor() { constructor() {
this.i18n = new I18nManager(); this.i18n = new I18nManager();
@@ -59,7 +63,7 @@ class AppCore {
=========================== */ =========================== */
class I18nManager { class I18nManager {
constructor() { constructor() {
this.lang = localStorage.getItem('lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en'); this.lang = getStoredLanguage();
this.dict = { this.dict = {
zh: { zh: {
"nav.home": "首页", "nav.home": "首页",
@@ -282,20 +286,20 @@ class DataManager {
// Fix: API field compatibility // Fix: API field compatibility
const stars = repo.stargazers_count !== undefined ? repo.stargazers_count : (repo.stars || 0); 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 forks = repo.forks_count !== undefined ? repo.forks_count : (repo.forks || 0);
const desc = repo.description || repo.desc || 'No description.'; const descRaw = repo.description || repo.desc || 'No description.';
const url = repo.html_url || repo.url || '#'; const url = repo.html_url || repo.url || '#';
const dShort = (desc || '').length > 120 ? (desc.slice(0, 117) + '...') : desc; const dShort = (descRaw || '').length > 120 ? (descRaw.slice(0, 117) + '...') : descRaw;
html += ` html += `
<div class="repo-card" onclick="window.open('${url}')"> <div class="repo-card" onclick="window.open('${url}', '_blank', 'noopener,noreferrer')">
<div class="repo-head"> <div class="repo-head">
<span class="gradient-text">${repo.name}</span> <span class="gradient-text">${escapeHtml(repo.name)}</span>
<span> <span>
<i class="ri-star-fill"></i> ${stars} <i class="ri-star-fill"></i> ${stars}
<i class="ri-git-branch-fill"></i> ${forks} <i class="ri-git-branch-fill"></i> ${forks}
</span> </span>
</div> </div>
<div class="repo-desc">${dShort}</div> <div class="repo-desc">${escapeHtml(dShort)}</div>
</div>`; </div>`;
}); });
$('#projects-container').html(html); $('#projects-container').html(html);
@@ -371,7 +375,7 @@ class DataManager {
renderBlog(list) { renderBlog(list) {
let html = ''; let html = '';
const lang = localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en'); const lang = getStoredLanguage();
const fmtDate = (dStr) => { const fmtDate = (dStr) => {
if (!dStr) return ''; if (!dStr) return '';
const d = new Date(dStr); const d = new Date(dStr);
@@ -398,12 +402,12 @@ class DataManager {
const link = post.link || post.url || '#'; const link = post.link || post.url || '#';
html += ` html += `
<div class="blog-item" onclick="window.open('${link}')"> <div class="blog-item" onclick="window.open('${link}', '_blank', 'noopener,noreferrer')">
<div class="b-info"> <div class="b-info">
<div class="b-title">${post.title}</div> <div class="b-title">${escapeHtml(post.title)}</div>
<div class="b-date">${date}</div> <div class="b-date">${escapeHtml(date)}</div>
</div> </div>
<div class="b-cat">${cat}</div> <div class="b-cat">${escapeHtml(cat)}</div>
</div>`; </div>`;
}); });
$('#blog-container').html(html); $('#blog-container').html(html);
@@ -459,11 +463,12 @@ class UIManager {
} }
initArtalk() { initArtalk() {
// Safe initialization
const isHttps = location.protocol === 'https:'; const isHttps = location.protocol === 'https:';
const isLocal = !!(window.SiteConfig?.dev?.isLocal); const isLocal = !!(window.SiteConfig?.dev?.isLocal);
if (!isHttps || 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>`); const lang = getStoredLanguage();
const msg = lang === 'zh' ? '当前评论区已关闭' : 'Comments are closed';
$('#artalk-container').html(`<div style="text-align:center;color:#999;padding:20px;">${msg}</div>`);
return; return;
} }
if (typeof Artalk !== 'undefined' && window.SiteConfig?.artalk) { if (typeof Artalk !== 'undefined' && window.SiteConfig?.artalk) {
@@ -478,10 +483,14 @@ class UIManager {
}); });
} catch (e) { } catch (e) {
console.error("Artalk Error", 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>`); const lang = getStoredLanguage();
const msg = lang === 'zh' ? '当前评论区已关闭' : 'Comments are closed';
$('#artalk-container').html(`<div style="text-align:center;color:#999;padding:20px;">${msg}</div>`);
} }
} else { } else {
$('#artalk-container').html(`<div style="text-align:center;color:#999;padding:20px;">${(new I18nManager()).dict[(localStorage.getItem('lang') || 'zh')]['comment.closed']}</div>`); const lang = getStoredLanguage();
const msg = lang === 'zh' ? '当前评论区已关闭' : 'Comments are closed';
$('#artalk-container').html(`<div style="text-align:center;color:#999;padding:20px;">${msg}</div>`);
} }
} }
@@ -633,6 +642,7 @@ class UIManager {
}; };
main.addEventListener('click', () => { main.addEventListener('click', () => {
menu.classList.toggle('open'); menu.classList.toggle('open');
main.setAttribute('aria-expanded', menu.classList.contains('open') ? 'true' : 'false');
updateLabels(); updateLabels();
}); });
fLang.addEventListener('click', () => { fLang.addEventListener('click', () => {
@@ -668,10 +678,7 @@ class UIManager {
let isDragging = false; let isDragging = false;
let initialX, initialY, currentX, currentY, xOffset = 0, yOffset = 0; let initialX, initialY, currentX, currentY, xOffset = 0, yOffset = 0;
const windowWidth = window.innerWidth; fab.style.willChange = 'transform';
const windowHeight = window.innerHeight;
const fabWidth = fab.offsetWidth;
const fabHeight = fab.offsetHeight;
// 拖拽相关方法 // 拖拽相关方法
const setTranslate = (xPos, yPos, el) => { const setTranslate = (xPos, yPos, el) => {
@@ -706,9 +713,13 @@ class UIManager {
currentY = e.clientY - initialY; currentY = e.clientY - initialY;
} }
// 限制在屏幕内 const ww = window.innerWidth;
currentX = Math.max(0, Math.min(currentX, windowWidth - fabWidth)); const wh = window.innerHeight;
currentY = Math.max(0, Math.min(currentY, windowHeight - fabHeight)); const rect = fab.getBoundingClientRect();
const fw = rect.width;
const fh = rect.height;
currentX = Math.max(0, Math.min(currentX, ww - fw));
currentY = Math.max(0, Math.min(currentY, wh - fh));
xOffset = currentX; xOffset = currentX;
yOffset = currentY; yOffset = currentY;