添加目录树和大纲宽度调整功能
This commit is contained in:
@@ -37,5 +37,5 @@
|
|||||||
"topic_20260707-092711_af706c3fcc7e435e": "帮我检查一下app.vue里的too…",
|
"topic_20260707-092711_af706c3fcc7e435e": "帮我检查一下app.vue里的too…",
|
||||||
"topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…",
|
"topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…",
|
||||||
"topic_20260708-020616_9ab54d98daf65053": "帮我分析一下现在的markdownE…",
|
"topic_20260708-020616_9ab54d98daf65053": "帮我分析一下现在的markdownE…",
|
||||||
"topic_20260708-093419_71d1fea2e4b5a40f": "新的会话"
|
"topic_20260708-093419_71d1fea2e4b5a40f": "src/components/Dir…"
|
||||||
}
|
}
|
||||||
@@ -67,15 +67,74 @@ const rootEntry = computed<DirEntry | null>(() => {
|
|||||||
// ---- sidebar view mode ----
|
// ---- sidebar view mode ----
|
||||||
type ViewMode = "folder" | "search" | "setting";
|
type ViewMode = "folder" | "search" | "setting";
|
||||||
const activeView = ref<ViewMode>("folder");
|
const activeView = ref<ViewMode>("folder");
|
||||||
const expanded = ref(true);
|
|
||||||
|
// ---- sidebar resize ----
|
||||||
|
const MIN_SIDEBAR_WIDTH = 180;
|
||||||
|
const MAX_SIDEBAR_WIDTH = 500;
|
||||||
|
const COLLAPSED_WIDTH = 48;
|
||||||
|
const DEFAULT_EXPANDED_WIDTH = 288;
|
||||||
|
|
||||||
|
const sidebarWidth = ref(DEFAULT_EXPANDED_WIDTH);
|
||||||
|
const lastExpandedWidth = ref(DEFAULT_EXPANDED_WIDTH);
|
||||||
|
const isResizing = ref(false);
|
||||||
|
const isCollapsed = computed(() => sidebarWidth.value <= COLLAPSED_WIDTH);
|
||||||
|
|
||||||
|
function expand() {
|
||||||
|
sidebarWidth.value = lastExpandedWidth.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapse() {
|
||||||
|
sidebarWidth.value = COLLAPSED_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCollapse() {
|
||||||
|
if (isCollapsed.value) {
|
||||||
|
expand();
|
||||||
|
} else {
|
||||||
|
collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function switchView(view: ViewMode) {
|
function switchView(view: ViewMode) {
|
||||||
if (activeView.value === view) {
|
if (activeView.value === view) {
|
||||||
expanded.value = !expanded.value;
|
toggleCollapse();
|
||||||
} else {
|
} else {
|
||||||
activeView.value = view;
|
activeView.value = view;
|
||||||
expanded.value = true;
|
if (isCollapsed.value) {
|
||||||
|
expand();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- resize handle drag ----
|
||||||
|
let resizeStartX = 0;
|
||||||
|
let resizeStartWidth = 0;
|
||||||
|
|
||||||
|
function startResize(e: MouseEvent) {
|
||||||
|
resizeStartX = e.clientX;
|
||||||
|
resizeStartWidth = sidebarWidth.value;
|
||||||
|
isResizing.value = true;
|
||||||
|
document.addEventListener("mousemove", onResize);
|
||||||
|
document.addEventListener("mouseup", stopResize);
|
||||||
|
document.body.style.cursor = "col-resize";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize(e: MouseEvent) {
|
||||||
|
if (!isResizing.value) return;
|
||||||
|
const delta = e.clientX - resizeStartX;
|
||||||
|
const newWidth = Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, resizeStartWidth + delta));
|
||||||
|
sidebarWidth.value = newWidth;
|
||||||
|
lastExpandedWidth.value = newWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResize() {
|
||||||
|
isResizing.value = false;
|
||||||
|
document.removeEventListener("mousemove", onResize);
|
||||||
|
document.removeEventListener("mouseup", stopResize);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- workspace switcher ----
|
// ---- workspace switcher ----
|
||||||
@@ -240,6 +299,10 @@ watch(() => props.folderPath, () => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener("click", onDocumentClick);
|
document.removeEventListener("click", onDocumentClick);
|
||||||
|
document.removeEventListener("mousemove", onResize);
|
||||||
|
document.removeEventListener("mouseup", stopResize);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- settings modal ----
|
// ---- settings modal ----
|
||||||
@@ -259,7 +322,9 @@ function openSettings(page: SettingsPage) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
:class="['directory-sidebar flex flex-row transition-all duration-300 ease-in-out overflow-hidden shrink-0', expanded ? 'w-72' : 'w-12']"
|
class="directory-sidebar flex flex-row overflow-hidden shrink-0"
|
||||||
|
:class="{ 'transition-all duration-300 ease-in-out': !isResizing }"
|
||||||
|
:style="{ width: sidebarWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<!-- ============ Icon bar ============ -->
|
<!-- ============ Icon bar ============ -->
|
||||||
<div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3">
|
<div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3">
|
||||||
@@ -293,7 +358,7 @@ function openSettings(page: SettingsPage) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============ Expanded panel ============ -->
|
<!-- ============ Expanded panel ============ -->
|
||||||
<div v-show="expanded" class="directory-sidebar-panel flex-1 flex flex-col overflow-hidden">
|
<div v-show="!isCollapsed" class="directory-sidebar-panel flex-1 flex flex-col overflow-hidden">
|
||||||
<!-- ===== FOLDER VIEW ===== -->
|
<!-- ===== FOLDER VIEW ===== -->
|
||||||
<div v-if="activeView === 'folder'" class="flex flex-col flex-1 overflow-hidden">
|
<div v-if="activeView === 'folder'" class="flex flex-col flex-1 overflow-hidden">
|
||||||
<!-- File tree -->
|
<!-- File tree -->
|
||||||
@@ -413,6 +478,16 @@ function openSettings(page: SettingsPage) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ Resize handle ============ -->
|
||||||
|
<div
|
||||||
|
v-show="!isCollapsed"
|
||||||
|
class="sidebar-resize-handle"
|
||||||
|
:class="{ 'sidebar-resize-handle-active': isResizing }"
|
||||||
|
@mousedown="startResize"
|
||||||
|
>
|
||||||
|
<div class="sidebar-resize-handle-bar"></div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
@@ -563,6 +638,7 @@ function openSettings(page: SettingsPage) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.directory-sidebar {
|
.directory-sidebar {
|
||||||
|
position: relative;
|
||||||
border-right: 1px solid var(--app-border);
|
border-right: 1px solid var(--app-border);
|
||||||
background: color-mix(in srgb, var(--app-sidebar-bg) 82%, transparent);
|
background: color-mix(in srgb, var(--app-sidebar-bg) 82%, transparent);
|
||||||
}
|
}
|
||||||
@@ -571,6 +647,39 @@ function openSettings(page: SettingsPage) {
|
|||||||
border-left: 1px solid var(--app-border);
|
border-left: 1px solid var(--app-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Resize handle ---- */
|
||||||
|
.sidebar-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -2px;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 25;
|
||||||
|
display: flex;
|
||||||
|
width: 7px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: col-resize;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle:hover,
|
||||||
|
.sidebar-resize-handle-active {
|
||||||
|
background: color-mix(in srgb, var(--app-accent) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle-bar {
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle:hover .sidebar-resize-handle-bar,
|
||||||
|
.sidebar-resize-handle-active .sidebar-resize-handle-bar {
|
||||||
|
background: color-mix(in srgb, var(--app-accent) 36%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-icon-btn {
|
.sidebar-icon-btn {
|
||||||
color: var(--app-subtle);
|
color: var(--app-subtle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { ref, computed, onBeforeUnmount } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
markdownContent: string;
|
markdownContent: string;
|
||||||
@@ -42,11 +42,56 @@ const headings = computed<OutlineItem[]>(() => {
|
|||||||
const onHeadingClick = (item: OutlineItem) => {
|
const onHeadingClick = (item: OutlineItem) => {
|
||||||
emit("headingClick", { text: item.text, level: item.level });
|
emit("headingClick", { text: item.text, level: item.level });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- panel resize ----
|
||||||
|
const MIN_PANEL_WIDTH = 150;
|
||||||
|
const MAX_PANEL_WIDTH = 500;
|
||||||
|
const DEFAULT_PANEL_WIDTH = 208; // w-52
|
||||||
|
|
||||||
|
const panelWidth = ref(DEFAULT_PANEL_WIDTH);
|
||||||
|
const isResizing = ref(false);
|
||||||
|
|
||||||
|
let resizeStartX = 0;
|
||||||
|
let resizeStartWidth = 0;
|
||||||
|
|
||||||
|
function startResize(e: MouseEvent) {
|
||||||
|
resizeStartX = e.clientX;
|
||||||
|
resizeStartWidth = panelWidth.value;
|
||||||
|
isResizing.value = true;
|
||||||
|
document.addEventListener("mousemove", onResize);
|
||||||
|
document.addEventListener("mouseup", stopResize);
|
||||||
|
document.body.style.cursor = "col-resize";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize(e: MouseEvent) {
|
||||||
|
if (!isResizing.value) return;
|
||||||
|
const delta = e.clientX - resizeStartX;
|
||||||
|
panelWidth.value = Math.min(MAX_PANEL_WIDTH, Math.max(MIN_PANEL_WIDTH, resizeStartWidth + delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResize() {
|
||||||
|
isResizing.value = false;
|
||||||
|
document.removeEventListener("mousemove", onResize);
|
||||||
|
document.removeEventListener("mouseup", stopResize);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener("mousemove", onResize);
|
||||||
|
document.removeEventListener("mouseup", stopResize);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
class="outline-panel flex flex-col w-52 shrink-0 overflow-hidden"
|
class="outline-panel flex flex-col shrink-0 overflow-hidden"
|
||||||
|
:class="{ 'transition-all duration-300 ease-in-out': !isResizing }"
|
||||||
|
:style="{ width: panelWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="outline-panel-header flex items-center px-3 py-3">
|
<div class="outline-panel-header flex items-center px-3 py-3">
|
||||||
@@ -81,15 +126,58 @@ const onHeadingClick = (item: OutlineItem) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Resize handle -->
|
||||||
|
<div
|
||||||
|
class="outline-resize-handle"
|
||||||
|
:class="{ 'outline-resize-handle-active': isResizing }"
|
||||||
|
@mousedown="startResize"
|
||||||
|
>
|
||||||
|
<div class="outline-resize-handle-bar"></div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.outline-panel {
|
.outline-panel {
|
||||||
|
position: relative;
|
||||||
border-right: 1px solid var(--app-border);
|
border-right: 1px solid var(--app-border);
|
||||||
background: color-mix(in srgb, var(--app-sidebar-bg) 82%, transparent);
|
background: color-mix(in srgb, var(--app-sidebar-bg) 82%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Resize handle ---- */
|
||||||
|
.outline-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -2px;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 25;
|
||||||
|
display: flex;
|
||||||
|
width: 7px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: col-resize;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-resize-handle:hover,
|
||||||
|
.outline-resize-handle-active {
|
||||||
|
background: color-mix(in srgb, var(--app-accent) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-resize-handle-bar {
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-resize-handle:hover .outline-resize-handle-bar,
|
||||||
|
.outline-resize-handle-active .outline-resize-handle-bar {
|
||||||
|
background: color-mix(in srgb, var(--app-accent) 36%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.outline-panel-header {
|
.outline-panel-header {
|
||||||
border-bottom: 1px solid var(--app-border);
|
border-bottom: 1px solid var(--app-border);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user