初步添加FileTree功能
This commit is contained in:
184
app/components/content/FileTree.vue
Normal file
184
app/components/content/FileTree.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||||
|
<div v-if="title" class="flex items-center mb-3 font-mono text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<Icon v-if="icon" :name="icon" class="mr-2 text-gray-500" size="16" />
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 p-3">
|
||||||
|
<ClientOnly>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="item in parsedTree"
|
||||||
|
:key="item.title"
|
||||||
|
class="file-tree-item"
|
||||||
|
>
|
||||||
|
<FileTreeItem
|
||||||
|
:item="item"
|
||||||
|
:level="0"
|
||||||
|
:show-arrow="showArrow"
|
||||||
|
:show-icon="showIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务端渲染时的占位符 -->
|
||||||
|
<template #fallback>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="item in parsedTree"
|
||||||
|
:key="item.title"
|
||||||
|
class="file-tree-item"
|
||||||
|
>
|
||||||
|
<div class="flex items-center py-1 px-2">
|
||||||
|
<div class="flex items-center" :style="{ marginLeft: '0px' }">
|
||||||
|
<Icon
|
||||||
|
v-if="showIcon"
|
||||||
|
:name="item.icon || 'i-lucide-file'"
|
||||||
|
class="mr-2 text-gray-500 w-4 h-4"
|
||||||
|
:class="{
|
||||||
|
'text-green-500': item.icon?.includes('vue'),
|
||||||
|
'text-blue-500': item.icon?.includes('typescript') || item.icon?.includes('javascript'),
|
||||||
|
'text-orange-500': item.icon?.includes('markdown'),
|
||||||
|
'text-yellow-500': item.icon?.includes('json')
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-sm font-mono"
|
||||||
|
:class="{
|
||||||
|
'font-semibold': item.isFolder,
|
||||||
|
'underline': item.highlighted
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type InputTreeItem = string | {
|
||||||
|
[key: string]: InputTreeItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type FileTreeItemDiff = 'none' | 'addition' | 'deletion';
|
||||||
|
|
||||||
|
interface FileTreeItem {
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
children?: FileTreeItem[];
|
||||||
|
highlighted?: boolean;
|
||||||
|
diff?: FileTreeItemDiff;
|
||||||
|
isFolder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
tree,
|
||||||
|
autoSlash = true,
|
||||||
|
showArrow = false,
|
||||||
|
showIcon = true,
|
||||||
|
} = defineProps<{
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
autoSlash?: boolean;
|
||||||
|
showArrow?: boolean;
|
||||||
|
showIcon?: boolean;
|
||||||
|
tree: InputTreeItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 默认图标映射
|
||||||
|
const defaultIcons = {
|
||||||
|
vue: 'i-simple-icons-vuedotjs',
|
||||||
|
ts: 'i-simple-icons-typescript',
|
||||||
|
js: 'i-simple-icons-javascript',
|
||||||
|
md: 'i-simple-icons-markdown',
|
||||||
|
json: 'i-simple-icons-json',
|
||||||
|
folder: 'i-lucide-folder',
|
||||||
|
file: 'i-lucide-file'
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIcon(filename: string, type: 'folder' | 'file'): string {
|
||||||
|
if (filename === '...') return 'i-lucide-more-horizontal';
|
||||||
|
if (filename.endsWith('/')) return defaultIcons.folder;
|
||||||
|
|
||||||
|
const parts = filename.split('.');
|
||||||
|
const extension = parts.length > 1 ? parts[parts.length - 1]?.toLowerCase() || '' : '';
|
||||||
|
|
||||||
|
// 根据扩展名返回对应图标
|
||||||
|
switch (extension) {
|
||||||
|
case 'vue': return defaultIcons.vue;
|
||||||
|
case 'ts': return defaultIcons.ts;
|
||||||
|
case 'js': return defaultIcons.js;
|
||||||
|
case 'md': return defaultIcons.md;
|
||||||
|
case 'json': return defaultIcons.json;
|
||||||
|
default: return type === 'file' ? defaultIcons.file : defaultIcons.folder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItem(key: string, type: 'folder' | 'file', children?: InputTreeItem[]): FileTreeItem {
|
||||||
|
let title = key;
|
||||||
|
let highlighted = false;
|
||||||
|
|
||||||
|
if (title.startsWith('^') && title.endsWith('^')) {
|
||||||
|
title = title.substring(1, title.length - 1);
|
||||||
|
highlighted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff: FileTreeItemDiff = 'none';
|
||||||
|
if (title.startsWith('+')) diff = 'addition';
|
||||||
|
else if (title.startsWith('-')) diff = 'deletion';
|
||||||
|
|
||||||
|
if (type === 'file') {
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
icon: getIcon(title, 'file'),
|
||||||
|
highlighted,
|
||||||
|
diff,
|
||||||
|
isFolder: false
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
title: `${title}${autoSlash ? '/' : ''}`,
|
||||||
|
icon: getIcon(title, 'folder'),
|
||||||
|
children: children ? getTree(children) : undefined,
|
||||||
|
highlighted,
|
||||||
|
diff,
|
||||||
|
isFolder: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTree(tree: InputTreeItem[]): FileTreeItem[] {
|
||||||
|
const res: FileTreeItem[] = [];
|
||||||
|
|
||||||
|
for (const item of tree) {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
res.push(getItem(item, 'file'));
|
||||||
|
} else if (typeof item === 'object' && item !== null) {
|
||||||
|
for (const key of Object.keys(item)) {
|
||||||
|
const children = (item as Record<string, InputTreeItem[]>)[key];
|
||||||
|
if (children) {
|
||||||
|
res.push(getItem(key, 'folder', children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedTree = computed(() => {
|
||||||
|
return getTree(tree);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 provide/inject 来管理展开状态
|
||||||
|
const expandedState = ref(new Set<string>());
|
||||||
|
|
||||||
|
provide('expandedState', expandedState);
|
||||||
|
</script>
|
108
app/components/content/FileTreeItem.vue
Normal file
108
app/components/content/FileTreeItem.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-tree-item">
|
||||||
|
<div
|
||||||
|
class="flex items-center py-1 px-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-50 dark:bg-blue-900/20': item.highlighted,
|
||||||
|
'text-green-600 dark:text-green-400': item.diff === 'addition',
|
||||||
|
'text-red-600 dark:text-red-400': item.diff === 'deletion'
|
||||||
|
}"
|
||||||
|
@click="toggleFolder"
|
||||||
|
>
|
||||||
|
<!-- 缩进 -->
|
||||||
|
<div class="flex items-center" :style="{ marginLeft: level * 16 + 'px' }">
|
||||||
|
<!-- 箭头(仅文件夹且有子项时显示) -->
|
||||||
|
<Icon
|
||||||
|
v-if="showArrow && item.isFolder && item.children && item.children.length > 0"
|
||||||
|
:name="isExpanded ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
||||||
|
class="mr-1 text-gray-400 w-4 h-4 transition-transform"
|
||||||
|
:class="{ 'rotate-90': isExpanded }"
|
||||||
|
/>
|
||||||
|
<div v-else-if="showArrow" class="w-4 mr-1"></div>
|
||||||
|
|
||||||
|
<!-- 图标 -->
|
||||||
|
<Icon
|
||||||
|
v-if="showIcon"
|
||||||
|
:name="item.icon || 'i-lucide-file'"
|
||||||
|
class="mr-2 text-gray-500 w-4 h-4"
|
||||||
|
:class="{
|
||||||
|
'text-green-500': item.icon?.includes('vue'),
|
||||||
|
'text-blue-500': item.icon?.includes('typescript') || item.icon?.includes('javascript'),
|
||||||
|
'text-orange-500': item.icon?.includes('markdown'),
|
||||||
|
'text-yellow-500': item.icon?.includes('json')
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<span
|
||||||
|
class="text-sm font-mono"
|
||||||
|
:class="{
|
||||||
|
'font-semibold': item.isFolder,
|
||||||
|
'underline': item.highlighted
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 子项 -->
|
||||||
|
<div v-if="item.children && item.children.length > 0 && isExpanded" class="ml-4">
|
||||||
|
<FileTreeItem
|
||||||
|
v-for="child in item.children"
|
||||||
|
:key="child.title"
|
||||||
|
:item="child"
|
||||||
|
:level="level + 1"
|
||||||
|
:show-arrow="showArrow"
|
||||||
|
:show-icon="showIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type FileTreeItemDiff = 'none' | 'addition' | 'deletion';
|
||||||
|
|
||||||
|
interface FileTreeItem {
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
children?: FileTreeItem[];
|
||||||
|
highlighted?: boolean;
|
||||||
|
diff?: FileTreeItemDiff;
|
||||||
|
isFolder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: FileTreeItem;
|
||||||
|
level: number;
|
||||||
|
showArrow: boolean;
|
||||||
|
showIcon: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const expandedState = inject('expandedState', ref(new Set<string>()));
|
||||||
|
const itemKey = computed(() => `${props.item.title}-${props.level}`);
|
||||||
|
|
||||||
|
const isExpanded = computed({
|
||||||
|
get: () => expandedState.value.has(itemKey.value),
|
||||||
|
set: (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
expandedState.value.add(itemKey.value);
|
||||||
|
} else {
|
||||||
|
expandedState.value.delete(itemKey.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化时展开所有文件夹
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.item.isFolder && props.item.children && props.item.children.length > 0) {
|
||||||
|
isExpanded.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleFolder() {
|
||||||
|
if (props.item.isFolder && props.item.children && props.item.children.length > 0) {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@@ -9,35 +9,19 @@ This template is a ready-to-use documentation template made with [Nuxt UI Pro](h
|
|||||||
|
|
||||||
There are already many websites based on this template:
|
There are already many websites based on this template:
|
||||||
|
|
||||||
::UPageCard
|
::FileTree
|
||||||
---
|
---
|
||||||
icon: lucide:rocket
|
tree:
|
||||||
|
- app:
|
||||||
|
- components:
|
||||||
|
- Header.vue
|
||||||
|
- Footer.vue
|
||||||
|
- composables:
|
||||||
|
- useErrorHandler.ts
|
||||||
|
- ^app.vue^ # This is highlighted
|
||||||
|
- docs:
|
||||||
|
- index.md
|
||||||
---
|
---
|
||||||
|
|
||||||
#title
|
|
||||||
Card Title
|
|
||||||
|
|
||||||
#description
|
|
||||||
Description
|
|
||||||
|
|
||||||
#content
|
|
||||||
Beautifully designed **Nuxt Content** template with **shadcn-vue**. _Customizable. Compatible. Open Source._
|
|
||||||
|
|
||||||
#highlight
|
|
||||||
Tailwind CSS
|
|
||||||
|
|
||||||
#highlight-color
|
|
||||||
primary
|
|
||||||
|
|
||||||
title="Tailwind CSS"
|
|
||||||
description="Nuxt UI v3 integrates with latest Tailwind CSS v4, bringing significant improvements."
|
|
||||||
icon="i-simple-icons-tailwindcss"
|
|
||||||
orientation="horizontal"
|
|
||||||
highlight
|
|
||||||
highlight-color="primary"
|
|
||||||
|
|
||||||
#footer
|
|
||||||
Footer
|
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="4"}
|
::steps{level="4"}
|
||||||
|
Reference in New Issue
Block a user