压缩历史至 95081aff

This commit is contained in:
ryan
2026-06-08 20:34:27 +08:00
commit 8a782525de
435 changed files with 71146 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import type { ThemeConfig } from "./types"
/**
* 主题系统配置
* 定义主题文件的存储位置和 localStorage 键名
*/
export const THEME_CONFIG: ThemeConfig = {
styleDir: "public/style",
storageKey: "app-theme-id",
}
+129
View File
@@ -0,0 +1,129 @@
"use client"
import React, { createContext, useContext, useEffect, useState, useCallback } from "react"
import { useTheme as useNextTheme } from "next-themes"
import type { Theme } from "./types"
import { getStoredThemeId, setStoredThemeId } from "./storage"
import { getAvailableThemes } from "./parser"
/**
* 主题上下文类型定义
*/
interface ThemeContextValue {
/** 所有可用主题列表 */
themes: Theme[]
/** 当前激活的主题对象 */
currentTheme: Theme | null
/** 当前激活的主题 ID */
currentThemeId: string | null
/** 设置主题的方法 */
setTheme: (themeId: string) => void
/** 是否正在加载主题 */
isLoading: boolean
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
/**
* 自定义主题 Hook
* 必须在 CustomThemeProvider 内部使用
*/
export function useCustomTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error("useCustomTheme must be used within CustomThemeProvider")
}
return context
}
/**
* 自定义主题提供者
* 管理用户选择的界面外观主题(配色方案),独立于明暗模式
*/
export function CustomThemeProvider({ children }: { children: React.ReactNode }) {
const [themes, setThemes] = useState<Theme[]>([])
const [currentThemeId, setCurrentThemeId] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [mounted, setMounted] = useState(false)
const { resolvedTheme } = useNextTheme()
/* 加载所有可用主题 */
useEffect(() => {
async function loadThemes() {
const availableThemes = await getAvailableThemes()
const storedId = getStoredThemeId()
setThemes(availableThemes)
setCurrentThemeId(storedId)
setIsLoading(false)
}
loadThemes()
}, [])
/* 确保组件已挂载,避免水合不一致 */
useEffect(() => {
setMounted(true)
}, [])
/**
* 将主题颜色应用到 DOM
* @param theme - 要应用的主题
* @param isDark - 是否为暗色模式
*/
const applyThemeColors = useCallback((theme: Theme, isDark: boolean) => {
if (typeof window === "undefined") return
const root = document.documentElement
const colors = isDark ? theme.colors.dark : theme.colors.light
/* 应用所有 CSS 变量到根元素 */
Object.entries(colors).forEach(([key, value]) => {
root.style.setProperty(`--${ key }`, value)
})
}, [])
/* 当主题 ID 或模式改变时,重新应用颜色 */
useEffect(() => {
if (!mounted || !currentThemeId || themes.length === 0) return
const theme = themes.find(t => t.id === currentThemeId)
if (!theme) return
/* 使用 next-themes 的 resolvedTheme,它会自动处理系统偏好 */
const isDark = resolvedTheme === "dark"
applyThemeColors(theme, isDark)
}, [currentThemeId, resolvedTheme, themes, mounted, applyThemeColors])
/**
* 设置主题
* @param themeId - 要设置的主题 ID
*/
const setTheme = useCallback((themeId: string) => {
const theme = themes.find(t => t.id === themeId)
if (!theme) return
setCurrentThemeId(themeId)
setStoredThemeId(themeId)
/* 根据当前解析的主题模式计算是否为暗色 */
const isDark = resolvedTheme === "dark"
applyThemeColors(theme, isDark)
}, [themes, resolvedTheme, applyThemeColors])
const currentTheme = themes.find(t => t.id === currentThemeId) || null
return (
<ThemeContext.Provider
value={{
themes,
currentTheme,
currentThemeId,
setTheme,
isLoading
}}
>
{children}
</ThemeContext.Provider>
)
}
+19
View File
@@ -0,0 +1,19 @@
/**
* 主题系统
* 统一导出整个主题系统的所有功能
*/
/* 类型定义 */
export type { Theme, ThemeColors, ThemeConfig } from "./types"
/* 配置 */
export { THEME_CONFIG } from "./config"
/* 服务端工具 */
export { getAvailableThemes } from "./parser"
/* 客户端工具 */
export { getStoredThemeId, setStoredThemeId, removeStoredThemeId } from "./storage"
/* React 上下文和 Hooks */
export { CustomThemeProvider, useCustomTheme } from "./context"
+79
View File
@@ -0,0 +1,79 @@
"use server"
import fs from "fs/promises"
import path from "path"
import type { Theme } from "./types"
import { THEME_CONFIG } from "./config"
/**
* 从 CSS 片段中解析 CSS 变量
* @param cssSection - CSS 代码片段
* @returns CSS 变量键值对映射
*/
function parseCSSVariables(cssSection: string): Record<string, string> {
const variables: Record<string, string> = {}
const regex = /--([a-z0-9-]+):\s*([^;]+);/g
let match
while ((match = regex.exec(cssSection)) !== null) {
variables[match[1]] = match[2].trim()
}
return variables
}
/**
* 获取所有可用主题
* 通过解析 CSS 文件获取主题列表和颜色配置
* @returns 主题列表,按默认主题优先,其余按名称排序
*/
export async function getAvailableThemes(): Promise<Theme[]> {
try {
const styleDir = path.join(process.cwd(), THEME_CONFIG.styleDir)
const files = await fs.readdir(styleDir)
const themes = await Promise.all(
files
.filter((file) => file.endsWith(".css"))
.map(async (file) => {
/* 生成主题名称 */
const name = file === "default.css"
? "Default"
: file
.replace(".css", "")
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
const content = await fs.readFile(path.join(styleDir, file), "utf-8")
/* 从 :root 解析明亮模式颜色 */
const rootSection = content.match(/:root\s*\{([^}]+)\}/s)?.[1] || ""
const lightColors = parseCSSVariables(rootSection)
/* 从 .dark 解析暗色模式颜色 */
const darkSection = content.match(/\.dark\s*\{([^}]+)\}/s)?.[1] || ""
const darkColors = parseCSSVariables(darkSection)
return {
id: file,
name,
colors: {
light: lightColors,
dark: darkColors,
}
}
})
)
/* 默认主题排在最前面,其余按名称排序 */
return themes.sort((a, b) => {
if (a.id === "default.css") return -1
if (b.id === "default.css") return 1
return a.name.localeCompare(b.name)
})
} catch (error) {
console.error("Failed to parse themes:", error)
return []
}
}
+32
View File
@@ -0,0 +1,32 @@
"use client"
import { THEME_CONFIG } from "./config"
/**
* 客户端主题存储工具
* 用于在 localStorage 中保存和读取用户的主题偏好
*/
/**
* 获取存储的主题 ID
*/
export function getStoredThemeId(): string | null {
if (typeof window === "undefined") return null
return localStorage.getItem(THEME_CONFIG.storageKey)
}
/**
* 保存主题 ID 到本地存储
*/
export function setStoredThemeId(themeId: string): void {
if (typeof window === "undefined") return
localStorage.setItem(THEME_CONFIG.storageKey, themeId)
}
/**
* 移除存储的主题 ID
*/
export function removeStoredThemeId(): void {
if (typeof window === "undefined") return
localStorage.removeItem(THEME_CONFIG.storageKey)
}
+34
View File
@@ -0,0 +1,34 @@
/**
* 主题系统类型定义
*/
/**
* 主题颜色配置
* 包含明亮和暗色两种模式的 CSS 变量映射
*/
export interface ThemeColors {
light: Record<string, string>
dark: Record<string, string>
}
/**
* 主题定义
*/
export interface Theme {
/** 主题唯一标识符(CSS 文件名) */
id: string
/** 主题显示名称 */
name: string
/** 主题颜色配置 */
colors: ThemeColors
}
/**
* 主题系统配置
*/
export interface ThemeConfig {
/** CSS 样式文件所在目录 */
styleDir: string
/** 主题 ID 在 localStorage 中的存储键名 */
storageKey: string
}