Skip to content

Commit b769ffe

Browse files
committed
feat(主题): 添加主题切换功能并优化导航栏样式
在布局文件中添加主题切换逻辑,支持根据用户偏好或系统设置自动应用主题。新增主题切换组件,优化导航栏样式,支持动态高亮当前页面链接。
1 parent d230d80 commit b769ffe

4 files changed

Lines changed: 131 additions & 36 deletions

File tree

blog-web/src/components/Header.astro

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
---
22
import ScrollHeader from "../components/solid/ScrollHeader";
3+
import ThemeToggle from "../components/solid/ThemeToggle";
4+
5+
// Get current path to highlight active navigation item
6+
const currentPath = Astro.url.pathname;
7+
const isHomePage = currentPath === "/";
8+
const isBlogPage = currentPath.startsWith("/blog");
9+
const isAboutPage = currentPath.startsWith("/about");
10+
const isProjectsPage = currentPath.startsWith("/projects");
11+
12+
// Helper function to determine tab classes
13+
const getTabClass = (isActive: boolean) => {
14+
return isActive ? "tab tab-active" : "tab";
15+
};
316
---
417

518
<ScrollHeader client:load>
@@ -25,48 +38,26 @@ import ScrollHeader from "../components/solid/ScrollHeader";
2538
tabindex="0"
2639
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
2740
>
28-
<li><a href="/">首页</a></li>
29-
<li><a href="/blog">博客</a></li>
30-
<li><a href="/about">关于我</a></li>
31-
<li><a href="/projects">项目</a></li>
41+
<li><a href="/" class={isHomePage ? "font-bold text-primary" : ""}>首页</a></li>
42+
<li><a href="/blog" class={isBlogPage ? "font-bold text-primary" : ""}>博客</a></li>
43+
<li><a href="/about" class={isAboutPage ? "font-bold text-primary" : ""}>关于我</a></li>
44+
<li><a href="/projects" class={isProjectsPage ? "font-bold text-primary" : ""}>项目</a></li>
3245
</ul>
3346
</div>
3447
<a href="/" class="btn btn-ghost text-xl">
3548
<span class="font-bold transition-all">ExquisiteCore</span>
3649
</a>
3750
</div>
3851
<div class="navbar-center hidden lg:flex">
39-
<ul class="menu menu-horizontal px-1">
40-
<li><a href="/">首页</a></li>
41-
<li><a href="/blog">博客</a></li>
42-
<li><a href="/about">关于我</a></li>
43-
<li><a href="/projects">项目</a></li>
44-
</ul>
52+
<div role="tablist" class="tabs tabs-bordered">
53+
<a href="/" role="tab" class={getTabClass(isHomePage)}>首页</a>
54+
<a href="/blog" role="tab" class={getTabClass(isBlogPage)}>博客</a>
55+
<a href="/about" role="tab" class={getTabClass(isAboutPage)}>关于我</a>
56+
<a href="/projects" role="tab" class={getTabClass(isProjectsPage)}>项目</a>
57+
</div>
4558
</div>
4659
<div class="navbar-end">
47-
<label class="swap swap-rotate btn btn-ghost btn-circle">
48-
<input type="checkbox" class="theme-controller" value="dark" />
49-
<!-- Sun icon -->
50-
<svg
51-
class="swap-on fill-current w-5 h-5"
52-
xmlns="http://www.w3.org/2000/svg"
53-
viewBox="0 0 24 24"
54-
>
55-
<path
56-
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"
57-
></path>
58-
</svg>
59-
<!-- Moon icon -->
60-
<svg
61-
class="swap-off fill-current w-5 h-5"
62-
xmlns="http://www.w3.org/2000/svg"
63-
viewBox="0 0 24 24"
64-
>
65-
<path
66-
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"
67-
></path>
68-
</svg>
69-
</label>
60+
<ThemeToggle client:load />
7061
<button class="btn btn-primary ml-2">登录</button>
7162
</div>
7263
</div>

blog-web/src/components/solid/ScrollHeader.jsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { createSignal, createEffect, onCleanup, onMount } from "solid-js";
1+
import { createSignal, createEffect, onCleanup, onMount, createMemo } from "solid-js";
22

