From e10c20eec5ee29a4292456abd7da11d13d730224 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 20 Mar 2026 18:49:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=9C=8B=E6=9D=BF=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E6=B8=B2=E6=9F=93=E4=B8=BA=E4=B8=BB=E9=A1=B5=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E5=AD=A6=E7=94=9F=E5=8D=A1=E7=89=87=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 203 ++++++++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 12 deletions(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 0a0e38f..96aefcb 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -40,6 +40,16 @@ interface BoardManagerProps { canManage: boolean } +interface BoardStudentCardData { + key: string + name: string + score?: number + rewardPoints?: number + weekChange?: number + weekDeducted?: number + answeredCount?: number +} + const makeId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() @@ -112,6 +122,72 @@ const resolveSqlTemplate = (sql: string) => { .join(formatIso(at(-30))) } +const parseNumber = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value)) return value + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined + } + return undefined +} + +const pickStudentName = (row: Record): string | null => { + const nameCandidate = row.student_name ?? row.name ?? row.studentName + if (typeof nameCandidate !== "string") return null + const name = nameCandidate.trim() + return name ? name : null +} + +const toStudentCards = (rows: any[]): BoardStudentCardData[] => { + const cards: BoardStudentCardData[] = [] + rows.forEach((row, index) => { + if (!row || typeof row !== "object") return + const name = pickStudentName(row as Record) + if (!name) return + + const data = row as Record + cards.push({ + key: `${name}-${index}`, + name, + score: parseNumber(data.score), + rewardPoints: parseNumber(data.reward_points ?? data.rewardPoints), + weekChange: parseNumber(data.week_change ?? data.range_change ?? data.change), + weekDeducted: parseNumber(data.week_deducted ?? data.deducted), + answeredCount: parseNumber(data.answered_count ?? data.answer_count), + }) + }) + + if (cards.every((item) => item.score === undefined)) return cards + + return cards.sort((a, b) => (b.score ?? Number.MIN_SAFE_INTEGER) - (a.score ?? Number.MIN_SAFE_INTEGER)) +} + +const getAvatarText = (name: string): string => { + const chars = name.trim() + if (!chars) return "?" + const first = chars[0] + const second = chars.length > 1 ? chars[1] : "" + return `${first}${second}`.trim() +} + +const getAvatarColor = (name: string): string => { + const palette = [ + "#1677ff", + "#13c2c2", + "#52c41a", + "#faad14", + "#eb2f96", + "#722ed1", + "#2f54eb", + "#08979c", + ] + let hash = 0 + for (let i = 0; i < name.length; i += 1) { + hash = (hash * 31 + name.charCodeAt(i)) >>> 0 + } + return palette[hash % palette.length] +} + export const BoardManager: React.FC = ({ canManage }) => { const { t } = useTranslation() const [messageApi, contextHolder] = message.useMessage() @@ -481,6 +557,8 @@ ORDER BY reward_points DESC, score DESC`, {activeBoard.lists.map((list) => { const rows = resultMap[list.id] || [] + const studentCards = toStudentCards(rows) + const useCardView = studentCards.length > 0 const columns = rows.length > 0 ? Object.keys(rows[0]).map((key) => ({ @@ -561,18 +639,119 @@ ORDER BY reward_points DESC, score DESC`, )}
- `${list.id}-${index}`} - dataSource={rows} - columns={columns} - loading={Boolean(runningIds[list.id])} - locale={{ - emptyText: , - }} - scroll={{ x: true }} - pagination={{ pageSize: 20, showSizeChanger: false }} - size="small" - /> + {useCardView ? ( +
+ {studentCards.map((item, index) => { + const avatarColor = getAvatarColor(item.name) + const avatarText = getAvatarText(item.name) + const rankBadge = index === 0 ? "πŸ₯‡" : index === 1 ? "πŸ₯ˆ" : index === 2 ? "πŸ₯‰" : null + return ( + + {rankBadge && ( +
+ {rankBadge} +
+ )} +
+
+ {avatarText} +
+
+
+ {item.name} +
+
+ {item.score !== undefined && ( + = 0 ? "success" : "error"} style={{ margin: 0 }}> + ζ€»εˆ†: {item.score > 0 ? `+${item.score}` : item.score} + + )} + {item.rewardPoints !== undefined && ( + + ε₯–εŠ±εˆ†: {item.rewardPoints} + + )} + {item.weekChange !== undefined && ( + = 0 ? "success" : "error"} style={{ margin: 0 }}> + θΏ‘7倩: {item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange} + + )} + {item.weekDeducted !== undefined && ( + + θΏ‘7ε€©ζ‰£εˆ†: {item.weekDeducted} + + )} + {item.answeredCount !== undefined && ( + + 今ζ—₯ε›žη­”: {item.answeredCount} + + )} +
+
+
+
+ ) + })} +
+ ) : ( +
`${list.id}-${index}`} + dataSource={rows} + columns={columns} + loading={Boolean(runningIds[list.id])} + locale={{ + emptyText: , + }} + scroll={{ x: true }} + pagination={{ pageSize: 20, showSizeChanger: false }} + size="small" + /> + )} )