自定义装饰栏功能
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-start-dragging",
|
||||
"opener:default",
|
||||
"dialog:default"
|
||||
]
|
||||
|
||||
241
src/App.vue
241
src/App.vue
@@ -2,6 +2,7 @@
|
||||
import {computed, ref, watch, onMounted, onUnmounted} from "vue";
|
||||
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
||||
import {invoke, isTauri} from "@tauri-apps/api/core";
|
||||
import {getCurrentWindow} from "@tauri-apps/api/window";
|
||||
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
||||
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
||||
import OutlinePanel from "./components/OutlinePanel.vue";
|
||||
@@ -135,10 +136,105 @@ const showMoreMenu = ref(false);
|
||||
const settingsRequest = ref(0);
|
||||
const settingsInitialPage = ref<"about" | "editor">("about");
|
||||
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
|
||||
const isWindowMaximized = ref(false);
|
||||
const showStartActions = computed(() => {
|
||||
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
|
||||
});
|
||||
|
||||
type TitlebarDragState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
};
|
||||
|
||||
const TITLEBAR_DRAG_THRESHOLD = 4;
|
||||
let titlebarDragState: TitlebarDragState | null = null;
|
||||
let unlistenWindowResize: (() => void) | null = null;
|
||||
|
||||
function isInteractiveTitlebarTarget(target: EventTarget | null) {
|
||||
if (!(target instanceof Element)) return false;
|
||||
return Boolean(target.closest("button, a, input, textarea, select, [data-no-window-drag]"));
|
||||
}
|
||||
|
||||
async function refreshWindowMaximized() {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
isWindowMaximized.value = await getCurrentWindow().isMaximized();
|
||||
} catch (error) {
|
||||
console.error("Failed to read window state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function minimizeWindow() {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
await getCurrentWindow().minimize();
|
||||
} catch (error) {
|
||||
console.error("Failed to minimize window:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleWindowMaximize() {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
const appWindow = getCurrentWindow();
|
||||
await appWindow.toggleMaximize();
|
||||
isWindowMaximized.value = await appWindow.isMaximized();
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle window maximize:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function closeWindow() {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
await getCurrentWindow().close();
|
||||
} catch (error) {
|
||||
console.error("Failed to close window:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function removeTitlebarDragListeners() {
|
||||
window.removeEventListener("mousemove", onTitlebarMouseMove);
|
||||
window.removeEventListener("mouseup", removeTitlebarDragListeners);
|
||||
titlebarDragState = null;
|
||||
}
|
||||
|
||||
function onTitlebarMouseDown(event: MouseEvent) {
|
||||
if (event.button !== 0 || event.detail > 1 || isInteractiveTitlebarTarget(event.target)) return;
|
||||
|
||||
event.preventDefault();
|
||||
titlebarDragState = {
|
||||
startX: event.screenX,
|
||||
startY: event.screenY,
|
||||
};
|
||||
window.addEventListener("mousemove", onTitlebarMouseMove);
|
||||
window.addEventListener("mouseup", removeTitlebarDragListeners, {once: true});
|
||||
}
|
||||
|
||||
function onTitlebarMouseMove(event: MouseEvent) {
|
||||
if (!titlebarDragState) return;
|
||||
|
||||
const movedX = Math.abs(event.screenX - titlebarDragState.startX);
|
||||
const movedY = Math.abs(event.screenY - titlebarDragState.startY);
|
||||
if (movedX < TITLEBAR_DRAG_THRESHOLD && movedY < TITLEBAR_DRAG_THRESHOLD) return;
|
||||
|
||||
event.preventDefault();
|
||||
removeTitlebarDragListeners();
|
||||
if (!isTauri()) return;
|
||||
|
||||
void getCurrentWindow().startDragging().catch((error) => {
|
||||
console.error("Failed to start window drag:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function onTitlebarDoubleClick(event: MouseEvent) {
|
||||
if (isInteractiveTitlebarTarget(event.target)) return;
|
||||
|
||||
event.preventDefault();
|
||||
removeTitlebarDragListeners();
|
||||
void toggleWindowMaximize();
|
||||
}
|
||||
|
||||
function openSettings(page: "about" | "editor" = "about") {
|
||||
settingsInitialPage.value = page;
|
||||
settingsRequest.value += 1;
|
||||
@@ -181,11 +277,26 @@ onMounted(() => {
|
||||
applyAppTheme(appTheme.value);
|
||||
applyZoomCssVars();
|
||||
window.addEventListener("wheel", onEditorWheel, {passive: false});
|
||||
if (isTauri()) {
|
||||
void refreshWindowMaximized();
|
||||
void getCurrentWindow().onResized(() => {
|
||||
void refreshWindowMaximized();
|
||||
}).then((unlisten) => {
|
||||
unlistenWindowResize = unlisten;
|
||||
}).catch((error) => {
|
||||
console.error("Failed to listen for window resize:", error);
|
||||
});
|
||||
}
|
||||
void restoreLastWorkspace();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("wheel", onEditorWheel);
|
||||
removeTitlebarDragListeners();
|
||||
if (unlistenWindowResize) {
|
||||
unlistenWindowResize();
|
||||
unlistenWindowResize = null;
|
||||
}
|
||||
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
|
||||
});
|
||||
|
||||
@@ -478,28 +589,45 @@ registerTauriListener("menu-event", (event) => {
|
||||
|
||||
<!-- Right: Editor area -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden relative">
|
||||
<!-- ════════════════ Toolbar ════════════════ -->
|
||||
<!-- ════════════════ Custom title bar / toolbar ════════════════ -->
|
||||
<div
|
||||
v-if="!showStartActions"
|
||||
class="editor-toolbar flex w-full items-center h-10 px-3 select-none shrink-0"
|
||||
class="editor-titlebar flex w-full items-center h-10 pl-3 pr-0 select-none shrink-0"
|
||||
@mousedown="onTitlebarMouseDown"
|
||||
@dblclick="onTitlebarDoubleClick"
|
||||
>
|
||||
<!-- Left: Outline toggle -->
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
:class="{ 'toolbar-btn-active': showOutline }"
|
||||
title="切换大纲"
|
||||
@click="showOutline = !showOutline"
|
||||
<div
|
||||
v-if="!showStartActions"
|
||||
class="flex items-center shrink-0"
|
||||
data-no-window-drag
|
||||
>
|
||||
<i class="ri-menu-line text-base"></i>
|
||||
</button>
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
:class="{ 'toolbar-btn-active': showOutline }"
|
||||
title="切换大纲"
|
||||
aria-label="切换大纲"
|
||||
@click="showOutline = !showOutline"
|
||||
>
|
||||
<i class="ri-menu-line text-base"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="titlebar-brand app-muted-text shrink-0 pointer-events-none">
|
||||
yurou
|
||||
</div>
|
||||
|
||||
<!-- Center: Filename -->
|
||||
<div class="app-muted-text flex-1 text-center text-sm font-medium truncate px-3 select-none">
|
||||
<div class="titlebar-drag-surface flex-1 min-w-0 px-3">
|
||||
<div class="app-muted-text titlebar-file-title text-center text-sm font-medium truncate pointer-events-none">
|
||||
{{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Actions -->
|
||||
<div v-if="!showStartActions" class="flex items-center gap-0.5">
|
||||
<div
|
||||
v-if="!showStartActions"
|
||||
class="flex items-center gap-0.5 shrink-0"
|
||||
data-no-window-drag
|
||||
>
|
||||
<!-- Document mode -->
|
||||
<div class="mode-toggle" role="group" aria-label="文档模式">
|
||||
<button
|
||||
@@ -557,6 +685,34 @@ registerTauriListener("menu-event", (event) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Window controls -->
|
||||
<div class="window-controls flex items-stretch self-stretch ml-2 shrink-0" data-no-window-drag>
|
||||
<button
|
||||
class="window-control-btn"
|
||||
title="最小化"
|
||||
aria-label="最小化"
|
||||
@click="minimizeWindow"
|
||||
>
|
||||
<i class="ri-subtract-line"></i>
|
||||
</button>
|
||||
<button
|
||||
class="window-control-btn"
|
||||
:title="isWindowMaximized ? '还原' : '最大化'"
|
||||
:aria-label="isWindowMaximized ? '还原' : '最大化'"
|
||||
@click="toggleWindowMaximize"
|
||||
>
|
||||
<i :class="isWindowMaximized ? 'ri-checkbox-multiple-blank-line' : 'ri-checkbox-blank-line'"></i>
|
||||
</button>
|
||||
<button
|
||||
class="window-control-btn window-control-close"
|
||||
title="关闭"
|
||||
aria-label="关闭"
|
||||
@click="closeWindow"
|
||||
>
|
||||
<i class="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 flex min-h-0 overflow-hidden">
|
||||
<Transition name="outline-slide">
|
||||
@@ -657,7 +813,7 @@ registerTauriListener("menu-event", (event) => {
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
.editor-titlebar {
|
||||
background: var(--app-bg);
|
||||
border-bottom: 1px solid var(--app-border-strong);
|
||||
}
|
||||
@@ -686,6 +842,28 @@ registerTauriListener("menu-event", (event) => {
|
||||
|
||||
/* ── Toolbar ───────────────────────────────────────────────────── */
|
||||
|
||||
.titlebar-drag-surface {
|
||||
height: 100%;
|
||||
min-width: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.titlebar-brand {
|
||||
height: 100%;
|
||||
min-width: 68px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.titlebar-file-title {
|
||||
width: 100%;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -711,6 +889,43 @@ registerTauriListener("menu-event", (event) => {
|
||||
background-color: var(--app-surface-muted);
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
color: var(--app-muted);
|
||||
}
|
||||
|
||||
.window-control-btn {
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.window-control-btn:hover {
|
||||
background-color: var(--app-hover);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.window-control-btn:focus-visible {
|
||||
outline: 1px solid var(--app-accent);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.window-control-close:hover {
|
||||
background-color: #c42b1c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user