优化看板结果渲染为主页风格学生卡片列表

This commit is contained in:
JSR
2026-03-20 18:49:25 +08:00
parent baf1ac0fdf
commit e10c20eec5
+191 -12
View File
@@ -40,6 +40,16 @@ interface BoardManagerProps {
canManage: boolean canManage: boolean
} }
interface BoardStudentCardData {
key: string
name: string
score?: number
rewardPoints?: number
weekChange?: number
weekDeducted?: number
answeredCount?: number
}
const makeId = () => const makeId = () =>
typeof crypto !== "undefined" && "randomUUID" in crypto typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID() ? crypto.randomUUID()
@@ -112,6 +122,72 @@ const resolveSqlTemplate = (sql: string) => {
.join(formatIso(at(-30))) .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, unknown>): 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<string, unknown>)
if (!name) return
const data = row as Record<string, unknown>
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<BoardManagerProps> = ({ canManage }) => { export const BoardManager: React.FC<BoardManagerProps> = ({ canManage }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
@@ -481,6 +557,8 @@ ORDER BY reward_points DESC, score DESC`,
{activeBoard.lists.map((list) => { {activeBoard.lists.map((list) => {
const rows = resultMap[list.id] || [] const rows = resultMap[list.id] || []
const studentCards = toStudentCards(rows)
const useCardView = studentCards.length > 0
const columns = const columns =
rows.length > 0 rows.length > 0
? Object.keys(rows[0]).map((key) => ({ ? Object.keys(rows[0]).map((key) => ({
@@ -561,18 +639,119 @@ ORDER BY reward_points DESC, score DESC`,
)} )}
<div style={{ marginTop: 12 }}> <div style={{ marginTop: 12 }}>
<Table {useCardView ? (
rowKey={(_, index) => `${list.id}-${index}`} <div
dataSource={rows} style={{
columns={columns} display: "grid",
loading={Boolean(runningIds[list.id])} gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
locale={{ gap: 12,
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("common.noData")} />, }}
}} >
scroll={{ x: true }} {studentCards.map((item, index) => {
pagination={{ pageSize: 20, showSizeChanger: false }} const avatarColor = getAvatarColor(item.name)
size="small" const avatarText = getAvatarText(item.name)
/> const rankBadge = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : null
return (
<Card
key={item.key}
style={{
backgroundColor: "var(--ss-card-bg)",
border: "1px solid var(--ss-border-color)",
boxShadow: "0 6px 16px rgba(0, 0, 0, 0.06)",
position: "relative",
}}
styles={{ body: { padding: "12px 14px" } }}
>
{rankBadge && (
<div
style={{
position: "absolute",
top: "-10px",
left: "-10px",
fontSize: "24px",
}}
>
{rankBadge}
</div>
)}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 42,
height: 42,
borderRadius: 12,
backgroundColor: avatarColor,
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 700,
boxShadow: `0 4px 10px ${avatarColor}40`,
flexShrink: 0,
}}
>
{avatarText}
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<div
style={{
fontSize: 15,
fontWeight: 600,
color: "var(--ss-text-main)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{item.name}
</div>
<div style={{ marginTop: 4, display: "flex", flexWrap: "wrap", gap: 6 }}>
{item.score !== undefined && (
<Tag color={item.score >= 0 ? "success" : "error"} style={{ margin: 0 }}>
: {item.score > 0 ? `+${item.score}` : item.score}
</Tag>
)}
{item.rewardPoints !== undefined && (
<Tag color="processing" style={{ margin: 0 }}>
: {item.rewardPoints}
</Tag>
)}
{item.weekChange !== undefined && (
<Tag color={item.weekChange >= 0 ? "success" : "error"} style={{ margin: 0 }}>
7: {item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange}
</Tag>
)}
{item.weekDeducted !== undefined && (
<Tag color="gold" style={{ margin: 0 }}>
7: {item.weekDeducted}
</Tag>
)}
{item.answeredCount !== undefined && (
<Tag color="cyan" style={{ margin: 0 }}>
: {item.answeredCount}
</Tag>
)}
</div>
</div>
</div>
</Card>
)
})}
</div>
) : (
<Table
rowKey={(_, index) => `${list.id}-${index}`}
dataSource={rows}
columns={columns}
loading={Boolean(runningIds[list.id])}
locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("common.noData")} />,
}}
scroll={{ x: true }}
pagination={{ pageSize: 20, showSizeChanger: false }}
size="small"
/>
)}
</div> </div>
</Card> </Card>
) )