增加资源管理器页面
一、预览
- 文件管理器
- 文章大纲
- 微信
- 邮箱
- 腾讯会议
- 网络云盘
- 文心一言
- 通义千问
- CSDN
- GitHub
- 绘图工具
- Mermaid
- PlantUml
- Json编辑
- Yaml编辑
二、文件管理器
经常写文档的时候,是把所有的md文件放在一起,这样找起来也比较好找,这样就需要一个文件管理器来显示所有的文件。
当前的功能,是从用户指定的文件夹下进行显示,不支持全系统盘处理,那文件太多了。
目前只支持显示.md
、.png
、.jpg
后缀的文件,其他类型的不在管理器中显示。

图标什么的后面再进行优化吧。
2.1 打开文件夹
利用electron的dialog和fs.readdir,递归读取文件夹下的文件列表,只保留文件夹和.md
、.png
、.jpg
后缀的文件属性。
通过mainWindow.webContents.send('file-system-data', JSON.stringify(mdFiles))
将信息传递给渲染进程。
就是我们的资源管理器vue组件。
递归读取文件夹Tree参考
TypeScript |
---|
| function shouOpenDirectoryDialog(mainWindow: Electron.BrowserWindow) {
dialog
.showOpenDialog(mainWindow, {
properties: ['openDirectory']
})
.then((result) => {
if (result.canceled) return
const dirPath = result.filePaths[0]
traverseDirectory(dirPath, (mdFiles) => {
// 发送文件名列表到渲染进程
mainWindow.webContents.send('file-system-data', JSON.stringify(mdFiles))
})
})
.catch((err) => {
console.error('Error opening directory dialog:', err)
})
}
// 递归读取目录中的 .md 文件
// 递归读取目录中的 .md 文件,并构建目录树
function traverseDirectory(dir, callback) {
fs.readdir(dir, (err, files) => {
if (err) {
console.error(err)
return
}
const items = files.map((file) => {
const fullPath = path.join(dir, file)
return {
name: file,
path: fullPath,
isDirectory: false, // 默认为文件
children: [] // 初始化 children 为空数组
}
})
Promise.all(
items.map((item) => {
return new Promise((resolve, reject) => {
fs.lstat(item.path, (err, stats) => {
if (err) {
reject(err)
} else {
item.isDirectory = stats.isDirectory()
if (item.isDirectory) {
// 如果是目录,则递归调用 traverseDirectory
traverseDirectory(item.path, (subItems) => {
item.children = subItems
item.type = 'folder'
resolve(item)
})
} else if (
path.extname(item.name) === '.md' ||
path.extname(item.name) === '.png' ||
path.extname(item.name) === '.jpg'
) {
// 如果是 .md 文件,则直接解析
item.type = 'file'
resolve(item)
} else {
// 对于非 .md 文件,我们不需要它,所以简单地解析
resolve(null)
}
}
})
})
})
)
.then((resolvedItems) => {
// 过滤掉非 .md 文件和目录(它们为 null)
const filteredItems = resolvedItems.filter(Boolean)
// 构建完整的目录树
const tree = filteredItems.reduce((acc, item) => {
if (item.isDirectory) {
// 如果目录已经在树中,则添加其子项
const existingDir = acc.find((dir) => dir.path === item.path)
if (existingDir) {
existingDir.children = existingDir.children.concat(item.children)
} else {
acc.push(item)
}
} else {
// 对于文件,直接添加到树中(假设它们总是添加到顶层目录)
acc.push(item)
}
return acc
}, [])
// 调用回调并传入目录树
callback(tree)
})
.catch((err) => {
console.error(err)
})
})
}
|
2.2 资源管理器
HTML |
---|
| <template>
<div v-show="showFileExplorer" id="resource-manager-component" class="resource-manager-component">
<div id="resizer-navi-tab-file-manager" class="resizer-navi-tab-file-manager"></div>
<div id="file-manager" class="file-manager">
<div id="file-tree">
<FileTreeNode
v-for="item in fileNodes"
:key="item.id"
:ref="`node-${item.id}`"
v-model:is-expanded="item.isExpanded"
v-model:file-extension="item.fileExtension"
:is-indented="false"
:node="item"
/>
</div>
</div>
</div>
<div v-show="showMarkdownToc" id="markdown-toc-component" class="markdown-toc-component">
<div
style="width: 1px; height: 100%; background-color: #00b0ff; color: #00b0ff; fill: #00b0ff"
></div>
<div id="markdown-toc-heading">
<div v-for="item in tocArray" :id="item.id" :key="item.id" @click="scrollToSection(item)">
<!-- 根据 level 添加适当的缩进 -->
<span class="markdown-toc-title" v-html="getIndentedText(item)"></span>
</div>
</div>
</div>
</template>
|
在资源管理器组件中,监听主进程的消息,收到后给组件进行解析显示。这里用了子组件FileTreeNode.vue和MarkdownTOC.vue,通过v-show进行显示的切换
TypeScript |
---|
| window.electron.ipcRenderer.on('file-system-data', (_, fileTree: string) => {
try {
// 更新响应式数据
fileNodes.value = JSON.parse(fileTree) as FileSysItem[]
} catch (error) {
console.error('Error parsing file system data:', error)
}
})
function SwitchResourceManager(value: string) {
if (value == 'markdown-toc') {
// 保存当前tree信息
showMarkdownToc.value = true
showFileExplorer.value = false
EventBus.$emit('monaco-editor-get-chapters', true)
} else if (value == 'file-explorer') {
showFileExplorer.value = true
showMarkdownToc.value = false
EventBus.$emit('monaco-editor-switch-explorer', true)
}
}
// 监听父组件切换
watch(
() => props.naviShow,
(value) => {
SwitchResourceManager(value)
}
)
|
2.3 FileTreeNode

