diff --git a/apps/web/index.html b/apps/web/index.html index 5e3836a..da4e35e 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -1,10 +1,11 @@ - + - + + - web + TodoList
diff --git a/apps/web/public/favicon.png b/apps/web/public/favicon.png new file mode 100644 index 0000000..0f8c608 Binary files /dev/null and b/apps/web/public/favicon.png differ diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 233d2f5..c77f482 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,6 +1,22 @@ -import { useState } from "react"; -import { Navigate, Route, Routes, useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import type { LucideIcon } from "lucide-react"; +import { + Bell, + ChevronLeft, + ChevronRight, + LayoutDashboard, + ListTodo, + LogOut, + Menu, + Moon, + Settings, + Sparkles, + Sun, + X +} from "lucide-react"; +import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; import { EmailLoginPage } from "@/pages/email-login-page"; import { OAuthCallbackPage } from "@/pages/oauth-callback-page"; import { TodoShellPage } from "@/pages/todo-shell-page"; @@ -11,6 +27,26 @@ import { saveSession, type WebSession } from "@/services/session-storage"; +import { + applyThemeMode, + loadThemeMode, + saveThemeMode, + type ThemeMode +} from "@/services/theme-storage"; + +type SidebarItem = { + key: string; + label: string; + icon: LucideIcon; +}; + +const SIDEBAR_ITEMS: SidebarItem[] = [ + { key: "dashboard", label: "概览面板", icon: LayoutDashboard }, + { key: "todo", label: "待办事项", icon: ListTodo }, + { key: "ai", label: "AI 建议", icon: Sparkles }, + { key: "notice", label: "提醒中心", icon: Bell }, + { key: "settings", label: "系统设置", icon: Settings } +]; function toWebSession(payload: EmailLoginResult): WebSession { return { @@ -26,7 +62,19 @@ function toWebSession(payload: EmailLoginResult): WebSession { function App() { const [session, setSession] = useState(() => loadSession()); const [loggingOut, setLoggingOut] = useState(false); + const [themeMode, setThemeMode] = useState(() => loadThemeMode()); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const navigate = useNavigate(); + const location = useLocation(); + + const isAuthPage = + location.pathname === "/login/email" || location.pathname.startsWith("/auth/callback/"); + + useEffect(() => { + applyThemeMode(themeMode); + saveThemeMode(themeMode); + }, [themeMode]); async function handleLogout(): Promise { if (!session || loggingOut) { @@ -37,75 +85,232 @@ function App() { setLoggingOut(true); await revokeRefreshToken(session.refreshToken); } catch { - // 登出流程以本地会话清理为最终兜底,避免页面卡在登录态。 + // 无论接口成功与否,都要清理本地会话,避免页面卡在登录态。 } finally { clearSession(); setSession(null); setLoggingOut(false); + setMobileSidebarOpen(false); navigate("/login/email", { replace: true }); } } - return ( -
-
-
-
-
- TodoList + function handleToggleTheme(): void { + setThemeMode((currentTheme) => (currentTheme === "dark" ? "light" : "dark")); + } + + function handleLoginSuccess(payload: EmailLoginResult): void { + const nextSession = toWebSession(payload); + saveSession(nextSession); + setSession(nextSession); + setMobileSidebarOpen(false); + navigate("/", { replace: true }); + } + + function handleBootstrapSession(nextSession: WebSession): void { + setSession(nextSession); + setMobileSidebarOpen(false); + } + + function renderSidebarContent(options: { collapsed: boolean; mobile: boolean }) { + const { collapsed, mobile } = options; + + return ( +
+ {mobile ? ( +
+
- {session ? ( -
- {session.user.email} - -
- ) : ( - 未登录 - )} + ) : null} + +
+ +
+ +
+ + + +
+
+ ); + } + + if (isAuthPage) { + return ( +
+
+
+ + } + /> + } + /> + } /> + +
+
+
+ ); + } + + return ( +
+
+
+
+ + TodoList + TodoList +
+ + {session ? session.user.email : "未登录"} +
-
- - { - const nextSession = toWebSession(payload); - saveSession(nextSession); - setSession(nextSession); - navigate("/"); - }} - /> - } - /> - { - setSession(nextSession); - }} - /> - } - /> - : - } - /> - } /> - -
+ + {mobileSidebarOpen ? ( + + + +
+
+
+ + + ) : ( + + ) + } + /> + } + /> + +
+
+
+
); } diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 3239bed..8faabac 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1,21 +1,77 @@ -@import "@fontsource-variable/geist"; +@import "@fontsource-variable/geist"; @tailwind base; @tailwind components; @tailwind utilities; +html { + height: 100%; + font-size: 120%; +} + :root { - --radius: 0.625rem; - --background: #f6f8f7; - --foreground: #122117; - --primary: #0a7a5a; - --primary-foreground: #ffffff; - --border: #d7e2db; + --radius: 0.9rem; + --background: 246 100% 98%; + --foreground: 248 40% 18%; + --card: 0 0% 100%; + --border: 248 53% 89%; + --input: 248 48% 84%; + --ring: 246 53% 53%; + --primary: 246 53% 53%; + --primary-foreground: 0 0% 100%; + --secondary: 250 70% 94%; + --secondary-foreground: 248 40% 25%; + --muted: 249 78% 95%; + --muted-foreground: 248 17% 45%; + --accent: 173 56% 66%; + --accent-foreground: 248 40% 25%; + --destructive: 343 40% 50%; + --destructive-foreground: 0 0% 100%; + color-scheme: light; font-family: "Geist Variable", "Noto Sans SC", sans-serif; } +.dark { + --background: 248 46% 10%; + --foreground: 240 44% 96%; + --card: 249 41% 15%; + --border: 249 24% 30%; + --input: 249 23% 28%; + --ring: 246 76% 68%; + --primary: 246 76% 68%; + --primary-foreground: 248 43% 12%; + --secondary: 249 30% 24%; + --secondary-foreground: 240 38% 92%; + --muted: 249 29% 20%; + --muted-foreground: 247 16% 74%; + --accent: 173 52% 58%; + --accent-foreground: 248 43% 12%; + --destructive: 343 64% 58%; + --destructive-foreground: 0 0% 100%; + color-scheme: dark; +} + body { margin: 0; - min-height: 100vh; - background: var(--background); - color: var(--foreground); + height: 100%; + overflow: hidden; + background: + radial-gradient( + 1100px 620px at -12% -25%, + hsl(var(--primary) / 0.2), + hsl(var(--primary) / 0) 60% + ), + radial-gradient( + 900px 540px at 112% -14%, + hsl(var(--accent) / 0.35), + hsl(var(--accent) / 0) 58% + ), + hsl(var(--background)); + color: hsl(var(--foreground)); + transition: + background-color 220ms ease, + color 220ms ease; +} + +#root { + height: 100%; } diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 85a16d5..366221b 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import "./index.css"; import App from "./App.tsx"; +import { applyThemeMode, loadThemeMode } from "@/services/theme-storage"; + +applyThemeMode(loadThemeMode()); createRoot(document.getElementById("root")!).render( diff --git a/apps/web/src/pages/email-login-page.tsx b/apps/web/src/pages/email-login-page.tsx index fe17509..7da5a46 100644 --- a/apps/web/src/pages/email-login-page.tsx +++ b/apps/web/src/pages/email-login-page.tsx @@ -84,31 +84,39 @@ export function EmailLoginPage({ onLoginSuccess }: EmailLoginPageProps) { } return ( -
-

邮箱验证码登录

-

- 输入邮箱后获取验证码,再完成登录。你也可以直接使用第三方账号登录。 -

+
+
+ TodoList +
+

邮箱验证码登录

+
+
-
); } diff --git a/apps/web/src/pages/oauth-callback-page.tsx b/apps/web/src/pages/oauth-callback-page.tsx index f4ae5f3..e369704 100644 --- a/apps/web/src/pages/oauth-callback-page.tsx +++ b/apps/web/src/pages/oauth-callback-page.tsx @@ -49,13 +49,16 @@ export function OAuthCallbackPage({ onBootstrapSession }: OAuthCallbackPageProps } return ( -
-

OAuth 回调处理中

-

+

+
+ TodoList +

OAuth 回调处理中

+
+

{parseResult.ok ? "已收到回调参数,点击继续进入工作台。" : parseResult.reason}