file-tree.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { useState } from "react"
  2. import { ChevronRight, ChevronDown, File, Folder, FolderOpen } from "lucide-react"
  3. import { ScrollArea } from "@/components/ui/scroll-area"
  4. import { useWikiStore } from "@/stores/wiki-store"
  5. import type { FileNode } from "@/types/wiki"
  6. import { useTranslation } from "react-i18next"
  7. import { openProjectFolder } from "@/commands/fs"
  8. import { isTauri } from "@/lib/platform"
  9. function TreeNode({ node, depth }: { node: FileNode; depth: number }) {
  10. const [expanded, setExpanded] = useState(depth < 1)
  11. const selectedFile = useWikiStore((s) => s.selectedFile)
  12. const setSelectedFile = useWikiStore((s) => s.setSelectedFile)
  13. const isSelected = selectedFile === node.path
  14. const paddingLeft = 12 + depth * 16
  15. if (node.is_dir) {
  16. return (
  17. <div>
  18. <button
  19. onClick={() => setExpanded(!expanded)}
  20. className="flex w-full items-center gap-1 py-1 text-sm text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground"
  21. style={{ paddingLeft }}
  22. >
  23. {expanded ? (
  24. <ChevronDown className="h-3.5 w-3.5 shrink-0" />
  25. ) : (
  26. <ChevronRight className="h-3.5 w-3.5 shrink-0" />
  27. )}
  28. <Folder className="h-3.5 w-3.5 shrink-0 text-blue-400" />
  29. <span className="truncate">{node.name}</span>
  30. </button>
  31. {expanded && node.children?.map((child) => (
  32. <TreeNode key={child.path} node={child} depth={depth + 1} />
  33. ))}
  34. </div>
  35. )
  36. }
  37. return (
  38. <button
  39. onClick={() => setSelectedFile(node.path)}
  40. className={`flex w-full items-center gap-1 py-1 text-sm ${
  41. isSelected
  42. ? "qm-selected"
  43. : "text-muted-foreground qm-hover"
  44. }`}
  45. style={{ paddingLeft: paddingLeft + 14 }}
  46. >
  47. <File className="h-3.5 w-3.5 shrink-0" />
  48. <span className="truncate">{node.name}</span>
  49. </button>
  50. )
  51. }
  52. export function FileTree() {
  53. const { t } = useTranslation()
  54. const fileTree = useWikiStore((s) => s.fileTree)
  55. const project = useWikiStore((s) => s.project)
  56. const handleOpenProjectFolder = async () => {
  57. if (!project) return
  58. try {
  59. await openProjectFolder(project.path)
  60. } catch (err) {
  61. console.error("[FileTree] open project folder failed:", err)
  62. if (isTauri()) {
  63. const { message } = await import("@tauri-apps/plugin-dialog")
  64. await message(
  65. t("fileTree.openProjectFolderFailed", {
  66. defaultValue: "Failed to open the project folder.",
  67. }),
  68. {
  69. title: t("fileTree.openProjectFolder", {
  70. defaultValue: "Open project folder",
  71. }),
  72. kind: "error",
  73. },
  74. )
  75. } else {
  76. window.alert("无法打开项目文件夹")
  77. }
  78. }
  79. }
  80. if (!project) {
  81. return (
  82. <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
  83. {t("fileTree.noProject")}
  84. </div>
  85. )
  86. }
  87. return (
  88. <div className="flex h-full min-w-0 flex-col overflow-hidden">
  89. <ScrollArea className="min-h-0 flex-1 overflow-hidden">
  90. <div className="p-2">
  91. <div className="mb-2 px-2 text-xs font-semibold uppercase text-muted-foreground">
  92. {project.name}
  93. </div>
  94. {fileTree.map((node) => (
  95. <TreeNode key={node.path} node={node} depth={0} />
  96. ))}
  97. </div>
  98. </ScrollArea>
  99. <div className="shrink-0 border-t p-2">
  100. <button
  101. type="button"
  102. onClick={() => void handleOpenProjectFolder()}
  103. 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"
  104. title={project.path}
  105. >
  106. <FolderOpen className="h-3.5 w-3.5 shrink-0" />
  107. <span className="truncate">
  108. {t("fileTree.openProjectFolder", { defaultValue: "Open project folder" })}
  109. </span>
  110. </button>
  111. </div>
  112. </div>
  113. )
  114. }