当前位置: 首页 > ops >正文

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

重命名文件

在这里插入图片描述

实现思路

  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.xdnf.cn/news/9852.html

相关文章:

  • 基于分布式状态机的集装箱智能道口软件架构方法
  • 室内VR全景助力房产营销及装修
  • 机器学习与深度学习05-决策树01
  • 2022 RoboCom 世界机器人开发者大赛-本科组(省赛)解题报告 | 珂学家
  • Telerik生态整合:Kendo UI for Angular组件在WinForms应用中的深度嵌入(一)
  • 直线模组在手术机器人中有哪些技术挑战?
  • “百亿补贴”商家承担比例升至70%-80%,京东外卖家也没“余粮”了?
  • 基于定制开发开源AI智能名片S2B2C商城小程序的大零售渗透策略研究
  • 代码随想录算法训练营 Day60 图论Ⅹ Bellmen_ford 系列算法
  • Visual Studio中的宏变量
  • (ICML-2025) RIFLEx:视频扩散Transformer中长度外推的“免费午餐”
  • NVIDIA英伟达AI图片视频内容描述总结软件describe-anything整合包
  • 十二、FTP服务器配置与应用
  • 【博客系统】博客系统第十一弹:从零开始在 Linux 系统上搭建 Java 部署环境并部署 Web 项目
  • 扫地机产品异物进入吸尘口堵塞异常检测方案
  • 软考-系统架构设计师-第十六章 层次式架构设计理论与实践
  • Dif-Fusion:第一个基于扩散模型实现的红外光与可见光图像融合的论文
  • 【Linux系统移植】Cortex-A8 Linux系统移植(超详细)
  • [250529] CrateDB 5.10.7 发布:一系列重要修复与升级注意事项
  • 实战指南:步进电机规格书参数解析——以28BYJ48为例(不聊原理,只讲应用)
  • 【HarmonyOS 5】UIAbility上下文切换途中造成的Toast提示展示错窗口的解决方案
  • PyTorch中 torch.utils.data.DataLoader 的详细解析和读取点云数据示例
  • 机动车结构化检测算法AI智能分析网关V4打造全场景应用解决方案
  • 从数据持久化到网络通信与OpenCV:Qt应用程序开发的深度探索与实战
  • 从 API 调用到智能 Agent:面向未来产品的插件化 AI 中台设计
  • Android bindservice绑定服务,并同步返回service对象的两个方法
  • 易经六十四卦象解释数据集分享!智能体知识库收集~
  • PostgreSQL 修改表结构卡住不动
  • MySQL索引与事务
  • 华为防火墙NAPT配置