功能完善
This commit is contained in:
146
src/App.vue
146
src/App.vue
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user