mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-07 14:14:44 +08:00
release: v1.0.7
This commit is contained in:
@@ -6,7 +6,7 @@ export default function Layout() {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
<div className="min-h-screen bg-gray-50 flex dark:bg-gray-950">
|
||||
<Sidebar collapsed={sidebarCollapsed} onToggle={() => setSidebarCollapsed(!sidebarCollapsed)} />
|
||||
<main className={`flex-1 transition-all duration-300 ${sidebarCollapsed ? 'ml-16' : 'ml-60'}`}>
|
||||
<div className="p-6">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
|
||||
export type StatsRangeKey = '30m' | '1h' | '1d' | '1w'
|
||||
|
||||
@@ -45,17 +46,19 @@ export default function ResourceStatsPanel({
|
||||
charts: ResourceChartConfig[]
|
||||
}) {
|
||||
return (
|
||||
<section className="border border-gray-200 rounded-lg bg-white overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b border-gray-200 bg-white">
|
||||
<h2 className="text-sm font-semibold text-gray-950">统计信息</h2>
|
||||
<section className="border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
||||
<h2 className="text-sm font-semibold text-gray-950 dark:text-white">统计信息</h2>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="inline-flex rounded border border-gray-200 bg-gray-50 p-0.5">
|
||||
<div className="inline-flex rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-0.5">
|
||||
{(Object.keys(rangeLabels) as StatsRangeKey[]).map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
onClick={() => onRangeChange(item)}
|
||||
className={`h-7 px-3 rounded text-xs font-medium transition-colors ${
|
||||
range === item ? 'bg-gray-800 text-white shadow-sm' : 'text-gray-500 hover:text-gray-900'
|
||||
range === item
|
||||
? 'bg-gray-800 text-white shadow-sm dark:bg-white dark:text-black'
|
||||
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{rangeLabels[item]}
|
||||
@@ -64,7 +67,7 @@ export default function ResourceStatsPanel({
|
||||
</div>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="h-8 w-8 inline-flex items-center justify-center rounded border border-gray-200 text-gray-500 hover:bg-gray-50 hover:text-gray-900"
|
||||
className="h-8 w-8 inline-flex items-center justify-center rounded border border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white"
|
||||
title="刷新"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
@@ -90,11 +93,11 @@ function DetailedChart({ chart, className }: { chart: ResourceChartConfig; class
|
||||
<div className={`p-4 ${className}`}>
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-sm font-semibold text-gray-950">
|
||||
<span className="text-gray-500">{chart.icon}</span>
|
||||
<div className="flex items-center gap-1.5 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
<span className="text-gray-500 dark:text-gray-400">{chart.icon}</span>
|
||||
<span>{chart.title}</span>
|
||||
</div>
|
||||
{chart.detail && <p className="mt-0.5 text-[11px] text-gray-400">{chart.detail}</p>}
|
||||
{chart.detail && <p className="mt-0.5 text-[11px] text-gray-400 dark:text-gray-500">{chart.detail}</p>}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-right">
|
||||
<Stat label="当前" value={chart.formatValue(chart.current)} />
|
||||
@@ -115,8 +118,8 @@ function DetailedChart({ chart, className }: { chart: ResourceChartConfig; class
|
||||
function Stat({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[10px] text-gray-400">{label}</div>
|
||||
<div className="text-xs font-semibold text-gray-900 tabular-nums whitespace-nowrap">{value}</div>
|
||||
<div className="text-[10px] text-gray-400 dark:text-gray-500">{label}</div>
|
||||
<div className="text-xs font-semibold text-gray-900 dark:text-gray-100 tabular-nums whitespace-nowrap">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -132,6 +135,9 @@ function LineAreaChart({
|
||||
formatValue: (value: number) => string
|
||||
unitLabel?: string
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const isDark = theme === 'dark'
|
||||
|
||||
const width = 520
|
||||
const height = 150
|
||||
const left = 50
|
||||
@@ -158,12 +164,20 @@ function LineAreaChart({
|
||||
const yTicks = [1, 0.5, 0]
|
||||
const xTicks = [0, 0.5, 1]
|
||||
|
||||
// Dark mode colors
|
||||
const gridStroke = isDark ? '#374151' : '#e5e7eb'
|
||||
const gridStrokeV = isDark ? '#1f2937' : '#edf0f2'
|
||||
const axisStroke = isDark ? '#9ca3af' : '#888'
|
||||
const lineStroke = isDark ? '#f9fafb' : '#444'
|
||||
const gradientTop = isDark ? '#f9fafb' : '#555'
|
||||
const gradientBottom = isDark ? '#374151' : '#555'
|
||||
|
||||
return (
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-[140px]" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id="resource-chart-fill" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#555" stopOpacity="0.25" />
|
||||
<stop offset="100%" stopColor="#555" stopOpacity="0.02" />
|
||||
<stop offset="0%" stopColor={gradientTop} stopOpacity="0.25" />
|
||||
<stop offset="100%" stopColor={gradientBottom} stopOpacity="0.02" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -171,8 +185,8 @@ function LineAreaChart({
|
||||
const y = top + (1 - tick) * innerHeight
|
||||
return (
|
||||
<g key={tick}>
|
||||
<line x1={left} y1={y} x2={left + innerWidth} y2={y} stroke="#e5e7eb" strokeDasharray="3 3" />
|
||||
<text x={left - 8} y={y + 3} textAnchor="end" fontSize="10" fill="#888">
|
||||
<line x1={left} y1={y} x2={left + innerWidth} y2={y} stroke={gridStroke} strokeDasharray="3 3" />
|
||||
<text x={left - 8} y={y + 3} textAnchor="end" fontSize="10" fill={axisStroke}>
|
||||
{formatValue(maxValue * tick)}
|
||||
</text>
|
||||
</g>
|
||||
@@ -184,8 +198,8 @@ function LineAreaChart({
|
||||
const ts = minTs + tick * span
|
||||
return (
|
||||
<g key={tick}>
|
||||
<line x1={x} y1={top} x2={x} y2={top + innerHeight} stroke="#edf0f2" strokeDasharray="3 3" />
|
||||
<text x={x} y={height - 5} textAnchor={tick === 0 ? 'start' : tick === 1 ? 'end' : 'middle'} fontSize="10" fill="#888">
|
||||
<line x1={x} y1={top} x2={x} y2={top + innerHeight} stroke={gridStrokeV} strokeDasharray="3 3" />
|
||||
<text x={x} y={height - 5} textAnchor={tick === 0 ? 'start' : tick === 1 ? 'end' : 'middle'} fontSize="10" fill={axisStroke}>
|
||||
{formatTime(ts)}
|
||||
</text>
|
||||
</g>
|
||||
@@ -193,15 +207,15 @@ function LineAreaChart({
|
||||
})}
|
||||
|
||||
{unitLabel && (
|
||||
<text x={left - 45} y={top + 10} fontSize="10" fill="#888">
|
||||
<text x={left - 45} y={top + 10} fontSize="10" fill={axisStroke}>
|
||||
{unitLabel}
|
||||
</text>
|
||||
)}
|
||||
|
||||
<line x1={left} y1={top} x2={left} y2={top + innerHeight} stroke="#888" />
|
||||
<line x1={left} y1={top + innerHeight} x2={left + innerWidth} y2={top + innerHeight} stroke="#888" />
|
||||
<line x1={left} y1={top} x2={left} y2={top + innerHeight} stroke={axisStroke} />
|
||||
<line x1={left} y1={top + innerHeight} x2={left + innerWidth} y2={top + innerHeight} stroke={axisStroke} />
|
||||
<polygon points={area} fill="url(#resource-chart-fill)" />
|
||||
<polyline points={line} fill="none" stroke="#444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<polyline points={line} fill="none" stroke={lineStroke} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
|
||||
interface RingStatProps {
|
||||
value: number
|
||||
@@ -10,11 +11,17 @@ interface RingStatProps {
|
||||
}
|
||||
|
||||
export function RingStat({ value, max = 100, label, subLabel, size = 120, strokeWidth = 8 }: RingStatProps) {
|
||||
const { theme } = useTheme()
|
||||
const isDark = theme === 'dark'
|
||||
|
||||
const radius = (size - strokeWidth) / 2
|
||||
const circumference = radius * 2 * Math.PI
|
||||
const percentage = Math.min(Math.max(value / max * 100, 0), 100)
|
||||
const strokeDashoffset = circumference - (percentage / 100) * circumference
|
||||
|
||||
const bgStroke = isDark ? '#374151' : '#f3f4f6'
|
||||
const progressStroke = isDark ? '#f9fafb' : '#000000'
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative" style={{ width: size, height: size }}>
|
||||
@@ -25,7 +32,7 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="#f3f4f6"
|
||||
stroke={bgStroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
{/* Progress ring */}
|
||||
@@ -34,7 +41,7 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke={progressStroke}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
@@ -44,12 +51,12 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke
|
||||
</svg>
|
||||
{/* Center value */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold text-black">{value.toFixed(percentage < 1 ? 2 : 1)}%</span>
|
||||
<span className="text-2xl font-bold text-black dark:text-white">{value.toFixed(percentage < 1 ? 2 : 1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-center">
|
||||
<div className="text-sm font-medium text-gray-800">{label}</div>
|
||||
{subLabel && <div className="text-xs text-gray-400 mt-0.5">{subLabel}</div>}
|
||||
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{label}</div>
|
||||
{subLabel && <div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{subLabel}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -96,8 +103,8 @@ export default function RingStats({
|
||||
const hasSwap = swapTotal > 0
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-5">
|
||||
<h2 className="text-sm font-semibold text-black mb-4">状态</h2>
|
||||
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-5">
|
||||
<h2 className="text-sm font-semibold text-black dark:text-white mb-4">状态</h2>
|
||||
<div className={`grid ${hasSwap ? 'grid-cols-5' : 'grid-cols-4'} gap-3`}>
|
||||
<RingStat
|
||||
value={cpuPercent}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
ChevronLeft,
|
||||
@@ -6,15 +7,19 @@ import {
|
||||
Camera,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Moon,
|
||||
Package,
|
||||
Route,
|
||||
ScrollText,
|
||||
Server,
|
||||
Settings2,
|
||||
ShieldAlert,
|
||||
Sun,
|
||||
UserCog,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import { getVersion } from '../services/api'
|
||||
import AppIcon from './AppIcon'
|
||||
|
||||
interface SidebarProps {
|
||||
@@ -26,6 +31,18 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { logout, isSubUser } = useAuth()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const [version, setVersion] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
getVersion()
|
||||
.then(res => {
|
||||
if (res.data?.data?.version) {
|
||||
setVersion(res.data.data.version)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const isContainerPage =
|
||||
location.pathname.startsWith('/containers') ||
|
||||
@@ -42,27 +59,27 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`fixed left-0 top-0 h-full bg-white border-r border-gray-200 flex flex-col transition-all duration-300 z-30 ${
|
||||
className={`fixed left-0 top-0 h-full bg-white border-r border-gray-200 flex flex-col transition-all duration-300 z-30 dark:bg-gray-900 dark:border-gray-700 ${
|
||||
collapsed ? 'w-16' : 'w-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between h-14 px-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between h-14 px-4 border-b border-gray-200 dark:border-gray-700">
|
||||
{!collapsed && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 bg-gray-100 rounded flex items-center justify-center">
|
||||
<div className="w-7 h-7 bg-gray-100 rounded flex items-center justify-center dark:bg-gray-800">
|
||||
<AppIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="font-bold text-black text-sm">CLICD</span>
|
||||
<span className="font-bold text-black text-sm dark:text-white">CLICD</span>
|
||||
</div>
|
||||
)}
|
||||
{collapsed && (
|
||||
<div className="w-7 h-7 bg-gray-100 rounded flex items-center justify-center mx-auto">
|
||||
<div className="w-7 h-7 bg-gray-100 rounded flex items-center justify-center mx-auto dark:bg-gray-800">
|
||||
<AppIcon className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-1 rounded hover:bg-gray-100 text-gray-500"
|
||||
className="p-1 rounded hover:bg-gray-100 text-gray-500 dark:hover:bg-gray-800 dark:text-gray-400"
|
||||
title="切换侧边栏"
|
||||
>
|
||||
{collapsed ? (
|
||||
@@ -79,8 +96,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
location.pathname === '/'
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
@@ -92,8 +109,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/containers')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isContainerPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Server className="w-4 h-4" />
|
||||
@@ -105,8 +122,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/images')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isImagesPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Package className="w-4 h-4" />
|
||||
@@ -120,8 +137,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/oversell')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isOversellPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Settings2 className="w-4 h-4" />
|
||||
@@ -132,8 +149,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/security')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isSecurityPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<ShieldAlert className="w-4 h-4" />
|
||||
@@ -144,8 +161,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/snapshots')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isSnapshotsPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Camera className="w-4 h-4" />
|
||||
@@ -156,8 +173,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/routing')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isRoutingPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Route className="w-4 h-4" />
|
||||
@@ -168,8 +185,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/audit-logs')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isAuditLogsPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<ScrollText className="w-4 h-4" />
|
||||
@@ -180,8 +197,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/api-integration')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isApiIntegrationPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Code2 className="w-4 h-4" />
|
||||
@@ -192,8 +209,8 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => navigate('/settings')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||
isSettingsPage
|
||||
? 'bg-black text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<UserCog className="w-4 h-4" />
|
||||
@@ -203,10 +220,36 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-gray-200 p-2">
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-2 space-y-1">
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm text-gray-600 hover:bg-gray-100 transition-colors dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
title={theme === 'dark' ? '切换亮色模式' : '切换暗黑模式'}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="w-4 h-4" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4" />
|
||||
)}
|
||||
{!collapsed && <span>{theme === 'dark' ? '亮色模式' : '暗黑模式'}</span>}
|
||||
</button>
|
||||
|
||||
{/* Version */}
|
||||
{version && (
|
||||
<div className={`px-3 py-2 text-xs text-gray-400 dark:text-gray-500 ${collapsed ? 'text-center' : ''}`}>
|
||||
{collapsed ? (
|
||||
<span title={`v${version}`}>v{version.split('.').slice(0, 2).join('.')}</span>
|
||||
) : (
|
||||
<span>v{version}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Logout */}
|
||||
<button
|
||||
onClick={logout}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm text-gray-600 hover:bg-gray-100 transition-colors"
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm text-gray-600 hover:bg-gray-100 transition-colors dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
{!collapsed && <span>退出登录</span>}
|
||||
|
||||
Reference in New Issue
Block a user