feat(frontend): 「目录」列可折叠、记忆折叠状态(UX P2-2)
useChapterListCollapse 用 localStorage 记忆折叠态(SSR 首帧恒展开避免 hydration 不一致);收起降为带展开按钮的窄轨、让宽给正文,展开时标题旁加收起按钮;三栏栅格据折叠态切列宽,章节列表/高亮/切章导航全保留。
This commit is contained in:
64
apps/web/lib/workbench/useChapterListCollapse.test.ts
Normal file
64
apps/web/lib/workbench/useChapterListCollapse.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
CHAPTER_LIST_COLLAPSE_KEY,
|
||||
useChapterListCollapse,
|
||||
} from "./useChapterListCollapse";
|
||||
|
||||
// 本仓库 jsdom 未提供可用的 Storage(setItem/clear 缺失),装一个内存版 localStorage。
|
||||
function installMemoryStorage(): Storage {
|
||||
const map = new Map<string, string>();
|
||||
const storage: Storage = {
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
clear: () => map.clear(),
|
||||
getItem: (key) => (map.has(key) ? (map.get(key) ?? null) : null),
|
||||
key: (index) => Array.from(map.keys())[index] ?? null,
|
||||
removeItem: (key) => map.delete(key),
|
||||
setItem: (key, value) => map.set(key, String(value)),
|
||||
};
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: storage,
|
||||
configurable: true,
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
|
||||
describe("useChapterListCollapse", () => {
|
||||
beforeEach(() => {
|
||||
installMemoryStorage();
|
||||
});
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
it("默认展开(无存档时 collapsed=false)", () => {
|
||||
const { result } = renderHook(() => useChapterListCollapse());
|
||||
expect(result.current.collapsed).toBe(false);
|
||||
});
|
||||
|
||||
it("toggle 翻转并把状态写入 localStorage", () => {
|
||||
const { result } = renderHook(() => useChapterListCollapse());
|
||||
|
||||
act(() => result.current.toggle());
|
||||
|
||||
expect(result.current.collapsed).toBe(true);
|
||||
expect(window.localStorage.getItem(CHAPTER_LIST_COLLAPSE_KEY)).toBe("1");
|
||||
|
||||
act(() => result.current.toggle());
|
||||
|
||||
expect(result.current.collapsed).toBe(false);
|
||||
expect(window.localStorage.getItem(CHAPTER_LIST_COLLAPSE_KEY)).toBe("0");
|
||||
});
|
||||
|
||||
it("挂载时从 localStorage 恢复已收起状态", () => {
|
||||
window.localStorage.setItem(CHAPTER_LIST_COLLAPSE_KEY, "1");
|
||||
|
||||
const { result } = renderHook(() => useChapterListCollapse());
|
||||
|
||||
expect(result.current.collapsed).toBe(true);
|
||||
});
|
||||
});
|
||||
46
apps/web/lib/workbench/useChapterListCollapse.ts
Normal file
46
apps/web/lib/workbench/useChapterListCollapse.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
// 「目录」列折叠偏好的持久化键(对齐既有 `ww.` 前缀约定)。
|
||||
export const CHAPTER_LIST_COLLAPSE_KEY = "ww.workbench_chapter_list_collapsed";
|
||||
|
||||
export interface ChapterListCollapse {
|
||||
// true=目录列收起(让宽给正文);false=展开。
|
||||
collapsed: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
function persist(collapsed: boolean): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(CHAPTER_LIST_COLLAPSE_KEY, collapsed ? "1" : "0");
|
||||
} catch {
|
||||
// 隐私模式/配额异常忽略:折叠是纯 UI 偏好,写盘失败不影响导航功能。
|
||||
}
|
||||
}
|
||||
|
||||
// 记住「目录」列收起/展开状态(P2-2):SSR 首帧恒展开(避免 hydration 不一致),
|
||||
// 挂载后再从 localStorage 恢复;toggle 即时写回。仅桌面(≥xl)用得上,窄屏走抽屉不受影响。
|
||||
export function useChapterListCollapse(): ChapterListCollapse {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
setCollapsed(window.localStorage.getItem(CHAPTER_LIST_COLLAPSE_KEY) === "1");
|
||||
} catch {
|
||||
// 读盘失败则维持默认展开。
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
persist(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { collapsed, toggle };
|
||||
}
|
||||
Reference in New Issue
Block a user