| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- import { useState } from "react"
- import { ChevronRight, ChevronDown, File, Folder, FolderOpen } from "lucide-react"
- import { ScrollArea } from "@/components/ui/scroll-area"
- import { useWikiStore } from "@/stores/wiki-store"
- import type { FileNode } from "@/types/wiki"
- import { useTranslation } from "react-i18next"
- import { openProjectFolder } from "@/commands/fs"
- import { isTauri } from "@/lib/platform"
- function TreeNode({ node, depth }: { node: FileNode; depth: number }) {
- const [expanded, setExpanded] = useState(depth < 1)
- const selectedFile = useWikiStore((s) => s.selectedFile)
- const setSelectedFile = useWikiStore((s) => s.setSelectedFile)
- const isSelected = selectedFile === node.path
- const paddingLeft = 12 + depth * 16
- if (node.is_dir) {
- return (
- <div>
- <button
- onClick={() => setExpanded(!expanded)}
- className="flex w-full items-center gap-1 py-1 text-sm text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground"
- style={{ paddingLeft }}
- >
- {expanded ? (
- <ChevronDown className="h-3.5 w-3.5 shrink-0" />
- ) : (
- <ChevronRight className="h-3.5 w-3.5 shrink-0" />
- )}
- <Folder className="h-3.5 w-3.5 shrink-0 text-blue-400" />
- <span className="truncate">{node.name}</span>
- </button>
- {expanded && node.children?.map((child) => (
- <TreeNode key={child.path} node={child} depth={depth + 1} />
- ))}
- </div>
- )
- }
- return (
- <button
- onClick={() => setSelectedFile(node.path)}
- className={`flex w-full items-center gap-1 py-1 text-sm ${
- isSelected
- ? "qm-selected"
- : "text-muted-foreground qm-hover"
- }`}
- style={{ paddingLeft: paddingLeft + 14 }}
- >
- <File className="h-3.5 w-3.5 shrink-0" />
- <span className="truncate">{node.name}</span>
- </button>
- )
- }
- export function FileTree() {
- const { t } = useTranslation()
- const fileTree = useWikiStore((s) => s.fileTree)
- const project = useWikiStore((s) => s.project)
- const handleOpenProjectFolder = async () => {
- if (!project) return
- try {
- await openProjectFolder(project.path)
- } catch (err) {
- console.error("[FileTree] open project folder failed:", err)
- if (isTauri()) {
- const { message } = await import("@tauri-apps/plugin-dialog")
- await message(
- t("fileTree.openProjectFolderFailed", {
- defaultValue: "Failed to open the project folder.",
- }),
- {
- title: t("fileTree.openProjectFolder", {
- defaultValue: "Open project folder",
- }),
- kind: "error",
- },
- )
- } else {
- window.alert("无法打开项目文件夹")
- }
- }
- }
- if (!project) {
- return (
- <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
- {t("fileTree.noProject")}
- </div>
- )
- }
- return (
- <div className="flex h-full min-w-0 flex-col overflow-hidden">
- <ScrollArea className="min-h-0 flex-1 overflow-hidden">
- <div className="p-2">
- <div className="mb-2 px-2 text-xs font-semibold uppercase text-muted-foreground">
- {project.name}
- </div>
- {fileTree.map((node) => (
- <TreeNode key={node.path} node={node} depth={0} />
- ))}
- </div>
- </ScrollArea>
- <div className="shrink-0 border-t p-2">
- <button
- type="button"
- onClick={() => void handleOpenProjectFolder()}
- className="flex w-full items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent/50 hover:text-accent-foreground"
- title={project.path}
- >
- <FolderOpen className="h-3.5 w-3.5 shrink-0" />
- <span className="truncate">
- {t("fileTree.openProjectFolder", { defaultValue: "Open project folder" })}
- </span>
- </button>
- </div>
- </div>
- )
- }
|