看板支持列表/卡片/网格三种学生视图切换

This commit is contained in:
JSR
2026-03-20 18:51:39 +08:00
parent e10c20eec5
commit 562639f75f
3 changed files with 86 additions and 14 deletions
+62 -14
View File
@@ -21,6 +21,7 @@ interface StudentListConfig {
id: string
name: string
sql: string
viewMode: BoardStudentViewMode
}
interface BoardConfig {
@@ -40,6 +41,8 @@ interface BoardManagerProps {
canManage: boolean
}
type BoardStudentViewMode = "list" | "card" | "grid"
interface BoardStudentCardData {
key: string
name: string
@@ -66,6 +69,7 @@ const createDefaultList = (): StudentListConfig => ({
id: makeId(),
name: "学生积分榜",
sql: getDefaultSql(),
viewMode: "card",
})
const createDefaultBoard = (): BoardConfig => ({
@@ -86,6 +90,10 @@ const normalizeBoards = (input: unknown): BoardConfig[] => {
name:
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
: "card",
}))
.filter((list: StudentListConfig) => list.sql.trim())
: []
@@ -421,7 +429,8 @@ ORDER BY reward_points DESC, score DESC`,
{
id: makeId(),
name: t("board.newList"),
sql: getDefaultSql(),
sql: getDefaultSql(),
viewMode: "card",
},
],
}
@@ -594,6 +603,19 @@ ORDER BY reward_points DESC, score DESC`,
onChange={(presetId) => applyPreset(activeBoard.id, list.id, presetId)}
disabled={!canManage}
/>
<Select
style={{ width: 140 }}
value={list.viewMode}
onChange={(viewMode: BoardStudentViewMode) =>
updateList(activeBoard.id, list.id, { viewMode })
}
options={[
{ value: "list", label: t("board.viewModes.list") },
{ value: "card", label: t("board.viewModes.card") },
{ value: "grid", label: t("board.viewModes.grid") },
]}
disabled={!canManage}
/>
<Button
type="primary"
icon={<PlayCircleOutlined />}
@@ -643,7 +665,12 @@ ORDER BY reward_points DESC, score DESC`,
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
gridTemplateColumns:
list.viewMode === "grid"
? "repeat(auto-fill, minmax(180px, 1fr))"
: list.viewMode === "list"
? "1fr"
: "repeat(auto-fill, minmax(220px, 1fr))",
gap: 12,
}}
>
@@ -660,7 +687,12 @@ ORDER BY reward_points DESC, score DESC`,
boxShadow: "0 6px 16px rgba(0, 0, 0, 0.06)",
position: "relative",
}}
styles={{ body: { padding: "12px 14px" } }}
styles={{
body: {
padding:
list.viewMode === "grid" ? "12px" : list.viewMode === "list" ? "10px 12px" : "12px 14px",
},
}}
>
{rankBadge && (
<div
@@ -674,11 +706,18 @@ ORDER BY reward_points DESC, score DESC`,
{rankBadge}
</div>
)}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
display: "flex",
alignItems: list.viewMode === "grid" ? "flex-start" : "center",
flexDirection: list.viewMode === "grid" ? "column" : "row",
gap: 10,
}}
>
<div
style={{
width: 42,
height: 42,
width: list.viewMode === "grid" ? 56 : 42,
height: list.viewMode === "grid" ? 56 : 42,
borderRadius: 12,
backgroundColor: avatarColor,
color: "#fff",
@@ -692,43 +731,52 @@ ORDER BY reward_points DESC, score DESC`,
>
{avatarText}
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{ minWidth: 0, flex: 1, width: "100%" }}>
<div
style={{
fontSize: 15,
fontSize: list.viewMode === "grid" ? 16 : 15,
fontWeight: 600,
color: "var(--ss-text-main)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
textAlign: list.viewMode === "grid" ? "center" : "left",
}}
>
{item.name}
</div>
<div style={{ marginTop: 4, display: "flex", flexWrap: "wrap", gap: 6 }}>
<div
style={{
marginTop: 4,
display: "flex",
flexWrap: "wrap",
gap: 6,
justifyContent: list.viewMode === "grid" ? "center" : "flex-start",
}}
>
{item.score !== undefined && (
<Tag color={item.score >= 0 ? "success" : "error"} style={{ margin: 0 }}>
: {item.score > 0 ? `+${item.score}` : item.score}
{t("board.metrics.totalScore")}: {item.score > 0 ? `+${item.score}` : item.score}
</Tag>
)}
{item.rewardPoints !== undefined && (
<Tag color="processing" style={{ margin: 0 }}>
: {item.rewardPoints}
{t("board.metrics.rewardPoints")}: {item.rewardPoints}
</Tag>
)}
{item.weekChange !== undefined && (
<Tag color={item.weekChange >= 0 ? "success" : "error"} style={{ margin: 0 }}>
7: {item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange}
{t("board.metrics.weekChange")}: {item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange}
</Tag>
)}
{item.weekDeducted !== undefined && (
<Tag color="gold" style={{ margin: 0 }}>
7: {item.weekDeducted}
{t("board.metrics.weekDeducted")}: {item.weekDeducted}
</Tag>
)}
{item.answeredCount !== undefined && (
<Tag color="cyan" style={{ margin: 0 }}>
: {item.answeredCount}
{t("board.metrics.todayAnswered")}: {item.answeredCount}
</Tag>
)}
</div>
+12
View File
@@ -578,6 +578,18 @@
"applyPreset": "Apply Preset",
"run": "Run SQL",
"runAll": "Run All",
"viewModes": {
"list": "List View",
"card": "Card View",
"grid": "Grid View"
},
"metrics": {
"totalScore": "Score",
"rewardPoints": "Reward",
"weekChange": "7d",
"weekDeducted": "7d Deduct",
"todayAnswered": "Today"
},
"sqlPlaceholder": "Enter SQL (single SELECT / WITH query only)",
"templateHint": "Template variables: {{today_start}}, {{since_7d}}, {{since_30d}}, {{now}}",
"templateDescription": "Use template variables directly in SQL. They will be replaced with ISO timestamps at runtime for both SQLite and PostgreSQL.",
+12
View File
@@ -578,6 +578,18 @@
"applyPreset": "套用预设",
"run": "运行 SQL",
"runAll": "运行全部",
"viewModes": {
"list": "列表视图",
"card": "卡片视图",
"grid": "网格视图"
},
"metrics": {
"totalScore": "总分",
"rewardPoints": "奖励分",
"weekChange": "近7天",
"weekDeducted": "近7天扣分",
"todayAnswered": "今日回答"
},
"sqlPlaceholder": "输入 SQL(仅支持单条 SELECT / WITH 查询)",
"templateHint": "支持时间模板变量:{{today_start}}、{{since_7d}}、{{since_30d}}、{{now}}",
"templateDescription": "可直接在 SQL 中使用模板变量,运行时会自动替换为 ISO 时间,兼容 SQLite 与 PostgreSQL。",