33
export default function ScrollHeader(props) {
44
const [isVisible, setIsVisible] = createSignal(true);
55
const [lastScrollY, setLastScrollY] = createSignal(0);
66
const [isAtTop, setIsAtTop] = createSignal(true);
7+
const [theme, setTheme] = createSignal('light');
78

89
// Handle scroll events with debounce
910
const handleScroll = () => {
@@ -50,11 +51,26 @@ export default function ScrollHeader(props) {
5051
}
5152
};
5253

54+
// Listen for theme changes in localStorage
55+
const handleStorageChange = (e) => {
56+
if (typeof localStorage !== 'undefined' && e.key === 'theme') {
57+
setTheme(e.newValue || 'light');
58+
}
59+
};
60+
5361
onMount(() => {
5462
// Set initial scroll position
5563
setLastScrollY(window.scrollY);
5664
// Initialize header state
5765
handleScroll();
66+
67+
// Set initial theme from localStorage (only available in browser)
68+
if (typeof localStorage !== 'undefined') {
69+
setTheme(localStorage.getItem('theme') || 'light');
70+
}
71+
72+
// Listen for theme changes from other components
73+
window.addEventListener('storage', handleStorageChange);
5874
});
5975

6076
createEffect(() => {
@@ -64,15 +80,23 @@ export default function ScrollHeader(props) {
6480
onCleanup(() => {
6581
// Remove scroll event listener
6682
window.removeEventListener("scroll", onScroll);
83+
window.removeEventListener('storage', handleStorageChange);
6784
});
6885
});
6986

87+
// Theme-based background color class
88+
const bgColorClass = createMemo(() => {
89+
return theme() === 'dark'
90+
? 'bg-base-100/80 backdrop-blur-sm shadow-md'
91+
: 'bg-base-100/95 backdrop-blur-sm shadow-md';
92+
});
93+
7094
return (
7195
<header
7296
class={`fixed top-0 w-full z-50 transition-all duration-300 transform ${
7397
isVisible() ? "translate-y-0" : "-translate-y-full"
7498
} ${
75-
!isAtTop() ? "bg-base-100/95 backdrop-blur-sm shadow-md" : "bg-base-100"
99+
!isAtTop() ? bgColorClass() : "bg-base-100"
76100
}`}
77101
>
78102
{props.children}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { createSignal, onMount } from "solid-js";
2+
3+
export default function ThemeToggle() {
4+
const [isDarkMode, setIsDarkMode] = createSignal(false);
5+
6+
// Initialize theme based on localStorage or system preference
7+
onMount(() => {
8+
if (typeof localStorage !== 'undefined') {
9+
const savedTheme = localStorage.getItem('theme');
10+
if (savedTheme === 'dark') {
11+
setIsDarkMode(true);
12+
} else if (savedTheme === 'light') {
13+
setIsDarkMode(false);
14+
} else {
15+
// Check system preference if no saved preference
16+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
17+
setIsDarkMode(prefersDark);
18+
localStorage.setItem('theme', prefersDark ? 'dark' : 'light');
19+
}
20+
}
21+
});
22+
23+
// Handle theme toggle
24+
const toggleTheme = () => {
25+
const newTheme = !isDarkMode();
26+
setIsDarkMode(newTheme);
27+
28+
// Update theme in localStorage and document
29+
const themeName = newTheme ? 'dark' : 'light';
30+
if (typeof localStorage !== 'undefined') {
31+
localStorage.setItem('theme', themeName);
32+
}
33+
document.documentElement.setAttribute('data-theme', themeName);
34+
};
35+
36+
return (
37+
<label class="swap swap-rotate btn btn-ghost btn-circle">
38+
<input
39+
type="checkbox"
40+
class="theme-controller"
41+
checked={isDarkMode()}
42+
onChange={toggleTheme}
43+
/>
44+
{/* Sun icon */}
45+
<svg
46+
class="swap-on fill-current w-5 h-5"
47+
xmlns="http://www.w3.org/2000/svg"
48+
viewBox="0 0 24 24"
49+
>
50+
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
51+
</svg>
52+
{/* Moon icon */}
53+
<svg
54+
class="swap-off fill-current w-5 h-5"
55+
xmlns="http://www.w3.org/2000/svg"
56+
viewBox="0 0 24 24"
57+
>
58+
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
59+
</svg>
60+
</label>
61+
);
62+
}

blog-web/src/layouts/Layout.astro

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,31 @@ const { title = "My Blog" } = Astro.props;
1212
---
1313

1414
<!doctype html>
15-
<html lang="zh-CN" data-theme="light">
15+
<html lang="zh-CN">
1616
<head>
1717
<meta charset="UTF-8" />
1818
<meta name="viewport" content="width=device-width" />
1919
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
2020
<meta name="generator" content={Astro.generator} />
2121
<title>{title}</title>
22+
<script is:inline>
23+
// Apply saved theme immediately to prevent flash
24+
try {
25+
const savedTheme = localStorage.getItem('theme');
26+
if (savedTheme) {
27+
document.documentElement.setAttribute('data-theme', savedTheme);
28+
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
29+
document.documentElement.setAttribute('data-theme', 'dark');
30+
localStorage.setItem('theme', 'dark');
31+
} else {
32+
document.documentElement.setAttribute('data-theme', 'light');
33+
localStorage.setItem('theme', 'light');
34+
}
35+
} catch (e) {
36+
// Fallback for environments where localStorage is not available
37+
document.documentElement.setAttribute('data-theme', 'light');
38+
}
39+
</script>
2240
</head>
2341
<body class="flex flex-col min-h-screen">
2442
<img

0 commit comments

Comments
 (0)