feat: add horizontal nav (#45)

* feat: add horizontal nav

* workflow: update linter.yml

* fix: update

* fix: update

* fix: update

* Rename Link.vue to link.vue

* Rename SidebarItem.vue to sidebarItem.vue

* Rename Logo.vue to logo.vue

* Rename AppMain.vue to appMain.vue

* Rename Navbar.vue to navbar.vue

* fix: update

* fix: update

* fix: update

* workflow: update linter.yml

* fix: update

* chore: update

* workflow: update

* fix: update

* fix: update

* fix: update

* perf: 外链功能实现方式改变

* style: update nav style

* perf: 优化国际化

* fix: update

* fix: update
This commit is contained in:
啝裳
2021-09-29 01:55:56 +08:00
committed by GitHub
parent e86757e30e
commit fd8de45bff
31 changed files with 1314 additions and 682 deletions

View File

@@ -1,5 +0,0 @@
export { default as Navbar } from "./Navbar.vue";
export { default as Sidebar } from "./sidebar/index.vue";
export { default as AppMain } from "./AppMain.vue";
export { default as setting } from "./setting/index.vue";
export { default as tag } from "./tag/index.vue";

View File

@@ -8,98 +8,84 @@
<Breadcrumb class="breadcrumb-container" />
<div class="right-menu">
<div class="vertical-header-right">
<!-- 全屏 -->
<screenfull v-show="!deviceDetection()" />
<!-- 国际化 -->
<div
v-show="!deviceDetection()"
class="inter"
:title="currentLocale ? '中文' : 'English'"
@click="toggleLang"
>
<img :src="currentLocale ? ch : en" />
</div>
<i
class="el-icon-setting hsset"
:title="$t('message.hssystemSet')"
@click="onPanel"
></i>
<el-dropdown trigger="click">
<iconinternationality />
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="{
background: locale === 'zh' ? '#1b2a47' : '',
color: locale === 'zh' ? '#f4f4f5' : '#000'
}"
@click="translationCh"
>简体中文</el-dropdown-item
>
<el-dropdown-item
:style="{
background: locale === 'en' ? '#1b2a47' : '',
color: locale === 'en' ? '#f4f4f5' : '#000'
}"
@click="translationEn"
>English</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登陆 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img :src="favicon" />
<img
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
/>
<p>{{ usename }}</p>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-menu class="logout">
<el-dropdown-item icon="el-icon-switch-button" @click="logout">{{
$t("message.hsLoginOut")
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<i
class="el-icon-setting"
:title="$t('message.hssystemSet')"
@click="onPanel"
></i>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
onMounted,
unref,
watch,
getCurrentInstance
} from "vue";
import { defineComponent, unref, watch, getCurrentInstance } from "vue";
import Breadcrumb from "/@/components/ReBreadCrumb";
import Hamburger from "/@/components/ReHamBurger";
import screenfull from "../components/screenfull/index.vue";
import { useRouter, useRoute } from "vue-router";
import { useAppStoreHook } from "/@/store/modules/app";
import { storageSession } from "/@/utils/storage";
import ch from "/@/assets/ch.png";
import en from "/@/assets/en.png";
import favicon from "/favicon.ico";
import { emitter } from "/@/utils/mitt";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useI18n } from "vue-i18n";
import iconinternationality from "/@/assets/svg/iconinternationality.svg";
let routerArrays: Array<object> = [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
showLink: true,
savedPosition: false
}
}
];
export default defineComponent({
name: "Navbar",
components: {
Breadcrumb,
Hamburger,
screenfull
screenfull,
iconinternationality
},
// @ts-ignore
computed: {
// eslint-disable-next-line vue/return-in-computed-property
currentLocale() {
if (
!this.$storage.routesInStorage ||
this.$storage.routesInStorage.length === 0
) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.$storage.routesInStorage = routerArrays;
}
if (!this.$storage.locale) {
// eslint-disable-next-line
this.$storage.locale = { locale: "zh" };
useI18n().locale.value = "zh";
}
switch (this.$storage.locale?.locale) {
case "zh":
return true;
@@ -114,25 +100,9 @@ export default defineComponent({
const pureApp = useAppStoreHook();
const router = useRouter();
const route = useRoute();
let usename = storageSession.getItem("info")?.username;
const { locale, t } = useI18n();
//
const toggleLang = (): void => {
switch (instance.locale.locale) {
case "zh":
instance.locale = { locale: "en" };
locale.value = "en";
break;
case "en":
instance.locale = { locale: "zh" };
locale.value = "zh";
break;
}
};
watch(
() => locale.value,
() => {
@@ -155,28 +125,31 @@ export default defineComponent({
pureApp.toggleSideBar();
}
onMounted(() => {
document
.querySelector(".el-dropdown__popper")
?.setAttribute("class", "resetTop");
document
.querySelector(".el-popper__arrow")
?.setAttribute("class", "hidden");
});
//
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
window.location.reload();
}
// English
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
window.location.reload();
}
return {
pureApp,
toggleSideBar,
usename,
toggleLang,
logout,
ch,
en,
favicon,
onPanel,
deviceDetection,
locale,
t
usename,
pureApp,
favicon,
logout,
onPanel,
translationCh,
translationEn,
toggleSideBar,
deviceDetection
};
}
});
@@ -185,13 +158,13 @@ export default defineComponent({
<style lang="scss" scoped>
.navbar {
width: 100%;
height: 50px;
height: 48px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 46px;
line-height: 48px;
height: 100%;
float: left;
cursor: pointer;
@@ -203,56 +176,46 @@ export default defineComponent({
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
position: absolute;
right: 0;
.vertical-header-right {
display: flex;
align-items: center;
min-width: 280px;
height: 48px;
line-height: 48px;
align-items: center;
color: #000000d9;
justify-content: flex-end;
.inter {
width: 40px;
height: 48px;
display: flex;
align-items: center;
justify-content: space-around;
.screen-full {
cursor: pointer;
&:hover {
cursor: pointer;
background: #f0f0f0;
}
img {
width: 25px;
background: #f6f6f6;
}
}
.hsset {
width: 40px;
.iconinternationality {
height: 48px;
display: flex;
align-items: center;
justify-content: space-around;
margin-right: 5px;
width: 40px;
padding: 11px;
cursor: pointer;
&:hover {
cursor: pointer;
background: #f0f0f0;
background: #f6f6f6;
}
}
.el-dropdown-link {
width: 70px;
width: 100px;
height: 48px;
padding: 10px;
display: flex;
align-items: center;
justify-content: space-around;
margin-right: 10px;
cursor: pointer;
color: #000000d9;
&:hover {
background: #f6f6f6;
}
p {
font-size: 14px;
@@ -261,22 +224,50 @@ export default defineComponent({
img {
width: 22px;
height: 22px;
border-radius: 50%;
}
}
.el-icon-setting {
height: 48px;
width: 40px;
padding: 11px;
display: flex;
cursor: pointer;
align-items: center;
&:hover {
background: #f6f6f6;
}
}
}
}
// single element-plus reset
.el-dropdown-menu__item {
padding: 0 10px;
.breadcrumb-container {
float: left;
}
}
.el-dropdown-menu {
padding: 6px 0;
.translation {
.el-dropdown-menu__item {
padding: 0 40px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
.logout {
.el-dropdown-menu__item {
padding: 0 18px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
</style>

View File

@@ -3,7 +3,7 @@ import { ref } from "vue";
import { useEventListener, onClickOutside } from "@vueuse/core";
import { emitter } from "/@/utils/mitt";
let show = ref(false);
let show = ref<Boolean>(false);
const target = ref(null);
onClickOutside(target, () => {
show.value = false;

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { useFullscreen } from "@vueuse/core";
const { isFullscreen, toggle } = useFullscreen();
</script>
@@ -23,15 +22,10 @@ const { isFullscreen, toggle } = useFullscreen();
<style lang="scss" scoped>
.screen-full {
width: 40px;
height: 48px;
width: 36px;
height: 62px;
display: flex;
align-items: center;
justify-content: space-around;
&:hover {
cursor: pointer;
background: #f0f0f0;
}
}
</style>

View File

@@ -1,14 +1,18 @@
<script setup lang="ts">
import { split } from "lodash-es";
import panel from "../panel/index.vue";
import { useRouter } from "vue-router";
import { emitter } from "/@/utils/mitt";
import { templateRef } from "@vueuse/core";
import { reactive, ref, unref, useCssModule } from "vue";
import { storageLocal, storageSession } from "/@/utils/storage";
import { reactive, ref, unref, useCssModule, getCurrentInstance } from "vue";
const router = useRouter();
const { isSelect } = useCssModule();
const instance =
getCurrentInstance().appContext.app.config.globalProperties.$storage;
// 默认灵动模式
const markValue = ref(storageLocal.getItem("showModel") || "smart");
@@ -80,37 +84,54 @@ function onChange({ label }) {
emitter.emit("tagViewsShowModel", label);
}
const firstTheme = templateRef<HTMLElement | null>("firstTheme", null);
const secondTheme = templateRef<HTMLElement | null>("secondTheme", null);
const verticalDarkDom = templateRef<HTMLElement | null>(
"verticalDarkDom",
null
);
const verticalLightDom = templateRef<HTMLElement | null>(
"verticalLightDom",
null
);
const horizontalDarkDom = templateRef<HTMLElement | null>(
"horizontalDarkDom",
null
);
const horizontalLightDom = templateRef<HTMLElement | null>(
"horizontalLightDom",
null
);
const dataTheme = ref(storageLocal.getItem("data-theme") || "dark");
if (dataTheme.value) {
storageLocal.setItem("data-theme", unref(dataTheme));
window.document.body.setAttribute("data-theme", unref(dataTheme));
}
// dark主题
function onDark() {
storageLocal.setItem("data-theme", "dark");
window.document.body.setAttribute("data-theme", "dark");
toggleClass(true, isSelect, unref(firstTheme));
toggleClass(false, isSelect, unref(secondTheme));
}
// light主题
function onLight() {
storageLocal.setItem("data-theme", "light");
window.document.body.setAttribute("data-theme", "light");
toggleClass(false, isSelect, unref(firstTheme));
toggleClass(true, isSelect, unref(secondTheme));
let dataTheme =
ref(storageLocal.getItem("responsive-layout")) ||
ref({
layout: "horizontal-dark"
});
if (unref(dataTheme)) {
// 设置主题
let theme = split(unref(dataTheme).layout, "-")[1];
window.document.body.setAttribute("data-theme", theme);
// 设置导航模式
let layout = split(unref(dataTheme).layout, "-")[0];
window.document.body.setAttribute("data-layout", layout);
}
// 侧边栏Logo
function logoChange() {
unref(logoVal) === "1"
? storageLocal.setItem("logoVal", "1")
: storageLocal.setItem("logoVal", "-1");
emitter.emit("logoChange", unref(logoVal));
}
function setTheme(layout: string, theme: string, dom: HTMLElement) {
dataTheme.value.layout = `${layout}-${theme}`;
window.document.body.setAttribute("data-layout", layout);
window.document.body.setAttribute("data-theme", theme);
instance.layout = { layout: `${layout}-${theme}` };
toggleClass(true, isSelect, unref(dom));
toggleClass(false, isSelect, unref(dom));
}
</script>
<template>
@@ -124,9 +145,9 @@ function logoChange() {
placement="bottom"
>
<li
:class="dataTheme === 'dark' ? $style.isSelect : ''"
ref="firstTheme"
@click="onDark"
:class="dataTheme.layout === 'vertical-dark' ? $style.isSelect : ''"
ref="verticalDarkDom"
@click="setTheme('vertical', 'dark', verticalDarkDom)"
>
<div></div>
<div></div>
@@ -140,9 +161,43 @@ function logoChange() {
placement="bottom"
>
<li
:class="dataTheme === 'light' ? $style.isSelect : ''"
ref="secondTheme"
@click="onLight"
:class="dataTheme.layout === 'vertical-light' ? $style.isSelect : ''"
ref="verticalLightDom"
@click="setTheme('vertical', 'light', verticalLightDom)"
>
<div></div>
<div></div>
</li>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="暗色主题"
placement="bottom"
>
<li
:class="dataTheme.layout === 'horizontal-dark' ? $style.isSelect : ''"
ref="horizontalDarkDom"
@click="setTheme('horizontal', 'dark', horizontalDarkDom)"
>
<div></div>
<div></div>
</li>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="暗色主题"
placement="bottom"
>
<li
:class="
dataTheme.layout === 'horizontal-light' ? $style.isSelect : ''
"
ref="horizontalLightDom"
@click="setTheme('horizontal', 'light', horizontalLightDom)"
>
<div></div>
<div></div>
@@ -237,18 +292,19 @@ function logoChange() {
.theme-stley {
margin-top: 25px;
width: 100%;
height: 60px;
height: 180px;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
li {
width: 30%;
height: 100%;
margin: 10px;
width: 36%;
height: 70px;
background: #f0f2f5;
position: relative;
overflow: hidden;
cursor: pointer;
background-color: #f0f2f5;
border-radius: 4px;
box-shadow: 0 1px 2.5px 0 rgb(0 0 0 / 18%);
@@ -265,7 +321,7 @@ function logoChange() {
height: 30%;
top: 0;
right: 0;
background-color: #fff;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
@@ -278,7 +334,7 @@ function logoChange() {
width: 30%;
height: 100%;
box-shadow: 0 0 1px #888;
background-color: #fff;
background: #fff;
border-radius: 4px 0 0 4px;
}
@@ -287,12 +343,34 @@ function logoChange() {
height: 30%;
top: 0;
right: 0;
background-color: #fff;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
&:nth-child(3) {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
}
}
}
&:nth-child(4) {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #fff;
box-shadow: 0 0 1px #888;
}
}
}
}
}
</style>

View File

@@ -1,51 +0,0 @@
<template>
<component :is="type" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script lang="ts">
import { computed, defineComponent, unref } from "vue";
import { isUrl } from "/@/utils/is";
export default defineComponent({
name: "Link",
props: {
to: {
type: String,
required: true
}
},
setup(props) {
const isExternal = computed(() => {
return isUrl(props.to);
});
const type = computed(() => {
if (unref(isExternal)) {
return "a";
}
return "router-link";
});
function linkProps(to) {
if (unref(isExternal)) {
return {
href: to,
target: "_blank",
rel: "noopener"
};
}
return {
to: to
};
}
return {
isExternal,
type,
linkProps
};
}
});
</script>

View File

@@ -1,116 +0,0 @@
<template>
<div v-if="!item.hidden">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!item.alwaysShow
"
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<i :class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
<template #title>
<span>{{ $t(onlyOneChild.meta.title) }}</span>
</template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu
v-else
ref="subMenu"
:index="resolvePath(item.path)"
popper-append-to-body
>
<template #title>
<i :class="item.meta.icon"></i>
<span>{{ $t(item.meta.title) }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<script lang="ts">
import path from "path";
import AppLink from "./Link.vue";
import { defineComponent, PropType, ref } from "vue";
import { RouteRecordRaw } from "vue-router";
import { isUrl } from "/@/utils/is.ts";
export default defineComponent({
name: "SidebarItem",
components: { AppLink },
props: {
item: {
type: Object as PropType<RouteRecordRaw>,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ""
}
},
setup(props) {
const onlyOneChild = ref<RouteRecordRaw>({} as any);
function hasOneShowingChild(
children: RouteRecordRaw[] = [],
parent: RouteRecordRaw
) {
const showingChildren = children.filter((item: any) => {
if (item.hidden) {
// 不显示hidden属性为true的菜单
return false;
} else {
onlyOneChild.value = item;
return true;
}
});
if (showingChildren.length === 1) {
return true;
}
if (showingChildren.length === 0) {
// @ts-ignore
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
// const resolvePath = (routePath: string) => {
// return path.resolve(props.basePath, routePath);
// };
function resolvePath(routePath) {
if (isUrl(routePath)) {
return routePath;
}
if (isUrl(this.basePath)) {
return props.basePath;
}
// @ts-ignore
return path.resolve(props.basePath, routePath);
}
return { hasOneShowingChild, resolvePath, onlyOneChild };
}
});
</script>

View File

@@ -0,0 +1,254 @@
<template>
<div class="horizontal-header">
<div class="horizontal-header-left" @click="backHome">
<i class="fa fa-optin-monster"></i>
<h4>{{ settings.title }}</h4>
</div>
<el-menu
:default-active="activeMenu"
unique-opened
router
class="horizontal-header-menu"
mode="horizontal"
@select="menuSelect"
>
<sidebar-item
v-for="route in routeStore.wholeRoutes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
<div class="horizontal-header-right">
<!-- 全屏 -->
<screenfull v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown trigger="click">
<iconinternationality />
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="{
background: locale === 'zh' ? '#1b2a47' : '',
color: locale === 'zh' ? '#f4f4f5' : '#000'
}"
@click="translationCh"
>简体中文</el-dropdown-item
>
<el-dropdown-item
:style="{
background: locale === 'en' ? '#1b2a47' : '',
color: locale === 'en' ? '#f4f4f5' : '#000'
}"
@click="translationEn"
>English</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登陆 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
/>
<p>{{ usename }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item icon="el-icon-switch-button" @click="logout">{{
$t("message.hsLoginOut")
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<i
class="el-icon-setting"
:title="$t('message.hssystemSet')"
@click="onPanel"
></i>
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
unref,
watch,
getCurrentInstance
} from "vue";
import { useI18n } from "vue-i18n";
import settings from "/@/settings";
import { emitter } from "/@/utils/mitt";
import SidebarItem from "./sidebarItem.vue";
import { algorithm } from "/@/utils/algorithm";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { storageSession } from "/@/utils/storage";
import { deviceDetection } from "/@/utils/deviceDetection";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import iconinternationality from "/@/assets/svg/iconinternationality.svg";
let routerArrays: Array<object> = [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
showLink: true,
savedPosition: false
}
}
];
export default defineComponent({
name: "sidebar",
components: { SidebarItem, screenfull, iconinternationality },
// @ts-ignore
computed: {
// eslint-disable-next-line vue/return-in-computed-property
currentLocale() {
if (
!this.$storage.routesInStorage ||
this.$storage.routesInStorage.length === 0
) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.$storage.routesInStorage = routerArrays;
}
if (!this.$storage.locale) {
// eslint-disable-next-line
this.$storage.locale = { locale: "zh" };
useI18n().locale.value = "zh";
}
switch (this.$storage.locale?.locale) {
case "zh":
return true;
case "en":
return false;
}
}
},
setup() {
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const routeStore = usePermissionStoreHook();
const route = useRoute();
const router = useRouter();
const routers = useRouter().options.routes;
let usename = storageSession.getItem("info")?.username;
const { locale, t } = useI18n();
watch(
() => locale.value,
() => {
//@ts-ignore
// 动态title
document.title = t(unref(route.meta.title));
}
);
// 退出登录
const logout = (): void => {
storageSession.removeItem("info");
router.push("/login");
};
function onPanel() {
emitter.emit("openPanel");
}
const activeMenu = computed(() => {
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
});
const menuSelect = (indexPath: string): void => {
let parentPath = "";
let parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
function findCurrentRoute(routes) {
return routes.map(item => {
if (item.path === indexPath) {
// 切换左侧菜单 通知标签页
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
} else {
if (item.children) findCurrentRoute(item.children);
}
});
}
findCurrentRoute(algorithm.increaseIndexes(routers));
};
function backHome() {
router.push("/welcome");
}
// 简体中文
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
window.location.reload();
}
// English
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
window.location.reload();
}
return {
locale,
usename,
settings,
routeStore,
activeMenu,
logout,
onPanel,
backHome,
menuSelect,
translationCh,
translationEn,
deviceDetection
};
}
});
</script>
<style lang="scss" scoped>
.translation {
.el-dropdown-menu__item {
padding: 0 40px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
.logout {
.el-dropdown-menu__item {
padding: 0 18px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
</style>

View File

@@ -1,9 +1,17 @@
<script setup lang="ts">
import settings from "/@/settings";
const props = defineProps({
collapse: Boolean
});
</script>
<template>
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
<div class="sidebar-logo-container" :class="{ collapse: props.collapse }">
<transition name="sidebarLogoFade">
<router-link
v-if="collapse"
key="collapse"
v-if="props.collapse"
key="props.collapse"
:title="settings.title"
class="sidebar-logo-link"
to="/"
@@ -25,25 +33,6 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import settings from "/@/settings";
export default defineComponent({
props: {
collapse: {
type: Boolean,
required: true
}
},
setup() {
return {
settings
};
}
});
</script>
<style lang="scss" scoped>
.sidebar-logo-container {
position: relative;

View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import path from "path";
import { PropType, ref } from "vue";
import { RouteRecordRaw } from "vue-router";
const props = defineProps({
item: {
type: Object as PropType<RouteRecordRaw>
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ""
}
});
type childrenType = {
path?: string;
noShowingChildren?: boolean;
children?: RouteRecordRaw[];
meta?: {
icon?: string;
title?: string;
};
};
const onlyOneChild = ref<RouteRecordRaw | childrenType>({} as any);
function hasOneShowingChild(
children: RouteRecordRaw[] = [],
parent: RouteRecordRaw
) {
const showingChildren = children.filter((item: any) => {
if (item.hidden) {
// 不显示hidden属性为true的菜单
return false;
} else {
onlyOneChild.value = item;
return true;
}
});
if (showingChildren.length === 1) {
return true;
}
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
function resolvePath(routePath) {
return path.resolve(props.basePath, routePath);
}
</script>
<template>
<template
v-if="
hasOneShowingChild(props.item.children, props.item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!props.item.alwaysShow
"
>
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<i
:class="
onlyOneChild.meta.icon || (props.item.meta && props.item.meta.icon)
"
/>
<template #title>
<span>{{ $t(onlyOneChild.meta.title) }}</span>
</template>
</el-menu-item>
</template>
<el-sub-menu
v-else
ref="subMenu"
:index="resolvePath(props.item.path)"
popper-append-to-body
>
<template #title>
<i :class="props.item.meta.icon"></i>
<span>{{ $t(props.item.meta.title) }}</span>
</template>
<sidebar-item
v-for="child in props.item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</template>

View File

@@ -1,11 +1,12 @@
<template>
<div :class="{ 'has-logo': showLogo }">
<div :class="['sidebar-container', showLogo ? 'has-logo' : '']">
<Logo v-if="showLogo === '1'" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
unique-opened
router
:collapse-transition="false"
mode="vertical"
@select="menuSelect"
@@ -22,14 +23,14 @@
</template>
<script lang="ts">
import { computed, defineComponent, ref, onBeforeMount } from "vue";
import Logo from "./logo.vue";
import { emitter } from "/@/utils/mitt";
import SidebarItem from "./sidebarItem.vue";
import { algorithm } from "/@/utils/algorithm";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { useAppStoreHook } from "/@/store/modules/app";
import SidebarItem from "./SidebarItem.vue";
import { algorithm } from "/@/utils/algorithm";
import { emitter } from "/@/utils/mitt";
import Logo from "./Logo.vue";
import { storageLocal } from "/@/utils/storage";
import { computed, defineComponent, ref, onBeforeMount } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
export default defineComponent({
@@ -61,6 +62,7 @@ export default defineComponent({
parentPath = indexPath.slice(0, parentPathIndex);
}
//
// eslint-disable-next-line no-inner-declarations
function findCurrentRoute(routes) {
return routes.map(item => {
if (item.path === indexPath) {

View File

@@ -1,3 +1,41 @@
<script lang="ts">
let routerArrays: Array<object> = [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
showLink: true,
savedPosition: false
}
}
];
export default {
computed: {
layout() {
if (!this.$storage.layout) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.$storage.layout = { layout: "vertical-dark" };
}
if (
!this.$storage.routesInStorage ||
this.$storage.routesInStorage.length === 0
) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.$storage.routesInStorage = routerArrays;
}
if (!this.$storage.locale) {
// eslint-disable-next-line
this.$storage.locale = { locale: "zh" };
useI18n().locale.value = "zh";
}
return this.$storage?.layout.layout;
}
}
};
</script>
<script setup lang="ts">
import {
ref,
@@ -10,13 +48,20 @@ import {
useCssModule
} from "vue";
import options from "/@/settings";
import { useI18n } from "vue-i18n";
import { toggleClass } from "/@/utils/operate";
import { useEventListener } from "@vueuse/core";
import { useAppStoreHook } from "/@/store/modules/app";
import fullScreen from "/@/assets/svg/full_screen.svg";
import exitScreen from "/@/assets/svg/exit_screen.svg";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { Navbar, Sidebar, AppMain, setting, tag } from "./components";
import navbar from "./components/navbar.vue";
import tag from "./components/tag/index.vue";
import appMain from "./components/appMain.vue";
import setting from "./components/setting/index.vue";
import Vertical from "./components/sidebar/vertical.vue";
import Horizontal from "./components/sidebar/horizontal.vue";
interface setInter {
sidebar: any;
@@ -118,19 +163,25 @@ onBeforeMount(() => {
</script>
<template>
<div :class="set.classes" class="app-wrapper">
<div :class="['app-wrapper', set.classes]">
<div
v-if="set.device === 'mobile' && set.sidebar.opened"
v-show="
set.device === 'mobile' &&
set.sidebar.opened &&
layout.includes('vertical')
"
class="drawer-bg"
@click="handleClickOutside(false)"
/>
<!-- 侧边栏 -->
<sidebar class="sidebar-container" v-if="!containerHiddenSideBar" />
<Vertical v-show="!containerHiddenSideBar && layout.includes('vertical')" />
<div class="main-container">
<div :class="{ 'fixed-header': set.fixedHeader }">
<!-- 顶部导航栏 -->
<navbar v-show="!containerHiddenSideBar" />
<navbar
v-show="!containerHiddenSideBar && layout.includes('vertical')"
/>
<!-- tabs标签页 -->
<Horizontal v-show="layout.includes('horizontal')" />
<tag>
<span @click="onFullScreen">
<fullScreen v-if="!containerHiddenSideBar" />
@@ -194,10 +245,6 @@ $sideBarWidth: 210px;
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 100%;
}