Skip to content

Commit 075400f

Browse files
committed
fix: 修复导出时总是命中第一个 tab 页的问题
1 parent 89cd4ef commit 075400f

6 files changed

Lines changed: 290 additions & 206 deletions

File tree

src/main/index.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import * as fs from "node:fs";
21
import * as path from "node:path";
32
import { app, BrowserWindow, globalShortcut, ipcMain, protocol, shell } from "electron";
4-
import { cleanupProtocolUrls, detectFileTraits, normalizeMarkdown } from "./fileFormat";
3+
import { isMarkdownFilePath, normalizeMarkdownFilePath, readMarkdownFile } from "./markdownFile";
54
import {
65
close,
76
getIsQuitting,
@@ -163,46 +162,36 @@ export async function createThemeEditorWindow() {
163162
* 整合了文件读取、验证和发送到渲染进程的逻辑
164163
*/
165164
function sendFileToRenderer(filePath: string) {
166-
// 验证文件路径
167-
if (!filePath.endsWith(".md") && !filePath.endsWith(".markdown")) {
165+
const result = readMarkdownFile(filePath);
166+
if (!result) {
167+
console.warn("[main] 无法读取启动文件:", filePath);
168168
return;
169169
}
170170

171-
// 检查文件是否存在
172-
if (!fs.existsSync(filePath)) {
173-
console.warn("[main] 文件不存在:", filePath);
174-
return;
175-
}
176-
177-
// 读取文件内容
178-
const raw = fs.readFileSync(filePath, "utf-8");
179-
const fileTraits = detectFileTraits(raw);
180-
const content = cleanupProtocolUrls(normalizeMarkdown(raw));
181-
182171
// 发送到渲染进程的函数
183172
const sendFile = () => {
184173
const targetWin = getAvailableWindow();
185174
if (targetWin) {
186175
targetWin.webContents.send("open-file-at-launch", {
187-
filePath,
188-
content,
189-
fileTraits,
176+
filePath: result.filePath,
177+
content: result.content,
178+
fileTraits: result.fileTraits,
190179
});
191180
}
192181
};
193182

194183
if (isRendererReady) {
195184
sendFile();
196185
} else {
197-
pendingStartupFile = filePath;
186+
pendingStartupFile = result.filePath;
198187
}
199188
}
200189

201190
function sendLaunchFileIfExists(argv = process.argv) {
202-
const fileArg = argv.find((arg) => arg.endsWith(".md") || arg.endsWith(".markdown"));
191+
const fileArg = argv.find((arg) => isMarkdownFilePath(arg));
203192

204193
if (fileArg) {
205-
const absolutePath = path.resolve(fileArg);
194+
const absolutePath = path.resolve(normalizeMarkdownFilePath(fileArg));
206195
sendFileToRenderer(absolutePath);
207196
}
208197
}

src/main/ipcBridge.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
restoreFileTraits,
1818
} from "./fileFormat";
1919
import { createThemeEditorWindow } from "./index";
20+
import { normalizeMarkdownFilePath, readMarkdownFile } from "./markdownFile";
2021
import {
2122
cancelDragFollow,
2223
clearWindowDragPreview,
@@ -365,11 +366,7 @@ export function registerIpcHandleHandlers() {
365366
properties: ["openFile"],
366367
});
367368
if (canceled) return null;
368-
const filePath = filePaths[0];
369-
const raw = fs.readFileSync(filePath, "utf-8");
370-
const fileTraits = detectFileTraits(raw);
371-
const content = cleanupProtocolUrls(normalizeMarkdown(raw));
372-
return { filePath, content, fileTraits };
369+
return filePaths[0] ? readMarkdownFile(filePaths[0]) : null;
373370
});
374371

