From 8194048c07f913d9a16711fa090d3167ae45fd58 Mon Sep 17 00:00:00 2001 From: XiaoZuoOvO <2826618614@qq.com> Date: Mon, 3 Nov 2025 15:48:52 +0800 Subject: [PATCH 1/2] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E5=90=8D=E7=A7=B0=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BB=A5=E5=8E=BB=E9=99=A4=E5=A4=9A=E4=BD=99=E5=BC=95=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFont.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/hooks/useFont.ts b/src/hooks/useFont.ts index 25968f3..5ea7a2b 100644 --- a/src/hooks/useFont.ts +++ b/src/hooks/useFont.ts @@ -20,10 +20,13 @@ async function init() { try { const systemFonts = await window.electronAPI.getSystemFonts() // Font 对象数组 - fontList.value = systemFonts.map(fontName => ({ - label: fontName, - value: fontName, - })) + fontList.value = systemFonts.map((fontName) => { + const name = fontName.replace(/^['"]|['"]$/g, '') + return { + label: name, + value: name, + } + }) // 应用当前字体配置到 DOM const fontConfig = getConf('font').family From d223c493259a29a6f189a40a061b47be418afd58 Mon Sep 17 00:00:00 2001 From: XiaoZuoOvO <2826618614@qq.com> Date: Wed, 5 Nov 2025 15:36:01 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=96=87=E4=BB=B6=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=8F=98=E5=8C=96=E7=9B=91=E5=90=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lang/index.json | 120 ++++++++++++++++++ package.json | 1 + src/hooks/useContext.ts | 14 +- src/hooks/useSaveConfirmDialog.ts | 13 +- src/hooks/useTab.ts | 81 ++++++++++++ src/main/ipcBridge.ts | 55 +++++++- src/preload.ts | 2 + src/renderer/components/SaveConfirmDialog.vue | 25 +++- src/renderer/events/index.ts | 1 + src/renderer/global.d.ts | 2 + 10 files changed, 309 insertions(+), 5 deletions(-) diff --git a/lang/index.json b/lang/index.json index 6e9f094..fd0113b 100644 --- a/lang/index.json +++ b/lang/index.json @@ -1967,6 +1967,126 @@ "en": "", "fr": "" }, + "ccpga9z": { + "zh-cn": "文件已被外部修改,但您有未保存的更改。请先保存或丢弃更改后再重新加载。", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "d4k9sh4": { + "zh-cn": "文件 \"", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "t04it76": { + "zh-cn": "\" 已被删除", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "uje4506": { + "zh-cn": "监听文件 \"", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "egxfh7": { + "zh-cn": "\" 时出错: ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "diprg14": { + "zh-cn": "未知错误", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "6lbcrh7": { + "zh-cn": "文件已重新加载", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "r2ntmg7": { + "zh-cn": " 文件已更改 ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "i8xfh8c": { + "zh-cn": "\" 已被其他程序修改。 ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "cbnuzvd": { + "zh-cn": " 文件已被其他程序修改。 ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "ygx5m7r": { + "zh-cn": " 是否要重新加载文件内容?当前未保存的更改将会丢失。 ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "u5udsw6": { + "zh-cn": " 重新加载 ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "qzrwgd7": { + "zh-cn": " 文件已变动 ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "umfap0k": { + "zh-cn": "\" 已经变动,是否覆盖当前编辑的内容? ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, + "fdal1pl": { + "zh-cn": " 文件已经变动,是否覆盖当前编辑的内容? ", + "ja": "", + "ko": "", + "ru": "", + "en": "", + "fr": "" + }, "aroz724": { "zh-cn": "切换语言", "ja": "", diff --git a/package.json b/package.json index d26804a..314687a 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@vueuse/core": "^14.0.0", "autodialog.js": "^0.0.9", "autotoast.js": "^3.0.3", + "chokidar": "^4.0.3", "docx": "^9.5.1", "font-list": "^1.5.1", "he": "^1.2.0", diff --git a/src/hooks/useContext.ts b/src/hooks/useContext.ts index 0095f47..0bb06cb 100644 --- a/src/hooks/useContext.ts +++ b/src/hooks/useContext.ts @@ -15,11 +15,11 @@ import { useUpdateDialog } from './useUpdateDialog' export function useContext() { // 初始化所有hooks const { updateTitle } = useTitle() - const { markdown } = useContent() + const { markdown, originalContent } = useContent() const { currentTheme, init: initTheme } = useTheme() const { init: initFont, currentFont } = useFont() const { isShowSource } = useSourceCode() - const { isDialogVisible, dialogType, fileName, tabName, showDialog, showOverwriteDialog, handleSave, handleDiscard, handleCancel, handleOverwrite } = useSaveConfirmDialog() + const { isDialogVisible, dialogType, fileName, tabName, showDialog, showOverwriteDialog, showFileChangedDialog, handleSave, handleDiscard, handleCancel, handleOverwrite } = useSaveConfirmDialog() const { isDialogVisible: isUpdateDialogVisible, showDialog: showUpdateDialog, hideDialog: hideUpdateDialog, handleIgnore, handleLater, handleUpdate } = useUpdateDialog() const { onSave } = useFile() const { close, switchToTab, saveCurrentTab, activeTabId, getUnsavedTabs, shouldOffsetTabBar, currentTab } = useTab() @@ -49,6 +49,10 @@ export function useContext() { // 监听文件变化事件 emitter.on('file:Change', () => { + if (currentTab.value) { + markdown.value = currentTab.value.content + originalContent.value = currentTab.value.originalContent + } reBuildMilkdown() }) @@ -123,6 +127,12 @@ export function useContext() { data.resolver(result) }) + // 监听文件变动确认事件 + emitter.on('file:changed-confirm', async (data: { fileName: string, resolver: (choice: 'cancel' | 'overwrite') => void }) => { + const result = await showFileChangedDialog(data.fileName) + data.resolver(result) + }) + // 监听tab关闭确认事件 emitter.on('tab:close-confirm', async (data: { tabId: string, tabName: string, isLastTab?: boolean }) => { pendingCloseTab.value = data diff --git a/src/hooks/useSaveConfirmDialog.ts b/src/hooks/useSaveConfirmDialog.ts index 24daead..ad6306e 100644 --- a/src/hooks/useSaveConfirmDialog.ts +++ b/src/hooks/useSaveConfirmDialog.ts @@ -1,8 +1,9 @@ import { ref } from 'vue' -type DialogType = 'close' | 'overwrite' +type DialogType = 'close' | 'overwrite' | 'file-changed' type CloseChoice = 'save' | 'discard' | 'cancel' type OverwriteChoice = 'save' | 'overwrite' | 'cancel' +type FileChangedChoice = 'overwrite' | 'cancel' export function useSaveConfirmDialog() { const isDialogVisible = ref(false) @@ -29,6 +30,15 @@ export function useSaveConfirmDialog() { }) } + const showFileChangedDialog = (file: string): Promise => { + return new Promise((resolve) => { + dialogType.value = 'file-changed' + fileName.value = file + isDialogVisible.value = true + resolvePromise.value = resolve as (value: CloseChoice | OverwriteChoice | FileChangedChoice) => void + }) + } + const handleSave = () => { isDialogVisible.value = false if (resolvePromise.value) { @@ -68,6 +78,7 @@ export function useSaveConfirmDialog() { tabName, showDialog, showOverwriteDialog, + showFileChangedDialog, handleSave, handleDiscard, handleCancel, diff --git a/src/hooks/useTab.ts b/src/hooks/useTab.ts index 6b79ab5..a934c7b 100644 --- a/src/hooks/useTab.ts +++ b/src/hooks/useTab.ts @@ -426,6 +426,87 @@ const currentTab = computed(() => getCurrentTab()) // 是否偏移 const shouldOffsetTabBar = computed(() => isShowOutline.value) +// 添加新tab时不通知,只有当filePath实际变化时才通知 +watch( + () => tabs.value.map(tab => tab.filePath), + (newPaths, oldPaths) => { + // 获取所有真实的filePath + const newFilePaths = newPaths.filter(Boolean) as string[] + const oldFilePaths = oldPaths?.filter(Boolean) as string[] || [] + + // 判断是否有新的路径,包括首次执行时从空到有路径的情况,以及untitled标签被替换时监听不到的问题 + const hasNewPaths = newFilePaths.some(path => !oldFilePaths.includes(path)) + // 判断是否有删除的路径 + const hasRemovedPaths = oldFilePaths.some(path => !newFilePaths.includes(path)) + // 判断是否有路径变化,首次执行时 oldPaths 为 undefined,oldFilePaths 为 [],如果有新路径会被 hasNewPaths 捕获 + const hasPathChanges = hasNewPaths || hasRemovedPaths + + if (!hasPathChanges) + return + // 通知ipc + console.log('通知ipc', newFilePaths) + + window.electronAPI.watchFiles(newFilePaths) + }, + { + immediate: true, + }, +) + +// 文件变动回调事件 +window.electronAPI.on?.('file:changed', async (paths) => { + const tab = tabs.value.find(tab => tab.filePath === paths) + if (!tab) + return + + if (!tab.isModified) { + const result = await window.electronAPI.readFileByPath(paths) + if (!result) + return + // 处理图片路径 + const processedContent = await processImagePaths(result.content, result.filePath) + // 更新 + tab.content = processedContent + tab.originalContent = result.content + tab.isModified = false + + // 如果当前tab是活跃的,触发内容更新事件 + if (tab.id === activeTabId.value) { + emitter.emit('file:Change') + } + } else { + // 文件已变动,触发是否覆盖 + const fileName = getFileName(paths) + const choice = await new Promise<'overwrite' | 'cancel'>((resolve) => { + emitter.emit('file:changed-confirm', { + fileName, + resolver: resolve, + }) + }) + + if (choice === 'cancel') { + return + } + + // 读取新文件内容 + const result = await window.electronAPI.readFileByPath(paths) + if (!result) + return + + // 处理图片路径 + const processedContent = await processImagePaths(result.content, result.filePath) + // 更新 + tab.content = processedContent + tab.originalContent = result.content + tab.isModified = false + + // 触发内容更新 + if (tab.id === activeTabId.value) { + emitter.emit('file:Change') + } + } +}) + function useTab() { return { // 状态 diff --git a/src/main/ipcBridge.ts b/src/main/ipcBridge.ts index 86a72ea..f60ffd0 100644 --- a/src/main/ipcBridge.ts +++ b/src/main/ipcBridge.ts @@ -1,17 +1,23 @@ // ipcBridge.ts +import type { FSWatcher } from 'chokidar' import type { Block, ExportPDFOptions } from './types' import { execSync } from 'node:child_process' import * as fs from 'node:fs' import path from 'node:path' +import chokidar from 'chokidar' import { Document, HeadingLevel, Packer, Paragraph, TextRun } from 'docx' -import { app, clipboard, dialog, ipcMain, shell } from 'electron' +import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from 'electron' import { getFonts } from 'font-list' import { createThemeEditorWindow } from './index' let isSaved = true let isQuitting = false +// 存储已监听的文件路径和对应的 watcher +const watchedFiles = new Set() +let watcher: FSWatcher | null = null + // 所有 on 类型监听 export function registerIpcOnHandlers(win: Electron.BrowserWindow) { ipcMain.on('set-title', (_event, filePath: string | null) => { @@ -608,6 +614,53 @@ export function registerGlobalIpcHandlers() { return [] } }) + + // 监听文件变化 + ipcMain.on('file:watch', (_event, filePaths: string[]) => { + console.log('监听文件变化:', filePaths) + + // 先差异对比 + const newFiles = filePaths.filter(filePath => !watchedFiles.has(filePath)) + const removedFiles = Array.from(watchedFiles).filter(filePath => !filePaths.includes(filePath)) + + // 如果 watcher 不存在,创建它并设置事件监听 + if (!watcher) { + watcher = chokidar.watch([], { + ignored: (path, stats) => { + // 确保总是返回 boolean 类型 + if (!stats) + return false + return stats.isFile() && !path.endsWith('.md') + }, + persistent: true, + }) + + // 设置文件变化监听 + watcher.on('change', (filePath) => { + console.log('文件变化:', filePath) + console.log(123123123123123) + + const mainWindow = BrowserWindow.getAllWindows()[0] + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('file:changed', filePath) + } + }) + } + + // 新增的文件 - 添加到 watcher + if (newFiles.length > 0) { + watcher.add(newFiles) + newFiles.forEach(filePath => watchedFiles.add(filePath)) + console.log('➕:', newFiles) + } + + // 移除的文件 - 从 watcher 中移除 + if (removedFiles.length > 0) { + watcher.unwatch(removedFiles) + removedFiles.forEach(filePath => watchedFiles.delete(filePath)) + console.log('➖:', removedFiles) + } + }) } export function close(win: Electron.BrowserWindow) { // 防止重复调用 diff --git a/src/preload.ts b/src/preload.ts index 53f9f99..82f51ad 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -50,6 +50,8 @@ contextBridge.exposeInMainWorld('electronAPI', { getSystemFonts: () => ipcRenderer.invoke('get-system-fonts'), // 文件夹相关 getDirectoryFiles: (dirPath: string) => ipcRenderer.invoke('workspace:getDirectoryFiles', dirPath), + // 监听文件变化 + watchFiles: (filePaths: string[]) => ipcRenderer.send('file:watch', filePaths), // 主题编辑器相关 openThemeEditor: (theme?: any) => ipcRenderer.send('open-theme-editor', theme), diff --git a/src/renderer/components/SaveConfirmDialog.vue b/src/renderer/components/SaveConfirmDialog.vue index 2e0f400..30cef31 100644 --- a/src/renderer/components/SaveConfirmDialog.vue +++ b/src/renderer/components/SaveConfirmDialog.vue @@ -1,5 +1,5 @@