feat: 主页支持一键撤销上一步积分操作

This commit is contained in:
JSR
2026-03-20 17:10:10 +08:00
parent 10eb07573c
commit 4d7b077a5a
3 changed files with 81 additions and 3 deletions
+73 -3
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react" import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"
import { Card, Space, Button, Tag, Input, Select, Modal, Drawer, message, InputNumber, Divider } from "antd" 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 { useTranslation } from "react-i18next"
import { match, pinyin } from "pinyin-pro" import { match, pinyin } from "pinyin-pro"
import { getAvatarFromExtraJson } from "../utils/studentAvatar" import { getAvatarFromExtraJson } from "../utils/studentAvatar"
@@ -23,6 +23,17 @@ interface reason {
category: string 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 SortType = "alphabet" | "surname" | "score"
type LayoutType = "grouped" | "squareGrid" type LayoutType = "grouped" | "squareGrid"
type SearchKeyboardLayout = "t9" | "qwerty26" type SearchKeyboardLayout = "t9" | "qwerty26"
@@ -82,6 +93,8 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
const [customScore, setCustomScore] = useState<number | undefined>(undefined) const [customScore, setCustomScore] = useState<number | undefined>(undefined)
const [reasonContent, setReasonContent] = useState("") const [reasonContent, setReasonContent] = useState("")
const [submitLoading, setSubmitLoading] = useState(false) const [submitLoading, setSubmitLoading] = useState(false)
const [undoLoading, setUndoLoading] = useState(false)
const [latestEvent, setLatestEvent] = useState<scoreEvent | null>(null)
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const [quickActionStudentId, setQuickActionStudentId] = useState<number | null>(null) const [quickActionStudentId, setQuickActionStudentId] = useState<number | null>(null)
const longPressTimerRef = useRef<number | null>(null) const longPressTimerRef = useRef<number | null>(null)
@@ -126,17 +139,33 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
if (!silent) setLoading(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(() => { useEffect(() => {
fetchData() fetchData()
fetchLatestEvent()
const onDataUpdated = (e: any) => { const onDataUpdated = (e: any) => {
const category = e?.detail?.category const category = e?.detail?.category
if (category === "students" || category === "reasons" || category === "all") { if (
category === "events" ||
category === "students" ||
category === "reasons" ||
category === "all"
) {
fetchData(true) fetchData(true)
fetchLatestEvent()
} }
} }
window.addEventListener("ss:data-updated", onDataUpdated as any) window.addEventListener("ss:data-updated", onDataUpdated as any)
return () => window.removeEventListener("ss:data-updated", onDataUpdated as any) return () => window.removeEventListener("ss:data-updated", onDataUpdated as any)
}, [fetchData]) }, [fetchData, fetchLatestEvent])
useEffect(() => { useEffect(() => {
const api = (window as any).api const api = (window as any).api
@@ -434,6 +463,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
prev.map((s) => (s.id === student.id ? { ...s, score: s.score + delta } : s)) prev.map((s) => (s.id === student.id ? { ...s, score: s.score + delta } : s))
) )
fetchLatestEvent()
emitDataUpdated("events") emitDataUpdated("events")
} else { } else {
messageApi.error(res.message || t("home.submitFailed")) messageApi.error(res.message || t("home.submitFailed"))
@@ -441,6 +471,30 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
setSubmitLoading(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 () => { const handleSubmit = async () => {
if (!selectedStudent) return if (!selectedStudent) return
@@ -1858,6 +1912,22 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
{ value: "squareGrid", label: t("home.layoutBy.squareGrid") }, { value: "squareGrid", label: t("home.layoutBy.squareGrid") },
]} ]}
/> />
<Button
icon={<UndoOutlined />}
onClick={handleUndoLastEvent}
loading={undoLoading}
disabled={!canEdit || !latestEvent}
title={
latestEvent
? t("home.undoLastHint", {
name: latestEvent.student_name,
delta: latestEvent.delta > 0 ? `+${latestEvent.delta}` : latestEvent.delta,
})
: t("home.undoUnavailable")
}
>
{t("home.undoLastAction")}
</Button>
</Space> </Space>
</div> </div>
+4
View File
@@ -396,6 +396,10 @@
"studentCount": "{{count}} students", "studentCount": "{{count}} students",
"operationTitle": "Points Operation: {{name}}", "operationTitle": "Points Operation: {{name}}",
"submitOperation": "Submit Operation", "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", "addPoints": "Add Points",
"deductPoints": "Deduct Points", "deductPoints": "Deduct Points",
"pointsChange": "Points Change", "pointsChange": "Points Change",
+4
View File
@@ -396,6 +396,10 @@
"studentCount": "{{count}} 人", "studentCount": "{{count}} 人",
"operationTitle": "积分操作:{{name}}", "operationTitle": "积分操作:{{name}}",
"submitOperation": "提交操作", "submitOperation": "提交操作",
"undoLastAction": "撤销上一步",
"undoLastHint": "撤销上一条:{{name}} {{delta}}",
"undoUnavailable": "当前没有可撤销的积分操作",
"undoLastSuccess": "已撤销上一步积分操作",
"addPoints": "加分", "addPoints": "加分",
"deductPoints": "扣分", "deductPoints": "扣分",
"pointsChange": "积分变更", "pointsChange": "积分变更",