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

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.lryc.cn/news/2396421.html

相关文章:

  • 华为云Flexus+DeepSeek征文|华为云Flexus云服务器X实例上部署Dify:打造高效的开源大语言模型应用开发平台
  • [git每日一句]Your branch is up to date with ‘origin/master‘
  • 高密爆炸警钟长鸣:AI为化工安全戴上“智能护盾”
  • 机器人学基础——正运动学(理论推导及c++实现)
  • [网页五子棋][对战模块]处理连接成功,通知玩家就绪,逻辑问题(线程安全,先手判定错误)
  • TensorFlow Extended (TFX) 生产环境模型版本控制与回滚实战指南
  • 【Web应用】若依框架:基础篇11功能详解-系统接口
  • 【Docker项目实战篇】Docker部署PDF查看器PdfDing
  • Redis 常用数据类型和命令使用
  • 【Linux系统】第八节—进程概念(上)—冯诺依曼体系结构+操作系统+进程及进程状态+僵尸进程—详解!
  • WPF 全局加载界面、多界面实现渐变过渡效果
  • WebSocket与实时对话式AI服务的集成
  • 【xmb】】内部文档148344599
  • MobaXterm国内下载与安装使用教程
  • 数据结构——优先级队列(PriorityQueue)
  • 代谢组数据分析(二十六):LC-MS/MS代谢组学和脂质组学数据的分析流程
  • 服务器上用脚本跑python深度学习的注意事项(ubantu系统)
  • 【ARM】【FPGA】【硬件开发】Chapter.1 AXI4总线协议
  • 青少年编程与数学 02-020 C#程序设计基础 10课题、桌面应用开发
  • 把 jar 打包成 exe
  • 【目标检测】检测网络中neck的核心作用
  • 【经验】Ubuntu中设置terminator的滚动行数、从Virtualbox复制到Windows时每行后多一空行
  • 使用微软最近开源的WSL在Windows上优雅的运行Linux
  • HackMyVM-Teacher
  • BugKu Web渗透之矛盾
  • hot100 -- 4.子串系列
  • Python实现P-PSO优化算法优化卷积神经网络CNN回归模型项目实战
  • ssm 学习笔记day03
  • mkdir: cannot create directory ‘gitlab-stu’: No space left on device
  • 【前端面经】云智慧一面