2025-12-28 02:25:18 +08:00

224 lines
6.4 KiB
Vue

<template>
<n-config-provider :theme-overrides="themeOverrides" class="h-full">
<div class="relative flex h-full flex-col">
<n-layout class="h-full" has-sider>
<n-layout-sider
v-if="!isSignupFlow && !$isMobile && !isHideSidebar && $session.user"
bordered
collapse-mode="width"
:collapsed-width="64"
:width="240"
v-model:collapsed="sidebarCollapsed"
:show-trigger="true"
:responsive="true"
:breakpoint="768"
class="app-sidebar-sider"
@collapse="sidebarCollapsed = true"
@expand="sidebarCollapsed = false"
>
<AppSidebar :collapsed="sidebarCollapsed" />
</n-layout-sider>
<n-layout class="h-full">
<div class="w-full h-full overflow-auto" id="scrollContainer">
<MobileNav
v-if="!isSignupFlow && $isMobile && !isHideSidebar && $session.user"
/>
<div
v-if="
!isSignupFlow &&
!isSiteLogin &&
!$session.user &&
!$route.meta.isLoginPage
"
class="border bg-red-200 px-5 py-3 text-base text-red-900"
>
You are not logged in.
<router-link to="/login" class="underline">Login</router-link> to
access dashboard.
</div>
<router-view />
</div>
</n-layout>
</n-layout>
<Toaster position="top-right" />
<component v-for="dialog in dialogs" :is="dialog" :key="dialog.id" />
</div>
</n-config-provider>
</template>
<script setup>
import { defineAsyncComponent, computed, watch, ref, provide, onMounted } from 'vue';
import { NLayout, NLayoutSider, NConfigProvider } from 'naive-ui';
import { Toaster } from 'vue-sonner';
import { dialogs } from './utils/components';
import { useRoute } from 'vue-router';
import { getTeam } from './data/team';
import { session } from './data/session.js';
const AppSidebar = defineAsyncComponent(
() => import('./components/AppSidebar.vue'),
);
const MobileNav = defineAsyncComponent(
() => import('./components/MobileNav.vue'),
);
const route = useRoute();
const team = getTeam();
// Naive UI 主题配置 - 使用标准方式配置 tooltip 和 menu
const themeOverrides = {
Tooltip: {
color: '#fafafa',
textColor: '#4a5568',
padding: '6px 12px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
},
Menu: {
// 使用主题系统配置菜单样式,减少深度选择器
itemTextColor: '#4a5568',
itemTextColorHover: '#4a5568',
itemTextColorActive: '#18a058',
itemTextColorChildActive: '#18a058',
itemColorHover: 'rgba(0, 0, 0, 0.04)',
itemColorActive: 'rgba(24, 160, 88, 0.1)',
itemColorActiveHover: 'rgba(24, 160, 88, 0.15)',
itemBorderRadius: '8px',
itemPadding: '10px 12px',
itemIconColor: '#8b8e95',
itemIconColorHover: '#18a058',
itemIconColorActive: '#18a058',
itemIconColorChildActive: '#18a058',
itemIconSize: '18px',
fontSize: '14px',
},
};
// 侧边栏折叠状态
const sidebarCollapsed = ref(false);
// 响应式:在移动端默认折叠
onMounted(() => {
const checkMobile = () => {
if (window.innerWidth < 768) {
sidebarCollapsed.value = true;
}
};
checkMobile();
window.addEventListener('resize', checkMobile);
});
const isHideSidebar = computed(() => {
const alwaysHideSidebarRoutes = [
'Site Login',
'SignupLoginToSite',
'SignupSetup',
];
const alwaysHideSidebarPaths = ['/dashboard/site-login'];
if (!session.user) return false;
if (
alwaysHideSidebarRoutes.includes(route.name) ||
alwaysHideSidebarPaths.includes(window.location.pathname)
)
return true;
return (
route.meta.hideSidebar && session.user && team?.pg?.hide_sidebar === true
);
});
const isSignupFlow = ref(
window.location.pathname.startsWith('/dashboard/create-site') ||
window.location.pathname.startsWith('/dashboard/setup-account') ||
window.location.pathname.startsWith('/dashboard/site-login') ||
window.location.pathname.startsWith('/dashboard/signup'),
);
const isSiteLogin = ref(window.location.pathname.endsWith('/site-login'));
watch(
() => route.name,
() => {
isSignupFlow.value =
window.location.pathname.startsWith('/dashboard/create-site') ||
window.location.pathname.startsWith('/dashboard/setup-account') ||
window.location.pathname.startsWith('/dashboard/site-login') ||
window.location.pathname.startsWith('/dashboard/signup');
},
);
provide('team', team);
provide('session', session);
</script>
<style src="../src/assets/style.css"></style>
<style>
/* 侧边栏整体样式优化 - 平滑过渡 */
.app-sidebar-sider {
background: #fafafa !important;
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
/* 使用平滑的缓动函数,避免抖动 */
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.app-sidebar-sider:not(.n-layout-sider--collapsed) {
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.04) !important;
}
/* 触发器样式优化 - 平滑过渡 */
.app-sidebar-sider .n-layout-sider-trigger {
background: #fff !important;
border: 1px solid rgba(0, 0, 0, 0.08) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
/* 使用平滑的缓动函数,避免抖动 */
transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
width: 32px !important;
height: 32px !important;
border-radius: 50% !important;
right: -16px !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.app-sidebar-sider .n-layout-sider-trigger:hover {
background: #f5f5f5 !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
.app-sidebar-sider .n-layout-sider-trigger:active {
background: #eeeeee !important;
}
.app-sidebar-sider .n-layout-sider-trigger .n-base-icon {
color: #666;
font-size: 14px;
/* 移除旋转动画,避免抖动 */
transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
/* 确保侧边栏内容正确显示 */
.app-sidebar-sider .n-layout-sider-scroll-container {
height: 100%;
overflow: hidden;
/* 添加内容过渡效果 */
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
/* 确保 n-config-provider 占满高度 */
.n-config-provider {
height: 100%;
display: flex;
flex-direction: column;
}
/* 确保侧边栏占满高度 */
.app-sidebar-sider {
height: 100% !important;
min-height: 100vh !important;
}
</style>