功能完善

This commit is contained in:
2026-07-08 11:36:49 +08:00
parent 78352dd193
commit 519aa5aff0
2 changed files with 141 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import {ref, watch, onMounted, onUnmounted} from "vue";
import {computed, ref, watch, onMounted, onUnmounted} from "vue";
import {listen} from "@tauri-apps/api/event";
import {invoke} from "@tauri-apps/api/core";
import MarkdownEditor from "./components/MarkdownEditor.vue";
@@ -15,6 +15,7 @@ type DocumentMode = "preview" | "edit" | "mixed";
// File state
const currentFilePath = ref<string | null>(null);
const saveStatus = ref("");
const hasDraftDocument = ref(false);
// Folder state
interface DirEntry {
@@ -26,6 +27,8 @@ interface DirEntry {
const folderPath = ref("");
const dirEntries = ref<DirEntry[]>([]);
const LAST_WORKSPACE_PATH_KEY = "yurou:last-workspace-path";
const editorResetKey = ref(0);
// ---- editor zoom (Ctrl+scroll) ----
const ZOOM_MIN = 0.7;
@@ -81,6 +84,9 @@ const showOutline = ref(false);
const documentMode = ref<DocumentMode>("edit");
const showMoreMenu = ref(false);
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
const showStartActions = computed(() => {
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
});
function handleExportPdf() {
showMoreMenu.value = false;
@@ -102,6 +108,7 @@ watch(showMoreMenu, (val) => {
onMounted(() => {
applyZoomCssVars();
window.addEventListener("wheel", onEditorWheel, {passive: false});
void restoreLastWorkspace();
});
onUnmounted(() => {
@@ -135,11 +142,31 @@ function stripMarkdown(md: string): string {
function onUpdate(md: string) {
content.value = md;
updateDocumentMetrics(md);
}
function updateDocumentMetrics(md: string) {
const text = stripMarkdown(md);
charCount.value = text.length;
wordCount.value = text.trim() ? text.trim().split(/\s+/).length : 0;
}
function setDocumentContent(md: string) {
content.value = md;
updateDocumentMetrics(md);
}
function resetMarkdownEditorState() {
setDocumentContent("");
currentFilePath.value = null;
hasDraftDocument.value = false;
saveStatus.value = "";
showOutline.value = false;
documentMode.value = "edit";
showMoreMenu.value = false;
editorResetKey.value += 1;
}
function onHeadingClick(heading: { text: string; level: number }) {
const selector = `.markdown-preview h${heading.level}`;
const headingEls = document.querySelectorAll(selector);
@@ -151,21 +178,73 @@ function onHeadingClick(heading: { text: string; level: number }) {
}
}
async function loadDir(path: string) {
function getLastWorkspacePath() {
try {
return localStorage.getItem(LAST_WORKSPACE_PATH_KEY);
} catch {
return null;
}
}
function saveLastWorkspacePath(path: string) {
try {
localStorage.setItem(LAST_WORKSPACE_PATH_KEY, path);
} catch { /* noop */
}
}
function clearLastWorkspacePath() {
try {
localStorage.removeItem(LAST_WORKSPACE_PATH_KEY);
} catch { /* noop */
}
}
async function loadDir(path: string): Promise<boolean> {
folderPath.value = path;
dirEntries.value = [];
try {
dirEntries.value = await invoke<DirEntry[]>("read_dir", {path});
return true;
} catch (e) {
console.error("Failed to read directory:", e);
folderPath.value = "";
dirEntries.value = [];
return false;
}
}
async function switchWorkspace(path: string, options: { persist?: boolean; resetEditor?: boolean } = {}) {
if (options.resetEditor !== false) {
resetMarkdownEditorState();
}
const loaded = await loadDir(path);
if (loaded && options.persist !== false) {
saveLastWorkspacePath(path);
} else if (!loaded) {
clearLastWorkspacePath();
}
return loaded;
}
async function restoreLastWorkspace() {
const lastWorkspacePath = getLastWorkspacePath();
if (!lastWorkspacePath) return;
await switchWorkspace(lastWorkspacePath, {
persist: false,
resetEditor: true,
});
}
async function onOpenFolder() {
try {
const chosen = await invoke<string | null>("pick_folder");
if (chosen) {
await loadDir(chosen);
await switchWorkspace(chosen);
}
} catch (e) {
console.error("Failed to pick folder:", e);
@@ -202,8 +281,9 @@ async function onToggleFolder(path: string) {
async function onOpenFile(path: string) {
try {
const fileContent = await invoke<string>("read_file", {path});
content.value = fileContent;
setDocumentContent(fileContent);
currentFilePath.value = path;
hasDraftDocument.value = false;
saveStatus.value = "";
} catch (e) {
console.error("Failed to open file:", e);
@@ -261,14 +341,16 @@ async function doSaveAs() {
}
function doNew() {
content.value = "";
setDocumentContent("");
currentFilePath.value = null;
hasDraftDocument.value = true;
saveStatus.value = "";
editorResetKey.value += 1;
}
// Listen for menu events from Rust
listen<string>("folder-opened", (event) => {
loadDir(event.payload);
void switchWorkspace(event.payload);
});
listen("menu-save", () => {
@@ -306,6 +388,7 @@ listen("menu-event", (event) => {
<main class="flex-1 flex flex-col overflow-hidden relative">
<!-- Toolbar -->
<div
v-if="!showStartActions"
class="flex w-full items-center h-10 px-3 bg-[#faf9f6] border-b border-[#e0dbcf] select-none shrink-0"
>
<!-- Left: Outline toggle -->
@@ -320,11 +403,11 @@ listen("menu-event", (event) => {
<!-- Center: Filename -->
<div class="flex-1 text-center text-sm text-[#8c877d] font-medium truncate px-3 select-none">
{{ currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
{{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
</div>
<!-- Right: Actions -->
<div class="flex items-center gap-0.5">
<div v-if="!showStartActions" class="flex items-center gap-0.5">
<!-- Document mode -->
<div class="mode-toggle" role="group" aria-label="文档模式">
<button
@@ -386,13 +469,15 @@ listen("menu-event", (event) => {
<div class="flex-1 flex min-h-0 overflow-hidden">
<Transition name="outline-slide">
<OutlinePanel
v-if="showOutline"
v-if="showOutline && !showStartActions"
:markdown-content="content"
@heading-click="onHeadingClick"
/>
</Transition>
<MarkdownEditor
v-if="!showStartActions"
:key="editorResetKey"
ref="markdownEditorRef"
:model-value="content"
:document-mode="documentMode"
@@ -401,6 +486,19 @@ listen("menu-event", (event) => {
@save-shortcut="doSave"
class="flex-1 w-full min-w-0"
/>
<div
v-else
class="start-actions flex-1 flex items-center justify-center px-8"
>
<div class="flex flex-col items-center gap-3">
<button class="start-action-btn" @click="onOpenFolder">
选择工作目录
</button>
<button class="start-action-btn" @click="doNew">
新建文件
</button>
</div>
</div>
</div>
<!-- Zoom indicator -->
@@ -562,6 +660,36 @@ listen("menu-event", (event) => {
.dropdown-item:hover {
background-color: #f4f1ea;
}
.start-actions {
background:
linear-gradient(180deg, rgba(255, 253, 250, 0.35), rgba(244, 241, 235, 0.2)),
#faf9f6;
}
.start-action-btn {
border: none;
background: transparent;
color: #8c877d;
font-size: 15px;
line-height: 1.4;
cursor: pointer;
padding: 4px 2px;
transition:
color 0.15s ease,
text-decoration-color 0.15s ease;
text-decoration: underline;
text-underline-offset: 6px;
text-decoration-thickness: 1px;
text-decoration-color: transparent;
}
.start-action-btn:hover,
.start-action-btn:focus-visible {
color: #bf6a3b;
text-decoration-color: currentColor;
outline: none;
}
</style>

View File

@@ -66,7 +66,7 @@ const showSettings = ref(false);
<div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3">
<!-- Folder -->
<button
class="p-2 rounded-md transition-colors"
class="px-2 py-1 cursor-pointer rounded-md transition-colors"
:class="activeView === 'folder' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
title="文件"
@click="switchView('folder')"
@@ -75,7 +75,7 @@ const showSettings = ref(false);
</button>
<!-- Search -->
<button
class="p-2 rounded-md transition-colors"
class="px-2 py-1 cursor-pointer rounded-md transition-colors"
:class="activeView === 'search' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
title="搜索"
@click="switchView('search')"
@@ -84,7 +84,7 @@ const showSettings = ref(false);
</button>
<!-- ===== Settings ===== -->
<button
class="p-2 rounded-md transition-colors mt-auto"
class="px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto"
:class="activeView === 'setting' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
title="设置"
@click="showSettings = true"
@@ -116,7 +116,7 @@ const showSettings = ref(false);
>
<p class="mb-3">暂无打开的文件夹</p>
<button
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-[#fdf0e5] text-[#bf6a3b] border border-[#d4a574]/40 hover:bg-[#fdf0e5]/80 hover:text-[#bf6a3b] transition-colors"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs cursor-pointer rounded-md bg-[#fdf0e5] text-[#bf6a3b] border border-[#d4a574]/40 hover:bg-[#fdf0e5]/80 hover:text-[#bf6a3b] transition-colors"
@click="$emit('openFolder')"
>
<i class="ri-folder-open-line"></i>