优化首页按钮组的响应式布局

- 启用 Space 组件的 wrap 属性,允许按钮在空间不足时自动换行
- 使用 useResponsive hook 检测屏幕尺寸
- 根据屏幕尺寸动态调整搜索框、下拉框和按钮的宽度
- 在移动端和平板设备上使用更紧凑的布局
- 添加 flexShrink 属性防止按钮被过度压缩
This commit is contained in:
Fox_block
2026-03-20 21:28:58 +08:00
parent 73db5778f1
commit 5a483f781e
+171 -89
View File
@@ -1,9 +1,22 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react" import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"
import { Card, Space, Button, Tag, Input, Select, Modal, Drawer, message, InputNumber, Divider } from "antd" import {
Card,
Space,
Button,
Tag,
Input,
Select,
Modal,
Drawer,
message,
InputNumber,
Divider,
} from "antd"
import { SearchOutlined, DeleteOutlined, UndoOutlined } from "@ant-design/icons" import { SearchOutlined, DeleteOutlined, UndoOutlined } from "@ant-design/icons"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { match, pinyin } from "pinyin-pro" import { match, pinyin } from "pinyin-pro"
import { getAvatarFromExtraJson } from "../utils/studentAvatar" import { getAvatarFromExtraJson } from "../utils/studentAvatar"
import { useResponsive } from "../hooks/useResponsive"
interface student { interface student {
id: number id: number
@@ -81,6 +94,9 @@ interface HomeProps {
export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) => { export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) => {
const { t } = useTranslation() const { t } = useTranslation()
const breakpoint = useResponsive()
const isMobile = breakpoint === "xs" || breakpoint === "sm"
const isTablet = breakpoint === "md"
const [students, setStudents] = useState<student[]>([]) const [students, setStudents] = useState<student[]>([])
const [reasons, setReasons] = useState<reason[]>([]) const [reasons, setReasons] = useState<reason[]>([])
const [rewards, setRewards] = useState<rewardSetting[]>([]) const [rewards, setRewards] = useState<rewardSetting[]>([])
@@ -184,7 +200,8 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
if (!(window as any).api) return if (!(window as any).api) return
const res = await (window as any).api.queryEvents({ limit: 1 }) const res = await (window as any).api.queryEvents({ limit: 1 })
if (res.success) { if (res.success) {
const latest = Array.isArray(res.data) && res.data.length > 0 ? (res.data[0] as scoreEvent) : null const latest =
Array.isArray(res.data) && res.data.length > 0 ? (res.data[0] as scoreEvent) : null
setLatestEvent(latest) setLatestEvent(latest)
logHome("fetchLatestEvent:response", { logHome("fetchLatestEvent:response", {
hasLatest: Boolean(latest), hasLatest: Boolean(latest),
@@ -243,7 +260,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
api api
.onSettingChanged((change: any) => { .onSettingChanged((change: any) => {
if (change?.key === "search_keyboard_layout" && (change?.value === "t9" || change?.value === "qwerty26")) { if (
change?.key === "search_keyboard_layout" &&
(change?.value === "t9" || change?.value === "qwerty26")
) {
setSearchKeyboardLayout(change.value) setSearchKeyboardLayout(change.value)
} }
if (change?.key === "disable_search_keyboard") { if (change?.key === "disable_search_keyboard") {
@@ -278,7 +298,9 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
setShowPinyinKeyboard(false) setShowPinyinKeyboard(false)
} }
const quickCard = (target as HTMLElement | null)?.closest?.('[data-student-quick-card="true"]') const quickCard = (target as HTMLElement | null)?.closest?.(
'[data-student-quick-card="true"]'
)
if (!quickCard) { if (!quickCard) {
setQuickActionStudentId(null) setQuickActionStudentId(null)
} }
@@ -413,7 +435,8 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
}) })
case "score": case "score":
return filtered.sort( return filtered.sort(
(a, b) => getDisplayPoints(b) - getDisplayPoints(a) || a.name.localeCompare(b.name, "zh-CN") (a, b) =>
getDisplayPoints(b) - getDisplayPoints(a) || a.name.localeCompare(b.name, "zh-CN")
) )
default: default:
return filtered return filtered
@@ -630,7 +653,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
fetchData(true) fetchData(true)
fetchLatestEvent() fetchLatestEvent()
emitDataUpdated("events") emitDataUpdated("events")
logHome("performSubmit:afterSuccessRefreshDispatched", { studentCount: targetStudents.length, delta }) logHome("performSubmit:afterSuccessRefreshDispatched", {
studentCount: targetStudents.length,
delta,
})
} else { } else {
messageApi.warning( messageApi.warning(
t("home.batchPartial", { success: successCount, total: targetStudents.length }) t("home.batchPartial", { success: successCount, total: targetStudents.length })
@@ -848,7 +874,9 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
{rankBadge} {rankBadge}
</div> </div>
)} )}
<div style={{ display: "flex", alignItems: "center", gap: isPortraitMode ? "10px" : "12px" }}> <div
style={{ display: "flex", alignItems: "center", gap: isPortraitMode ? "10px" : "12px" }}
>
{student.avatarUrl ? ( {student.avatarUrl ? (
<img <img
src={student.avatarUrl} src={student.avatarUrl}
@@ -911,40 +939,40 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
pointerEvents: isQuickActionMode ? "auto" : "none", pointerEvents: isQuickActionMode ? "auto" : "none",
}} }}
> >
<Button <Button
type="primary" type="primary"
size="small" size="small"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handleQuickAdjust(student, 1) handleQuickAdjust(student, 1)
}} }}
style={{ style={{
minWidth: "54px", minWidth: "54px",
height: "36px", height: "36px",
borderRadius: "18px", borderRadius: "18px",
fontWeight: 700, fontWeight: 700,
paddingInline: "12px", paddingInline: "12px",
}} }}
> >
+1 +1
</Button> </Button>
<Button <Button
danger danger
size="small" size="small"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handleQuickAdjust(student, -1) handleQuickAdjust(student, -1)
}} }}
style={{ style={{
minWidth: "54px", minWidth: "54px",
height: "36px", height: "36px",
borderRadius: "18px", borderRadius: "18px",
fontWeight: 700, fontWeight: 700,
paddingInline: "12px", paddingInline: "12px",
}} }}
> >
-1 -1
</Button> </Button>
</div> </div>
<div <div
style={{ style={{
@@ -984,7 +1012,9 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
} }
style={{ fontWeight: "bold" }} style={{ fontWeight: "bold" }}
> >
{getDisplayPoints(student) > 0 ? `+${getDisplayPoints(student)}` : getDisplayPoints(student)} {getDisplayPoints(student) > 0
? `+${getDisplayPoints(student)}`
: getDisplayPoints(student)}
</Tag> </Tag>
</div> </div>
</div> </div>
@@ -1037,8 +1067,7 @@ 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: background: isQuickActionMode || isSelected ? "rgba(22, 119, 255, 0.06)" : "transparent",
isQuickActionMode || isSelected ? "rgba(22, 119, 255, 0.06)" : "transparent",
transition: "background-color 160ms ease", transition: "background-color 160ms ease",
}} }}
> >
@@ -1102,7 +1131,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
e.stopPropagation() e.stopPropagation()
handleQuickAdjust(student, 1) handleQuickAdjust(student, 1)
}} }}
style={{ minWidth: "44px", height: "26px", borderRadius: "13px", paddingInline: "8px" }} style={{
minWidth: "44px",
height: "26px",
borderRadius: "13px",
paddingInline: "8px",
}}
> >
+1 +1
</Button> </Button>
@@ -1113,7 +1147,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
e.stopPropagation() e.stopPropagation()
handleQuickAdjust(student, -1) handleQuickAdjust(student, -1)
}} }}
style={{ minWidth: "44px", height: "26px", borderRadius: "13px", paddingInline: "8px" }} style={{
minWidth: "44px",
height: "26px",
borderRadius: "13px",
paddingInline: "8px",
}}
> >
-1 -1
</Button> </Button>
@@ -1127,11 +1166,17 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
)} )}
<Tag <Tag
color={ color={
getDisplayPoints(student) > 0 ? "success" : getDisplayPoints(student) < 0 ? "error" : "default" getDisplayPoints(student) > 0
? "success"
: getDisplayPoints(student) < 0
? "error"
: "default"
} }
style={{ fontWeight: "bold", marginInlineEnd: 0 }} style={{ fontWeight: "bold", marginInlineEnd: 0 }}
> >
{getDisplayPoints(student) > 0 ? `+${getDisplayPoints(student)}` : getDisplayPoints(student)} {getDisplayPoints(student) > 0
? `+${getDisplayPoints(student)}`
: getDisplayPoints(student)}
</Tag> </Tag>
</Space> </Space>
)} )}
@@ -1283,7 +1328,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
e.stopPropagation() e.stopPropagation()
handleQuickAdjust(student, 1) handleQuickAdjust(student, 1)
}} }}
style={{ minWidth: "44px", height: "28px", borderRadius: "14px", paddingInline: "8px" }} style={{
minWidth: "44px",
height: "28px",
borderRadius: "14px",
paddingInline: "8px",
}}
> >
+1 +1
</Button> </Button>
@@ -1294,7 +1344,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
e.stopPropagation() e.stopPropagation()
handleQuickAdjust(student, -1) handleQuickAdjust(student, -1)
}} }}
style={{ minWidth: "44px", height: "28px", borderRadius: "14px", paddingInline: "8px" }} style={{
minWidth: "44px",
height: "28px",
borderRadius: "14px",
paddingInline: "8px",
}}
> >
-1 -1
</Button> </Button>
@@ -1333,7 +1388,9 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
} }
style={{ fontWeight: "bold", marginInlineEnd: 0 }} style={{ fontWeight: "bold", marginInlineEnd: 0 }}
> >
{getDisplayPoints(student) > 0 ? `+${getDisplayPoints(student)}` : getDisplayPoints(student)} {getDisplayPoints(student) > 0
? `+${getDisplayPoints(student)}`
: getDisplayPoints(student)}
</Tag> </Tag>
{isSelected && ( {isSelected && (
<Tag color="processing" style={{ marginInlineEnd: 0 }}> <Tag color="processing" style={{ marginInlineEnd: 0 }}>
@@ -2051,35 +2108,31 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
}) })
}, []) }, [])
const batchToolbar = rewardMode const batchToolbar = rewardMode ? null : !batchMode ? (
? null <Button onClick={handleEnterBatchMode} disabled={!canEdit}>
: !batchMode {t("home.multiSelect")}
? ( </Button>
<Button onClick={handleEnterBatchMode} disabled={!canEdit}> ) : (
{t("home.multiSelect")} <Space size={8} wrap>
</Button> <Button onClick={handleSelectAllStudents} disabled={!canEdit || students.length === 0}>
) {t("home.selectAll")}
: ( </Button>
<Space size={8} wrap> <Button
<Button onClick={handleSelectAllStudents} disabled={!canEdit || students.length === 0}> onClick={handleClearSelectedStudents}
{t("home.selectAll")} disabled={!canEdit || selectedStudentIds.length === 0}
</Button> >
<Button {t("home.clearSelected")}
onClick={handleClearSelectedStudents} </Button>
disabled={!canEdit || selectedStudentIds.length === 0} <Button
> type="primary"
{t("home.clearSelected")} onClick={handleOpenBatchOperation}
</Button> disabled={!canEdit || selectedStudentIds.length === 0}
<Button >
type="primary" {t("home.batchOperate")}
onClick={handleOpenBatchOperation} </Button>
disabled={!canEdit || selectedStudentIds.length === 0} <Button onClick={handleExitBatchMode}>{t("common.cancel")}</Button>
> </Space>
{t("home.batchOperate")} )
</Button>
<Button onClick={handleExitBatchMode}>{t("common.cancel")}</Button>
</Space>
)
return ( return (
<div <div
@@ -2116,7 +2169,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
<Space <Space
size="middle" size="middle"
align="start" align="start"
wrap={isPortraitMode} wrap
style={{ style={{
width: isPortraitMode ? "100%" : undefined, width: isPortraitMode ? "100%" : undefined,
justifyContent: isPortraitMode ? "flex-start" : undefined, justifyContent: isPortraitMode ? "flex-start" : undefined,
@@ -2126,8 +2179,9 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
ref={searchAreaRef} ref={searchAreaRef}
style={{ style={{
position: "relative", position: "relative",
width: isPortraitMode ? "100%" : "220px", width: isMobile ? "100%" : isTablet ? "180px" : "220px",
minWidth: isPortraitMode ? "100%" : undefined, minWidth: isMobile ? "100%" : isTablet ? "150px" : "180px",
flexShrink: isMobile ? 0 : 1,
}} }}
> >
<Input <Input
@@ -2193,7 +2247,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
: t9KeyRows.map((row, rowIndex) => ( : t9KeyRows.map((row, rowIndex) => (
<div <div
key={`pinyin-row-${rowIndex}`} key={`pinyin-row-${rowIndex}`}
style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "6px" }} style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "6px",
}}
> >
{row.map((keyItem) => ( {row.map((keyItem) => (
<Button <Button
@@ -2215,7 +2273,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
<Select <Select
value={sortType} value={sortType}
onChange={(v) => setSortType(v as SortType)} onChange={(v) => setSortType(v as SortType)}
style={{ width: isPortraitMode ? "calc(50% - 8px)" : "140px", minWidth: "120px" }} style={{
width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px",
minWidth: isMobile ? "100px" : "120px",
flexShrink: isMobile ? 1 : 0,
}}
options={[ options={[
{ value: "alphabet", label: t("home.sortBy.alphabet") }, { value: "alphabet", label: t("home.sortBy.alphabet") },
{ value: "surname", label: t("home.sortBy.surname") }, { value: "surname", label: t("home.sortBy.surname") },
@@ -2225,7 +2287,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
<Select <Select
value={layoutType} value={layoutType}
onChange={(v) => setLayoutType(v as LayoutType)} onChange={(v) => setLayoutType(v as LayoutType)}
style={{ width: isPortraitMode ? "calc(50% - 8px)" : "140px", minWidth: "120px" }} style={{
width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px",
minWidth: isMobile ? "100px" : "120px",
flexShrink: isMobile ? 1 : 0,
}}
options={[ options={[
{ value: "grouped", label: t("home.layoutBy.grouped") }, { value: "grouped", label: t("home.layoutBy.grouped") },
{ value: "squareGrid", label: t("home.layoutBy.squareGrid") }, { value: "squareGrid", label: t("home.layoutBy.squareGrid") },
@@ -2244,10 +2310,20 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
}) })
: t("home.undoUnavailable") : t("home.undoUnavailable")
} }
style={{
flexShrink: isMobile ? 1 : 0,
}}
> >
{t("home.undoLastAction")} {t("home.undoLastAction")}
</Button> </Button>
<Button type={rewardMode ? "default" : "primary"} onClick={handleToggleRewardMode} disabled={!canEdit}> <Button
type={rewardMode ? "default" : "primary"}
onClick={handleToggleRewardMode}
disabled={!canEdit}
style={{
flexShrink: isMobile ? 1 : 0,
}}
>
{rewardMode ? t("rewardExchange.exitMode") : t("rewardExchange.enterMode")} {rewardMode ? t("rewardExchange.exitMode") : t("rewardExchange.enterMode")}
</Button> </Button>
{batchToolbar} {batchToolbar}
@@ -2307,7 +2383,9 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
{t("rewardExchange.currentRewardPoints", { points: rewardStudent.reward_points })} {t("rewardExchange.currentRewardPoints", { points: rewardStudent.reward_points })}
</div> </div>
{affordableRewards.length === 0 ? ( {affordableRewards.length === 0 ? (
<div style={{ color: "var(--ss-text-secondary)" }}>{t("rewardExchange.noAffordableRewards")}</div> <div style={{ color: "var(--ss-text-secondary)" }}>
{t("rewardExchange.noAffordableRewards")}
</div>
) : ( ) : (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}> <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{affordableRewards.map((reward) => ( {affordableRewards.map((reward) => (
@@ -2328,7 +2406,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
{t("rewardExchange.costLabel", { points: reward.cost_points })} {t("rewardExchange.costLabel", { points: reward.cost_points })}
</div> </div>
</div> </div>
<Button type="primary" loading={redeemLoading} onClick={() => handleRedeemReward(reward)}> <Button
type="primary"
loading={redeemLoading}
onClick={() => handleRedeemReward(reward)}
>
{t("rewardExchange.redeemNow")} {t("rewardExchange.redeemNow")}
</Button> </Button>
</div> </div>