diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a4a7fb5..7c67abd 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,7 @@ import { decode } from "js-base64"; import { getConfig } from "./store"; -import { Dayjs } from "dayjs" -import { Attachment, File as FileModel } from "./model"; +import dayjs, { Dayjs } from "dayjs" +import { Assignment, AssignmentDate, Attachment, File as FileModel } from "./model"; export function formatDate(inputDate: string | undefined | null): string { if (!inputDate) { @@ -113,4 +113,29 @@ export function attachmentToFile(attachment: Attachment) { size: attachment.size, mime_class: attachment.mime_class, } as FileModel; +} + +export function getBaseDate(dates: AssignmentDate[]) { + return dates.find(date => date.base); +} + +export function assignmentIsEnded(assignment: Assignment) { + const baseDate = getBaseDate(assignment.all_dates); + const dued = dayjs(baseDate?.due_at).isBefore(dayjs()); + const locked = dayjs(baseDate?.lock_at).isBefore(dayjs()); + return dued || locked; +} + +export function assignmentIsNotUnlocked(assignment: Assignment) { + const baseDate = getBaseDate(assignment.all_dates); + if (!baseDate?.unlock_at) { + return false; + } + const locked = dayjs(baseDate.unlock_at).isAfter(dayjs()); + return locked; +} + +export function assignmentNotNeedSubmit(assignment: Assignment) { + return !assignment.submission || + assignment.submission_types.includes("none") || assignment.submission_types.includes("not_graded"); } \ No newline at end of file diff --git a/src/page/assignments.tsx b/src/page/assignments.tsx index 8482e1b..e11c540 100644 --- a/src/page/assignments.tsx +++ b/src/page/assignments.tsx @@ -2,9 +2,9 @@ import { Avatar, Button, Checkbox, CheckboxProps, Divider, List, Space, Table, T import BasicLayout from "../components/layout"; import useMessage from "antd/es/message/useMessage"; import { useEffect, useState } from "react"; -import { Assignment, AssignmentDate, Attachment, Course, GradeStatus, Submission, User } from "../lib/model"; +import { Assignment, Attachment, Course, GradeStatus, Submission, User } from "../lib/model"; import { invoke } from "@tauri-apps/api"; -import { attachmentToFile, formatDate } from "../lib/utils"; +import { assignmentIsEnded, assignmentNotNeedSubmit, attachmentToFile, formatDate, getBaseDate } from "../lib/utils"; import CourseSelect from "../components/course_select"; import { usePreview } from "../lib/hooks"; import dayjs from "dayjs"; @@ -95,20 +95,6 @@ export default function AssignmentsPage() { } } - const assignmentIsEnded = (assignment: Assignment) => { - const baseDate = getBaseDate(assignment.all_dates); - const dued = dayjs(baseDate?.due_at).isBefore(dayjs()); - const locked = dayjs(baseDate?.lock_at).isBefore(dayjs()); - return dued || locked; - } - - const assignmentNotNeedSubmit = (assignment: Assignment) => { - return !assignment.submission || - assignment.submission_types.includes("none") || assignment.submission_types.includes("not_graded"); - } - - const getBaseDate = (dates: AssignmentDate[]) => dates.find(date => date.base); - const getColumns = () => { const columns = [{ title: '作业名', diff --git a/src/page/qrcode.tsx b/src/page/qrcode.tsx index 740ab90..ccb393e 100644 --- a/src/page/qrcode.tsx +++ b/src/page/qrcode.tsx @@ -1,4 +1,4 @@ -import { Card, Image, Space } from "antd"; +import { Card, Image, List, Space } from "antd"; import BasicLayout from "../components/layout"; import CourseSelect from "../components/course_select"; import { useEffect, useState } from "react"; @@ -62,14 +62,17 @@ export function QRCodePage() { { scanResults.length > 0 && - scanResults.map(scanResult => { - return } - > - - ; - }) + + } + > + + + + }> + } {scanResults.length === 0 && } diff --git a/src/page/submissions.tsx b/src/page/submissions.tsx index d73bba7..378ac2c 100644 --- a/src/page/submissions.tsx +++ b/src/page/submissions.tsx @@ -1,15 +1,16 @@ import { Button, Input, Select, Space, Table, Tag } from "antd"; import BasicLayout from "../components/layout"; import useMessage from "antd/es/message/useMessage"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useEffect, useMemo, useState } from "react"; import { Assignment, Attachment, Course, FileDownloadTask, GradeStatistic, Submission, User } from "../lib/model"; import { invoke } from "@tauri-apps/api"; -import { attachmentToFile, formatDate } from "../lib/utils"; +import { assignmentIsNotUnlocked, attachmentToFile, formatDate } from "../lib/utils"; import CourseSelect from "../components/course_select"; import FileDownloadTable from "../components/file_download_table"; import GradeStatisticChart from "../components/grade_statistic"; import { usePreview } from "../lib/hooks"; import CommentPanel from "../components/comment_panel"; +import { WarningOutlined } from "@ant-design/icons" export default function SubmissionsPage() { const [messageApi, contextHolder] = useMessage(); @@ -24,13 +25,14 @@ export default function SubmissionsPage() { const [downloadTasks, setDownloadTasks] = useState([]); const [selectedAssignment, setSelectedAssignment] = useState(undefined); const [selectedAttachments, setSelectedAttachments] = useState([]); - const usersMap = new Map(users.map(user => ([user.id, user]))); + const usersMap = useMemo(() => new Map(users.map(user => ([user.id, user]))), [users]); const [statistic, setStatistic] = useState(undefined); const [keyword, setKeyword] = useState(""); const [attachmentToComment, setAttachmentToComment] = useState(-1); const [expandedRowKeys, setExpandedRowKeys] = useState([]); const [previewFooter, setPreviewFooter] = useState(undefined); const [commentingWhilePreviewing, setCommentingWhilePreviewing] = useState(false); + const [notSubmitStudents, setNotSubmitStudents] = useState([]); const refreshSubmission = async (studentId: number) => { const submission = await invoke("get_single_course_assignment_submission", { @@ -94,6 +96,12 @@ export default function SubmissionsPage() { setEntries(attachments.map(attachmentToFile)); }, [attachments]); + useEffect(() => { + if (attachments.length > 0) { + setNotSubmitStudents(getNotSubmitStudents()); + } + }, [attachments]) + const validateGrade = (grade: string) => { if (grade.length === 0) { return true; @@ -309,7 +317,6 @@ export default function SubmissionsPage() { const handleAssignmentSelect = (assignmentId: number) => { setStatistic(undefined); - setAttachments([]); setSelectedAttachments([]); let assignment = assignments.find(assignment => assignment.id === assignmentId); if (assignment) { @@ -341,8 +348,35 @@ export default function SubmissionsPage() { } } + const getNotSubmitStudents = () => { + const notSubmitStudentsMap = new Map(); + usersMap.forEach(user => { + notSubmitStudentsMap.set(user.id, user); + }); + attachments.forEach(attachment => { + notSubmitStudentsMap.delete(attachment.user_id); + }); + return [...notSubmitStudentsMap.values()].filter(student => student.name !== "测验学生"); + } + + const getAssignmentTag = (assignment: Assignment) => { + const count = assignment.needs_grading_count ?? 0; + const notUnlocked = assignmentIsNotUnlocked(assignment); + if (notUnlocked) { + return 尚未解锁 + } + if (count === 0) { + return 暂无待批改 + } + + return }>{count}份待批改 + } + const assignmentOptions = assignments.map(assignment => ({ - label: assignment.name, + label: + {assignment.name} + {getAssignmentTag(assignment)} + , value: assignment.id, })); @@ -372,6 +406,16 @@ export default function SubmissionsPage() { selectedAssignment?.points_possible && 满分:{selectedAssignment.points_possible} } + { + attachments.length > 0 && notSubmitStudents.length > 0 && + 未提交学生: {notSubmitStudents.map(s => {s.name})} + + } + { + attachments.length === 0 && + 未提交学生: 暂无任何提交 + + } {statistic && }