From 779722fe85415ffd1dc2a3714c9a1cb8406810c7 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 7 Jul 2026 23:26:44 +0800 Subject: [PATCH] refactor: preserve URL search params during navigation Introduce `useNavigateWithParams` to ensure that existing URL search parameters are maintained when navigating between pages. This is applied to the Article component, FileTree, and journal links to prevent losing state (like session or player info) during client-side navigation. Also intercepts markdown anchor links in the Article component to use client-side navigation. --- src/components/Article.tsx | 29 ++++++++++++++++++++++--- src/components/FileTree.tsx | 7 +++--- src/components/journal/types/link.tsx | 6 ++--- src/components/useNavigateWithParams.ts | 21 ++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 src/components/useNavigateWithParams.ts diff --git a/src/components/Article.tsx b/src/components/Article.tsx index 80cb251..10fd247 100644 --- a/src/components/Article.tsx +++ b/src/components/Article.tsx @@ -15,7 +15,7 @@ import { extractSection } from "../data-loader"; import mermaid from "mermaid"; import { getIndexedData } from "../data-loader/file-index"; import { resolvePath } from "./utils/path"; -import { useLocation } from "@solidjs/router"; +import { useNavigateWithParams } from "./useNavigateWithParams"; export interface ArticleProps { src: string; @@ -76,7 +76,7 @@ function scrollToHash(hash: string) { * 用于将特定 src 位置的 md 文件显示为 markdown 文章 */ export const Article: Component = (props) => { - const location = useLocation(); + const navigate = useNavigateWithParams(); const [content, { refetch }] = createResource( () => ({ src: props.src, section: props.section }), fetchArticleContent, @@ -97,10 +97,33 @@ export const Article: Component = (props) => { void mermaid.run(); // 内容渲染后检查 hash 并滚动 - scrollToHash(location.hash); + scrollToHash(window.location.hash); } }); + // Intercept markdown links to use client-side navigation with + // preserved URL search params. + createEffect(() => { + const dom = contentDom(); + if (!dom) return; + + const onClick = (e: MouseEvent) => { + const anchor = (e.target as HTMLElement).closest("a[href]"); + if (!anchor) return; + const href = anchor.getAttribute("href"); + if (!href) return; + // Only intercept same-origin navigation links (not external, not anchors) + if (href.startsWith("http") || href.startsWith("//")) return; + if (href.startsWith("#")) return; + + e.preventDefault(); + navigate(href); + }; + + dom.addEventListener("click", onClick); + onCleanup(() => dom.removeEventListener("click", onClick)); + }); + onCleanup(() => { // 清理时清空内容,触发内部组件的销毁 }); diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index 4c22660..d8a083f 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -1,6 +1,6 @@ import { Component, createMemo, createSignal, Show } from "solid-js"; import { type FileNode, type TocNode } from "../data-loader"; -import { useNavigate } from "@solidjs/router"; +import { useNavigateWithParams } from "./useNavigateWithParams"; /** * 检查当前文件路径是否在文件夹内 @@ -22,7 +22,7 @@ export const FileTreeNode: Component<{ onClose: () => void; isHidden?: (node: FileNode) => boolean; }> = (props) => { - const navigate = useNavigate(); + const navigate = useNavigateWithParams(); const isDir = !!props.node.children; const isActive = createMemo(() => props.currentPath === props.node.path); // 默认收起,除非当前文件在该文件夹内 @@ -85,7 +85,7 @@ export const HeadingNode: Component<{ depth: number; isHidden?: (node: TocNode) => boolean; }> = (props) => { - const navigate = useNavigate(); + const navigate = useNavigateWithParams(); const anchor = props.node.id || ""; const href = `${props.basePath}#${anchor}`; const hasChildren = !!props.node.children; @@ -99,6 +99,7 @@ export const HeadingNode: Component<{ } }; const handleClick = (e: MouseEvent) => { + e.preventDefault(); navigate(href); // 滚动到目标元素,考虑导航栏高度偏移 requestAnimationFrame(() => { diff --git a/src/components/journal/types/link.tsx b/src/components/journal/types/link.tsx index 5c149b2..c1d7238 100644 --- a/src/components/journal/types/link.tsx +++ b/src/components/journal/types/link.tsx @@ -6,7 +6,7 @@ */ import { Component } from "solid-js"; -import { useNavigate } from "@solidjs/router"; +import { useNavigateWithParams } from "../../useNavigateWithParams"; import { z } from "zod"; import { registerMessageType } from "../registry"; import { journalSetState } from "../../stores/journalStream"; @@ -22,7 +22,7 @@ export type LinkPayload = z.infer; function fileNameFromPath(path: string): string { const parts = path.split("/").filter(Boolean); - return parts[parts.length - 1] || path; + return decodeURIComponent(parts[parts.length - 1] || path); } /** Encode a path, preserving / separators but encoding each segment */ @@ -42,7 +42,7 @@ function slugToTitle(slug: string): string { } const RevealLink: Component = (p) => { - const navigate = useNavigate(); + const navigate = useNavigateWithParams(); const target = () => { let t = encodePath(p.path); diff --git a/src/components/useNavigateWithParams.ts b/src/components/useNavigateWithParams.ts new file mode 100644 index 0000000..ce6b699 --- /dev/null +++ b/src/components/useNavigateWithParams.ts @@ -0,0 +1,21 @@ +/** + * useNavigateWithParams — wraps navigate() to preserve the current URL + * search params (e.g. ?session=abc&player=alice) when navigating to a + * new path. + */ + +import { useNavigate, useLocation } from "@solidjs/router"; + +export function useNavigateWithParams() { + const navigate = useNavigate(); + const location = useLocation(); + + return (to: string) => { + // Split hash from the path so we can re-attach it after search params + const hashIdx = to.indexOf("#"); + const path = hashIdx >= 0 ? to.slice(0, hashIdx) : to; + const hash = hashIdx >= 0 ? to.slice(hashIdx) : ""; + + navigate(path + location.search + hash); + }; +}