feat: frontend skeleton done

This commit is contained in:
simonkoson
2026-05-15 09:46:42 +08:00
parent 2bee0ce8c8
commit d29b1bb394
36 changed files with 5008 additions and 0 deletions
@@ -0,0 +1,21 @@
import { useEffect } from 'react'
import { Navigate, Outlet } from 'react-router-dom'
import useAuthStore from '../../stores/authStore'
function AuthGuard() {
const { user, fetchMe } = useAuthStore()
useEffect(() => {
if (user === null) {
fetchMe()
}
}, [user, fetchMe])
if (user === null) {
return null
}
return user ? <Outlet /> : <Navigate to="/login" replace />
}
export default AuthGuard
@@ -0,0 +1,35 @@
.app-layout {
min-height: 100vh;
}
.app-sider {
background: #fff !important;
border-right: 1px solid #f0f0f0;
position: fixed !important;
left: 0;
top: 0;
bottom: 0;
overflow-y: auto;
}
.app-main {
margin-left: 220px;
min-height: 100vh;
}
.app-header {
background: #fff !important;
border-bottom: 1px solid #f0f0f0;
padding: 0 24px !important;
height: 64px;
line-height: 64px;
position: sticky;
top: 0;
z-index: 100;
}
.app-content {
padding: 24px;
background: var(--color-bg-cream);
min-height: calc(100vh - 64px);
}
@@ -0,0 +1,27 @@
import { Layout } from 'antd'
import { Outlet } from 'react-router-dom'
import SideNav from './SideNav'
import TopBar from './TopBar'
import './AppLayout.css'
const { Sider, Header, Content } = Layout
function AppLayout() {
return (
<Layout className="app-layout">
<Sider width={220} className="app-sider">
<SideNav />
</Sider>
<Layout className="app-main">
<Header className="app-header">
<TopBar />
</Header>
<Content className="app-content">
<Outlet />
</Content>
</Layout>
</Layout>
)
}
export default AppLayout
@@ -0,0 +1,38 @@
.side-nav {
padding: 16px 0;
height: 100%;
}
.side-nav-logo {
padding: 16px 24px 24px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 8px;
}
.logo-text {
display: block;
font-size: 18px;
font-weight: 600;
color: var(--color-primary-green);
}
.logo-sub {
display: block;
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
.side-nav-menu {
border-right: none !important;
}
.side-nav-menu .ant-menu-item {
margin: 4px 8px;
border-radius: 8px;
}
.side-nav-menu .ant-menu-item-selected {
background: var(--color-accent-green) !important;
color: var(--color-primary-green) !important;
}
@@ -0,0 +1,42 @@
import { Menu } from 'antd'
import { useNavigate, useLocation } from 'react-router-dom'
import {
DashboardOutlined,
FileTextOutlined,
BookOutlined,
SyncOutlined,
UserOutlined,
TeamOutlined,
} from '@ant-design/icons'
const menuItems = [
{ key: '/dashboard', icon: <DashboardOutlined />, label: '仪表盘' },
{ key: '/tps', icon: <FileTextOutlined />, label: 'TPS 选题策划' },
{ key: '/knowledge', icon: <BookOutlined />, label: '知识库' },
{ key: '/doco', icon: <SyncOutlined />, label: '文稿对齐' },
{ key: '/editor-home', icon: <UserOutlined />, label: '个人首页' },
{ key: '/users', icon: <TeamOutlined />, label: '用户管理' },
]
function SideNav() {
const navigate = useNavigate()
const location = useLocation()
return (
<div className="side-nav">
<div className="side-nav-logo">
<span className="logo-text">军事科技</span>
<span className="logo-sub">TPS 工作台</span>
</div>
<Menu
mode="inline"
selectedKeys={[location.pathname]}
items={menuItems}
onClick={({ key }) => navigate(key)}
className="side-nav-menu"
/>
</div>
)
}
export default SideNav
+27
View File
@@ -0,0 +1,27 @@
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.top-bar-search {
flex: 1;
max-width: 400px;
}
.top-bar-actions {
display: flex;
align-items: center;
gap: 16px;
}
.top-bar-icon {
font-size: 18px;
color: var(--color-text-secondary);
cursor: pointer;
}
.top-bar-avatar {
cursor: pointer;
}
+38
View File
@@ -0,0 +1,38 @@
import { Input, Dropdown, Avatar } from 'antd'
import { SearchOutlined, BellOutlined, LogoutOutlined } from '@ant-design/icons'
import useAuthStore from '../../stores/authStore'
import './TopBar.css'
function TopBar() {
const { user, logout } = useAuthStore()
const items = [
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出',
onClick: () => logout(),
},
]
const displayName = user?.display_name || user?.username || '未知用户'
const initials = displayName.slice(0, 1).toUpperCase()
return (
<div className="top-bar">
<div className="top-bar-search">
<Input placeholder="搜索..." prefix={<SearchOutlined />} />
</div>
<div className="top-bar-actions">
<BellOutlined className="top-bar-icon" />
<Dropdown menu={{ items }} placement="bottomRight">
<Avatar className="top-bar-avatar" style={{ backgroundColor: 'var(--color-primary-green)' }}>
{initials}
</Avatar>
</Dropdown>
</div>
</div>
)
}
export default TopBar