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
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/84879.shtml
英文地址,請注明出處:http://en.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…