From 2910dd6ccfcd9d93a0cf982eea6679cdc8f5a579 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Wed, 25 Mar 2026 20:44:27 +0800 Subject: [PATCH] Enhance Sidebar with new menu items for materials management and update TypeScript configuration. Add subjects and materials APIs to the API index. Introduce new routes for materials and subjects with appropriate guards. --- src/api/index.ts | 2 + src/api/materials.ts | 135 ++++++ src/components/Layout/Sidebar.tsx | 153 +++++-- src/pages/MaterialSubjects.tsx | 420 +++++++++++++++++++ src/pages/Materials.tsx | 665 ++++++++++++++++++++++++++++++ src/router/index.tsx | 22 + tsconfig.app.json | 2 +- 7 files changed, 1363 insertions(+), 36 deletions(-) create mode 100644 src/api/materials.ts create mode 100644 src/pages/MaterialSubjects.tsx create mode 100644 src/pages/Materials.tsx diff --git a/src/api/index.ts b/src/api/index.ts index feb97f0..8598609 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -12,3 +12,5 @@ export { groupsApi } from './groups' export type { GroupQueryParams, GroupDetail, GroupMember } from './groups' export { dashboardApi } from './dashboard' export type { DashboardStatsData, ActivityTrendData, ContentStatsData, PendingContent } from './dashboard' +export { subjectsApi, materialsApi } from './materials' +export type { MaterialSubject, MaterialFile, MaterialFileType, SubjectUpsertRequest, MaterialCreateRequest, MaterialUpdateRequest, MaterialQueryParams } from './materials' diff --git a/src/api/materials.ts b/src/api/materials.ts new file mode 100644 index 0000000..3726485 --- /dev/null +++ b/src/api/materials.ts @@ -0,0 +1,135 @@ +import apiClient from './client' +import type { ApiResponse, PaginatedResponse } from '@/types' + +// ===================================================== +// 类型定义 +// ===================================================== + +// 文件类型 +export type MaterialFileType = 'pdf' | 'word' | 'ppt' + +// 学科 +export interface MaterialSubject { + id: string + name: string + icon: string + color: string + description: string + sort_order: number + is_active: boolean + material_count: number + created_at: string + updated_at: string +} + +// 学习资料 +export interface MaterialFile { + id: string + subject_id: string + title: string + description: string + file_type: MaterialFileType + file_size: number + file_url: string + file_name: string + download_count: number + author_id: string + tags: string[] + status: 'active' | 'inactive' + created_at: string + updated_at: string + subject?: MaterialSubject +} + +// 学科创建/更新请求 +export interface SubjectUpsertRequest { + name: string + icon?: string + color?: string + description?: string + sort_order?: number + is_active?: boolean +} + +// 资料创建请求 +export interface MaterialCreateRequest { + subject_id: string + title: string + description?: string + file_type: MaterialFileType + file_size?: number + file_url: string + file_name?: string + tags?: string[] +} + +// 资料更新请求 +export interface MaterialUpdateRequest { + subject_id?: string + title?: string + description?: string + file_type?: MaterialFileType + file_size?: number + file_url?: string + file_name?: string + tags?: string[] + status?: 'active' | 'inactive' +} + +// 资料查询参数 +export interface MaterialQueryParams { + page?: number + page_size?: number + subject_id?: string + file_type?: MaterialFileType + status?: string + keyword?: string +} + +// ===================================================== +// API 接口 +// ===================================================== + +// 学科管理 API +export const subjectsApi = { + // 获取学科列表 + getSubjects: () => + apiClient.get>('/admin/materials/subjects'), + + // 创建学科 + createSubject: (data: SubjectUpsertRequest) => + apiClient.post>('/admin/materials/subjects', data), + + // 更新学科 + updateSubject: (id: string, data: SubjectUpsertRequest) => + apiClient.put>(`/admin/materials/subjects/${id}`, data), + + // 删除学科 + deleteSubject: (id: string) => + apiClient.delete>(`/admin/materials/subjects/${id}`), +} + +// 学习资料管理 API +export const materialsApi = { + // 获取资料列表 + getMaterials: (params: MaterialQueryParams) => + apiClient.get>>('/admin/materials', { params }), + + // 获取资料详情 + getMaterialById: (id: string) => + apiClient.get>(`/admin/materials/${id}`), + + // 创建资料 + createMaterial: (data: MaterialCreateRequest) => + apiClient.post>('/admin/materials', data), + + // 更新资料 + updateMaterial: (id: string, data: MaterialUpdateRequest) => + apiClient.put>(`/admin/materials/${id}`, data), + + // 删除资料 + deleteMaterial: (id: string) => + apiClient.delete>(`/admin/materials/${id}`), +} + +export default materialsApi \ No newline at end of file diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index 9692000..7ad97a0 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -10,15 +10,27 @@ import { Users as UsersIcon, ChevronLeft, ChevronRight, + FolderOpen, + BookOpen, + ChevronDown, } from 'lucide-react' import { Button } from '@/components/ui/button' +import { useState } from 'react' interface SidebarProps { collapsed: boolean onCollapse: (collapsed: boolean) => void } -const menuItems = [ +interface MenuItem { + title: string + path?: string + icon: React.ComponentType<{ className?: string }> + roles: string[] + children?: MenuItem[] +} + +const menuItems: MenuItem[] = [ { title: 'Dashboard', path: '/dashboard', @@ -55,23 +67,112 @@ const menuItems = [ icon: UsersIcon, roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.MODERATOR], }, + { + title: '学习资料', + icon: FolderOpen, + roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN], + children: [ + { + title: '学科管理', + path: '/materials/subjects', + icon: BookOpen, + roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN], + }, + { + title: '资料管理', + path: '/materials', + icon: FileText, + roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN], + }, + ], + }, ] export default function Sidebar({ collapsed, onCollapse }: SidebarProps) { const { hasAnyRole, roles } = useAuthStore() - - // DEBUG: 输出角色信息 - console.log('[Sidebar DEBUG] roles:', roles) - console.log('[Sidebar DEBUG] ROLES constants:', ROLES) - + const [expandedItems, setExpandedItems] = useState(['学习资料']) + + // 切换展开状态 + const toggleExpand = (title: string) => { + setExpandedItems((prev) => + prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title] + ) + } + // 过滤出当前用户有权访问的菜单 - const visibleMenuItems = menuItems.filter(item => { - const hasAccess = hasAnyRole(item.roles) - console.log(`[Sidebar DEBUG] Menu "${item.title}" - required roles:`, item.roles, '- hasAccess:', hasAccess) - return hasAccess - }) - - console.log('[Sidebar DEBUG] visibleMenuItems count:', visibleMenuItems.length) + const filterMenuItems = (items: MenuItem[]): MenuItem[] => { + return items + .filter((item) => hasAnyRole(item.roles)) + .map((item) => ({ + ...item, + children: item.children ? filterMenuItems(item.children) : undefined, + })) + .filter((item) => item.path || (item.children && item.children.length > 0)) + } + + const visibleMenuItems = filterMenuItems(menuItems) + + // 渲染单个菜单项 + const renderMenuItem = (item: MenuItem) => { + const Icon = item.icon + + // 如果有子菜单 + if (item.children && item.children.length > 0) { + const isExpanded = expandedItems.includes(item.title) + return ( +
+ + {!collapsed && isExpanded && ( +
+ {item.children.map((child) => renderMenuItem(child))} +
+ )} +
+ ) + } + + // 普通菜单项 + return ( + + cn( + 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors', + isActive + ? 'bg-primary text-primary-foreground' + : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground', + collapsed && 'justify-center px-2' + ) + } + title={collapsed ? item.title : undefined} + > + + {!collapsed && {item.title}} + + ) + } return ( ) -} +} \ No newline at end of file diff --git a/src/pages/MaterialSubjects.tsx b/src/pages/MaterialSubjects.tsx new file mode 100644 index 0000000..8a00075 --- /dev/null +++ b/src/pages/MaterialSubjects.tsx @@ -0,0 +1,420 @@ +import { useState, useEffect, useCallback } from 'react' +import { subjectsApi, type MaterialSubject, type SubjectUpsertRequest } from '@/api' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { PaginationNavigator } from '@/components/ui/pagination' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Loader2, Plus, Pencil, Trash2, Search, RefreshCw, BookOpen } from 'lucide-react' +import { format } from 'date-fns' + +// 颜色预设选项 +const COLOR_PRESETS = [ + { name: '红色', value: '#ef4444' }, + { name: '橙色', value: '#f97316' }, + { name: '黄色', value: '#eab308' }, + { name: '绿色', value: '#22c55e' }, + { name: '蓝色', value: '#3b82f6' }, + { name: '紫色', value: '#a855f7' }, + { name: '粉色', value: '#ec4899' }, + { name: '青色', value: '#06b6d4' }, +] + +// 图标预设选项 +const ICON_PRESETS = [ + '📚', '📖', '📐', '📏', '🔢', '🔤', '🌍', '⚗️', + '🧪', '🧬', '💻', '📊', '📈', '🎨', '🎵', '⚽', +] + +export default function MaterialSubjects() { + const [subjects, setSubjects] = useState([]) + const [loading, setLoading] = useState(true) + const [page, setPage] = useState(1) + const pageSize = 10 + + // 创建/编辑对话框 + const [dialogOpen, setDialogOpen] = useState(false) + const [editingSubject, setEditingSubject] = useState(null) + const [formData, setFormData] = useState({ + name: '', + icon: '📚', + color: '#3b82f6', + description: '', + sort_order: 0, + is_active: true, + }) + const [dialogLoading, setDialogLoading] = useState(false) + + // 删除确认对话框 + const [deleteOpen, setDeleteOpen] = useState(false) + const [deletingSubject, setDeletingSubject] = useState(null) + const [deleteLoading, setDeleteLoading] = useState(false) + + // 加载学科列表 + const loadSubjects = useCallback(async () => { + setLoading(true) + try { + const response = await subjectsApi.getSubjects() + setSubjects(response.data.data || []) + } catch (error) { + console.error('Failed to load subjects:', error) + setSubjects([]) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + loadSubjects() + }, [loadSubjects]) + + // 打开创建对话框 + const handleOpenCreate = () => { + setEditingSubject(null) + setFormData({ + name: '', + icon: '📚', + color: '#3b82f6', + description: '', + sort_order: subjects.length, + is_active: true, + }) + setDialogOpen(true) + } + + // 打开编辑对话框 + const handleOpenEdit = (subject: MaterialSubject) => { + setEditingSubject(subject) + setFormData({ + name: subject.name, + icon: subject.icon, + color: subject.color, + description: subject.description, + sort_order: subject.sort_order, + is_active: subject.is_active, + }) + setDialogOpen(true) + } + + // 保存学科 + const handleSave = async () => { + if (!formData.name?.trim()) return + setDialogLoading(true) + try { + if (editingSubject) { + await subjectsApi.updateSubject(editingSubject.id, formData) + } else { + await subjectsApi.createSubject(formData) + } + setDialogOpen(false) + loadSubjects() + } catch (error) { + console.error('Failed to save subject:', error) + } finally { + setDialogLoading(false) + } + } + + // 打开删除确认 + const handleOpenDelete = (subject: MaterialSubject) => { + setDeletingSubject(subject) + setDeleteOpen(true) + } + + // 确认删除 + const handleConfirmDelete = async () => { + if (!deletingSubject) return + setDeleteLoading(true) + try { + await subjectsApi.deleteSubject(deletingSubject.id) + setDeleteOpen(false) + loadSubjects() + } catch (error: any) { + console.error('Failed to delete subject:', error) + alert(error?.response?.data?.message || '删除失败,该学科下可能存在资料') + } finally { + setDeleteLoading(false) + } + } + + // 格式化日期 + const formatDate = (dateStr: string) => { + try { + return format(new Date(dateStr), 'yyyy-MM-dd HH:mm') + } catch { + return dateStr + } + } + + // 分页后的数据 + const paginatedSubjects = subjects.slice((page - 1) * pageSize, page * pageSize) + const totalPages = Math.ceil(subjects.length / pageSize) + + return ( +
+
+

