Feature(custom): support indicate current picbed config page

This commit is contained in:
Kuingsmile
2026-01-20 22:27:33 +08:00
parent 4cfa61c040
commit 79808e7d39
3 changed files with 96 additions and 498 deletions

View File

@@ -1,26 +1,38 @@
<template> <template>
<nav class="navigation" :class="{ collapsed: isCollapsed }"> <nav
<div class="title-bar"> class="group no-scrollbar flex h-screen w-[150px] flex-col overflow-hidden border-r border-r-border-secondary/50 bg-bg-secondary transition-all duration-medium ease-apple max-md:w-[60px] [.collapsed]:w-[60px]"
<div v-show="!isCollapsed" class="app-title"> :class="{ collapsed: isCollapsed }"
<div class="app-text" @click="openGithubPage"> >
<div
class="relative flex items-center justify-center bg-bg-secondary px-4 py-5 group-[.collapsed]:px-2 group-[.collapsed]:py-4"
>
<div v-show="!isCollapsed" class="flex flex-col items-center gap-1 group-[.collapsed]:hidden max-md:hidden">
<div
class="text-[16px] font-bold tracking-tight text-main hover:cursor-pointer hover:text-accent"
@click="openGithubPage"
>
{{ t('app.title') }} {{ t('app.title') }}
</div> </div>
<div class="app-version">v{{ version }}</div> <div
class="rounded-lg border border-border/50 bg-bg-secondary px-[8px] py-[3px] text-[10px] font-medium text-secondary"
>
v{{ version }}
</div>
</div> </div>
<button <button
:title="isCollapsed ? t('navigation.expand') : t('navigation.collapse')" :title="isCollapsed ? t('navigation.expand') : t('navigation.collapse')"
class="collapse-button" class="absolute top-1/2 right-[8px] flex -translate-y-1/2 cursor-pointer items-center justify-center rounded-sm border-none bg-transparent p-[4px] transition-all duration-200 ease-apple group-[.collapsed]:absolute group-[.collapsed]:top-[20px] group-[.collapsed]:right-[16px] group-[.collapsed]:transform-none hover:bg-surface-elevated hover:text-main"
@click="isCollapsed = !isCollapsed" @click="isCollapsed = !isCollapsed"
> >
<component :is="isCollapsed ? ChevronRightIcon : ChevronLeftIcon" :size="16" /> <component :is="isCollapsed ? ChevronRightIcon : ChevronLeftIcon" :size="16" />
</button> </button>
</div> </div>
<div class="theme-section"> <div class="flex items-center justify-center p-3">
<ThemeSwitcher :collapsed="isCollapsed" /> <ThemeSwitcher :collapsed="isCollapsed" />
</div> </div>
<div class="nav-menu"> <div class="no-scrollbar min-h-0 flex-1 overflow-y-auto py-4">
<div <div
v-for="item in navigationItems.slice(0, 3)" v-for="item in navigationItems.slice(0, 3)"
:key="item.path" :key="item.path"
@@ -32,22 +44,29 @@
<div class="nav-icon-container"> <div class="nav-icon-container">
<component :is="item.icon" :size="18" /> <component :is="item.icon" :size="18" />
</div> </div>
<span v-show="!isCollapsed" class="nav-label">{{ item.name }}</span> <span v-show="!isCollapsed" class="max-md:hidden" :class="isCollapsed ? 'hidden' : ''">{{ item.name }}</span>
</div> </div>
<Disclosure v-show="!isCollapsed" v-slot="{ open }" as="div" class="nav-submenu"> <Disclosure v-show="!isCollapsed" v-slot="{ open }" as="div" class="relative mt-[4px] justify-center">
<DisclosureButton class="nav-item submenu-trigger"> <DisclosureButton
class="nav-item relative flex w-full cursor-pointer items-center justify-center gap-3 border-none bg-transparent px-4 py-3 text-sm font-medium text-secondary no-underline transition-all duration-200 ease-apple hover:bg-surface-elevated hover:text-main"
>
<div class="nav-icon-container"> <div class="nav-icon-container">
<DatabaseIcon :size="18" /> <DatabaseIcon :size="18" />
</div> </div>
<span class="nav-label">{{ t('navigation.picbed') }}</span> <span class="shrink-0 max-md:hidden" :class="isCollapsed ? 'hidden' : ''">{{ t('navigation.picbed') }}</span>
<ChevronDownIcon :size="16" class="submenu-arrow" :class="{ 'rotate-180': open }" /> <ChevronDownIcon
:size="16"
class="absolute right-4 shrink-0 transition-all duration-200 ease-apple"
:class="{ 'rotate-180': open }"
/>
</DisclosureButton> </DisclosureButton>
<DisclosurePanel class="submenu-panel"> <DisclosurePanel class="mt-[2px] flex flex-col gap-[4px] pl-11">
<div <div
v-for="item in visiblePicBeds" v-for="item in visiblePicBeds"
:key="item.type" :key="item.type"
class="submenu-item" :class="{ 'router-link-active': isPicBedPathActive(item.type) }"
class="flex cursor-pointer items-center px-4 py-2 text-sm font-medium text-secondary no-underline transition-all duration-200 ease-apple hover:bg-surface-elevated hover:text-accent-hover [.router-link-active]:border-r-4 [.router-link-active]:border-accent [.router-link-active]:bg-surface [.router-link-active]:text-accent"
@click="navigateToUploaderConfig(item.type)" @click="navigateToUploaderConfig(item.type)"
> >
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
@@ -56,7 +75,7 @@
</Disclosure> </Disclosure>
<div <div
v-show="isCollapsed" v-show="isCollapsed"
class="nav-item collapsed-picbed" class="nav-item cursor-default bg-surface-elevated hover:text-main"
:title="t('navigation.picbed')" :title="t('navigation.picbed')"
@click="isCollapsed = !isCollapsed" @click="isCollapsed = !isCollapsed"
> >
@@ -76,11 +95,15 @@
<div class="nav-icon-container"> <div class="nav-icon-container">
<component :is="item.icon" :size="18" /> <component :is="item.icon" :size="18" />
</div> </div>
<span v-show="!isCollapsed" class="nav-label">{{ item.name }}</span> <span v-show="!isCollapsed" class="max-md:hidden" :class="isCollapsed ? 'hidden' : ''">{{ item.name }}</span>
</div> </div>
</div> </div>
<div class="sidebar-footer"> <div class="border-t border-t-border p-3">
<button class="footer-button" :title="t('navigation.moreOptions')" @click="openMenu"> <button
class="fixed bottom-[4px] left-[4px] cursor-pointer rounded-full border-none bg-transparent p-[8px] text-tertiary hover:bg-surface-elevated hover:text-main"
:title="t('navigation.moreOptions')"
@click="openMenu"
>
<Info :size="20" /> <Info :size="20" />
</button> </button>
</div> </div>
@@ -89,38 +112,51 @@
<FirstTimeGuide ref="guideRef" /> <FirstTimeGuide ref="guideRef" />
<TransitionRoot appear :show="qrcodeVisible" as="template"> <TransitionRoot appear :show="qrcodeVisible" as="template">
<Dialog as="div" class="qr-dialog" @close="qrcodeVisible = false"> <Dialog
<div class="dialog-container"> as="div"
class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@close="qrcodeVisible = false"
>
<div class="fixed inset-0 z-50 flex min-h-screen items-center justify-center overflow-y-auto p-[16px]">
<TransitionChild as="template"> <TransitionChild as="template">
<DialogPanel class="dialog-panel"> <DialogPanel
<DialogTitle class="dialog-title"> class="w-full max-w-[500px] overflow-visible rounded-xl border border-border bg-bg-tertiary shadow-md"
>
<DialogTitle class="m-0 px-[24px] pt-[20px] text-2xl font-semibold text-main">
{{ t('navigation.picBedQrCode') }} {{ t('navigation.picBedQrCode') }}
</DialogTitle> </DialogTitle>
<div class="dialog-content"> <div class="p-4">
<div class="form-group"> <div class="mb-5">
<label class="form-label">{{ t('navigation.choosePicBed') }}</label> <label class="mb-2 block text-base font-medium text-main">{{ t('navigation.choosePicBed') }}</label>
<Listbox v-model="choosedPicBedForQRCode" multiple> <Listbox v-model="choosedPicBedForQRCode" multiple>
<div class="listbox-container"> <div class="relative">
<ListboxButton class="listbox-button"> <ListboxButton
<span v-if="choosedPicBedForQRCode.length === 0" class="placeholder"> class="flex w-full cursor-pointer items-center justify-between rounded-2xl border border-border bg-surface px-4 py-3 text-base text-main hover:border-accent"
>
<span v-if="choosedPicBedForQRCode.length === 0" class="text-secondary">
{{ t('navigation.selectPicBeds') }} {{ t('navigation.selectPicBeds') }}
</span> </span>
<span v-else class="selected-count"> <span v-else class="text-main">
{{ choosedPicBedForQRCode.length }} {{ t('navigation.selected') }} {{ choosedPicBedForQRCode.length }} {{ t('navigation.selected') }}
</span> </span>
<ChevronDownIcon :size="16" class="listbox-arrow" /> <ChevronDownIcon :size="16" class="text-secondary" />
</ListboxButton> </ListboxButton>
<transition> <transition>
<ListboxOptions class="listbox-options"> <ListboxOptions
class="absolute top-full right-0 left-0 z-1000 mt-[4px] max-h-[300px] overflow-y-auto rounded-sm border border-border bg-bg-tertiary shadow-md"
>
<ListboxOption <ListboxOption
v-for="picbed in picBedG" v-for="picbed in picBedG"
:key="picbed.type" :key="picbed.type"
v-slot="{ active, selected }" v-slot="{ active, selected }"
:value="picbed.type" :value="picbed.type"
> >
<li class="listbox-option" :class="{ active, selected }"> <li
class="flex cursor-pointer items-center justify-between px-4 py-3 text-base text-main [.active]:bg-surface-elevated [.selected]:bg-accent [.selected]:text-white"
:class="{ active, selected }"
>
<span>{{ picbed.name }}</span> <span>{{ picbed.name }}</span>
<CheckIcon v-if="selected" :size="16" /> <CheckIcon v-if="selected" :size="16" />
</li> </li>
@@ -130,19 +166,26 @@
</div> </div>
</Listbox> </Listbox>
<button v-if="choosedPicBedForQRCode.length > 0" class="copy-button" @click="handleCopyPicBedConfig"> <button
v-if="choosedPicBedForQRCode.length > 0"
class="mt-3 flex cursor-pointer items-center gap-2 rounded-sm border-none bg-accent px-4 py-2 text-base font-medium text-white hover:bg-accent-hover"
@click="handleCopyPicBedConfig"
>
<CopyIcon :size="16" /> <CopyIcon :size="16" />
{{ t('navigation.copyPicBedConfig') }} {{ t('navigation.copyPicBedConfig') }}
</button> </button>
</div> </div>
<div v-if="choosedPicBedForQRCode.length > 0" class="qr-container"> <div v-if="choosedPicBedForQRCode.length > 0" class="flex justify-center py-5">
<qrcode-vue :size="280" :value="picBedConfigString" class="qr-code" /> <qrcode-vue :size="280" :value="picBedConfigString" class="overflow-hidden shadow-sm" />
</div> </div>
</div> </div>
<div class="dialog-actions"> <div class="flex justify-end gap-3 px-4 pb-4">
<button class="cancel-button" @click="qrcodeVisible = false"> <button
class="cursor-pointer rounded-sm border border-border bg-danger/50 px-4 py-2 text-base text-main hover:bg-danger/70"
@click="qrcodeVisible = false"
>
{{ $t('navigation.close') }} {{ $t('navigation.close') }}
</button> </button>
</div> </div>
@@ -268,6 +311,11 @@ function isPathActive(path: string): boolean {
return route.path === path return route.path === path
} }
function isPicBedPathActive(type: string): boolean {
console.log('type:', type, 'route.params.type:', route.params.type)
return route.name === routerConfig.UPLOADER_CONFIG_PAGE && route.params.type === type
}
const navigationItems = computed(() => [ const navigationItems = computed(() => [
{ name: t('navigation.upload'), path: '/main-page/upload', icon: UploadIcon }, { name: t('navigation.upload'), path: '/main-page/upload', icon: UploadIcon },
{ name: t('navigation.manage'), path: '/main-page/manage-login-page', icon: BriefcaseBusiness }, { name: t('navigation.manage'), path: '/main-page/manage-login-page', icon: BriefcaseBusiness },

View File

@@ -1,464 +1,14 @@
.navigation { @import "tailwindcss" reference;
display: flex; @import "../../assets/css/theme.css" reference;
overflow: hidden; @import "../../assets/css/utilities.css" reference;
border-right: 1px solid var(--color-border-secondary);
width: 150px;
height: 100vh;
background: var(--color-background-secondary);
transition: width 0.3s ease;
flex-direction: column;
}
.navigation.collapsed {
width: 60px;
}
.title-bar {
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 1.25rem 1rem;
background: var(--color-background-secondary);
}
.navigation.collapsed .title-bar {
padding: 1rem 0.5rem;
}
.collapse-button {
position: absolute;
top: 50%;
right: 8px;
display: flex;
justify-content: center;
align-items: center;
border: none;
border-radius: 4px;
padding: 4px;
color: var(--color-text-primary);
background: transparent;
transition: all 0.2s ease;
transform: translateY(-50%);
cursor: pointer;
}
.collapse-button:hover {
color: var(--color-text-primary);
background: var(--color-surface-elevated);
}
.navigation.collapsed .collapse-button {
position: static;
transform: none;
}
.app-title {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.app-text {
font-size: 16px;
font-weight: 700;
color: var(--color-text-primary);
letter-spacing: -0.025em;
}
.app-text:hover {
cursor: pointer;
color: var(--color-accent);
}
.app-version {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 3px 8px;
font-size: 10px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-background-secondary);
}
.theme-section {
display: flex;
justify-content: center;
align-items: center;
padding: 0.75rem;
}
.nav-menu {
overflow-y: auto;
padding: 1rem 0;
min-height: 0;
flex: 1;
}
.nav-item { .nav-item {
display: flex; @apply flex items-center justify-center py-3 px-4 text-sm font-medium no-underline text-secondary gap-3 cursor-pointer transition-all duration-200 ease-apple;
justify-content: center; @apply hover:text-main hover:bg-surface;
align-items: center; @apply group-[.collapsed]:justify-center group-[.collapsed]:py-3 group-[.collapsed]:px-2 group-[.collapsed]:gap-0 ;
padding: 0.75rem 1rem; @apply [.router-link-active]:border-accent [.router-link-active]:border-r-4 [.router-link-active]:text-accent [.router-link-active]:bg-surface;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
color: var(--color-text-secondary);
transition: all 0.2s ease;
gap: 0.75rem;
cursor: pointer;
}
.navigation.collapsed .nav-item {
justify-content: center;
padding: 0.75rem 0.5rem;
gap: 0;
}
.navigation.collapsed .nav-label {
display: none;
}
.nav-item:hover {
color: var(--color-text-primary);
background: var(--color-surface);
}
.nav-item.router-link-active {
border-right: 3px solid var(--color-accent);
color: var(--color-accent);
background: var(--color-surface);
} }
.nav-icon-container { .nav-icon-container {
position: relative; @apply relative flex justify-center items-center w-[20px] h-[20px] shrink-0;
display: flex;
justify-content: center;
align-items: center;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.sidebar-footer {
border-top: 1px solid var(--color-border);
padding: 12px;
}
.footer-button {
position: fixed;
bottom: 4px;
left: 4px;
border: none;
border-radius: var(--radius-sm);
padding: 8px;
color: var(--color-text-tertiary);
background: transparent;
cursor: pointer;
}
.footer-button:hover {
color: var(--color-text-primary);
background: var(--color-surface-elevated);
}
.nav-submenu {
position: relative;
justify-content: center;
margin-top: 4px;
}
.submenu-trigger {
position: relative;
display: flex;
justify-content: center;
align-items: center;
border: none;
padding: 0.75rem 1rem;
width: 100%;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
color: var(--color-text-secondary);
background: transparent;
transition: all 0.2s ease;
gap: 0.75rem;
cursor: pointer;
}
.submenu-trigger:hover {
color: var(--color-text-primary);
background: var(--color-surface-elevated);
}
.submenu-trigger .nav-icon-container {
flex-shrink: 0;
}
.submenu-trigger span {
flex-shrink: 0;
}
.submenu-arrow {
position: absolute;
right: 1rem;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.rotate-180 {
transform: rotate(180deg);
}
.submenu-panel {
display: flex;
margin-top: 2px;
padding-left: 2.75rem;
flex-direction: column;
gap: 4px;
}
.submenu-item {
display: flex;
align-items: center;
border-radius: var(--radius-sm);
padding: 0.5rem 1rem;
font-size: 0.8125rem;
font-weight: 500;
text-decoration: none;
color: var(--color-text-secondary);
transition: all 0.2s ease;
cursor: pointer;
}
.submenu-item:hover {
color: var(--color-text-primary);
background: var(--color-surface-elevated);
}
.collapsed-picbed {
cursor: default;
}
.collapsed-picbed:hover {
color: var(--color-text-primary);
background: var(--color-surface-elevated);
}
.qr-dialog {
position: fixed;
z-index: 50;
display: flex;
justify-content: center;
align-items: center;
overflow-y: auto;
inset: 0;
}
.dialog-container {
position: fixed;
z-index: 50;
display: flex;
justify-content: center;
align-items: center;
overflow-y: auto;
padding: 16px;
min-height: 100vh;
inset: 0;
}
.dialog-panel {
overflow: visible;
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
width: 100%;
max-width: 500px;
background: var(--color-background-tertiary);
box-shadow: var(--shadow-md);
}
.dialog-title {
margin: 0;
padding: 20px 24px 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
}
.dialog-content {
padding: 20px 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
}
.listbox-container {
position: relative;
}
.listbox-button {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: 12px 16px;
width: 100%;
font-size: 14px;
color: var(--color-text-primary);
background: var(--color-surface);
transition: var(--transition);
cursor: pointer;
}
.listbox-button:hover {
border-color: var(--color-accent);
}
.placeholder {
color: var(--color-text-secondary);
}
.selected-count {
color: var(--color-text-primary);
}
.listbox-arrow {
color: var(--color-text-secondary);
}
.listbox-options {
position: absolute;
top: 100%;
right: 0;
left: 0;
z-index: 1000;
overflow-y: auto;
margin-top: 4px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
max-height: 300px;
background: var(--color-background-tertiary);
box-shadow: var(--shadow-md);
}
.listbox-option {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
font-size: 14px;
color: var(--color-text-primary);
transition: var(--transition);
cursor: pointer;
}
.listbox-option.active {
background: var(--color-surface-elevated);
}
.listbox-option.selected {
color: white;
background: var(--color-accent);
}
.copy-button {
display: flex;
align-items: center;
margin-top: 12px;
border: none;
border-radius: var(--border-radius);
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
color: white;
background: var(--color-accent);
transition: var(--transition);
gap: 8px;
cursor: pointer;
}
.copy-button:hover {
background: var(--color-accent-hover);
}
.qr-container {
display: flex;
justify-content: center;
padding: 20px 0;
}
.qr-code {
overflow: hidden;
border-radius: var(--border-radius);
box-shadow: var(--shadow-sm);
}
.dialog-actions {
display: flex;
justify-content: flex-end;
padding: 0 24px 20px;
gap: 12px;
}
.cancel-button {
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: 10px 20px;
font-size: 14px;
color: var(--color-text-primary);
background: var(--color-surface-elevated);
transition: var(--transition);
cursor: pointer;
}
.cancel-button:hover {
background: var(--color-border);
}
/* Responsive Design */
@media (width <= 768px) {
.navigation {
width: 60px;
}
.nav-label {
display: none;
}
.app-title {
display: none;
}
.collapse-button {
display: none;
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
display: none;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 0;
background: var(--color-border);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
} }

View File

@@ -34,10 +34,10 @@
<!-- Config Grid --> <!-- Config Grid -->
<div <div
class="no-scrollbar flex w-full flex-1 items-center gap-4 overflow-auto rounded-2xl border border-border-secondary px-4 py-6 shadow-md" class="no-scrollbar flex w-full flex-1 items-start gap-4 overflow-auto rounded-2xl border border-border-secondary px-4 py-6 shadow-md"
> >
<div <div
class="no-scrollbar grid h-full w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5 overflow-auto border-none p-1 max-md:grid-cols-1 max-md:gap-4 xl:grid-cols-[repeat(auto-fill,minmax(325px,1fr))]" class="no-scrollbar grid h-auto w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5 overflow-auto border-none p-1 max-md:grid-cols-1 max-md:gap-4 xl:grid-cols-[repeat(auto-fill,minmax(325px,1fr))]"
> >
<!-- Config Items --> <!-- Config Items -->
<div <div