feat: 首页支持多选全选与批量积分

This commit is contained in:
JSR
2026-03-20 17:42:30 +08:00
parent d7361b82d3
commit ad6b0808cb
3 changed files with 270 additions and 70 deletions
+248 -70
View File
@@ -89,6 +89,8 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
const searchAreaRef = useRef<HTMLDivElement>(null) const searchAreaRef = useRef<HTMLDivElement>(null)
const [selectedStudent, setSelectedStudent] = useState<student | null>(null) const [selectedStudent, setSelectedStudent] = useState<student | null>(null)
const [batchMode, setBatchMode] = useState(false)
const [selectedStudentIds, setSelectedStudentIds] = useState<number[]>([])
const [operationVisible, setOperationVisible] = useState(false) const [operationVisible, setOperationVisible] = useState(false)
const [customScore, setCustomScore] = useState<number | undefined>(undefined) const [customScore, setCustomScore] = useState<number | undefined>(undefined)
const [reasonContent, setReasonContent] = useState("") const [reasonContent, setReasonContent] = useState("")
@@ -280,6 +282,14 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
} }
}, []) }, [])
useEffect(() => {
setSelectedStudentIds((prev) => {
if (prev.length === 0) return prev
const validIds = new Set(students.map((s) => s.id))
return prev.filter((id) => validIds.has(id))
})
}, [students])
const t9KeyRows = [ const t9KeyRows = [
[ [
{ digit: "2", letters: "ABC" }, { digit: "2", letters: "ABC" },
@@ -430,6 +440,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
}) })
}, [reasons]) }, [reasons])
const selectedStudents = useMemo(() => {
if (selectedStudentIds.length === 0) return []
const idSet = new Set(selectedStudentIds)
return students.filter((s) => idSet.has(s.id))
}, [students, selectedStudentIds])
const getAvatarColor = (name: string) => { const getAvatarColor = (name: string) => {
const colors = [ const colors = [
"#FF6B6B", "#FF6B6B",
@@ -463,55 +479,80 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
messageApi.error(t("common.readOnly")) messageApi.error(t("common.readOnly"))
return return
} }
if (batchMode) {
setSelectedStudentIds((prev) =>
prev.includes(student.id) ? prev.filter((id) => id !== student.id) : [...prev, student.id]
)
return
}
setSelectedStudent(student) setSelectedStudent(student)
setCustomScore(undefined) setCustomScore(undefined)
setReasonContent("") setReasonContent("")
setOperationVisible(true) setOperationVisible(true)
} }
const performSubmit = async (student: student, delta: number, content: string) => { const performSubmit = async (targetStudents: student[], delta: number, content: string) => {
if (!(window as any).api) return if (!(window as any).api) return
if (!canEdit) { if (!canEdit) {
messageApi.error(t("common.readOnly")) messageApi.error(t("common.readOnly"))
return return
} }
if (targetStudents.length === 0) {
messageApi.warning(t("home.selectStudentFirst"))
return
}
setSubmitLoading(true) setSubmitLoading(true)
logHome("performSubmit:start", { logHome("performSubmit:start", { studentCount: targetStudents.length, delta, content })
student: student.name, let successCount = 0
delta,
content,
localScoreBefore: student.score,
})
const res = await (window as any).api.createEvent({
student_name: student.name,
reason_content: content,
delta: delta,
})
logHome("performSubmit:createEvent:response", { for (const student of targetStudents) {
student: student.name, const res = await (window as any).api.createEvent({
delta, student_name: student.name,
success: Boolean(res?.success), reason_content: content,
message: (res as any)?.message, delta: delta,
}) })
if (res.success) { logHome("performSubmit:createEvent:response", {
messageApi.success( student: student.name,
delta > 0 delta,
? t("home.scoreAdded", { name: student.name, points: Math.abs(delta) }) success: Boolean(res?.success),
: t("home.scoreDeducted", { name: student.name, points: Math.abs(delta) }) message: (res as any)?.message,
) })
if (res.success) successCount += 1
}
if (successCount > 0) {
if (targetStudents.length === 1) {
const student = targetStudents[0]
messageApi.success(
delta > 0
? t("home.scoreAdded", { name: student.name, points: Math.abs(delta) })
: t("home.scoreDeducted", { name: student.name, points: Math.abs(delta) })
)
} else if (successCount === targetStudents.length) {
messageApi.success(t("home.batchSuccess", { count: successCount }))
} else {
messageApi.warning(
t("home.batchPartial", { success: successCount, total: targetStudents.length })
)
}
setSelectedStudentIds([])
setBatchMode(false)
setSelectedStudent(null)
setOperationVisible(false) setOperationVisible(false)
setCustomScore(undefined)
setReasonContent("")
setQuickActionStudentId(null)
fetchData(true) fetchData(true)
fetchLatestEvent() fetchLatestEvent()
emitDataUpdated("events") emitDataUpdated("events")
logHome("performSubmit:afterSuccessRefreshDispatched", { logHome("performSubmit:afterSuccessRefreshDispatched", { studentCount: targetStudents.length, delta })
student: student.name,
delta,
})
} else { } else {
messageApi.error(res.message || t("home.submitFailed")) messageApi.warning(
t("home.batchPartial", { success: successCount, total: targetStudents.length })
)
} }
setSubmitLoading(false) setSubmitLoading(false)
} }
@@ -541,7 +582,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (!selectedStudent) return const targets = batchMode ? selectedStudents : selectedStudent ? [selectedStudent] : []
if (targets.length === 0) {
messageApi.warning(t("home.selectStudentFirst"))
return
}
const delta = customScore const delta = customScore
if (delta === undefined || !Number.isFinite(delta)) { if (delta === undefined || !Number.isFinite(delta)) {
@@ -556,12 +601,16 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
: delta < 0 : delta < 0
? t("home.deductPoints") ? t("home.deductPoints")
: t("home.pointsChange")) : t("home.pointsChange"))
await performSubmit(selectedStudent, delta, content) await performSubmit(targets, delta, content)
} }
const handleReasonSelect = (reason: reason) => { const handleReasonSelect = (reason: reason) => {
if (!selectedStudent) return const targets = batchMode ? selectedStudents : selectedStudent ? [selectedStudent] : []
performSubmit(selectedStudent, reason.delta, reason.content) if (targets.length === 0) {
messageApi.warning(t("home.selectStudentFirst"))
return
}
performSubmit(targets, reason.delta, reason.content)
} }
const cancelLongPress = () => { const cancelLongPress = () => {
@@ -596,10 +645,49 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
const handleQuickAdjust = (student: student, delta: number) => { const handleQuickAdjust = (student: student, delta: number) => {
const content = delta > 0 ? t("home.addPoints") : t("home.deductPoints") const content = delta > 0 ? t("home.addPoints") : t("home.deductPoints")
performSubmit(student, delta, content) performSubmit([student], delta, content)
setQuickActionStudentId(null) setQuickActionStudentId(null)
} }
const handleSelectAllStudents = () => {
setSelectedStudentIds(students.map((s) => s.id))
}
const handleClearSelectedStudents = () => {
setSelectedStudentIds([])
}
const handleEnterBatchMode = () => {
if (!canEdit) {
messageApi.error(t("common.readOnly"))
return
}
setBatchMode(true)
setSelectedStudent(null)
setOperationVisible(false)
setQuickActionStudentId(null)
}
const handleExitBatchMode = () => {
setBatchMode(false)
setSelectedStudentIds([])
}
const handleOpenBatchOperation = () => {
if (!canEdit) {
messageApi.error(t("common.readOnly"))
return
}
if (selectedStudents.length === 0) {
messageApi.warning(t("home.selectStudentFirst"))
return
}
setSelectedStudent(null)
setCustomScore(undefined)
setReasonContent("")
setOperationVisible(true)
}
const renderStudentCard = (student: student, index: number) => { const renderStudentCard = (student: student, index: number) => {
const avatarText = getDisplayText(student.name) const avatarText = getDisplayText(student.name)
const avatarColor = getAvatarColor(student.name) const avatarColor = getAvatarColor(student.name)
@@ -612,6 +700,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
} }
const isQuickActionMode = quickActionStudentId === student.id const isQuickActionMode = quickActionStudentId === student.id
const isSelected = selectedStudentIds.includes(student.id)
return ( return (
<div <div
@@ -627,15 +716,20 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
openOperation(student) openOperation(student)
}} }}
onMouseDown={(e) => { onMouseDown={(e) => {
if (batchMode) return
if (e.button !== 0) return if (e.button !== 0) return
startLongPress(student) startLongPress(student)
}} }}
onMouseUp={cancelLongPress} onMouseUp={cancelLongPress}
onMouseLeave={cancelLongPress} onMouseLeave={cancelLongPress}
onTouchStart={() => startLongPress(student)} onTouchStart={() => {
if (batchMode) return
startLongPress(student)
}}
onTouchEnd={cancelLongPress} onTouchEnd={cancelLongPress}
onTouchCancel={cancelLongPress} onTouchCancel={cancelLongPress}
onContextMenu={(e) => { onContextMenu={(e) => {
if (batchMode) return
e.preventDefault() e.preventDefault()
openQuickAction(student) openQuickAction(student)
}} }}
@@ -647,9 +741,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
transition: "all 0.2s cubic-bezier(0.38, 0, 0.24, 1)", transition: "all 0.2s cubic-bezier(0.38, 0, 0.24, 1)",
border: isQuickActionMode border: isQuickActionMode
? "1px solid var(--ant-color-primary, #1677ff)" ? "1px solid var(--ant-color-primary, #1677ff)"
: "1px solid var(--ss-border-color)", : isSelected
? "1px solid var(--ant-color-primary, #1677ff)"
: "1px solid var(--ss-border-color)",
overflow: "visible", overflow: "visible",
boxShadow: isQuickActionMode ? "0 8px 18px rgba(22, 119, 255, 0.18)" : undefined, boxShadow:
isQuickActionMode || isSelected ? "0 8px 18px rgba(22, 119, 255, 0.18)" : undefined,
}} }}
styles={{ body: { padding: isPortraitMode ? "10px 12px" : "12px" } }} styles={{ body: { padding: isPortraitMode ? "10px 12px" : "12px" } }}
> >
@@ -787,6 +884,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
<div <div
style={{ display: "flex", alignItems: "center", gap: "4px", marginTop: "2px" }} style={{ display: "flex", alignItems: "center", gap: "4px", marginTop: "2px" }}
> >
{isSelected && (
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
{t("home.selected")}
</Tag>
)}
<Tag <Tag
color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"} color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"}
style={{ fontWeight: "bold" }} style={{ fontWeight: "bold" }}
@@ -806,6 +908,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
const avatarText = getDisplayText(student.name) const avatarText = getDisplayText(student.name)
const avatarColor = getAvatarColor(student.name) const avatarColor = getAvatarColor(student.name)
const isQuickActionMode = quickActionStudentId === student.id const isQuickActionMode = quickActionStudentId === student.id
const isSelected = selectedStudentIds.includes(student.id)
return ( return (
<div <div
@@ -821,15 +924,20 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
openOperation(student) openOperation(student)
}} }}
onMouseDown={(e) => { onMouseDown={(e) => {
if (batchMode) return
if (e.button !== 0) return if (e.button !== 0) return
startLongPress(student) startLongPress(student)
}} }}
onMouseUp={cancelLongPress} onMouseUp={cancelLongPress}
onMouseLeave={cancelLongPress} onMouseLeave={cancelLongPress}
onTouchStart={() => startLongPress(student)} onTouchStart={() => {
if (batchMode) return
startLongPress(student)
}}
onTouchEnd={cancelLongPress} onTouchEnd={cancelLongPress}
onTouchCancel={cancelLongPress} onTouchCancel={cancelLongPress}
onContextMenu={(e) => { onContextMenu={(e) => {
if (batchMode) return
e.preventDefault() e.preventDefault()
openQuickAction(student) openQuickAction(student)
}} }}
@@ -838,7 +946,8 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
position: "relative", position: "relative",
padding: "8px 10px", padding: "8px 10px",
borderBottom: isLast ? "none" : "1px solid var(--ss-border-color)", borderBottom: isLast ? "none" : "1px solid var(--ss-border-color)",
background: isQuickActionMode ? "rgba(22, 119, 255, 0.06)" : "transparent", background:
isQuickActionMode || isSelected ? "rgba(22, 119, 255, 0.06)" : "transparent",
transition: "background-color 160ms ease", transition: "background-color 160ms ease",
}} }}
> >
@@ -919,12 +1028,19 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
</Button> </Button>
</Space> </Space>
) : ( ) : (
<Tag <Space size={4}>
color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"} {isSelected && (
style={{ fontWeight: "bold", marginInlineEnd: 0 }} <Tag color="processing" style={{ marginInlineEnd: 0 }}>
> {t("home.selected")}
{student.score > 0 ? `+${student.score}` : student.score} </Tag>
</Tag> )}
<Tag
color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"}
style={{ fontWeight: "bold", marginInlineEnd: 0 }}
>
{student.score > 0 ? `+${student.score}` : student.score}
</Tag>
</Space>
)} )}
</div> </div>
</div> </div>
@@ -935,6 +1051,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
const avatarText = getDisplayText(student.name) const avatarText = getDisplayText(student.name)
const avatarColor = getAvatarColor(student.name) const avatarColor = getAvatarColor(student.name)
const isQuickActionMode = quickActionStudentId === student.id const isQuickActionMode = quickActionStudentId === student.id
const isSelected = selectedStudentIds.includes(student.id)
let rankBadge: string | null = null let rankBadge: string | null = null
if (sortType === "score" && !searchKeyword) { if (sortType === "score" && !searchKeyword) {
@@ -957,15 +1074,20 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
openOperation(student) openOperation(student)
}} }}
onMouseDown={(e) => { onMouseDown={(e) => {
if (batchMode) return
if (e.button !== 0) return if (e.button !== 0) return
startLongPress(student) startLongPress(student)
}} }}
onMouseUp={cancelLongPress} onMouseUp={cancelLongPress}
onMouseLeave={cancelLongPress} onMouseLeave={cancelLongPress}
onTouchStart={() => startLongPress(student)} onTouchStart={() => {
if (batchMode) return
startLongPress(student)
}}
onTouchEnd={cancelLongPress} onTouchEnd={cancelLongPress}
onTouchCancel={cancelLongPress} onTouchCancel={cancelLongPress}
onContextMenu={(e) => { onContextMenu={(e) => {
if (batchMode) return
e.preventDefault() e.preventDefault()
openQuickAction(student) openQuickAction(student)
}} }}
@@ -991,9 +1113,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
transition: "all 0.2s cubic-bezier(0.38, 0, 0.24, 1)", transition: "all 0.2s cubic-bezier(0.38, 0, 0.24, 1)",
border: isQuickActionMode border: isQuickActionMode
? "1px solid var(--ant-color-primary, #1677ff)" ? "1px solid var(--ant-color-primary, #1677ff)"
: "1px solid var(--ss-border-color)", : isSelected
? "1px solid var(--ant-color-primary, #1677ff)"
: "1px solid var(--ss-border-color)",
overflow: "visible", overflow: "visible",
boxShadow: isQuickActionMode ? "0 8px 18px rgba(22, 119, 255, 0.18)" : undefined, boxShadow:
isQuickActionMode || isSelected ? "0 8px 18px rgba(22, 119, 255, 0.18)" : undefined,
}} }}
styles={{ body: { height: "100%", padding: "8px" } }} styles={{ body: { height: "100%", padding: "8px" } }}
> >
@@ -1111,6 +1236,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
> >
{student.score > 0 ? `+${student.score}` : student.score} {student.score > 0 ? `+${student.score}` : student.score}
</Tag> </Tag>
{isSelected && (
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
{t("home.selected")}
</Tag>
)}
</div> </div>
)} )}
</div> </div>
@@ -1502,7 +1632,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
) )
} }
const operationPanelContent = selectedStudent && ( const operationTargets = batchMode ? selectedStudents : selectedStudent ? [selectedStudent] : []
const isBatchOperation = operationTargets.length > 1 || (batchMode && operationTargets.length > 0)
const operationPanelContent = operationTargets.length > 0 && (
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -1525,7 +1658,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
}} }}
> >
<div style={{ display: "flex", alignItems: "center", gap: "12px", minWidth: 0, flex: 1 }}> <div style={{ display: "flex", alignItems: "center", gap: "12px", minWidth: 0, flex: 1 }}>
{selectedStudent.avatarUrl ? ( {!isBatchOperation && selectedStudent?.avatarUrl ? (
<img <img
src={selectedStudent.avatarUrl} src={selectedStudent.avatarUrl}
alt={selectedStudent.name} alt={selectedStudent.name}
@@ -1537,13 +1670,13 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
border: "1px solid var(--ss-border-color)", border: "1px solid var(--ss-border-color)",
}} }}
/> />
) : ( ) : !isBatchOperation ? (
<div <div
style={{ style={{
width: "32px", width: "32px",
height: "32px", height: "32px",
borderRadius: "50%", borderRadius: "50%",
backgroundColor: getAvatarColor(selectedStudent.name), backgroundColor: getAvatarColor(selectedStudent?.name || ""),
color: "white", color: "white",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -1552,8 +1685,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
fontWeight: "bold", fontWeight: "bold",
}} }}
> >
{getDisplayText(selectedStudent.name)} {getDisplayText(selectedStudent?.name || "")}
</div> </div>
) : (
<Tag color="processing">{t("home.batchMode")}</Tag>
)} )}
<span <span
style={{ style={{
@@ -1564,22 +1699,30 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
> >
{selectedStudent.name} {!isBatchOperation
? selectedStudent?.name
: t("home.selectedCount", { count: operationTargets.length })}
</span> </span>
</div> </div>
<div style={{ display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }}> {!isBatchOperation && selectedStudent && (
<span style={{ color: "var(--ss-text-secondary)", fontSize: "13px" }}> <div style={{ display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }}>
{t("home.currentScore")} <span style={{ color: "var(--ss-text-secondary)", fontSize: "13px" }}>
</span> {t("home.currentScore")}
<Tag </span>
color={ <Tag
selectedStudent.score > 0 ? "success" : selectedStudent.score < 0 ? "error" : "default" color={
} selectedStudent.score > 0
style={{ fontWeight: "bold" }} ? "success"
> : selectedStudent.score < 0
{selectedStudent.score > 0 ? `+${selectedStudent.score}` : selectedStudent.score} ? "error"
</Tag> : "default"
</div> }
style={{ fontWeight: "bold" }}
>
{selectedStudent.score > 0 ? `+${selectedStudent.score}` : selectedStudent.score}
</Tag>
</div>
)}
</div> </div>
{groupedReasons.length > 0 && ( {groupedReasons.length > 0 && (
@@ -1769,7 +1912,9 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
{t("home.preview")} {t("home.preview")}
</div> </div>
<div style={{ fontSize: "15px" }}> <div style={{ fontSize: "15px" }}>
{selectedStudent.name}{" "} {isBatchOperation
? t("home.selectedCount", { count: operationTargets.length })
: selectedStudent?.name}{" "}
<span <span
style={{ style={{
fontWeight: "bold", fontWeight: "bold",
@@ -1973,6 +2118,31 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
> >
{t("home.undoLastAction")} {t("home.undoLastAction")}
</Button> </Button>
{!batchMode ? (
<Button onClick={handleEnterBatchMode} disabled={!canEdit}>
{t("home.multiSelect")}
</Button>
) : (
<Space size={8} wrap>
<Button onClick={handleSelectAllStudents} disabled={!canEdit || students.length === 0}>
{t("home.selectAll")}
</Button>
<Button
onClick={handleClearSelectedStudents}
disabled={!canEdit || selectedStudentIds.length === 0}
>
{t("home.clearSelected")}
</Button>
<Button
type="primary"
onClick={handleOpenBatchOperation}
disabled={!canEdit || selectedStudentIds.length === 0}
>
{t("home.batchOperate")}
</Button>
<Button onClick={handleExitBatchMode}>{t("common.cancel")}</Button>
</Space>
)}
</Space> </Space>
</div> </div>
@@ -2016,7 +2186,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
{isPortraitMode ? ( {isPortraitMode ? (
<Drawer <Drawer
title={ title={
<div data-tauri-drag-region>{t("home.operationTitle", { name: selectedStudent?.name })}</div> <div data-tauri-drag-region>
{isBatchOperation
? t("home.operationTitleBatch", { count: operationTargets.length })
: t("home.operationTitle", { name: selectedStudent?.name })}
</div>
} }
className="ss-operation-drawer" className="ss-operation-drawer"
placement="bottom" placement="bottom"
@@ -2041,7 +2215,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
</Drawer> </Drawer>
) : ( ) : (
<Modal <Modal
title={t("home.operationTitle", { name: selectedStudent?.name })} title={
isBatchOperation
? t("home.operationTitleBatch", { count: operationTargets.length })
: t("home.operationTitle", { name: selectedStudent?.name })
}
open={operationVisible} open={operationVisible}
onCancel={() => setOperationVisible(false)} onCancel={() => setOperationVisible(false)}
onOk={handleSubmit} onOk={handleSubmit}
+11
View File
@@ -396,10 +396,21 @@
"studentCount": "{{count}} students", "studentCount": "{{count}} students",
"operationTitle": "Points Operation: {{name}}", "operationTitle": "Points Operation: {{name}}",
"submitOperation": "Submit Operation", "submitOperation": "Submit Operation",
"operationTitleBatch": "Points Operation: {{count}} Selected",
"undoLastAction": "Undo Last", "undoLastAction": "Undo Last",
"undoLastHint": "Undo latest: {{name}} {{delta}}", "undoLastHint": "Undo latest: {{name}} {{delta}}",
"undoUnavailable": "No points operation available to undo", "undoUnavailable": "No points operation available to undo",
"undoLastSuccess": "Last points operation has been undone", "undoLastSuccess": "Last points operation has been undone",
"multiSelect": "Multi-select",
"batchMode": "Batch Mode",
"selectAll": "Select All",
"clearSelected": "Clear Selected",
"batchOperate": "Batch Points",
"selected": "Selected",
"selectedCount": "{{count}} selected",
"batchSuccess": "Submitted points for {{count}} students",
"batchPartial": "Successfully submitted for {{success}}/{{total}} students",
"selectStudentFirst": "Please select student(s) first",
"addPoints": "Add Points", "addPoints": "Add Points",
"deductPoints": "Deduct Points", "deductPoints": "Deduct Points",
"pointsChange": "Points Change", "pointsChange": "Points Change",
+11
View File
@@ -396,10 +396,21 @@
"studentCount": "{{count}} 人", "studentCount": "{{count}} 人",
"operationTitle": "积分操作:{{name}}", "operationTitle": "积分操作:{{name}}",
"submitOperation": "提交操作", "submitOperation": "提交操作",
"operationTitleBatch": "积分操作:已选 {{count}} 人",
"undoLastAction": "撤销上一步", "undoLastAction": "撤销上一步",
"undoLastHint": "撤销上一条:{{name}} {{delta}}", "undoLastHint": "撤销上一条:{{name}} {{delta}}",
"undoUnavailable": "当前没有可撤销的积分操作", "undoUnavailable": "当前没有可撤销的积分操作",
"undoLastSuccess": "已撤销上一步积分操作", "undoLastSuccess": "已撤销上一步积分操作",
"multiSelect": "多选",
"batchMode": "多选模式",
"selectAll": "全选",
"clearSelected": "清空已选",
"batchOperate": "批量积分",
"selected": "已选",
"selectedCount": "已选 {{count}} 人",
"batchSuccess": "已为 {{count}} 名学生提交积分",
"batchPartial": "成功提交 {{success}}/{{total}} 名学生积分",
"selectStudentFirst": "请先选择学生",
"addPoints": "加分", "addPoints": "加分",
"deductPoints": "扣分", "deductPoints": "扣分",
"pointsChange": "积分变更", "pointsChange": "积分变更",