feat(layout): 认证页面统一导航栏,含主题切换与退出按钮
新增 Layout 组件作为认证路由的公共布局,提供 sticky 导航栏, 移除各页面重复的顶栏代码。
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
import { Link, Outlet } from 'react-router-dom'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { useThemeStore } from '../stores/theme'
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
const { user, logout } = useAuthStore()
|
||||||
|
const { theme, toggleTheme } = useThemeStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav
|
||||||
|
style={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
background: 'var(--nav-bg)',
|
||||||
|
borderBottom: '1px solid var(--nav-border)',
|
||||||
|
height: 56,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '0 24px',
|
||||||
|
transition: 'background-color 0.3s ease, border-color 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
style={{
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
gen2d
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="btn-secondary"
|
||||||
|
style={{ padding: '6px 10px', fontSize: 16, lineHeight: 1 }}
|
||||||
|
title={theme === 'light' ? '切换到暗色模式' : '切换到浅色模式'}
|
||||||
|
>
|
||||||
|
{theme === 'light' ? '☾' : '☀'}
|
||||||
|
</button>
|
||||||
|
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||||||
|
{user?.username}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={logout}
|
||||||
|
style={{ padding: '6px 16px', fontSize: 13 }}
|
||||||
|
>
|
||||||
|
退出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useTaskStore } from '../stores/task'
|
import { useTaskStore } from '../stores/task'
|
||||||
import { useGenerationStore } from '../stores/generation'
|
import { useGenerationStore } from '../stores/generation'
|
||||||
import GenerateForm from '../components/GenerateForm'
|
import GenerateForm from '../components/GenerateForm'
|
||||||
@@ -46,28 +46,7 @@ export default function GeneratePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
<div className="container" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
||||||
{/* 顶栏 */}
|
<h1 style={{ fontSize: 24, marginBottom: 32 }}>新建生成</h1>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 32,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1 style={{ fontSize: 24 }}>新建生成</h1>
|
|
||||||
<Link
|
|
||||||
to={`/projects/${projectId}`}
|
|
||||||
className="btn-secondary"
|
|
||||||
style={{
|
|
||||||
padding: '8px 20px',
|
|
||||||
fontSize: 13,
|
|
||||||
borderRadius: 'var(--radius)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
返回工程
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 生成表单 */}
|
{/* 生成表单 */}
|
||||||
{status === 'idle' || status === 'submitting' ? (
|
{status === 'idle' || status === 'submitting' ? (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { useProjectStore } from '../stores/project'
|
import { useProjectStore } from '../stores/project'
|
||||||
import { useAuthStore } from '../stores/auth'
|
|
||||||
import { getTasks } from '../api/project'
|
import { getTasks } from '../api/project'
|
||||||
import type { Task } from '../api/types'
|
import type { Task } from '../api/types'
|
||||||
import StyleSelector from '../components/StyleSelector'
|
import StyleSelector from '../components/StyleSelector'
|
||||||
@@ -15,7 +14,6 @@ const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
|||||||
|
|
||||||
export default function ProjectPage() {
|
export default function ProjectPage() {
|
||||||
const { projectId = 'proj-default' } = useParams()
|
const { projectId = 'proj-default' } = useParams()
|
||||||
const { user, logout } = useAuthStore()
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
@@ -45,25 +43,7 @@ export default function ProjectPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
<div className="container" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
||||||
{/* 顶栏 */}
|
<h1 style={{ fontSize: 24, marginBottom: 32 }}>{loading ? '加载中...' : name}</h1>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 32,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1 style={{ fontSize: 24 }}>{loading ? '加载中...' : name}</h1>
|
|
||||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
|
||||||
{user?.username}
|
|
||||||
</span>
|
|
||||||
<button className="btn-secondary" onClick={logout} style={{ padding: '6px 16px', fontSize: 13 }}>
|
|
||||||
退出
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 工程风格 */}
|
{/* 工程风格 */}
|
||||||
<section className="card" style={{ marginBottom: 24 }}>
|
<section className="card" style={{ marginBottom: 24 }}>
|
||||||
|
|||||||
@@ -42,28 +42,7 @@ export default function ResultPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
<div className="container" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
||||||
{/* 顶栏 */}
|
<h1 style={{ fontSize: 24, marginBottom: 32 }}>生成结果</h1>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 32,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1 style={{ fontSize: 24 }}>生成结果</h1>
|
|
||||||
<Link
|
|
||||||
to={`/projects/${projectId}`}
|
|
||||||
className="btn-secondary"
|
|
||||||
style={{
|
|
||||||
padding: '8px 20px',
|
|
||||||
fontSize: 13,
|
|
||||||
borderRadius: 'var(--radius)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
返回工程
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 任务信息 */}
|
{/* 任务信息 */}
|
||||||
<section className="card" style={{ marginBottom: 24 }}>
|
<section className="card" style={{ marginBottom: 24 }}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom'
|
import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import Layout from '../components/Layout'
|
||||||
import LoginPage from '../pages/LoginPage'
|
import LoginPage from '../pages/LoginPage'
|
||||||
import RegisterPage from '../pages/RegisterPage'
|
import RegisterPage from '../pages/RegisterPage'
|
||||||
import ProjectPage from '../pages/ProjectPage'
|
import ProjectPage from '../pages/ProjectPage'
|
||||||
@@ -25,20 +26,25 @@ export const router = createBrowserRouter([
|
|||||||
element: <ProtectedRoute />,
|
element: <ProtectedRoute />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/',
|
element: <Layout />,
|
||||||
element: <Navigate to="/projects/proj-default" replace />,
|
children: [
|
||||||
},
|
{
|
||||||
{
|
path: '/',
|
||||||
path: '/projects/:projectId',
|
element: <Navigate to="/projects/proj-default" replace />,
|
||||||
element: <ProjectPage />,
|
},
|
||||||
},
|
{
|
||||||
{
|
path: '/projects/:projectId',
|
||||||
path: '/projects/:projectId/generate',
|
element: <ProjectPage />,
|
||||||
element: <GeneratePage />,
|
},
|
||||||
},
|
{
|
||||||
{
|
path: '/projects/:projectId/generate',
|
||||||
path: '/projects/:projectId/tasks/:taskId',
|
element: <GeneratePage />,
|
||||||
element: <ResultPage />,
|
},
|
||||||
|
{
|
||||||
|
path: '/projects/:projectId/tasks/:taskId',
|
||||||
|
element: <ResultPage />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user