master
1---
2import { t } from '@utils/i18n';
3import { Icon } from 'astro-icon/components';
4import type { HTMLAttributes } from 'astro/types';
5import Button from './Button.astro';
6
7interface Props extends Omit<HTMLAttributes<'button'>, 'onclick'> {
8 showText?: boolean;
9 useDefaultBtnClass?: boolean;
10}
11
12const { class: className, showText, useDefaultBtnClass, ...rest } = Astro.props;
13---
14
15<Button
16 class:list={['darkmode-btn swap swap-rotate', className]}
17 useDefaultClass={useDefaultBtnClass}
18 {...rest}
19 data-text-light={t.button.themeToggle.lightMode()}
20 data-text-dark={t.button.themeToggle.darkMode()}
21 data-text-auto={t.button.themeToggle.systemMode()}
22 role="switch"
23 aria-label={t.button.themeToggle.title()}
24>
25 <input type="checkbox" inert />
26 <Icon
27 class="darkmode-icon-light swap-off"
28 name="material-symbols:light-mode-rounded"
29 role="presentation"
30 aria-hidden
31 />
32 <Icon
33 class="darkmode-icon-dark swap-on"
34 name="material-symbols:dark-mode-rounded"
35 role="presentation"
36 aria-hidden
37 />
38 <Icon
39 class="darkmode-icon-auto swap-indeterminate"
40 name="material-symbols:night-sight-auto-rounded"
41 role="presentation"
42 aria-hidden
43 />
44 <span class={showText ? 'darkmode-text pl-6' : 'sr-only'}></span>
45</Button>
46
47<script>
48 import { buildConfig } from '@/config';
49
50 function setup() {
51 const darkmodeBtns = document.querySelectorAll('button.darkmode-btn');
52
53 // 获取当前主题模式
54 function getCurrentMode(): 'system' | 'light' | 'dark' {
55 if (!('darkMode' in localStorage)) {
56 return 'system';
57 }
58 return localStorage.darkMode === 'true' ? 'dark' : 'light';
59 }
60
61 // 获取下一个主题模式
62 function getNextMode(
63 currentMode: 'system' | 'light' | 'dark'
64 ): 'system' | 'light' | 'dark' {
65 switch (currentMode) {
66 case 'system':
67 return 'light';
68 case 'light':
69 return 'dark';
70 case 'dark':
71 return 'system';
72 }
73 }
74
75 darkmodeBtns.forEach((btn) => {
76 const checkbox = btn.querySelector('input[type="checkbox"]') as HTMLInputElement;
77 const text = btn.querySelector('.darkmode-text') || btn.querySelector('.sr-only');
78
79 // 更新UI状态和文本
80 function updateUI(mode: 'system' | 'light' | 'dark') {
81 if (mode === 'system') {
82 checkbox.indeterminate = true;
83 } else {
84 checkbox.indeterminate = false;
85 }
86 checkbox.checked = mode === 'dark';
87
88 const textContent =
89 mode === 'system'
90 ? btn.getAttribute('data-text-auto')
91 : mode === 'dark'
92 ? btn.getAttribute('data-text-dark')
93 : btn.getAttribute('data-text-light');
94
95 btn.setAttribute('title', textContent || '');
96
97 if (text) {
98 text.textContent = textContent;
99 }
100 }
101
102 // 点击处理
103 btn.addEventListener('click', () => {
104 const currentMode = getCurrentMode();
105 const nextMode = getNextMode(currentMode);
106
107 // 更新 localStorage
108 if (nextMode === 'system') {
109 localStorage.removeItem('darkMode');
110 } else {
111 localStorage.darkMode = nextMode === 'dark';
112 }
113
114 // 触发主题变化事件
115 const isDark =
116 nextMode === 'system'
117 ? window.matchMedia('(prefers-color-scheme: dark)').matches
118 : nextMode === 'dark';
119
120 document.dispatchEvent(
121 new CustomEvent('blog:darkmode-change', {
122 detail: { isDark, nextMode },
123 })
124 );
125 });
126
127 document.addEventListener('blog:darkmode-change', (e) => {
128 // @ts-expect-error CustomEvent.detail is not defined in TypeScript
129 const { nextMode } = e.detail;
130 updateUI(nextMode);
131 });
132
133 // 初始化 UI
134 updateUI(getCurrentMode());
135 });
136
137 document.addEventListener('blog:darkmode-change', (e) => {
138 // @ts-expect-error CustomEvent.detail is not defined in TypeScript
139 const { isDark } = e.detail;
140 document.documentElement.setAttribute(
141 'data-theme',
142 isDark ? buildConfig.themeNames.dark : buildConfig.themeNames.light
143 );
144 });
145 }
146
147 document.addEventListener('astro:page-load', setup);
148 setup();
149</script>