跳转至

增加资源管理器页面

一、预览

  • 文件管理器
  • 文章大纲
  • 微信
  • 邮箱
  • 腾讯会议
  • 网络云盘
  • 文心一言
  • 通义千问
  • 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 = '&nbsp;'.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
1
2
3
  ipcMain.on('navi-tab-open-exe', async (_, exePath: string) => {
    await shell.openPath(exePath)
  })

这里增加了常用的邮件、浏览器、会议、微信、QQ、百度网盘、钉钉等应用,目前应用地址是写死的,后续可以考虑优化,改成可配置的。

最终的方案,期望做成通过配置按钮,配置应用的路径、应用图标、应用类型。也可以增加一些网站的快速链接。