@jianghujs/jianghu
Version:
Progressive Enterprise Framework
458 lines (435 loc) • 15.6 kB
HTML
<!-- jhMenu.html >>>>>>>>>>>>> -->
<script type="text/html" id="jh-menu-side">
<div>
<!-- 左边栏菜单 >>>>>>>>>>>>> -->
<v-navigation-drawer
v-model="isMobileMenuDrawerShown"
app
clipped
class="jh-page-nav-bar"
width="208"
floating
mobile-breakpoint="600"
:hide-overlay="!isMobile"
style="z-index: 99;"
>
<!-- 页面标题 -->
<v-toolbar-title ref="toolbarTitle" class="px-4 jh-toolbar-title">
<span class="text-h7 font-weight-bold">{{ appTitle }}</span>
</v-toolbar-title>
<v-divider class="jh-divider"></v-divider>
<!-- 菜单 -->
<v-list flat class="mx-2">
<v-list-item-group :value="currentMenuTabIndex" >
<template v-for="(item, index) in inMenuList">
<template v-if="item.children && item.children.length > 0">
<v-list-group :value="item.active" :class="{'mx-2': isMobile}">
<template v-slot:activator>
<v-list-item class="pl-0" :ripple="false">
<v-list-item-content>
<v-list-item-title>
{{ item.title }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
<v-list-item @click="openUrl(menu.path, menu.query)" v-for="menu in item.children" color="rgba(0, 0, 0, 0.87)"
:class="{'second-active': currentSecondMenuPageId === menu.pageId && currentMenuTabIndex === index}">
<v-list-item-content>
<v-list-item-title class="pl-6 pl-sm-4">{{ menu.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-group>
</template>
<v-list-item
v-else
:key="item.path"
@click="openUrl(item.path, item.query)"
:class="{'second-active': currentMenuTabIndex === index, 'mx-2': isMobile}"
>
<v-list-item-content class="pl-0">
<v-list-item-title>
{{ item.title }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-list-item-group>
</v-list>
<!-- pc端底部用户信息 -->
<template v-solt:bottom>
<div v-if="!isMobile" class="jh-user-info">
<v-divider class="jh-divider"></v-divider>
<v-menu offset-y v-model="isAvatarMenuShown" :nudge-right="208">
<!-- 头像 -->
<template v-slot:activator="{on, attrs}">
<div text v-bind="attrs" v-on="on" class="d-flex justify-space-between px-4 pt-4">
<v-icon :size="34">mdi-account-circle</v-icon>
<div class="ml-1">
<p class="jh-font-size-12 black--text mb-0">{{ userInfo.user.username }}</p>
<p class="jh-font-size-10 grey--text mb-0">{{ userInfo.user.userId }}</p>
</div>
<v-spacer></v-spacer>
<v-icon>mdi-chevron-right</v-icon>
</div>
</template>
<!-- 菜单 -->
<v-list nav dense>
<v-list-item v-for="avatarMenu of inAvatarMenuList" :key="avatarMenu.path" @click="openUrl(avatarMenu.path)">
<v-list-item-icon class="mr-1 mt-1">
<v-icon size="16" color="grey darken-3">mdi-account-cog-outline</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title color="grey darken-3">{{ avatarMenu.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item @click="logout">
<v-list-item-icon class="mr-1 mt-1">
<v-icon size="16" color="grey darken-3">mdi-logout</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title color="grey darken-3">登出</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</div>
</template>
<!-- 移动端抽屉关闭按钮 -->
<v-btn
elevation="0" color="success" fab absolute top left small tile class="jh-menu-side-drawer-close-float-btn"
@click="isMobileMenuDrawerShown = !isMobileMenuDrawerShown" v-if="isMobile">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-navigation-drawer>
<!-- <<<<<<<<<<<<< 左边栏菜单 -->
<!-- 手机端页面头部 >>>>>>>>>>>>> -->
<v-app-bar
app
clipped-left
height="60"
class="jh-page-header px-8"
style="z-index: 50;"
flat
v-if="isMobile"
>
<!-- 左侧菜单开启按钮 -->
<v-app-bar-nav-icon color="primary" @click.stop="isMobileMenuDrawerShown = !isMobileMenuDrawerShown"></v-app-bar-nav-icon>
<!-- 页面标题 -->
<v-toolbar-title ref="toolbarTitle" class="mr-8 pl-0">
<span class="text-h7 font-weight-bold">{{ appTitle }}</span>
</v-toolbar-title>
<!-- 用户信息 -->
<div style="white-space: nowrap">
<v-menu offset-y v-model="isAvatarMenuShown">
<template v-slot:activator="{on, attrs}">
<!-- 头像 -->
<div text v-bind="attrs" v-on="on" class="jh-avatar-menu-btn px-1">
<v-icon :size="32">mdi-account-circle</v-icon>
</div>
</template>
<v-list nav dense>
<!-- 下拉菜单 -->
<v-list-item>
<v-list-item-content>
<p class="jh-font-size-12 black--text mb-0">{{ userInfo.user.username }}</p>
<p class="jh-font-size-10 grey--text mb-0">{{ userInfo.user.userId }}</p>
</v-list-item-content>
</v-list-item>
<v-list-item v-for="avatarMenu of inAvatarMenuList" :key="avatarMenu.path" @click="openUrl(avatarMenu.path)">
<v-list-item-icon class="mr-1 mt-1">
<v-icon size="16" color="grey darken-3">mdi-account-cog-outline</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title color="grey darken-3">{{ avatarMenu.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item @click="logout">
<v-list-item-icon class="mr-1 mt-1">
<v-icon size="16" color="grey darken-3">mdi-logout</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title color="grey darken-3">登出</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</div>
</v-app-bar>
<!-- <<<<<<<<<<<<< 手机端页面头部 -->
</div>
</script>
<script>
Vue.component('jh-menu-side', {
template: "#jh-menu-side",
vueComponent: 'jh-menu-side',
vuetify: new Vuetify(),
data() {
return {
isMobile: window.innerWidth < 600,
tabsMaxWidth: 'calc(100vw - 353px)',
appDirectoryLink: '<$ ctx.app.config.appDirectoryLink $>',
appType: '<$ ctx.app.config.appType $>',
appTitle: '<$ ctx.app.config.appTitle $>',
userInfo: window.userInfo,
// 弹出菜单数据
isMobileMenuDrawerShown: !(window.innerWidth < 600),
isAvatarMenuShown: false,
// 用户菜单
menuSort: null,
menuGroup: null,
inMenuList: [],
inAvatarMenuList: [],
inMenuShownStatusList: [],
currentMenuTabIndex: -1,
// 当前二级菜单pageID
currentSecondMenuPageId: null,
};
},
watch: {
isMobileMenuDrawerShown(value) {
this.setCurrentMenuItem();
},
inMenuShownStatusList: {
deep: true,
handler(value, oValue) {
if (this.currentMenuTabIndex > -1 && !value[this.currentMenuTabIndex]) {
this.setCurrentMenuItem();
}
}
}
},
created() {
this.computedPageId();
this.groupMenuList();
this.computedMenuList();
this.computedMultiAppMenuList();
this.computedAvatarMenuList();
this.setCurrentMenuItem();
this.getTabsMaxWidth();
},
methods: {
// 获取当前页面PageId
computedPageId() {
const urlPathList = window.location.pathname.split('/');
this.currentPageId = urlPathList && urlPathList[urlPathList.length - 1];
},
// 动态计算菜单栏目的最大宽度,按照实际的标题宽度计算
getTabsMaxWidth() {
this.$nextTick(() => {
if (this.$refs.toolbarTitle) {
this.tabsMaxWidth = 'calc(100vw - ' + (this.$refs.toolbarTitle.offsetWidth + 195) + 'px)';
}
})
},
// 跳转链接
openUrl(url, queryParams) {
if (queryParams) {
const queryStrings = Object.keys(queryParams)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(queryParams[k]))
.join('&');
window.location.href = url + '?' + queryStrings;
} else {
window.location.href = url;
}
},
// 定位当前页面在属于哪个菜单
setCurrentMenuItem() {
// 一级菜单定位
const index = _.findIndex(this.inMenuList, { path: location.pathname });
if (index > -1) {
// 设置标题、菜单选中
this.currentMenuTabIndex = index;
const currentPageTitle = this.inMenuList[index].title;
document.title = this.appTitle + (currentPageTitle ? " - " + currentPageTitle : "");
return;
}
// 子菜单定位
this.inMenuList.some((item, index) => {
const findSecondMenuSuccess = item.children && item.children.some(child => {
if (child.pageId === this.currentPageId) {
this.currentMenuTabIndex = index;
this.currentSecondMenuPageId = child.pageId;
item.active = true;
const currentPageTitle = `[${item.title}]${child.title}`;
document.title = this.appTitle + (currentPageTitle ? " - " + currentPageTitle : "");
return true;
}
return false;
})
return findSecondMenuSuccess;
})
},
// 菜单数据分组、排序处理
groupMenuList() {
// 菜单元数据处理
const menuListResource = _
.chain(this.userInfo.allowPageList)
.filter(page => _.includes(['showInMenu', 'link'], page.pageType) || (_.includes(['dynamicInMenu', 'avatarInMenu'], page.pageType) && this.currentPageId === page.pageId))
.value();
// 分组菜单数据
this.menuGroup = _.groupBy(menuListResource, function (item) {
if (item.pageName.startsWith("[")) {
const pageName = item.pageName.substring(1);
const pageNameArr = pageName.split("]");
return `${pageNameArr[0]}`;
}
return `${item.pageName}`;
});
// 二级菜单排序
for (let key in this.menuGroup) {
this.menuGroup[key] = _.orderBy(this.menuGroup[key], ['sort'], ['asc'])
}
// 一级菜单排序
this.menuSort = Object.keys(this.menuGroup).sort((a, b) => {
return this.menuGroup[a][0].sort - this.menuGroup[b][0].sort
})
},
// 菜单构建
computedMenuList() {
this.inMenuList = this.menuSort.map(key => {
const page = this.menuGroup[key];
// 没有子菜单,直接构建菜单
if (page.length === 1) {
const menu = page[0];
return {
path: menu.pageType === 'link' ? menu.pageFile : `/${window.appInfo.appId}/page/${menu.pageId}`,
pageId: menu.pageId,
title: menu.pageName,
children: null
};
}
// 二级的菜单创建
const children = page.map(child => {
let title = '';
if (child.pageName.startsWith("[")) {
const pageName = child.pageName.substring(1);
title = pageName.split("]")[1];
} else if (child.pageName.includes("|")) {
title = child.pageName.split('|')[1];
} else {
title = child.pageName;
}
return {
path: child.pageType === 'link' ? child.pageFile : `/${window.appInfo.appId}/page/${child.pageId}`,
pageId: child.pageId,
title,
children: null
};
});
return {
path: null,
pageId: null,
title: key,
children
}
})
},
// 多应用项目,添加回到目录
computedMultiAppMenuList() {
if (window.appInfo.appId !== 'directory') {
const appType = '<$ ctx.app.config.appType $>';
if (appType === 'multiApp' && this.appDirectoryLink) {
this.inMenuList.unshift({
path: this.appDirectoryLink,
pageId: 'directory',
title: '回到目录',
children: null
});
}
}
},
// 头像菜单构建
computedAvatarMenuList() {
this.inAvatarMenuList = _
.chain(this.userInfo.allowPageList)
.filter(['pageType', 'showInAvatarMenu'])
.map((page) => {
return {
path: `/${window.appInfo.appId}/page/${page.pageId}`,
title: page.pageName,
sort: parseInt(page.sort)
};
})
.orderBy(['sort'], ['asc'])
.value();
},
// 登出
async logout() {
try {
await window.jianghuAxios({
data: {
appData: {
pageId: 'allPage',
actionId: 'logout'
}
}
})
vtoast.success('注销成功');
this.routeToLoginPage();
} catch (error) {
vtoast.fail(error.errorReason);
this.routeToLoginPage();
}
},
// 导航到登陆页
routeToLoginPage() {
localStorage.removeItem(`${window.appInfo.authTokenKey}_authToken`);
setTimeout(() => {
location.href = `/${window.appInfo.appId}/page/login`;
}, 700);
}
},
});
</script>
<style>
.jh-user-info {
position: fixed;
bottom: 16px;
width: 100%;
white-space: nowrap;
}
.jh-page-nav-bar .v-list-item,
.jh-page-nav-bar .v-list-group__header {
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
}
.jh-page-nav-bar .v-list-group__header .v-list-item {
border-bottom: none;
}
@media (max-width: 600px) {
body .jh-page-nav-bar .v-list-group .v-list-group__header {
padding: 0 ;
}
body .jh-page-nav-bar .v-list-item {
border-top: none;
}
}
.second-active .v-list-item__title {
color: var(--cPrimaryColor) ;
}
.jh-avatar-menu-btn {
display: flex;
align-items: center;
cursor: pointer;
transition: all .3s;
border-radius: 5px;
}
.jh-avatar-menu-btn:hover {
opacity: 0.8;
}
.second-active {
caret-color: var(--cPrimaryColor) ;
background-color: rgba(76, 175, 80, 0.1) ;
}
.jh-toolbar-title {
height: 60px;
line-height: 60px;
}
.jh-menu-side-drawer-close-float-btn {
top: 120px ;
right: -40px;
position: fixed;
left: auto ;
}
</style>
<!-- <<<<<<<<<<<<< jhMenu.html -->