Electron-vite【实战】MD 编辑器 -- 文件列表(含右键快捷菜单,重命名文件,删除本地文件,打开本地目录等)
最终效果
页面
src/renderer/src/App.vue
<div class="dirPanel"><div class="panelTitle">文件列表</div><div class="searchFileBox"><Icon class="searchFileInputIcon" icon="material-symbols-light:search" /><inputv-model="searchFileKeyWord"class="searchFileInput"type="text"placeholder="请输入文件名"/><Iconv-show="searchFileKeyWord"class="clearSearchFileInputBtn"icon="codex:cross"@click="clearSearchFileInput"/></div><div class="dirListBox"><divv-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.contentcurrentFilePath.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}
}
重命名文件
实现思路
- 点击右键快捷菜单的“重命名”
- 将被点击的文件列表项的 contenteditable 变为 true,使其成为一个可编辑的div
- 全选文件列表项内的文本
- 输入新的文件名
- 在失去焦点/按Enter键时,开始尝试保存文件名
- 若新文件名与旧文件名相同,则直接将被点击的文件列表项的 contenteditable 变为 false
- 若新文件名与本地文件名重复,则弹窗提示该文件名已存在,需换其他文件名
- 若新文件名合规,则执行保存文件名
- 被点击的文件列表项的 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,使其成为一个可编辑的divfile.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 变为 falseif (newFileName === item.fileName.replace('.md', '')) {item.editable = falsereturn}// 拼接新的文件路径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 变为 falseif (currentFilePath.value === item.filePath) {currentFilePath.value = newFilePath}item.fileName = `${newFileName}.md`item.filePath = newFilePathitem.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 = ''}}})
相关文章:

Electron-vite【实战】MD 编辑器 -- 文件列表(含右键快捷菜单,重命名文件,删除本地文件,打开本地目录等)
最终效果 页面 src/renderer/src/App.vue <div class"dirPanel"><div class"panelTitle">文件列表</div><div class"searchFileBox"><Icon class"searchFileInputIcon" icon"material-symbols-light:…...

华为云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 设置管理员账号 …...
[git每日一句]Your branch is up to date with ‘origin/master‘
这句话是 Git 版本控制系统的提示信息,意思是: "你当前所在的分支已经与远程仓库(origin)的 master 分支同步,没有需要推送的提交。" 详细解释: Your branch - 指你当前所在的本地分支 is up …...

高密爆炸警钟长鸣:AI为化工安全戴上“智能护盾”
一、高密爆炸:一声巨响,撕开化工安全“伤疤” 2025年5月27日,山东高密友道化学有限公司的车间爆炸声,像一把利刃划破了化工行业的平静。剧烈的冲击波将车间夷为平地,黑色蘑菇云腾空而起,刺鼻的化学气味弥漫…...

机器人学基础——正运动学(理论推导及c++实现)
机器人正运动学 机器人正运动学一般是指从机器人的关节位置到基于参考坐标系下末端执行器的位置。 平移变换和旋转变换 平移变换 假设我们有两个坐标系A和B,坐标系A与B的方位相同,xyz轴的指向都是一致的,即没有旋转变换。有一点p…...

[网页五子棋][对战模块]处理连接成功,通知玩家就绪,逻辑问题(线程安全,先手判定错误)
文章目录 处理连接成功通知玩家就绪逻辑图问题 1:线程安全问题 2:先手判定错误两边都是提示:轮到对方落子 处理连接成功 实现 GameAPI 的 afterC…...
TensorFlow Extended (TFX) 生产环境模型版本控制与回滚实战指南
TFX 版本控制核心架构 TFX 通过以下组件构建完整的模型生命周期管理系统: ML Metadata (MLMD):记录所有实验和管道的元数据Pusher 组件:负责模型部署与版本标记Model Registry:集中式模型存储库&#x…...

【Web应用】若依框架:基础篇11功能详解-系统接口
文章目录 ⭐前言⭐一、课程讲解⭐二、自己动手实操⭐总结 标题详情作者JosieBook头衔CSDN博客专家资格、阿里云社区专家博主、软件设计工程师博客内容开源、框架、软件工程、全栈(,NET/Java/Python/C)、数据库、操作系统、大数据、人工智能、工控、网络、…...

