feat: 完成TikTok短剧管理后台基础功能开发

本次提交搭建了完整的后台管理系统,包含:
1.  登录认证与权限校验体系
2.  工作台、短剧、用户、角色、人员、日志等全业务模块
3.  响应式布局与通用样式组件
4.  菜单动态过滤与路由权限控制
5.  移除了README中冗余的权限管理菜单项
This commit is contained in:
2026-07-01 15:24:13 +08:00
parent 6722752d5e
commit 51e26025c6
27 changed files with 1364 additions and 1282 deletions

114
src/layout/index.tsx Normal file
View File

@@ -0,0 +1,114 @@
import { useEffect, useMemo, useState } from 'react';
import {
Avatar,
Button,
Dropdown,
Layout,
Menu,
Space,
Typography,
} from '@arco-design/web-react';
import { IconMenuFold, IconMenuUnfold } from '@arco-design/web-react/icon';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import type { ApiResponse, AdminProfile } from '../api/types';
import { API_BASE_URL, TOKEN_KEY, api } from '../api/client';
import { filterMenuItems, findCurrentMenu, menuItems, renderMenuItem } from './menu';
import { PermissionRoute } from './PermissionRoute';
import DashboardPage from '../Dashboard';
import DramasPage from '../Drama';
import UsersPage from '../User';
import RolesPage from '../Role';
import PersonnelPage from '../Personnel';
import ProfilePage from '../Profile';
import LogsPage from '../Log';
import './layout.css';
const { Header, Sider, Content } = Layout;
const { Text } = Typography;
export function AdminLayout() {
const navigate = useNavigate();
const location = useLocation();
const [collapsed, setCollapsed] = useState(false);
const [profile, setProfile] = useState<AdminProfile | null>(null);
const current = useMemo(() => findCurrentMenu(location.pathname), [location.pathname]);
const visibleMenuItems = useMemo(() => filterMenuItems(menuItems, profile), [profile]);
const [openMenuKeys, setOpenMenuKeys] = useState<string[]>(current?.parent ? [current.parent.key] : []);
useEffect(() => {
api.get<ApiResponse<AdminProfile>>('/api/admin/v1/auth/profile')
.then((response) => setProfile(response.data.data))
.catch(() => {
localStorage.removeItem(TOKEN_KEY);
navigate('/login', { replace: true });
});
}, [navigate]);
useEffect(() => {
if (current?.parent && !openMenuKeys.includes(current.parent.key)) {
setOpenMenuKeys((keys) => [...keys, current.parent!.key]);
}
}, [current, openMenuKeys]);
const logout = () => {
localStorage.removeItem(TOKEN_KEY);
navigate('/login', { replace: true });
};
return (
<Layout className="layout">
<Sider className="sidebar" width={220} collapsed={collapsed} collapsible trigger={null}>
<div className="logo-row">
<span className="logo-mark">TK</span>
{!collapsed && <span>TK短剧管理后台</span>}
</div>
<Menu
selectedKeys={[current?.item.key || '/dashboard']}
openKeys={openMenuKeys}
onClickMenuItem={(key) => navigate(key)}
onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)}
>
{visibleMenuItems.map((item) => renderMenuItem(item))}
</Menu>
</Sider>
<Layout>
<Header className="header">
<Space>
<Button icon={collapsed ? <IconMenuUnfold /> : <IconMenuFold />} onClick={() => setCollapsed(!collapsed)} />
<div className="header-title">
<strong>{current?.item.label || '工作台'}</strong>
<span> API {API_BASE_URL}</span>
</div>
</Space>
<Dropdown
position="br"
trigger="click"
droplist={(
<Menu>
<Menu.Item key="profile" onClick={() => navigate('/profile')}></Menu.Item>
<Menu.Item key="logout" onClick={logout}>退</Menu.Item>
</Menu>
)}
>
<button className="user-menu-trigger" type="button">
<Avatar size={28}>{profile?.username?.slice(0, 1).toUpperCase() || 'A'}</Avatar>
<Text>{profile?.nickname || profile?.username || '管理员'}</Text>
</button>
</Dropdown>
</Header>
<Content className="content">
<Routes>
<Route path="/dashboard" element={<DashboardPage profile={profile} />} />
<Route path="/dramas" element={<PermissionRoute profile={profile} requiredPermissions={['drama:read']}><DramasPage /></PermissionRoute>} />
<Route path="/users" element={<PermissionRoute profile={profile} requiredPermissions={['user:read']}><UsersPage /></PermissionRoute>} />
<Route path="/roles" element={<PermissionRoute profile={profile} requiredPermissions={['role:read']}><RolesPage /></PermissionRoute>} />
<Route path="/personnel" element={<PermissionRoute profile={profile} requiredPermissions={['admin-user:read']}><PersonnelPage /></PermissionRoute>} />
<Route path="/profile" element={<PermissionRoute profile={profile}><ProfilePage /></PermissionRoute>} />
<Route path="/logs" element={<PermissionRoute profile={profile} requiredPermissions={['log:read']}><LogsPage /></PermissionRoute>} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Content>
</Layout>
</Layout>
);
}