-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
293 lines (256 loc) · 11.1 KB
/
Copy pathscript.js
File metadata and controls
293 lines (256 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/* ===== 中英双语词典 ===== */
const i18n = {
"hero.kicker": { zh: "学生 / 开发者", en: "Student / Developer" },
"hero.subtitle": { zh: "不会<span class=\"hl\">Coding</span>的<span class=\"hl\">Coder</span>, 不懂<span class=\"hl\">OI</span>的<span class=\"hl\">OIer</span>.",
en: "A <span class=\"hl\">Coder</span> who can't <span class=\"hl\">Code</span>, an <span class=\"hl\">OIer</span> who doesn't know <span class=\"hl\">OI</span>." },
"about.kicker": { zh: "关于", en: "About" },
"about.title": { zh: "关于我", en: "About Me" },
"about.body": { zh: "你好!我是 CodeSeed,一名热爱技术的学生。我享受把脑中的想法用代码变成现实的过程,目前正在不断学习、探索新的技术,希望有一天能写出解决真实世界问题的程序。",
en: "Hi! I'm CodeSeed, a student who loves technology. I enjoy turning ideas into real things with code. I'm always learning and exploring — hoping to one day build software that solves real-world problems." },
"projects.kicker": { zh: "项目", en: "Work" },
"projects.title": { zh: "项目", en: "Projects" },
"projects.p1.title": { zh: "sbackup", en: "sbackup" },
"projects.p1.desc": { zh: "轻量、高效的文件夹备份工具,支持命令行操作,帮助你轻松管理备份策略。",
en: "A lightweight, efficient folder backup tool with a command-line interface, helping you easily manage your backup strategy." },
"projects.p2.title": { zh: "slicense", en: "slicense" },
"projects.p2.desc": { zh: "一个功能全面的项目许可证管理 CLI 工具,支持多语言生态(Rust/Cargo、Node.js/npm、Go、Python/pip)。",
en: "A full-featured project license management CLI tool, supporting multiple language ecosystems (Rust/Cargo, Node.js/npm, Go, Python/pip)." },
"projects.view": { zh: "查看项目 →", en: "View project →" },
"contact.kicker": { zh: "联系", en: "Contact" },
"contact.title": { zh: "联系我", en: "Contact" },
"contact.github": { zh: "GitHub", en: "GitHub" },
"contact.email": { zh: "邮箱", en: "Email" },
"footer.related": { zh: "相关链接", en: "Links" },
};
/* ===== 语言状态 ===== */
let currentLang = localStorage.getItem("lang") || "zh";
/* ===== 应用翻译(独立抽出:首帧前同步执行一次 + 切换时复用) ===== */
function applyTranslations(lang) {
document.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.getAttribute("data-i18n");
const entry = i18n[key];
if (entry && entry[lang]) {
if (el.hasAttribute("data-i18n-html")) {
el.innerHTML = entry[lang];
} else {
el.textContent = entry[lang];
}
}
});
}
// script.js 阻塞加载在 body 末尾,执行时 DOM 已完整且早于首次绘制,
// 这里同步应用一次已保存语言,避免刷新后先显示默认语言再切换的闪烁
applyTranslations(currentLang);
/* ===== 切换语言 ===== */
function setLang(lang) {
currentLang = lang;
localStorage.setItem("lang", lang);
applyTranslations(lang);
// 更新切换按钮文字
const langBtn = document.getElementById("lang-toggle");
if (langBtn) {
langBtn.textContent = lang === "zh" ? "EN" : "中";
langBtn.setAttribute("aria-label", lang === "zh" ? "Switch to English" : "切换到中文");
}
// 更新 HTML lang
document.documentElement.lang = lang;
// 更新页面标题与描述
document.title = "CodeSeed";
const metaDesc = document.querySelector('meta[name="description"]');
if (metaDesc) {
metaDesc.content = lang === "zh"
? "CodeSeed 的个人主页 —— 学生 / 开发者"
: "CodeSeed's personal site — student & developer";
}
// 同步主题按钮文字
const theme = document.documentElement.getAttribute("data-theme") || "light";
const themeBtn = document.getElementById("theme-toggle");
if (themeBtn) themeBtn.textContent = themeBtnLabel(theme);
// 同步移动端导航链接文案(nav 不带 data-i18n,从词典取值,与页面文案同源)
const navMap = {
"about": i18n["about.kicker"][lang],
"projects": i18n["projects.kicker"][lang],
"contact": i18n["contact.kicker"][lang],
};
document.querySelectorAll("[data-nav]").forEach((el) => {
const key = navMap[el.getAttribute("data-nav")];
if (key) el.textContent = key;
});
}
/* ===== 主题状态 ===== */
const THEME_KEY = "theme";
function getInitialTheme() {
const saved = localStorage.getItem(THEME_KEY);
if (saved === "dark" || saved === "light") return saved;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
/* 主题按钮文字(明/暗 × 中/英 四种取值集中一处,避免 setLang/setTheme 各写一遍) */
function themeBtnLabel(theme) {
const dark = theme === "dark";
return dark ? (currentLang === "zh" ? "明" : "light") : (currentLang === "zh" ? "暗" : "dark");
}
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem(THEME_KEY, theme);
const btn = document.getElementById("theme-toggle");
if (btn) {
const dark = theme === "dark";
btn.textContent = themeBtnLabel(theme);
btn.setAttribute("aria-label", dark ? "Switch to light mode" : "Switch to dark mode");
btn.title = dark ? "Light" : "Dark";
}
}
/* ===== 复制邮箱 ===== */
function setupCopyEmail() {
document.querySelectorAll("[data-copy-email]").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
const email = el.getAttribute("data-copy-email");
if (email && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(email).then(
() => showToast(currentLang === "zh" ? "已复制" : "Copied"),
() => selectEmail(el)
);
} else {
selectEmail(el);
}
});
});
}
/* 降级:clipboard 不可用时全选文本,并提示用户手动复制 */
function selectEmail(el) {
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(el);
sel.removeAllRanges();
sel.addRange(range);
showToast(currentLang === "zh" ? "已选中,请手动复制" : "Selected — copy manually");
}
function showToast(text) {
document.querySelectorAll(".copy-toast").forEach((n) => n.remove());
const tip = document.createElement("span");
tip.className = "copy-toast";
tip.textContent = text;
tip.setAttribute("role", "status");
tip.setAttribute("aria-live", "polite");
document.body.appendChild(tip);
setTimeout(() => tip.remove(), 1500);
}
/* ===== 平滑滚动 ===== */
/* (closeMenu 仅供 setupNav 内部使用,其内部已有定义,此前的模块级副本是死代码,已删除) */
function smoothScrollTo(target) {
let targetY;
if (typeof target === "number") {
targetY = target;
} else {
const id = (target || "").replace(/^#/, "");
const el = document.getElementById(id);
if (!el) return;
targetY = el.getBoundingClientRect().top + window.scrollY - 84;
}
window.scrollTo({ top: targetY, behavior: "smooth" });
}
function setupNav() {
const navbar = document.getElementById("navbar");
const links = document.querySelectorAll(".nav-link");
const hamburger = document.getElementById("hamburger");
const navLinks = document.getElementById("nav-links");
let lastY = 0;
// 回到顶部按钮
const backToTop = document.getElementById("back-to-top");
function onScroll() {
const y = window.scrollY;
navbar.classList.toggle("scrolled", y > 24);
navbar.classList.toggle("hidden", y > 200 && y > lastY + 2);
if (y < 80) navbar.classList.remove("hidden");
if (backToTop) backToTop.classList.toggle("visible", y > 480);
lastY = y;
}
if (backToTop) {
backToTop.addEventListener("click", () => smoothScrollTo(0));
}
// 当前 section 高亮
function observeSections() {
const ids = ["about", "projects", "contact"];
const observer = new IntersectionObserver((entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
if (visible.length > 0) {
setActive(visible[0].target.id);
}
}, { rootMargin: "-40% 0px -50% 0px", threshold: [0, .2, .5] });
ids.forEach((id) => {
const el = document.getElementById(id);
if (el) observer.observe(el);
});
}
function setActive(id) {
links.forEach((l) => {
const active = l.getAttribute("data-nav") === id;
l.classList.toggle("active", active);
if (active) l.setAttribute("aria-current", "true");
else l.removeAttribute("aria-current");
});
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
observeSections();
// 点击导航链接:平滑滚动 + 关闭移动端菜单
links.forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
smoothScrollTo(link.getAttribute("href"));
closeMenu();
});
});
// Logo 点击平滑回到顶部
const logo = document.querySelector(".nav-logo");
if (logo) {
logo.addEventListener("click", (e) => {
e.preventDefault();
smoothScrollTo(0);
});
}
// 底部滚动提示箭头
const scrollHint = document.querySelector(".scroll-hint");
if (scrollHint) {
scrollHint.addEventListener("click", (e) => {
e.preventDefault();
smoothScrollTo(scrollHint.getAttribute("href"));
});
}
// 汉堡菜单
function toggleMenu() {
const open = navLinks.classList.toggle("open");
hamburger.classList.toggle("open", open);
hamburger.setAttribute("aria-expanded", String(open));
hamburger.setAttribute("aria-label", open ? (currentLang === "zh" ? "关闭菜单" : "Close menu") : (currentLang === "zh" ? "打开菜单" : "Open menu"));
}
function closeMenu() {
navLinks.classList.remove("open");
hamburger.classList.remove("open");
hamburger.setAttribute("aria-expanded", "false");
}
hamburger.addEventListener("click", toggleMenu);
}
/* ===== 初始化 ===== */
document.addEventListener("DOMContentLoaded", () => {
// 防止浏览器"记住上次滚动位置",刷新时强制回到顶部
if ("scrollRestoration" in history) {
history.scrollRestoration = "manual";
}
setTheme(getInitialTheme());
setLang(currentLang);
document.getElementById("theme-toggle").addEventListener("click", () => {
const next = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
setTheme(next);
});
document.getElementById("lang-toggle").addEventListener("click", () => {
setLang(currentLang === "zh" ? "en" : "zh");
});
// 自动更新页脚年份
document.getElementById("year").textContent = new Date().getFullYear();
setupCopyEmail();
setupNav();
});