最終效果
頁面
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);
}