这里面就是递归显示处理了,并在文件操作上增加了点击事件。
递归显示文件树
JavaScript |
---|
| <template>
<div id="file-tree-node" class="file-tree-node" :class="{ indented: isIndented }">
<div
id="node-content"
class="node-content"
@click="handleClick(node)"
@contextmenu.prevent="onContextMenu($event, node)"
>
<!-- 如果是文件夹,显示文件夹图标和名称,并提供一个展开/收起按钮 -->
<span v-if="node.type === 'folder'">
<button style="border: none; background-color: transparent" @click="toggleFolder">
<svg
v-if="getSvg(isExpanded, 'collapse')"
:class="['folder-collapse', getSvg(isExpanded, 'collapse').className]"
:style="getSvg(isExpanded, 'collapse').style"
:viewBox="getSvg(isExpanded, 'collapse').viewBox"
>
<path :d="getSvg(isExpanded, 'collapse').path" />
</svg>
</button>
<svg
v-if="getSvg(isExpanded, 'folder')"
:class="['folder-icon', getSvg(isExpanded, 'folder').className]"
:style="getSvg(isExpanded, 'folder').style"
:viewBox="getSvg(isExpanded, 'folder').viewBox"
>
<path :d="getSvg(isExpanded, 'folder').path" />
</svg>
</span>
<!-- 如果是文件,只显示文件图标和名称 -->
<span v-else>
<svg
v-if="fileExtension && getSvg(false, fileExtension)"
:class="['file-icon', getSvg(false, fileExtension).className]"
:style="getSvg(false, fileExtension).style"
:viewBox="getSvg(false, fileExtension).viewBox"
>
<path :d="getSvg(false, fileExtension).path" />
</svg>
</span>
<span id="file-manager-node" class="file-manager-node">{{ node.name }}</span>
</div>
<!-- 如果当前是文件夹并且已经展开,递归显示子节点 -->
<div v-if="node.type === 'folder' && isExpanded" id="file-subtree" class="file-subtree">
<FileTreeNode
v-for="child in node.children"
:key="child.id"
:ref="`node-${child.id}`"
v-model:is-expanded="child.isExpanded"
v-model:file-extension="child.fileExtension"
:node="child"
:is-indented="true"
@contextmenu:node="onContextMenu($event, node)"
/>
</div>
</div>
</template>
|
打开文件处理这里不说了,可以参考文件菜单的处理
这里就是给主进程发送了一条消息,主进程收到后,读取文件内容,然后将内容发送给monaco-editor组件的渲染进程,然后显示在编辑器,同时通过markdown-it进行渲染,显示在预览区域。
三、文章大纲

