Electron-vite【实战】MD 编辑器 -- 编辑区(含工具条、自定义右键快捷菜单、快捷键编辑、拖拽打开文件等)

最终效果

在这里插入图片描述

页面

src/renderer/src/App.vue

    <div class="editorPanel"><div class="btnBox"><divv-for="(config, key) in actionDic":key="key"class="btnItem":title="config.label"@click="config.action"><Icon :icon="config.icon" /></div></div><textarearef="editorRef"v-model="markdownContent"spellcheck="false"class="editor":class="{ dragging: isDragging }"placeholder="请输入内容 ( Markdown语法 ) ..."@keydown="handleKeyDown"@contextmenu.prevent="show_edite_contextMenu"@dragover="handleDragOver"@dragleave="handleDragLeave"@drop="handleDrop"></textarea></div>

相关样式

.editorPanel {flex: 1;border: 1px solid gray;border-left: none;display: flex;flex-direction: column;width: 620px;
}
.editor {padding: 10px;border: none;box-sizing: border-box;flex: 1;word-break: break-all;resize: none;font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-size: 14px;line-height: 1.5;outline: none;
}
/* 滚动条样式优化 */
.editor,
.preview,
.outlineListBox {scrollbar-width: thin;scrollbar-color: #c1c1c1 #ffffff;scrollbar-gutter: stable;
}
.btnBox {display: flex;justify-content: space-evenly;background-color: #f0f0f0;height: 34px;
}
.btnItem {cursor: pointer;background-color: #f0f0f0;padding: 6px;font-size: 20px;display: inline-block;
}
.btnItem:hover {background-color: #e0e0e0;
}

相关依赖

实现图标

npm i --save-dev @iconify/vue

导入使用

import { Icon } from '@iconify/vue'

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

相关变量

const editorRef = ref<HTMLTextAreaElement>()
const markdownContent = ref('')

工具条

所有的功能都是插入 markdown 语法

在这里插入图片描述

      <div class="btnBox"><divv-for="(config, key) in actionDic":key="key"class="btnItem":title="config.label"@click="config.action"><Icon :icon="config.icon" /></div></div>
// 操作字典
const actionDic: {[key: string]: {icon: stringlabel: stringcontextMenu?: booleanaction: () => void}
} = {h1: {icon: 'codex:h1',label: '一级标题 Ctrl+1',action: () => {addTag('#')}},h2: {icon: 'codex:h2',label: '二级标题 Ctrl+2',action: () => {addTag('##')}},h3: {icon: 'codex:h3',label: '三级标题 Ctrl+3',action: () => {addTag('###')}},h4: {icon: 'codex:h4',label: '四级标题 Ctrl+4',action: () => {addTag('####')}},h5: {icon: 'codex:h5',label: '五级标题 Ctrl+5',action: () => {addTag('#####')}},h6: {icon: 'codex:h6',label: '六级标题 Ctrl+6',action: () => {addTag('######')}},p: {icon: 'codex:text',label: '正文 Ctrl+0',action: () => {setParagraph()}},code: {icon: 'codex:brackets',label: '代码块 Ctrl+Shift+K',contextMenu: true,action: () => {insertCode()}},link: {icon: 'codex:link',label: '超链接 Ctrl+L',contextMenu: true,action: () => {inserthyperlink()}},quote: {icon: 'codex:quote',label: '引用 Ctrl+Q',contextMenu: true,action: () => {addTag('>')}},b: {icon: 'codex:bold',label: '加粗 Ctrl+B',action: () => {addStyle('bold')}},i: {icon: 'codex:italic',label: '斜体 Ctrl+I',action: () => {addStyle('italic')}},d: {icon: 'codex:strikethrough',label: '删除线  Ctrl+D',action: () => {addStyle('delLine')}},ul: {icon: 'codex:list-bulleted',label: '无序列表 Ctrl+Shift+U',action: () => {addTag('-')}},ol: {icon: 'codex:list-numbered',label: '有序列表 Ctrl+Shift+O',action: () => {addTag('1.')}},todo: {icon: 'codex:checklist',label: '待办列表 Ctrl+Shift+D',action: () => {addTag('- [ ]')}},table: {icon: 'codex:table-with-headings',label: '表格 Ctrl+Shift+T',action: () => {insertTable()}},img: {icon: 'codex:picture',label: '图片 Ctrl+Shift+I',action: () => {insertImg()}},video: {icon: 'codex:play',label: '视频 Ctrl+Shift+V',action: () => {insertVideo()}}
}

