style(dashboard): restore sidebar rail design

This commit is contained in:
Fridemn
2026-07-13 12:56:17 +08:00
parent b5f2a86899
commit a5ba828019
6 changed files with 272 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
/* Auto-generated MDI subset 273 icons */
/* Auto-generated MDI subset 271 icons */
/* Do not edit manually. Run: pnpm run subset-icons */
@font-face {
@@ -324,10 +324,6 @@
content: "\F0CFD";
}
.mdi-cursor-move::before {
content: "\F01BE";
}
.mdi-database::before {
content: "\F01BC";
}
@@ -404,10 +400,6 @@
content: "\F0209";
}
.mdi-eye-outline::before {
content: "\F06D0";
}
.mdi-eyedropper::before {
content: "\F020A";
}

View File

@@ -1,17 +1,15 @@
<script setup>
import { useI18n } from '@/i18n/composables';
import { useCustomizerStore } from '@/stores/customizer';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const props = defineProps({ item: Object, level: Number });
const props = defineProps({ item: Object, level: Number, rail: Boolean });
const { t } = useI18n();
const customizer = useCustomizerStore();
const route = useRoute();
const itemStyle = computed(() => {
const lvl = props.level ?? 0;
const indent = customizer.mini_sidebar ? '0px' : `${lvl * 24}px`;
const indent = props.rail ? '0px' : `${lvl * 24}px`;
return { '--indent-padding': indent };
});
@@ -22,7 +20,11 @@ const isItemActive = computed(() => {
const [path, hash] = props.item.to.split('#');
return route.path === path && route.hash === `#${hash}`;
}
return route.path === props.item.to;
const targetPath = props.item.to.replace(/\/$/, '') || '/';
if (targetPath === '/') {
return route.path === targetPath;
}
return route.path === targetPath || route.path.startsWith(`${targetPath}/`);
});
const itemTitle = computed(() => {
@@ -33,9 +35,19 @@ const itemTitle = computed(() => {
</script>
<template>
<v-list-group v-if="item.children" :value="item.title" :class="{ 'group-bordered': customizer.mini_sidebar }">
<template v-slot:activator="{ props }">
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
<v-list-group v-if="item.children" :value="item.title" :class="{ 'rail-group': rail }">
<template v-slot:activator="{ props: groupProps }">
<v-tooltip v-if="rail" location="right" :text="itemTitle" open-delay="180">
<template v-slot:activator="{ props: tooltipProps }">
<v-list-item v-bind="{ ...groupProps, ...tooltipProps }" rounded class="mb-1" color="secondary"
:prepend-icon="item.icon" :style="{ '--indent-padding': '0px' }" :aria-label="itemTitle">
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
{{ itemTitle }}
</v-list-item-title>
</v-list-item>
</template>
</v-tooltip>
<v-list-item v-else v-bind="groupProps" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
:style="{ '--indent-padding': '0px' }">
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
{{ itemTitle }}
@@ -45,10 +57,33 @@ const itemTitle = computed(() => {
<!-- children -->
<template v-for="(child, index) in item.children" :key="child.title || child.to || `child-${index}`">
<NavItem :item="child" :level="(level || 0) + 1" />
<NavItem :item="child" :level="(level || 0) + 1" :rail="rail" />
</template>
</v-list-group>
<v-tooltip v-else-if="rail" location="right" :text="itemTitle" open-delay="180">
<template v-slot:activator="{ props: tooltipProps }">
<v-list-item v-bind="tooltipProps" :to="item.type === 'external' ? '' : item.to"
:href="item.type === 'external' ? item.to : ''" :active="isItemActive" rounded class="mb-1"
color="secondary" :disabled="item.disabled" :target="item.type === 'external' ? '_blank' : ''"
:style="itemStyle" :aria-label="itemTitle">
<template v-slot:prepend>
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
</template>
<v-list-item-title style="font-size: 14px;">{{ itemTitle }}</v-list-item-title>
<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
{{ item.subCaption }}
</v-list-item-subtitle>
<template v-slot:append v-if="item.chip">
<v-chip :color="item.chipColor" class="sidebarchip hide-menu" :size="item.chipIcon ? 'small' : 'default'"
:variant="item.chipVariant" :prepend-icon="item.chipIcon">
{{ item.chip }}
</v-chip>
</template>
</v-list-item>
</template>
</v-tooltip>
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''"
:active="isItemActive" rounded class="mb-1" color="secondary" :disabled="item.disabled"
:target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
@@ -69,10 +104,16 @@ const itemTitle = computed(() => {
</template>
<style>
/* 在折叠(mini)状态下,分组展开时给整个分组(母项+子项)加边框以便区分 */
.group-bordered.v-list-group--open {
border: 2px solid rgba(var(--v-theme-borderLight), 0.35);
border-radius: 8px;
background: rgba(var(--v-theme-borderLight), 0.04);
.rail-group {
border-radius: 12px;
transition: background-color 0.18s ease;
}
.rail-group.v-list-group--open {
background: rgba(var(--v-theme-primary), 0.06);
}
.rail-group.v-list-group--open > .v-list-group__items {
padding-bottom: 2px;
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
import { ref, shallowRef, computed, onMounted, onUnmounted, watch } from 'vue';
import { useCustomizerStore } from '../../../stores/customizer';
import { useI18n } from '@/i18n/composables';
import sidebarItems, { MORE_GROUP_KEY } from './sidebarItem';
@@ -108,8 +108,11 @@ const maxSidebarWidth = 300;
const isResizing = ref(false);
const isMobile = window.innerWidth < 768;
const isRailSidebar = computed(() => !isMobile && customizer.mini_sidebar);
if (isMobile) {
customizer.Sidebar_drawer = false;
} else {
customizer.Sidebar_drawer = true;
}
function startSidebarResize(event) {
@@ -156,23 +159,39 @@ function startSidebarResize(event) {
app
class="leftSidebar"
:width="sidebarWidth"
:rail="customizer.mini_sidebar"
:rail="isRailSidebar"
>
<div class="sidebar-container">
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': customizer.mini_sidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
<div v-if="!isRailSidebar" class="sidebar-brand">
<img
class="sidebar-brand-logo"
src="@/assets/images/plugin_icon.png"
alt="AstrBot logo"
>
<div class="sidebar-brand-copy">
<span class="sidebar-brand-title">AstrBot</span>
<span class="sidebar-brand-subtitle">WebUI</span>
</div>
</div>
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': isRailSidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
<template v-for="(item, i) in sidebarMenu" :key="item.title || item.to || `sidebar-item-${i}`">
<NavItem :item="item" class="leftPadding" />
<NavItem :item="item" class="leftPadding" :rail="isRailSidebar" />
</template>
</v-list>
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
<v-btn class="sidebar-footer-btn" size="small" variant="tonal" color="primary" to="/settings" prepend-icon="mdi-cog">
{{ t('core.navigation.settings') }}
<div class="sidebar-footer">
<v-btn class="sidebar-footer-btn" :class="{ 'sidebar-footer-icon-btn': isRailSidebar }" :size="isRailSidebar ? 'default' : 'small'"
variant="text" to="/settings"
:prepend-icon="isRailSidebar ? undefined : 'mdi-cog'" :aria-label="t('core.navigation.settings')">
<v-icon v-if="isRailSidebar" icon="mdi-cog" />
<template v-else>{{ t('core.navigation.settings') }}</template>
<v-tooltip v-if="isRailSidebar" activator="parent" location="right" :text="t('core.navigation.settings')" open-delay="180" />
</v-btn>
</div>
</div>
<div
v-if="!customizer.mini_sidebar && customizer.Sidebar_drawer"
v-if="!isRailSidebar && !isMobile && customizer.Sidebar_drawer"
class="sidebar-resize-handle"
@mousedown="startSidebarResize"
:class="{ 'resizing': isResizing }"
@@ -224,4 +243,75 @@ function startSidebarResize(event) {
.leftSidebar .v-navigation-drawer__content {
position: relative;
}
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer {
align-items: stretch;
padding: 10px 16px 16px !important;
border-top: 1px solid rgba(var(--v-theme-borderLight), 0.35);
}
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn {
width: 100% !important;
max-width: none !important;
min-height: 40px;
justify-content: flex-start !important;
border-radius: 12px !important;
color: rgba(var(--v-theme-on-surface), 0.76);
font-weight: 500;
letter-spacing: 0;
padding-inline: 12px !important;
transition:
background-color 0.18s ease,
color 0.18s ease;
}
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn:hover,
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn.v-btn--active {
color: rgb(var(--v-theme-primary));
background: rgba(var(--v-theme-primary), 0.09);
}
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn :deep(.v-btn__content) {
justify-content: flex-start;
gap: 8px;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 10px;
min-height: 64px;
padding: 14px 18px 10px;
border-bottom: 1px solid rgba(var(--v-theme-borderLight), 0.35);
flex-shrink: 0;
}
.sidebar-brand-logo {
width: 36px;
height: 36px;
object-fit: contain;
flex: 0 0 auto;
}
.sidebar-brand-copy {
display: flex;
flex-direction: column;
min-width: 0;
line-height: 1.1;
}
.sidebar-brand-title {
color: rgb(var(--v-theme-on-surface));
font-size: 18px;
font-weight: 800;
white-space: nowrap;
}
.sidebar-brand-subtitle {
margin-top: 3px;
color: rgba(var(--v-theme-on-surface), 0.58);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
</style>

View File

@@ -65,6 +65,101 @@
.leftPadding {
margin-left: 0px;
}
.listitem {
padding: 10px !important;
.v-list-group__items .v-list-item,
.v-list-item {
width: 56px;
min-width: 56px;
max-width: 56px;
min-height: 50px;
margin-bottom: 8px !important;
margin-inline: auto;
padding-inline: 0 !important;
grid-template-areas: "prepend" !important;
grid-template-columns: 1fr !important;
justify-content: center;
place-items: center;
}
.rail-group {
width: 56px;
margin-bottom: 8px;
margin-inline: auto;
padding: 4px 0 2px;
.v-list-item {
width: 56px;
min-width: 56px;
max-width: 56px;
margin-bottom: 6px !important;
}
}
.v-list-item__prepend {
grid-area: prepend;
width: 100%;
min-width: 0;
padding: 0 !important;
justify-content: center;
margin-inline-end: 0;
.v-icon {
margin: 0 !important;
}
}
.v-list-item__spacer,
.v-list-item__append,
.v-list-item__content {
display: none;
}
}
.sidebar-container {
align-items: center;
}
.sidebar-footer {
width: 100%;
padding: 12px 10px 18px !important;
gap: 8px;
.sidebar-footer-icon-btn {
width: 56px !important;
min-width: 56px !important;
max-width: 56px !important;
height: 52px !important;
border-radius: 14px !important;
color: rgba(var(--v-theme-on-surface), 0.82);
margin-bottom: 0 !important;
padding: 0 !important;
justify-content: center !important;
text-align: center;
transition:
background-color 0.18s ease,
color 0.18s ease;
.v-btn__content {
width: 100%;
justify-content: center;
}
.v-icon {
font-size: 22px;
margin: 0 !important;
opacity: 0.98;
}
&:hover,
&.v-btn--active {
color: rgb(var(--v-theme-primary));
background: rgba(var(--v-theme-primary), 0.09);
}
}
}
}
@media only screen and (min-width: 1170px) {
.mini-sidebar {
@@ -122,3 +217,26 @@
}
}
}
.v-navigation-drawer--rail.leftSidebar {
.sidebar-container .sidebar-footer {
.sidebar-footer-btn.sidebar-footer-icon-btn {
justify-content: center !important;
text-align: center;
.v-btn__content {
justify-content: center;
}
}
}
}
@media (prefers-reduced-motion: reduce) {
.leftSidebar {
.rail-group,
.sidebar-footer-btn,
.sidebar-footer-icon-btn {
transition: none !important;
}
}
}