feat: add pure-admin-thin
This commit is contained in:
71
src/layout/components/appMain.vue
Normal file
71
src/layout/components/appMain.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance } from "vue";
|
||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||
|
||||
const keepAlive: Boolean = ref(
|
||||
getCurrentInstance().appContext.config.globalProperties.$config?.KeepAlive
|
||||
);
|
||||
|
||||
const transition = computed(() => {
|
||||
return route => {
|
||||
return route.meta.transition;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition
|
||||
:name="
|
||||
transition(route) && route.meta.transition.enterTransition
|
||||
? 'pure-classes-transition'
|
||||
: (transition(route) && route.meta.transition.name) ||
|
||||
'fade-transform'
|
||||
"
|
||||
:enter-active-class="
|
||||
transition(route) &&
|
||||
`animate__animated ${route.meta.transition.enterTransition}`
|
||||
"
|
||||
:leave-active-class="
|
||||
transition(route) &&
|
||||
`animate__animated ${route.meta.transition.leaveTransition}`
|
||||
"
|
||||
mode="out-in"
|
||||
appear
|
||||
>
|
||||
<keep-alive
|
||||
v-if="keepAlive"
|
||||
:include="usePermissionStoreHook().cachePageList"
|
||||
>
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component v-else :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-main {
|
||||
min-height: calc(100vh - 70px);
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-popup-parent--hidden {
|
||||
.fixed-header {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
234
src/layout/components/navbar.vue
Normal file
234
src/layout/components/navbar.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import Hamburger from "./sidebar/hamBurger.vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { storageSession } from "/@/utils/storage";
|
||||
import Breadcrumb from "./sidebar/breadCrumb.vue";
|
||||
import { useAppStoreHook } from "/@/store/modules/app";
|
||||
import { unref, watch, getCurrentInstance } from "vue";
|
||||
import { deviceDetection } from "/@/utils/deviceDetection";
|
||||
import screenfull from "../components/screenfull/index.vue";
|
||||
import globalization from "/@/assets/svg/globalization.svg";
|
||||
|
||||
const instance =
|
||||
getCurrentInstance().appContext.config.globalProperties.$storage;
|
||||
const pureApp = useAppStoreHook();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
let usename = storageSession.getItem("info")?.username;
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
watch(
|
||||
() => locale.value,
|
||||
() => {
|
||||
//@ts-ignore
|
||||
document.title = t(unref(route.meta.title)); // 动态title
|
||||
}
|
||||
);
|
||||
|
||||
// 退出登录
|
||||
const logout = (): void => {
|
||||
storageSession.removeItem("info");
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
function onPanel() {
|
||||
emitter.emit("openPanel");
|
||||
}
|
||||
|
||||
function toggleSideBar() {
|
||||
pureApp.toggleSideBar();
|
||||
}
|
||||
|
||||
// 简体中文
|
||||
function translationCh() {
|
||||
instance.locale = { locale: "zh" };
|
||||
locale.value = "zh";
|
||||
}
|
||||
|
||||
// English
|
||||
function translationEn() {
|
||||
instance.locale = { locale: "en" };
|
||||
locale.value = "en";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<Hamburger
|
||||
:is-active="pureApp.sidebar.opened"
|
||||
class="hamburger-container"
|
||||
@toggleClick="toggleSideBar"
|
||||
/>
|
||||
|
||||
<Breadcrumb class="breadcrumb-container" />
|
||||
|
||||
<div class="vertical-header-right">
|
||||
<!-- 全屏 -->
|
||||
<screenfull v-show="!deviceDetection()" />
|
||||
<!-- 国际化 -->
|
||||
<el-dropdown trigger="click">
|
||||
<globalization />
|
||||
<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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 48px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-header-right {
|
||||
display: flex;
|
||||
min-width: 280px;
|
||||
height: 48px;
|
||||
align-items: center;
|
||||
color: #000000d9;
|
||||
justify-content: flex-end;
|
||||
|
||||
.screen-full {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
}
|
||||
|
||||
.globalization {
|
||||
height: 48px;
|
||||
width: 40px;
|
||||
padding: 11px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
width: 100px;
|
||||
height: 48px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
cursor: pointer;
|
||||
color: #000000d9;
|
||||
|
||||
&:hover {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
150
src/layout/components/panel/index.vue
Normal file
150
src/layout/components/panel/index.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useEventListener, onClickOutside } from "@vueuse/core";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
|
||||
let show = ref<Boolean>(false);
|
||||
const target = ref(null);
|
||||
onClickOutside(target, () => {
|
||||
show.value = false;
|
||||
});
|
||||
|
||||
const addEventClick = (): void => {
|
||||
useEventListener("click", closeSidebar);
|
||||
};
|
||||
|
||||
const closeSidebar = (evt: any): void => {
|
||||
const parent = evt.target.closest(".right-panel");
|
||||
if (!parent) {
|
||||
show.value = false;
|
||||
window.removeEventListener("click", closeSidebar);
|
||||
}
|
||||
};
|
||||
|
||||
emitter.on("openPanel", () => {
|
||||
show.value = true;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
addEventClick
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ show: show }" class="right-panel-container">
|
||||
<div class="right-panel-background" />
|
||||
<div ref="target" class="right-panel">
|
||||
<div class="right-panel-items">
|
||||
<div class="project-configuration">
|
||||
<h3>项目配置</h3>
|
||||
<i class="el-icon-close" @click="show = !show"></i>
|
||||
</div>
|
||||
<div style="border-bottom: 1px solid #dcdfe6"></div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.showright-panel {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: calc(100% - 15px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.right-panel-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
transform: translate(100%);
|
||||
background: #fff;
|
||||
z-index: 40000;
|
||||
}
|
||||
|
||||
.show {
|
||||
transition: all 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
|
||||
.right-panel-background {
|
||||
z-index: 20000;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.handle-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
left: -48px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
border-radius: 6px 0 0 6px !important;
|
||||
z-index: 0;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
line-height: 48px;
|
||||
top: 45%;
|
||||
background: rgb(24, 144, 255);
|
||||
|
||||
i {
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel-items {
|
||||
margin-top: 60px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.project-configuration {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
position: fixed;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
top: 15px;
|
||||
margin-left: 10px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
margin-right: 20px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-divider--horizontal) {
|
||||
width: 90%;
|
||||
margin: 20px auto 0 auto;
|
||||
}
|
||||
</style>
|
31
src/layout/components/screenfull/index.vue
Normal file
31
src/layout/components/screenfull/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useFullscreen } from "@vueuse/core";
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="screen-full" @click="toggle">
|
||||
<i
|
||||
:title="
|
||||
isFullscreen
|
||||
? $t('message.hsexitfullscreen')
|
||||
: $t('message.hsfullscreen')
|
||||
"
|
||||
:class="
|
||||
isFullscreen
|
||||
? 'iconfont team-iconexit-fullscreen'
|
||||
: 'iconfont team-iconfullscreen'
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.screen-full {
|
||||
width: 36px;
|
||||
height: 62px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
403
src/layout/components/setting/index.vue
Normal file
403
src/layout/components/setting/index.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<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 { debounce } from "/@/utils/debounce";
|
||||
import { useAppStoreHook } from "/@/store/modules/app";
|
||||
import { storageLocal, storageSession } from "/@/utils/storage";
|
||||
import {
|
||||
reactive,
|
||||
ref,
|
||||
unref,
|
||||
watch,
|
||||
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");
|
||||
|
||||
const logoVal = ref(storageLocal.getItem("logoVal") || "1");
|
||||
|
||||
const localOperate = (key: string, value?: any, model?: string): any => {
|
||||
model && model === "set"
|
||||
? storageLocal.setItem(key, value)
|
||||
: storageLocal.getItem(key);
|
||||
};
|
||||
|
||||
const settings = reactive({
|
||||
greyVal: storageLocal.getItem("greyVal"),
|
||||
weekVal: storageLocal.getItem("weekVal"),
|
||||
tagsVal: storageLocal.getItem("tagsVal")
|
||||
});
|
||||
|
||||
settings.greyVal === null
|
||||
? localOperate("greyVal", false, "set")
|
||||
: document.querySelector("html")?.setAttribute("class", "html-grey");
|
||||
|
||||
settings.weekVal === null
|
||||
? localOperate("weekVal", false, "set")
|
||||
: document.querySelector("html")?.setAttribute("class", "html-weakness");
|
||||
|
||||
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
|
||||
const targetEl = target || document.body;
|
||||
let { className } = targetEl;
|
||||
className = className.replace(clsName, "");
|
||||
targetEl.className = flag ? `${className} ${clsName} ` : className;
|
||||
}
|
||||
|
||||
// 灰色模式设置
|
||||
const greyChange = ({ value }): void => {
|
||||
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
|
||||
value
|
||||
? localOperate("greyVal", true, "set")
|
||||
: localOperate("greyVal", false, "set");
|
||||
};
|
||||
|
||||
// 色弱模式设置
|
||||
const weekChange = ({ value }): void => {
|
||||
toggleClass(
|
||||
settings.weekVal,
|
||||
"html-weakness",
|
||||
document.querySelector("html")
|
||||
);
|
||||
value
|
||||
? localOperate("weekVal", true, "set")
|
||||
: localOperate("weekVal", false, "set");
|
||||
};
|
||||
|
||||
const tagsChange = () => {
|
||||
let showVal = settings.tagsVal;
|
||||
showVal
|
||||
? storageLocal.setItem("tagsVal", true)
|
||||
: storageLocal.setItem("tagsVal", false);
|
||||
emitter.emit("tagViewsChange", showVal);
|
||||
};
|
||||
|
||||
function onReset() {
|
||||
storageLocal.clear();
|
||||
storageSession.clear();
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
function onChange({ label }) {
|
||||
storageLocal.setItem("showModel", label);
|
||||
emitter.emit("tagViewsShowModel", label);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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 setFalse(Doms): any {
|
||||
Doms.forEach(v => {
|
||||
toggleClass(false, isSelect, unref(v));
|
||||
});
|
||||
}
|
||||
|
||||
watch(instance, ({ layout }) => {
|
||||
switch (layout["layout"]) {
|
||||
case "vertical-dark":
|
||||
toggleClass(true, isSelect, unref(verticalDarkDom));
|
||||
debounce(
|
||||
setFalse([verticalLightDom, horizontalDarkDom, horizontalLightDom]),
|
||||
50
|
||||
);
|
||||
break;
|
||||
case "vertical-light":
|
||||
toggleClass(true, isSelect, unref(verticalLightDom));
|
||||
debounce(
|
||||
setFalse([verticalDarkDom, horizontalDarkDom, horizontalLightDom]),
|
||||
50
|
||||
);
|
||||
break;
|
||||
case "horizontal-dark":
|
||||
toggleClass(true, isSelect, unref(horizontalDarkDom));
|
||||
debounce(
|
||||
setFalse([verticalDarkDom, verticalLightDom, horizontalLightDom]),
|
||||
50
|
||||
);
|
||||
break;
|
||||
case "horizontal-light":
|
||||
toggleClass(true, isSelect, unref(horizontalLightDom));
|
||||
debounce(
|
||||
setFalse([verticalDarkDom, verticalLightDom, horizontalDarkDom]),
|
||||
50
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function setTheme(layout: string, theme: string) {
|
||||
dataTheme.value.layout = `${layout}-${theme}`;
|
||||
window.document.body.setAttribute("data-layout", layout);
|
||||
window.document.body.setAttribute("data-theme", theme);
|
||||
instance.layout = { layout: `${layout}-${theme}` };
|
||||
useAppStoreHook().setLayout(layout);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<panel>
|
||||
<el-divider>主题风格</el-divider>
|
||||
<ul class="theme-stley">
|
||||
<el-tooltip class="item" content="左侧菜单暗色模式" placement="bottom">
|
||||
<li
|
||||
:class="dataTheme.layout === 'vertical-dark' ? $style.isSelect : ''"
|
||||
ref="verticalDarkDom"
|
||||
@click="setTheme('vertical', 'dark')"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="item" content="左侧菜单亮色模式" placement="bottom">
|
||||
<li
|
||||
:class="dataTheme.layout === 'vertical-light' ? $style.isSelect : ''"
|
||||
ref="verticalLightDom"
|
||||
@click="setTheme('vertical', 'light')"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="item" content="顶部菜单暗色模式" placement="bottom">
|
||||
<li
|
||||
:class="dataTheme.layout === 'horizontal-dark' ? $style.isSelect : ''"
|
||||
ref="horizontalDarkDom"
|
||||
@click="setTheme('horizontal', 'dark')"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="item" content="顶部菜单亮色模式" placement="bottom">
|
||||
<li
|
||||
:class="
|
||||
dataTheme.layout === 'horizontal-light' ? $style.isSelect : ''
|
||||
"
|
||||
ref="horizontalLightDom"
|
||||
@click="setTheme('horizontal', 'light')"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
</ul>
|
||||
|
||||
<el-divider>界面显示</el-divider>
|
||||
<ul class="setting">
|
||||
<li>
|
||||
<span>灰色模式</span>
|
||||
<vxe-switch
|
||||
v-model="settings.greyVal"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
@change="greyChange"
|
||||
></vxe-switch>
|
||||
</li>
|
||||
<li>
|
||||
<span>色弱模式</span>
|
||||
<vxe-switch
|
||||
v-model="settings.weekVal"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
@change="weekChange"
|
||||
></vxe-switch>
|
||||
</li>
|
||||
<li>
|
||||
<span>隐藏标签页</span>
|
||||
<vxe-switch
|
||||
v-model="settings.tagsVal"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
@change="tagsChange"
|
||||
></vxe-switch>
|
||||
</li>
|
||||
<li>
|
||||
<span>侧边栏Logo</span>
|
||||
<vxe-switch
|
||||
v-model="logoVal"
|
||||
open-value="1"
|
||||
close-value="-1"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
@change="logoChange"
|
||||
></vxe-switch>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span>标签风格</span>
|
||||
<vxe-radio-group v-model="markValue" @change="onChange">
|
||||
<vxe-radio label="card" content="卡片"></vxe-radio>
|
||||
<vxe-radio label="smart" content="灵动"></vxe-radio>
|
||||
</vxe-radio-group>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<el-divider />
|
||||
<vxe-button
|
||||
status="danger"
|
||||
style="width: 90%; margin: 24px 15px"
|
||||
content="清空缓存并返回登录页"
|
||||
icon="fa fa-sign-out"
|
||||
@click="onReset"
|
||||
></vxe-button>
|
||||
</panel>
|
||||
</template>
|
||||
|
||||
<style scoped module>
|
||||
.isSelect {
|
||||
border: 2px solid #0960bd;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting {
|
||||
width: 100%;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-divider__text) {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.theme-stley {
|
||||
margin-top: 25px;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
|
||||
li {
|
||||
margin: 10px;
|
||||
width: 36%;
|
||||
height: 70px;
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2.5px 0 rgb(0 0 0 / 18%);
|
||||
|
||||
&:nth-child(1) {
|
||||
div {
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
background: #1b2a47;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 70%;
|
||||
height: 30%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px #888;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
div {
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 1px #888;
|
||||
background: #fff;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 70%;
|
||||
height: 30%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
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>
|
79
src/layout/components/sidebar/breadCrumb.vue
Normal file
79
src/layout/components/sidebar/breadCrumb.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
|
||||
|
||||
const levelList = ref([]);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const isDashboard = (route: RouteLocationMatched): boolean | string => {
|
||||
const name = route && (route.name as string);
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
return name.trim().toLocaleLowerCase() === "welcome".toLocaleLowerCase();
|
||||
};
|
||||
|
||||
const getBreadcrumb = (): void => {
|
||||
let matched = route.matched.filter(item => item.meta && item.meta.title);
|
||||
const first = matched[0];
|
||||
if (!isDashboard(first)) {
|
||||
matched = [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: { title: "message.hshome" }
|
||||
} as unknown as RouteLocationMatched
|
||||
].concat(matched);
|
||||
}
|
||||
levelList.value = matched.filter(
|
||||
item => item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||
);
|
||||
};
|
||||
|
||||
getBreadcrumb();
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => getBreadcrumb()
|
||||
);
|
||||
|
||||
const handleLink = (item: RouteLocationMatched): any => {
|
||||
const { redirect, path } = item;
|
||||
if (redirect) {
|
||||
router.push(redirect.toString());
|
||||
return;
|
||||
}
|
||||
router.push(path);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group appear name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
|
||||
<span
|
||||
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
|
||||
class="no-redirect"
|
||||
>{{ $t(item.meta.title) }}</span
|
||||
>
|
||||
<a v-else @click.prevent="handleLink(item)">
|
||||
{{ $t(item.meta.title) }}
|
||||
</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
52
src/layout/components/sidebar/hamBurger.vue
Normal file
52
src/layout/components/sidebar/hamBurger.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isActive: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "toggleClick"): void;
|
||||
}>();
|
||||
|
||||
const toggleClick = () => {
|
||||
emit("toggleClick");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes.container" @click="toggleClick">
|
||||
<svg
|
||||
:class="['hamburger', props.isActive ? 'is-active' : '']"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
>
|
||||
<path
|
||||
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module="classes" scoped>
|
||||
.container {
|
||||
padding: 0 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.hamburger {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
215
src/layout/components/sidebar/horizontal.vue
Normal file
215
src/layout/components/sidebar/horizontal.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
unref,
|
||||
watch,
|
||||
nextTick,
|
||||
onMounted,
|
||||
getCurrentInstance
|
||||
} from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import { templateRef } from "@vueuse/core";
|
||||
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 globalization from "/@/assets/svg/globalization.svg";
|
||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||
|
||||
const instance =
|
||||
getCurrentInstance().appContext.config.globalProperties.$storage;
|
||||
|
||||
const title =
|
||||
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
|
||||
|
||||
const menuRef = templateRef<ElRef | null>("menu", null);
|
||||
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((): string => {
|
||||
const { meta, path } = route;
|
||||
if (meta.activeMenu) {
|
||||
// @ts-ignore
|
||||
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 handleResize() {
|
||||
// @ts-ignore
|
||||
menuRef.value.handleResize();
|
||||
}
|
||||
|
||||
// 简体中文
|
||||
function translationCh() {
|
||||
instance.locale = { locale: "zh" };
|
||||
locale.value = "zh";
|
||||
handleResize();
|
||||
}
|
||||
|
||||
// English
|
||||
function translationEn() {
|
||||
instance.locale = { locale: "en" };
|
||||
locale.value = "en";
|
||||
handleResize();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
handleResize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="horizontal-header">
|
||||
<div class="horizontal-header-left" @click="backHome">
|
||||
<i class="fa fa-optin-monster"></i>
|
||||
<h4>{{ title }}</h4>
|
||||
</div>
|
||||
<el-menu
|
||||
ref="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">
|
||||
<globalization />
|
||||
<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>
|
||||
|
||||
<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>
|
72
src/layout/components/sidebar/logo.vue
Normal file
72
src/layout/components/sidebar/logo.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance } from "vue";
|
||||
const props = defineProps({
|
||||
collapse: Boolean
|
||||
});
|
||||
|
||||
const title =
|
||||
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{ collapse: props.collapse }">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link
|
||||
v-if="props.collapse"
|
||||
key="props.collapse"
|
||||
:title="title"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
>
|
||||
<i class="fa fa-optin-monster"></i>
|
||||
<h1 class="sidebar-title">{{ title }}</h1>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-else
|
||||
key="expand"
|
||||
:title="title"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
>
|
||||
<i class="fa fa-optin-monster"></i>
|
||||
<h1 class="sidebar-title">{{ title }}</h1>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
.sidebar-logo-link {
|
||||
height: 100%;
|
||||
|
||||
.sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
margin-top: 16px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.fa-optin-monster {
|
||||
font-size: 30px;
|
||||
color: #1890ff;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
.sidebar-logo {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
99
src/layout/components/sidebar/sidebarItem.vue
Normal file
99
src/layout/components/sidebar/sidebarItem.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import path from "path";
|
||||
import { PropType, ref } from "vue";
|
||||
import { childrenType } from "../../types";
|
||||
import Icon from "/@/components/ReIcon/src/Icon.vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<childrenType>
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const onlyOneChild: childrenType = ref(null);
|
||||
|
||||
function hasOneShowingChild(
|
||||
children: childrenType[] = [],
|
||||
parent: childrenType
|
||||
) {
|
||||
const showingChildren = children.filter((item: any) => {
|
||||
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)
|
||||
"
|
||||
>
|
||||
<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>
|
||||
<Icon
|
||||
v-if="onlyOneChild.meta.extraIcon"
|
||||
:svg="onlyOneChild.meta.extraIcon.svg ? true : false"
|
||||
:content="`${onlyOneChild.meta.extraIcon.name}`"
|
||||
/>
|
||||
</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>
|
||||
<Icon
|
||||
v-if="props.item.meta.extraIcon"
|
||||
:svg="props.item.meta.extraIcon.svg ? true : false"
|
||||
:content="`${props.item.meta.extraIcon.name}`"
|
||||
/>
|
||||
</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>
|
82
src/layout/components/sidebar/vertical.vue
Normal file
82
src/layout/components/sidebar/vertical.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
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 { computed, ref, onBeforeMount } from "vue";
|
||||
import { useAppStoreHook } from "/@/store/modules/app";
|
||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||
|
||||
const route = useRoute();
|
||||
const pureApp = useAppStoreHook();
|
||||
const router = useRouter().options.routes;
|
||||
const routeStore = usePermissionStoreHook();
|
||||
const showLogo = ref(storageLocal.getItem("logoVal") || "1");
|
||||
const isCollapse = computed(() => {
|
||||
return !pureApp.getSidebarStatus;
|
||||
});
|
||||
const activeMenu = computed((): string => {
|
||||
const { meta, path } = route;
|
||||
if (meta.activeMenu) {
|
||||
// @ts-ignore
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path;
|
||||
});
|
||||
|
||||
const menuSelect = (indexPath: string): void => {
|
||||
let parentPath = "";
|
||||
let parentPathIndex = indexPath.lastIndexOf("/");
|
||||
if (parentPathIndex > 0) {
|
||||
parentPath = indexPath.slice(0, parentPathIndex);
|
||||
}
|
||||
// 找到当前路由的信息
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
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(router));
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
emitter.on("logoChange", key => {
|
||||
showLogo.value = key;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in routeStore.wholeRoutes"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
807
src/layout/components/tag/index.vue
Normal file
807
src/layout/components/tag/index.vue
Normal file
@@ -0,0 +1,807 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
onBeforeMount,
|
||||
unref,
|
||||
nextTick,
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
ComputedRef
|
||||
} from "vue";
|
||||
import { RouteConfigs, relativeStorageType, tagsViewsType } from "../../types";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import { templateRef } from "@vueuse/core";
|
||||
import { handleAliveRoute } from "/@/router";
|
||||
import { storageLocal } from "/@/utils/storage";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
|
||||
|
||||
import close from "/@/assets/svg/close.svg";
|
||||
import refresh from "/@/assets/svg/refresh.svg";
|
||||
import closeAll from "/@/assets/svg/close_all.svg";
|
||||
import closeLeft from "/@/assets/svg/close_left.svg";
|
||||
import closeOther from "/@/assets/svg/close_other.svg";
|
||||
import closeRight from "/@/assets/svg/close_right.svg";
|
||||
|
||||
let refreshButton = "refresh-button";
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
// 响应式storage
|
||||
let relativeStorage: relativeStorageType;
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const showTags = ref(storageLocal.getItem("tagsVal") || false);
|
||||
const containerDom = templateRef<HTMLElement | null>("containerDom", null);
|
||||
const activeIndex = ref(-1);
|
||||
let routerArrays: Array<RouteConfigs> = [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: {
|
||||
title: "message.hshome",
|
||||
icon: "el-icon-s-home",
|
||||
showLink: true
|
||||
}
|
||||
}
|
||||
];
|
||||
const tagsViews = ref<Array<tagsViewsType>>([
|
||||
{
|
||||
icon: refresh,
|
||||
text: "message.hsreload",
|
||||
divided: false,
|
||||
disabled: false,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: close,
|
||||
text: "message.hscloseCurrentTab",
|
||||
divided: false,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeLeft,
|
||||
text: "message.hscloseLeftTabs",
|
||||
divided: true,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeRight,
|
||||
text: "message.hscloseRightTabs",
|
||||
divided: false,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeOther,
|
||||
text: "message.hscloseOtherTabs",
|
||||
divided: true,
|
||||
disabled: routerArrays.length > 2 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeAll,
|
||||
text: "message.hscloseAllTabs",
|
||||
divided: false,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
show: true
|
||||
}
|
||||
]);
|
||||
const dynamicTagList: ComputedRef<Array<RouteConfigs>> = computed(() => {
|
||||
return relativeStorage.routesInStorage;
|
||||
});
|
||||
|
||||
// 显示模式,默认灵动模式显示
|
||||
const showModel = ref(storageLocal.getItem("showModel") || "smart");
|
||||
if (!showModel.value) {
|
||||
storageLocal.setItem("showModel", "card");
|
||||
}
|
||||
|
||||
let visible = ref(false);
|
||||
let buttonLeft = ref(0);
|
||||
let buttonTop = ref(0);
|
||||
|
||||
// 当前右键选中的路由信息
|
||||
let currentSelect = ref({});
|
||||
|
||||
function dynamicRouteTag(value: string, parentPath: string): void {
|
||||
const hasValue = relativeStorage.routesInStorage.some((item: any) => {
|
||||
return item.path === value;
|
||||
});
|
||||
|
||||
function concatPath(arr: object[], value: string, parentPath: string) {
|
||||
if (!hasValue) {
|
||||
arr.forEach((arrItem: any) => {
|
||||
let pathConcat = parentPath + arrItem.path;
|
||||
if (arrItem.path === value || pathConcat === value) {
|
||||
routerArrays.push({
|
||||
path: value,
|
||||
parentPath: `/${parentPath.split("/")[1]}`,
|
||||
meta: arrItem.meta
|
||||
});
|
||||
relativeStorage.routesInStorage = routerArrays;
|
||||
} else {
|
||||
if (arrItem.children && arrItem.children.length > 0) {
|
||||
concatPath(arrItem.children, value, parentPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
concatPath(router.options.routes, value, parentPath);
|
||||
}
|
||||
|
||||
// 重新加载
|
||||
function onFresh() {
|
||||
toggleClass(true, refreshButton, document.querySelector(".rotate"));
|
||||
const { fullPath } = unref(route);
|
||||
router.replace({
|
||||
path: "/redirect" + fullPath
|
||||
});
|
||||
setTimeout(() => {
|
||||
removeClass(document.querySelector(".rotate"), refreshButton);
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function deleteDynamicTag(obj: any, current: any, tag?: string) {
|
||||
let valueIndex: number = routerArrays.findIndex((item: any) => {
|
||||
return item.path === obj.path;
|
||||
});
|
||||
|
||||
const spliceRoute = (start?: number, end?: number, other?: boolean): void => {
|
||||
if (other) {
|
||||
relativeStorage.routesInStorage = [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: {
|
||||
title: "message.hshome",
|
||||
icon: "el-icon-s-home",
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
obj
|
||||
];
|
||||
routerArrays = relativeStorage.routesInStorage;
|
||||
} else {
|
||||
routerArrays.splice(start, end);
|
||||
relativeStorage.routesInStorage = routerArrays;
|
||||
}
|
||||
router.push(obj.path);
|
||||
// 删除缓存路由
|
||||
handleAliveRoute(route.matched, "delete");
|
||||
};
|
||||
|
||||
if (tag === "other") {
|
||||
spliceRoute(1, 1, true);
|
||||
} else if (tag === "left") {
|
||||
spliceRoute(1, valueIndex - 1);
|
||||
} else if (tag === "right") {
|
||||
spliceRoute(valueIndex + 1, routerArrays.length);
|
||||
} else {
|
||||
// 从当前匹配到的路径中删除
|
||||
spliceRoute(valueIndex, 1);
|
||||
}
|
||||
|
||||
if (current === obj.path) {
|
||||
// 如果删除当前激活tag就自动切换到最后一个tag
|
||||
let newRoute: any = routerArrays.slice(-1);
|
||||
nextTick(() => {
|
||||
router.push({
|
||||
path: newRoute[0].path
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deleteMenu(item, tag?: string) {
|
||||
deleteDynamicTag(item, item.path, tag);
|
||||
}
|
||||
|
||||
function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
||||
if (item && item.disabled) return;
|
||||
// 当前路由信息
|
||||
switch (key) {
|
||||
case 0:
|
||||
// 重新加载
|
||||
onFresh();
|
||||
break;
|
||||
case 1:
|
||||
// 关闭当前标签页
|
||||
selectRoute
|
||||
? deleteMenu({ path: selectRoute.path, meta: selectRoute.meta })
|
||||
: deleteMenu({ path: route.path, meta: route.meta });
|
||||
break;
|
||||
case 2:
|
||||
// 关闭左侧标签页
|
||||
selectRoute
|
||||
? deleteMenu(
|
||||
{
|
||||
path: selectRoute.path,
|
||||
meta: selectRoute.meta
|
||||
},
|
||||
"left"
|
||||
)
|
||||
: deleteMenu({ path: route.path, meta: route.meta }, "left");
|
||||
break;
|
||||
case 3:
|
||||
// 关闭右侧标签页
|
||||
selectRoute
|
||||
? deleteMenu(
|
||||
{
|
||||
path: selectRoute.path,
|
||||
meta: selectRoute.meta
|
||||
},
|
||||
"right"
|
||||
)
|
||||
: deleteMenu({ path: route.path, meta: route.meta }, "right");
|
||||
break;
|
||||
case 4:
|
||||
// 关闭其他标签页
|
||||
selectRoute
|
||||
? deleteMenu(
|
||||
{
|
||||
path: selectRoute.path,
|
||||
meta: selectRoute.meta
|
||||
},
|
||||
"other"
|
||||
)
|
||||
: deleteMenu({ path: route.path, meta: route.meta }, "other");
|
||||
break;
|
||||
case 5:
|
||||
// 关闭全部标签页
|
||||
routerArrays.splice(1, routerArrays.length);
|
||||
relativeStorage.routesInStorage = routerArrays;
|
||||
usePermissionStoreHook().clearAllCachePage();
|
||||
router.push("/welcome");
|
||||
|
||||
break;
|
||||
}
|
||||
setTimeout(() => {
|
||||
showMenuModel(route.fullPath);
|
||||
});
|
||||
}
|
||||
|
||||
// 触发右键中菜单的点击事件
|
||||
function selectTag(key, item) {
|
||||
onClickDrop(key, item, currentSelect.value);
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function showMenus(value: boolean) {
|
||||
Array.of(1, 2, 3, 4, 5).forEach(v => {
|
||||
tagsViews.value[v].show = value;
|
||||
});
|
||||
}
|
||||
|
||||
function disabledMenus(value: boolean) {
|
||||
Array.of(1, 2, 3, 4, 5).forEach(v => {
|
||||
tagsViews.value[v].disabled = value;
|
||||
});
|
||||
}
|
||||
|
||||
// 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页
|
||||
function showMenuModel(currentPath: string, refresh = false) {
|
||||
let allRoute = unref(relativeStorage.routesInStorage);
|
||||
let routeLength = unref(relativeStorage.routesInStorage).length;
|
||||
// currentIndex为1时,左侧的菜单是首页,则不显示关闭左侧标签页
|
||||
let currentIndex = allRoute.findIndex(v => v.path === currentPath);
|
||||
// 如果currentIndex等于routeLength-1,右侧没有菜单,则不显示关闭右侧标签页
|
||||
showMenus(true);
|
||||
|
||||
if (refresh) {
|
||||
tagsViews.value[0].show = true;
|
||||
}
|
||||
|
||||
if (currentIndex === 1 && routeLength !== 2) {
|
||||
// 左侧的菜单是首页,右侧存在别的菜单
|
||||
tagsViews.value[2].show = false;
|
||||
Array.of(1, 3, 4, 5).forEach(v => {
|
||||
tagsViews.value[v].disabled = false;
|
||||
});
|
||||
tagsViews.value[2].disabled = true;
|
||||
} else if (currentIndex === 1 && routeLength === 2) {
|
||||
disabledMenus(false);
|
||||
// 左侧的菜单是首页,右侧不存在别的菜单
|
||||
Array.of(2, 3, 4).forEach(v => {
|
||||
tagsViews.value[v].show = false;
|
||||
tagsViews.value[v].disabled = true;
|
||||
});
|
||||
} else if (routeLength - 1 === currentIndex && currentIndex !== 0) {
|
||||
// 当前路由是所有路由中的最后一个
|
||||
tagsViews.value[3].show = false;
|
||||
Array.of(1, 2, 4, 5).forEach(v => {
|
||||
tagsViews.value[v].disabled = false;
|
||||
});
|
||||
tagsViews.value[3].disabled = true;
|
||||
} else if (currentIndex === 0 || currentPath === "/redirect/welcome") {
|
||||
// 当前路由为首页
|
||||
disabledMenus(true);
|
||||
} else {
|
||||
disabledMenus(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openMenu(tag, e) {
|
||||
closeMenu();
|
||||
if (tag.path === "/welcome") {
|
||||
// 右键菜单为首页,只显示刷新
|
||||
showMenus(false);
|
||||
tagsViews.value[0].show = true;
|
||||
} else if (route.path !== tag.path) {
|
||||
// 右键菜单不匹配当前路由,隐藏刷新
|
||||
tagsViews.value[0].show = false;
|
||||
showMenuModel(tag.path);
|
||||
} else if (
|
||||
// eslint-disable-next-line no-dupe-else-if
|
||||
relativeStorage.routesInStorage.length === 2 &&
|
||||
route.path !== tag.path
|
||||
) {
|
||||
showMenus(true);
|
||||
// 只有两个标签时不显示关闭其他标签页
|
||||
tagsViews.value[4].show = false;
|
||||
} else if (route.path === tag.path) {
|
||||
// 右键当前激活的菜单
|
||||
showMenuModel(tag.path, true);
|
||||
}
|
||||
|
||||
currentSelect.value = tag;
|
||||
const menuMinWidth = 105;
|
||||
const offsetLeft = unref(containerDom).getBoundingClientRect().left;
|
||||
const offsetWidth = unref(containerDom).offsetWidth;
|
||||
const maxLeft = offsetWidth - menuMinWidth;
|
||||
const left = e.clientX - offsetLeft + 5;
|
||||
if (left > maxLeft) {
|
||||
buttonLeft.value = maxLeft;
|
||||
} else {
|
||||
buttonLeft.value = left;
|
||||
}
|
||||
buttonTop.value = e.clientY + 10;
|
||||
setTimeout(() => {
|
||||
visible.value = true;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// 触发tags标签切换
|
||||
function tagOnClick(item) {
|
||||
showMenuModel(item.path);
|
||||
}
|
||||
|
||||
// 鼠标移入
|
||||
function onMouseenter(item, index) {
|
||||
if (index) activeIndex.value = index;
|
||||
if (unref(showModel) === "smart") {
|
||||
if (hasClass(instance.refs["schedule" + index], "schedule-active")) return;
|
||||
toggleClass(true, "schedule-in", instance.refs["schedule" + index]);
|
||||
toggleClass(false, "schedule-out", instance.refs["schedule" + index]);
|
||||
} else {
|
||||
if (hasClass(instance.refs["dynamic" + index], "card-active")) return;
|
||||
toggleClass(true, "card-in", instance.refs["dynamic" + index]);
|
||||
toggleClass(false, "card-out", instance.refs["dynamic" + index]);
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标移出
|
||||
function onMouseleave(item, index) {
|
||||
activeIndex.value = -1;
|
||||
if (unref(showModel) === "smart") {
|
||||
if (hasClass(instance.refs["schedule" + index], "schedule-active")) return;
|
||||
toggleClass(false, "schedule-in", instance.refs["schedule" + index]);
|
||||
toggleClass(true, "schedule-out", instance.refs["schedule" + index]);
|
||||
} else {
|
||||
if (hasClass(instance.refs["dynamic" + index], "card-active")) return;
|
||||
toggleClass(false, "card-in", instance.refs["dynamic" + index]);
|
||||
toggleClass(true, "card-out", instance.refs["dynamic" + index]);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (val) {
|
||||
document.body.addEventListener("click", closeMenu);
|
||||
} else {
|
||||
document.body.removeEventListener("click", closeMenu);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!instance) return;
|
||||
relativeStorage = instance.appContext.app.config.globalProperties.$storage;
|
||||
routerArrays = relativeStorage.routesInStorage ?? routerArrays;
|
||||
|
||||
// 根据当前路由初始化操作标签页的禁用状态
|
||||
showMenuModel(route.fullPath);
|
||||
|
||||
// 触发隐藏标签页
|
||||
emitter.on("tagViewsChange", key => {
|
||||
if (unref(showTags) === key) return;
|
||||
showTags.value = key;
|
||||
});
|
||||
|
||||
// 改变标签风格
|
||||
emitter.on("tagViewsShowModel", key => {
|
||||
showModel.value = key;
|
||||
});
|
||||
|
||||
// 接收侧边栏切换传递过来的参数
|
||||
emitter.on("changLayoutRoute", ({ indexPath, parentPath }) => {
|
||||
dynamicRouteTag(indexPath, parentPath);
|
||||
setTimeout(() => {
|
||||
showMenuModel(indexPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerDom" class="tags-view" v-if="!showTags">
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper" class="scroll-container">
|
||||
<div
|
||||
v-for="(item, index) in dynamicTagList"
|
||||
:key="index"
|
||||
:ref="'dynamic' + index"
|
||||
:class="[
|
||||
'scroll-item is-closable',
|
||||
$route.path === item.path ? 'is-active' : '',
|
||||
$route.path === item.path && showModel === 'card' ? 'card-active' : ''
|
||||
]"
|
||||
@contextmenu.prevent="openMenu(item, $event)"
|
||||
@mouseenter.prevent="onMouseenter(item, index)"
|
||||
@mouseleave.prevent="onMouseleave(item, index)"
|
||||
>
|
||||
<router-link :to="item.path" @click="tagOnClick(item)">{{
|
||||
$t(item.meta.title)
|
||||
}}</router-link>
|
||||
<span
|
||||
v-if="
|
||||
($route.path === item.path && index !== 0) ||
|
||||
(index === activeIndex && index !== 0)
|
||||
"
|
||||
class="el-icon-close"
|
||||
@click="deleteMenu(item)"
|
||||
></span>
|
||||
<div
|
||||
:ref="'schedule' + index"
|
||||
v-if="showModel !== 'card'"
|
||||
:class="[$route.path === item.path ? 'schedule-active' : '']"
|
||||
></div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<!-- 右键菜单按钮 -->
|
||||
<transition name="el-zoom-in-top">
|
||||
<ul
|
||||
v-show="visible"
|
||||
:key="Math.random()"
|
||||
:style="{ left: buttonLeft + 'px', top: buttonTop + 'px' }"
|
||||
class="contextmenu"
|
||||
>
|
||||
<div
|
||||
v-for="(item, key) in tagsViews"
|
||||
:key="key"
|
||||
style="display: flex; align-items: center"
|
||||
>
|
||||
<li v-if="item.show" @click="selectTag(key, item)">
|
||||
<component :is="item.icon" :key="key" />
|
||||
{{ $t(item.text) }}
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</transition>
|
||||
<!-- 右侧功能按钮 -->
|
||||
<ul class="right-button">
|
||||
<li>
|
||||
<i
|
||||
:title="$t('message.hsrefreshRoute')"
|
||||
class="el-icon-refresh-right rotate"
|
||||
@click="onFresh"
|
||||
></i>
|
||||
</li>
|
||||
<li>
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<i class="el-icon-arrow-down"></i>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="(item, key) in tagsViews"
|
||||
:key="key"
|
||||
:divided="item.divided"
|
||||
:disabled="item.disabled"
|
||||
@click="onClickDrop(key, item)"
|
||||
>
|
||||
<component :is="item.icon" :key="key" />
|
||||
{{ $t(item.text) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<slot></slot>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes scheduleInWidth {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes scheduleOutWidth {
|
||||
from {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes rotate {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes rotate {
|
||||
from {
|
||||
-moz-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
-moz-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-o-keyframes rotate {
|
||||
from {
|
||||
-o-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
-o-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.tags-view {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
box-shadow: 0 0 1px #888;
|
||||
|
||||
.scroll-item {
|
||||
border-radius: 3px 3px 0 0;
|
||||
padding: 2px 6px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-right: 4px;
|
||||
height: 28px;
|
||||
line-height: 25px;
|
||||
transition: all 0.4s;
|
||||
|
||||
.el-icon-close {
|
||||
font-size: 10px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: #b4bccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-closable:not(:first-child) {
|
||||
&:hover {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
padding: 0 4px 0 4px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
padding: 5px 0;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
|
||||
.scroll-item {
|
||||
&:nth-child(1) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-wrapper {
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
color: #000000d9;
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
outline: 0;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 7px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
|
||||
li {
|
||||
width: 40px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
border-right: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dropdown-menu {
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
color: #606266;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item) i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item--divided::before {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.is-active {
|
||||
background-color: #eaf4fe;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
|
||||
a {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片模式
|
||||
.card-active {
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
// 卡片模式下鼠标移入显示蓝色边框
|
||||
.card-in {
|
||||
border: 1px solid #1890ff;
|
||||
color: #1890ff;
|
||||
|
||||
a {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
// 卡片模式下鼠标移出隐藏蓝色边框
|
||||
.card-out {
|
||||
border: none;
|
||||
color: #666;
|
||||
|
||||
a {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
// 灵动模式
|
||||
.schedule-active {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background: #1890ff;
|
||||
}
|
||||
// 灵动模式下鼠标移入显示蓝色进度条
|
||||
.schedule-in {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background: #1890ff;
|
||||
animation: scheduleInWidth 400ms ease-in;
|
||||
}
|
||||
// 灵动模式下鼠标移出隐藏蓝色进度条
|
||||
.schedule-out {
|
||||
width: 0;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background: #1890ff;
|
||||
animation: scheduleOutWidth 400ms ease-in;
|
||||
}
|
||||
// 刷新按钮动画效果
|
||||
.refresh-button {
|
||||
-webkit-animation: rotate 600ms linear infinite;
|
||||
-moz-animation: rotate 600ms linear infinite;
|
||||
-o-animation: rotate 600ms linear infinite;
|
||||
animation: rotate 600ms linear infinite;
|
||||
}
|
||||
</style>
|
253
src/layout/index.vue
Normal file
253
src/layout/index.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<script lang="ts">
|
||||
import { routerArrays } from "./types";
|
||||
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,
|
||||
unref,
|
||||
reactive,
|
||||
computed,
|
||||
onMounted,
|
||||
watchEffect,
|
||||
useCssModule,
|
||||
onBeforeMount,
|
||||
getCurrentInstance
|
||||
} from "vue";
|
||||
import { setType } from "./types";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import { toggleClass } from "/@/utils/operate";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { storageLocal } from "/@/utils/storage";
|
||||
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 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";
|
||||
|
||||
const pureSetting = useSettingStoreHook();
|
||||
const { hiddenMainContainer } = useCssModule();
|
||||
|
||||
const instance =
|
||||
getCurrentInstance().appContext.app.config.globalProperties.$storage;
|
||||
|
||||
const hiddenSideBar = ref(
|
||||
getCurrentInstance().appContext.config.globalProperties.$config?.HiddenSideBar
|
||||
);
|
||||
|
||||
const set: setType = reactive({
|
||||
sidebar: computed(() => {
|
||||
return useAppStoreHook().sidebar;
|
||||
}),
|
||||
|
||||
device: computed(() => {
|
||||
return useAppStoreHook().device;
|
||||
}),
|
||||
|
||||
fixedHeader: computed(() => {
|
||||
return pureSetting.fixedHeader;
|
||||
}),
|
||||
|
||||
classes: computed(() => {
|
||||
return {
|
||||
hideSidebar: !set.sidebar.opened,
|
||||
openSidebar: set.sidebar.opened,
|
||||
withoutAnimation: set.sidebar.withoutAnimation,
|
||||
mobile: set.device === "mobile"
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
const handleClickOutside = (params: boolean) => {
|
||||
useAppStoreHook().closeSideBar({ withoutAnimation: params });
|
||||
};
|
||||
|
||||
function setTheme(layoutModel: string) {
|
||||
let { layout } = storageLocal.getItem("responsive-layout");
|
||||
let theme = layout.match(/-(.*)/)[1];
|
||||
window.document.body.setAttribute("data-layout", layoutModel);
|
||||
window.document.body.setAttribute("data-theme", theme);
|
||||
instance.layout = { layout: `${layoutModel}-${theme}` };
|
||||
}
|
||||
|
||||
// 监听容器
|
||||
emitter.on("resize", ({ detail }) => {
|
||||
let { width } = detail;
|
||||
width <= 670 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (set.device === "mobile" && !set.sidebar.opened) {
|
||||
handleClickOutside(false);
|
||||
}
|
||||
});
|
||||
|
||||
const $_isMobile = () => {
|
||||
const rect = document.body.getBoundingClientRect();
|
||||
return rect.width - 1 < 992;
|
||||
};
|
||||
|
||||
const $_resizeHandler = () => {
|
||||
if (!document.hidden) {
|
||||
const isMobile = $_isMobile();
|
||||
useAppStoreHook().toggleDevice(isMobile ? "mobile" : "desktop");
|
||||
if (isMobile) {
|
||||
handleClickOutside(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function onFullScreen() {
|
||||
if (unref(hiddenSideBar)) {
|
||||
hiddenSideBar.value = false;
|
||||
toggleClass(
|
||||
false,
|
||||
hiddenMainContainer,
|
||||
document.querySelector(".main-container")
|
||||
);
|
||||
} else {
|
||||
hiddenSideBar.value = true;
|
||||
toggleClass(
|
||||
true,
|
||||
hiddenMainContainer,
|
||||
document.querySelector(".main-container")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const isMobile = $_isMobile();
|
||||
if (isMobile) {
|
||||
useAppStoreHook().toggleDevice("mobile");
|
||||
handleClickOutside(true);
|
||||
}
|
||||
toggleClass(
|
||||
unref(hiddenSideBar),
|
||||
hiddenMainContainer,
|
||||
document.querySelector(".main-container")
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
useEventListener("resize", $_resizeHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['app-wrapper', set.classes]" v-resize>
|
||||
<div
|
||||
v-show="
|
||||
set.device === 'mobile' &&
|
||||
set.sidebar.opened &&
|
||||
layout.includes('vertical')
|
||||
"
|
||||
class="drawer-bg"
|
||||
@click="handleClickOutside(false)"
|
||||
/>
|
||||
<Vertical v-show="!hiddenSideBar && layout.includes('vertical')" />
|
||||
<div class="main-container">
|
||||
<div :class="{ 'fixed-header': set.fixedHeader }">
|
||||
<!-- 顶部导航栏 -->
|
||||
<navbar v-show="!hiddenSideBar && layout.includes('vertical')" />
|
||||
<!-- tabs标签页 -->
|
||||
<Horizontal v-show="!hiddenSideBar && layout.includes('horizontal')" />
|
||||
<tag>
|
||||
<span @click="onFullScreen">
|
||||
<fullScreen v-if="!hiddenSideBar" />
|
||||
<exitScreen v-else />
|
||||
</span>
|
||||
</tag>
|
||||
</div>
|
||||
<!-- 主体内容 -->
|
||||
<app-main />
|
||||
</div>
|
||||
<!-- 系统设置 -->
|
||||
<setting />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped module>
|
||||
.hiddenMainContainer {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin clearfix {
|
||||
&::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
@include clearfix;
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.mobile.openSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - 210px);
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.re-screen {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
64
src/layout/types.ts
Normal file
64
src/layout/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export type RouteConfigs = {
|
||||
path?: string;
|
||||
parentPath?: string;
|
||||
meta?: {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
showLink?: boolean;
|
||||
savedPosition?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type relativeStorageType = {
|
||||
routesInStorage: Array<RouteConfigs>;
|
||||
};
|
||||
|
||||
export type tagsViewsType = {
|
||||
icon: string;
|
||||
text: string;
|
||||
divided: boolean;
|
||||
disabled: boolean;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
export interface setType {
|
||||
sidebar: {
|
||||
opened: boolean;
|
||||
withoutAnimation: boolean;
|
||||
};
|
||||
device: string;
|
||||
fixedHeader: boolean;
|
||||
classes: {
|
||||
hideSidebar: boolean;
|
||||
openSidebar: boolean;
|
||||
withoutAnimation: boolean;
|
||||
mobile: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const routerArrays: Array<RouteConfigs> = [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: {
|
||||
title: "message.hshome",
|
||||
icon: "el-icon-s-home",
|
||||
showLink: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export type childrenType = {
|
||||
path?: string;
|
||||
noShowingChildren?: boolean;
|
||||
children?: childrenType[];
|
||||
value: unknown;
|
||||
meta?: {
|
||||
icon?: string;
|
||||
title?: string;
|
||||
extraIcon?: {
|
||||
svg?: boolean;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user