公共方法

// 根据新内容,重新渲染页面,并恢复光标位置和滚动位置
const restoreCursorAndScroll = async (newContent: string,newCursorPosition: number
): Promise<void> => {// 更新文本内容markdownContent.value = newContentif (!editorRef.value) returnconst textarea = editorRef.value// 记录当前编辑区的滚动位置const originalScrollTop = textarea.scrollTop// 等待 DOM 更新完成await nextTick()// 重新聚焦到 textareatextarea.focus()textarea.setSelectionRange(newCursorPosition, newCursorPosition)// 恢复编辑区的滚动位置textarea.scrollTop = originalScrollTop
}

标题

在这里插入图片描述
以一级标题为例

addTag('#')
// 光标所在行前添加标记
const addTag = async (type: string): Promise<void> => {if (!editorRef.value) returnconst content = markdownContent.valueconst selectionStart = editorRef.value.selectionStart// 找到光标所在行的起始和结束位置let lineStart = content.lastIndexOf('\n', selectionStart - 1) + 1let lineEnd = content.indexOf('\n', selectionStart)if (lineEnd === -1) {lineEnd = content.length}// 获取当前行的文本let lineText = content.slice(lineStart, lineEnd)// 移除行首原有标记lineText = lineText.replace(/^[#>-]+\s*/, '')// 添加新的标记lineText = `${type} ${lineText.trimStart()}`// 构造新的内容const newContent = content.slice(0, lineStart) + lineText + content.slice(lineEnd)// 设置新的光标位置const newCursorPosition = lineStart + lineText.lengthrestoreCursorAndScroll(newContent, newCursorPosition)
}

段落

在这里插入图片描述

setParagraph()
// 将光标所在行的文本设置为段落
const setParagraph = async (): Promise<void> => {if (!editorRef.value) returnconst content = markdownContent.valueconst selectionStart = editorRef.value.selectionStart// 找到光标所在行的起始和结束位置let lineStart = content.lastIndexOf('\n', selectionStart - 1) + 1let lineEnd = content.indexOf('\n', selectionStart)if (lineEnd === -1) {lineEnd = content.length}// 获取当前行的文本let lineText = content.slice(lineStart, lineEnd)// 移除行首的标题和引用标记(#>)lineText = lineText.replace(/^[#>]+\s*/, '')// 构造新的内容const newContent = content.slice(0, lineStart) + lineText + content.slice(lineEnd)// 设置新的光标位置const newCursorPosition = lineStart + lineText.lengthrestoreCursorAndScroll(newContent, newCursorPosition)
}

代码块

在这里插入图片描述

insertCode()
// 插入代码块
const insertCode = async (): Promise<void> => {if (!editorRef.value) returnconst start = editorRef.value.selectionStartconst end = editorRef.value.selectionEndconst content = markdownContent.valueconst selectedText = content.slice(start, end)const newContent = `${content.slice(0, start)}\n${'```js'}\n${selectedText}\n${'```'}\n${content.slice(end)}`const newCursorPosition = start + 7 + selectedText.lengthrestoreCursorAndScroll(newContent, newCursorPosition)
}

超链接

在这里插入图片描述

inserthyperlink()
// 在光标所在的位置插入超链接
const inserthyperlink = async (): Promise<void> => {if (!editorRef.value) returnconst textarea = editorRef.valueconst content = markdownContent.valueconst start = textarea.selectionStartconst end = textarea.selectionEnd// 获取选中的文本,若未选中则默认显示 '链接文本'const selectedText = content.slice(start, end) || '链接文本'// 构造超链接的 Markdown 语法const hyperlink = `[${selectedText}]()`// 构造新的内容const newContent = `${content.slice(0, start)}${hyperlink}${content.slice(end)}`// 设置新的光标位置const newCursorPosition = start + hyperlink.length - 1restoreCursorAndScroll(newContent, newCursorPosition)
}

引用

在这里插入图片描述

