feat(viewer): Add data source dialog

Introduce a dialog for switching between built-in content and a
user-selected local folder. Persist the folder handle in IndexedDB
so the selection survives page refreshes. Scan the folder for .md,
.yarn, and .csv files to build the file index.

Update App and Sidebar to pass file tree and heading data, and
add TypeScript declarations for the File System Access API.
This commit is contained in:
hyper
2026-06-30 19:20:08 +08:00
parent a05b0f4291
commit 53c8ce0677
7 changed files with 523 additions and 29 deletions
+31 -17
View File
@@ -6,6 +6,8 @@ import { FileTreeNode, HeadingNode } from "./FileTree";
export interface SidebarProps {
isOpen: boolean;
onClose: () => void;
fileTree?: FileNode[];
pathHeadings?: Record<string, TocNode[]>;
}
interface SidebarContentProps {
@@ -25,7 +27,9 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
// 响应式获取当前文件的标题列表
const currentFileHeadings = createMemo(() => {
const pathname = decodeURIComponent(location.pathname);
return props.pathHeadings[pathname] || props.pathHeadings[`${pathname}.md`] || [];
return (
props.pathHeadings[pathname] || props.pathHeadings[`${pathname}.md`] || []
);
});
return (
@@ -68,11 +72,7 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
</h3>
{currentFileHeadings().map((node) => (
<HeadingNode
node={node}
basePath={location.pathname}
depth={0}
/>
<HeadingNode node={node} basePath={location.pathname} depth={0} />
))}
</div>
</Show>
@@ -85,14 +85,20 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
*/
export const MobileSidebar: Component<SidebarProps> = (props) => {
const location = useLocation();
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
const [pathHeadings, setPathHeadings] = createSignal<Record<string, TocNode[]>>({});
const [selfFileTree, setSelfFileTree] = createSignal<FileNode[]>([]);
const [selfPathHeadings, setSelfPathHeadings] = createSignal<
Record<string, TocNode[]>
>({});
// 加载目录数据
const fileTree = () => props.fileTree ?? selfFileTree();
const pathHeadings = () => props.pathHeadings ?? selfPathHeadings();
// 加载目录数据 (only if props not provided)
onMount(async () => {
if (props.fileTree && props.pathHeadings) return;
const toc = await generateToc();
setFileTree(toc.fileTree);
setPathHeadings(toc.pathHeadings);
setSelfFileTree(toc.fileTree);
setSelfPathHeadings(toc.pathHeadings);
});
return (
@@ -124,16 +130,24 @@ export const MobileSidebar: Component<SidebarProps> = (props) => {
/**
* 桌面端固定侧边栏
*/
export const DesktopSidebar: Component<{}> = () => {
export const DesktopSidebar: Component<{
fileTree?: FileNode[];
pathHeadings?: Record<string, TocNode[]>;
}> = (props) => {
const location = useLocation();
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
const [pathHeadings, setPathHeadings] = createSignal<Record<string, TocNode[]>>({});
const [selfFileTree, setSelfFileTree] = createSignal<FileNode[]>([]);
const [selfPathHeadings, setSelfPathHeadings] = createSignal<
Record<string, TocNode[]>
>({});
const fileTree = () => props.fileTree ?? selfFileTree();
const pathHeadings = () => props.pathHeadings ?? selfPathHeadings();
// 加载目录数据
onMount(async () => {
if (props.fileTree && props.pathHeadings) return;
const toc = await generateToc();
setFileTree(toc.fileTree);
setPathHeadings(toc.pathHeadings);
setSelfFileTree(toc.fileTree);
setSelfPathHeadings(toc.pathHeadings);
});
return (