feat: 沉浸模式新增底部悬浮操作栏

This commit is contained in:
JSR
2026-03-27 21:00:45 +08:00
parent 3def710468
commit 51d22bba94
2 changed files with 343 additions and 166 deletions
+1
View File
@@ -270,6 +270,7 @@ export function ContentArea({
<Home
canEdit={permission === "admin" || permission === "points"}
isPortraitMode={isPortraitMode}
immersiveMode={immersiveMode}
/>
}
/>
+181 -5
View File
@@ -91,9 +91,14 @@ const T9_KEY_MAP: Record<string, string> = {
interface HomeProps {
canEdit: boolean
isPortraitMode?: boolean
immersiveMode?: boolean
}
export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) => {
export const Home: React.FC<HomeProps> = ({
canEdit,
isPortraitMode = false,
immersiveMode = false,
}) => {
const { t } = useTranslation()
const breakpoint = useResponsive()
const isMobile = breakpoint === "xs" || breakpoint === "sm"
@@ -2136,17 +2141,22 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
}, [])
const batchToolbar = rewardMode ? null : !batchMode ? (
<Button onClick={handleEnterBatchMode} disabled={!canEdit}>
<Button onClick={handleEnterBatchMode} disabled={!canEdit} style={{ borderRadius: "999px" }}>
{t("home.multiSelect")}
</Button>
) : (
<Space size={8} wrap>
<Button onClick={handleSelectAllStudents} disabled={!canEdit || students.length === 0}>
<Button
onClick={handleSelectAllStudents}
disabled={!canEdit || students.length === 0}
style={{ borderRadius: "999px" }}
>
{t("home.selectAll")}
</Button>
<Button
onClick={handleClearSelectedStudents}
disabled={!canEdit || selectedStudentIds.length === 0}
style={{ borderRadius: "999px" }}
>
{t("home.clearSelected")}
</Button>
@@ -2154,10 +2164,13 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
type="primary"
onClick={handleOpenBatchOperation}
disabled={!canEdit || selectedStudentIds.length === 0}
style={{ borderRadius: "999px" }}
>
{t("home.batchOperate")}
</Button>
<Button onClick={handleExitBatchMode}>{t("common.cancel")}</Button>
<Button onClick={handleExitBatchMode} style={{ borderRadius: "999px" }}>
{t("common.cancel")}
</Button>
</Space>
)
@@ -2165,6 +2178,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
<div
style={{
padding: isPortraitMode ? "16px" : "24px",
paddingBottom: immersiveMode ? (isPortraitMode ? "108px" : "124px") : undefined,
width: "100%",
boxSizing: "border-box",
maxWidth: isPortraitMode ? "100%" : "1200px",
@@ -2176,7 +2190,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
{contextHolder}
<div
style={{
marginBottom: "32px",
marginBottom: immersiveMode ? "8px" : "32px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
@@ -2184,6 +2198,8 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
flexWrap: "wrap",
}}
>
{!immersiveMode && (
<>
<div>
<h2 style={{ margin: 0, color: "var(--ss-text-main)", fontSize: "24px" }}>
{rewardMode ? t("rewardExchange.title") : t("home.title")}
@@ -2356,6 +2372,8 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
</Button>
{batchToolbar}
</Space>
</>
)}
</div>
{renderQuickNav()}
@@ -2449,6 +2467,164 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
)}
</Modal>
{immersiveMode && (
<div
style={{
position: "fixed",
left: "50%",
bottom: isPortraitMode ? "12px" : "16px",
transform: "translateX(-50%)",
zIndex: 1100,
width: "min(calc(100vw - 20px), 1120px)",
borderRadius: "999px",
border: "1px solid color-mix(in srgb, var(--ss-border-color) 80%, transparent)",
backgroundColor: "var(--ss-card-bg)",
background: "color-mix(in srgb, var(--ss-card-bg) 78%, transparent)",
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
boxShadow: "0 8px 30px rgba(0, 0, 0, 0.16)",
padding: "10px 12px",
}}
>
<div
ref={searchAreaRef}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
overflowX: "auto",
scrollbarWidth: "thin",
}}
>
<Input
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onFocus={() => {
if (!disableSearchKeyboard) setShowPinyinKeyboard(true)
}}
onClick={() => {
if (!disableSearchKeyboard) setShowPinyinKeyboard(true)
}}
onKeyDown={(e) => {
if (e.key === "Escape") setShowPinyinKeyboard(false)
}}
placeholder={t("home.searchPlaceholder")}
prefix={<SearchOutlined />}
allowClear
style={{ width: isPortraitMode ? "170px" : "220px", borderRadius: "999px", flexShrink: 0 }}
/>
<Select
value={sortType}
onChange={(v) => setSortType(v as SortType)}
style={{ width: 126, flexShrink: 0 }}
options={[
{ value: "alphabet", label: t("home.sortBy.alphabet") },
{ value: "surname", label: t("home.sortBy.surname") },
{ value: "group", label: t("home.sortBy.group") },
{ value: "score", label: t("home.sortBy.score") },
]}
/>
<Select
value={layoutType}
onChange={(v) => setLayoutType(v as LayoutType)}
style={{ width: 126, flexShrink: 0 }}
options={[
{ value: "grouped", label: t("home.layoutBy.grouped") },
{ value: "squareGrid", label: t("home.layoutBy.squareGrid") },
]}
/>
<Button
icon={<UndoOutlined />}
onClick={handleUndoLastEvent}
loading={undoLoading}
disabled={!canEdit || !latestEvent || rewardMode}
title={
latestEvent
? t("home.undoLastHint", {
name: latestEvent.student_name,
delta: latestEvent.delta > 0 ? `+${latestEvent.delta}` : latestEvent.delta,
})
: t("home.undoUnavailable")
}
style={{ borderRadius: "999px", flexShrink: 0 }}
>
{t("home.undoLastAction")}
</Button>
<Button
type={rewardMode ? "default" : "primary"}
onClick={handleToggleRewardMode}
disabled={!canEdit}
style={{ borderRadius: "999px", flexShrink: 0 }}
>
{rewardMode ? t("rewardExchange.exitMode") : t("rewardExchange.enterMode")}
</Button>
<div style={{ flexShrink: 0 }}>{batchToolbar}</div>
{!disableSearchKeyboard && showPinyinKeyboard && (
<div
style={{
position: "absolute",
bottom: "calc(100% + 8px)",
left: isPortraitMode ? "8px" : "12px",
width: searchKeyboardLayout === "qwerty26" ? "288px" : "220px",
padding: "8px",
borderRadius: "10px",
border: "1px solid var(--ss-border-color)",
backgroundColor: "var(--ss-card-bg)",
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.12)",
zIndex: 20,
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
{searchKeyboardLayout === "qwerty26"
? qwertyKeyRows.map((row, rowIndex) => (
<div
key={`qwerty-immersive-row-${rowIndex}`}
style={{
display: "grid",
gridTemplateColumns: `repeat(${row.length}, 1fr)`,
gap: "7px",
}}
>
{row.map((keyItem) => (
<Button
key={keyItem}
size="small"
onClick={() => handleSearchKeyPress(keyItem)}
style={{ height: "32px", fontSize: "12px", padding: 0 }}
>
{keyItem === "⌫" ? "⌫" : keyItem.toUpperCase()}
</Button>
))}
</div>
))
: t9KeyRows.map((row, rowIndex) => (
<div
key={`pinyin-immersive-row-${rowIndex}`}
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "6px",
}}
>
{row.map((keyItem) => (
<Button
key={keyItem.digit}
size="small"
onClick={() => handleSearchKeyPress(keyItem.digit)}
style={{ height: "28px", fontSize: "11px", padding: 0 }}
>
{keyItem.digit === "⌫" ? "⌫" : `${keyItem.digit} ${keyItem.letters}`}
</Button>
))}
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
{isPortraitMode ? (
<Drawer
title={