Electron-vite【实战】MD 编辑器 -- 文件列表(含右键快捷菜单,重命名文件,删除本地文件,打开本地目录等)

news2025/6/4 2:59:13

最终效果

在这里插入图片描述

页面

src/renderer/src/App.vue

    <div class="dirPanel">
      <div class="panelTitle">文件列表</div>
      <div class="searchFileBox">
        <Icon class="searchFileInputIcon" icon="material-symbols-light:search" />
        <input
          v-model="searchFileKeyWord"
          class="searchFileInput"
          type="text"
          placeholder="请输入文件名"
        />
        <Icon
          v-show="searchFileKeyWord"
          class="clearSearchFileInputBtn"
          icon="codex:cross"
          @click="clearSearchFileInput"
        />
      </div>
      <div class="dirListBox">
        <div
          v-for="(item, index) in fileList_filtered"
          :id="`file-${index}`"
          :key="item.filePath"
          class="dirItem"
          spellcheck="false"
          :class="currentFilePath === item.filePath ? 'activeDirItem' : ''"
          :contenteditable="item.editable"
          @click="openFile(item)"
          @contextmenu.prevent="showContextMenu(item.filePath)"
          @blur="saveFileName(item, index)"
          @keydown.enter.prevent="saveFileName_enter(index)"
        >
          {{ item.fileName.slice(0, -3) }}
        </div>
      </div>
    </div>

相关样式

.dirPanel {
  width: 200px;
  border: 1px solid gray;
}
.dirListBox {
  padding: 0px 10px 10px 10px;
}
.dirItem {
  padding: 6px;
  font-size: 12px;
  cursor: pointer;
  border-radius: 4px;
  margin-bottom: 6px;
}
.searchFileBox {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px;
}
.searchFileInput {
  display: block;
  font-size: 12px;
  padding: 4px 20px;
}
.searchFileInputIcon {
  position: absolute;
  font-size: 16px;
  transform: translateX(-80px);
}
.clearSearchFileInputBtn {
  position: absolute;
  cursor: pointer;
  font-size: 16px;
  transform: translateX(77px);
}
.panelTitle {
  font-size: 16px;
  font-weight: bold;
  text-align: center;
  background-color: #f0f0f0;
  height: 34px;
  line-height: 34px;
}

相关依赖

实现图标

npm i --save-dev @iconify/vue

导入使用

import { Icon } from '@iconify/vue'

搜索图标
https://icon-sets.iconify.design/?query=home

常规功能

文件搜索

根据搜索框的值 searchFileKeyWord 的变化动态计算 computed 过滤文件列表 fileList 得到 fileList_filtered ,页面循环遍历渲染 fileList_filtered

const fileList = ref<FileItem[]>([])
const searchFileKeyWord = ref('')
const fileList_filtered = computed(() => {
  return fileList.value.filter((file) => {
    return file.filePath.toLowerCase().includes(searchFileKeyWord.value.toLowerCase())
  })
})

文件搜索框的清空按钮点击事件

const clearSearchFileInput = (): void => {
  searchFileKeyWord.value = ''
}

当前文件高亮

const currentFilePath = ref('')
:class="currentFilePath === item.filePath ? 'activeDirItem' : ''"
.activeDirItem {
  background-color: #e4e4e4;
}

切换打开的文件

点击文件列表的文件名称,打开对应的文件

@click="openFile(item)"
const openFile = (item: FileItem): void => {
  markdownContent.value = item.content
  currentFilePath.value = item.filePath
}

右键快捷菜单

@contextmenu.prevent="showContextMenu(item.filePath)"
const showContextMenu = (filePath: string): void => {
  window.electron.ipcRenderer.send('showContextMenu', filePath)
  // 隐藏其他右键菜单 -- 不能同时有多个右键菜单显示
  hide_editor_contextMenu()
}

触发创建右键快捷菜单

src/main/ipc.ts

import { createContextMenu } from './menu'
  ipcMain.on('showContextMenu', (_e, filePath) => {
    createContextMenu(mainWindow, filePath)
  })

执行创建右键快捷菜单

src/main/menu.ts

