From 42ae3457b50e829a64cf66d4050b930d8647f6a2 Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:01:03 +0800 Subject: [PATCH] feat(frontend): persist filter state across page reloads (#189) * feat(frontend): persist filter state across page reloads Add useLocalStorageState hook to persist filter/search state in forward, node, tunnel, user, and limit pages. Reset filter clears the persisted value from localStorage. * feat(ui): add collapsible sidebar and smooth animations --- vite-frontend/.env.production | 3 +- .../src/hooks/use-local-storage-state.ts | 51 ++++ vite-frontend/src/layouts/admin.tsx | 85 +++++-- vite-frontend/src/pages/forward.tsx | 234 +++++++++--------- vite-frontend/src/pages/limit.tsx | 6 +- vite-frontend/src/pages/node.tsx | 6 +- vite-frontend/src/pages/tunnel.tsx | 19 +- vite-frontend/src/pages/user.tsx | 6 +- 8 files changed, 253 insertions(+), 157 deletions(-) create mode 100644 vite-frontend/src/hooks/use-local-storage-state.ts diff --git a/vite-frontend/.env.production b/vite-frontend/.env.production index a41b3e9..12cb71f 100644 --- a/vite-frontend/.env.production +++ b/vite-frontend/.env.production @@ -1 +1,2 @@ -VITE_API_BASE= +VITE_GITHUB_REPO=https://github.com/Sagit-chu/flvx +VITE_APP_VERSION=beta diff --git a/vite-frontend/src/hooks/use-local-storage-state.ts b/vite-frontend/src/hooks/use-local-storage-state.ts new file mode 100644 index 0000000..3cbca48 --- /dev/null +++ b/vite-frontend/src/hooks/use-local-storage-state.ts @@ -0,0 +1,51 @@ +import { useCallback, useState, type SetStateAction } from "react"; + +const readStoredValue = (key: string, fallbackValue: T): T => { + try { + const rawValue = localStorage.getItem(key); + + if (rawValue === null) { + return fallbackValue; + } + + return JSON.parse(rawValue) as T; + } catch { + return fallbackValue; + } +}; + +export const useLocalStorageState = ( + key: string, + initialValue: T, +): readonly [T, (value: SetStateAction) => void, () => void] => { + const [value, setValue] = useState(() => + readStoredValue(key, initialValue), + ); + + const setPersistedValue = useCallback( + (nextValue: SetStateAction) => { + setValue((prevValue) => { + const resolvedValue = + typeof nextValue === "function" + ? (nextValue as (value: T) => T)(prevValue) + : nextValue; + + try { + localStorage.setItem(key, JSON.stringify(resolvedValue)); + } catch {} + + return resolvedValue; + }); + }, + [key], + ); + + const resetPersistedValue = useCallback(() => { + setValue(initialValue); + try { + localStorage.removeItem(key); + } catch {} + }, [initialValue, key]); + + return [value, setPersistedValue, resetPersistedValue] as const; +}; diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index 846301e..8105279 100644 --- a/vite-frontend/src/layouts/admin.tsx +++ b/vite-frontend/src/layouts/admin.tsx @@ -50,6 +50,7 @@ export default function AdminLayout({ const { isOpen, onOpen, onOpenChange } = useDisclosure(); const [mobileMenuVisible, setMobileMenuVisible] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(() => localStorage.getItem("sidebar_collapsed") === "true"); const [username, setUsername] = useState(""); const [isAdmin, setIsAdmin] = useState(false); const [passwordLoading, setPasswordLoading] = useState(false); @@ -204,6 +205,13 @@ export default function AdminLayout({ setMobileMenuVisible(false); }; + // 切换折叠状态 + const toggleCollapse = () => { + const newCollapsed = !isCollapsed; + setIsCollapsed(newCollapsed); + localStorage.setItem("sidebar_collapsed", newCollapsed.toString()); + }; + // 菜单点击处理 const handleMenuClick = (path: string) => { navigate(path); @@ -304,32 +312,32 @@ export default function AdminLayout({ className={` ${isMobile ? "fixed" : "relative"} ${isMobile && !mobileMenuVisible ? "-translate-x-full" : "translate-x-0"} - ${isMobile ? "w-64" : "w-72"} + ${isMobile ? "w-64" : isCollapsed ? "w-20" : "w-72"} bg-white dark:bg-black shadow-lg border-r border-gray-200 dark:border-gray-600 z-50 - transition-transform duration-300 ease-in-out + transition-all duration-300 ease-in-out flex flex-col ${isMobile ? "h-screen" : "h-full"} ${isMobile ? "top-0 left-0" : ""} `} > {/* Logo 区域 */} -
-
- -
-

- {siteConfig.name} -

-

v{siteConfig.version}

-
+
+
+ +
+
+

+ {siteConfig.name} +

+

v{siteConfig.version}

{/* 菜单导航 */} -