video-shuoshan/web/src/pages/TeamAdminLayout.tsx
seaislee1209 e2973284d0
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m20s
feat: 账号安全管控 + 内容资产页 + UI修缮 (v0.9.5 & v0.9.6)
v0.9.5 — 账号安全管控 + 内容资产页:
- 首次登录强制改密(must_change_password + ForceChangePasswordModal)
- 并发会话限制(ActiveSession + SessionJWT认证,可配置桌面/移动端会话数)
- Token生命周期缩短(access 30min, refresh 1天)
- 登录IP记录(LoginRecord模型,为异常检测打基础)
- 内容资产页(超管三级折叠/团队管两级折叠,按需懒加载)

v0.9.6 — UI修缮:
- 侧栏导航排序(内容资产移到用户管理下方)
- 视频网格高度调整(440px,3行+暗示可滚动)
- 秒数单位统一(不再换算为分钟/小时)
- 提示词标签溢出修复 + 弹窗方向自适应

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 12:02:54 +08:00

86 lines
3.7 KiB
TypeScript

import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/auth';
import { useState } from 'react';
import logoImg from '../assets/logo_32.png';
import styles from './AdminLayout.module.css';
const navItems = [
{ path: '/team/dashboard', label: '概览', icon: 'M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z' },
{ path: '/team/members', label: '成员管理', icon: 'M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z' },
{ path: '/team/assets', label: '内容资产', icon: 'M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z' },
];
export function TeamAdminLayout() {
const user = useAuthStore((s) => s.user);
const logout = useAuthStore((s) => s.logout);
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
const handleLogout = () => {
logout();
navigate('/login', { replace: true });
};
return (
<div className={styles.layout}>
<aside className={`${styles.sidebar} ${collapsed ? styles.collapsed : ''}`}>
<div className={styles.sidebarHeader}>
<div className={styles.logo}>
<img src={logoImg} alt="AirDrama" width="24" height="24" />
{!collapsed && <span className={styles.logoText}></span>}
</div>
<button className={styles.collapseBtn} onClick={() => setCollapsed(!collapsed)}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="var(--color-text-secondary)">
{collapsed ? (
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
) : (
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
)}
</svg>
</button>
</div>
<nav className={styles.nav}>
<button className={styles.navItem} onClick={() => navigate('/app')} style={{ border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left', width: '100%' }}>
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
{!collapsed && <span></span>}
</button>
<div className={styles.navDivider} />
{navItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`${styles.navItem} ${isActive ? styles.navItemActive : ''}`
}
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d={item.icon} />
</svg>
{!collapsed && <span>{item.label}</span>}
</NavLink>
))}
</nav>
<div className={styles.sidebarFooter}>
<div className={styles.userInfo}>
<div className={styles.userAvatar}>{user?.username.charAt(0).toUpperCase()}</div>
{!collapsed && (
<div className={styles.userMeta}>
<span className={styles.userName}>{user?.username}</span>
<button className={styles.logoutLink} onClick={handleLogout}>退</button>
</div>
)}
</div>
</div>
</aside>
<main className={`${styles.content} ${collapsed ? styles.contentExpanded : ''}`}>
<Outlet />
</main>
</div>
);
}