mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 21:14:21 +08:00
feat: 沉浸模式新增底部悬浮操作栏
This commit is contained in:
@@ -270,6 +270,7 @@ export function ContentArea({
|
|||||||
<Home
|
<Home
|
||||||
canEdit={permission === "admin" || permission === "points"}
|
canEdit={permission === "admin" || permission === "points"}
|
||||||
isPortraitMode={isPortraitMode}
|
isPortraitMode={isPortraitMode}
|
||||||
|
immersiveMode={immersiveMode}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+342
-166
@@ -91,9 +91,14 @@ const T9_KEY_MAP: Record<string, string> = {
|
|||||||
interface HomeProps {
|
interface HomeProps {
|
||||||
canEdit: boolean
|
canEdit: boolean
|
||||||
isPortraitMode?: 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 { t } = useTranslation()
|
||||||
const breakpoint = useResponsive()
|
const breakpoint = useResponsive()
|
||||||
const isMobile = breakpoint === "xs" || breakpoint === "sm"
|
const isMobile = breakpoint === "xs" || breakpoint === "sm"
|
||||||
@@ -2136,17 +2141,22 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const batchToolbar = rewardMode ? null : !batchMode ? (
|
const batchToolbar = rewardMode ? null : !batchMode ? (
|
||||||
<Button onClick={handleEnterBatchMode} disabled={!canEdit}>
|
<Button onClick={handleEnterBatchMode} disabled={!canEdit} style={{ borderRadius: "999px" }}>
|
||||||
{t("home.multiSelect")}
|
{t("home.multiSelect")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Space size={8} wrap>
|
<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")}
|
{t("home.selectAll")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleClearSelectedStudents}
|
onClick={handleClearSelectedStudents}
|
||||||
disabled={!canEdit || selectedStudentIds.length === 0}
|
disabled={!canEdit || selectedStudentIds.length === 0}
|
||||||
|
style={{ borderRadius: "999px" }}
|
||||||
>
|
>
|
||||||
{t("home.clearSelected")}
|
{t("home.clearSelected")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -2154,10 +2164,13 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleOpenBatchOperation}
|
onClick={handleOpenBatchOperation}
|
||||||
disabled={!canEdit || selectedStudentIds.length === 0}
|
disabled={!canEdit || selectedStudentIds.length === 0}
|
||||||
|
style={{ borderRadius: "999px" }}
|
||||||
>
|
>
|
||||||
{t("home.batchOperate")}
|
{t("home.batchOperate")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleExitBatchMode}>{t("common.cancel")}</Button>
|
<Button onClick={handleExitBatchMode} style={{ borderRadius: "999px" }}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2165,6 +2178,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: isPortraitMode ? "16px" : "24px",
|
padding: isPortraitMode ? "16px" : "24px",
|
||||||
|
paddingBottom: immersiveMode ? (isPortraitMode ? "108px" : "124px") : undefined,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
maxWidth: isPortraitMode ? "100%" : "1200px",
|
maxWidth: isPortraitMode ? "100%" : "1200px",
|
||||||
@@ -2176,7 +2190,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: "32px",
|
marginBottom: immersiveMode ? "8px" : "32px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -2184,178 +2198,182 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
{!immersiveMode && (
|
||||||
<h2 style={{ margin: 0, color: "var(--ss-text-main)", fontSize: "24px" }}>
|
<>
|
||||||
{rewardMode ? t("rewardExchange.title") : t("home.title")}
|
<div>
|
||||||
</h2>
|
<h2 style={{ margin: 0, color: "var(--ss-text-main)", fontSize: "24px" }}>
|
||||||
<p style={{ margin: "4px 0 0", color: "var(--ss-text-secondary)", fontSize: "13px" }}>
|
{rewardMode ? t("rewardExchange.title") : t("home.title")}
|
||||||
{t("home.subtitle", { count: students.length })}
|
</h2>
|
||||||
</p>
|
<p style={{ margin: "4px 0 0", color: "var(--ss-text-secondary)", fontSize: "13px" }}>
|
||||||
</div>
|
{t("home.subtitle", { count: students.length })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
size="middle"
|
size="middle"
|
||||||
align="start"
|
align="start"
|
||||||
wrap
|
wrap
|
||||||
style={{
|
style={{
|
||||||
width: isPortraitMode ? "100%" : undefined,
|
width: isPortraitMode ? "100%" : undefined,
|
||||||
justifyContent: isPortraitMode ? "flex-start" : undefined,
|
justifyContent: isPortraitMode ? "flex-start" : undefined,
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={searchAreaRef}
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
width: isMobile ? "100%" : isTablet ? "180px" : "220px",
|
|
||||||
minWidth: isMobile ? "100%" : isTablet ? "150px" : "180px",
|
|
||||||
flexShrink: isMobile ? 0 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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: "100%" }}
|
|
||||||
/>
|
|
||||||
{!disableSearchKeyboard && showPinyinKeyboard && (
|
|
||||||
<div
|
<div
|
||||||
|
ref={searchAreaRef}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "relative",
|
||||||
top: "calc(100% + 8px)",
|
width: isMobile ? "100%" : isTablet ? "180px" : "220px",
|
||||||
left: isPortraitMode ? "0" : "50%",
|
minWidth: isMobile ? "100%" : isTablet ? "150px" : "180px",
|
||||||
transform: isPortraitMode ? "none" : "translateX(-50%)",
|
flexShrink: isMobile ? 0 : 1,
|
||||||
width: isPortraitMode
|
|
||||||
? `min(calc(100vw - 32px), ${searchKeyboardLayout === "qwerty26" ? "288px" : "220px"})`
|
|
||||||
: 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" }}>
|
<Input
|
||||||
{searchKeyboardLayout === "qwerty26"
|
value={searchKeyword}
|
||||||
? qwertyKeyRows.map((row, rowIndex) => (
|
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||||
<div
|
onFocus={() => {
|
||||||
key={`qwerty-row-${rowIndex}`}
|
if (!disableSearchKeyboard) setShowPinyinKeyboard(true)
|
||||||
style={{
|
}}
|
||||||
display: "grid",
|
onClick={() => {
|
||||||
gridTemplateColumns: `repeat(${row.length}, 1fr)`,
|
if (!disableSearchKeyboard) setShowPinyinKeyboard(true)
|
||||||
gap: "7px",
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Escape") setShowPinyinKeyboard(false)
|
||||||
{row.map((keyItem) => (
|
}}
|
||||||
<Button
|
placeholder={t("home.searchPlaceholder")}
|
||||||
key={keyItem}
|
prefix={<SearchOutlined />}
|
||||||
size="small"
|
allowClear
|
||||||
onClick={() => handleSearchKeyPress(keyItem)}
|
style={{ width: "100%" }}
|
||||||
style={{ height: "32px", fontSize: "12px", padding: 0 }}
|
/>
|
||||||
|
{!disableSearchKeyboard && showPinyinKeyboard && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "calc(100% + 8px)",
|
||||||
|
left: isPortraitMode ? "0" : "50%",
|
||||||
|
transform: isPortraitMode ? "none" : "translateX(-50%)",
|
||||||
|
width: isPortraitMode
|
||||||
|
? `min(calc(100vw - 32px), ${searchKeyboardLayout === "qwerty26" ? "288px" : "220px"})`
|
||||||
|
: 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-row-${rowIndex}`}
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: `repeat(${row.length}, 1fr)`,
|
||||||
|
gap: "7px",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{keyItem === "⌫" ? "⌫" : keyItem.toUpperCase()}
|
{row.map((keyItem) => (
|
||||||
</Button>
|
<Button
|
||||||
))}
|
key={keyItem}
|
||||||
</div>
|
size="small"
|
||||||
))
|
onClick={() => handleSearchKeyPress(keyItem)}
|
||||||
: t9KeyRows.map((row, rowIndex) => (
|
style={{ height: "32px", fontSize: "12px", padding: 0 }}
|
||||||
<div
|
>
|
||||||
key={`pinyin-row-${rowIndex}`}
|
{keyItem === "⌫" ? "⌫" : keyItem.toUpperCase()}
|
||||||
style={{
|
</Button>
|
||||||
display: "grid",
|
))}
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
</div>
|
||||||
gap: "6px",
|
))
|
||||||
}}
|
: t9KeyRows.map((row, rowIndex) => (
|
||||||
>
|
<div
|
||||||
{row.map((keyItem) => (
|
key={`pinyin-row-${rowIndex}`}
|
||||||
<Button
|
style={{
|
||||||
key={keyItem.digit}
|
display: "grid",
|
||||||
size="small"
|
gridTemplateColumns: "repeat(3, 1fr)",
|
||||||
onClick={() => handleSearchKeyPress(keyItem.digit)}
|
gap: "6px",
|
||||||
style={{ height: "28px", fontSize: "11px", padding: 0 }}
|
}}
|
||||||
>
|
>
|
||||||
{keyItem.digit === "⌫" ? "⌫" : `${keyItem.digit} ${keyItem.letters}`}
|
{row.map((keyItem) => (
|
||||||
</Button>
|
<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>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={sortType}
|
value={sortType}
|
||||||
onChange={(v) => setSortType(v as SortType)}
|
onChange={(v) => setSortType(v as SortType)}
|
||||||
style={{
|
style={{
|
||||||
width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px",
|
width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px",
|
||||||
minWidth: isMobile ? "100px" : "120px",
|
minWidth: isMobile ? "100px" : "120px",
|
||||||
flexShrink: isMobile ? 1 : 0,
|
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") },
|
||||||
{ value: "group", label: t("home.sortBy.group") },
|
{ value: "group", label: t("home.sortBy.group") },
|
||||||
{ value: "score", label: t("home.sortBy.score") },
|
{ value: "score", label: t("home.sortBy.score") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={layoutType}
|
value={layoutType}
|
||||||
onChange={(v) => setLayoutType(v as LayoutType)}
|
onChange={(v) => setLayoutType(v as LayoutType)}
|
||||||
style={{
|
style={{
|
||||||
width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px",
|
width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px",
|
||||||
minWidth: isMobile ? "100px" : "120px",
|
minWidth: isMobile ? "100px" : "120px",
|
||||||
flexShrink: isMobile ? 1 : 0,
|
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") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<UndoOutlined />}
|
icon={<UndoOutlined />}
|
||||||
onClick={handleUndoLastEvent}
|
onClick={handleUndoLastEvent}
|
||||||
loading={undoLoading}
|
loading={undoLoading}
|
||||||
disabled={!canEdit || !latestEvent || rewardMode}
|
disabled={!canEdit || !latestEvent || rewardMode}
|
||||||
title={
|
title={
|
||||||
latestEvent
|
latestEvent
|
||||||
? t("home.undoLastHint", {
|
? t("home.undoLastHint", {
|
||||||
name: latestEvent.student_name,
|
name: latestEvent.student_name,
|
||||||
delta: latestEvent.delta > 0 ? `+${latestEvent.delta}` : latestEvent.delta,
|
delta: latestEvent.delta > 0 ? `+${latestEvent.delta}` : latestEvent.delta,
|
||||||
})
|
})
|
||||||
: t("home.undoUnavailable")
|
: t("home.undoUnavailable")
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
flexShrink: isMobile ? 1 : 0,
|
flexShrink: isMobile ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("home.undoLastAction")}
|
{t("home.undoLastAction")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type={rewardMode ? "default" : "primary"}
|
type={rewardMode ? "default" : "primary"}
|
||||||
onClick={handleToggleRewardMode}
|
onClick={handleToggleRewardMode}
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
style={{
|
style={{
|
||||||
flexShrink: isMobile ? 1 : 0,
|
flexShrink: isMobile ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{rewardMode ? t("rewardExchange.exitMode") : t("rewardExchange.enterMode")}
|
{rewardMode ? t("rewardExchange.exitMode") : t("rewardExchange.enterMode")}
|
||||||
</Button>
|
</Button>
|
||||||
{batchToolbar}
|
{batchToolbar}
|
||||||
</Space>
|
</Space>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderQuickNav()}
|
{renderQuickNav()}
|
||||||
@@ -2449,6 +2467,164 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
)}
|
)}
|
||||||
</Modal>
|
</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 ? (
|
{isPortraitMode ? (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
|
|||||||
Reference in New Issue
Block a user