mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +08:00
feat: 支持移动端底部导航自定义与更多收纳
This commit is contained in:
+180
-43
@@ -1,5 +1,16 @@
|
||||
import { Layout, Modal, Input, message, ConfigProvider, theme as antTheme } from "antd"
|
||||
import { HomeOutlined, SettingOutlined } from "@ant-design/icons"
|
||||
import {
|
||||
HomeOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
HistoryOutlined,
|
||||
SyncOutlined,
|
||||
AppstoreAddOutlined,
|
||||
ApartmentOutlined,
|
||||
UnorderedListOutlined,
|
||||
FileTextOutlined,
|
||||
MoreOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { HashRouter, useLocation, useNavigate, Routes, Route } from "react-router-dom"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -7,6 +18,13 @@ import { Sidebar } from "./components/Sidebar"
|
||||
import { ContentArea } from "./components/ContentArea"
|
||||
import { OOBE } from "./components/OOBE/OOBE"
|
||||
import { ThemeProvider, useTheme } from "./contexts/ThemeContext"
|
||||
import {
|
||||
MOBILE_NAV_ITEMS,
|
||||
MobileNavKey,
|
||||
sanitizeMobileNavKeys,
|
||||
} from "./shared/mobileNavigation"
|
||||
|
||||
const DEFAULT_MOBILE_BOTTOM_NAV_ITEMS: MobileNavKey[] = MOBILE_NAV_ITEMS.map((item) => item.key)
|
||||
|
||||
function MainContent(): React.JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
@@ -59,6 +77,10 @@ function MainContent(): React.JSX.Element {
|
||||
const [authVisible, setAuthVisible] = useState(false)
|
||||
const [authPassword, setAuthPassword] = useState("")
|
||||
const [authLoading, setAuthLoading] = useState(false)
|
||||
const [mobileBottomNavItems, setMobileBottomNavItems] = useState<MobileNavKey[]>(
|
||||
DEFAULT_MOBILE_BOTTOM_NAV_ITEMS
|
||||
)
|
||||
const [moreNavVisible, setMoreNavVisible] = useState(false)
|
||||
const [isPortraitMode] = useState(defaultPortraitMode)
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultPortraitMode)
|
||||
const [floatingSidebarExpanded, setFloatingSidebarExpanded] = useState(false)
|
||||
@@ -107,11 +129,49 @@ function MainContent(): React.JSX.Element {
|
||||
setHasAnyPassword(anyPwd)
|
||||
if (anyPwd && authRes.data.permission === "view") setAuthVisible(true)
|
||||
}
|
||||
const settingsRes = await (window as any).api.getAllSettings()
|
||||
if (settingsRes?.success && settingsRes.data) {
|
||||
setMobileBottomNavItems(
|
||||
sanitizeMobileNavKeys(
|
||||
settingsRes.data.mobile_bottom_nav_items,
|
||||
DEFAULT_MOBILE_BOTTOM_NAV_ITEMS
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loadAuthAndSettings()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const api = (window as any).api
|
||||
if (!api || typeof api.onSettingChanged !== "function") return
|
||||
|
||||
let disposed = false
|
||||
let unlisten: (() => void) | null = null
|
||||
|
||||
api
|
||||
.onSettingChanged((change: { key?: string; value?: unknown }) => {
|
||||
if (change?.key !== "mobile_bottom_nav_items") return
|
||||
setMobileBottomNavItems(
|
||||
sanitizeMobileNavKeys(change.value, DEFAULT_MOBILE_BOTTOM_NAV_ITEMS)
|
||||
)
|
||||
})
|
||||
.then((fn: () => void) => {
|
||||
if (disposed) {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
unlisten = fn
|
||||
})
|
||||
.catch(() => void 0)
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
if (unlisten) unlisten()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const api = (window as any).api
|
||||
if (!api || !isIosDevice) return
|
||||
@@ -309,6 +369,7 @@ function MainContent(): React.JSX.Element {
|
||||
|
||||
const onMenuChange = (v: string) => {
|
||||
const key = String(v)
|
||||
setMoreNavVisible(false)
|
||||
if (immersiveMode && key !== "home") return
|
||||
if (key === "home") navigate("/")
|
||||
if (key === "students") navigate("/students")
|
||||
@@ -355,6 +416,45 @@ function MainContent(): React.JSX.Element {
|
||||
const brandColor = currentTheme?.config?.tdesign?.brandColor || "#0052D9"
|
||||
const isMobileDevice = isIosDevice || isAndroidDevice
|
||||
const showMobileBottomNav = isPortraitMode && !immersiveMode
|
||||
const mobileBottomNavIconMap: Record<MobileNavKey, React.ReactNode> = {
|
||||
home: <HomeOutlined style={{ fontSize: "18px" }} />,
|
||||
students: <UserOutlined style={{ fontSize: "18px" }} />,
|
||||
score: <HistoryOutlined style={{ fontSize: "18px" }} />,
|
||||
"auto-score": <SyncOutlined style={{ fontSize: "18px" }} />,
|
||||
"reward-settings": <AppstoreAddOutlined style={{ fontSize: "18px" }} />,
|
||||
boards: <ApartmentOutlined style={{ fontSize: "18px" }} />,
|
||||
leaderboard: <UnorderedListOutlined style={{ fontSize: "18px" }} />,
|
||||
settlements: <FileTextOutlined style={{ fontSize: "18px" }} />,
|
||||
reasons: <UnorderedListOutlined style={{ fontSize: "18px" }} />,
|
||||
settings: <SettingOutlined style={{ fontSize: "18px" }} />,
|
||||
}
|
||||
|
||||
const mobileBottomVisibleItems = useMemo(() => {
|
||||
const preferredKeys = sanitizeMobileNavKeys(
|
||||
mobileBottomNavItems,
|
||||
DEFAULT_MOBILE_BOTTOM_NAV_ITEMS
|
||||
).slice()
|
||||
for (const baseKey of ["home", "settings"] as const) {
|
||||
if (!preferredKeys.includes(baseKey)) {
|
||||
preferredKeys.push(baseKey)
|
||||
}
|
||||
}
|
||||
|
||||
return preferredKeys
|
||||
.map((key) => MOBILE_NAV_ITEMS.find((item) => item.key === key))
|
||||
.filter((item): item is (typeof MOBILE_NAV_ITEMS)[number] => Boolean(item))
|
||||
.filter((item) => !(item.adminOnly && permission !== "admin"))
|
||||
}, [mobileBottomNavItems, permission])
|
||||
|
||||
const mobileBottomPrimaryItems = useMemo(
|
||||
() => mobileBottomVisibleItems.slice(0, 4),
|
||||
[mobileBottomVisibleItems]
|
||||
)
|
||||
const mobileBottomOverflowItems = useMemo(
|
||||
() => mobileBottomVisibleItems.slice(4),
|
||||
[mobileBottomVisibleItems]
|
||||
)
|
||||
const isMoreActive = mobileBottomOverflowItems.some((item) => item.key === activeMenu)
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
@@ -423,54 +523,91 @@ function MainContent(): React.JSX.Element {
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", height: "60px" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMenuChange("home")}
|
||||
style={{
|
||||
flex: 1,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color:
|
||||
activeMenu === "home"
|
||||
{mobileBottomPrimaryItems.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => onMenuChange(item.key)}
|
||||
style={{
|
||||
flex: 1,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color:
|
||||
activeMenu === item.key
|
||||
? "var(--ant-color-primary)"
|
||||
: "var(--ss-text-secondary, var(--ss-text-main))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "2px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
{mobileBottomNavIconMap[item.key]}
|
||||
<span>{t(item.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
{mobileBottomOverflowItems.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMoreNavVisible(true)}
|
||||
style={{
|
||||
flex: 1,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: isMoreActive
|
||||
? "var(--ant-color-primary)"
|
||||
: "var(--ss-text-secondary, var(--ss-text-main))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "2px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
<HomeOutlined style={{ fontSize: "18px" }} />
|
||||
<span>{t("sidebar.home")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMenuChange("settings")}
|
||||
style={{
|
||||
flex: 1,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color:
|
||||
activeMenu === "home"
|
||||
? "var(--ss-text-secondary, var(--ss-text-main))"
|
||||
: "var(--ant-color-primary)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "2px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
<SettingOutlined style={{ fontSize: "18px" }} />
|
||||
<span>{t("sidebar.settings")}</span>
|
||||
</button>
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "2px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
<MoreOutlined style={{ fontSize: "18px" }} />
|
||||
<span>{t("common.more", "更多")}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={t("common.more", "更多")}
|
||||
open={moreNavVisible}
|
||||
onCancel={() => setMoreNavVisible(false)}
|
||||
footer={null}
|
||||
width={360}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
{mobileBottomOverflowItems.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => onMenuChange(item.key)}
|
||||
style={{
|
||||
border: "1px solid var(--ss-border-color)",
|
||||
borderRadius: "10px",
|
||||
background: "var(--ss-card-bg)",
|
||||
color: "var(--ss-text-main)",
|
||||
height: "44px",
|
||||
padding: "0 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{mobileBottomNavIconMap[item.key]}
|
||||
<span>{t(item.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<OOBE visible={wizardVisible} onComplete={() => setWizardVisible(false)} />
|
||||
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user