feat(FileTree): 为文件树节点添加类型化图标和颜色

- 使用 `#icon` 插槽为树节点添加自定义图标组件
- 重构图标和颜色逻辑,使用 `FILE_TYPE_CONFIG` 对象集中管理文件类型配置
- 为文件夹添加展开/收起状态的不同图标,并使用黄色区分
- 调整树节点标题的字体大小和行高以改善视觉一致性
This commit is contained in:
cfq 2026-01-27 15:37:08 +08:00
parent 495a8a0bb4
commit 7b200dcb68
1 changed files with 65 additions and 64 deletions

View File

@ -42,6 +42,12 @@
:load-data="onLoadData" :load-data="onLoadData"
@select="onSelect" @select="onSelect"
> >
<template #icon="{ dataRef, expanded }">
<component
:is="getFileIcon(dataRef.name, dataRef.type, expanded)"
:style="{ color: getFileIconColor(dataRef.name, dataRef.type), fontSize: '18px', marginRight: '6px' }"
/>
</template>
<template #title="{ dataRef }"> <template #title="{ dataRef }">
<a-dropdown :trigger="['contextmenu']"> <a-dropdown :trigger="['contextmenu']">
<span class="tree-node-title" :title="dataRef.name">{{ dataRef.name }}</span> <span class="tree-node-title" :title="dataRef.name">{{ dataRef.name }}</span>
@ -90,7 +96,20 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { EyeInvisibleOutlined, EyeOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons-vue'; import {
EyeInvisibleOutlined,
EyeOutlined,
ReloadOutlined,
SearchOutlined,
FileOutlined,
FileMarkdownOutlined,
FileImageOutlined,
FilePdfOutlined,
CodeOutlined,
FileTextOutlined,
FolderFilled,
FolderOpenFilled
} from '@ant-design/icons-vue';
import { useFileTree } from '../composables/useFileTree'; import { useFileTree } from '../composables/useFileTree';
import { useTabs } from '../composables/useTabs'; import { useTabs } from '../composables/useTabs';
import { useConfig } from '../composables/useConfig'; import { useConfig } from '../composables/useConfig';
@ -189,71 +208,51 @@ const onSelect = (keys, { node }) => {
const isMdFile = (name) => name && name.toLowerCase().endsWith('.md'); const isMdFile = (name) => name && name.toLowerCase().endsWith('.md');
const getFileIcon = (fileName) => { const FILE_TYPE_CONFIG = {
if (!fileName) return FileOutlined; md: { icon: FileMarkdownOutlined, color: '#1890ff' },
const ext = fileName.split('.').pop().toLowerCase(); markdown: { icon: FileMarkdownOutlined, color: '#1890ff' },
switch (ext) { png: { icon: FileImageOutlined, color: '#722ed1' },
case 'md': jpg: { icon: FileImageOutlined, color: '#722ed1' },
case 'markdown': jpeg: { icon: FileImageOutlined, color: '#722ed1' },
return FileMarkdownOutlined; gif: { icon: FileImageOutlined, color: '#722ed1' },
case 'png': webp: { icon: FileImageOutlined, color: '#722ed1' },
case 'jpg': svg: { icon: FileImageOutlined, color: '#722ed1' },
case 'jpeg': ico: { icon: FileImageOutlined, color: '#722ed1' },
case 'gif': pdf: { icon: FilePdfOutlined, color: '#f5222d' },
case 'webp': js: { icon: CodeOutlined, color: '#52c41a' },
case 'svg': json: { icon: CodeOutlined, color: '#52c41a' },
case 'ico': ts: { icon: CodeOutlined, color: '#52c41a' },
return FileImageOutlined; css: { icon: CodeOutlined, color: '#52c41a' },
case 'pdf': html: { icon: CodeOutlined, color: '#52c41a' },
return FilePdfOutlined; vue: { icon: CodeOutlined, color: '#52c41a' },
case 'js': xml: { icon: CodeOutlined, color: '#52c41a' },
case 'json': yaml: { icon: CodeOutlined, color: '#52c41a' },
case 'ts': yml: { icon: CodeOutlined, color: '#52c41a' },
case 'css': txt: { icon: FileTextOutlined, color: 'inherit' },
case 'html': log: { icon: FileTextOutlined, color: 'inherit' },
case 'vue':
case 'xml':
case 'yaml':
case 'yml':
return CodeOutlined;
case 'txt':
case 'log':
return FileTextOutlined;
default:
return FileOutlined;
}
}; };
const getFileIconColor = (fileName) => { const getFileExtension = (fileName) => {
if (!fileName) return 'inherit'; if (!fileName) return '';
const ext = fileName.split('.').pop().toLowerCase(); const lastDotIndex = fileName.lastIndexOf('.');
switch (ext) { if (lastDotIndex === -1 || lastDotIndex === 0) return '';
case 'md': return fileName.substring(lastDotIndex + 1).toLowerCase();
case 'markdown': };
return '#1890ff';
case 'png': const getFileIcon = (fileName, type, expanded) => {
case 'jpg': if (type === 'directory') {
case 'jpeg': return expanded ? FolderOpenFilled : FolderFilled;
case 'gif':
case 'webp':
case 'svg':
case 'ico':
return '#722ed1';
case 'pdf':
return '#f5222d';
case 'js':
case 'json':
case 'ts':
case 'css':
case 'html':
case 'vue':
case 'xml':
case 'yaml':
case 'yml':
return '#52c41a';
default:
return 'inherit';
} }
const ext = getFileExtension(fileName);
return FILE_TYPE_CONFIG[ext]?.icon || FileOutlined;
};
const getFileIconColor = (fileName, type) => {
if (type === 'directory') {
return '#ffc60a'; //
}
const ext = getFileExtension(fileName);
return FILE_TYPE_CONFIG[ext]?.color || 'inherit';
}; };
const isPinned = (path) => config.pinnedFiles.includes(path); const isPinned = (path) => config.pinnedFiles.includes(path);
@ -431,13 +430,15 @@ const handleModalOk = async () => {
flex: 1; /* 让文字占满剩余空间 */ flex: 1; /* 让文字占满剩余空间 */
min-width: 0; /* 关键:允许在 flex 容器中收缩,从而触发省略号 */ min-width: 0; /* 关键:允许在 flex 容器中收缩,从而触发省略号 */
margin-left: 4px; /* spacing between icon and text */ margin-left: 4px; /* spacing between icon and text */
font-size: 16px;
line-height: 28px;
} }
:deep(.ant-tree-node-content-wrapper) { :deep(.ant-tree-node-content-wrapper) {
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
min-height: 28px; /* improve touch target / spacing */ min-height: 32px; /* improve touch target / spacing */
padding: 0 4px; padding: 0 4px;
border-radius: 4px; border-radius: 4px;
transition: all 0.2s; transition: all 0.2s;