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

article/2025/8/2 7:33:16

最终效果

在这里插入图片描述

页面

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}
}

重命名文件

在这里插入图片描述

实现思路

  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,使其成为一个可编辑的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 = ''}}})

http://www.hkcw.cn/article/bjXSePMIih.shtml

相关文章

【数据库】并发控制

并发控制 在数据库系统&#xff0c;经常需要多个用户同时使用。同一时间并发的事务可达数百个&#xff0c;这就是并发引入的必要性。 常见的并发系统有三种&#xff1a; 串行事务执行&#xff08;X&#xff09;&#xff0c;每个时刻只有一个事务运行&#xff0c;不能充分利用…

Golang持续集成与自动化测试和部署

概述 Golang是一门性能优异的静态类型语言&#xff0c;但因其奇快的编译速度&#xff0c;结合DevOps, 使得它也非常适合快速开发和迭代。 本文讲述如何使用Golang, 进行持续集成与自动化测试和部署。主要使用了以下相关技术&#xff1a; dep&#xff1a; 进行包的依赖管理gin…

Google car key:安全、便捷的汽车解锁新选择

有了兼容的汽车和 Android 手机&#xff0c;Google car key可让您将Android 手机用作车钥匙。您可以通过兼容的 Android 手机锁定、解锁、启动汽车并执行更多功能。但是&#xff0c;Google car key安全吗&#xff1f;它是如何工作的&#xff1f;如果我的手机电池没电了怎么办&a…

QT开发技术【QTableView分页实现】

一、引言 在开发桌面应用程序时&#xff0c;当需要展示大量数据到表格中&#xff0c;一次性加载所有数据可能会导致界面卡顿、响应缓慢&#xff0c;甚至内存溢出。QTableView 是 Qt 框架中用于展示表格数据的强大组件&#xff0c;结合 QAbstractTableModel 可以实现数据的分页…

新增Vulkan支持|UWA Gears V1.1.0

UWA Gears 是UWA最新发布的无SDK性能分析工具。针对移动平台&#xff0c;提供了实时监测和截帧分析功能&#xff0c;帮助您精准定位性能热点&#xff0c;提升应用的整体表现。 本次版本更新主要是Frame Capture模式新增对Vulkan项目的支持&#xff0c;进一步满足使用Vulkan开发…

mapbox高阶,PMTiles介绍,MBTiles、PMTiles对比,加载PMTiles文件

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️Fill面图层样式1.4 ☘️PMTiles介绍1.5…

Sums of Sliding Window Maximum_abc407F分析与解答

倒着考虑&#xff0c;考虑每个a_i对哪些k值做出贡献&#xff0c;对一个a_i&#xff0c;定义L_i和R_i为&#xff1a; 以上笔误&#xff1a;R_i的定义应该是&#xff1a;连续最多R_i个元素比a_i 小 如果得到了 L_i和R_i&#xff0c;我们从k的长度从小到大依次看看&#xff0c;a_…

用通义灵码2.5打造智能倒计时日历:从零开始的Python开发体验

前言:为什么选择通义灵码2.5? 通义灵码2.5版本带来了令人兴奋的升级,特别是全新的智能体模式让编程体验焕然一新。作为一名长期关注AI编程助手的开发者,我决定通过开发一个实用的倒计时日历小工具,来全面体验通义灵码2.5的各项新特性。 一、项目构思与智能体协作 首先,…

历年西安电子科技大学计算机保研上机真题

2025西安电子科技大学计算机保研上机真题 2024西安电子科技大学计算机保研上机真题 2023西安电子科技大学计算机保研上机真题 在线测评链接&#xff1a;https://pgcode.cn/school 查找不同的连续数字串个数 题目描述 给定一个数字串&#xff0c;查找其中不同的连续数字串的个…

一文读懂 STP:交换机接口状态详解及工作原理

一文读懂 STP&#xff1a;交换机接口状态详解及工作原理 一. 引言&#xff1a;STP 是什么&#xff0c;为何如此重要&#xff1f;二. STP 的核心作用&#xff1a;避免网络环路2.1 什么是 STP&#xff1f;2.2 STP 的核心概念 三. STP 交换机接口状态详解四. STP 的工作原理&#…