学科管理

+

管理学习资料的学科分类

+
+ + + +
+ 学科列表 + 共 {subjects.length} 个学科 +
+ +
+ + + + + 排序 + 图标 + 名称 + 颜色 + 描述 + 资料数 + 状态 + 创建时间 + 操作 + + + + {loading ? ( + + + +

加载中...

+
+
+ ) : paginatedSubjects.length === 0 ? ( + + + 暂无数据 + + + ) : ( + paginatedSubjects.map((subject) => ( + + {subject.sort_order} + + {subject.icon} + + {subject.name} + +
+ + + {subject.description || '-'} + + + {subject.material_count} + + + + {subject.is_active ? '启用' : '禁用'} + + + + {formatDate(subject.created_at)} + + +
+ + +
+
+ + )) + )} + +
+ + {totalPages > 1 && ( +
+ +
+ )} +
+
+ + {/* 创建/编辑对话框 */} + + + + + {editingSubject ? '编辑学科' : '添加学科'} + + + {editingSubject ? '修改学科信息' : '创建新的学科分类'} + + +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="如:高等数学" + /> +
+ +
+ +
+ {ICON_PRESETS.map((icon) => ( + + ))} +
+
+ +
+ +
+ {COLOR_PRESETS.map((color) => ( +
+
+ +
+ +