Phase A: index.css 完全重写 [data-theme="light"] block
- 玻璃方向修正: bg-card 从 rgba(0,0,0,0.05) 黑透明 → 双 token 拆分
--color-bg-card: #ffffff 实体白 (admin 卡)
--color-bg-glass: rgba(255,255,255,0.65) 透明白玻璃 (sidebar/modal/banner)
- Aurora 浅色不再 display:none, 改 pastel 紫蓝桃 0.20-0.32 alpha
- Inset highlight 方向反转: 浅色用 rgba(255,255,255,0.50) 白高光 (玻璃顶边标志)
- Backdrop-filter 五档标准: --bf-glass-sm/md/lg/xl (12-40px + saturate 140-180%)
- Multi-layer shadow: --shadow-card-light (2 stops) + --shadow-glass-light (3 stops + inset)
- 暖调 chip: --color-chip-warm-* GitBook 公告风格
- 文字主色: #171823 微紫 → #171717 Vercel Black
Phase B: LandingPage + AuroraCanvas 浅色化
- 移除 LandingPage 的 data-theme="dark" 强制 (V1 的回避)
- LandingPage.module.css 21 处颜色全 var 化
- AuroraCanvas: 订阅 useThemeStore, 新 LIGHT_ORBS 数组 pastel 紫蓝桃,
vignette 浅色用白色, grain opacity 减半
Phase C: 13 个玻璃面升级 (3 sub-agent 并行)
- Modal 类 (Login/ForceChange/VideoDetail.infoPanel/RecordDetail/AssetLibrary/
Announcement/Confirm/TeamsPage.detailModal): 接入 bg-modal-glass +
bf-glass-lg/xl + shadow-glass-light (含 inset highlight)
- Bar/Dropdown/Toast (AnnouncementBanner/Toast/Dropdown/Select/DatePicker):
bg-glass-strong + bf-glass-md + inset-highlight
- Sidebar + 生成页 (Sidebar/PromptInput/GenerationCard): glass + 顶边白高光
- AnnouncementBanner 写双套独立 [data-theme] 规则 (CSS gradient 内不能 var alpha)
Phase D: admin 实体卡 multi-layer shadow (13 处, 1 sub-agent)
- DashboardPage / TeamsPage / UsersPage / RecordsPage / AdminAssetsPage /
LoginRecordsPage / AuditLogsPage / ProfilePage / SettingsPage
的 .statCard / .tableWrapper / .chartWrapper / .accordionItem 等
加 var(--shadow-card-light) 双层柔阴影
AdminLayout 修复 (V1 漏的):
- .layout 改 transparent, 让 AmbientBackground pastel aurora 在主区透出
- .sidebar 加 bf-glass-md + inset highlight + 立体阴影
LoginModal / ForceChangePassword 残留 mint 清理:
- submitBtn bg/border/color 用 mint-accent var, 字重 500→600 + 字距 0.04em
- input:focus border 用 var(--color-mint-accent)
- 加 bf-glass-sm + inset highlight
验证:
- TS 编译过
- vitest 71 fail / 162 pass 与 V1 基线完全一致, 无新增回归
- 24 张 V2 截图位于 docs/screenshots/v2/ (本地, .gitignore 排除 png)
完成报告: docs/todo/亮色主题切换V2-完成报告.md
V2 plan: docs/todo/亮色主题切换V2.md
视觉对齐稿: docs/todo/showcase.html
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
265 lines
9.0 KiB
TypeScript
265 lines
9.0 KiB
TypeScript
import { useRef, useEffect, useCallback } from 'react';
|
|
import { useThemeStore } from '../store/theme';
|
|
|
|
/**
|
|
* Aurora background — 5 large diffuse orbs with additive blending.
|
|
* V2: 接入 themeStore,浅色态用 pastel orb + 白色 vignette/gradient,深色保持原品牌色。
|
|
*/
|
|
|
|
interface Orb {
|
|
cx: number; // base center X ratio (0-1)
|
|
cy: number; // base center Y ratio (0-1)
|
|
color: [number, number, number];
|
|
alpha: number;
|
|
phase: number;
|
|
freqX: number;
|
|
freqY: number;
|
|
ampX: number;
|
|
ampY: number;
|
|
radius: number;
|
|
breathFreq: number;
|
|
breathAmp: number;
|
|
}
|
|
|
|
// Dark theme orbs — vivid 品牌色(青/紫/蓝)
|
|
const DARK_ORBS: Orb[] = [
|
|
{ cx: 0.25, cy: 0.30, color: [126, 220, 200], alpha: 0.28,
|
|
phase: 0, freqX: 0.00012, freqY: 0.00010, ampX: 0.12, ampY: 0.10,
|
|
radius: 0.50, breathFreq: 0.0004, breathAmp: 0.06 },
|
|
{ cx: 0.72, cy: 0.65, color: [108, 99, 255], alpha: 0.22,
|
|
phase: 1.8, freqX: 0.00010, freqY: 0.00014, ampX: 0.14, ampY: 0.12,
|
|
radius: 0.45, breathFreq: 0.0003, breathAmp: 0.08 },
|
|
{ cx: 0.58, cy: 0.38, color: [59, 130, 246], alpha: 0.18,
|
|
phase: 3.2, freqX: 0.00015, freqY: 0.00008, ampX: 0.10, ampY: 0.15,
|
|
radius: 0.38, breathFreq: 0.0005, breathAmp: 0.07 },
|
|
{ cx: 0.20, cy: 0.70, color: [167, 139, 250], alpha: 0.15,
|
|
phase: 4.5, freqX: 0.00008, freqY: 0.00012, ampX: 0.16, ampY: 0.08,
|
|
radius: 0.40, breathFreq: 0.00035, breathAmp: 0.10 },
|
|
{ cx: 0.78, cy: 0.25, color: [34, 211, 238], alpha: 0.20,
|
|
phase: 5.8, freqX: 0.00014, freqY: 0.00016, ampX: 0.08, ampY: 0.12,
|
|
radius: 0.35, breathFreq: 0.00045, breathAmp: 0.09 },
|
|
];
|
|
|
|
// Light theme orbs — pastel 浅色,alpha 减半左右,给玻璃面提供穿透色源
|
|
const LIGHT_ORBS: Orb[] = [
|
|
{ cx: 0.25, cy: 0.30, color: [180, 167, 255], alpha: 0.32, // pastel lavender
|
|
phase: 0, freqX: 0.00012, freqY: 0.00010, ampX: 0.12, ampY: 0.10,
|
|
radius: 0.55, breathFreq: 0.0004, breathAmp: 0.06 },
|
|
{ cx: 0.72, cy: 0.65, color: [167, 200, 255], alpha: 0.28, // pastel sky
|
|
phase: 1.8, freqX: 0.00010, freqY: 0.00014, ampX: 0.14, ampY: 0.12,
|
|
radius: 0.50, breathFreq: 0.0003, breathAmp: 0.08 },
|
|
{ cx: 0.58, cy: 0.38, color: [255, 180, 130, ], alpha: 0.18, // pastel peach
|
|
phase: 3.2, freqX: 0.00015, freqY: 0.00008, ampX: 0.10, ampY: 0.15,
|
|
radius: 0.42, breathFreq: 0.0005, breathAmp: 0.07 },
|
|
{ cx: 0.20, cy: 0.70, color: [220, 167, 255], alpha: 0.20, // pastel pink-violet
|
|
phase: 4.5, freqX: 0.00008, freqY: 0.00012, ampX: 0.16, ampY: 0.08,
|
|
radius: 0.45, breathFreq: 0.00035, breathAmp: 0.10 },
|
|
{ cx: 0.78, cy: 0.25, color: [180, 220, 255], alpha: 0.22, // pastel blue
|
|
phase: 5.8, freqX: 0.00014, freqY: 0.00016, ampX: 0.08, ampY: 0.12,
|
|
radius: 0.40, breathFreq: 0.00045, breathAmp: 0.09 },
|
|
];
|
|
|
|
const MOUSE_RADIUS = 400;
|
|
const MOUSE_PUSH = 28;
|
|
|
|
export function AuroraCanvas() {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const grainRef = useRef<HTMLCanvasElement>(null);
|
|
const mouseRef = useRef({ x: -9999, y: -9999, active: false });
|
|
const theme = useThemeStore((s) => s.theme);
|
|
const isLight = theme === 'light';
|
|
|
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
mouseRef.current.x = e.clientX;
|
|
mouseRef.current.y = e.clientY;
|
|
mouseRef.current.active = true;
|
|
}, []);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
mouseRef.current.active = false;
|
|
}, []);
|
|
|
|
// ── Aurora animation ──
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
let w = window.innerWidth;
|
|
let h = window.innerHeight;
|
|
|
|
function resize() {
|
|
w = window.innerWidth;
|
|
h = window.innerHeight;
|
|
const dpr = w < 768 ? 0.5 : Math.min(window.devicePixelRatio, 1.5);
|
|
canvas!.width = w * dpr;
|
|
canvas!.height = h * dpr;
|
|
canvas!.style.width = w + 'px';
|
|
canvas!.style.height = h + 'px';
|
|
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
}
|
|
|
|
resize();
|
|
|
|
let animId: number;
|
|
const t0 = performance.now();
|
|
let smoothMx = -9999;
|
|
let smoothMy = -9999;
|
|
|
|
const orbs = isLight ? LIGHT_ORBS : DARK_ORBS;
|
|
|
|
function draw(now: number) {
|
|
const t = now - t0;
|
|
ctx!.clearRect(0, 0, w, h);
|
|
// 浅色用 source-over 让 pastel 互融时不会过曝;深色继续用 lighter 加合
|
|
ctx!.globalCompositeOperation = isLight ? 'source-over' : 'lighter';
|
|
|
|
const mouse = mouseRef.current;
|
|
if (mouse.active) {
|
|
smoothMx += (mouse.x - smoothMx) * 0.035;
|
|
smoothMy += (mouse.y - smoothMy) * 0.035;
|
|
} else {
|
|
smoothMx += (-9999 - smoothMx) * 0.01;
|
|
smoothMy += (-9999 - smoothMy) * 0.01;
|
|
}
|
|
|
|
for (const orb of orbs) {
|
|
let x = w * (orb.cx + Math.sin(t * orb.freqX + orb.phase) * orb.ampX);
|
|
let y = h * (orb.cy + Math.cos(t * orb.freqY + orb.phase * 0.7) * orb.ampY);
|
|
|
|
const dx = x - smoothMx;
|
|
const dy = y - smoothMy;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < MOUSE_RADIUS && dist > 1) {
|
|
const strength = (1 - dist / MOUSE_RADIUS) * MOUSE_PUSH;
|
|
x += (dx / dist) * strength;
|
|
y += (dy / dist) * strength;
|
|
}
|
|
|
|
const breathT = Math.sin(t * orb.breathFreq + orb.phase * 1.3);
|
|
const r = Math.max(w, h) * orb.radius * (1 + breathT * orb.breathAmp);
|
|
const a = orb.alpha * (1 + breathT * 0.15);
|
|
|
|
const [cr, cg, cb] = orb.color;
|
|
const grad = ctx!.createRadialGradient(x, y, 0, x, y, r);
|
|
grad.addColorStop(0, `rgba(${cr},${cg},${cb},${(a).toFixed(3)})`);
|
|
grad.addColorStop(0.25, `rgba(${cr},${cg},${cb},${(a * 0.5).toFixed(3)})`);
|
|
grad.addColorStop(0.5, `rgba(${cr},${cg},${cb},${(a * 0.15).toFixed(3)})`);
|
|
grad.addColorStop(0.8, `rgba(${cr},${cg},${cb},${(a * 0.03).toFixed(3)})`);
|
|
grad.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
|
|
|
|
ctx!.fillStyle = grad;
|
|
ctx!.beginPath();
|
|
ctx!.arc(x, y, r, 0, Math.PI * 2);
|
|
ctx!.fill();
|
|
}
|
|
|
|
ctx!.globalCompositeOperation = 'source-over';
|
|
animId = requestAnimationFrame(draw);
|
|
}
|
|
|
|
animId = requestAnimationFrame(draw);
|
|
window.addEventListener('resize', resize);
|
|
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
|
document.addEventListener('mouseleave', handleMouseLeave);
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animId);
|
|
window.removeEventListener('resize', resize);
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseleave', handleMouseLeave);
|
|
};
|
|
}, [handleMouseMove, handleMouseLeave, isLight]);
|
|
|
|
// ── Film grain — 4 FPS low-noise ──
|
|
useEffect(() => {
|
|
const canvas = grainRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
canvas.width = 256;
|
|
canvas.height = 256;
|
|
|
|
let animId: number;
|
|
let lastFrame = 0;
|
|
|
|
function drawGrain(now: number) {
|
|
if (now - lastFrame > 250) {
|
|
lastFrame = now;
|
|
const img = ctx!.createImageData(256, 256);
|
|
const d = img.data;
|
|
for (let i = 0; i < d.length; i += 4) {
|
|
const v = Math.random() * 255;
|
|
d[i] = d[i + 1] = d[i + 2] = v;
|
|
d[i + 3] = 255;
|
|
}
|
|
ctx!.putImageData(img, 0, 0);
|
|
}
|
|
animId = requestAnimationFrame(drawGrain);
|
|
}
|
|
|
|
animId = requestAnimationFrame(drawGrain);
|
|
return () => cancelAnimationFrame(animId);
|
|
}, []);
|
|
|
|
// 浅色态:vignette / gradient 反相 — 用白色压边,黑色压边在浅色上是错的
|
|
const vignetteColor = isLight ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.8)';
|
|
const fadeColor = isLight ? 'rgba(250,250,250,0.7)' : 'rgba(0,0,0,0.5)';
|
|
|
|
return (
|
|
<>
|
|
{/* Layer 1: Vignette — radial fading, 浅色下用白色 */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
zIndex: 1,
|
|
background: `radial-gradient(ellipse 65% 55% at 50% 50%, transparent 0%, ${vignetteColor} 100%)`,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
|
|
{/* Layer 2: Film grain — 浅色下大幅减弱避免噪点过曝 */}
|
|
<canvas
|
|
ref={grainRef}
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
zIndex: 2,
|
|
width: '100%',
|
|
height: '100%',
|
|
opacity: isLight ? 0.015 : 0.035,
|
|
pointerEvents: 'none',
|
|
mixBlendMode: isLight ? 'multiply' : 'overlay',
|
|
}}
|
|
/>
|
|
|
|
{/* Layer 3: Aurora — blur 让 orb 融成有机晕染 */}
|
|
<canvas
|
|
ref={canvasRef}
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
zIndex: 3,
|
|
filter: 'blur(50px)',
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
|
|
{/* Layer 4: 顶/底渐变压角 */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
zIndex: 4,
|
|
pointerEvents: 'none',
|
|
background:
|
|
`linear-gradient(to bottom, ${fadeColor} 0%, transparent 18%, transparent 82%, ${fadeColor} 100%)`,
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|