清华大学发Nature!光学工程+神经网络创新结合

2025深度学习发论文&模型涨点之——光学工程神经网络 清华大学的一项开创性研究成果在《Nature》上发表&#xff0c;为光学神经网络的发展注入了强劲动力。该研究团队巧妙地提出了一种全前向模式&#xff08;Fully Forward Mode&#xff0c;FFM&#xff09;的训练方法&…

PHP学习笔记(十一)

类常量 可以把在类中始终保持不变的值定义为常量&#xff0c;类常量的默认可见性是public。 接口中也可以定义常量。 可以用一个变量来动态调用类&#xff0c;但该变量的值不能为关键字 需要注意的是类常量只为每个类分配一次&#xff0c;而不是为每个类的实例分配。 特殊的…

NodeMediaEdge快速上手

NodeMediaEdge快速上手 简介 NodeMediaEdge是一款部署在监控摄像机网络前端中&#xff0c;拉取Onvif或者rtsp/rtmp/http视频流并使用rtmp/kmp推送到公网流媒体服务器的工具。 通过云平台协议注册到NodeMediaServer后&#xff0c;可以同NodeMediaServer结合使用。使用图形化的…

强化学习的前世今生(五)— SAC算法

书接前四篇 强化学习的前世今生&#xff08;一&#xff09; 强化学习的前世今生&#xff08;二&#xff09; 强化学习的前世今生&#xff08;三&#xff09;— PPO算法 强化学习的前世今生&#xff08;四&#xff09;— DDPG算法 本文为大家介绍SAC算法 7 SAC 7.1 最大熵强化…

优质电子实验记录本如何确保数据不泄密?

实验数据是企业和科研机构的核心资产&#xff0c;承载着创新成果与竞争优势&#xff0c;选择合适的实验记录载体至关重要。本文从传统纸质记录的安全性优劣势出发&#xff0c;对比分析普通电子实验记录本存在的安全问题&#xff0c;详细阐述优质电子实验记录本如何构建数据防护…

RFID 助力钢铁钢帘线生产效率质量双提升

RFID 助力钢铁钢帘线生产效率质量双提升 应用背景 钢铁钢帘线广泛应用于建筑、公路、桥梁、隧道、海洋工程等领域。&#xff0c;其质量和生产效率直接影响性能与安全性。在钢铁钢帘线的生产过程中&#xff0c;面临着诸多挑战。传统生产模式下&#xff0c;各生产环节信息传递不…

4.5V~100V, 3.8A 峰值电流限, 非同步, 降压转换器,LA1823完美替换MP9487方案

一&#xff1a;综述 LA1823 是一款易用的非同步&#xff0c;降压转换器。 该模块集成了 500mΩ 低导通阻抗的高侧 MOSFET。LA1823 使用 COT 控制技术。此种控制方式有利于快速动态响应,同时简化了反馈环路的设计。LA1823 可以提供最大 2A 的持续负载电流。LA1823有150kHz/240kH…

多杆合一驱动城市空间治理智慧化

引言&#xff1a;城市“杆林困境”与智慧化破局 走在现代城市的街道上&#xff0c;路灯、监控、交通信号灯、5G基站等杆体林立&#xff0c;不仅侵占公共空间&#xff0c;更暴露了城市治理的碎片化问题。如何让这些“沉默的钢铁”升级为城市的“智慧神经元”&#xff1f;答案在…

ElasticSearch迁移至openGauss

Elasticsearch 作为一种高效的全文搜索引擎&#xff0c;广泛应用于实时搜索、日志分析等场景。而 openGauss&#xff0c;作为一款企业级关系型数据库&#xff0c;强调事务处理与数据一致性。那么&#xff0c;当这两者的应用场景和技术架构发生交集时&#xff0c;如何实现它们之…

搭建 Select 三级联动架构-东方仙盟插件开发 JavaScript ——仙盟创梦IDE

三级级联开卡必要性 在 “东方仙盟” 相关插件开发中&#xff0c;使用原生 HTML 和 JavaScript 实现三级联动选择&#xff08;如村庄 - 建筑 - 单元的选择&#xff09;有以下好处和意义&#xff0c;学校管理&#xff1a; 对游戏体验的提升 增强交互性&#xff1a;玩家能够通…