From e82866bbfc9e0ccf271c15e0d67a5b20b5080bfd Mon Sep 17 00:00:00 2001 From: JSR Date: Sat, 18 Apr 2026 11:16:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=A4=A7=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E8=A7=86=E5=9B=BE=E5=B9=B6=E5=AE=8C=E5=96=84=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 126 ++++++++++++++- src/components/Home.tsx | 267 +++++++++++++++++++++++++++++++- src/i18n/locales/ar-SA.json | 3 +- src/i18n/locales/de-DE.json | 3 +- src/i18n/locales/en-US.json | 6 +- src/i18n/locales/es-ES.json | 3 +- src/i18n/locales/fr-FR.json | 3 +- src/i18n/locales/ja-JP.json | 6 +- src/i18n/locales/ko-KR.json | 3 +- src/i18n/locales/pt-BR.json | 3 +- src/i18n/locales/ru-RU.json | 3 +- src/i18n/locales/zh-CN.json | 6 +- 12 files changed, 414 insertions(+), 18 deletions(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 806599a..2b8b021 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -18,7 +18,7 @@ import { import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from "@ant-design/icons" import { useTranslation } from "react-i18next" -type BoardStudentViewMode = "list" | "card" | "grid" +type BoardStudentViewMode = "list" | "card" | "grid" | "largeAvatar" type BoardScoreDisplayMode = "total" | "split" type SplitDirection = "horizontal" | "vertical" @@ -68,6 +68,7 @@ interface BoardManagerProps { interface BoardStudentCardData { key: string name: string + avatarUrl?: string score?: number addScore?: number deductScore?: number @@ -221,7 +222,10 @@ const normalizeBoards = (input: unknown): BoardConfig[] => { typeof list?.name === "string" && list.name.trim() ? list.name.trim() : "学生列表", sql: typeof list?.sql === "string" && list.sql.trim() ? list.sql : getDefaultSql(), viewMode: - list?.viewMode === "list" || list?.viewMode === "card" || list?.viewMode === "grid" + list?.viewMode === "list" || + list?.viewMode === "card" || + list?.viewMode === "grid" || + list?.viewMode === "largeAvatar" ? list.viewMode : "card", scoreDisplayMode: @@ -347,6 +351,11 @@ const toStudentCards = (rows: any[]): BoardStudentCardData[] => { cards.push({ key: `${name}-${index}`, name, + avatarUrl: + typeof (data.avatar_url ?? data.avatarUrl ?? data.avatar) === "string" && + String(data.avatar_url ?? data.avatarUrl ?? data.avatar).trim() + ? String(data.avatar_url ?? data.avatarUrl ?? data.avatar).trim() + : undefined, score: parseNumber(data.score), addScore, deductScore, @@ -851,6 +860,8 @@ ORDER BY reward_points DESC, score DESC`, gridTemplateColumns: list.viewMode === "grid" ? "repeat(auto-fill, minmax(102px, 1fr))" + : list.viewMode === "largeAvatar" + ? "repeat(auto-fill, minmax(180px, 1fr))" : list.viewMode === "list" ? "1fr" : "repeat(auto-fill, minmax(220px, 1fr))", @@ -897,11 +908,22 @@ ORDER BY reward_points DESC, score DESC`, : item.answeredCount !== undefined ? { label: t("board.metrics.todayAnswered"), value: item.answeredCount } : null + const metricValueText = primaryMetric + ? primaryMetric.value > 0 + ? `+${primaryMetric.value}` + : String(primaryMetric.value) + : null return (
@@ -1006,6 +1033,96 @@ ORDER BY reward_points DESC, score DESC`, )}
+ ) : list.viewMode === "largeAvatar" ? ( +
+ {item.avatarUrl ? ( + {item.name} + ) : ( +
1 ? "46px" : "56px", + letterSpacing: "0.02em", + }} + > + {avatarText} +
+ )} + +
+ +
+
+ {item.name} +
+ {metricValueText !== null && ( +
= 0 + ? "#52c41a" + : "#ff4d4f", + background: "rgba(255,255,255,0.62)", + border: "1px solid rgba(255,255,255,0.82)", + borderRadius: 8, + padding: "2px 9px", + backdropFilter: "blur(4px)", + }} + > + {metricValueText} +
+ )} +
+
) : (
diff --git a/src/components/Home.tsx b/src/components/Home.tsx index a48014e..73ad675 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -56,7 +56,7 @@ interface rewardSetting { } type SortType = "alphabet" | "surname" | "group" | "score" -type LayoutType = "grouped" | "squareGrid" +type LayoutType = "grouped" | "squareGrid" | "largeAvatar" type SearchKeyboardLayout = "t9" | "qwerty26" const T9_KEY_MAP: Record = { @@ -1749,6 +1749,255 @@ export const Home: React.FC = ({ ) } + const renderStudentLargeAvatarCard = (student: student, index: number) => { + const avatarText = getDisplayText(student.name) + const avatarColor = getAvatarColor(student.name) + const isQuickActionMode = quickActionStudentId === student.id + const isSelected = selectedStudentIds.includes(student.id) + const displayPoints = getDisplayPoints(student) + const scoreColor = displayPoints > 0 ? "#52c41a" : displayPoints < 0 ? "#ff4d4f" : "#595959" + + let rankBadge: string | null = null + if (sortType === "score" && !searchKeyword) { + if (index === 0) rankBadge = "🥇" + else if (index === 1) rankBadge = "🥈" + else if (index === 2) rankBadge = "🥉" + } + + return ( +
{ + if (suppressClickRef.current) { + suppressClickRef.current = false + e.preventDefault() + e.stopPropagation() + return + } + openOperation(student, e.currentTarget as HTMLElement) + }} + onMouseDown={(e) => { + if (batchMode) return + if (e.button !== 0) return + startLongPress(student) + }} + onMouseUp={cancelLongPress} + onMouseLeave={cancelLongPress} + onTouchStart={() => { + if (batchMode) return + startLongPress(student) + }} + onTouchEnd={cancelLongPress} + onTouchCancel={cancelLongPress} + onContextMenu={(e) => { + if (batchMode) return + e.preventDefault() + openQuickAction(student) + }} + style={{ + cursor: "pointer", + position: "relative", + aspectRatio: "1.2 / 1", + }} + ref={(el) => { + groupedStudents.forEach((group) => { + if (group.key === "all") return + if (firstStudentIdByGroup.get(group.key) === student.id) { + groupRefs.current[group.key] = el + } + }) + }} + > + +
+ {student.avatarUrl ? ( + {student.name} + ) : ( +
1 ? "46px" : "56px", + letterSpacing: "0.02em", + }} + > + {avatarText} +
+ )} + +
+ + {rankBadge && ( +
+ {rankBadge} +
+ )} + + {isSelected && ( + + {t("home.selected")} + + )} + + {isQuickActionMode ? ( +
+ + +
+ ) : ( +
+
+ {student.name} +
+
+ {displayPoints > 0 ? `+${displayPoints}` : displayPoints} +
+
+ )} +
+ +
+ ) + } + const renderGroupedCards = () => { if (layoutType === "squareGrid") { return ( @@ -1764,6 +2013,20 @@ export const Home: React.FC = ({ ) } + if (layoutType === "largeAvatar") { + return ( +
+ {sortedStudents.map((student, idx) => renderStudentLargeAvatarCard(student, idx))} +
+ ) + } + return groupedStudents.map((group) => (
= ({ options={[ { value: "grouped", label: t("home.layoutBy.grouped") }, { value: "squareGrid", label: t("home.layoutBy.squareGrid") }, + { value: "largeAvatar", label: t("home.layoutBy.largeAvatar") }, ]} />