feat: add shadcn theme switcher

This commit is contained in:
hailin
2026-07-02 15:57:35 +08:00
parent ce423bd90b
commit 32ed881732
5 changed files with 110 additions and 3 deletions

View File

@@ -1,14 +1,17 @@
import { AppShell } from './layout/AppShell' import { AppShell } from './layout/AppShell'
import { Router } from './router' import { Router } from './router'
import { AppThemeProvider } from '@/components/app/AppThemeProvider'
import { AuthProvider, useAuth } from '@/features/auth/AuthProvider' import { AuthProvider, useAuth } from '@/features/auth/AuthProvider'
import { LoginPage } from '@/features/auth/LoginPage' import { LoginPage } from '@/features/auth/LoginPage'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
export function App() { export function App() {
return ( return (
<AuthProvider> <AppThemeProvider>
<AppContent /> <AuthProvider>
</AuthProvider> <AppContent />
</AuthProvider>
</AppThemeProvider>
) )
} }

View File

@@ -0,0 +1,14 @@
import type * as React from 'react'
import { ThemeProvider } from 'next-themes'
type AppThemeProviderProps = {
children: React.ReactNode
}
export function AppThemeProvider({ children }: AppThemeProviderProps) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
)
}

View File

@@ -0,0 +1,53 @@
import { Laptop, Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
const themeOptions = [
{ value: 'light', label: '亮色', icon: Sun },
{ value: 'dark', label: '暗色', icon: Moon },
{ value: 'system', label: '跟随系统', icon: Laptop }
]
export function AppThemeToggle() {
const { theme = 'system', setTheme } = useTheme()
const activeTheme = themeOptions.find((option) => option.value === theme) ?? themeOptions[2]
const ActiveIcon = activeTheme.icon
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label={`主题:${activeTheme.label}`}>
<ActiveIcon data-icon="inline-start" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuLabel>界面主题</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
{themeOptions.map((option) => {
const Icon = option.icon
return (
<DropdownMenuRadioItem key={option.value} value={option.value}>
<Icon data-icon="inline-start" />
{option.label}
</DropdownMenuRadioItem>
)
})}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -1,5 +1,6 @@
import { Bell, ChevronRight, LogOut, Menu, UserRound } from 'lucide-react' import { Bell, ChevronRight, LogOut, Menu, UserRound } from 'lucide-react'
import { AppThemeToggle } from '@/components/app/AppThemeToggle'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
@@ -34,6 +35,7 @@ export function AppTopbar({ onMobileMenuClick }: AppTopbarProps) {
<Button variant="ghost" size="icon" aria-label="通知"> <Button variant="ghost" size="icon" aria-label="通知">
<Bell data-icon="inline-start" /> <Bell data-icon="inline-start" />
</Button> </Button>
<AppThemeToggle />
<Button variant="ghost" className="hidden gap-2 sm:inline-flex"> <Button variant="ghost" className="hidden gap-2 sm:inline-flex">
<UserRound data-icon="inline-start" /> <UserRound data-icon="inline-start" />
<span>{user?.display_name || user?.username || '当前用户'}</span> <span>{user?.display_name || user?.username || '当前用户'}</span>

View File

@@ -39,6 +39,41 @@
--sidebar-ring: 213 52% 32%; --sidebar-ring: 213 52% 32%;
} }
.dark {
--background: 222 28% 8%;
--foreground: 214 24% 94%;
--surface: 222 26% 11%;
--muted: 219 22% 17%;
--muted-foreground: 215 14% 70%;
--border: 219 20% 24%;
--primary: 186 52% 46%;
--primary-foreground: 222 28% 8%;
--success: 151 55% 46%;
--warning: 38 86% 58%;
--danger: 0 70% 60%;
--info: 204 76% 58%;
--ai-accent: 186 52% 46%;
--card: 222 26% 11%;
--card-foreground: 214 24% 94%;
--popover: 222 26% 11%;
--popover-foreground: 214 24% 94%;
--secondary: 219 22% 17%;
--secondary-foreground: 214 24% 94%;
--accent: 219 22% 17%;
--accent-foreground: 214 24% 94%;
--destructive: 0 70% 60%;
--input: 219 20% 24%;
--ring: 186 52% 46%;
--sidebar: 222 32% 11%;
--sidebar-foreground: 214 24% 92%;
--sidebar-primary: 186 52% 46%;
--sidebar-primary-foreground: 222 28% 8%;
--sidebar-accent: 219 26% 20%;
--sidebar-accent-foreground: 0 0% 100%;
--sidebar-border: 219 24% 24%;
--sidebar-ring: 186 52% 46%;
}
body { body {
margin: 0; margin: 0;
background: hsl(var(--background)); background: hsl(var(--background));