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>
136 lines
5.9 KiB
TypeScript
136 lines
5.9 KiB
TypeScript
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>
|
||
);
|
||
}
|