Skip to content

Commit d67f81f

Browse files
author
CMSZ
committed
feat(profile): 为个人简介添加动态一言显示效果
- 在 Profile 组件中集成 hitokoto API,实现打字机动画效果显示随机一言 - 优化导航栏搜索框交互逻辑,改进焦点和悬停状态处理 - 移除冗余的显示设置面板悬停控制代码,简化 DOM 事件绑定 - 添加打字机光标动画样式,增强视觉反馈 - 保留原有 bio 作为备用内容,确保 API 调用失败时的优雅降级
1 parent 40916e2 commit d67f81f

File tree

2 files changed

+77
-55
lines changed

2 files changed

+77
-55
lines changed

src/components/Navbar.astro

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ const themeIcons = [
4747
</div>
4848
<div class="flex">
4949
<!-- Search bar for desktop - instant display with Astro icons -->
50-
<div id="search-bar" class="hidden lg:flex transition-all items-center h-11 mr-2 rounded-lg
50+
<div id="search-bar" class="hidden lg:flex transition-colors items-center h-11 mr-2 rounded-lg
5151
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
5252
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
5353
">
5454
<Icon name="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30" />
5555
<input id="search-input-desktop" placeholder={i18n(I18nKey.search)}
56-
class="transition-all pl-10 text-sm bg-transparent outline-0
56+
class="transition-all pl-10 text-sm bg-transparent outline-none border-0 ring-0 focus:ring-0
5757
h-full w-40 text-black/50 dark:text-white/50"
5858
/>
5959
</div>
@@ -79,6 +79,7 @@ const themeIcons = [
7979
</div>
8080
))}
8181
</button>
82+
{/* @ts-ignore */}
8283
<LightDarkSwitch client:idle></LightDarkSwitch>
8384
</div>
8485
<button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" id="nav-menu-switch">
@@ -102,53 +103,7 @@ function loadButtonScript() {
102103
}
103104
};
104105

105-
if (settingBtn.dataset.hoverBound !== "1") {
106-
settingBtn.dataset.hoverBound = "1";
107-
108-
let closeTimer = 0;
109-
110-
const getPanel = () => document.getElementById("display-setting");
111-
const open = () => {
112-
const panel = getPanel();
113-
if (panel) panel.classList.remove("float-panel-closed");
114-
};
115-
const close = () => {
116-
const panel = getPanel();
117-
if (panel) panel.classList.add("float-panel-closed");
118-
};
119-
const cancelClose = () => {
120-
window.clearTimeout(closeTimer);
121-
};
122-
const scheduleClose = () => {
123-
window.clearTimeout(closeTimer);
124-
closeTimer = window.setTimeout(close, 120);
125-
};
126-
127-
const bindPanelHover = (panel) => {
128-
if (panel.dataset.hoverBound === "1") return;
129-
panel.dataset.hoverBound = "1";
130-
panel.addEventListener("mouseenter", cancelClose);
131-
panel.addEventListener("mouseleave", (e) => {
132-
const to = e.relatedTarget;
133-
if (to instanceof Node && settingBtn.contains(to)) return;
134-
scheduleClose();
135-
});
136-
};
137-
138-
settingBtn.addEventListener("mouseenter", () => {
139-
cancelClose();
140-
open();
141-
const panel = getPanel();
142-
if (panel) bindPanelHover(panel);
143-
});
144-
145-
settingBtn.addEventListener("mouseleave", (e) => {
146-
const panel = getPanel();
147-
const to = e.relatedTarget;
148-
if (panel && to instanceof Node && panel.contains(to)) return;
149-
scheduleClose();
150-
});
151-
}
106+
152107
}
153108

154109
let menuBtn = document.getElementById("nav-menu-switch");
@@ -167,19 +122,30 @@ loadButtonScript();
167122
function setupSearchBar() {
168123
const bar = document.getElementById("search-bar");
169124
const input = document.getElementById("search-input-desktop");
170-
if (!bar || !input) return;
125+
if (!bar || !input || !(input instanceof HTMLInputElement)) return;
126+
171127
const expand = () => input.classList.add("w-60");
172128
const collapse = () => input.classList.remove("w-60");
129+
173130
bar.addEventListener("mouseenter", () => {
174131
expand();
175132
input.focus();
176133
});
177134
bar.addEventListener("mouseleave", () => {
178-
input.blur();
179-
collapse();
135+
if (document.activeElement === input && input.value.trim() === "") {
136+
input.blur();
137+
} else if (document.activeElement !== input) {
138+
collapse();
139+
}
140+
});
141+
input.addEventListener("focus", () => {
142+
expand();
143+
});
144+
input.addEventListener("blur", () => {
145+
if (!bar.matches(':hover')) {
146+
collapse();
147+
}
180148
});
181-
input.addEventListener("focus", expand);
182-
// keep expanded while typing; collapse only on mouseleave
183149
}
184150

185151
setupSearchBar();

src/components/widget/Profile.astro

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const config = profileConfig;
2121
<div class="px-2">
2222
<div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div>
2323
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
24-
<div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div>
24+
<div class="text-center text-neutral-400 mb-2.5 transition" id="hitokoto"></div>
2525
<div class="flex flex-wrap gap-2 justify-center mb-1">
2626
{config.links.length > 1 && config.links.map(item =>
2727
<a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
@@ -110,3 +110,59 @@ const config = profileConfig;
110110
// 监听 Swup 内容替换事件,确保页面切换后重新获取数据
111111
document.addEventListener('content:replace', loadSiteStats);
112112
</script>
113+
114+
<script define:vars={{ config }}>
115+
/** @param {HTMLElement} element @param {string} text */
116+
function typewriter(element, text) {
117+
element.classList.add("typewriter-cursor");
118+
element.textContent = "";
119+
let index = 0;
120+
const speed = 150;
121+
function step() {
122+
if (index < text.length) {
123+
element.textContent += text.charAt(index);
124+
index += 1;
125+
window.setTimeout(step, speed);
126+
} else {
127+
element.classList.remove("typewriter-cursor");
128+
}
129+
}
130+
step();
131+
}
132+
133+
async function loadHitokoto() {
134+
const el = document.getElementById("hitokoto");
135+
if (!el) return;
136+
try {
137+
const response = await fetch("https://v1.hitokoto.cn/?c=k");
138+
const data = await response.json();
139+
const text = data?.hitokoto || "";
140+
if (!text) return;
141+
typewriter(el, text);
142+
} catch (error) {
143+
console.error("获取一言失败:", error);
144+
const fallback = config.bio;
145+
typewriter(el, fallback);
146+
}
147+
}
148+
149+
document.addEventListener("DOMContentLoaded", loadHitokoto);
150+
</script>
151+
152+
<style>
153+
:global(.typewriter-cursor)::after {
154+
content: "";
155+
width: 2px;
156+
height: 1.5em;
157+
vertical-align: text-bottom;
158+
display: inline-block;
159+
background-color: var(--primary);
160+
animation: cursor-blink 1.2s step-end infinite;
161+
margin-left: 4px;
162+
opacity: 0.8;
163+
}
164+
@keyframes cursor-blink {
165+
0%, 100% { opacity: 1; }
166+
50% { opacity: 0; }
167+
}
168+
</style>

0 commit comments

Comments
 (0)