Feature(custom): support collapse navi bar

This commit is contained in:
Kuingsmile
2025-08-14 16:19:24 +08:00
parent ae366bab21
commit d4af5fc8f6
7 changed files with 925 additions and 677 deletions

View File

@@ -1,7 +1,13 @@
<template>
<nav class="navigation">
<nav
class="navigation"
:class="{ collapsed: isCollapsed }"
>
<div class="title-bar">
<div class="app-title">
<div
v-if="!isCollapsed"
class="app-title"
>
<div
class="app-text"
@click="openGithubPage"
@@ -12,10 +18,24 @@
v{{ version }}
</div>
</div>
<button
:title="isCollapsed ? t('navigation.expand') : t('navigation.collapse')"
class="collapse-button"
@click="isCollapsed = !isCollapsed"
>
<ChevronLeftIcon
v-if="!isCollapsed"
:size="20"
/>
<ChevronRightIcon
v-else
:size="20"
/>
</button>
</div>
<div class="theme-section">
<ThemeSwitcher />
<ThemeSwitcher :collapsed="isCollapsed" />
</div>
<div class="nav-menu">
@@ -32,9 +52,13 @@
:size="18"
/>
</div>
<span class="nav-label">{{ item.name }}</span>
<span
v-if="!isCollapsed"
class="nav-label"
>{{ item.name }}</span>
</router-link>
<Disclosure
v-if="!isCollapsed"
v-slot="{ open }"
as="div"
class="nav-submenu"
@@ -61,6 +85,15 @@
</router-link>
</DisclosurePanel>
</Disclosure>
<div
v-else
class="nav-item collapsed-picbed"
:title="t('navigation.picbed')"
>
<div class="nav-icon-container">
<DatabaseIcon :size="18" />
</div>
</div>
</div>
<div class="sidebar-footer">
<button
@@ -205,7 +238,7 @@ import {
TransitionRoot
} from '@headlessui/vue'
import { pick } from 'lodash-es'
import { CheckIcon, ChevronDownIcon, CopyIcon, DatabaseIcon, FolderIcon, Info, PieChartIcon, PlugIcon, Settings, UploadIcon } from 'lucide-vue-next'
import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DatabaseIcon, FolderIcon, Info, PieChartIcon, PlugIcon, Settings, UploadIcon } from 'lucide-vue-next'
import QrcodeVue from 'qrcode.vue'
import pkg from 'root/package.json'
import { computed, nextTick, onBeforeMount, onBeforeUnmount, reactive, Ref, ref, watch } from 'vue'
@@ -220,6 +253,7 @@ import { picBedGlobal, updatePicBedGlobal } from '@/utils/global'
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
const version = ref(pkg.version)
const isCollapsed = ref(false)
const { t } = useI18n()
const message = useMessage()
@@ -230,6 +264,11 @@ const picBedConfigString = ref('')
let removeIpcListener: () => void = () => {}
// Save collapsed state to localStorage when it changes
watch(isCollapsed, (newValue) => {
localStorage.setItem('navigation-collapsed', JSON.stringify(newValue))
})
watch(
() => choosedPicBedForQRCode,
val => {
@@ -278,6 +317,12 @@ function openGithubPage () {
}
onBeforeMount(() => {
// Load collapsed state from localStorage
const savedState = localStorage.getItem('navigation-collapsed')
if (savedState !== null) {
isCollapsed.value = JSON.parse(savedState)
}
updatePicBedGlobal()
removeIpcListener = window.electron.ipcRendererOn(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler)
})
@@ -297,6 +342,11 @@ onBeforeUnmount(() => {
background: var(--color-background-secondary);
border-right: 1px solid rgb(229 231 235);
overflow: hidden;
transition: width 0.3s ease;
}
.navigation.collapsed {
width: 60px;
}
:root.dark .navigation,
@@ -312,6 +362,38 @@ onBeforeUnmount(() => {
padding: 1.25rem 1rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-background-secondary);
position: relative;
}
.navigation.collapsed .title-bar {
padding: 1rem 0.5rem;
}
.collapse-button {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--color-text-primary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.collapse-button:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.navigation.collapsed .collapse-button {
position: static;
transform: none;
}
:root.dark .title-bar,
@@ -382,6 +464,16 @@ onBeforeUnmount(() => {
transition: all 0.2s ease;
}
.navigation.collapsed .nav-item {
padding: 0.75rem 0.5rem;
justify-content: center;
gap: 0;
}
.navigation.collapsed .nav-label {
display: none;
}
:root.dark .nav-item,
:root.auto.dark .nav-item {
color: rgb(209 213 219);
@@ -526,15 +618,19 @@ onBeforeUnmount(() => {
color: var(--color-text-primary);
}
.submenu-item.router-link-active {
background: rgb(239 246 255);
color: rgb(99 102 241);
.collapsed-picbed {
cursor: default;
}
:root.dark .submenu-item.router-link-active,
:root.auto.dark .submenu-item.router-link-active {
background: rgb(30 58 138 / 0.2);
color: rgb(129 140 248);
.collapsed-picbed:hover {
background: rgb(243 244 246);
color: rgb(17 24 39);
}
:root.dark .collapsed-picbed:hover,
:root.auto.dark .collapsed-picbed:hover {
background: rgb(55 65 81);
color: rgb(243 244 246);
}
.qr-dialog {
@@ -718,7 +814,7 @@ onBeforeUnmount(() => {
}
/* Responsive Design */
@media (max-width: 768px) {
.sidebar {
.navigation {
width: 60px;
}
@@ -726,6 +822,13 @@ onBeforeUnmount(() => {
display: none;
}
.app-title {
display: none;
}
.collapse-button {
display: none;
}
}
/* Scrollbar Styling */

View File

@@ -5,6 +5,12 @@ import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/hooks/useAppStore'
interface Props {
collapsed?: boolean
}
defineProps<Props>()
const { t } = useI18n()
const appStore = useAppStore()
@@ -44,6 +50,7 @@ const toggleTheme = () => {
<div class="theme-switcher">
<button
class="theme-toggle-btn"
:class="{ collapsed }"
:title="t('settings.theme.toggle')"
@click="toggleTheme"
>
@@ -51,7 +58,10 @@ const toggleTheme = () => {
:is="currentThemeOption.icon"
:size="18"
/>
<span class="theme-label">{{ currentThemeOption.label }}</span>
<span
v-if="!collapsed"
class="theme-label"
>{{ currentThemeOption.label }}</span>
</button>
</div>
</template>
@@ -74,6 +84,13 @@ const toggleTheme = () => {
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.theme-toggle-btn.collapsed {
padding: 0.5rem;
gap: 0;
justify-content: center;
}
.theme-toggle-btn:hover {
@@ -90,5 +107,11 @@ const toggleTheme = () => {
.theme-label {
display: none;
}
.theme-toggle-btn {
padding: 0.5rem;
gap: 0;
justify-content: center;
}
}
</style>

View File

@@ -23,7 +23,9 @@
"picBedQrCode": "PicBed QR Code",
"choosePicBed": "Choose PicBed",
"selectPicBeds": "Select PicBeds",
"copySuccess": "Copy Success"
"copySuccess": "Copy Success",
"collapse": "Collapse Sidebar",
"expand": "Expand Sidebar"
},
"settings": {
"theme": {

View File

@@ -23,7 +23,9 @@
"picBedQrCode": "图床配置二维码",
"choosePicBed": "选择图床",
"selectPicBeds": "请选择图床",
"copySuccess": "复制成功"
"copySuccess": "复制成功",
"collapse": "收起侧边栏",
"expand": "展开侧边栏"
},
"settings": {
"theme": {

View File

@@ -23,7 +23,9 @@
"picBedQrCode": "圖床配置 QRCODE",
"choosePicBed": "選擇圖床",
"selectPicBeds": "請選擇圖床",
"copySuccess": "複製成功"
"copySuccess": "複製成功",
"collapse": "收起側邊欄",
"expand": "展開側邊欄"
},
"settings": {
"theme": {

View File

@@ -42,7 +42,11 @@
<!-- Main Content Card -->
<div class="manage-card main-card">
<div class="main-layout">
<div class="sidebar">
<div
ref="sidebar"
class="sidebar"
:style="{ width: sidebarWidth + 'px' }"
>
<div class="sidebar-header">
<h3 class="sidebar-title">
{{ menuTitleMap[currentPicBedName] }}
@@ -80,16 +84,11 @@
v-else-if="currentPicBedName === 'github'"
class="menu-icon"
/>
<span class="menu-text">
{{
currentPicBedName === 'tcyun'
? item.slice(0, item.length - 11)
: currentPicBedName === 'github'
? item.length > 10
? `${item.slice(0, 5)}..${item.slice(-5)}`
: item
: item
}}
<span
class="menu-text"
:title="item"
>
{{ truncateText(item, currentPicBedName) }}
</span>
</div>
</div>
@@ -122,7 +121,18 @@
</div>
</div>
<div class="content-area">
<!-- Resize Handle -->
<div
class="resize-handle"
@mousedown="startResize"
>
<div class="resize-line" />
</div>
<div
ref="contentArea"
class="content-area"
>
<router-view />
</div>
</div>
@@ -337,7 +347,7 @@ import {
SettingsIcon,
XIcon
} from 'lucide-vue-next'
import { onBeforeMount, reactive, ref, watch } from 'vue'
import { computed, onBeforeMount, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -357,6 +367,10 @@ const message = useMessage()
const currentAlias = ref(route.query.alias as string)
const currentPicBedName = ref(route.query.picBedName as string)
const contentArea = ref<HTMLElement>()
const sidebarWidth = ref(160)
const isResizing = ref(false)
let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string)
let currentPagePicBedConfig = reactive(JSON.parse(route.query.config as string))
@@ -369,6 +383,46 @@ const isLoadingBucketList = ref(false)
const nweBucketDrawerVisible = ref(false)
const picBedSwitchDialogVisible = ref(false)
const maxTextLength = computed(() => {
const fixedSpace = 16 + 12 + 24 + 8
const availableWidth = sidebarWidth.value - fixedSpace
const estimatedCharWidth = 14 * 0.6
const maxChars = Math.floor(availableWidth / estimatedCharWidth)
return Math.max(6, Math.min(maxChars, 60))
})
const truncateText = (text: string, picBedName: string): string => {
if (!text) return ''
if (picBedName === 'tcyun') {
const baseName = text.slice(0, text.length - 11)
if (baseName.length <= maxTextLength.value) {
return baseName
}
return `${baseName.slice(0, maxTextLength.value - 3)}...`
} else if (picBedName === 'github') {
if (text.length <= maxTextLength.value) {
return text
}
const minSideLength = 3
const totalEllipsis = 2 // '..'
const availableForContent = maxTextLength.value - totalEllipsis
if (availableForContent < minSideLength * 2) {
return `${text.slice(0, maxTextLength.value - 3)}...`
}
const prefixLength = Math.ceil(availableForContent / 2)
const suffixLength = availableForContent - prefixLength
return `${text.slice(0, prefixLength)}..${text.slice(-suffixLength)}`
} else {
if (text.length <= maxTextLength.value) {
return text
}
return `${text.slice(0, maxTextLength.value - 3)}...`
}
}
watch(route, async (newRoute) => {
if (newRoute.fullPath.split('?')[0] === '/main-page/manage-main-page') {
currentAlias.value = newRoute.query.alias as string
@@ -379,6 +433,8 @@ watch(route, async (newRoute) => {
}
}, { deep: true })
watch(sidebarWidth, () => {}, { immediate: false })
const urlMap: IStringKeyMap = {
aliyun: 'https://oss.console.aliyun.com',
github: 'https://github.com',
@@ -563,6 +619,33 @@ function openBucketPageSetting () {
})
}
function startResize (event: MouseEvent) {
isResizing.value = true
const startX = event.clientX
const startWidth = sidebarWidth.value
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing.value) return
const deltaX = e.clientX - startX
const newWidth = Math.max(120, Math.min(400, startWidth + deltaX))
sidebarWidth.value = newWidth
}
const handleMouseUp = () => {
isResizing.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
}
onBeforeMount(() => {
getBucketList()
})

File diff suppressed because it is too large Load Diff