Files
estel_docs/app/components/content/FileTree.vue

187 lines
5.1 KiB
Vue

<template>
<UPage class="mt-4 mb-4 bg-white dark:bg-gray-900 rounded-xl border border-gray-300 dark:border-gray-700">
<div v-if="title" class="flex items-center font-mono text-base m-3 text-gray-700 dark:text-gray-300 rounded-xl ">
<Icon v-if="icon" :name="icon" class="mr-2" size="20" />
<span>{{ title }}</span>
</div>
<div class="bg-gray-50 dark:bg-gray-800 rounded border-t 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 || '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>
</UPage>
</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: 'lucide-folder',
file: 'lucide-file'
};
function getIcon(filename: string, type: 'folder' | 'file'): string {
if (filename === '...') return '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>