diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 625c8e5..86710f7 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react" import { Card, Space, Button, Tag, Input, Select, Modal, Drawer, message, InputNumber, Divider } from "antd" -import { SearchOutlined, DeleteOutlined } from "@ant-design/icons" +import { SearchOutlined, DeleteOutlined, UndoOutlined } from "@ant-design/icons" import { useTranslation } from "react-i18next" import { match, pinyin } from "pinyin-pro" import { getAvatarFromExtraJson } from "../utils/studentAvatar" @@ -23,6 +23,17 @@ interface reason { category: string } +interface scoreEvent { + id: number + uuid: string + student_name: string + reason_content: string + delta: number + val_prev: number + val_curr: number + event_time: string +} + type SortType = "alphabet" | "surname" | "score" type LayoutType = "grouped" | "squareGrid" type SearchKeyboardLayout = "t9" | "qwerty26" @@ -82,6 +93,8 @@ export const Home: React.FC = ({ canEdit, isPortraitMode = false }) = const [customScore, setCustomScore] = useState(undefined) const [reasonContent, setReasonContent] = useState("") const [submitLoading, setSubmitLoading] = useState(false) + const [undoLoading, setUndoLoading] = useState(false) + const [latestEvent, setLatestEvent] = useState(null) const [messageApi, contextHolder] = message.useMessage() const [quickActionStudentId, setQuickActionStudentId] = useState(null) const longPressTimerRef = useRef(null) @@ -126,17 +139,33 @@ export const Home: React.FC = ({ canEdit, isPortraitMode = false }) = if (!silent) setLoading(false) }, []) + const fetchLatestEvent = useCallback(async () => { + if (!(window as any).api) return + const res = await (window as any).api.queryEvents({ limit: 1 }) + if (res.success) { + const latest = Array.isArray(res.data) && res.data.length > 0 ? (res.data[0] as scoreEvent) : null + setLatestEvent(latest) + } + }, []) + useEffect(() => { fetchData() + fetchLatestEvent() const onDataUpdated = (e: any) => { const category = e?.detail?.category - if (category === "students" || category === "reasons" || category === "all") { + if ( + category === "events" || + category === "students" || + category === "reasons" || + category === "all" + ) { fetchData(true) + fetchLatestEvent() } } window.addEventListener("ss:data-updated", onDataUpdated as any) return () => window.removeEventListener("ss:data-updated", onDataUpdated as any) - }, [fetchData]) + }, [fetchData, fetchLatestEvent]) useEffect(() => { const api = (window as any).api @@ -434,6 +463,7 @@ export const Home: React.FC = ({ canEdit, isPortraitMode = false }) = prev.map((s) => (s.id === student.id ? { ...s, score: s.score + delta } : s)) ) + fetchLatestEvent() emitDataUpdated("events") } else { messageApi.error(res.message || t("home.submitFailed")) @@ -441,6 +471,30 @@ export const Home: React.FC = ({ canEdit, isPortraitMode = false }) = setSubmitLoading(false) } + const handleUndoLastEvent = async () => { + if (!(window as any).api) return + if (!canEdit) { + messageApi.error(t("common.readOnly")) + return + } + if (!latestEvent) { + messageApi.warning(t("home.undoUnavailable")) + return + } + + setUndoLoading(true) + const res = await (window as any).api.deleteEvent(latestEvent.uuid) + if (res.success) { + messageApi.success(t("home.undoLastSuccess")) + fetchData(true) + fetchLatestEvent() + emitDataUpdated("events") + } else { + messageApi.error((res as any).message || t("score.undoFailed")) + } + setUndoLoading(false) + } + const handleSubmit = async () => { if (!selectedStudent) return @@ -1858,6 +1912,22 @@ export const Home: React.FC = ({ canEdit, isPortraitMode = false }) = { value: "squareGrid", label: t("home.layoutBy.squareGrid") }, ]} /> + diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 392e1eb..e0dd003 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -396,6 +396,10 @@ "studentCount": "{{count}} students", "operationTitle": "Points Operation: {{name}}", "submitOperation": "Submit Operation", + "undoLastAction": "Undo Last", + "undoLastHint": "Undo latest: {{name}} {{delta}}", + "undoUnavailable": "No points operation available to undo", + "undoLastSuccess": "Last points operation has been undone", "addPoints": "Add Points", "deductPoints": "Deduct Points", "pointsChange": "Points Change", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index c596fa9..622999e 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -396,6 +396,10 @@ "studentCount": "{{count}} 人", "operationTitle": "积分操作:{{name}}", "submitOperation": "提交操作", + "undoLastAction": "撤销上一步", + "undoLastHint": "撤销上一条:{{name}} {{delta}}", + "undoUnavailable": "当前没有可撤销的积分操作", + "undoLastSuccess": "已撤销上一步积分操作", "addPoints": "加分", "deductPoints": "扣分", "pointsChange": "积分变更",