大纲的显示,直接从文章中获取对应的标题,通过内部的通信方式(Event-bus),传递给资源管理器组件,然后进行显示。
JavaScript |
---|
| function getIndentedText(item: MarkdownTOC): string {
const levelStr = item.level.slice(1)
if (!levelStr) {
return item.text
}
// 返回带有缩进的文本
const levelNum = parseInt(levelStr, 10)
const indent = ' '.repeat(levelNum * 2)
return `${indent}${item.text}`
}
function scrollToSection(item: MarkdownTOC) {
EventBus.$emit('monaco-editor-locate-target-line', item)
}
onMounted(() => {
EventBus.$on('monaco-editor-chapters', (toc: MarkdownTOC[]) => {
tocArray.value = toc
})
onBeforeUnmount(() => {
EventBus.$off('monaco-editor-chapters', () => {})
})
})
|
四、辅助按钮
为了方便日常的使用,这里在资源管理器左侧的导航列,增加了一些日常使用的应用快捷通道。
在NaviTab.vue中增加下拉按钮,按照如下方式增加快捷按钮:
JavaScript |
---|
| <button data-index="2" title="邮件" class="navi-tab-item outline" @click="onOpenEmailTool">
<svg viewBox="0 -0.5 1025 1025" class="icon" xmlns="http://www.w3.org/2000/svg">
<path
d="M509.3 606.2c-27.9 0-55.6-9-78.7-26.9L36.4 245.7c-18-15.2-20.2-42.2-5-60.1 15.2-18 42.2-20.2 60.1-5L484.3 513c14.4 11.1 36.5 11.1 52.4-1.2l396.2-331.4c18.1-15.1 45-12.8 60.1 5.4 15.1 18.1 12.7 45-5.4 60.1L590.1 578.3c-24.1 18.7-52.6 27.9-80.8 27.9z"
fill="#5F6379"
/>
<path
d="M894.8 938.6H129.4c-71.3 0-129.4-58-129.4-129.4v-552c0-71.3 58-129.4 129.4-129.4h765.4c71.3 0 129.4 58 129.4 129.4v552.1c0 71.3-58.1 129.3-129.4 129.3zM129.4 213.2c-24.3 0-44 19.8-44 44v552.1c0 24.3 19.8 44 44 44h765.4c24.3 0 44-19.8 44-44V257.2c0-24.3-19.8-44-44-44H129.4z"
fill="#3688FF"
/>
</svg>
</button>
function onOpenEmailTool() {
console.log('navi-tab-open-exe')
window.electron.ipcRenderer.send(
'navi-tab-open-exe',
'D:\\yeah.net\\MailMaster\\Application\\mailmaster.exe'
)
}
|
通过electron的API接口,发送消息到主进程,由主进程调用接口,启动应用。
JavaScript |
---|
| ipcMain.on('navi-tab-open-exe', async (_, exePath: string) => {
await shell.openPath(exePath)
})
|

这里增加了常用的邮件、浏览器、会议、微信、QQ、百度网盘、钉钉等应用,目前应用地址是写死的,后续可以考虑优化,改成可配置的。
最终的方案,期望做成通过配置按钮,配置应用的路径、应用图标、应用类型。也可以增加一些网站的快速链接。