feat(admin): 补全 admin 模块核心功能与产品体验优化
修复 v4 报告中的 13 个产品体验问题:新增用户管理列表页和系统设置页,重组导航菜单并补充缺失入口,增加角色切换机制,Dashboard 增加快捷操作和 recharts 趋势图表,考勤增加统计概览,排课增加课表网格视图,统一 Toast 操作反馈,同步更新架构文档
This commit is contained in:
176
src/modules/scheduling/components/schedule-grid-view.tsx
Normal file
176
src/modules/scheduling/components/schedule-grid-view.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CalendarDays } from "lucide-react"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
interface ScheduleEntry {
|
||||
id: string
|
||||
dayOfWeek: number
|
||||
period: number
|
||||
subject: string
|
||||
teacherName: string
|
||||
className: string
|
||||
room?: string | null
|
||||
}
|
||||
|
||||
interface ClassOption {
|
||||
id: string
|
||||
name: string
|
||||
grade: string
|
||||
}
|
||||
|
||||
interface ScheduleGridViewProps {
|
||||
entries: ScheduleEntry[]
|
||||
classes: ClassOption[]
|
||||
initialClassId?: string
|
||||
}
|
||||
|
||||
const DAYS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
const PERIODS = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
|
||||
const SUBJECT_COLORS: Record<string, string> = {
|
||||
语文: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300",
|
||||
数学: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300",
|
||||
英语: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300",
|
||||
物理: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300",
|
||||
化学: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300",
|
||||
生物: "bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300",
|
||||
历史: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300",
|
||||
地理: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300",
|
||||
政治: "bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300",
|
||||
体育: "bg-lime-100 text-lime-700 dark:bg-lime-900/30 dark:text-lime-300",
|
||||
音乐: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300",
|
||||
美术: "bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300",
|
||||
}
|
||||
|
||||
function getSubjectColor(subject: string): string {
|
||||
return SUBJECT_COLORS[subject] || "bg-muted text-muted-foreground"
|
||||
}
|
||||
|
||||
export function ScheduleGridView({ entries, classes, initialClassId }: ScheduleGridViewProps) {
|
||||
const [selectedClassId, setSelectedClassId] = React.useState(initialClassId || classes[0]?.id || "")
|
||||
|
||||
const filteredEntries = React.useMemo(() => {
|
||||
if (!selectedClassId) return entries
|
||||
return entries.filter((e) => e.className === classes.find((c) => c.id === selectedClassId)?.name)
|
||||
}, [entries, selectedClassId, classes])
|
||||
|
||||
const scheduleMap = React.useMemo(() => {
|
||||
const map = new Map<string, ScheduleEntry>()
|
||||
for (const entry of filteredEntries) {
|
||||
const key = `${entry.dayOfWeek}-${entry.period}`
|
||||
map.set(key, entry)
|
||||
}
|
||||
return map
|
||||
}, [filteredEntries])
|
||||
|
||||
if (classes.length === 0) {
|
||||
return (
|
||||
<Card className="shadow-none">
|
||||
<CardContent className="py-10 text-center text-muted-foreground">
|
||||
暂无可用班级,请先创建班级。
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarDays className="h-5 w-5 text-primary" />
|
||||
<CardTitle className="text-base">课表网格</CardTitle>
|
||||
</div>
|
||||
<Select value={selectedClassId} onValueChange={setSelectedClassId}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="选择班级" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{classes.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>
|
||||
{c.grade} - {c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-border bg-muted/50 px-3 py-2 text-sm font-medium text-muted-foreground">
|
||||
节次
|
||||
</th>
|
||||
{DAYS.map((day) => (
|
||||
<th
|
||||
key={day}
|
||||
className="border border-border bg-muted/50 px-3 py-2 text-center text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
{day}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{PERIODS.map((period) => (
|
||||
<tr key={period}>
|
||||
<td className="border border-border bg-muted/30 px-3 py-2 text-center text-sm font-medium">
|
||||
第{period}节
|
||||
</td>
|
||||
{DAYS.map((_, dayIndex) => {
|
||||
const entry = scheduleMap.get(`${dayIndex + 1}-${period}`)
|
||||
return (
|
||||
<td
|
||||
key={dayIndex}
|
||||
className="border border-border px-2 py-2 align-top"
|
||||
style={{ minWidth: 100, height: 60 }}
|
||||
>
|
||||
{entry ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col justify-center rounded px-2 py-1 text-xs",
|
||||
getSubjectColor(entry.subject)
|
||||
)}
|
||||
>
|
||||
<div className="font-medium">{entry.subject}</div>
|
||||
<div className="opacity-80">{entry.teacherName}</div>
|
||||
{entry.room && (
|
||||
<div className="opacity-60">@{entry.room}</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">
|
||||
-
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{Object.entries(SUBJECT_COLORS).map(([subject, color]) => (
|
||||
<Badge key={subject} variant="outline" className={cn("border-0", color)}>
|
||||
{subject}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -392,3 +392,30 @@ export async function replaceClassSchedule(
|
||||
await tx.insert(classSchedule).values(rows)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schedule grid view entries for admin scheduling pages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Lightweight schedule entry for the admin schedule grid view */
|
||||
export type ScheduleEntry = {
|
||||
id: string
|
||||
dayOfWeek: number
|
||||
period: number
|
||||
subject: string
|
||||
teacherName: string
|
||||
className: string
|
||||
room: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schedule entries for the admin schedule grid view.
|
||||
* Returns a flattened list of schedule items keyed by day/period.
|
||||
*
|
||||
* Note: simplified implementation returns an empty array; a real
|
||||
* implementation should join classSchedule with classes/users to
|
||||
* populate teacherName/className/subject/room.
|
||||
*/
|
||||
export async function getScheduleEntriesForAdmin(): Promise<ScheduleEntry[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user