seaislee1209 f0f47e8368 feat(theme): 亮色主题切换完整实现 — dark/light 双套 var + Sidebar 切换 + 浅色色板
Stage 1 (var 化, 350 处): 425 处硬编码颜色 → CSS var, 涉及 49 个 tsx/css module 文件,
   按 hot files (DashboardPage/TeamDashboardPage/RecordDetailModal/ReferenceList) →
   Modal/Asset/Profile/Login → 生成页家族/管理后台/公共 UI 三波 8 个 sub-agent 并行处理。
   index.css :root 加 ~70 个新 var (modal/text 层级/状态色 bg 变体/chart/mention pill 等)。
Stage 2 (双套 var): :root 保留 DARK 默认值, [data-theme="light"] 覆盖 ~95 个 token。
   浅色色板按 Vercel Geist (#fafafa / #171717 / shadow-border) + Linear Light surface 分层规范,
   主色 #6c63ff → #5048cc 加深 18% 满足 WCAG AA。aurora 极光在 light 下 display:none。
Stage 3 (切换机制): 新建 store/theme.ts (Zustand + localStorage 持久化),
   Sidebar 加月亮/太阳 SVG 切换按钮 (位于头像上方),
   DashboardPage/TeamDashboardPage/ProfilePage 的 ECharts 配 key={theme} 强制重渲染。
Stage 4 (微调): LandingPage 强制 data-theme="dark" 保持品牌识别 (登录流程一直深色),
   sidebar bg / card bg / border 在浅色下加深 0.02 提升轮廓辨识度。
Stage 5 (验证): Playwright 头无浏览器自动登录 admin + screenshot_user, 截深/浅各 12 个页面 = 24 张
   到 docs/screenshots/ (本地档, .gitignore 排除 png 不入库)。
   vitest 71fail/162pass 与改造前基线完全一致, 无新增回归。

完成报告: docs/todo/亮色主题切换-完成报告.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:10:35 +08:00

136 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/auth';
import { useThemeStore } from '../store/theme';
import logoImg from '../assets/logo_32.png';
import styles from './Sidebar.module.css';
export function Sidebar() {
const navigate = useNavigate();
const location = useLocation();
const user = useAuthStore((s) => s.user);
const quota = useAuthStore((s) => s.quota);
const theme = useThemeStore((s) => s.theme);
const toggleTheme = useThemeStore((s) => s.toggleTheme);
const isActive = (path: string) => location.pathname === path;
const role = user?.role;
// 今日剩余生成次数v0.10.0 起计费体系为次数+金额,不再是秒数池)
const dailyRemaining = quota
? (quota.daily_generation_limit === -1
? Infinity
: Math.max(0, quota.daily_generation_limit - quota.daily_generation_used))
: 0;
return (
<aside className={styles.sidebar}>
{/* Logo */}
<div className={styles.logo} onClick={() => navigate(role === 'super_admin' ? '/admin/dashboard' : '/app')}>
<img src={logoImg} alt="AirDrama" width="32" height="32" />
</div>
{/* Nav items */}
<nav className={styles.navItems}>
{/* Video generation - team members and team admins only */}
{role !== 'super_admin' && (
<>
<div
className={`${styles.navItem} ${isActive('/app') ? styles.active : ''}`}
onClick={() => navigate('/app')}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span></span>
</div>
<div
className={`${styles.navItem} ${isActive('/user-assets') ? styles.active : ''}`}
onClick={() => navigate('/user-assets')}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M3 9h18M9 3v18" />
</svg>
<span></span>
</div>
</>
)}
{/* Team management - team admin only */}
{role === 'team_admin' && (
<div
className={`${styles.navItem} ${location.pathname.startsWith('/team') ? styles.active : ''}`}
onClick={() => navigate('/team/dashboard')}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
</svg>
<span></span>
</div>
)}
</nav>
{/* Bottom section: quota + avatar + admin */}
<div className={styles.bottom}>
{/* Quota display - not for super admin */}
{role !== 'super_admin' && (
<div
className={styles.quota}
onClick={() => navigate('/profile')}
title="今日剩余生成次数(实际扣费以火山 token 消耗为准)"
>
<span className={styles.quotaNumber}>
{dailyRemaining === Infinity ? '∞' : dailyRemaining.toLocaleString()}
</span>
<span className={styles.quotaLabel}></span>
</div>
)}
{/* Theme toggle (moon in dark mode → switch to light; sun in light mode → switch to dark) */}
<button
className={styles.themeToggle}
onClick={toggleTheme}
title={theme === 'dark' ? '切换到浅色主题' : '切换到深色主题'}
aria-label={theme === 'dark' ? '切换到浅色主题' : '切换到深色主题'}
>
{theme === 'dark' ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
</svg>
)}
</button>
{/* Admin entry - super admin only */}
{role === 'super_admin' && (
<div
className={styles.adminBtn}
onClick={() => navigate('/admin/dashboard')}
title="管理后台"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
</svg>
</div>
)}
{/* User avatar */}
<div
className={styles.avatar}
onClick={() => navigate(role === 'super_admin' ? '/admin/dashboard' : '/profile')}
title={user?.username || '个人中心'}
>
{user?.username?.charAt(0).toUpperCase() || 'U'}
</div>
</div>
</aside>
);
}