addTag('>')
// 光标所在行前添加标记
const addTag = async (type: string): Promise<void> => {if (!editorRef.value) returnconst content = markdownContent.valueconst selectionStart = editorRef.value.selectionStart// 找到光标所在行的起始和结束位置let lineStart = content.lastIndexOf('\n', selectionStart - 1) + 1let lineEnd = content.indexOf('\n', selectionStart)if (lineEnd === -1) {lineEnd = content.length}// 获取当前行的文本let lineText = content.slice(lineStart, lineEnd)// 移除行首原有标记lineText = lineText.replace(/^[#>-]+\s*/, '')// 添加新的标记lineText = `${type} ${lineText.trimStart()}`// 构造新的内容const newContent = content.slice(0, lineStart) + lineText + content.slice(lineEnd)// 设置新的光标位置const newCursorPosition = lineStart + lineText.lengthrestoreCursorAndScroll(newContent, newCursorPosition)
}

加粗,斜体,删除线

在这里插入图片描述

addStyle('bold')
addStyle('italic')
addStyle('delLine')
// 给所选内容添加样式
const addStyle = async (type: string): Promise<void> => {if (!editorRef.value) returnconst textarea = editorRef.valueconst content = markdownContent.valueconst start = textarea.selectionStartconst end = textarea.selectionEnd// 获取选中的文本let selectedText = content.slice(start, end)let defaultText = ''let tag = ''switch (type) {case 'bold':defaultText = '加粗文本'tag = '**'breakcase 'italic':defaultText = '斜体文本'tag = '*'breakcase 'delLine':defaultText = '删除线文本'tag = '~~'breakdefault:}if (!selectedText) {selectedText = defaultText}const newText = `${tag}${selectedText}${tag}`// 构造新的内容const newContent = `${content.slice(0, start)}${newText}${content.slice(end)}`// 设置新的光标位置const newCursorPosition = start + newText.lengthrestoreCursorAndScroll(newContent, newCursorPosition)
}

无序列表,有序列表,待办列表

在这里插入图片描述

addTag('-')
addTag('1.')
addTag('- [ ]')
// 光标所在行前添加标记
const addTag = async (type: string): Promise<void> => {if (!editorRef.value) returnconst content = markdownContent.valueconst selectionStart = editorRef.value.selectionStart// 找到光标所在行的起始和结束位置let lineStart = content.lastIndexOf('\n', selectionStart - 1) + 1let lineEnd = content.indexOf('\n', selectionStart)if (lineEnd === -1) {lineEnd = content.length}// 获取当前行的文本let lineText = content.slice(lineStart, lineEnd)// 移除行首原有标记lineText = lineText.replace(/^[#>-]+\s*/, '')// 添加新的标记lineText = `${type} ${lineText.trimStart()}`// 构造新的内容const newContent = content.slice(0, lineStart) + lineText + content.slice(lineEnd)// 设置新的光标位置const newCursorPosition = lineStart + lineText.lengthrestoreCursorAndScroll(newContent, newCursorPosition)
}

表格

在这里插入图片描述

insertTable()
// 插入表格
const insertTable = (): void => {const table = '|  |  |\n|:----:|:----:|\n|  |  |\n|  |  |\n'const editor = editorRef.valueif (editor) {const start = editor.selectionStartconst end = editor.selectionEndconst before = markdownContent.value.slice(0, start)const after = markdownContent.value.slice(end)restoreCursorAndScroll(before + table + after, start + 2)}
}

图片

在这里插入图片描述

insertImg()
// 在光标所在的位置插入图片
const insertImg = async (): Promise<void> => {if (!editorRef.value) returnconst textarea = editorRef.valueconst content = markdownContent.valueconst start = textarea.selectionStartconst end = textarea.selectionEndlet selectedText = content.slice(start, end) || '图片'// 构造图片的 Markdown 语法const hyperlink = `![${selectedText}]()`// 构造新的内容const newContent = `${content.slice(0, start)}${hyperlink}${content.slice(end)}`// 设置新的光标位置const newCursorPosition = start + hyperlink.length - 1restoreCursorAndScroll(newContent, newCursorPosition)
}

视频

在这里插入图片描述