【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…...
Redis 常用数据类型和命令使用
目录 1 string 2 hash 3 list 4 set集合 5 zset有序集合 1 string 值可以是字符串、数字和二进制的value,值最大不能超过512MB 应用场景: 应用程序缓存 计数器 web共享session 限速 1.1 设置单个键值 set <key> value [EX seconds|PX…...

【Linux系统】第八节—进程概念(上)—冯诺依曼体系结构+操作系统+进程及进程状态+僵尸进程—详解!
hi,我是云边有个稻草人 偶尔中二的博主^(* ̄(oo) ̄)^,与你分享专业知识,祝博主们端午节快乐! Linux—本节博客所属专栏—持续更新中—欢迎订阅! 目录 一、冯诺依曼体系结构 二、操作系统(Opera…...
WPF 全局加载界面、多界面实现渐变过渡效果
WPF 全局加载界面与渐变过渡效果 完整实现方案 MainWindow.xaml <Window x:Class"LoadingScreenDemo.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml&quo…...
WebSocket与实时对话式AI服务的集成
WebSocket与实时对话式AI服务的集成 在现代对话式AI系统中,传统的HTTP请求-响应模型已难以满足实时交互的体验需求。特别是用户对响应速度、逐字输出、会话上下文保持等方面提出更高要求时,需要一种能够建立持久连接并支持双向通信的协议。WebSocket正是在这一背景下,成为A…...
【xmb】】内部文档148344599
这里写自定义目录标题 CyberDog 2 仿真智能物流配送系统 – 初赛设计报告摘要目录1 引言2 任务与需求分析3 系统总体设计4 核心算法与模块实现5 仿真测试与结果分析6 结论与展望 CyberDog 2 仿真智能物流配送系统 – 初赛设计报告 团队名称: (晚点写&am…...
MobaXterm国内下载与安装使用教程
MobaXterm是一款为 Windows 用户量身打造的远程终端工具,它将多种网络功能集成在一个轻量级、便携式的界面中,尤其适合需要频繁与远程主机交互的开发者、系统运维工程师以及科研技术人员。无论是管理 Linux 服务器、远程执行命令,还是图形化运…...

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

代谢组数据分析(二十六):LC-MS/MS代谢组学和脂质组学数据的分析流程
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包依赖包安装包加载需要的R包数据下载以及转换mzML数据预处理代谢物注释LipidFinder过滤MultiABLER数据预处理过滤补缺失值对数变换数据标准化下游数据分析总结系统信息参考介…...
服务器上用脚本跑python深度学习的注意事项(ubantu系统)
bash: $\r: command not found 问题原因: 出现 bash: $\r: command not found 以及路径中出现 \r 通常是因为脚本文件是在Windows系统下编辑,然后在Linux(如Ubuntu)系统中运行。在Windows系统中,文本文件的换行符是 \…...

【ARM】【FPGA】【硬件开发】Chapter.1 AXI4总线协议
Chapter.1 AXI4总线协议 作者:齐花Guyc(CAUC) 一、总线介绍 AXI4总线 AXI4总线就像是SoC内部的“高速公路”,负责在不同硬件模块之间高效传输数据。 AXI4协议通过 5个独立通道 传输数据和控制信号,每个通道都有自己的信号线,互…...
青少年编程与数学 02-020 C#程序设计基础 10课题、桌面应用开发
青少年编程与数学 02-020 C#程序设计基础 10课题、桌面应用开发 一、桌面应用1. 主要特点2. 常见类型3. 优势4. 局限性 二、开发步骤1. 准备工作2. 创建项目3. 开发应用4. 运行调试5. 打包发布 三、Windows 窗体应用(一)定义(二)特…...

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

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

【经验】Ubuntu中设置terminator的滚动行数、从Virtualbox复制到Windows时每行后多一空行
1、设置terminator的滚动行数 1.1 问题描述 在终端 terminator 中,调试程序时,只能查看有限行数的打印日志,大约是500行,怎么能增加行数 1.2 解决方法 1)安装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渗透之矛盾
开启场景,打开网页。发现是一段php代码。 这段代码也很好理解,就是get方式传参num,如果num不是数字类型,那么输出num的值,并且num1时,输出flag的值。 首先看看is_numeric的意思。 开始我想到了使用科学技术…...
hot100 -- 4.子串系列
1.和为 K 的子数组 问题: 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。 子数组是数组中元素的连续非空序列。 方法1:暴力枚举 # 方法1:暴力枚举(遍历子数组起点和终点&…...

Python实现P-PSO优化算法优化卷积神经网络CNN回归模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 随着人工智能和深度学习技术的快速发展,卷积神经网络(CNN)在图像分类、目标检测…...

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

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