diff --git a/packages/cra-template-typescript/template/src/App.tsx b/packages/cra-template-typescript/template/src/App.tsx
index a53698aab3c..775e7ab4c34 100644
--- a/packages/cra-template-typescript/template/src/App.tsx
+++ b/packages/cra-template-typescript/template/src/App.tsx
@@ -1,15 +1,20 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
+import React from 'react'
+import logo from './logo.svg'
+import './App.css'
+import Header from './components/Header'
function App() {
return (
- );
+ )
}
-export default App;
+export default App
diff --git a/packages/cra-template-typescript/template/src/components/Header.tsx b/packages/cra-template-typescript/template/src/components/Header.tsx
new file mode 100644
index 00000000000..7e6777e2af7
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/components/Header.tsx
@@ -0,0 +1,36 @@
+import React from 'react'
+import { useDarkMode } from '../hooks/useDarkMode'
+
+const Header: React.FC = () => {
+ const { isDark, toggle } = useDarkMode()
+
+ return (
+
+
+ {isDark ? '🌙 深色模式' : '☀️ 浅色模式'}
+
+
+ )
+}
+
+export default Header
diff --git a/packages/cra-template-typescript/template/src/hooks/uDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/uDarkMode.ts
new file mode 100644
index 00000000000..d3d844f6ade
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/hooks/uDarkMode.ts
@@ -0,0 +1,62 @@
+import { useState, useEffect, useCallback } from 'react'
+
+type Theme = 'light' | 'dark'
+
+const STORAGE_KEY = 'theme'
+
+function getInitialTheme(): Theme {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (stored === 'light' || stored === 'dark') {
+ return stored
+ }
+
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ return 'dark'
+ }
+
+ return 'light'
+}
+
+export function useDarkMode() {
+ const [theme, setThemeState] = useState(() => getInitialTheme())
+
+ useEffect(() => {
+ const root = document.documentElement
+ root.setAttribute('data-theme', theme)
+ localStorage.setItem(STORAGE_KEY, theme)
+ }, [theme])
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+
+ const handleChange = (e: MediaQueryListEvent) => {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (!stored) {
+ setThemeState(e.matches ? 'dark' : 'light')
+ }
+ }
+
+ mediaQuery.addEventListener('change', handleChange)
+
+ return () => {
+ mediaQuery.removeEventListener('change', handleChange)
+ }
+ }, [])
+
+ const toggle = useCallback(() => {
+ setThemeState(prev => (prev === 'light' ? 'dark' : 'light'))
+ }, [])
+
+ const setTheme = useCallback((theme: Theme) => {
+ setThemeState(theme)
+ }, [])
+
+ return {
+ theme,
+ isDark: theme === 'dark',
+ toggle,
+ setTheme
+ }
+}
+
+export default useDarkMode
diff --git a/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts
new file mode 100644
index 00000000000..872b1e4ad54
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts
@@ -0,0 +1,78 @@
+import { useEffect, useState } from 'react'
+
+type Theme = 'light' | 'dark'
+
+const STORAGE_KEY = 'theme'
+const MEDIA_QUERY = '(prefers-color-scheme: dark)'
+
+function getStoredTheme(): Theme | null {
+ try {
+ const value = window.localStorage.getItem(STORAGE_KEY)
+ return value === 'light' || value === 'dark' ? value : null
+ } catch {
+ return null
+ }
+}
+
+function getSystemTheme(): Theme {
+ return window.matchMedia(MEDIA_QUERY).matches ? 'dark' : 'light'
+}
+
+export function useDarkMode() {
+ const [theme, setThemeState] = useState(() => {
+ if (typeof window === 'undefined') {
+ return 'light'
+ }
+
+ return getStoredTheme() ?? getSystemTheme()
+ })
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme)
+
+ try {
+ window.localStorage.setItem(STORAGE_KEY, theme)
+ } catch {
+ // ignore storage errors
+ }
+ }, [theme])
+
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return
+ }
+
+ const mediaQuery = window.matchMedia(MEDIA_QUERY)
+
+ const handleChange = (event: MediaQueryListEvent) => {
+ const storedTheme = getStoredTheme()
+
+ if (!storedTheme) {
+ setThemeState(event.matches ? 'dark' : 'light')
+ }
+ }
+
+ if (typeof mediaQuery.addEventListener === 'function') {
+ mediaQuery.addEventListener('change', handleChange)
+ return () => mediaQuery.removeEventListener('change', handleChange)
+ }
+
+ mediaQuery.addListener(handleChange)
+ return () => mediaQuery.removeListener(handleChange)
+ }, [])
+
+ const setTheme = (nextTheme: Theme) => {
+ setThemeState(nextTheme)
+ }
+
+ const toggleTheme = () => {
+ setThemeState(prev => (prev === 'light' ? 'dark' : 'light'))
+ }
+
+ return {
+ theme,
+ isDark: theme === 'dark',
+ setTheme,
+ toggleTheme
+ }
+}