FileTreeNode.vue 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. <script setup lang="ts">
  2. import { computed } from 'vue'
  3. import type { TreeNode, File } from './types'
  4. const props = defineProps<{
  5. node: TreeNode
  6. activeFile: File
  7. level?: number
  8. }>()
  9. const emit = defineEmits<{
  10. (e: 'select-file', file: File): void
  11. (e: 'toggle-folder', node: TreeNode): void
  12. }>()
  13. const isSelected = computed(() => {
  14. return props.node.type === 'file' && props.node.fileData?.name === props.activeFile.name
  15. })
  16. const currentLevel = computed(() => props.level || 0)
  17. const handleClick = () => {
  18. if (props.node.type === 'folder') {
  19. emit('toggle-folder', props.node)
  20. } else {
  21. if (props.node.fileData) {
  22. emit('select-file', props.node.fileData)
  23. }
  24. }
  25. }
  26. </script>
  27. <template>
  28. <div class="tree-node">
  29. <div
  30. class="node-label"
  31. :class="{ 'is-selected': isSelected, 'is-folder': node.type === 'folder' }"
  32. @click="handleClick"
  33. :style="{ paddingLeft: (currentLevel * 12 + 10) + 'px' }"
  34. >
  35. <span v-if="node.type === 'folder'" class="folder-arrow" :class="{ open: node.isOpen }">
  36. <svg width="10" height="10" viewBox="0 0 24 24"><path fill="currentColor" d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/></svg>
  37. </span>
  38. <span class="node-icon">
  39. <template v-if="node.type === 'folder'">
  40. <svg v-if="node.isOpen" width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z"/></svg>
  41. <svg v-else width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
  42. </template>
  43. <template v-else>
  44. <svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
  45. </template>
  46. </span>
  47. <span class="node-name">{{ node.name }}</span>
  48. </div>
  49. <div v-if="node.type === 'folder' && node.isOpen" class="node-children">
  50. <FileTreeNode
  51. v-for="child in node.children"
  52. :key="child.path"
  53. :node="child"
  54. :active-file="activeFile"
  55. :level="currentLevel + 1"
  56. @select-file="$emit('select-file', $event)"
  57. @toggle-folder="$emit('toggle-folder', $event)"
  58. />
  59. </div>
  60. </div>
  61. </template>
  62. <style scoped>
  63. .node-label {
  64. display: flex;
  65. align-items: center;
  66. cursor: pointer;
  67. padding: 4px 0;
  68. color: var(--vp-c-text-2);
  69. user-select: none;
  70. transition: color 0.1s;
  71. white-space: nowrap;
  72. }
  73. .node-label:hover {
  74. color: var(--vp-c-text-1);
  75. background-color: var(--vp-c-bg-mute);
  76. }
  77. .node-label.is-selected {
  78. background-color: var(--vp-c-brand-dimm);
  79. color: var(--vp-c-brand);
  80. }
  81. .folder-arrow {
  82. display: flex;
  83. align-items: center;
  84. margin-right: 4px;
  85. transform: rotate(0deg);
  86. transition: transform 0.2s;
  87. opacity: 0.7;
  88. }
  89. .folder-arrow.open {
  90. transform: rotate(90deg);
  91. }
  92. .node-icon {
  93. margin-right: 6px;
  94. display: flex;
  95. align-items: center;
  96. opacity: 0.8;
  97. }
  98. .node-name {
  99. white-space: nowrap;
  100. overflow: hidden;
  101. text-overflow: ellipsis;
  102. }
  103. </style>