mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
✨ Feature(custom): support collapse navi bar
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
"picBedQrCode": "图床配置二维码",
|
||||
"choosePicBed": "选择图床",
|
||||
"selectPicBeds": "请选择图床",
|
||||
"copySuccess": "复制成功"
|
||||
"copySuccess": "复制成功",
|
||||
"collapse": "收起侧边栏",
|
||||
"expand": "展开侧边栏"
|
||||
},
|
||||
"settings": {
|
||||
"theme": {
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
"picBedQrCode": "圖床配置 QRCODE",
|
||||
"choosePicBed": "選擇圖床",
|
||||
"selectPicBeds": "請選擇圖床",
|
||||
"copySuccess": "複製成功"
|
||||
"copySuccess": "複製成功",
|
||||
"collapse": "收起側邊欄",
|
||||
"expand": "展開側邊欄"
|
||||
},
|
||||
"settings": {
|
||||
"theme": {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -75,12 +75,42 @@
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 160px;
|
||||
min-width: 120px;
|
||||
max-width: 400px;
|
||||
background: var(--color-surface-secondary);
|
||||
border-right: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* Fix for flex overflow */
|
||||
transition: width 0.1s ease-out;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.resize-handle:hover .resize-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.resize-line {
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background: var(--color-accent);
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
@@ -180,6 +210,9 @@
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
|
||||
Reference in New Issue
Block a user