feat(FileTree): 改进文件树交互并添加主题支持

- 修复目录加载逻辑,避免空数组误判为已加载
- 改进新建/重命名操作的父目录路径计算
- 为文件树节点添加悬停提示和主题样式适配
- 在工具栏添加设置按钮
- 扩展主题系统以支持暗色模式
This commit is contained in:
cfq 2026-01-26 14:06:34 +08:00
parent 6b26049909
commit 4a7bd83835
4 changed files with 88 additions and 14 deletions

View File

@ -49,7 +49,8 @@ window.services = {
name: item.name, name: item.name,
path: path.join(dirPath, item.name), path: path.join(dirPath, item.name),
type: item.isDirectory() ? "directory" : "file", type: item.isDirectory() ? "directory" : "file",
children: item.isDirectory() ? [] : undefined, // 目录初始化为空数组,标记为可展开 // children 不设置或设为 undefined表示未加载如果设为 [],组件会认为已加载且为空
children: undefined,
})); }));
} catch (error) { } catch (error) {
console.error("读取目录失败:", error); console.error("读取目录失败:", error);

View File

@ -21,7 +21,7 @@
> >
<template #title="{ dataRef }"> <template #title="{ dataRef }">
<a-dropdown :trigger="['contextmenu']"> <a-dropdown :trigger="['contextmenu']">
<span class="tree-node-title">{{ dataRef.name }}</span> <span class="tree-node-title" :title="dataRef.name">{{ dataRef.name }}</span>
<template #overlay> <template #overlay>
<a-menu @click="(e) => handleMenuClick(e, dataRef)"> <a-menu @click="(e) => handleMenuClick(e, dataRef)">
<a-menu-item key="open" v-if="dataRef.type === 'file'">打开</a-menu-item> <a-menu-item key="open" v-if="dataRef.type === 'file'">打开</a-menu-item>
@ -101,11 +101,12 @@ const handleRefresh = async () => {
const onLoadData = (treeNode) => { const onLoadData = (treeNode) => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
if (treeNode.dataRef.children) { // 使 key (path)
if (treeNode.dataRef.children && treeNode.dataRef.children.length > 0) {
resolve(); resolve();
return; return;
} }
await loadDirectory(treeNode.eventKey); // eventKey is path await loadDirectory(treeNode.key || treeNode.eventKey);
resolve(); resolve();
}); });
}; };
@ -116,7 +117,7 @@ const onSelect = (keys, { node }) => {
} }
}; };
const isMdFile = (name) => name.toLowerCase().endsWith('.md'); const isMdFile = (name) => name && name.toLowerCase().endsWith('.md');
const isPinned = (path) => config.pinnedFiles.includes(path); const isPinned = (path) => config.pinnedFiles.includes(path);
@ -153,7 +154,7 @@ const handleMenuClick = async ({ key }, node) => {
if (window.services?.deleteItem) { if (window.services?.deleteItem) {
await window.services.deleteItem(node.path); await window.services.deleteItem(node.path);
message.success('删除成功'); message.success('删除成功');
await refresh(); // await refresh();
} }
} }
}); });
@ -192,7 +193,23 @@ const handleModalOk = async () => {
} }
const node = currentNode.value; const node = currentNode.value;
const parentPath = node.type === 'directory' ? node.path : node.path.substring(0, node.path.lastIndexOf(window.utools.isWindows() ? '\\' : '/')); // node.path node
//
// Ant Design Tree node
// directory
// file
let parentPath = '';
if (currentAction.value === 'newFile' || currentAction.value === 'newFolder') {
if (node.type === 'directory') {
parentPath = node.path;
} else {
parentPath = node.path.substring(0, node.path.lastIndexOf(window.utools.isWindows() ? '\\' : '/'));
}
} else {
// rename
parentPath = node.path.substring(0, node.path.lastIndexOf(window.utools.isWindows() ? '\\' : '/'));
}
try { try {
if (currentAction.value === 'rename') { if (currentAction.value === 'rename') {
@ -200,14 +217,13 @@ const handleModalOk = async () => {
await window.services.renameItem(node.path, newPath); await window.services.renameItem(node.path, newPath);
message.success('重命名成功'); message.success('重命名成功');
} else if (currentAction.value === 'newFile') { } else if (currentAction.value === 'newFile') {
// node
const separator = window.utools.isWindows() ? '\\' : '/'; const separator = window.utools.isWindows() ? '\\' : '/';
const newPath = `${node.path}${separator}${modalInputValue.value}`; const newPath = `${parentPath}${separator}${modalInputValue.value}`;
await window.services.writeFile(newPath, ''); // await window.services.writeFile(newPath, ''); //
message.success('创建文件成功'); message.success('创建文件成功');
} else if (currentAction.value === 'newFolder') { } else if (currentAction.value === 'newFolder') {
const separator = window.utools.isWindows() ? '\\' : '/'; const separator = window.utools.isWindows() ? '\\' : '/';
const newPath = `${node.path}${separator}${modalInputValue.value}`; const newPath = `${parentPath}${separator}${modalInputValue.value}`;
await window.services.createDirectory(newPath); await window.services.createDirectory(newPath);
message.success('创建文件夹成功'); message.success('创建文件夹成功');
} }
@ -225,6 +241,8 @@ const handleModalOk = async () => {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--card-background, #fff);
color: var(--text-color, #000);
} }
.tree-header { .tree-header {
@ -232,7 +250,7 @@ const handleModalOk = async () => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid var(--border-color, #f0f0f0);
} }
.title { .title {
@ -245,6 +263,17 @@ const handleModalOk = async () => {
padding: 8px 0; padding: 8px 0;
} }
/* 覆盖 Ant Design Tree 的一些样式以美化 */
:deep(.ant-tree) {
background: transparent;
color: inherit;
}
:deep(.ant-tree-treenode) {
padding-bottom: 4px;
}
.empty-tip { .empty-tip {
padding: 20px; padding: 20px;
text-align: center; text-align: center;
@ -252,7 +281,30 @@ const handleModalOk = async () => {
} }
.tree-node-title { .tree-node-title {
display: inline-block; overflow: hidden;
width: 100%; text-overflow: ellipsis;
white-space: nowrap;
flex: 1; /* allow it to take remaining space but not force width */
margin-left: 4px; /* spacing between icon and text */
} }
:deep(.ant-tree-node-content-wrapper) {
display: flex;
align-items: center;
overflow: hidden;
min-height: 28px; /* improve touch target / spacing */
padding: 0 4px;
border-radius: 4px;
transition: all 0.2s;
}
:deep(.ant-tree-node-content-wrapper:hover) {
background-color: var(--hover-background, rgba(0, 0, 0, 0.04));
}
:deep(.ant-tree-node-selected .ant-tree-node-content-wrapper) {
background-color: var(--primary-color-bg, #e6f7ff) !important;
color: var(--primary-color, #1890ff);
}
</style> </style>

View File

@ -29,6 +29,10 @@
<a-button type="text" size="small" @click="$emit('toggle-git')" title="Git 操作"> <a-button type="text" size="small" @click="$emit('toggle-git')" title="Git 操作">
<template #icon><GithubOutlined /></template> <template #icon><GithubOutlined /></template>
</a-button> </a-button>
<a-button type="text" size="small" @click="$emit('toggle-settings')" title="设置">
<template #icon><SettingOutlined /></template>
</a-button>
<a-dropdown> <a-dropdown>
<a-button type="text" size="small" title="导出"> <a-button type="text" size="small" title="导出">

View File

@ -18,7 +18,24 @@ export function useTheme() {
const updateCSSVariables = () => { const updateCSSVariables = () => {
const root = document.documentElement; const root = document.documentElement;
root.style.setProperty('--primary-color', primaryColor.value); root.style.setProperty('--primary-color', primaryColor.value);
// Ant Design uses generated colors, but we can set the main one for our custom CSS
// 生成一个浅色背景用于选中态
// 简单变淡处理,或者直接使用 opacity
root.style.setProperty('--primary-color-bg', primaryColor.value + '1A'); // 10% opacity
if (isDark.value) {
root.style.setProperty('--card-background', '#1f1f1f');
root.style.setProperty('--text-color', 'rgba(255, 255, 255, 0.85)');
root.style.setProperty('--border-color', '#303030');
root.style.setProperty('--hover-background', 'rgba(255, 255, 255, 0.08)');
root.style.setProperty('--text-color-secondary', 'rgba(255, 255, 255, 0.45)');
} else {
root.style.setProperty('--card-background', '#ffffff');
root.style.setProperty('--text-color', 'rgba(0, 0, 0, 0.85)');
root.style.setProperty('--border-color', '#f0f0f0');
root.style.setProperty('--hover-background', 'rgba(0, 0, 0, 0.04)');
root.style.setProperty('--text-color-secondary', 'rgba(0, 0, 0, 0.45)');
}
}; };
const getAntdTheme = () => { const getAntdTheme = () => {