功能修复

This commit is contained in:
2026-07-07 17:34:48 +08:00
parent 0253831702
commit 13a0e56a38
5 changed files with 338 additions and 79 deletions

View File

@@ -30,5 +30,9 @@
"topic_20260706-142728_07221245f16f20ec": "auto",
"topic_20260706-145507_076a32df5f4276b5": "auto",
"topic_20260706-153317_238f65a7a470703c": "auto",
"topic_20260707-083706_2398eb8e87085000": "auto"
"topic_20260707-083706_2398eb8e87085000": "auto",
"topic_20260707-084048_75f72710c486f3de": "auto",
"topic_20260707-091519_dc062b2bfdf4249c": "auto",
"topic_20260707-092315_b8c3939bfc696802": "auto",
"topic_20260707-092711_af706c3fcc7e435e": "auto"
}

View File

@@ -30,5 +30,9 @@
"topic_20260706-142728_07221245f16f20ec": "已粘贴文本 #1 · 26 行] …",
"topic_20260706-145507_076a32df5f4276b5": "当用户选中了侧边栏中的文件的时候,这…",
"topic_20260706-153317_238f65a7a470703c": "文件被选中的高亮颜色不太搭配现在的主…",
"topic_20260707-083706_2398eb8e87085000": "这个页面渲染md的时候关于有序和无…"
"topic_20260707-083706_2398eb8e87085000": "这个页面渲染md的时候关于有序和无…",
"topic_20260707-084048_75f72710c486f3de": "在这个文件的区域的头部添加一行内容…",
"topic_20260707-091519_dc062b2bfdf4249c": "关于App.vue里的toolabr…",
"topic_20260707-092315_b8c3939bfc696802": "当我点击目录树切换显示文件的时候,右…",
"topic_20260707-092711_af706c3fcc7e435e": "帮我检查一下app.vue里的too…"
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import {ref, watch, onMounted, onUnmounted, computed} from "vue";
import {listen} from "@tauri-apps/api/event";
import {invoke} from "@tauri-apps/api/core";
import MarkdownEditor from "./components/MarkdownEditor.vue";
@@ -13,6 +13,11 @@ const charCount = ref(0);
const currentFilePath = ref<string | null>(null);
const saveStatus = ref("");
// Check if current file is a Markdown file (for display-mode toggle)
const isMarkdownFile = computed(() => {
return currentFilePath.value?.toLowerCase().endsWith(".md") ?? false;
});
// Folder state
interface DirEntry {
name: string;
@@ -20,6 +25,7 @@ interface DirEntry {
is_dir: boolean;
children?: DirEntry[];
}
const folderPath = ref("");
const dirEntries = ref<DirEntry[]>([]);
@@ -35,7 +41,8 @@ function loadZoomPref(): number {
const val = parseFloat(stored);
if (!isNaN(val) && val >= ZOOM_MIN && val <= ZOOM_MAX) return val;
}
} catch { /* localStorage unavailable */ }
} catch { /* localStorage unavailable */
}
return 1.0;
}
@@ -48,7 +55,10 @@ function applyZoomCssVars() {
}
function saveZoomPref() {
try { localStorage.setItem("editor-zoom", String(editorZoom.value)); } catch { /* noop */ }
try {
localStorage.setItem("editor-zoom", String(editorZoom.value));
} catch { /* noop */
}
}
function onEditorWheel(e: WheelEvent) {
@@ -63,20 +73,28 @@ function onEditorWheel(e: WheelEvent) {
// Show zoom indicator and reset timer
showZoomIndicator.value = true;
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
zoomIndicatorTimer = setTimeout(() => { showZoomIndicator.value = false; }, 1500);
zoomIndicatorTimer = setTimeout(() => {
showZoomIndicator.value = false;
}, 1500);
}
// ---- full-width mode ----
function loadFullWidthPref(): boolean {
try {
return localStorage.getItem("editor-full-width") === "true";
} catch { return false; }
} catch {
return false;
}
}
const isFullWidth = ref(loadFullWidthPref());
function onToggleFullWidth(val: boolean) {
isFullWidth.value = val;
try { localStorage.setItem("editor-full-width", String(val)); } catch { /* noop */ }
try {
localStorage.setItem("editor-full-width", String(val));
} catch { /* noop */
}
}
// ---- display mode (split / wysiwyg) ----
@@ -86,15 +104,58 @@ function loadDisplayModePref(): DisplayMode {
try {
const v = localStorage.getItem("editor-display-mode");
if (v === "split" || v === "wysiwyg") return v;
} catch { /* noop */ }
} catch { /* noop */
}
return "split";
}
const displayMode = ref<DisplayMode>(loadDisplayModePref());
function onToggleDisplayMode(val: DisplayMode) {
displayMode.value = val;
try { localStorage.setItem("editor-display-mode", val); } catch { /* noop */ }
try {
localStorage.setItem("editor-display-mode", val);
} catch { /* noop */
}
}
// ---- toolbar state ----
const showThumbnail = ref(true);
const previewOnly = ref(false);
const showMoreMenu = ref(false);
function toggleFullWidth() {
isFullWidth.value = !isFullWidth.value;
try {
localStorage.setItem("editor-full-width", String(isFullWidth.value));
} catch { /* noop */
}
}
function toggleDisplayMode() {
displayMode.value = displayMode.value === "split" ? "wysiwyg" : "split";
try {
localStorage.setItem("editor-display-mode", displayMode.value);
} catch { /* noop */
}
}
function handleExportPdf() {
showMoreMenu.value = false;
// TODO: implement PDF export
}
function closeMoreMenu() {
showMoreMenu.value = false;
}
watch(showMoreMenu, (val) => {
if (val) {
setTimeout(() => {
document.addEventListener("click", closeMoreMenu, {once: true});
}, 0);
}
});
onMounted(() => {
applyZoomCssVars();
@@ -223,7 +284,9 @@ async function doSave() {
try {
await invoke("write_file", {path: currentFilePath.value, content: content.value});
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
setTimeout(() => { saveStatus.value = ""; }, 3000);
setTimeout(() => {
saveStatus.value = "";
}, 3000);
} catch (e) {
console.error("Save failed:", e);
saveStatus.value = `保存失败: ${e}`;
@@ -237,7 +300,9 @@ async function doSaveAs() {
currentFilePath.value = chosen;
await invoke("write_file", {path: currentFilePath.value, content: content.value});
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
setTimeout(() => { saveStatus.value = ""; }, 3000);
setTimeout(() => {
saveStatus.value = "";
}, 3000);
} catch (e) {
console.error("Save failed:", e);
saveStatus.value = `保存失败: ${e}`;
@@ -284,6 +349,7 @@ listen("menu-event", (event) => {
:current-file-path="currentFilePath"
:is-full-width="isFullWidth"
:display-mode="displayMode"
:is-markdown-file="isMarkdownFile"
@heading-click="onHeadingClick"
@open-folder="onOpenFolder"
@toggle-folder="onToggleFolder"
@@ -294,10 +360,111 @@ listen("menu-event", (event) => {
<!-- Right: Editor area -->
<main class="flex-1 flex flex-col overflow-hidden relative">
<!-- Toolbar -->
<div
class="flex w-full items-center h-10 px-3 bg-[#faf9f6] border border-[#e0dbcf] border-b-0 select-none shrink-0"
>
<!-- Left: Thumbnail toggle -->
<button
class="toolbar-btn"
:class="{ 'toolbar-btn-active': showThumbnail }"
title="切换缩略图"
@click="showThumbnail = !showThumbnail"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="1" y="1" width="6" height="6" rx="1"/>
<rect x="9" y="1" width="6" height="6" rx="1"/>
<rect x="1" y="9" width="6" height="6" rx="1"/>
<rect x="9" y="9" width="6" height="6" rx="1"/>
</svg>
</button>
<!-- Center: Filename -->
<div class="flex-1 text-center text-sm text-[#8c877d] font-medium truncate px-3 select-none">
{{ currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
</div>
<!-- Right: Actions -->
<div class="flex items-center gap-0.5">
<!-- Display mode toggle (only for .md files) -->
<button
v-if="isMarkdownFile"
class="toolbar-btn"
:title="displayMode === 'wysiwyg' ? '切换到双栏' : '切换到单栏'"
@click="toggleDisplayMode"
>
<!-- Split icon (shown when single, meaning click to switch to split) -->
<svg v-if="displayMode === 'wysiwyg'" width="16" height="16" viewBox="0 0 16 16" fill="none"
stroke="currentColor" stroke-width="1.5">
<rect x="1" y="2" width="6" height="12" rx="1"/>
<rect x="9" y="2" width="6" height="12" rx="1"/>
</svg>
<!-- Single icon -->
<svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor"
stroke-width="1.5">
<rect x="3" y="2" width="10" height="12" rx="1"/>
</svg>
</button>
<!-- Preview/Edit toggle -->
<button
class="toolbar-btn"
:class="{ 'toolbar-btn-active': !previewOnly }"
:title="previewOnly ? '切换到编辑模式' : '切换到预览模式'"
@click="previewOnly = !previewOnly"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z"/>
</svg>
</button>
<!-- Full-width toggle -->
<button
class="toolbar-btn"
:class="{ 'toolbar-btn-active': isFullWidth }"
title="切换全宽"
@click="toggleFullWidth"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M10 1h5v5M6 15H1v-5M15 1l-5 5M1 15l5-5"/>
</svg>
</button>
<!-- More menu -->
<div class="relative">
<button
class="toolbar-btn"
title="更多"
@click="showMoreMenu = !showMoreMenu"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<circle cx="3" cy="8" r="1.5"/>
<circle cx="8" cy="8" r="1.5"/>
<circle cx="13" cy="8" r="1.5"/>
</svg>
</button>
<div
v-if="showMoreMenu"
class="dropdown-menu"
@click.stop
>
<button class="dropdown-item" @click="handleExportPdf">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M5 1h6l4 4v9a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1z"/>
<path d="M5 1v4h4"/>
<path d="M5 9h6M5 12h4"/>
</svg>
导出 PDF
</button>
</div>
</div>
</div>
</div>
<MarkdownEditor
:model-value="content"
:is-full-width="isFullWidth"
:display-mode="displayMode"
:preview-only="previewOnly"
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"
@update:model-value="onUpdate"
@save-shortcut="doSave"
@@ -344,10 +511,71 @@ listen("menu-event", (event) => {
.zoom-fade-leave-active {
transition: opacity 0.2s ease;
}
.zoom-fade-enter-from,
.zoom-fade-leave-to {
opacity: 0;
}
/* ── Toolbar ───────────────────────────────────────────────────── */
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
color: #8c877d;
background: transparent;
border: none;
cursor: pointer;
transition: background-color 0.15s, color 0.15s;
flex-shrink: 0;
}
.toolbar-btn:hover {
background-color: #ede8dd;
color: #38342e;
}
.toolbar-btn-active {
color: #bf6a3b;
background-color: #f4f1ea;
}
.dropdown-menu {
position: absolute;
right: 0;
top: 100%;
margin-top: 4px;
background: white;
border: 1px solid #e0dbcf;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
min-width: 140px;
padding: 4px;
z-index: 50;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
color: #38342e;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
}
.dropdown-item:hover {
background-color: #f4f1ea;
}
</style>

View File

@@ -17,12 +17,14 @@ const props = withDefaults(
currentFilePath?: string | null;
isFullWidth?: boolean;
displayMode?: "split" | "wysiwyg";
isMarkdownFile?: boolean;
}>(),
{
content: "",
currentFilePath: null,
isFullWidth: false,
displayMode: "split",
isMarkdownFile: false,
},
);
@@ -252,8 +254,8 @@ const showSettings = ref(false);
</button>
</div>
<div class="px-4 py-6 space-y-5">
<!-- Display mode toggle -->
<div class="flex items-center justify-between">
<!-- Display mode toggle (only for .md files) -->
<div v-if="props.isMarkdownFile" class="flex items-center justify-between">
<div class="flex-1 mr-3">
<p class="text-sm text-[#38342e] font-medium">编辑模式</p>
<p class="text-xs text-[#8c877d] mt-0.5 leading-relaxed">

View File

@@ -13,12 +13,14 @@ const props = withDefaults(
placeholder?: string;
isFullWidth?: boolean;
displayMode?: "split" | "wysiwyg";
previewOnly?: boolean;
}>(),
{
modelValue: "",
placeholder: "开始写作...",
isFullWidth: false,
displayMode: "split",
previewOnly: false,
},
);
@@ -62,6 +64,11 @@ watch(
() => props.modelValue,
(val) => {
if (!isSingle.value) renderFullHtml(val);
// 切换文件时重置滚动位置到顶部
nextTick(() => {
if (previewRef.value) previewRef.value.scrollTop = 0;
if (splitTextareaRef.value) splitTextareaRef.value.scrollTop = 0;
});
},
{ immediate: true },
);
@@ -126,11 +133,17 @@ function escapeHtml(s: string): string {
.replace(/>/g, "&gt;");
}
const singlePaneRef = ref<HTMLDivElement>();
// Watch modelValue → re-parse blocks in single mode
watch(
() => props.modelValue,
(val) => {
if (isSingle.value) parseBlocks(val);
// 切换文件时重置滚动位置到顶部
nextTick(() => {
if (singlePaneRef.value) singlePaneRef.value.scrollTop = 0;
});
},
{ immediate: true },
);
@@ -318,6 +331,7 @@ function onSplitScroll() {
<!-- Single-pane: block editing -->
<div
v-if="isSingle"
ref="singlePaneRef"
class="flex-1 overflow-auto p-8 pb-40"
:style="{ fontSize: `calc(1rem * var(--editor-zoom, 1))` }"
>
@@ -351,27 +365,33 @@ function onSplitScroll() {
<!-- Rendered state (clickable) -->
<div
v-else
class="
markdown-preview block-render
cursor-text rounded
-mx-1 px-1 py-px
hover:bg-[#faf9f6] transition-colors
"
:class="[
'markdown-preview block-render py-px',
previewOnly ? '' : 'cursor-text rounded -mx-1 px-1 hover:bg-[#faf9f6] transition-colors',
]"
v-html="block.html"
@click="startEdit(block.id)"
@click="!previewOnly && startEdit(block.id)"
/>
</template>
</div>
<!-- Split-pane mode -->
<template v-else>
<div class="flex-1 min-w-0 border-r border-[#e0dbcf] flex flex-col">
<!-- Textarea column: smoothly collapses width on preview-only -->
<div
:class="[
'flex flex-col min-h-0 transition-[width,opacity] duration-300 ease-in-out',
previewOnly
? 'w-0 overflow-hidden opacity-0 border-r-0'
: 'flex-1 min-w-0 border-r border-[#e0dbcf] opacity-100',
]"
>
<textarea
ref="splitTextareaRef"
:value="props.modelValue"
:placeholder="props.placeholder"
class="
flex-1 w-full resize-none bg-white p-8 pb-40 outline-none
flex-1 min-h-0 w-full resize-none bg-white p-8 pb-40 outline-none
font-serif text-[#38342e] leading-relaxed
placeholder:text-[#b8b3a8]
"
@@ -382,9 +402,10 @@ function onSplitScroll() {
/>
</div>
<!-- Preview column: smoothly expands to fill available space -->
<div
ref="previewRef"
class="flex-1 min-w-0 overflow-auto p-8 pb-40"
class="flex-1 min-w-0 min-h-0 overflow-auto p-8 pb-40 transition-[width] duration-300 ease-in-out"
:style="{ fontSize: `calc(1rem * var(--editor-zoom, 1))` }"
>
<div v-if="renderError" class="text-red-500 text-sm">