95e55293c6
后端安全: - DEBUG 默认 True → False - 彻底移除 AUTH_BYPASS 认证绕过 - 验证码不再明文打印到日志 - 上传接口增加大小限制(500MB/20MB/100MB)与魔数校验 - python-jose → PyJWT, 更新 requirements.lock/uv.lock - Bandit 恢复关键规则(B104/B301/B305/B314/B324/B603/B607) - 修复 5 处 try_except_pass, 15 处加 nosec 注释 - 启用 Bandit pre-commit 钩子 前端安全: - 配置完整 CSP 策略 - 收紧 Capabilities(fs:allow-read-file → $RESOURCE/**) - 移除硬编码 devToken - 清理前端 TODO(美家卡智影命名统一) 部署修复: - docker-compose.prod 增加 alembic 迁移步骤 - api + scheduler 增加 Redis 心跳健康检查 - Nginx 添加安全响应头 - Nginx client_max_body_size 100M → 500M - .env.example 补充 UPLOAD_MAX_* 配置与安全注释 其他: - /voice/upload 合并到 /upload/audio - Rust 上传增加文件大小检查 - 清理 Rust 19 处 println! + 前端 21 处 console.info - 修复 VideoCompose.tsx toast 未导入(已有bug)
216 lines
7.1 KiB
TypeScript
216 lines
7.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { createNewProject, useAuthStore } from '../../store';
|
|
import { useNavigation } from '../../contexts/NavigationContext';
|
|
import './Sidebar.css';
|
|
|
|
interface NavItem {
|
|
id: string;
|
|
label: string;
|
|
icon: string;
|
|
children?: { id: string; label: string }[];
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{
|
|
id: 'video-creation',
|
|
label: '视频创作',
|
|
icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z',
|
|
},
|
|
{
|
|
id: 'content-management',
|
|
label: '内容管理',
|
|
icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
|
|
children: [
|
|
{ id: 'voice-material', label: '声音复刻' },
|
|
{ id: 'my-works', label: '我的作品' },
|
|
],
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: '系统设置',
|
|
icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
|
|
children: [
|
|
{ id: 'theme-settings', label: '主题设置' },
|
|
{ id: 'about-us', label: '关于我们' },
|
|
{ id: 'system-update', label: '系统更新' },
|
|
|
|
],
|
|
},
|
|
];
|
|
|
|
interface SidebarProps {
|
|
currentPath: string;
|
|
onNavigate: (path: string) => void;
|
|
}
|
|
|
|
export default function Sidebar({ currentPath, onNavigate }: SidebarProps) {
|
|
const { appEnvironment } = useNavigation();
|
|
const authUser = useAuthStore((s) => s.user);
|
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(
|
|
new Set(['content-management', 'settings'])
|
|
);
|
|
|
|
const toggleExpand = (id: string) => {
|
|
setExpandedItems(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleClick = (item: NavItem) => {
|
|
if (item.children) {
|
|
toggleExpand(item.id);
|
|
// Navigate to first child
|
|
if (!expandedItems.has(item.id)) {
|
|
onNavigate(item.children[0].id);
|
|
}
|
|
} else {
|
|
onNavigate(item.id);
|
|
}
|
|
};
|
|
|
|
const handleNewProject = async (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
await createNewProject();
|
|
onNavigate('video-creation');
|
|
};
|
|
|
|
const isActive = (id: string) => currentPath === id;
|
|
|
|
return (
|
|
<aside className="sidebar">
|
|
<div className="sidebar-header">
|
|
<div className="sidebar-logo">
|
|
<img
|
|
src="/assets/logo.png"
|
|
alt="美家卡智影"
|
|
style={{ width: 28, height: 24, objectFit: 'contain' }}
|
|
/>
|
|
<span className="sidebar-title">美家卡智影</span>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="sidebar-nav">
|
|
{navItems.map(item => (
|
|
<div key={item.id} className="nav-group">
|
|
<button
|
|
className={`nav-item ${isActive(item.id) ? 'active' : ''} ${
|
|
item.children && item.children.some(c => isActive(c.id)) ? 'child-active' : ''
|
|
}`}
|
|
onClick={() => handleClick(item)}
|
|
>
|
|
<svg
|
|
className="nav-icon"
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.8"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d={item.icon} />
|
|
</svg>
|
|
<span className="nav-label">{item.label}</span>
|
|
{item.id === 'video-creation' && (
|
|
<span
|
|
className="nav-new-project"
|
|
onClick={handleNewProject}
|
|
title="新建项目"
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
handleNewProject(e as unknown as React.MouseEvent);
|
|
}
|
|
}}
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="12" y1="8" x2="12" y2="16" />
|
|
<line x1="8" y1="12" x2="16" y2="12" />
|
|
</svg>
|
|
</span>
|
|
)}
|
|
{item.children && (
|
|
<svg
|
|
className={`nav-chevron ${expandedItems.has(item.id) ? 'expanded' : ''}`}
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<polyline points="9 18 15 12 9 6" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
{item.children && expandedItems.has(item.id) && (
|
|
<div className="nav-children">
|
|
{item.children.map(child => (
|
|
<button
|
|
key={child.id}
|
|
className={`nav-child-item ${isActive(child.id) ? 'active' : ''}`}
|
|
onClick={() => onNavigate(child.id)}
|
|
>
|
|
<span>{child.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
{appEnvironment !== 'production' && (
|
|
<div
|
|
style={{
|
|
margin: '0 var(--spacing-md) var(--spacing-sm)',
|
|
padding: 'var(--spacing-xs) var(--spacing-sm)',
|
|
background: 'var(--color-warning)',
|
|
color: '#fff',
|
|
borderRadius: '6px',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
textAlign: 'center',
|
|
letterSpacing: '0.5px',
|
|
}}
|
|
>
|
|
{appEnvironment === 'staging' ? '测试环境' : appEnvironment === 'development' ? '开发环境' : appEnvironment}
|
|
</div>
|
|
)}
|
|
<div className="sidebar-footer">
|
|
<div className="sidebar-user" onClick={() => onNavigate('profile')} title="个人中心">
|
|
<img
|
|
src={authUser?.avatar || '/default-avatar.svg'}
|
|
alt="avatar"
|
|
className="user-avatar"
|
|
style={{ objectFit: 'cover' }}
|
|
/>
|
|
<div className="user-info">
|
|
<span className="user-name">{authUser?.nickname || '用户'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|