insertVideo()
// 在光标所在的位置插入视频
const insertVideo = async (): Promise<void> => {if (!editorRef.value) returnconst textarea = editorRef.valueconst content = markdownContent.valueconst start = textarea.selectionStartconst end = textarea.selectionEnd// Markdown 语法无视频,可用html实现const hyperlink = `<video src="" controls width="100%">请升级浏览器以观看视频。
</video>`// 构造新的内容const newContent = `${content.slice(0, start)}${hyperlink}${content.slice(end)}`// 设置新的光标位置const newCursorPosition = start + 12restoreCursorAndScroll(newContent, newCursorPosition)
}

右键快捷菜单

在这里插入图片描述

@contextmenu.prevent="show_edite_contextMenu"
// 显示右键菜单 -- 编辑器
const show_edite_contextMenu = (event): void => {// 阻止默认右键菜单event.preventDefault()// 获取鼠标位置menuX.value = event.clientXmenuY.value = event.clientY// 显示菜单isMenuVisible.value = true
}

构建页面

    <!-- 右键快捷菜单--编辑器 --><divv-if="isMenuVisible"class="context-menu":style="{ left: `${menuX}px`, top: `${menuY}px` }"><div class="context-menu-btnBox"><div class="context-menu-btn" @click="copySelectedText"><Icon icon="codex:copy" width="24" style="margin-right: 4px" /><span>复制</span></div><div class="context-menu-btn" @click="paste"><Icon icon="mingcute:paste-line" width="20" style="margin-right: 4px" /><span>粘贴</span></div><div class="context-menu-btn" @click="cutSelectedText"><Icon icon="tabler:cut" width="20" style="margin-right: 4px" /><span>剪切</span></div></div><div class="context-menu-btnBox"><div class="context-menu-btn" @click="actionDic.h1.action"><Icon :icon="actionDic.h1.icon" width="20" /></div><div class="context-menu-btn" @click="actionDic.h2.action"><Icon :icon="actionDic.h2.icon" width="20" /></div><div class="context-menu-btn" @click="actionDic.h3.action"><Icon :icon="actionDic.h3.icon" width="20" /></div><div class="context-menu-btn" @click="actionDic.p.action"><Icon :icon="actionDic.p.icon" width="20" /></div><div class="context-menu-btn" @click="actionDic.b.action"><Icon :icon="actionDic.b.icon" width="20" /></div><div class="context-menu-btn" @click="actionDic.d.action"><Icon :icon="actionDic.d.icon" width="20" /></div></div><ul><template v-for="item in actionDic"><li v-if="item.contextMenu" :key="item.label" @click="item.action"><Icon :icon="item.icon" width="20" style="margin-right: 10px" /><span> {{ item.label }}</span></li></template></ul><div class="context-menu-btnBox"><div class="context-menu-btn" @click="actionDic.ul.action"><Icon :icon="actionDic.ul.icon" width="20" style="margin-right: 4px" /><span>无序</span></div><div class="context-menu-btn" @click="actionDic.ol.action"><Icon :icon="actionDic.ol.icon" width="20" style="margin-right: 4px" /><span>有序</span></div><div class="context-menu-btn" @click="actionDic.todo.action"><Icon :icon="actionDic.todo.icon" width="20" style="margin-right: 4px" /><span>待办</span></div></div><div class="context-menu-btnBox"><div class="context-menu-btn" @click="actionDic.table.action"><Icon :icon="actionDic.table.icon" width="20" style="margin-right: 4px" /><span>表格</span></div><div class="context-menu-btn" @click="actionDic.img.action"><Icon :icon="actionDic.img.icon" width="20" style="margin-right: 4px" /><span>图片</span></div><div class="context-menu-btn" @click="actionDic.video.action"><Icon :icon="actionDic.video.icon" width="20" style="margin-right: 4px" /><span>视频</span></div></div></div>

相关样式

/* 编辑器-右键菜单样式 */
.context-menu {position: fixed;z-index: 1000;width: 200px;background-color: white;border-radius: 4px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);padding: 4px 0;font-size: 12px;
}
.context-menu ul {list-style: none;margin: 0;padding: 0;
}
.context-menu li {padding: 8px 16px;cursor: pointer;display: flex;align-items: center;
}
.context-menu li:hover {background-color: #e0e0e0;
}
.context-menu li i {margin-right: 8px;text-align: center;
}
.context-menu-btnBox {display: flex;
}
.context-menu-btn {flex: 1;padding: 8px 4px;cursor: pointer;text-align: center;display: flex;align-items: center;justify-content: center;
}
.context-menu-btn:hover {background-color: #e0e0e0;
}

复制

const copySelectedText = (): void => {if (!editorRef.value) returnconst start = editorRef.value.selectionStartconst end = editorRef.value.selectionEndconst content = markdownContent.valueconst selectedText = content.slice(start, end)navigator.clipboard.writeText(selectedText)
}

剪切

const cutSelectedText = (): void => {if (!editorRef.value) returnconst start = editorRef.value.selectionStartconst end = editorRef.value.selectionEndconst content = markdownContent.valueconst selectedText = content.slice(start, end)navigator.clipboard.writeText(selectedText)const newContent = content.slice(0, start) + content.slice(end)restoreCursorAndScroll(newContent, start)
}

粘贴

const paste = (): void => {if (!editorRef.value) returnconst start = editorRef.value.selectionStartconst end = editorRef.value.selectionEndconst content = markdownContent.valuenavigator.clipboard.readText().then((text) => {const newContent = content.slice(0, start) + text + content.slice(end)restoreCursorAndScroll(newContent, start + text.length)})
}

其他快捷编辑相关的方法同工具栏

隐藏编辑器右键菜单

按ESC/点击鼠标时

onMounted 中

  // 监听点击鼠标左键时隐藏编辑器右键菜单document.addEventListener('click', hide_editor_contextMenu)// 监听按下ESC键时隐藏编辑器右键菜单document.addEventListener('keydown', ESC_hide_editor_contextMenu)
// 按ESC时隐藏编辑器右键菜单
const ESC_hide_editor_contextMenu = ({ key }): void => {if (key === 'Escape') {hide_editor_contextMenu()}
}

onBeforeUnmount 中

document.removeEventListener('click', hide_editor_contextMenu)
document.removeEventListener('keydown', ESC_hide_editor_contextMenu)

显示其他快捷菜单时

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

快捷键编辑

除了响应工具栏的快捷键,还需支持按下回车键时

  • 若当前行为无序列表且有内容,则下一行继续无序列表,若无内容,则不再继续无序列表
  • 若当前行为有序列表且有内容,则下一行继续有序列表,且序号加 1,若无内容,则不再继续有序列表
  • 若当前行为待办列表且有内容,则下一行继续待办列表,若无内容,则不再继续待办列表
  • 若当前行为引用,则下一行继续为引用
@keydown="handleKeyDown"
// 编辑器按下键盘事件
const handleKeyDown = async (event: KeyboardEvent): Promise<void> => {// 同步预览滚动位置syncPreviewScroll()// 生成快捷键组合字符串const modifiers: string[] = []if (event.ctrlKey) modifiers.push('Ctrl')if (event.shiftKey) modifiers.push('Shift')if (event.altKey) modifiers.push('Alt')const key = event.key.toUpperCase()const shortcut = [...modifiers, key].join('+')// 检查是否有对应的快捷键处理函数if (shortcutMap[shortcut]) {event.preventDefault()await shortcutMap[shortcut]()return}if (event.key === 'Enter' && editorRef.value && !event.shiftKey) {const textarea = editorRef.valueconst content = markdownContent.valueconst cursorPosition = textarea.selectionStart// 找到当前行的起始位置const lineStart = content.lastIndexOf('\n', cursorPosition - 1) + 1const currentLine = content.slice(lineStart, cursorPosition)// 检查当前行是否为列表格式且不为空行const listMatch = currentLine.match(/^(\s*)([-*+]|\d+\.)\s/)const isLineEmpty =currentLine.trim().length === listMatch?.[0]?.trim().length || currentLine.trim() === ''// 检查当前行是否为引用格式const quoteMatch = currentLine.match(/^(\s*)>\s*/)// 检查当前行是否为待办列表格式const todoMatch = currentLine.match(/^(\s*)- \[[ xX]\]\s*/)const isTodoEmpty = currentLine.trim().length === todoMatch?.[0]?.trim().lengthif (listMatch && !isLineEmpty && !todoMatch) {event.preventDefault()const indentation = listMatch[1]const listMarker = listMatch[2]let newListMarker = listMarker// 若为有序列表,序号递增if (/^\d+\.$/.test(listMarker)) {const currentNumber = parseInt(listMarker.replace('.', ''), 10)newListMarker = `${currentNumber + 1}.`}const newContent = `${content.slice(0, cursorPosition)}\n${indentation}${newListMarker} ${content.slice(cursorPosition)}`// 设置新的光标位置const newCursorPosition = cursorPosition + indentation.length + newListMarker.length + 2textarea.setSelectionRange(newCursorPosition, newCursorPosition)restoreCursorAndScroll(newContent, newCursorPosition)} else if (quoteMatch) {event.preventDefault()const indentation = quoteMatch[1]const newContent =`${content.slice(0, cursorPosition)}\n${indentation}> ` + content.slice(cursorPosition)// 设置新的光标位置const newCursorPosition = cursorPosition + indentation.length + 3textarea.setSelectionRange(newCursorPosition, newCursorPosition)restoreCursorAndScroll(newContent, newCursorPosition)} else if (todoMatch && !isTodoEmpty) {event.preventDefault()const indentation = todoMatch[1]const newContent =`${content.slice(0, cursorPosition)}\n${indentation}- [ ] ` + content.slice(cursorPosition)// 设置新的光标位置const newCursorPosition = cursorPosition + indentation.length + 7textarea.setSelectionRange(newCursorPosition, newCursorPosition)restoreCursorAndScroll(newContent, newCursorPosition)}}
}

相关方法

// 同步预览区滚动
const syncPreviewScroll = (): void => {// 点击大纲项时,不触发同步预览区滚动if (editorRef.value && previewRef.value && !ifClickOutLine.value) {const editor = editorRef.valueconst preview = previewRef.valueconst editorScrollRatio = editor.scrollTop / (editor.scrollHeight - editor.clientHeight)const previewScrollTop = editorScrollRatio * (preview.scrollHeight - preview.clientHeight)preview.scrollTop = previewScrollTop}
}

相关变量

// 快捷键映射表
const shortcutMap = {'Ctrl+0': setParagraph,'Ctrl+1': () => addTag('#'),'Ctrl+2': () => addTag('##'),'Ctrl+3': () => addTag('###'),'Ctrl+4': () => addTag('####'),'Ctrl+5': () => addTag('#####'),'Ctrl+6': () => addTag('######'),'Ctrl+Shift+K': insertCode,'Ctrl+L': inserthyperlink,'Ctrl+Q': () => addTag('>'),'Ctrl+B': () => addStyle('bold'),'Ctrl+I': () => addStyle('italic'),'Ctrl+D': () => addStyle('delLine'),'Ctrl+Shift+U': () => addTag('-'),'Ctrl+Shift+O': () => addTag('1.'),'Ctrl+Shift+D': () => addTag('- [ ]'),'Ctrl+Shift+T': insertTable,'Ctrl+Shift+V': insertVideo,'Ctrl+Shift+I': insertImg
}

拖拽打开文件

在这里插入图片描述

响应拖拽

:class="{ dragging: isDragging }"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
// 文件拖拽到编辑器中时
const handleDragOver = (e: DragEvent): void => {e.preventDefault()if (e.dataTransfer) {e.dataTransfer.dropEffect = 'link'}dragMessage.value = '释放鼠标打开文件(仅支持 md 文件)'isDragging.value = true
}
// 拖拽文件离开编辑器后
const handleDragLeave = (e): void => {// 确保是真正离开容器而不是子元素if (e.relatedTarget === null || !e.currentTarget.contains(e.relatedTarget)) {isDragging.value = false}
}
// 拖拽文件到编辑器中松开鼠标后
const handleDrop = async (event: DragEvent): Promise<void> => {event.preventDefault()const files = event.dataTransfer?.files || []for (const file of files) {file.path = window.api.getDropFilePath(file)if (file.type === 'text/markdown' || file.name.endsWith('.md')) {try {const content = await file.text()markdownContent.value = contentcurrentFilePath.value = file.pathif (!isFileExists(file.path)) {fileList.value.unshift({content,fileName: file.name,filePath: file.path})}// 拖入文件后,立马打开文件openFile({content,fileName: file.name,filePath: file.path})} catch (error) {console.error('读取文件出错:', error)}}}isDragging.value = false
}

获取被拖拽文件的本地路径

通过预加载脚本实现

src/preload/index.ts

import { contextBridge, webUtils } from 'electron'
const api = {getDropFilePath: (file) => {return webUtils.getPathForFile(file)}
}

src/preload/index.d.ts

  // 定义 api 的类型interface ApiType {getDropFilePath: (item: File) => string}

拖拽提示层

    <!-- 拖拽提示层 --><div v-if="isDragging" class="drag-overlay"><div class="drag-message">{{ dragMessage }}</div></div>

相关样式

.editor.dragging {border-color: #2196f3;background-color: #f5f8ff;
}
.drag-overlay {position: absolute;top: 0;left: 0;width: 100vw;height: 100vh;background-color: rgba(33, 150, 243, 0.1);border: 1px dashed #2196f3;border-radius: 4px;display: flex;align-items: center;justify-content: center;pointer-events: none; /* 允许点击穿透到 textarea */box-sizing: border-box;
}
.drag-message {font-size: 16px;color: #2196f3;background-color: white;padding: 10px 20px;border-radius: 4px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

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

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

相关文章

没有宝塔面板的服务器上的WordPress网站打包下载到本地?

在服务器上部署的wordpress博客站&#xff0c;没有宝塔面板&#xff0c;怎么将服务器上的wordpress打包下载到本地&#xff1f; 作者: 晓北斗NorSnow 晓北斗动态视觉设计师&#xff0c;岚度视觉工作室执行人&#xff1b;主要从事展厅视频制作、图形工作站销售、AIGC研究&#…

Atcoder Beginner Contest 410 题解报告

零、前言 经过七七四十九天的分别&#xff0c;本期 ABC 题解又和大家见面啦&#xff01; 经过七周的奋勇杀题&#xff0c;我终于达成了三个小心愿&#xff1a; 不吃罚时AK上金排名 100 100 100 以内 且 Rated&#xff08;悲催的是&#xff0c;我 ABC400 排名两位数但没Rate…

pyspark非安装使用graphframes

pyspark版本3.1.3 需要文件 graphframes-0.8.2-spark3.1-s_2.12.jarspark-graphx_2.12-3.1.3.jar从 https://github.com/microsoft/adb2spark/raw/main/graphframes-0.8.2-py3-none-any.whl 下载graphframes-0.8.2-py3-none-any.whl。下载后把whl后缀改成zip&#xff0c;解压…

[Linux入门] Linux磁盘管理与文件系统

目录 Linux磁盘与文件系统管理详解&#xff1a;从基础到实践 ​​一、磁盘基础简述​​ 1️⃣​​硬盘类型​​&#xff1a; ​2️⃣机械硬盘结构​​&#xff1a; 3️⃣​​磁盘容量计算​​&#xff1a; 公式&#xff1a;磁盘容量磁头数柱面数每磁道扇区数每扇区字节数 …

【Flutter】性能优化总结

【Flutter】性能优化总结 Flutter 性能优化是提升应用流畅度、响应速度和用户体验的关键。可以从以下几个方面进行优化&#xff1a; 一、UI 构建与布局优化 1、避免不必要的重建 使用 const 构造函数&#xff1a;如 const Text(Hello)&#xff0c;可以减少 Widget 重建。使用…

5、ZYNQ PL 点灯--流水灯

目录 1、 概述 2 、硬件电路 3、 新建 VIVADO 工程 4、 添加工程文件 6、编写流水灯功能的Verilog代码 7 、添加管脚约束文件 8、 RTL 仿真 8.1 添加仿真测试源码 8.2 仿真结果 9、 编译并且产生 bit 文件 10、 下载程序 11、实验结果 ​编辑12、总结 1、 概述 本…

HTML5 浮动

1. 常见网页布局 1-3-1布局 1-2-1布局 2. 标准文档流 3. display属性⭐ display&#xff1a; block 给span元素设置成block display&#xff1a; inline 给div元素设置成inline display&#xff1a; inline-block 给div和span元素设置为inline-block display&#xff1a; no…

若依使用RedisCache需要注意的事项

存入redis对象的时候会带一个type字段&#xff0c;此处需要注意 存入方&#xff1a; 此处需要注意&#xff0c;存入redis的时候会带一个type&#xff0c;也就是类的路径名 redisCache.setCacheObject(screenPlayQueueName, userDemondDto,userDemondDto.getPlayDuration().in…

【STM32的通用定时器CR1的CKD[1:0]: 时钟分频因子 (Clock division)】

在 STM32 的通用定时器&#xff08;如 TIM2, TIM3, TIM4, TIM5 等&#xff09;中&#xff0c;CR1 (Control Register 1) 寄存器中的 CKD[1:0] (Clock division) 位域是一个与抗干扰和数字滤波相关的设置&#xff0c;它并不直接影响定时器计数器 (CNT) 的计数频率&#xff08;计…

渲染学进阶内容——机械动力的渲染系统(2)

Flywheel代码 这篇来研究一下实例 InstanceHandle 接口深度解析 接口核心作用 InstanceHandle 是 Flywheel 渲染引擎中的 GPU实例句柄 接口,它提供了对底层渲染实例的直接控制能力。这个接口是**实例化渲染(Instanced Rendering)**系统的核心操作接口,与之前讨论的 Vis…

Redis:极速缓存与数据结构存储揭秘

Redis —— 这个强大又灵活的 开源、内存中的数据结构存储系统。它常被用作数据库、缓存、消息代理和流处理引擎。 核心特点 (为什么它这么受欢迎&#xff1f;)&#xff1a; 内存存储 (In-Memory): 数据主要存储在 RAM 中&#xff0c;读写操作直接在内存中进行。核心优势&…

vulnyx Diff3r3ntS3c writeup

信息收集 arp-scan nmap 这里默认的话是只有80端口的&#xff0c;这个22端口是我拿到root后开的 获取userFlag 直接上web看看 扫个目录 把网页拉到最下面可以看到一个文件上传点 我们尝试上传一个php文件 失败了&#xff0c;那xxx呢 上传成功了&#xff0c;看来后端的后缀名…

【构建】CMake 构建系统重点内容

CMake 构建系统重点内容 1 基本语法与结构 cmake_minimum_required() 指定使用的最低 CMake 版本&#xff0c;防止不同版本行为不一致&#xff1a; cmake_minimum_required(VERSION 3.16)project() 定义项目名称、语言和版本&#xff1a; project(MyApp VERSION 1.0 LANGU…

Packagerun:VSCode 扩展 快捷执行命令

Packagerun&#xff1a;VSCode 快捷命令扩展&#xff08;兼容cursor&#xff09; Packagerun 是一个为 前端和node开发者设计的 VSCode 扩展&#xff0c;旨在简化 package.json 中脚本的执行&#xff0c;并支持自定义命令以提升开发效率。通过右键菜单、快捷键或自定义配置&am…

【C语言】计算机组成、计算机语言介绍

1.1 计算机组成 1946年2月14日&#xff0c;由美国军方定制的世界上第一台电子计算机“电子数字积分计算机”( ENIAC Electronic Numerical And Calculator)在美国宾夕法尼亚大学问世。 计算机(俗称电脑)堪称是人类智慧的结晶&#xff0c;随着计算机的不断发展&#xff0c;各行各…

(九)山东大学软件学院项目实训-基于大模型的模拟面试系统-面试对话标题自动总结

面试对话标题自动总结 主要实现思路&#xff1a;每当AI回复用户之后&#xff0c;调用方法查看当前对话是否大于三条&#xff0c;如果大于则将用户的两条和AI回复的一条对话传给DeepSeek让其进行总结&#xff08;后端&#xff09;&#xff0c;总结后调用updateChatTopic进行更新…

降阶法求解偏微分方程

求解给定的四个偏微分方程,采用降阶法,令 v = u x v = u_x v=ux​,从而将原方程转化为关于 v v v 的一阶方程。 方程 u x y = 0 u_{xy} = 0 uxy​=0 令 v = u x v = u_x v=ux​,则方程变为 v y = 0 v_y = 0 vy​=0。解得 v = C 1 ( x ) v = C_1(x) v=C1​(x),即 u …

提的缺陷开发不改,测试该怎么办?

经历长时间的细致检查&#xff0c;逐条执行数十条测试用例&#xff0c;终于发现一处疑似缺陷。截图留存、粘贴日志&#xff0c;认真整理好各项信息&#xff0c;将它提交到缺陷管理系统。可不到五分钟&#xff0c;这条缺陷就被打回了。开发人员给出的回复十分简洁&#xff1a;“…

【Flutter】Widget、Element和Render的关系-Flutter三棵树

【Flutter】Widget、Element和Render的关系-Flutter三棵树 一、前言 在 Flutter 中&#xff0c;所谓的“三棵树”是指&#xff1a; Widget Tree&#xff08;部件树&#xff09;Element Tree&#xff08;元素树&#xff09;Render Tree&#xff08;渲染树&#xff09; 它们是…

IO之详解cin(c++IO关键理解)

目录 cin原理介绍 控制符(hex、oct、dec) cin如何检查输入 cin与字符串 cin.get(char ch) cin.get(void) istream &get(char*,int) istream &get(char*,int,char) istream &getline(char*,int); 遇到文件结尾EOF 无法完成一次完整输入&#xff1a;设置f…