This commit is contained in:
NanGua-QWQ
2026-03-27 21:53:48 +08:00
4 changed files with 126 additions and 28 deletions
+9 -2
View File
@@ -366,7 +366,14 @@ function MainContent(): React.JSX.Element {
>
{contextHolder}
<Layout style={{ height: "100%", flexDirection: "row", overflow: "hidden" }}>
{!immersiveMode && (
<div
className={`ss-immersive-sidebar ${immersiveMode ? "is-hidden" : "is-visible"}`}
style={
{
"--ss-sidebar-width": `${sidebarCollapsed ? 64 : 200}px`,
} as React.CSSProperties
}
>
<Sidebar
activeMenu={activeMenu}
permission={permission}
@@ -376,7 +383,7 @@ function MainContent(): React.JSX.Element {
floatingExpanded={floatingSidebarExpanded}
onFloatingExpandedChange={setFloatingSidebarExpanded}
/>
)}
</div>
<ContentArea
permission={permission}
hasAnyPassword={hasAnyPassword}
+51
View File
@@ -225,3 +225,54 @@ html.platform-macos #root {
.ss-group-board-modal .ant-modal-body {
overflow: hidden;
}
.ss-immersive-sidebar {
--ss-sidebar-width: 200px;
height: 100%;
display: flex;
flex-shrink: 0;
width: var(--ss-sidebar-width);
overflow: hidden;
background: var(--ss-sidebar-bg);
opacity: 1;
transform: translateX(0);
transition:
width 260ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 220ms ease,
transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
will-change: width, opacity, transform;
}
.ss-immersive-sidebar .ant-layout-sider {
height: 100% !important;
}
.ss-immersive-sidebar.is-hidden {
width: 0;
opacity: 0;
transform: translateX(-10px);
pointer-events: none;
}
.ss-immersive-toolbar {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
transition:
width 420ms cubic-bezier(0.2, 1.2, 0.26, 1),
opacity 200ms ease,
transform 320ms cubic-bezier(0.2, 1.2, 0.26, 1);
will-change: width, opacity, transform;
}
.ss-immersive-toolbar.is-hidden {
opacity: 0;
transform: translateX(-50%) translateY(16px) scale(0.98);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.ss-immersive-sidebar,
.ss-immersive-toolbar {
transition: none !important;
}
}
+46 -6
View File
@@ -117,6 +117,9 @@ export const Home: React.FC<HomeProps> = ({
const scrollContainerRef = useRef<HTMLDivElement>(null)
const groupRefs = useRef<Record<string, HTMLDivElement | null>>({})
const searchAreaRef = useRef<HTMLDivElement>(null)
const immersiveToolbarRef = useRef<HTMLDivElement>(null)
const immersiveToolbarContentRef = useRef<HTMLDivElement>(null)
const [immersiveToolbarWidth, setImmersiveToolbarWidth] = useState<number | null>(null)
const [selectedStudent, setSelectedStudent] = useState<student | null>(null)
const [batchMode, setBatchMode] = useState(false)
@@ -320,6 +323,33 @@ export const Home: React.FC<HomeProps> = ({
return () => document.removeEventListener("mousedown", onDocumentClick)
}, [])
useEffect(() => {
if (!immersiveMode) return
const contentEl = immersiveToolbarContentRef.current
if (!contentEl) return
let frameId: number | null = null
const updateToolbarWidth = () => {
if (frameId !== null) cancelAnimationFrame(frameId)
frameId = requestAnimationFrame(() => {
const contentWidth = Math.ceil(contentEl.getBoundingClientRect().width)
const nextWidth = contentWidth + 20
setImmersiveToolbarWidth((prev) => (prev === nextWidth ? prev : nextWidth))
})
}
updateToolbarWidth()
const observer = new ResizeObserver(updateToolbarWidth)
observer.observe(contentEl)
window.addEventListener("resize", updateToolbarWidth)
return () => {
observer.disconnect()
window.removeEventListener("resize", updateToolbarWidth)
if (frameId !== null) cancelAnimationFrame(frameId)
}
}, [immersiveMode])
useEffect(() => {
return () => {
if (longPressTimerRef.current !== null) {
@@ -376,6 +406,10 @@ export const Home: React.FC<HomeProps> = ({
setSearchKeyword((prev) => `${prev}${keyValue}`)
}
const getImmersivePopupContainer = useCallback((triggerNode: HTMLElement) => {
return immersiveToolbarRef.current ?? triggerNode.parentElement ?? document.body
}, [])
const getDisplayText = (name: string) => {
if (!name) return ""
return name.length > 2 ? name.substring(name.length - 2) : name
@@ -2467,15 +2501,17 @@ export const Home: React.FC<HomeProps> = ({
)}
</Modal>
{immersiveMode && (
<div
ref={immersiveToolbarRef}
data-immersive-toolbar="true"
className={`ss-immersive-toolbar ${immersiveMode ? "is-visible" : "is-hidden"}`}
aria-hidden={!immersiveMode}
style={{
position: "fixed",
left: "50%",
bottom: isPortraitMode ? "12px" : "16px",
transform: "translateX(-50%)",
zIndex: 1100,
width: "max-content",
width: immersiveToolbarWidth ? `${immersiveToolbarWidth}px` : "max-content",
maxWidth: "calc(100vw - 20px)",
borderRadius: "999px",
border: "1px solid color-mix(in srgb, var(--ss-border-color) 80%, transparent)",
@@ -2485,11 +2521,14 @@ export const Home: React.FC<HomeProps> = ({
WebkitBackdropFilter: "blur(16px)",
boxShadow: "0 8px 30px rgba(0, 0, 0, 0.16)",
padding: "10px",
overflow: "hidden",
overflow: "visible",
}}
>
<div
ref={searchAreaRef}
ref={(node) => {
searchAreaRef.current = node
immersiveToolbarContentRef.current = node
}}
style={{
display: "flex",
alignItems: "center",
@@ -2519,6 +2558,7 @@ export const Home: React.FC<HomeProps> = ({
<Select
value={sortType}
onChange={(v) => setSortType(v as SortType)}
getPopupContainer={getImmersivePopupContainer}
style={{ width: 126, flexShrink: 0 }}
options={[
{ value: "alphabet", label: t("home.sortBy.alphabet") },
@@ -2530,6 +2570,7 @@ export const Home: React.FC<HomeProps> = ({
<Select
value={layoutType}
onChange={(v) => setLayoutType(v as LayoutType)}
getPopupContainer={getImmersivePopupContainer}
style={{ width: 126, flexShrink: 0 }}
options={[
{ value: "grouped", label: t("home.layoutBy.grouped") },
@@ -2626,7 +2667,6 @@ export const Home: React.FC<HomeProps> = ({
)}
</div>
</div>
)}
{isPortraitMode ? (
<Drawer
+2 -2
View File
@@ -423,7 +423,7 @@
"operationTitle": "积分操作:{{name}}",
"submitOperation": "提交操作",
"operationTitleBatch": "积分操作:已选 {{count}} 人",
"undoLastAction": "撤销上一步",
"undoLastAction": "撤销",
"undoLastHint": "撤销上一条:{{name}} {{delta}}",
"undoUnavailable": "当前没有可撤销的积分操作",
"undoLastSuccess": "已撤销上一步积分操作",
@@ -592,7 +592,7 @@
},
"rewardExchange": {
"title": "奖励兑换",
"enterMode": "进入奖励兑换模式",
"enterMode": "奖励兑换",
"exitMode": "退出奖励兑换模式",
"modeHint": "当前为奖励兑换模式,点击学生卡可选择可兑换奖励。",
"normalHint": "当前显示学生原始积分,点击按钮后将显示可兑换积分。",