375372
// 文件保存对话框
@@ -847,15 +844,8 @@ export function registerGlobalIpcHandlers() {
847844
// 通过文件路径读取 Markdown 文件(用于拖拽)
848845
ipcMain.handle("file:readByPath", async (_event, filePath: string) => {
849846
try {
850-
if (!filePath || !fs.existsSync(filePath)) return null;
851-
852-
const isMd = /\.(?:md|markdown)$/i.test(filePath);
853-
if (!isMd) return null;
854-
855-
const raw = fs.readFileSync(filePath, "utf-8");
856-
const fileTraits = detectFileTraits(raw);
857-
const content = cleanupProtocolUrls(normalizeMarkdown(raw), filePath);
858-
return { filePath, content, fileTraits };
847+
if (!filePath) return null;
848+
return readMarkdownFile(filePath);
859849
} catch (error) {
860850
console.error("Failed to read file:", error);
861851
return null;
@@ -1217,28 +1207,30 @@ export function isWindowClosing(winId: number): boolean {
12171207
return windowClosingSet.has(winId);
12181208
}
12191209
export function isFileReadOnly(filePath: string): boolean {
1210+
const normalizedPath = normalizeMarkdownFilePath(filePath);
1211+
12201212
// 先检测是否可写(跨平台)
12211213
try {
1222-
fs.accessSync(filePath, fs.constants.W_OK);
1214+
fs.accessSync(normalizedPath, fs.constants.W_OK);
12231215
} catch {
12241216
return true;
12251217
}
12261218

12271219
// 如果是 Windows,再额外检测 "R" 属性
12281220
if (process.platform === "win32") {
12291221
try {
1230-
const attrs = execSync(`attrib "${filePath}"`).toString();
1222+
const attrs = execSync(`attrib "${normalizedPath}"`).toString();
12311223
// attrs 输出格式类似于: "A R C:\path\to\file.md"
12321224
// 我们需要解析属性部分,忽略文件路径部分
12331225

12341226
// 1. 获取包含文件路径的那一行 (通常只有一行,但以防万一)
12351227
const lines = attrs.split("\r\n").filter((line) => line.trim());
1236-
const fileLine = lines.find((line) => line.trim().endsWith(filePath)) || lines[0];
1228+
const fileLine = lines.find((line) => line.trim().endsWith(normalizedPath)) || lines[0];
12371229

12381230
if (fileLine) {
12391231
// 2. 截取文件路径之前的部分作为属性区域
12401232
// 文件路径可能包含空格,所以不能简单 split
1241-
const lastIndex = fileLine.lastIndexOf(filePath);
1233+
const lastIndex = fileLine.lastIndexOf(normalizedPath);
12421234
if (lastIndex > -1) {
12431235
const attrPart = fileLine.substring(0, lastIndex);
12441236
// 3. 检查属性区域是否包含 'R'

src/main/markdownFile.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as fs from "node:fs";
2+
import path from "node:path";
3+
import { cleanupProtocolUrls, detectFileTraits, normalizeMarkdown } from "./fileFormat";
4+
5+
function stripWrappingQuotes(inputPath: string): string {
6+
if (
7+
(inputPath.startsWith('"') && inputPath.endsWith('"')) ||
8+
(inputPath.startsWith("'") && inputPath.endsWith("'"))
9+
) {
10+
return inputPath.slice(1, -1);
11+
}
12+
13+
return inputPath;
14+
}
15+
16+
function trimTrailingSeparators(inputPath: string): string {
17+
let normalizedPath = inputPath;
18+
19+
while (
20+
normalizedPath.length > 1 &&
21+
/[\\/]/.test(normalizedPath.at(-1) || "") &&
22+
path.dirname(normalizedPath) !== normalizedPath
23+
) {
24+
normalizedPath = normalizedPath.slice(0, -1);
25+
}
26+
27+
return normalizedPath;
28+
}
29+
30+
export function normalizeMarkdownFilePath(inputPath: string): string {
31+
return trimTrailingSeparators(stripWrappingQuotes(inputPath.trim()));
32+
}
33+
34+
export function isMarkdownFilePath(inputPath: string): boolean {
35+
const normalizedPath = normalizeMarkdownFilePath(inputPath);
36+
return /\.(?:md|markdown)$/i.test(path.basename(normalizedPath));
37+
}
38+
39+
export function readMarkdownFile(filePath: string) {
40+
const normalizedPath = normalizeMarkdownFilePath(filePath);
41+
42+
if (!normalizedPath || !isMarkdownFilePath(normalizedPath)) {
43+
return null;
44+
}
45+
46+
if (!fs.existsSync(normalizedPath)) {
47+
return null;
48+
}
49+
50+
let stats: fs.Stats;
51+
try {
52+
stats = fs.statSync(normalizedPath);
53+
} catch {
54+
return null;
55+
}
56+
57+
if (!stats.isFile()) {
58+
return null;
59+
}
60+
61+
const raw = fs.readFileSync(normalizedPath, "utf-8");
62+
const fileTraits = detectFileTraits(raw);
63+
const content = cleanupProtocolUrls(normalizeMarkdown(raw), normalizedPath);
64+
65+
return {
66+
filePath: normalizedPath,
67+
content,
68+
fileTraits,
69+
};
70+
}

src/renderer/components/editor/MilkupEditor.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,11 @@ defineExpose({
379379
</script>
380380

381381
<template>
382-
<div class="editor-box">
382+
<div
383+
class="editor-box milkup-editor-instance"
384+
:data-tab-id="tab.id"
385+
:data-active="isActive ? 'true' : 'false'"
386+
>
383387
<div ref="scrollViewRef" class="scrollView milkup" @scroll="updateScrollRatio">
384388
<div ref="containerRef" class="milkup-container"></div>
385389
</div>

src/renderer/components/settings/FileOptions.vue

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,68 @@
1-
<script setup lang='ts'>
2-
import autotoast from 'autotoast.js'
3-
import useContent from '@/renderer/hooks/useContent'
4-
import usefile from '@/renderer/hooks/useFile'
5-
import useWorkSpace from '@/renderer/hooks/useWorkSpace'
6-
import { exportElementAsPDF, exportMarkdownAsWord, exportElementWithStylesAndImages, exportAsText } from '@/renderer/utils/exports'
1+
<script setup lang="ts">
2+
import autotoast from "autotoast.js";
3+
import useContent from "@/renderer/hooks/useContent";
4+
import usefile from "@/renderer/hooks/useFile";
5+
import useWorkSpace from "@/renderer/hooks/useWorkSpace";
6+
import {
7+
exportElementAsPDF,
8+
exportMarkdownAsWord,
9+
exportElementWithStylesAndImages,
10+
exportAsText,
11+
getActiveEditorElement,
12+
getActiveEditorSelector,
13+
} from "@/renderer/utils/exports";
714
8-
const { onOpen, onSave, onSaveAs, currentTab } = usefile()
9-
const { setWorkSpace } = useWorkSpace()
10-
const { isModified, markdown } = useContent()
15+
const { onOpen, onSave, onSaveAs, currentTab } = usefile();
16+
const { setWorkSpace } = useWorkSpace();
17+
const { isModified, markdown } = useContent();
1118
1219
function onOpenFolder() {
13-
setWorkSpace().then(() => {
14-
const escEvent = new KeyboardEvent('keydown', { key: 'Escape' })
15-
document.dispatchEvent(escEvent)
16-
}).catch(() => {
17-
autotoast.show('取消选择')
18-
})
20+
setWorkSpace()
21+
.then(() => {
22+
const escEvent = new KeyboardEvent("keydown", { key: "Escape" });
23+
document.dispatchEvent(escEvent);
24+
})
25+
.catch(() => {
26+
autotoast.show("取消选择");
27+
});
1928
// 发射 Escape 按键事件 关闭菜单
2029
}
2130
function exportAsPDF() {
22-
exportElementAsPDF('.milkup-container', `${currentTab.value?.name.slice(0, -3)}.pdf` || '导出的文件', {
23-
pageSize: 'A4',
24-
scale: 1,
25-
}).then(() => {
26-
autotoast.show('导出成功', 'success')
27-
}).catch((err) => {
28-
autotoast.show(`导出失败: ${err.message}`, 'error')
29-
})
31+
exportElementAsPDF(
32+
getActiveEditorSelector(),
33+
`${currentTab.value?.name.slice(0, -3)}.pdf` || "导出的文件",
34+
{
35+
pageSize: "A4",
36+
scale: 1,
37+
}
38+
)
39+
.then(() => {
40+
autotoast.show("导出成功", "success");
41+
})
42+
.catch((err) => {
43+
autotoast.show(`导出失败: ${err.message}`, "error");
44+
});
3045
}
3146
function exportAsHTML() {
32-
exportElementWithStylesAndImages(document.querySelector('.milkup-container')!, `${currentTab.value?.name.slice(0, -3)}.html` || '导出的文件')
47+
exportElementWithStylesAndImages(
48+
getActiveEditorElement(),
49+
`${currentTab.value?.name.slice(0, -3)}.html` || "导出的文件"
50+
);
3351
}
3452
function exportAsDocx() {
35-
exportMarkdownAsWord(markdown.value, `${currentTab.value?.name.slice(0, -3)}.docx` || '导出的文件').then(() => {
36-
autotoast.show('导出成功', 'success')
37-
}).catch((err) => {
38-
autotoast.show(`导出失败: ${err.message}`, 'error')
39-
})
53+
exportMarkdownAsWord(
54+
markdown.value,
55+
`${currentTab.value?.name.slice(0, -3)}.docx` || "导出的文件"
56+
)
57+
.then(() => {
58+
autotoast.show("导出成功", "success");
59+
})
60+
.catch((err) => {
61+
autotoast.show(`导出失败: ${err.message}`, "error");
62+
});
4063
}
4164
function exportAsTxt() {
42-
exportAsText(markdown.value, `${currentTab.value?.name.slice(0, -3)}.txt` || '导出的文件')
65+
exportAsText(markdown.value, `${currentTab.value?.name.slice(0, -3)}.txt` || "导出的文件");
4366
}
4467
</script>
4568

@@ -59,7 +82,7 @@ function exportAsTxt() {
5982
<button @click="onSave">
6083
<span v-if="!isModified" class="iconfont icon-circle-check"></span>
6184
<span v-else class="iconfont icon-warning-outline"></span>
62-
<span>{{ isModified ? '保存' : '已保存' }}</span>
85+
<span>{{ isModified ? "保存" : "已保存" }}</span>
6386
</button>
6487
<button @click="onSaveAs">
6588
<span class="iconfont icon-document-copy"></span>
@@ -91,7 +114,7 @@ function exportAsTxt() {
91114
</div>
92115
</template>
93116

94-
<style lang='less' scoped>
117+
<style lang="less" scoped>
95118
.FileOptionsBox {
96119
width: 100%;
97120
height: 100%;

0 commit comments

Comments
 (0)