mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat: 首页支持多选全选与批量积分
This commit is contained in:
+248
-70
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "积分变更",
|
||||||
|
|||||||
Reference in New Issue
Block a user