const createContextMenu = (mainWindow: BrowserWindow, filePath: string): void => {
  const template = [
    {
      label: '重命名',
      click: async () => {
        mainWindow.webContents.send('do-rename-file', filePath)
      }
    },
    { type: 'separator' }, // 添加分割线
    {
      label: '移除',
      click: async () => {
        mainWindow.webContents.send('removeOut-fileList', filePath)
      }
    },
    {
      label: '清空文件列表',
      click: async () => {
        mainWindow.webContents.send('clear-fileList')
      }
    },
    { type: 'separator' }, // 添加分割线
    {
      label: '打开所在目录',
      click: async () => {
        // 打开目录
        shell.openPath(path.dirname(filePath))
      }
    },
    { type: 'separator' }, // 添加分割线
    {
      label: '删除',
      click: async () => {
        try {
          // 显示确认对话框
          const { response } = await dialog.showMessageBox(mainWindow, {
            type: 'question',
            buttons: ['确定', '取消'],
            title: '确认删除',
            message: `确定要删除文件 ${path.basename(filePath)} 吗?`
          })
          if (response === 0) {
            // 用户点击确定,删除本地文件
            await fs.unlink(filePath)
            // 通知渲染进程文件已删除
            mainWindow.webContents.send('delete-file', filePath)
          }
        } catch {
          dialog.showMessageBox(mainWindow, {
            type: 'error',
            title: '删除失败',
            message: `删除文件 ${path.basename(filePath)} 时出错,请稍后重试。`
          })
        }
      }
    }
  ]
  const menu = Menu.buildFromTemplate(template as MenuItemConstructorOptions[])
  menu.popup({ window: mainWindow })
}
export { createMenu, createContextMenu }

隐藏其他右键菜单

// 隐藏编辑器右键菜单
const hide_editor_contextMenu = (): void => {
  if (isMenuVisible.value) {
    isMenuVisible.value = false
  }
}

重命名文件

在这里插入图片描述

实现思路

  1. 点击右键快捷菜单的“重命名”
  2. 将被点击的文件列表项的 contenteditable 变为 true,使其成为一个可编辑的div
  3. 全选文件列表项内的文本
  4. 输入新的文件名
  5. 在失去焦点/按Enter键时,开始尝试保存文件名
  6. 若新文件名与旧文件名相同,则直接将被点击的文件列表项的 contenteditable 变为 false
  7. 若新文件名与本地文件名重复,则弹窗提示该文件名已存在,需换其他文件名
  8. 若新文件名合规,则执行保存文件名
  9. 被点击的文件列表项的 contenteditable 变为 false

src/renderer/src/App.vue

  window.electron.ipcRenderer.on('do-rename-file', (_, filePath) => {
    fileList_filtered.value.forEach(async (file, index) => {
      // 找到要重命名的文件
      if (file.filePath === filePath) {
        // 将被点击的文件列表项的 contenteditable 变为 true,使其成为一个可编辑的div
        file.editable = true
        // 等待 DOM 更新
        await nextTick()
        // 全选文件列表项内的文本
        let divElement = document.getElementById(`file-${index}`)
        if (divElement) {
          const range = document.createRange()
          range.selectNodeContents(divElement) // 选择 div 内所有内容
          const selection = window.getSelection()
          if (selection) {
            selection.removeAllRanges() // 清除现有选择
            selection.addRange(range) // 添加新选择
            divElement.focus() // 聚焦到 div
          }
        }
      }
    })
  })
          @blur="saveFileName(item, index)"
          @keydown.enter.prevent="saveFileName_enter(index)"
// 重命名文件时,保存文件名
const saveFileName = async (item: FileItem, index: number): Promise<void> => {
  // 获取新的文件名,若新文件名为空,则命名为 '无标题'
  let newFileName = document.getElementById(`file-${index}`)?.textContent?.trim() || '无标题'
  // 若新文件名与旧文件名相同,则直接将被点击的文件列表项的 contenteditable 变为 false
  if (newFileName === item.fileName.replace('.md', '')) {
    item.editable = false
    return
  }
  // 拼接新的文件路径
  const newFilePath = item.filePath.replace(item.fileName, `${newFileName}.md`)
  // 开始尝试保存文件名
  const error = await window.electron.ipcRenderer.invoke('rename-file', {
    oldFilePath: item.filePath,
    newFilePath,
    newFileName
  })
  if (error) {
    // 若重命名报错,则重新聚焦,让用户重新输入文件名
    document.getElementById(`file-${index}`)?.focus()
  } else {
    // 没报错,则重命名成功,更新当前文件路径,文件列表中的文件名,文件路径,将被点击的文件列表项的 contenteditable 变为 false
    if (currentFilePath.value === item.filePath) {
      currentFilePath.value = newFilePath
    }
    item.fileName = `${newFileName}.md`
    item.filePath = newFilePath
    item.editable = false
  }
}
// 按回车时,直接失焦,触发失焦事件执行保存文件名
const saveFileName_enter = (index: number): void => {
  document.getElementById(`file-${index}`)?.blur()
}

