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