添加目录树和大纲宽度调整功能
This commit is contained in:
@@ -37,5 +37,5 @@
|
||||
"topic_20260707-092711_af706c3fcc7e435e": "帮我检查一下app.vue里的too…",
|
||||
"topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…",
|
||||
"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 ----
|
||||
type ViewMode = "folder" | "search" | "setting";
|
||||
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) {
|
||||
if (activeView.value === view) {
|
||||
expanded.value = !expanded.value;
|
||||
toggleCollapse();
|
||||
} else {
|
||||
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 ----
|
||||
@@ -240,6 +299,10 @@ watch(() => props.folderPath, () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("click", onDocumentClick);
|
||||
document.removeEventListener("mousemove", onResize);
|
||||
document.removeEventListener("mouseup", stopResize);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
});
|
||||
|
||||
// ---- settings modal ----
|
||||
@@ -259,7 +322,9 @@ function openSettings(page: SettingsPage) {
|
||||
|
||||
<template>
|
||||
<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 ============ -->
|
||||
<div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3">
|
||||
@@ -293,7 +358,7 @@ function openSettings(page: SettingsPage) {
|
||||
</div>
|
||||
|
||||
<!-- ============ 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 ===== -->
|
||||
<div v-if="activeView === 'folder'" class="flex flex-col flex-1 overflow-hidden">
|
||||
<!-- File tree -->
|
||||
@@ -413,6 +478,16 @@ function openSettings(page: SettingsPage) {
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<SettingsModal
|
||||
@@ -563,6 +638,7 @@ function openSettings(page: SettingsPage) {
|
||||
|
||||
<style scoped>
|
||||
.directory-sidebar {
|
||||
position: relative;
|
||||
border-right: 1px solid var(--app-border);
|
||||
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);
|
||||
}
|
||||
|
||||
/* ---- 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 {
|
||||
color: var(--app-subtle);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ref, computed, onBeforeUnmount } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
markdownContent: string;
|
||||
@@ -42,11 +42,56 @@ const headings = computed<OutlineItem[]>(() => {
|
||||
const onHeadingClick = (item: OutlineItem) => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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 -->
|
||||
<div class="outline-panel-header flex items-center px-3 py-3">
|
||||
@@ -81,15 +126,58 @@ const onHeadingClick = (item: OutlineItem) => {
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.outline-panel {
|
||||
position: relative;
|
||||
border-right: 1px solid var(--app-border);
|
||||
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 {
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user