mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 21:14:21 +08:00
feat: 主页支持一键撤销上一步积分操作
This commit is contained in:
+73
-3
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "积分变更",
|
||||||
|
|||||||
Reference in New Issue
Block a user