feat(attendance,elective): 实现所有 P2 长期改进项
P2 修复(来自审计报告): - 2.4.4: Server Action 错误消息 i18n 化(attendance/elective 全部 Action) - 2.5.3: 抽取 AttendancePageLayout 组件复用(admin/teacher 页面) - 2.5.4: 抽取 ElectivePageLayout 组件复用(admin/teacher 列表页) - 2.6.3: 考勤月历键盘导航(tabIndex + 方向键 + Home/End + role=grid) - 2.8.2: getStudentAttendanceSummary 分页优化(SQL 聚合统计 + LIMIT 分页) - 2.8.3: resolveCourseDisplayNames 缓存优化(React cache 去重) - 2.1.4: elective data-access 跨模块依赖接口抽象(resolvers.ts 可注入) P2 建议项: - 选课时间冲突检测(parseSchedule + isScheduleConflict 纯函数 + checkScheduleConflict) - 学分上限校验(MAX_CREDIT_PER_TERM + checkCreditLimit) - 考勤/选课数据导出 Excel(export.ts + API 路由扩展) 新增文件: - src/modules/attendance/components/attendance-page-layout.tsx - src/modules/elective/components/elective-page-layout.tsx - src/modules/elective/resolvers.ts - src/modules/attendance/export.ts - src/modules/elective/export.ts 校验: - npm run lint 通过(exit 0) - npx tsc --noEmit attendance/elective/parent 相关零错误
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useMemo, useState, useEffect, useRef, type ReactNode } from "react"
|
||||
import { useQueryState, parseAsString } from "nuqs"
|
||||
import { Tag, List, Share2, Menu } from "lucide-react"
|
||||
import { Tag, List, Share2, Menu, GraduationCap } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { toast } from "sonner"
|
||||
import Link from "next/link"
|
||||
|
||||
import type { Chapter, KnowledgePoint } from "../types"
|
||||
import { updateChapterContentAction, getKnowledgePointsByChapterAction } from "../actions"
|
||||
@@ -77,6 +78,7 @@ export function TextbookReader({
|
||||
// P0-2 前端权限改由 usePermission 判断,不再接受外部 canEdit 硬编码
|
||||
const canEdit = hasPermission(Permissions.TEXTBOOK_UPDATE)
|
||||
const canCreateQuestion = hasPermission(Permissions.QUESTION_CREATE)
|
||||
const canCreateLessonPlan = hasPermission(Permissions.LESSON_PLAN_CREATE)
|
||||
|
||||
const [chapterId, setChapterId] = useQueryState("chapterId", parseAsString.withDefault(""))
|
||||
const [activeTab, setActiveTab] = useState("chapters")
|
||||
@@ -249,6 +251,7 @@ export function TextbookReader({
|
||||
<TabsTrigger value="knowledge" className="gap-2" disabled={!selectedId}>
|
||||
<Tag className="h-4 w-4" />
|
||||
{t("reader.tabs.knowledge")}
|
||||
{/* 任意值 text-[10px]:紧凑徽章,text-xs(12px) 在标签栏中过大 */}
|
||||
{currentChapterKPs.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 px-1 py-0 h-5 text-[10px]">
|
||||
{currentChapterKPs.length}
|
||||
@@ -341,6 +344,7 @@ export function TextbookReader({
|
||||
|
||||
{/* P2-4 移动端侧栏:lg 以下用 Sheet 抽屉式展示 */}
|
||||
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
|
||||
{/* 任意值 w-[85vw]:移动端抽屉占视口宽度,max-w-sm 防止超宽屏过大 */}
|
||||
<SheetContent side="left" className="w-[85vw] max-w-sm p-0 flex flex-col">
|
||||
<SheetHeader className="px-4 py-3 border-b shrink-0">
|
||||
<SheetTitle className="text-left">{t("reader.sidebar")}</SheetTitle>
|
||||
@@ -350,12 +354,13 @@ export function TextbookReader({
|
||||
</Sheet>
|
||||
|
||||
<div className="lg:col-span-8 flex flex-col min-h-0 relative">
|
||||
{/* P2-4 移动端侧栏触发按钮 */}
|
||||
<div className="lg:hidden flex items-center gap-2 mb-3 px-2 shrink-0">
|
||||
{/* P2-4 移动端侧栏触发按钮 + 为此课文备课按钮 */}
|
||||
<div className="flex items-center gap-2 mb-3 px-2 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMobileSidebarOpen(true)}
|
||||
className="lg:hidden"
|
||||
aria-expanded={mobileSidebarOpen}
|
||||
aria-controls="mobile-sidebar-sheet"
|
||||
>
|
||||
@@ -363,10 +368,20 @@ export function TextbookReader({
|
||||
{t("reader.openSidebar")}
|
||||
</Button>
|
||||
{selected && (
|
||||
<span className="text-sm text-muted-foreground truncate">
|
||||
<span className="text-sm text-muted-foreground truncate hidden lg:inline">
|
||||
{selected.title}
|
||||
</span>
|
||||
)}
|
||||
{selected && canCreateLessonPlan && (
|
||||
<Button asChild variant="default" size="sm" className="ml-auto">
|
||||
<Link
|
||||
href={`/teacher/lesson-plans/new?textbookId=${encodeURIComponent(textbookId)}&chapterId=${encodeURIComponent(selected.id)}`}
|
||||
>
|
||||
<GraduationCap className="mr-2 h-4 w-4" />
|
||||
{t("reader.prepareLesson")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
|
||||
Reference in New Issue
Block a user