429 lines
10 KiB
Vue
429 lines
10 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>
|
|
<!-- 移动端遮罩层 -->
|
|
<div
|
|
v-if="isMobile && !sidebarCollapsed && !isSignupFlow && !isHideSidebar && session.user"
|
|
class="mobile-overlay"
|
|
@click="sidebarCollapsed = true"
|
|
></div>
|
|
|
|
<n-layout-sider
|
|
v-if="!isSignupFlow && !isHideSidebar && session.user"
|
|
bordered
|
|
collapse-mode="width"
|
|
:collapsed-width="64"
|
|
:width="240"
|
|
v-model:collapsed="sidebarCollapsed"
|
|
:show-trigger="!isMobile"
|
|
:responsive="true"
|
|
:breakpoint="768"
|
|
class="app-sidebar-sider"
|
|
@collapse="sidebarCollapsed = true"
|
|
@expand="sidebarCollapsed = false"
|
|
>
|
|
<AppSidebar :collapsed="sidebarCollapsed" @menu-select="handleMenuSelect" />
|
|
</n-layout-sider>
|
|
<n-layout class="h-full">
|
|
<!-- 移动端顶部导航栏 -->
|
|
<div
|
|
v-if="isMobile && !isSignupFlow && !isHideSidebar && session.user"
|
|
class="mobile-header"
|
|
>
|
|
<div class="mobile-header-content">
|
|
<JLogo class="mobile-logo" />
|
|
<n-dropdown
|
|
:options="mobileDropdownOptions"
|
|
:show-arrow="true"
|
|
trigger="click"
|
|
@select="handleMobileDropdownSelect"
|
|
placement="bottom-start"
|
|
>
|
|
<div class="mobile-user-info-wrapper">
|
|
<div class="mobile-user-info">
|
|
{{ teamUserText }}
|
|
</div>
|
|
<n-icon class="mobile-chevron-icon">
|
|
<ChevronDown />
|
|
</n-icon>
|
|
</div>
|
|
</n-dropdown>
|
|
<n-button
|
|
quaternary
|
|
circle
|
|
@click="sidebarCollapsed = false"
|
|
class="mobile-menu-button"
|
|
>
|
|
<template #icon>
|
|
<n-icon><MenuIcon /></n-icon>
|
|
</template>
|
|
</n-button>
|
|
</div>
|
|
</div>
|
|
<div class="w-full h-full overflow-auto" id="scrollContainer">
|
|
<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" />
|
|
<!-- 切换团队对话框 -->
|
|
<SwitchTeamDialog v-model="showTeamSwitcher" />
|
|
</div>
|
|
</n-config-provider>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { defineAsyncComponent, computed, watch, ref, provide, onMounted, onUnmounted, h, getCurrentInstance } from 'vue';
|
|
import { NLayout, NLayoutSider, NConfigProvider, NButton, NIcon, NDropdown } 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';
|
|
import JLogo from '@/components/icons/JLogo.vue';
|
|
import MenuIcon from '~icons/lucide/menu';
|
|
import ChevronDown from '~icons/lucide/chevron-down';
|
|
|
|
const AppSidebar = defineAsyncComponent(
|
|
() => import('./components/AppSidebar.vue'),
|
|
);
|
|
|
|
const SwitchTeamDialog = defineAsyncComponent(
|
|
() => import('./components/SwitchTeamDialog.vue'),
|
|
);
|
|
|
|
const route = useRoute();
|
|
const team = getTeam();
|
|
const instance = getCurrentInstance();
|
|
const showTeamSwitcher = ref(false);
|
|
|
|
// Naive UI 主题配置 - 使用标准方式配置 tooltip 和 menu
|
|
const themeOverrides = {
|
|
Tooltip: {
|
|
color: '#fafafa',
|
|
textColor: '#4a5568',
|
|
padding: '6px 12px',
|
|
borderRadius: '8px',
|
|
},
|
|
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 isMobile = ref(false);
|
|
|
|
// 侧边栏折叠状态
|
|
const sidebarCollapsed = ref(false);
|
|
|
|
// 团队用户文本(用于移动端显示)
|
|
const teamUserText = computed(() => {
|
|
if (team?.get?.loading) {
|
|
return '加载中...';
|
|
}
|
|
return team?.pg?.user || '';
|
|
});
|
|
|
|
// 响应式:在移动端默认折叠
|
|
const checkMobile = () => {
|
|
const wasMobile = isMobile.value;
|
|
isMobile.value = window.innerWidth < 768;
|
|
|
|
// 切换到移动端时自动折叠
|
|
if (!wasMobile && isMobile.value) {
|
|
sidebarCollapsed.value = true;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
checkMobile();
|
|
window.addEventListener('resize', checkMobile);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', checkMobile);
|
|
});
|
|
|
|
// 处理菜单选择 - 移动端自动关闭侧边栏
|
|
const handleMenuSelect = () => {
|
|
if (isMobile.value) {
|
|
sidebarCollapsed.value = true;
|
|
}
|
|
};
|
|
|
|
// 移动端下拉菜单选项
|
|
const mobileDropdownOptions = computed(() => {
|
|
const options = [];
|
|
const teamData = team?.pg;
|
|
if (
|
|
teamData &&
|
|
(teamData.valid_teams?.length > 1 || teamData.is_desk_user)
|
|
) {
|
|
options.push({
|
|
label: '切换团队',
|
|
key: 'switch-team',
|
|
});
|
|
}
|
|
options.push({
|
|
label: '退出登录',
|
|
key: 'logout',
|
|
});
|
|
return options;
|
|
});
|
|
|
|
// 处理移动端下拉菜单选择
|
|
const handleMobileDropdownSelect = (key) => {
|
|
if (key === 'switch-team') {
|
|
showTeamSwitcher.value = true;
|
|
} else if (key === 'logout') {
|
|
if (session) {
|
|
session.logout.submit();
|
|
}
|
|
}
|
|
};
|
|
|
|
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;
|
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
z-index: 1000 !important;
|
|
}
|
|
|
|
.app-sidebar-sider .n-layout-sider-trigger {
|
|
background: #fff !important;
|
|
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
transition: background-color 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;
|
|
}
|
|
|
|
.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 {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.app-sidebar-sider {
|
|
height: 100% !important;
|
|
min-height: 100vh !important;
|
|
}
|
|
|
|
.mobile-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 998;
|
|
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.mobile-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 999;
|
|
background: #fff;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.mobile-header-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
padding: 12px 16px;
|
|
gap: 12px;
|
|
}
|
|
|
|
.mobile-logo {
|
|
width: 28px;
|
|
height: 28px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.mobile-user-info-wrapper {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
cursor: pointer;
|
|
padding: 6px 8px;
|
|
border-radius: 6px;
|
|
transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
flex-shrink: 1;
|
|
min-width: 0;
|
|
max-width: calc(100% - 80px);
|
|
}
|
|
|
|
.mobile-user-info-wrapper:hover {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.mobile-user-info-wrapper:active {
|
|
background: rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.mobile-user-info {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #1a1a1a;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
flex-shrink: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.mobile-chevron-icon {
|
|
font-size: 16px;
|
|
color: #8b8e95;
|
|
flex-shrink: 0;
|
|
transition: color 0.2s ease, transform 0.2s ease;
|
|
}
|
|
|
|
.mobile-user-info-wrapper:hover .mobile-chevron-icon {
|
|
color: #4a5568;
|
|
}
|
|
|
|
.mobile-menu-button {
|
|
flex-shrink: 0;
|
|
margin-left: auto;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.app-sidebar-sider {
|
|
position: fixed !important;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100vh !important;
|
|
z-index: 1000 !important;
|
|
width: 280px !important;
|
|
max-width: 85vw;
|
|
transform: translateX(-100%);
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
}
|
|
|
|
.app-sidebar-sider:not(.n-layout-sider--collapsed) {
|
|
transform: translateX(0) !important;
|
|
}
|
|
|
|
.n-layout {
|
|
margin-left: 0 !important;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.app-sidebar-sider {
|
|
position: relative !important;
|
|
transform: none !important;
|
|
}
|
|
|
|
.mobile-header {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-overlay {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|