src/main/ipc.ts

  • 检查新文件名是否包含非法字符 (\ / : * ? " < > |)
  • 检查新文件名是否在本地已存在
  • 检查合规,则重命名文件
  ipcMain.handle('rename-file', async (_e, { oldFilePath, newFilePath, newFileName }) => {
    // 检查新文件名是否包含非法字符(\ / : * ? " < > |)
    if (/[\\/:*?"<>|]/.test(newFileName)) {
      return await dialog.showMessageBox(mainWindow, {
        type: 'error',
        title: '重命名失败',
        message: `文件名称 ${newFileName} 包含非法字符,请重新输入。`
      })
    }
    try {
      await fs.access(newFilePath)
      // 若未抛出异常,说明文件存在
      return await dialog.showMessageBox(mainWindow, {
        type: 'error',
        title: '重命名失败',
        message: `文件 ${path.basename(newFilePath)} 已存在,请选择其他名称。`
      })
    } catch {
      // 若抛出异常,说明文件不存在,可以进行重命名操作
      return await fs.rename(oldFilePath, newFilePath)
    }
  })

移除文件

将文件从文件列表中移除(不会删除文件)

  window.electron.ipcRenderer.on('removeOut-fileList', (_, filePath) => {
    // 过滤掉要删除的文件
    fileList.value = fileList.value.filter((file) => {
      return file.filePath !== filePath
    })
    // 若移除的当前打开的文件
    if (currentFilePath.value === filePath) {
      // 若移除目标文件后,还有其他文件,则打开第一个文件
      if (fileList_filtered.value.length > 0) {
        openFile(fileList_filtered.value[0])
      } else {
        // 若移除目标文件后,没有其他文件,则清空内容和路径
        markdownContent.value = ''
        currentFilePath.value = ''
      }
    }
  })

清空文件列表

  window.electron.ipcRenderer.on('clear-fileList', () => {
    fileList.value = []
    markdownContent.value = ''
    currentFilePath.value = ''
  })

用资源管理器打开文件所在目录

在这里插入图片描述
直接用 shell 打开

src/main/menu.ts

    {
      label: '打开所在目录',
      click: async () => {
        shell.openPath(path.dirname(filePath))
      }
    },

删除文件

src/main/menu.ts

    {
      label: '删除',
      click: async () => {
        try {
          // 显示确认对话框
          const { response } = await dialog.showMessageBox(mainWindow, {
            type: 'question',
            buttons: ['确定', '取消'],
            title: '确认删除',
            message: `确定要删除文件 ${path.basename(filePath)} 吗?`
          })
          if (response === 0) {
            // 用户点击确定,删除本地文件
            await fs.unlink(filePath)
            // 通知渲染进程,将文件从列表中移除
            mainWindow.webContents.send('removeOut-fileList', filePath)
          }
        } catch {
          dialog.showMessageBox(mainWindow, {
            type: 'error',
            title: '删除失败',
            message: `删除文件 ${path.basename(filePath)} 时出错,请稍后重试。`
          })
        }
      }
    }

src/renderer/src/App.vue

同移除文件

  window.electron.ipcRenderer.on('removeOut-fileList', (_, filePath) => {
    // 过滤掉要删除的文件
    fileList.value = fileList.value.filter((file) => {
      return file.filePath !== filePath
    })
    // 若移除的当前打开的文件
    if (currentFilePath.value === filePath) {
      // 若移除目标文件后,还有其他文件,则打开第一个文件
      if (fileList_filtered.value.length > 0) {
        openFile(fileList_filtered.value[0])
      } else {
        // 若移除目标文件后,没有其他文件,则清空内容和路径
        markdownContent.value = ''
        currentFilePath.value = ''
      }
    }
  })

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2396421.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为云Flexus+DeepSeek征文|华为云Flexus云服务器X实例上部署Dify:打造高效的开源大语言模型应用开发平台

目录 前言 1 Dify与华为云部署概述 1.1 什么是 Dify 1.2 华为云与 Flexus 云服务器的优势 2 云服务器部署 Dify 的步骤详解 2.1 模板选择 2.2 参数配置 2.3 资源栈设置 2.4 确认部署信息并执行 3 部署成功后的操作与平台使用指南 3.1 访问平台 3.2 设置管理员账号 …

高密爆炸警钟长鸣:AI为化工安全戴上“智能护盾”

一、高密爆炸&#xff1a;一声巨响&#xff0c;撕开化工安全“伤疤” 2025年5月27日&#xff0c;山东高密友道化学有限公司的车间爆炸声&#xff0c;像一把利刃划破了化工行业的平静。剧烈的冲击波将车间夷为平地&#xff0c;黑色蘑菇云腾空而起&#xff0c;刺鼻的化学气味弥漫…

机器人学基础——正运动学(理论推导及c++实现)

机器人正运动学 机器人正运动学一般是指从机器人的关节位置到基于参考坐标系下末端执行器的位置。 平移变换和旋转变换 平移变换 假设我们有两个坐标系A和B&#xff0c;坐标系A与B的方位相同&#xff0c;xyz轴的指向都是一致的&#xff0c;即没有旋转变换。有一点p&#xf…

[网页五子棋][对战模块]处理连接成功,通知玩家就绪,逻辑问题(线程安全,先手判定错误)

文章目录 处理连接成功通知玩家就绪逻辑图问题 1&#xff1a;线程安全问题 2&#xff1a;先手判定错误两边都是提示&#xff1a;轮到对方落子![image.png](https://i-blog.csdnimg.cn/img_convert/c570cd26eadbe87ed467bc4edaa7945e.png) 处理连接成功 实现 GameAPI 的 afterC…

【Web应用】若依框架:基础篇11功能详解-系统接口

文章目录 ⭐前言⭐一、课程讲解⭐二、自己动手实操⭐总结 标题详情作者JosieBook头衔CSDN博客专家资格、阿里云社区专家博主、软件设计工程师博客内容开源、框架、软件工程、全栈&#xff08;,NET/Java/Python/C&#xff09;、数据库、操作系统、大数据、人工智能、工控、网络、…

【Docker项目实战篇】Docker部署PDF查看器PdfDing

【Docker项目实战篇】Docker部署PDD查看器PdfDing 一、PdfDing介绍1.1 PdfDing简介1.2 PdfDing主要特点1.3 主要使用场景 二、本次实践规划2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、下载Pd…

【Linux系统】第八节—进程概念(上)—冯诺依曼体系结构+操作系统+进程及进程状态+僵尸进程—详解!

hi&#xff0c;我是云边有个稻草人 偶尔中二的博主^(*&#xffe3;(oo)&#xffe3;)^&#xff0c;与你分享专业知识&#xff0c;祝博主们端午节快乐&#xff01; Linux—本节博客所属专栏—持续更新中—欢迎订阅&#xff01; 目录 一、冯诺依曼体系结构 二、操作系统(Opera…

数据结构——优先级队列(PriorityQueue)

1.优先级队列 优先级队列可以看作队列的另一个版本&#xff0c;队列的返回元素是由是由插入顺序决定的&#xff0c;先进先出嘛&#xff0c;但是有时我们可能想要返回优先级较高的元素&#xff0c;比如最大值&#xff1f;这种场景下就由优先级队列登场。 优先级队列底层是由堆实…

代谢组数据分析(二十六):LC-MS/MS代谢组学和脂质组学数据的分析流程

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包依赖包安装包加载需要的R包数据下载以及转换mzML数据预处理代谢物注释LipidFinder过滤MultiABLER数据预处理过滤补缺失值对数变换数据标准化下游数据分析总结系统信息参考介…

【ARM】【FPGA】【硬件开发】Chapter.1 AXI4总线协议

Chapter.1 AXI4总线协议 作者&#xff1a;齐花Guyc(CAUC) 一、总线介绍 AXI4总线 AXI4总线就像是SoC内部的“高速公路”&#xff0c;负责在不同硬件模块之间高效传输数据。 AXI4协议通过 5个独立通道 传输数据和控制信号&#xff0c;每个通道都有自己的信号线&#xff0c;互…

把 jar 打包成 exe

1. 把自己的项目先正常打成jar包 2. 使用exe4j工具将jar转换为exe 2.1 exe4j下载地址&#xff1a;https://www.ej-technologies.com/download/exe4j/files 2.2 下载完成之后激活 2.3 可以点击Change License&#xff0c;输入秘钥L-g782dn2d-1f1yqxx1rv1sqd 2.4 直接下一步…

【目标检测】检测网络中neck的核心作用

1. neck最主要的作用就是特征融合&#xff0c;融合就是将具有不同大小感受野的特征图进行了耦合&#xff0c;从而增强了特征图的表达能力。 2. neck决定了head的数量&#xff0c;进而潜在决定了不同尺度样本如何分配到不同的head&#xff0c;这一点可以看做是将整个网络的多尺…

【经验】Ubuntu中设置terminator的滚动行数、从Virtualbox复制到Windows时每行后多一空行

1、设置terminator的滚动行数 1.1 问题描述 在终端 terminator 中&#xff0c;调试程序时&#xff0c;只能查看有限行数的打印日志&#xff0c;大约是500行&#xff0c;怎么能增加行数 1.2 解决方法 1&#xff09;安装terminator sudo apt install terminator和 terminato…

使用微软最近开源的WSL在Windows上优雅的运行Linux

install wsl https://github.com/microsoft/WSL/releases/download/2.4.13/wsl.2.4.13.0.x64.msi install any distribution from microsoft store, such as kali-linux from Kali office website list of distribution PS C:\Users\50240> wsl -l -o 以下是可安装的有…

HackMyVM-Teacher

信息搜集 主机发现 ┌──(kali㉿kali)-[~] └─$ nmap -sn 192.168.43.0/24 Starting Nmap 7.95 ( https://nmap.org ) at 2025-06-01 01:02 EDT Nmap scan report for 192.168.43.1 Host is up (0.0084s latency). MAC Address: C6:45:66:05:91:88 (Unknow…

BugKu Web渗透之矛盾

开启场景&#xff0c;打开网页。发现是一段php代码。 这段代码也很好理解&#xff0c;就是get方式传参num&#xff0c;如果num不是数字类型&#xff0c;那么输出num的值&#xff0c;并且num1时&#xff0c;输出flag的值。 首先看看is_numeric的意思。 开始我想到了使用科学技术…

Python实现P-PSO优化算法优化卷积神经网络CNN回归模型项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档&#xff09;&#xff0c;如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 随着人工智能和深度学习技术的快速发展&#xff0c;卷积神经网络&#xff08;CNN&#xff09;在图像分类、目标检测…

ssm 学习笔记day03

环境搭建 spring配置数据库 1.在pom.xml安装相应的依赖 2.在properties里面配置数据库的相关信息&#xff0c;需要强调的一点是&#xff0c;一定不要在properties里面添加任何空格&#xff0c;否则就会像我一样搞了两小时&#xff0c;数据一直报错&#xff0c;然后发现是空格的…

mkdir: cannot create directory ‘gitlab-stu’: No space left on device

Linux中创建目录时报错“mkdir: cannot create directory ‘gitlab-stu’: No space left on device”&#xff0c;磁盘空间不足。 使用df命令查看&#xff0c;发现 / 下面use%占满了&#xff1a; 查看inode使用情况&#xff1a; 可以看到docker的数据大部分存放在/var/lib/do…

ESP8285乐鑫SOCwifi芯片32bit MCU和2.4 GHz Wi-Fi

简介 ESP8285 拥有完整的且⾃成体系的 Wi-Fi ⽹络功能&#xff0c;既能够独⽴应⽤&#xff0c;也可以作为从机搭载于其他主机 MCU 运⾏。当 ESP8285 独⽴应⽤时&#xff0c;能够直接从外接 flash 中启动。内置的⾼速缓冲存储器有利于提⾼系统性能&#xff0c;并且优化存储系统。…