项目初始化
This commit is contained in:
55
web/src/layouts/backend/components/aside.vue
Normal file
55
web/src/layouts/backend/components/aside.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-aside v-if="!navTabs.state.tabFullScreen" :class="['layout-aside-' + config.layout.layoutMode, config.layout.shrink ? 'shrink' : '']">
|
||||
<Logo v-if="config.layout.menuShowTopBar" />
|
||||
<MenuVerticalChildren v-if="config.layout.layoutMode == 'Double'" />
|
||||
<MenuVertical v-else />
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Logo from '/@/layouts/backend/components/logo.vue'
|
||||
import MenuVertical from '/@/layouts/backend/components/menus/menuVertical.vue'
|
||||
import MenuVerticalChildren from '/@/layouts/backend/components/menus/menuVerticalChildren.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
|
||||
|
||||
defineOptions({
|
||||
name: 'layout/aside',
|
||||
})
|
||||
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
|
||||
const menuWidth = computed(() => config.menuWidth())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-aside-Default:not(.shrink) {
|
||||
background: var(--ba-bg-color-overlay);
|
||||
margin: 16px 0 16px 16px;
|
||||
height: calc(100% - 32px);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
width: v-bind(menuWidth);
|
||||
}
|
||||
.layout-aside-Default.shrink,
|
||||
.layout-aside-Classic,
|
||||
.layout-aside-Double {
|
||||
background: var(--ba-bg-color-overlay);
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
width: v-bind(menuWidth);
|
||||
}
|
||||
.shrink {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: v-bind('SYSTEM_ZINDEX');
|
||||
}
|
||||
</style>
|
||||
259
web/src/layouts/backend/components/baAccount.vue
Normal file
259
web/src/layouts/backend/components/baAccount.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<!-- 模块市场 和 CRUD 记录页面等地方的 BuildAdmin 官方账户登录弹窗 -->
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog v-model="model" class="ba-account-dialog" width="25%" :title="t('layouts.Member information')">
|
||||
<template v-if="baAccount.token">
|
||||
<div v-loading="state.loading" class="userinfo">
|
||||
<div class="user-avatar-box">
|
||||
<img class="user-avatar" :src="baAccount.avatar" alt="" />
|
||||
<Icon
|
||||
class="user-avatar-gender"
|
||||
:name="baAccount.getGenderIcon()['name']"
|
||||
size="14"
|
||||
:color="baAccount.getGenderIcon()['color']"
|
||||
/>
|
||||
</div>
|
||||
<p class="username">{{ baAccount.nickname }}</p>
|
||||
<p class="user-info">
|
||||
<span>{{ $t('Integral') + ' ' + baAccount.score }}</span>
|
||||
<span>{{ $t('Balance') + ' ' + baAccount.money }}</span>
|
||||
</p>
|
||||
<div class="userinfo-buttons">
|
||||
<a href="https://uni.buildadmin.com/user" target="_blank" rel="noopener noreferrer">
|
||||
<el-button v-blur size="default" type="primary">
|
||||
{{ $t('layouts.Member center') }}
|
||||
</el-button>
|
||||
</a>
|
||||
<el-button @click="baAccount.logout()" v-blur size="default" type="warning">{{ $t('layouts.Logout') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ba-login">
|
||||
<h3 class="ba-title">{{ t('layouts.Login to the buildadmin') }}</h3>
|
||||
<el-form
|
||||
@keyup.enter="onBaAccountSubmitPre()"
|
||||
ref="baAccountFormRef"
|
||||
:rules="baAccountFormRules"
|
||||
class="ba-account-login-form"
|
||||
:model="state.user"
|
||||
>
|
||||
<FormItem
|
||||
v-model="state.user.username"
|
||||
type="string"
|
||||
prop="username"
|
||||
:placeholder="t('layouts.Please enter buildadmin account name or email')"
|
||||
:input-attr="{
|
||||
size: 'large',
|
||||
}"
|
||||
/>
|
||||
<FormItem
|
||||
v-model="state.user.password"
|
||||
type="password"
|
||||
prop="password"
|
||||
:placeholder="t('layouts.Please enter the buildadmin account password')"
|
||||
:input-attr="{
|
||||
size: 'large',
|
||||
}"
|
||||
/>
|
||||
<el-form-item class="form-buttons">
|
||||
<el-button @click="onBaAccountSubmitPre()" :loading="state.submitLoading" round type="primary" size="large">
|
||||
{{ t('layouts.Login') }}
|
||||
</el-button>
|
||||
<a
|
||||
target="_blank"
|
||||
class="ba-account-register"
|
||||
href="https://uni.buildadmin.com/user/login?type=register"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<el-button round plain type="info" size="large"> {{ t('layouts.Register') }} </el-button>
|
||||
</a>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { reactive, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { baAccountCheckIn, baAccountGetUserInfo } from '/@/api/backend/index'
|
||||
import clickCaptcha from '/@/components/clickCaptcha'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useBaAccount } from '/@/stores/baAccount'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { uuid } from '/@/utils/random'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
|
||||
const { t } = useI18n()
|
||||
const baAccount = useBaAccount()
|
||||
const siteConfig = useSiteConfig()
|
||||
const model = defineModel<boolean>()
|
||||
const baAccountFormRef = useTemplateRef('baAccountFormRef')
|
||||
|
||||
interface Props {
|
||||
loginCallback?: (res: ApiResponse) => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loginCallback: () => {},
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
submitLoading: false,
|
||||
user: {
|
||||
tab: 'login',
|
||||
username: '',
|
||||
password: '',
|
||||
captchaId: uuid(),
|
||||
captchaInfo: '',
|
||||
keep: false,
|
||||
},
|
||||
})
|
||||
|
||||
const onBaAccountSubmitPre = () => {
|
||||
baAccountFormRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
clickCaptcha(state.user.captchaId, (captchaInfo: string) => onBaAccountSubmit(captchaInfo), { apiBaseURL: siteConfig.apiUrl })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onBaAccountSubmit = (captchaInfo = '') => {
|
||||
state.submitLoading = true
|
||||
state.user.captchaInfo = captchaInfo
|
||||
baAccountCheckIn(state.user)
|
||||
.then((res) => {
|
||||
baAccount.dataFill(res.data.userInfo, false)
|
||||
props.loginCallback(res)
|
||||
})
|
||||
.finally(() => {
|
||||
state.submitLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
const baAccountFormRules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('layouts.Username') })],
|
||||
password: [buildValidatorData({ name: 'required', title: t('layouts.Password') }), buildValidatorData({ name: 'password' })],
|
||||
})
|
||||
|
||||
watch(
|
||||
() => model.value,
|
||||
(newVal) => {
|
||||
if (newVal && baAccount.token) {
|
||||
baAccountGetUserInfo()
|
||||
.then((res) => {
|
||||
baAccount.dataFill(res.data.userInfo)
|
||||
})
|
||||
.catch(() => {
|
||||
baAccount.removeToken()
|
||||
})
|
||||
.finally(() => {
|
||||
state.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.userinfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
.username {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
font-size: var(--el-font-size-large);
|
||||
font-weight: bold;
|
||||
}
|
||||
.user-info {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
font-size: var(--el-font-size-base);
|
||||
span {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
.user-avatar-box {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.user-avatar {
|
||||
display: block;
|
||||
width: 100px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
.user-avatar-gender {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 10px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--el-box-shadow);
|
||||
}
|
||||
.userinfo-buttons {
|
||||
margin-top: 10px;
|
||||
a {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ba-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 20px;
|
||||
.ba-title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.form-buttons {
|
||||
.el-button {
|
||||
width: 100%;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 300;
|
||||
margin-top: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.ba-account-register {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ba-account-login-form {
|
||||
width: 350px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 会员信息弹窗-s */
|
||||
@media screen and (max-width: 1440px) {
|
||||
:deep(.ba-account-dialog) {
|
||||
--el-dialog-width: 40% !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
:deep(.ba-account-dialog) {
|
||||
--el-dialog-width: 70% !important;
|
||||
}
|
||||
}
|
||||
/* 会员信息弹窗-e */
|
||||
</style>
|
||||
72
web/src/layouts/backend/components/closeFullScreen.vue
Normal file
72
web/src/layouts/backend/components/closeFullScreen.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div :title="$t('layouts.Exit full screen')" @mouseover.stop="onMouseover" @mouseout.stop="onMouseout">
|
||||
<div @click.stop="onCloseFullScreen" class="close-full-screen" :style="{ top: state.closeBoxTop + 'px' }">
|
||||
<Icon name="el-icon-Close" />
|
||||
</div>
|
||||
<div class="close-full-screen-on"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
|
||||
|
||||
const navTabs = useNavTabs()
|
||||
|
||||
const state = reactive({
|
||||
closeBoxTop: 20,
|
||||
})
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
state.closeBoxTop = -30
|
||||
}, 300)
|
||||
})
|
||||
/*
|
||||
* 鼠标滑到顶部显示关闭全屏按钮
|
||||
* 要检查 hover 的元素在外部,直接使用事件而不是css
|
||||
*/
|
||||
const onMouseover = () => {
|
||||
state.closeBoxTop = 20
|
||||
}
|
||||
const onMouseout = () => {
|
||||
state.closeBoxTop = -30
|
||||
}
|
||||
const onCloseFullScreen = () => {
|
||||
navTabs.setFullScreen(false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.close-full-screen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
right: calc(50% - 20px);
|
||||
z-index: v-bind('SYSTEM_ZINDEX');
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background-color: rgba($color: #000000, $alpha: 0.1);
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: all 0.3s ease;
|
||||
.icon {
|
||||
color: rgba($color: #000000, $alpha: 0.6) !important;
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba($color: #000000, $alpha: 0.3);
|
||||
.icon {
|
||||
color: rgba($color: #ffffff, $alpha: 0.6) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.close-full-screen-on {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: v-bind('SYSTEM_ZINDEX - 1');
|
||||
height: 60px;
|
||||
width: 100px;
|
||||
left: calc(50% - 50px);
|
||||
}
|
||||
</style>
|
||||
420
web/src/layouts/backend/components/config.vue
Normal file
420
web/src/layouts/backend/components/config.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<div class="layout-config-drawer">
|
||||
<el-drawer :model-value="configStore.layout.showDrawer" :title="t('layouts.Layout configuration')" size="310px" @close="onCloseDrawer">
|
||||
<el-scrollbar class="layout-mode-style-scrollbar">
|
||||
<el-form :model="configStore.layout">
|
||||
<div class="layout-mode-styles-box">
|
||||
<el-divider border-style="dashed">{{ t('layouts.Layout mode') }}</el-divider>
|
||||
<div class="layout-mode-box-style">
|
||||
<el-row class="layout-mode-box-style-row" :gutter="10">
|
||||
<el-col :span="12">
|
||||
<div
|
||||
@click="setLayoutMode('Default')"
|
||||
class="layout-mode-style default"
|
||||
:class="configStore.layout.layoutMode == 'Default' ? 'active' : ''"
|
||||
>
|
||||
<div class="layout-mode-style-box">
|
||||
<div class="layout-mode-style-aside"></div>
|
||||
<div class="layout-mode-style-container-box">
|
||||
<div class="layout-mode-style-header"></div>
|
||||
<div class="layout-mode-style-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-mode-style-name">{{ t('layouts.default') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div
|
||||
@click="setLayoutMode('Classic')"
|
||||
class="layout-mode-style classic"
|
||||
:class="configStore.layout.layoutMode == 'Classic' ? 'active' : ''"
|
||||
>
|
||||
<div class="layout-mode-style-box">
|
||||
<div class="layout-mode-style-aside"></div>
|
||||
<div class="layout-mode-style-container-box">
|
||||
<div class="layout-mode-style-header"></div>
|
||||
<div class="layout-mode-style-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-mode-style-name">{{ t('layouts.classic') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<div
|
||||
@click="setLayoutMode('Streamline')"
|
||||
class="layout-mode-style streamline"
|
||||
:class="configStore.layout.layoutMode == 'Streamline' ? 'active' : ''"
|
||||
>
|
||||
<div class="layout-mode-style-box">
|
||||
<div class="layout-mode-style-container-box">
|
||||
<div class="layout-mode-style-header"></div>
|
||||
<div class="layout-mode-style-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-mode-style-name">{{ t('layouts.Single column') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div
|
||||
@click="setLayoutMode('Double')"
|
||||
class="layout-mode-style double"
|
||||
:class="configStore.layout.layoutMode == 'Double' ? 'active' : ''"
|
||||
>
|
||||
<div class="layout-mode-style-box">
|
||||
<div class="layout-mode-style-aside"></div>
|
||||
<div class="layout-mode-style-container-box">
|
||||
<div class="layout-mode-style-header"></div>
|
||||
<div class="layout-mode-style-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-mode-style-name">{{ t('layouts.Double column') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-divider border-style="dashed">{{ t('layouts.overall situation') }}</el-divider>
|
||||
<div class="layout-config-global">
|
||||
<el-form-item size="large" :label="t('layouts.Dark mode')">
|
||||
<DarkSwitch @click="toggleDark()" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Background page switching animation')">
|
||||
<el-select
|
||||
@change="onCommitState($event, 'mainAnimation')"
|
||||
:model-value="configStore.layout.mainAnimation"
|
||||
:placeholder="t('layouts.Please select an animation name')"
|
||||
>
|
||||
<el-option label="slide-right" value="slide-right"></el-option>
|
||||
<el-option label="slide-left" value="slide-left"></el-option>
|
||||
<el-option label="el-fade-in-linear" value="el-fade-in-linear"></el-option>
|
||||
<el-option label="el-fade-in" value="el-fade-in"></el-option>
|
||||
<el-option label="el-zoom-in-center" value="el-zoom-in-center"></el-option>
|
||||
<el-option label="el-zoom-in-top" value="el-zoom-in-top"></el-option>
|
||||
<el-option label="el-zoom-in-bottom" value="el-zoom-in-bottom"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-divider border-style="dashed">{{ t('layouts.sidebar') }}</el-divider>
|
||||
<div class="layout-config-aside">
|
||||
<el-form-item :label="t('layouts.Side menu bar background color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'menuBackground')"
|
||||
:model-value="configStore.getColorVal('menuBackground')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu text color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'menuColor')"
|
||||
:model-value="configStore.getColorVal('menuColor')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu active item background color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'menuActiveBackground')"
|
||||
:model-value="configStore.getColorVal('menuActiveBackground')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu active item text color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'menuActiveColor')"
|
||||
:model-value="configStore.getColorVal('menuActiveColor')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Show side menu top bar (logo bar)')">
|
||||
<el-switch
|
||||
@change="onCommitState($event, 'menuShowTopBar')"
|
||||
:model-value="configStore.layout.menuShowTopBar"
|
||||
></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu top bar background color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'menuTopBarBackground')"
|
||||
:model-value="configStore.getColorVal('menuTopBarBackground')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu width (when expanded)')">
|
||||
<el-input
|
||||
@input="onCommitState($event, 'menuWidth')"
|
||||
type="number"
|
||||
:step="10"
|
||||
:model-value="configStore.layout.menuWidth"
|
||||
>
|
||||
<template #append>px</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu default icon')">
|
||||
<IconSelector
|
||||
@change="onCommitMenuDefaultIcon($event, 'menuDefaultIcon')"
|
||||
:model-value="configStore.layout.menuDefaultIcon"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu horizontal collapse')">
|
||||
<el-switch @change="onCommitState($event, 'menuCollapse')" :model-value="configStore.layout.menuCollapse"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Side menu accordion')">
|
||||
<el-switch
|
||||
@change="onCommitState($event, 'menuUniqueOpened')"
|
||||
:model-value="configStore.layout.menuUniqueOpened"
|
||||
></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-divider border-style="dashed">{{ t('layouts.Top bar') }}</el-divider>
|
||||
<div class="layout-config-aside">
|
||||
<el-form-item :label="t('layouts.Top bar background color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'headerBarBackground')"
|
||||
:model-value="configStore.getColorVal('headerBarBackground')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Top bar text color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'headerBarTabColor')"
|
||||
:model-value="configStore.getColorVal('headerBarTabColor')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Background color when hovering over the top bar')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'headerBarHoverBackground')"
|
||||
:model-value="configStore.getColorVal('headerBarHoverBackground')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Top bar menu active item background color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'headerBarTabActiveBackground')"
|
||||
:model-value="configStore.getColorVal('headerBarTabActiveBackground')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('layouts.Top bar menu active item text color')">
|
||||
<el-color-picker
|
||||
@change="onCommitColorState($event, 'headerBarTabActiveColor')"
|
||||
:model-value="configStore.getColorVal('headerBarTabActiveColor')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-popconfirm
|
||||
@confirm="restoreDefault"
|
||||
:title="t('layouts.Are you sure you want to restore all configurations to the default values?')"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="ba-center">
|
||||
<el-button class="w80" type="info">{{ t('layouts.Restore default') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import IconSelector from '/@/components/baInput/components/iconSelector.vue'
|
||||
import DarkSwitch from '/@/layouts/common/components/darkSwitch.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { BEFORE_RESIZE_LAYOUT, STORE_CONFIG } from '/@/stores/constant/cacheKey'
|
||||
import type { Layout } from '/@/stores/interface'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { Local, Session } from '/@/utils/storage'
|
||||
import toggleDark from '/@/utils/useDark'
|
||||
|
||||
const { t } = useI18n()
|
||||
const configStore = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
const router = useRouter()
|
||||
|
||||
const onCommitState = (value: any, name: any) => {
|
||||
configStore.setLayout(name, value)
|
||||
}
|
||||
|
||||
const onCommitColorState = (value: string | null, name: keyof Layout) => {
|
||||
if (value === null) return
|
||||
const colors = configStore.layout[name] as string[]
|
||||
if (configStore.layout.isDark) {
|
||||
colors[1] = value
|
||||
} else {
|
||||
colors[0] = value
|
||||
}
|
||||
configStore.setLayout(name, colors)
|
||||
}
|
||||
|
||||
const setLayoutMode = (mode: string) => {
|
||||
Session.set(BEFORE_RESIZE_LAYOUT, {
|
||||
layoutMode: mode,
|
||||
menuCollapse: configStore.layout.menuCollapse,
|
||||
})
|
||||
configStore.setLayoutMode(mode)
|
||||
}
|
||||
|
||||
// 修改默认菜单图标
|
||||
const onCommitMenuDefaultIcon = (value: any, name: any) => {
|
||||
configStore.setLayout(name, value)
|
||||
|
||||
const menus = navTabs.state.tabsViewRoutes
|
||||
navTabs.setTabsViewRoutes([])
|
||||
nextTick(() => {
|
||||
navTabs.setTabsViewRoutes(menus)
|
||||
})
|
||||
}
|
||||
|
||||
const onCloseDrawer = () => {
|
||||
configStore.setLayout('showDrawer', false)
|
||||
}
|
||||
|
||||
const restoreDefault = () => {
|
||||
Local.remove(STORE_CONFIG)
|
||||
Session.remove(BEFORE_RESIZE_LAYOUT)
|
||||
router.go(0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-config-drawer :deep(.el-input__inner) {
|
||||
padding: 0 0 0 6px;
|
||||
}
|
||||
.layout-config-drawer :deep(.el-input-group__append) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.layout-config-drawer :deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.layout-config-drawer :deep(.el-drawer__body) {
|
||||
padding: 0;
|
||||
}
|
||||
.layout-mode-styles-box {
|
||||
padding: 20px;
|
||||
}
|
||||
.layout-mode-box-style-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.layout-mode-style {
|
||||
position: relative;
|
||||
height: 100px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: var(--el-border-radius-small);
|
||||
&:hover,
|
||||
&.active {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
.layout-mode-style-name {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-color-primary-light-5);
|
||||
border-radius: 50%;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border: 1px solid var(--el-color-primary-light-3);
|
||||
}
|
||||
.layout-mode-style-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
&.default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.layout-mode-style-aside {
|
||||
width: 18%;
|
||||
height: 90%;
|
||||
background-color: var(--el-border-color-lighter);
|
||||
}
|
||||
.layout-mode-style-container-box {
|
||||
width: 68%;
|
||||
height: 90%;
|
||||
margin-left: 4%;
|
||||
.layout-mode-style-header {
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
background-color: var(--el-border-color-lighter);
|
||||
}
|
||||
.layout-mode-style-container {
|
||||
width: 100%;
|
||||
height: 85%;
|
||||
background-color: var(--el-border-color-extra-light);
|
||||
margin-top: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.classic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.layout-mode-style-aside {
|
||||
width: 18%;
|
||||
height: 100%;
|
||||
background-color: var(--el-border-color-lighter);
|
||||
}
|
||||
.layout-mode-style-container-box {
|
||||
width: 82%;
|
||||
height: 100%;
|
||||
.layout-mode-style-header {
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
background-color: var(--el-border-color);
|
||||
}
|
||||
.layout-mode-style-container {
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
background-color: var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.streamline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.layout-mode-style-container-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.layout-mode-style-header {
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
background-color: var(--el-border-color);
|
||||
}
|
||||
.layout-mode-style-container {
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
background-color: var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.double {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.layout-mode-style-aside {
|
||||
width: 18%;
|
||||
height: 100%;
|
||||
background-color: var(--el-border-color);
|
||||
}
|
||||
.layout-mode-style-container-box {
|
||||
width: 82%;
|
||||
height: 100%;
|
||||
.layout-mode-style-header {
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
background-color: var(--el-border-color);
|
||||
}
|
||||
.layout-mode-style-container {
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
background-color: var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.w80 {
|
||||
width: 90%;
|
||||
}
|
||||
</style>
|
||||
28
web/src/layouts/backend/components/header.vue
Normal file
28
web/src/layouts/backend/components/header.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<el-header v-if="!navTabs.state.tabFullScreen" class="layout-header">
|
||||
<component :is="config.layout.layoutMode + 'NavBar'"></component>
|
||||
</el-header>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import DefaultNavBar from '/@/layouts/backend/components/navBar/default.vue'
|
||||
import ClassicNavBar from '/@/layouts/backend/components/navBar/classic.vue'
|
||||
import StreamlineNavBar from '/@/layouts/backend/components/menus/menuHorizontal.vue'
|
||||
import DoubleNavBar from '/@/layouts/backend/components/navBar/double.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'layout/header',
|
||||
components: { DefaultNavBar, ClassicNavBar, StreamlineNavBar, DoubleNavBar },
|
||||
})
|
||||
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-header {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
79
web/src/layouts/backend/components/logo.vue
Normal file
79
web/src/layouts/backend/components/logo.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="layout-logo">
|
||||
<img v-if="!config.layout.menuCollapse" class="logo-img" src="~assets/logo.png" alt="logo" />
|
||||
<div v-if="!config.layout.menuCollapse" :style="{ color: config.getColorVal('menuActiveColor') }" class="website-name">
|
||||
{{ siteConfig.siteName }}
|
||||
</div>
|
||||
<Icon
|
||||
v-if="config.layout.layoutMode != 'Streamline'"
|
||||
@click="onMenuCollapse"
|
||||
:name="config.layout.menuCollapse ? 'fa fa-indent' : 'fa fa-dedent'"
|
||||
:class="config.layout.menuCollapse ? 'unfold' : ''"
|
||||
:color="config.getColorVal('menuActiveColor')"
|
||||
size="18"
|
||||
class="fold"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { closeShade } from '/@/utils/pageShade'
|
||||
import { Session } from '/@/utils/storage'
|
||||
import { BEFORE_RESIZE_LAYOUT } from '/@/stores/constant/cacheKey'
|
||||
import { setNavTabsWidth } from '/@/utils/layout'
|
||||
|
||||
const config = useConfig()
|
||||
const siteConfig = useSiteConfig()
|
||||
|
||||
const onMenuCollapse = function () {
|
||||
if (config.layout.shrink && !config.layout.menuCollapse) {
|
||||
closeShade()
|
||||
}
|
||||
|
||||
config.setLayout('menuCollapse', !config.layout.menuCollapse)
|
||||
|
||||
Session.set(BEFORE_RESIZE_LAYOUT, {
|
||||
layoutMode: config.layout.layoutMode,
|
||||
menuCollapse: config.layout.menuCollapse,
|
||||
})
|
||||
|
||||
// 等待侧边栏动画结束后重新计算导航栏宽度
|
||||
setTimeout(() => {
|
||||
setNavTabsWidth()
|
||||
}, 350)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-logo {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
background: v-bind('config.layout.layoutMode != "Streamline" ? config.getColorVal("menuTopBarBackground"):"transparent"');
|
||||
}
|
||||
.logo-img {
|
||||
width: 28px;
|
||||
}
|
||||
.website-name {
|
||||
display: block;
|
||||
width: 180px;
|
||||
padding-left: 4px;
|
||||
font-size: var(--el-font-size-extra-large);
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fold {
|
||||
margin-left: auto;
|
||||
}
|
||||
.unfold {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
110
web/src/layouts/backend/components/menus/menuHorizontal.vue
Normal file
110
web/src/layouts/backend/components/menus/menuHorizontal.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="layouts-menu-horizontal">
|
||||
<div class="menu-horizontal-logo" v-if="config.layout.menuShowTopBar">
|
||||
<Logo />
|
||||
</div>
|
||||
<el-scrollbar ref="layoutMenuScrollbarRef" class="horizontal-menus-scrollbar">
|
||||
<el-menu ref="layoutMenuRef" class="menu-horizontal" mode="horizontal" :default-active="state.defaultActive">
|
||||
<MenuTree :extends="{ position: 'horizontal', level: 1 }" :menus="navTabs.state.tabsViewRoutes" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<NavMenus />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import Logo from '/@/layouts/backend/components/logo.vue'
|
||||
import MenuTree from '/@/layouts/backend/components/menus/menuTree.vue'
|
||||
import NavMenus from '/@/layouts/backend/components/navMenus.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { layoutMenuRef, layoutMenuScrollbarRef } from '/@/stores/refs'
|
||||
import horizontalScroll from '/@/utils/horizontalScroll'
|
||||
import { getMenuKey } from '/@/utils/router'
|
||||
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
const route = useRoute()
|
||||
|
||||
const state = reactive({
|
||||
defaultActive: '',
|
||||
})
|
||||
|
||||
/**
|
||||
* 激活当前路由对应的菜单
|
||||
*/
|
||||
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
|
||||
// 以路由 fullPath 匹配的菜单优先,且 fullPath 无匹配时,回退到 path 的匹配菜单
|
||||
const tabView = navTabs.getTabsViewDataByRoute(currentRoute)
|
||||
if (tabView) {
|
||||
state.defaultActive = getMenuKey(tabView, tabView.meta!.matched as string)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动条横向滚动到激活菜单所在位置
|
||||
*/
|
||||
const verticalMenusScroll = () => {
|
||||
setTimeout(() => {
|
||||
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.menu-horizontal li.is-active')
|
||||
if (activeMenu) {
|
||||
layoutMenuScrollbarRef.value?.setScrollLeft(activeMenu.offsetLeft)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentRouteActive(route)
|
||||
verticalMenusScroll()
|
||||
|
||||
new horizontalScroll(layoutMenuScrollbarRef.value!.wrapRef!)
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
currentRouteActive(to)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layouts-menu-horizontal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: var(--el-header-height);
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
border-bottom: 1px solid var(--el-color-info-light-8);
|
||||
}
|
||||
.menu-horizontal-logo {
|
||||
width: 180px;
|
||||
&:hover {
|
||||
background-color: v-bind('config.getColorVal("headerBarHoverBackground")');
|
||||
}
|
||||
}
|
||||
.horizontal-menus-scrollbar {
|
||||
flex: 1;
|
||||
height: var(--el-header-height);
|
||||
}
|
||||
.menu-horizontal {
|
||||
border: none;
|
||||
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
|
||||
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
|
||||
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
|
||||
}
|
||||
|
||||
.el-sub-menu .icon,
|
||||
.el-menu-item .icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.is-active .icon {
|
||||
color: var(--el-menu-active-color) !important;
|
||||
}
|
||||
.el-menu-item.is-active {
|
||||
background-color: v-bind('config.getColorVal("menuActiveBackground")');
|
||||
}
|
||||
</style>
|
||||
84
web/src/layouts/backend/components/menus/menuTree.vue
Normal file
84
web/src/layouts/backend/components/menus/menuTree.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<template v-for="menu in props.menus">
|
||||
<template v-if="menu.children && menu.children.length > 0">
|
||||
<el-sub-menu @click="onClickSubMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
||||
<template #title>
|
||||
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
||||
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span>
|
||||
</template>
|
||||
<MenuTree :extends="{ ...props.extends, level: props.extends.level + 1 }" :menus="menu.children" />
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-menu-item @click="onClickMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
||||
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
||||
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { getFirstRoute, getMenuKey, onClickMenu } from '/@/utils/router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const config = useConfig()
|
||||
|
||||
interface Props {
|
||||
menus: RouteRecordRaw[]
|
||||
extends?: {
|
||||
level: number
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
menus: () => [],
|
||||
extends: () => {
|
||||
return {
|
||||
level: 1,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* sub-menu-item 被点击 - 用于单栏布局和双栏布局
|
||||
* 顶栏菜单:点击时打开第一个菜单
|
||||
* 侧边菜单(若有):点击只展开收缩
|
||||
*
|
||||
* sub-menu-item 被点击时,也会触发到 menu-item 的点击事件,由 el-menu 内部触发,无法很好的排除,在此检查 level 值
|
||||
*/
|
||||
const onClickSubMenu = (menu: RouteRecordRaw) => {
|
||||
if (props.extends?.position == 'horizontal' && props.extends.level <= 1 && menu.children?.length) {
|
||||
const firstRoute = getFirstRoute(menu.children)
|
||||
if (firstRoute) {
|
||||
onClickMenu(firstRoute)
|
||||
} else {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: t('utils.No child menu to jump to!'),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-sub-menu .icon,
|
||||
.el-menu-item .icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.is-active > .icon {
|
||||
color: var(--el-menu-active-color) !important;
|
||||
}
|
||||
.el-menu-item.is-active {
|
||||
background-color: v-bind('config.getColorVal("menuActiveBackground")');
|
||||
}
|
||||
</style>
|
||||
81
web/src/layouts/backend/components/menus/menuVertical.vue
Normal file
81
web/src/layouts/backend/components/menus/menuVertical.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<el-scrollbar ref="layoutMenuScrollbarRef" class="vertical-menus-scrollbar">
|
||||
<el-menu
|
||||
class="layouts-menu-vertical"
|
||||
:collapse-transition="false"
|
||||
:unique-opened="config.layout.menuUniqueOpened"
|
||||
:default-active="state.defaultActive"
|
||||
:collapse="config.layout.menuCollapse"
|
||||
ref="layoutMenuRef"
|
||||
>
|
||||
<MenuTree :menus="navTabs.state.tabsViewRoutes" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import MenuTree from '/@/layouts/backend/components/menus/menuTree.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { layoutMenuRef, layoutMenuScrollbarRef } from '/@/stores/refs'
|
||||
import { getMenuKey } from '/@/utils/router'
|
||||
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
const route = useRoute()
|
||||
|
||||
const state = reactive({
|
||||
defaultActive: '',
|
||||
})
|
||||
|
||||
const verticalMenusScrollbarHeight = computed(() => {
|
||||
const menuTopBarHeight = config.layout.menuShowTopBar ? 50 : 0
|
||||
return 'calc(100% - ' + menuTopBarHeight + 'px)'
|
||||
})
|
||||
|
||||
/**
|
||||
* 激活当前路由对应的菜单
|
||||
*/
|
||||
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
|
||||
// 以路由 fullPath 匹配的菜单优先,且 fullPath 无匹配时,回退到 path 的匹配菜单
|
||||
const tabView = navTabs.getTabsViewDataByRoute(currentRoute)
|
||||
if (tabView) {
|
||||
state.defaultActive = getMenuKey(tabView, tabView.meta!.matched as string)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动条滚动到激活菜单所在位置
|
||||
*/
|
||||
const verticalMenusScroll = () => {
|
||||
setTimeout(() => {
|
||||
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.layouts-menu-vertical li.is-active')
|
||||
if (activeMenu) {
|
||||
layoutMenuScrollbarRef.value?.setScrollTop(activeMenu.offsetTop)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentRouteActive(route)
|
||||
verticalMenusScroll()
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
currentRouteActive(to)
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.vertical-menus-scrollbar {
|
||||
height: v-bind(verticalMenusScrollbarHeight);
|
||||
background-color: v-bind('config.getColorVal("menuBackground")');
|
||||
}
|
||||
.layouts-menu-vertical {
|
||||
border: 0;
|
||||
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
|
||||
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
|
||||
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-scrollbar ref="layoutMenuScrollbarRef" class="children-vertical-menus-scrollbar">
|
||||
<el-menu
|
||||
class="layouts-menu-vertical-children"
|
||||
:collapse-transition="false"
|
||||
:unique-opened="config.layout.menuUniqueOpened"
|
||||
:default-active="state.defaultActive"
|
||||
:collapse="config.layout.menuCollapse"
|
||||
ref="layoutMenuRef"
|
||||
>
|
||||
<MenuTree v-if="state.routeChildren.length > 0" :menus="state.routeChildren" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, useTemplateRef } from 'vue'
|
||||
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
import MenuTree from '/@/layouts/backend/components/menus/menuTree.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { layoutMenuRef } from '/@/stores/refs'
|
||||
import { getMenuKey } from '/@/utils/router'
|
||||
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
const route = useRoute()
|
||||
|
||||
const layoutMenuScrollbarRef = useTemplateRef('layoutMenuScrollbarRef')
|
||||
|
||||
const state: {
|
||||
defaultActive: string
|
||||
routeChildren: RouteRecordRaw[]
|
||||
} = reactive({
|
||||
defaultActive: '',
|
||||
routeChildren: [],
|
||||
})
|
||||
|
||||
const verticalMenusScrollbarHeight = computed(() => {
|
||||
const menuTopBarHeight = config.layout.menuShowTopBar ? 50 : 0
|
||||
return 'calc(100% - ' + menuTopBarHeight + 'px)'
|
||||
})
|
||||
|
||||
/**
|
||||
* 激活当前路由的菜单
|
||||
*/
|
||||
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
|
||||
// 以路由 fullPath 匹配的菜单优先,且 fullPath 无匹配时,回退到 path 的匹配菜单
|
||||
const tabView = navTabs.getTabsViewDataByRoute(currentRoute)
|
||||
if (tabView) {
|
||||
state.defaultActive = getMenuKey(tabView, tabView.meta!.matched as string)
|
||||
}
|
||||
|
||||
let routeChildren = navTabs.getTabsViewDataByRoute(currentRoute, 'above')
|
||||
if (routeChildren) {
|
||||
if (routeChildren.children && routeChildren.children.length > 0) {
|
||||
state.routeChildren = routeChildren.children
|
||||
} else {
|
||||
state.routeChildren = [routeChildren]
|
||||
}
|
||||
} else if (!state.routeChildren) {
|
||||
state.routeChildren = navTabs.state.tabsViewRoutes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏菜单滚动条滚动到激活菜单所在位置
|
||||
*/
|
||||
const verticalMenusScroll = () => {
|
||||
setTimeout(() => {
|
||||
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.layouts-menu-vertical-children li.is-active')
|
||||
if (activeMenu) {
|
||||
layoutMenuScrollbarRef.value?.setScrollTop(activeMenu.offsetTop)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentRouteActive(route)
|
||||
verticalMenusScroll()
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
currentRouteActive(to)
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.children-vertical-menus-scrollbar {
|
||||
height: v-bind(verticalMenusScrollbarHeight);
|
||||
background-color: v-bind('config.getColorVal("menuBackground")');
|
||||
}
|
||||
.layouts-menu-vertical-children {
|
||||
border: 0;
|
||||
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
|
||||
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
|
||||
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
|
||||
}
|
||||
</style>
|
||||
79
web/src/layouts/backend/components/navBar/classic.vue
Normal file
79
web/src/layouts/backend/components/navBar/classic.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="nav-bar">
|
||||
<div v-if="config.layout.shrink && config.layout.menuCollapse" class="unfold">
|
||||
<Icon @click="onMenuCollapse" name="fa fa-indent" :color="config.getColorVal('menuActiveColor')" size="18" />
|
||||
</div>
|
||||
<NavTabs v-if="!config.layout.shrink" ref="layoutNavTabsRef" />
|
||||
<NavMenus />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import NavTabs from '/@/layouts/backend/components/navBar/tabs.vue'
|
||||
import { layoutNavTabsRef } from '/@/stores/refs'
|
||||
import NavMenus from '../navMenus.vue'
|
||||
import { showShade } from '/@/utils/pageShade'
|
||||
|
||||
const config = useConfig()
|
||||
|
||||
const onMenuCollapse = () => {
|
||||
showShade('ba-aside-menu-shade', () => {
|
||||
config.setLayout('menuCollapse', true)
|
||||
})
|
||||
config.setLayout('menuCollapse', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
background-color: v-bind('config.getColorVal("headerBarBackground")');
|
||||
:deep(.nav-tabs) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.ba-nav-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
color: v-bind('config.getColorVal("headerBarTabColor")');
|
||||
transition: all 0.2s;
|
||||
-webkit-transition: all 0.2s;
|
||||
.close-icon {
|
||||
padding: 2px;
|
||||
margin: 2px 0 0 4px;
|
||||
}
|
||||
.close-icon:hover {
|
||||
background: var(--ba-color-primary-light);
|
||||
color: var(--el-border-color) !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
&.active {
|
||||
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
|
||||
}
|
||||
&:hover {
|
||||
background-color: v-bind('config.getColorVal("headerBarHoverBackground")');
|
||||
}
|
||||
}
|
||||
.nav-tabs-active-box {
|
||||
position: absolute;
|
||||
height: 50px;
|
||||
background-color: v-bind('config.getColorVal("headerBarTabActiveBackground")');
|
||||
transition: all 0.2s;
|
||||
-webkit-transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
.unfold {
|
||||
align-self: center;
|
||||
padding-left: var(--ba-main-space);
|
||||
}
|
||||
</style>
|
||||
84
web/src/layouts/backend/components/navBar/default.vue
Normal file
84
web/src/layouts/backend/components/navBar/default.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="nav-bar" :class="config.layout.shrink ? 'shrink' : ''">
|
||||
<!-- 小屏设备下的展开菜单按钮 -->
|
||||
<div v-if="config.layout.shrink && config.layout.menuCollapse" class="unfold">
|
||||
<Icon @click="onMenuCollapse" name="fa fa-indent" :color="config.getColorVal('menuActiveColor')" size="18" />
|
||||
</div>
|
||||
<NavTabs v-if="!config.layout.shrink" ref="layoutNavTabsRef" />
|
||||
<NavMenus />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import NavTabs from '/@/layouts/backend/components/navBar/tabs.vue'
|
||||
import NavMenus from '../navMenus.vue'
|
||||
import { layoutNavTabsRef } from '/@/stores/refs'
|
||||
import { showShade } from '/@/utils/pageShade'
|
||||
|
||||
const config = useConfig()
|
||||
|
||||
const onMenuCollapse = () => {
|
||||
showShade('ba-aside-menu-shade', () => {
|
||||
config.setLayout('menuCollapse', true)
|
||||
})
|
||||
config.setLayout('menuCollapse', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
margin: 20px var(--ba-main-space) 0 var(--ba-main-space);
|
||||
:deep(.nav-tabs) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.ba-nav-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
opacity: 0.7;
|
||||
color: v-bind('config.getColorVal("headerBarTabColor")');
|
||||
.close-icon {
|
||||
padding: 2px;
|
||||
margin: 2px 0 0 4px;
|
||||
}
|
||||
.close-icon:hover {
|
||||
background: var(--ba-color-primary-light);
|
||||
color: var(--el-border-color) !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
&.active {
|
||||
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.nav-tabs-active-box {
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
background-color: v-bind('config.getColorVal("headerBarTabActiveBackground")');
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: all 0.2s;
|
||||
-webkit-transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav-bar.shrink {
|
||||
width: 100%;
|
||||
background-color: v-bind('config.getColorVal("headerBarBackground")');
|
||||
margin: 0;
|
||||
.unfold {
|
||||
align-self: center;
|
||||
padding-left: var(--ba-main-space);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
97
web/src/layouts/backend/components/navBar/double.vue
Normal file
97
web/src/layouts/backend/components/navBar/double.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="layouts-menu-horizontal-double">
|
||||
<el-scrollbar ref="layoutMenuScrollbarRef" class="double-menus-scrollbar">
|
||||
<el-menu ref="layoutMenuRef" class="menu-horizontal" mode="horizontal" :default-active="state.defaultActive">
|
||||
<MenuTree :extends="{ position: 'horizontal', level: 1 }" :menus="navTabs.state.tabsViewRoutes" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<NavMenus />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import MenuTree from '/@/layouts/backend/components/menus/menuTree.vue'
|
||||
import NavMenus from '/@/layouts/backend/components/navMenus.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { layoutMenuRef, layoutMenuScrollbarRef } from '/@/stores/refs'
|
||||
import horizontalScroll from '/@/utils/horizontalScroll'
|
||||
import { getMenuKey } from '/@/utils/router'
|
||||
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
const route = useRoute()
|
||||
|
||||
const state = reactive({
|
||||
defaultActive: '',
|
||||
})
|
||||
|
||||
/**
|
||||
* 激活当前路由的菜单
|
||||
*/
|
||||
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
|
||||
// 以路由 fullPath 匹配的菜单优先,且 fullPath 无匹配时,回退到 path 的匹配菜单
|
||||
const tabView = navTabs.getTabsViewDataByRoute(currentRoute)
|
||||
if (tabView) {
|
||||
state.defaultActive = getMenuKey(tabView, tabView.meta!.matched as string)
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条滚动到激活菜单所在位置
|
||||
const verticalMenusScroll = () => {
|
||||
setTimeout(() => {
|
||||
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.menu-horizontal li.is-active')
|
||||
if (activeMenu) {
|
||||
layoutMenuScrollbarRef.value?.setScrollLeft(activeMenu.offsetLeft)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentRouteActive(route)
|
||||
verticalMenusScroll()
|
||||
|
||||
new horizontalScroll(layoutMenuScrollbarRef.value!.wrapRef!)
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
currentRouteActive(to)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layouts-menu-horizontal-double {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--el-header-height);
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
border-bottom: 1px solid var(--el-color-info-light-8);
|
||||
}
|
||||
.double-menus-scrollbar {
|
||||
width: 70vw;
|
||||
height: var(--el-header-height);
|
||||
}
|
||||
.menu-horizontal {
|
||||
border: none;
|
||||
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
|
||||
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
|
||||
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
|
||||
}
|
||||
|
||||
.el-sub-menu .icon,
|
||||
.el-menu-item .icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.is-active .icon {
|
||||
color: var(--el-menu-active-color) !important;
|
||||
}
|
||||
.el-menu-item.is-active {
|
||||
background-color: v-bind('config.getColorVal("menuActiveBackground")');
|
||||
}
|
||||
</style>
|
||||
263
web/src/layouts/backend/components/navBar/tabs.vue
Normal file
263
web/src/layouts/backend/components/navBar/tabs.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="nav-tabs" ref="tabScrollbarRef">
|
||||
<div
|
||||
v-for="(item, idx) in navTabs.state.tabsView"
|
||||
@click="onTab(item)"
|
||||
@contextmenu.prevent="onContextmenu(item, $event)"
|
||||
class="ba-nav-tab"
|
||||
:class="navTabs.state.activeIndex == idx ? 'active' : ''"
|
||||
:ref="tabsRefs.set"
|
||||
:key="idx"
|
||||
>
|
||||
{{ item.meta.title }}
|
||||
<transition @after-leave="selectNavTab(tabsRefs[navTabs.state.activeIndex])" name="el-fade-in">
|
||||
<Icon v-show="navTabs.state.tabsView.length > 1" class="close-icon" @click.stop="closeTab(item)" size="15" name="el-icon-Close" />
|
||||
</transition>
|
||||
</div>
|
||||
<div :style="activeBoxStyle" class="nav-tabs-active-box"></div>
|
||||
<Contextmenu ref="contextmenuRef" :items="state.contextmenuItems" @menuClick="onContextMenuClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, reactive, useTemplateRef } from 'vue'
|
||||
import { useRoute, useRouter, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { useTemplateRefsList } from '@vueuse/core'
|
||||
import type { ContextMenuItem, ContextMenuItemClickEmitArg } from '/@/components/contextmenu/interface'
|
||||
import useCurrentInstance from '/@/utils/useCurrentInstance'
|
||||
import Contextmenu from '/@/components/contextmenu/index.vue'
|
||||
import horizontalScroll from '/@/utils/horizontalScroll'
|
||||
import { getFirstRoute, routePush } from '/@/utils/router'
|
||||
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
|
||||
const { proxy } = useCurrentInstance()
|
||||
const tabsRefs = useTemplateRefsList<HTMLDivElement>()
|
||||
const contextmenuRef = useTemplateRef('contextmenuRef')
|
||||
const tabScrollbarRef = useTemplateRef('tabScrollbarRef')
|
||||
|
||||
const state: {
|
||||
contextmenuItems: ContextMenuItem[]
|
||||
} = reactive({
|
||||
contextmenuItems: [
|
||||
{ name: 'refresh', label: '重新加载', icon: 'fa fa-refresh' },
|
||||
{ name: 'close', label: '关闭标签', icon: 'fa fa-times' },
|
||||
{ name: 'fullScreen', label: '当前标签全屏', icon: 'el-icon-FullScreen' },
|
||||
{ name: 'closeOther', label: '关闭其他标签', icon: 'fa fa-minus' },
|
||||
{ name: 'closeAll', label: '关闭全部标签', icon: 'fa fa-stop' },
|
||||
],
|
||||
})
|
||||
|
||||
const activeBoxStyle = reactive({
|
||||
width: '0',
|
||||
transform: 'translateX(0px)',
|
||||
})
|
||||
|
||||
const onTab = (menu: RouteLocationNormalized) => {
|
||||
router.push(menu.fullPath)
|
||||
}
|
||||
|
||||
// tab 激活状态切换
|
||||
const selectNavTab = function (dom: HTMLDivElement) {
|
||||
if (!dom) {
|
||||
return false
|
||||
}
|
||||
activeBoxStyle.width = dom.clientWidth + 'px'
|
||||
activeBoxStyle.transform = `translateX(${dom.offsetLeft}px)`
|
||||
|
||||
if (tabScrollbarRef.value) {
|
||||
let scrollLeft = dom.offsetLeft + dom.clientWidth - tabScrollbarRef.value.clientWidth
|
||||
if (dom.offsetLeft < tabScrollbarRef.value.scrollLeft) {
|
||||
tabScrollbarRef.value.scrollTo(dom.offsetLeft, 0)
|
||||
} else if (scrollLeft > tabScrollbarRef.value.scrollLeft) {
|
||||
tabScrollbarRef.value.scrollTo(scrollLeft, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toLastTab = () => {
|
||||
const lastTab = navTabs.state.tabsView.slice(-1)[0]
|
||||
if (lastTab) {
|
||||
router.push(lastTab.fullPath)
|
||||
} else {
|
||||
router.push(adminBaseRoutePath)
|
||||
}
|
||||
}
|
||||
|
||||
const closeTab = (route: RouteLocationNormalized) => {
|
||||
navTabs._closeTab(route)
|
||||
proxy.eventBus.emit('onTabViewClose', route)
|
||||
if (navTabs.state.activeRoute?.fullPath === route.fullPath) {
|
||||
toLastTab()
|
||||
} else {
|
||||
navTabs._setActiveRoute(navTabs.state.activeRoute!)
|
||||
nextTick(() => {
|
||||
selectNavTab(tabsRefs.value[navTabs.state.activeIndex])
|
||||
})
|
||||
}
|
||||
|
||||
contextmenuRef.value?.onHideContextmenu()
|
||||
}
|
||||
|
||||
const closeOtherTab = (menu: RouteLocationNormalized) => {
|
||||
navTabs._closeTabs(menu)
|
||||
navTabs._setActiveRoute(menu)
|
||||
if (navTabs.state.activeRoute?.fullPath !== route.fullPath) {
|
||||
router.push(menu!.fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有tab(等同于 navTabs.closeAllTab)
|
||||
* @param menu 需要保留的标签,否则关闭全部标签
|
||||
*/
|
||||
const closeAllTab = (menu?: RouteLocationNormalized) => {
|
||||
let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
|
||||
if (menu && firstRoute && firstRoute.path == menu.fullPath) {
|
||||
return closeOtherTab(menu)
|
||||
}
|
||||
if (firstRoute && firstRoute.path == navTabs.state.activeRoute?.fullPath) {
|
||||
return closeOtherTab(navTabs.state.activeRoute)
|
||||
}
|
||||
navTabs._closeTabs(false)
|
||||
if (firstRoute) routePush(firstRoute.path)
|
||||
}
|
||||
|
||||
const onContextmenu = (menu: RouteLocationNormalized, el: MouseEvent) => {
|
||||
// 禁用刷新
|
||||
state.contextmenuItems[0].disabled = route.fullPath !== menu.fullPath
|
||||
// 禁用关闭其他和关闭全部
|
||||
state.contextmenuItems[4].disabled = state.contextmenuItems[3].disabled = navTabs.state.tabsView.length == 1 ? true : false
|
||||
|
||||
const { clientX, clientY } = el
|
||||
contextmenuRef.value?.onShowContextmenu(menu, {
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
})
|
||||
}
|
||||
|
||||
const onContextMenuClick = (item: ContextMenuItemClickEmitArg<RouteLocationNormalized>) => {
|
||||
const { name, sourceData } = item
|
||||
if (!sourceData) return
|
||||
switch (name) {
|
||||
case 'refresh':
|
||||
proxy.eventBus.emit('onTabViewRefresh', sourceData)
|
||||
break
|
||||
case 'close':
|
||||
closeTab(sourceData)
|
||||
break
|
||||
case 'closeOther':
|
||||
closeOtherTab(sourceData)
|
||||
break
|
||||
case 'closeAll':
|
||||
closeAllTab(sourceData)
|
||||
break
|
||||
case 'fullScreen':
|
||||
if (route.fullPath !== sourceData.fullPath) {
|
||||
router.push(sourceData.fullPath as string)
|
||||
}
|
||||
navTabs.setFullScreen(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const updateTab = function (newRoute: RouteLocationNormalized) {
|
||||
// 添加tab
|
||||
navTabs._addTab(newRoute)
|
||||
// 激活当前tab
|
||||
navTabs._setActiveRoute(newRoute)
|
||||
|
||||
nextTick(() => {
|
||||
selectNavTab(tabsRefs.value[navTabs.state.activeIndex])
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to) => {
|
||||
updateTab(to)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateTab(router.currentRoute.value)
|
||||
if (tabScrollbarRef.value) {
|
||||
new horizontalScroll(tabScrollbarRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 通过路由路径关闭tab(等同于 navTabs.closeTabByPath)
|
||||
* @param fullPath 需要关闭的 tab 的路径
|
||||
*/
|
||||
const closeTabByPath = (fullPath: string) => {
|
||||
for (const key in navTabs.state.tabsView) {
|
||||
if (navTabs.state.tabsView[key].fullPath == fullPath) {
|
||||
closeTab(navTabs.state.tabsView[key])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 tab 标题(等同于 navTabs.updateTabTitle)
|
||||
* @param fullPath 需要修改标题的 tab 的路径
|
||||
* @param title 新的标题
|
||||
*/
|
||||
const updateTabTitle = (fullPath: string, title: string) => {
|
||||
navTabs._updateTabTitle(fullPath, title)
|
||||
nextTick(() => {
|
||||
selectNavTab(tabsRefs.value[navTabs.state.activeIndex])
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
closeAllTab,
|
||||
closeTabByPath,
|
||||
updateTabTitle,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dark {
|
||||
.close-icon {
|
||||
color: v-bind('config.getColorVal("headerBarTabColor")') !important;
|
||||
}
|
||||
.ba-nav-tab.active {
|
||||
.close-icon {
|
||||
color: v-bind('config.getColorVal("headerBarTabActiveColor")') !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav-tabs {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
margin-right: var(--ba-main-space);
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 5px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #eaeaea;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: v-bind('config.layout.layoutMode == "Default" ? "none":config.getColorVal("headerBarBackground")');
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: #c8c9cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ba-nav-tab {
|
||||
white-space: nowrap;
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
329
web/src/layouts/backend/components/navMenus.vue
Normal file
329
web/src/layouts/backend/components/navMenus.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="nav-menus" :class="[configStore.layout.layoutMode, configStore.layout.shrink ? 'shrink' : '']">
|
||||
<!-- 需要重启 Vite 热更新服务警告 -->
|
||||
<el-popover
|
||||
ref="reloadHotServerPopover"
|
||||
@show="onCurrentNavMenu(true, 'reloadHotServer')"
|
||||
@hide="onCurrentNavMenu(false, 'reloadHotServer')"
|
||||
:width="360"
|
||||
v-if="hotUpdateState.dirtyFile"
|
||||
>
|
||||
<div>
|
||||
<div class="el-popover__title">{{ t('vite.Reload hot server title') }}</div>
|
||||
<div class="reload-hot-server-content">
|
||||
<p>
|
||||
<span>{{ t('vite.Reload hot server tips 1') }}</span>
|
||||
<span>【{{ t(`vite.Close type ${hotUpdateState.closeType}`) }}】</span>
|
||||
<span>{{ t('vite.Reload hot server tips 2') }}</span>
|
||||
</p>
|
||||
<p>{{ t('vite.Reload hot server tips 3') }}</p>
|
||||
<div class="reload-hot-server-buttons">
|
||||
<el-button @click="onHotServerOpt('cancel')">{{ t('vite.Later') }}</el-button>
|
||||
<el-button @click="onHotServerOpt('reload')" type="primary">{{ t('vite.Restart hot update') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<div class="nav-menu-item" :class="state.currentNavMenu == 'reloadHotServer' ? 'hover' : ''">
|
||||
<Icon color="var(--el-color-danger)" class="nav-menu-icon" name="el-icon-Warning" size="18" />
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<!-- 站点主页 -->
|
||||
<router-link class="h100" target="_blank" :title="t('Home')" to="/">
|
||||
<div class="nav-menu-item">
|
||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="el-icon-Monitor" size="18" />
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- 语言切换 -->
|
||||
<el-dropdown
|
||||
@visible-change="onCurrentNavMenu($event, 'lang')"
|
||||
class="h100"
|
||||
size="large"
|
||||
:hide-timeout="50"
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
:hide-on-click="true"
|
||||
>
|
||||
<div class="nav-menu-item pt2" :class="state.currentNavMenu == 'lang' ? 'hover' : ''">
|
||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="local-lang" size="18" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="dropdown-menu-box">
|
||||
<el-dropdown-item v-for="item in configStore.lang.langArray" :key="item.name" @click="editDefaultLang(item.name)">
|
||||
{{ item.value }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 全屏切换 -->
|
||||
<div @click="onFullScreen" class="nav-menu-item" :class="state.isFullScreen ? 'hover' : ''">
|
||||
<Icon
|
||||
:color="configStore.getColorVal('headerBarTabColor')"
|
||||
class="nav-menu-icon"
|
||||
v-if="state.isFullScreen"
|
||||
name="local-full-screen-cancel"
|
||||
size="18"
|
||||
/>
|
||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" v-else name="el-icon-FullScreen" size="18" />
|
||||
</div>
|
||||
|
||||
<!-- 终端 - 仅超管 -->
|
||||
<div v-if="adminInfo.super" @click="terminal.toggle()" class="nav-menu-item pt2">
|
||||
<el-badge :is-dot="terminal.state.showDot">
|
||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="local-terminal" size="26" />
|
||||
</el-badge>
|
||||
</div>
|
||||
|
||||
<!-- 清理缓存 - 仅超管 -->
|
||||
<el-dropdown
|
||||
v-if="adminInfo.super"
|
||||
@visible-change="onCurrentNavMenu($event, 'clear')"
|
||||
class="h100"
|
||||
size="large"
|
||||
:hide-timeout="50"
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
:hide-on-click="true"
|
||||
>
|
||||
<div class="nav-menu-item" :class="state.currentNavMenu == 'clear' ? 'hover' : ''">
|
||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="el-icon-Delete" size="18" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="dropdown-menu-box">
|
||||
<el-dropdown-item @click="onClearCache('tp')">{{ t('utils.Clean up system cache') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="onClearCache('storage')">{{ t('utils.Clean up browser cache') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="onClearCache('all')" divided>{{ t('utils.Clean up all cache') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 管理员信息 -->
|
||||
<el-popover
|
||||
v-if="siteConfig.userInitialize"
|
||||
@show="onCurrentNavMenu(true, 'adminInfo')"
|
||||
@hide="onCurrentNavMenu(false, 'adminInfo')"
|
||||
placement="bottom-end"
|
||||
:hide-after="0"
|
||||
:width="260"
|
||||
trigger="click"
|
||||
popper-class="admin-info-box"
|
||||
v-model:visible="state.showAdminInfoPopover"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="admin-info" :class="state.currentNavMenu == 'adminInfo' ? 'hover' : ''">
|
||||
<el-avatar :size="25" :src="fullUrl(adminInfo.avatar)"></el-avatar>
|
||||
<div class="admin-name">{{ adminInfo.nickname }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div class="admin-info-base">
|
||||
<el-avatar :size="70" :src="fullUrl(adminInfo.avatar)"></el-avatar>
|
||||
<div class="admin-info-other">
|
||||
<div class="admin-info-name">{{ adminInfo.nickname }}</div>
|
||||
<div class="admin-info-lasttime">{{ timeFormat(adminInfo.last_login_time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-info-footer">
|
||||
<el-button @click="onAdminInfo" type="primary" plain>{{ t('layouts.Profile') }}</el-button>
|
||||
<el-button @click="onLogout" type="danger" plain>{{ t('layouts.Logout') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
|
||||
<!-- 配置 -->
|
||||
<div @click="configStore.setLayout('showDrawer', true)" class="nav-menu-item">
|
||||
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="fa fa-cogs" size="18" />
|
||||
</div>
|
||||
|
||||
<Config />
|
||||
<Terminal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import screenfull from 'screenfull'
|
||||
import { reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Config from './config.vue'
|
||||
import { logout } from '/@/api/backend/index'
|
||||
import { postClearCache } from '/@/api/common'
|
||||
import Terminal from '/@/components/terminal/index.vue'
|
||||
import { editDefaultLang } from '/@/lang'
|
||||
import router from '/@/router'
|
||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { ADMIN_INFO, BA_ACCOUNT } from '/@/stores/constant/cacheKey'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { useTerminal } from '/@/stores/terminal'
|
||||
import { fullUrl, timeFormat } from '/@/utils/common'
|
||||
import { routePush } from '/@/utils/router'
|
||||
import { Local, Session } from '/@/utils/storage'
|
||||
import { hotUpdateState, reloadServer } from '/@/utils/vite'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const adminInfo = useAdminInfo()
|
||||
const configStore = useConfig()
|
||||
const terminal = useTerminal()
|
||||
const siteConfig = useSiteConfig()
|
||||
const reloadHotServerPopover = useTemplateRef('reloadHotServerPopover')
|
||||
|
||||
const state = reactive({
|
||||
isFullScreen: false,
|
||||
currentNavMenu: '',
|
||||
showLayoutDrawer: false,
|
||||
showAdminInfoPopover: false,
|
||||
})
|
||||
|
||||
const onCurrentNavMenu = (status: boolean, name: string) => {
|
||||
state.currentNavMenu = status ? name : ''
|
||||
}
|
||||
|
||||
const onHotServerOpt = (opt: 'reload' | 'cancel') => {
|
||||
if (opt == 'cancel') {
|
||||
reloadHotServerPopover.value?.hide()
|
||||
} else {
|
||||
reloadServer('manual')
|
||||
}
|
||||
}
|
||||
|
||||
const onFullScreen = () => {
|
||||
if (!screenfull.isEnabled) {
|
||||
ElMessage.warning(t('layouts.Full screen is not supported'))
|
||||
return false
|
||||
}
|
||||
screenfull.toggle()
|
||||
screenfull.onchange(() => {
|
||||
state.isFullScreen = screenfull.isFullscreen
|
||||
})
|
||||
}
|
||||
|
||||
const onAdminInfo = () => {
|
||||
state.showAdminInfoPopover = false
|
||||
routePush({ name: 'routine/adminInfo' })
|
||||
}
|
||||
|
||||
const onLogout = () => {
|
||||
logout().then(() => {
|
||||
Local.remove(ADMIN_INFO)
|
||||
router.go(0)
|
||||
})
|
||||
}
|
||||
|
||||
const onClearCache = (type: string) => {
|
||||
if (type == 'storage' || type == 'all') {
|
||||
const adminInfo = Local.get(ADMIN_INFO)
|
||||
const baAccount = Local.get(BA_ACCOUNT)
|
||||
Session.clear()
|
||||
Local.clear()
|
||||
Local.set(ADMIN_INFO, adminInfo)
|
||||
Local.set(BA_ACCOUNT, baAccount)
|
||||
if (type == 'storage') return
|
||||
}
|
||||
postClearCache(type).then(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.nav-menus.Default:not(.shrink) {
|
||||
border-radius: var(--el-border-radius-base);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
.reload-hot-server-content {
|
||||
font-size: var(--el-font-size-small);
|
||||
p {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.reload-hot-server-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.nav-menus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
background-color: v-bind('configStore.getColorVal("headerBarBackground")');
|
||||
.nav-menu-item {
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
.nav-menu-icon {
|
||||
box-sizing: content-box;
|
||||
color: v-bind('configStore.getColorVal("headerBarTabColor")');
|
||||
}
|
||||
&:hover {
|
||||
.icon {
|
||||
animation: twinkle 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
.admin-info {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: v-bind('configStore.getColorVal("headerBarTabColor")');
|
||||
}
|
||||
.admin-name {
|
||||
padding-left: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-menu-item:hover,
|
||||
.admin-info:hover,
|
||||
.nav-menu-item.hover,
|
||||
.admin-info.hover {
|
||||
background: v-bind('configStore.getColorVal("headerBarHoverBackground")');
|
||||
}
|
||||
}
|
||||
.dropdown-menu-box :deep(.el-dropdown-menu__item) {
|
||||
justify-content: center;
|
||||
}
|
||||
.admin-info-base {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 10px;
|
||||
.admin-info-other {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
.admin-info-name {
|
||||
font-size: var(--el-font-size-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
.admin-info-footer {
|
||||
padding: 10px 0;
|
||||
margin: 0 -12px -12px -12px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.pt2 {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
80% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
web/src/layouts/backend/container/classic.vue
Normal file
31
web/src/layouts/backend/container/classic.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<Aside />
|
||||
<el-container class="content-wrapper">
|
||||
<Header />
|
||||
<Main />
|
||||
</el-container>
|
||||
</el-container>
|
||||
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Aside from '/@/layouts/backend/components/aside.vue'
|
||||
import Header from '/@/layouts/backend/components/header.vue'
|
||||
import Main from '/@/layouts/backend/router-view/main.vue'
|
||||
import CloseFullScreen from '/@/layouts/backend/components/closeFullScreen.vue'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
const navTabs = useNavTabs()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
31
web/src/layouts/backend/container/default.vue
Normal file
31
web/src/layouts/backend/container/default.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<Aside />
|
||||
<el-container class="content-wrapper">
|
||||
<Header />
|
||||
<Main />
|
||||
</el-container>
|
||||
</el-container>
|
||||
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Aside from '/@/layouts/backend/components/aside.vue'
|
||||
import Header from '/@/layouts/backend/components/header.vue'
|
||||
import Main from '/@/layouts/backend/router-view/main.vue'
|
||||
import CloseFullScreen from '/@/layouts/backend/components/closeFullScreen.vue'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
const navTabs = useNavTabs()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
31
web/src/layouts/backend/container/double.vue
Normal file
31
web/src/layouts/backend/container/double.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<Aside />
|
||||
<el-container class="content-wrapper">
|
||||
<Header />
|
||||
<Main />
|
||||
</el-container>
|
||||
</el-container>
|
||||
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Aside from '/@/layouts/backend/components/aside.vue'
|
||||
import Header from '/@/layouts/backend/components/header.vue'
|
||||
import Main from '/@/layouts/backend/router-view/main.vue'
|
||||
import CloseFullScreen from '/@/layouts/backend/components/closeFullScreen.vue'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
const navTabs = useNavTabs()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
29
web/src/layouts/backend/container/streamline.vue
Normal file
29
web/src/layouts/backend/container/streamline.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-container class="content-wrapper">
|
||||
<Header />
|
||||
<Main />
|
||||
</el-container>
|
||||
</el-container>
|
||||
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Header from '/@/layouts/backend/components/header.vue'
|
||||
import Main from '/@/layouts/backend/router-view/main.vue'
|
||||
import CloseFullScreen from '/@/layouts/backend/components/closeFullScreen.vue'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
const navTabs = useNavTabs()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
120
web/src/layouts/backend/index.vue
Normal file
120
web/src/layouts/backend/index.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<component :is="config.layout.layoutMode"></component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { useTerminal } from '/@/stores/terminal'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Default from '/@/layouts/backend/container/default.vue'
|
||||
import Classic from '/@/layouts/backend/container/classic.vue'
|
||||
import Streamline from '/@/layouts/backend/container/streamline.vue'
|
||||
import Double from '/@/layouts/backend/container/double.vue'
|
||||
import { onMounted, onBeforeMount } from 'vue'
|
||||
import { Session } from '/@/utils/storage'
|
||||
import { index } from '/@/api/backend'
|
||||
import { handleAdminRoute, getFirstRoute, routePush } from '/@/utils/router'
|
||||
import router from '/@/router/index'
|
||||
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { BEFORE_RESIZE_LAYOUT } from '/@/stores/constant/cacheKey'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { setNavTabsWidth } from '/@/utils/layout'
|
||||
|
||||
defineOptions({
|
||||
components: { Default, Classic, Streamline, Double },
|
||||
})
|
||||
|
||||
const terminal = useTerminal()
|
||||
const navTabs = useNavTabs()
|
||||
const config = useConfig()
|
||||
const route = useRoute()
|
||||
const siteConfig = useSiteConfig()
|
||||
const adminInfo = useAdminInfo()
|
||||
|
||||
const state = reactive({
|
||||
autoMenuCollapseLock: false,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!adminInfo.token) return router.push({ name: 'adminLogin' })
|
||||
|
||||
init()
|
||||
setNavTabsWidth()
|
||||
useEventListener(window, 'resize', setNavTabsWidth)
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
onAdaptiveLayout()
|
||||
useEventListener(window, 'resize', onAdaptiveLayout)
|
||||
})
|
||||
|
||||
const init = () => {
|
||||
/**
|
||||
* 后台初始化请求,获取站点配置,动态路由等信息
|
||||
*/
|
||||
index()
|
||||
.then((res) => {
|
||||
siteConfig.dataFill(res.data.siteConfig)
|
||||
terminal.changePackageManager(res.data.terminal.npmPackageManager)
|
||||
terminal.changePHPDevelopmentServer(res.data.terminal.phpDevelopmentServer)
|
||||
siteConfig.setInitialize(true)
|
||||
|
||||
if (!isEmpty(res.data.adminInfo)) {
|
||||
adminInfo.dataFill(res.data.adminInfo)
|
||||
siteConfig.setUserInitialize(true)
|
||||
}
|
||||
|
||||
if (res.data.menus) {
|
||||
handleAdminRoute(res.data.menus)
|
||||
|
||||
// 预跳转到上次路径
|
||||
if (route.params.to) {
|
||||
const lastRoute = JSON.parse(route.params.to as string)
|
||||
if (lastRoute.path != adminBaseRoutePath) {
|
||||
let query = !isEmpty(lastRoute.query) ? lastRoute.query : {}
|
||||
routePush({ path: lastRoute.path, query: query })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到第一个菜单
|
||||
let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
|
||||
if (firstRoute) routePush(firstRoute.path)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 303/401 等由 axios 拦截器处理跳转登录;其他错误由 loading 页 6 秒后显示重试按钮
|
||||
})
|
||||
}
|
||||
|
||||
const onAdaptiveLayout = () => {
|
||||
let defaultBeforeResizeLayout = {
|
||||
menuCollapse: config.layout.menuCollapse,
|
||||
}
|
||||
let beforeResizeLayout = Session.get(BEFORE_RESIZE_LAYOUT)
|
||||
if (!beforeResizeLayout) Session.set(BEFORE_RESIZE_LAYOUT, defaultBeforeResizeLayout)
|
||||
|
||||
const clientWidth = document.body.clientWidth
|
||||
if (clientWidth < 1024) {
|
||||
/**
|
||||
* 锁定窗口改变自动调整 menuCollapse
|
||||
* 避免已是小窗且打开了菜单栏时,意外的自动关闭菜单栏
|
||||
*/
|
||||
if (!state.autoMenuCollapseLock) {
|
||||
state.autoMenuCollapseLock = true
|
||||
config.setLayout('menuCollapse', true)
|
||||
}
|
||||
config.setLayout('shrink', true)
|
||||
} else {
|
||||
state.autoMenuCollapseLock = false
|
||||
let beforeResizeLayoutTemp = beforeResizeLayout || defaultBeforeResizeLayout
|
||||
|
||||
config.setLayout('menuCollapse', beforeResizeLayoutTemp.menuCollapse)
|
||||
config.setLayout('shrink', false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
105
web/src/layouts/backend/router-view/main.vue
Normal file
105
web/src/layouts/backend/router-view/main.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<el-main class="layout-main">
|
||||
<el-scrollbar class="layout-main-scrollbar" :style="layoutMainScrollbarStyle" ref="layoutMainScrollbarRef">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition :name="config.layout.mainAnimation" mode="out-in">
|
||||
<keep-alive :include="state.keepAliveComponentNameList">
|
||||
<component :is="Component" :key="state.componentKey" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, watch, onBeforeMount, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, type RouteLocationNormalized } from 'vue-router'
|
||||
import useCurrentInstance from '/@/utils/useCurrentInstance'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { layoutMainScrollbarRef, layoutMainScrollbarStyle } from '/@/stores/refs'
|
||||
|
||||
defineOptions({
|
||||
name: 'layout/main',
|
||||
})
|
||||
|
||||
const { proxy } = useCurrentInstance()
|
||||
|
||||
const route = useRoute()
|
||||
const config = useConfig()
|
||||
const navTabs = useNavTabs()
|
||||
|
||||
const state: {
|
||||
componentKey: string
|
||||
keepAliveComponentNameList: string[]
|
||||
} = reactive({
|
||||
componentKey: route.fullPath,
|
||||
keepAliveComponentNameList: [],
|
||||
})
|
||||
|
||||
const addKeepAliveComponentName = function (keepAliveName: string | undefined) {
|
||||
if (keepAliveName) {
|
||||
let exist = state.keepAliveComponentNameList.find((name: string) => {
|
||||
return name === keepAliveName
|
||||
})
|
||||
if (exist) return
|
||||
state.keepAliveComponentNameList.push(keepAliveName)
|
||||
}
|
||||
}
|
||||
|
||||
const addActiveRouteKeepAlive = () => {
|
||||
if (navTabs.state.activeRoute) {
|
||||
const tabView = navTabs.getTabsViewDataByRoute(navTabs.state.activeRoute)
|
||||
if (tabView && typeof tabView.meta?.keepalive == 'string') {
|
||||
addKeepAliveComponentName(tabView.meta.keepalive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
proxy.eventBus.on('onTabViewRefresh', (menu: RouteLocationNormalized) => {
|
||||
state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter((name: string) => menu.meta.keepalive !== name)
|
||||
state.componentKey = ''
|
||||
nextTick(() => {
|
||||
state.componentKey = menu.fullPath
|
||||
addKeepAliveComponentName(menu.meta.keepalive as string)
|
||||
})
|
||||
})
|
||||
proxy.eventBus.on('onTabViewClose', (menu: RouteLocationNormalized) => {
|
||||
state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter((name: string) => menu.meta.keepalive !== name)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
proxy.eventBus.off('onTabViewRefresh')
|
||||
proxy.eventBus.off('onTabViewClose')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 确保刷新页面时也能正确取得当前路由 keepalive 参数(热更新)
|
||||
addActiveRouteKeepAlive()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
state.componentKey = route.fullPath
|
||||
addActiveRouteKeepAlive()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-container .layout-main {
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.layout-main-scrollbar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
77
web/src/layouts/common/components/darkSwitch.vue
Normal file
77
web/src/layouts/common/components/darkSwitch.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="theme-toggle-content">
|
||||
<div class="switch">
|
||||
<div class="switch-action">
|
||||
<Icon name="local-dark" color="#f2f2f2" size="13px" class="switch-icon dark-icon" />
|
||||
<Icon name="local-light" color="#303133" size="13px" class="switch-icon light-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.theme-toggle-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.switch {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--ba-bg-color);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.3s,
|
||||
background-color 0.5s;
|
||||
}
|
||||
.switch-action {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
border-radius: 50%;
|
||||
background-color: #ffffff;
|
||||
transform: translate(0);
|
||||
color: var(--el-text-color-primary);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.switch-icon {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
bottom: 1px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.light-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
.dark-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@at-root .dark {
|
||||
.switch {
|
||||
background-color: #2c2c2c;
|
||||
}
|
||||
.switch-action {
|
||||
transform: translate(20px);
|
||||
background-color: #141414;
|
||||
}
|
||||
.dark-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
.light-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
web/src/layouts/common/components/loading.vue
Normal file
64
web/src/layouts/common/components/loading.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-loading="true"
|
||||
element-loading-background="var(--ba-bg-color-overlay)"
|
||||
:element-loading-text="$t('utils.Loading')"
|
||||
class="default-main ba-main-loading"
|
||||
></div>
|
||||
<div v-if="state.showReload" class="loading-footer">
|
||||
<el-button @click="refresh" type="warning">{{ $t('utils.Reload') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, reactive } from 'vue'
|
||||
import router from '/@/router/index'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { useNavTabs } from '/@/stores/navTabs'
|
||||
import { isAdminApp } from '/@/utils/common'
|
||||
import { getFirstRoute, routePush } from '/@/utils/router'
|
||||
let timer: number
|
||||
|
||||
const navTabs = useNavTabs()
|
||||
const memberCenter = useMemberCenter()
|
||||
const state = reactive({
|
||||
maximumWait: 1000 * 6,
|
||||
showReload: false,
|
||||
})
|
||||
|
||||
const refresh = () => {
|
||||
router.go(0)
|
||||
}
|
||||
|
||||
if (isAdminApp() && navTabs.state.tabsViewRoutes) {
|
||||
let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
|
||||
if (firstRoute) routePush(firstRoute.path)
|
||||
} else if (memberCenter.state.viewRoutes) {
|
||||
let firstRoute = getFirstRoute(memberCenter.state.viewRoutes)
|
||||
if (firstRoute) routePush(firstRoute.path)
|
||||
}
|
||||
|
||||
timer = window.setTimeout(() => {
|
||||
state.showReload = true
|
||||
}, state.maximumWait)
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-main-loading {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
31
web/src/layouts/common/router-view/iframe.vue
Normal file
31
web/src/layouts/common/router-view/iframe.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="iframe-main" v-loading="state.loading">
|
||||
<iframe :src="state.iframeSrc" :style="iframeStyle(35)" height="100%" width="100%" id="iframe" @load="hideLoading"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { mainHeight as iframeStyle } from '/@/utils/layout'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
iframeSrc: router.currentRoute.value.meta.url as string,
|
||||
})
|
||||
|
||||
const hideLoading = () => {
|
||||
state.loading = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.iframe-main {
|
||||
margin: var(--ba-main-space);
|
||||
iframe {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
160
web/src/layouts/frontend/components/aside.vue
Normal file
160
web/src/layouts/frontend/components/aside.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<el-aside class="ba-user-layouts">
|
||||
<div class="userinfo">
|
||||
<div @click="routerPush('account/profile')" class="user-avatar-box">
|
||||
<img class="user-avatar" :src="fullUrl(userInfo.avatar ? userInfo.avatar : '/static/images/avatar.png')" alt="" />
|
||||
<Icon class="user-avatar-gender" :name="userInfo.getGenderIcon()['name']" size="14" :color="userInfo.getGenderIcon()['color']" />
|
||||
</div>
|
||||
<p class="username">{{ userInfo.nickname }}</p>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
@click="routerPush('account/integral')"
|
||||
v-blur
|
||||
class="userinfo-button-item"
|
||||
:title="$t('Integral') + ' ' + userInfo.score"
|
||||
size="default"
|
||||
plain
|
||||
>
|
||||
<span>{{ $t('Integral') + ' ' + userInfo.score }}</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="routerPush('account/balance')"
|
||||
v-blur
|
||||
class="userinfo-button-item"
|
||||
:title="$t('Balance') + ' ' + userInfo.money"
|
||||
size="default"
|
||||
plain
|
||||
>
|
||||
<span>{{ $t('Balance') + ' ' + userInfo.money }}</span>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<div class="user-menus">
|
||||
<template v-for="(item, idx) in memberCenter.state.viewRoutes" :key="idx">
|
||||
<div v-if="memberCenter.state.showHeadline" class="user-menu-max-title">{{ item.meta?.title }}</div>
|
||||
<div
|
||||
v-for="(menu, index) in item.children"
|
||||
:key="index"
|
||||
@click="routerPush(menu)"
|
||||
class="user-menu-item"
|
||||
:class="route.fullPath == menu.path ? 'active' : ''"
|
||||
>
|
||||
<Icon :name="menu.meta?.icon" size="16" color="var(--el-text-color-secondary)" />
|
||||
<span>{{ menu.meta?.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter, type RouteRecordRaw } from 'vue-router'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { fullUrl } from '/@/utils/common'
|
||||
import { onClickMenu } from '/@/utils/router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userInfo = useUserInfo()
|
||||
const memberCenter = useMemberCenter()
|
||||
|
||||
const routerPush = (route: string | RouteRecordRaw) => {
|
||||
if (typeof route === 'string') {
|
||||
router.push({ name: route })
|
||||
} else {
|
||||
onClickMenu(route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-user-layouts {
|
||||
width: 240px;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
.userinfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.username {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
font-size: var(--el-font-size-large);
|
||||
font-weight: bold;
|
||||
}
|
||||
.user-avatar-box {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.user-avatar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.user-avatar-gender {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 10px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--el-box-shadow);
|
||||
}
|
||||
.userinfo-button-item {
|
||||
font-size: var(--el-font-size-small);
|
||||
height: 30px;
|
||||
}
|
||||
.user-menus {
|
||||
font-size: var(--el-font-size-base);
|
||||
color: var(--el-text-color-regular);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.user-menu-max-title {
|
||||
font-size: 15px;
|
||||
color: var(--el-text-color-secondary);
|
||||
padding: 5px 30px;
|
||||
}
|
||||
.user-menu-item {
|
||||
padding: 10px 30px;
|
||||
cursor: pointer;
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.user-menu-item:hover,
|
||||
.user-menu-item.active {
|
||||
border-left: 2px solid var(--el-color-primary);
|
||||
padding-left: 28px;
|
||||
color: var(--el-color-primary);
|
||||
.icon {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
background-color: var(--el-color-info-light-8);
|
||||
}
|
||||
@media screen and (max-width: 991px) {
|
||||
.ba-user-layouts {
|
||||
width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
web/src/layouts/frontend/components/footer.vue
Normal file
33
web/src/layouts/frontend/components/footer.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-footer class="footer">
|
||||
<div>
|
||||
Copyright @ 2020~{{ new Date().getFullYear() }} {{ siteConfig.siteName }} {{ $t('Copyright') }}
|
||||
<a href="http://beian.miit.gov.cn/">{{ siteConfig.recordNumber }}</a>
|
||||
</div>
|
||||
</el-footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
|
||||
const siteConfig = useSiteConfig()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--el-color-info-light-7);
|
||||
a {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
web/src/layouts/frontend/components/header.vue
Normal file
127
web/src/layouts/frontend/components/header.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<el-header class="header">
|
||||
<el-row justify="center">
|
||||
<el-col class="header-row" :xs="24" :sm="24" :md="16">
|
||||
<div @click="router.push({ name: '/' })" class="header-logo">
|
||||
<img src="~assets/logo.png" />
|
||||
<span class="site-name">{{ siteConfig.siteName }}</span>
|
||||
</div>
|
||||
<div v-if="!memberCenter.state.menuExpand" @click="memberCenter.toggleMenuExpand(true)" class="user-menus-expand hidden-md-and-up">
|
||||
<Icon name="fa fa-indent" color="var(--el-color-primary)" size="20" />
|
||||
</div>
|
||||
|
||||
<el-scrollbar ref="layoutMenuScrollbarRef" class="hidden-sm-and-down">
|
||||
<Menu class="frontend-header-menu" :ellipsis="false" mode="horizontal" />
|
||||
</el-scrollbar>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-drawer
|
||||
class="ba-aside-drawer"
|
||||
:append-to-body="true"
|
||||
v-model="memberCenter.state.menuExpand"
|
||||
:with-header="false"
|
||||
direction="ltr"
|
||||
:size="memberCenter.state.shrink ? '70%' : '40%'"
|
||||
>
|
||||
<div class="header-row">
|
||||
<div @click="router.push({ name: '/' })" class="header-logo">
|
||||
<img src="~assets/logo.png" />
|
||||
<span class="site-name">{{ siteConfig.siteName }}</span>
|
||||
</div>
|
||||
<div @click="memberCenter.toggleMenuExpand(false)" class="user-menus-expand hidden-md-and-up">
|
||||
<Icon name="fa fa-dedent" color="var(--el-color-primary)" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<Menu :show-icon="true" mode="vertical" />
|
||||
</el-drawer>
|
||||
</el-header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeRouteUpdate, useRouter } from 'vue-router'
|
||||
import { initialize } from '/@/api/frontend/index'
|
||||
import Menu from '/@/layouts/frontend/components/menu.vue'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { layoutMenuScrollbarRef } from '/@/stores/refs'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
|
||||
const router = useRouter()
|
||||
const siteConfig = useSiteConfig()
|
||||
const memberCenter = useMemberCenter()
|
||||
|
||||
onBeforeRouteUpdate(() => {
|
||||
memberCenter.toggleMenuExpand(false)
|
||||
})
|
||||
|
||||
/**
|
||||
* 前端初始化请求,获取站点配置信息,动态路由信息等
|
||||
*/
|
||||
initialize()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header {
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
box-shadow: 0 0 8px rgba(0 0 0 / 8%);
|
||||
.frontend-header-menu {
|
||||
height: var(--el-header-height);
|
||||
}
|
||||
}
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.header-logo {
|
||||
display: flex;
|
||||
height: var(--el-header-height);
|
||||
align-items: center;
|
||||
padding-right: 15px;
|
||||
cursor: pointer;
|
||||
img {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
}
|
||||
.site-name {
|
||||
padding-left: 4px;
|
||||
font-size: var(--el-font-size-large);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.user-menus-expand {
|
||||
display: flex;
|
||||
height: var(--el-header-height);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.ba-aside-drawer {
|
||||
.header-row {
|
||||
padding: 10px 20px;
|
||||
background-color: var(--el-color-info-light-9);
|
||||
.header-logo {
|
||||
img {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@at-root html.dark {
|
||||
.header-logo .site-name {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.user-menus-expand {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 414px) {
|
||||
.frontend-header-menu :deep(.el-sub-menu .el-sub-menu__title) {
|
||||
padding: 0 20px;
|
||||
.el-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
web/src/layouts/frontend/components/main.vue
Normal file
22
web/src/layouts/frontend/components/main.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<el-main class="layout-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition :name="config.layout.mainAnimation" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useConfig } from '/@/stores/config'
|
||||
|
||||
const config = useConfig()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-main {
|
||||
padding: 0 !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
256
web/src/layouts/frontend/components/menu.vue
Normal file
256
web/src/layouts/frontend/components/menu.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<el-menu ref="layoutMenuRef" :default-active="state.activeMenu" @select="onSelect">
|
||||
<el-menu-item @click="router.push({ name: '/' })" v-blur index="index">
|
||||
<Icon v-if="props.showIcon" name="fa fa-home" color="var(--el-text-color-primary)" />
|
||||
<template #title>{{ $t('Home') }}</template>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 动态菜单 -->
|
||||
<MenuSub :menus="siteConfig.headNav" :show-icon="showIcon" />
|
||||
|
||||
<template v-if="memberCenter.state.open">
|
||||
<el-sub-menu v-if="userInfo.isLogin()" @click="$attrs.mode == 'vertical' ? '' : router.push({ name: 'user' })" v-blur index="user-box">
|
||||
<template #title>
|
||||
<div class="header-user-box">
|
||||
<img
|
||||
class="header-user-avatar"
|
||||
:class="$attrs.mode == 'vertical' ? 'icon-header-user-avatar' : ''"
|
||||
:src="fullUrl(userInfo.avatar ? userInfo.avatar : '/static/images/avatar.png')"
|
||||
alt=""
|
||||
/>
|
||||
{{ userInfo.nickname }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-menu-item @click="router.push({ name: 'user' })" v-blur index="user">
|
||||
<Icon v-if="showIcon" name="fa fa-user-circle" color="var(--el-text-color-primary)" />
|
||||
{{ $t('Member Center') }}
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 动态菜单 -->
|
||||
<MenuSub :menus="memberCenter.state.navUserMenus" :show-icon="showIcon" />
|
||||
|
||||
<!-- 会员中心菜单 -->
|
||||
<MenuSub :menus="memberCenter.state.viewRoutes" :show-icon="showIcon" />
|
||||
|
||||
<el-menu-item @click="userInfo.logout()" v-blur index="user-logout">
|
||||
<Icon v-if="showIcon" name="fa fa-sign-out" color="var(--el-text-color-primary)" />
|
||||
{{ $t('Logout login') }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else @click="router.push({ name: 'user' })" v-blur index="user">
|
||||
<Icon v-if="showIcon" name="fa fa-user-circle" color="var(--el-text-color-primary)" />
|
||||
{{ $t('Member Center') }}
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu v-blur index="language-switch" class="language-switch">
|
||||
<template #title>
|
||||
<Icon v-if="showIcon" name="local-lang" color="var(--el-text-color-primary)" />
|
||||
{{ $t('Language') }}
|
||||
</template>
|
||||
<el-menu-item
|
||||
@click="editDefaultLang(item.name)"
|
||||
v-for="item in config.lang.langArray"
|
||||
:key="item.name"
|
||||
:index="'language-switch-' + item.value"
|
||||
class="language-switch"
|
||||
>
|
||||
<Icon v-if="showIcon" name="fa fa-circle-o" color="var(--el-text-color-primary)" />
|
||||
{{ item.value }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item index="theme-switch" class="theme-switch" :class="$attrs.mode + '-theme-switch'">
|
||||
<DarkSwitch @click="toggleDark()" />
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive } from 'vue'
|
||||
import { editDefaultLang } from '/@/lang/index'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { fullUrl } from '/@/utils/common'
|
||||
import MenuSub from '/@/layouts/frontend/components/menuSub.vue'
|
||||
import toggleDark from '/@/utils/useDark'
|
||||
import DarkSwitch from '/@/layouts/common/components/darkSwitch.vue'
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
|
||||
import { layoutMenuRef } from '/@/stores/refs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const userInfo = useUserInfo()
|
||||
const siteConfig = useSiteConfig()
|
||||
const memberCenter = useMemberCenter()
|
||||
|
||||
interface Props {
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showIcon: false,
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
activeMenu: '',
|
||||
})
|
||||
|
||||
/**
|
||||
* 设置激活菜单
|
||||
*/
|
||||
const setActiveMenu = (route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.path == '/') return (state.activeMenu = 'index')
|
||||
|
||||
const menuId = findMenus(route)
|
||||
if (menuId) {
|
||||
state.activeMenu = 'column-' + menuId
|
||||
} else if (route.path.startsWith('/user')) {
|
||||
state.activeMenu = 'user'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单被点击时额外对无需激活的菜单处理(外链、暗黑模式开关、语言切换等)
|
||||
* 检查菜单是否需要激活,如果否,还原 state.activeMenu
|
||||
*/
|
||||
const onSelect = (index: string) => {
|
||||
if (
|
||||
noNeedActive(siteConfig.headNav, index) ||
|
||||
noNeedActive(memberCenter.state.navUserMenus, index) ||
|
||||
noNeedActive(memberCenter.state.viewRoutes, index)
|
||||
) {
|
||||
const oldActiveMenu = state.activeMenu
|
||||
state.activeMenu = ''
|
||||
nextTick(() => {
|
||||
state.activeMenu = oldActiveMenu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查一个菜单是否需要激活态
|
||||
* @param menus
|
||||
* @param index
|
||||
*/
|
||||
const noNeedActive = (menus: RouteRecordRaw[], index: string) => {
|
||||
if (index.indexOf('language-switch') === 0 || index == 'theme-switch') {
|
||||
return true
|
||||
}
|
||||
return isExternalLink(menus, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查一个菜单是否是外站链接,如果是,不要激活
|
||||
* @param menus
|
||||
* @param index
|
||||
*/
|
||||
const isExternalLink = (menus: RouteRecordRaw[], index: string): boolean => {
|
||||
for (const key in menus) {
|
||||
const columnIndex = `column-${menus[key].meta?.id}`
|
||||
if (columnIndex == index) {
|
||||
return menus[key].meta?.menu_type == 'link'
|
||||
}
|
||||
if (menus[key].children?.length) {
|
||||
return isExternalLink(menus[key].children!, index)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归的搜索菜单 Index
|
||||
*/
|
||||
const searchMenuIndex = (menus: RouteRecordRaw[], route: RouteLocationNormalizedLoaded): number | false => {
|
||||
let find: boolean | number = false
|
||||
for (const key in menus) {
|
||||
if (menus[key].meta?.id && menus[key].path == route.fullPath) {
|
||||
return menus[key].meta.id as number
|
||||
}
|
||||
if (menus[key].children && menus[key].children?.length) {
|
||||
find = searchMenuIndex(menus[key].children, route)
|
||||
if (find !== false) return find
|
||||
}
|
||||
}
|
||||
return find
|
||||
}
|
||||
|
||||
/**
|
||||
* 从动态菜单(顶栏、会员中心下拉、会员中心菜单)中搜索一个菜单
|
||||
*/
|
||||
const findMenus = (route: RouteLocationNormalizedLoaded) => {
|
||||
// 顶栏菜单
|
||||
const headNavIndex = searchMenuIndex(siteConfig.headNav, route)
|
||||
if (headNavIndex !== false) return headNavIndex
|
||||
|
||||
// 会员中心下拉菜单
|
||||
const navUserMenuIndex = searchMenuIndex(memberCenter.state.navUserMenus, route)
|
||||
if (navUserMenuIndex !== false) return navUserMenuIndex
|
||||
|
||||
// 会员中心菜单
|
||||
return searchMenuIndex(memberCenter.state.viewRoutes, route)
|
||||
}
|
||||
|
||||
setActiveMenu(route)
|
||||
onBeforeRouteUpdate((to) => {
|
||||
setActiveMenu(to)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
.header-user-avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon-header-user-avatar {
|
||||
margin-left: 4px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.el-sub-menu .icon,
|
||||
.el-menu-item .icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 2px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.is-active > .icon {
|
||||
color: var(--el-menu-active-color) !important;
|
||||
}
|
||||
.el-menu {
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
.theme-switch.is-active,
|
||||
.language-switch.is-active {
|
||||
border-bottom: none;
|
||||
:deep(.el-sub-menu__title) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.theme-switch {
|
||||
--el-menu-hover-bg-color: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
.vertical-theme-switch {
|
||||
.theme-toggle-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.theme-toggle-content {
|
||||
padding-right: 0;
|
||||
}
|
||||
</style>
|
||||
59
web/src/layouts/frontend/components/menuSub.vue
Normal file
59
web/src/layouts/frontend/components/menuSub.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<template v-for="(item, idx) in props.menus" :key="idx">
|
||||
<template v-if="!isEmpty(item.children)">
|
||||
<el-sub-menu @click="onClickSubMenu(item)" v-blur :index="`column-${item.meta?.id}`">
|
||||
<template #title>
|
||||
<Icon v-if="showIcon" :name="item.meta?.icon" color="var(--el-text-color-primary)" />
|
||||
{{ item.meta?.title }}
|
||||
</template>
|
||||
<MenuSub :menus="item.children!" :show-icon="showIcon" />
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-menu-item @click="onClickMenu(item)" v-blur :index="'column-' + item.meta?.id" :class="(item.name as string).replace(/[\/]/g, '-')">
|
||||
<Icon v-if="showIcon" :name="item.meta?.icon" color="var(--el-text-color-primary)" />
|
||||
<template #title>{{ item.meta?.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { onClickMenu } from '/@/utils/router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import MenuSub from '/@/layouts/frontend/components/menuSub.vue'
|
||||
|
||||
interface Props {
|
||||
menus: RouteRecordRaw[]
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
menus: () => [],
|
||||
showIcon: false,
|
||||
})
|
||||
|
||||
const onClickSubMenu = (menu: RouteRecordRaw) => {
|
||||
/**
|
||||
* 1、'/'表示菜单规则的 path 为空
|
||||
* 2、会员中心菜单目录不需要跳转
|
||||
*/
|
||||
if (menu.path == '/' || menu.meta?.type == 'menu_dir') return
|
||||
onClickMenu(menu)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-sub-menu .icon,
|
||||
.el-menu-item .icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 2px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.is-active > .icon {
|
||||
color: var(--el-menu-active-color) !important;
|
||||
}
|
||||
</style>
|
||||
35
web/src/layouts/frontend/container/default.vue
Normal file
35
web/src/layouts/frontend/container/default.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<el-container class="is-vertical">
|
||||
<Header />
|
||||
<el-scrollbar :style="layoutMainScrollbarStyle" ref="layoutMainScrollbarRef">
|
||||
<el-row class="frontend-footer-brother" justify="center">
|
||||
<el-col class="user-layouts" :span="16" :xs="24">
|
||||
<Aside class="hidden-sm-and-down" />
|
||||
<Main />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<Footer />
|
||||
</el-scrollbar>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Header from '/@/layouts/frontend/components/header.vue'
|
||||
import Aside from '/@/layouts/frontend/components/aside.vue'
|
||||
import Main from '/@/layouts/frontend/components/main.vue'
|
||||
import Footer from '/@/layouts/frontend/components/footer.vue'
|
||||
import { layoutMainScrollbarRef, layoutMainScrollbarStyle } from '/@/stores/refs'
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-layouts {
|
||||
display: flex;
|
||||
padding-top: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.user-layouts {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
web/src/layouts/frontend/container/disable.vue
Normal file
32
web/src/layouts/frontend/container/disable.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-container class="is-vertical">
|
||||
<Header />
|
||||
<el-scrollbar :style="layoutMainScrollbarStyle" ref="layoutMainScrollbarRef">
|
||||
<el-row class="frontend-footer-brother" justify="center">
|
||||
<el-col class="user-layouts" :span="16" :xs="24">
|
||||
<el-alert :center="true" :title="$t('Member center disabled')" type="error" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<Footer />
|
||||
</el-scrollbar>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Header from '/@/layouts/frontend/components/header.vue'
|
||||
import Footer from '/@/layouts/frontend/components/footer.vue'
|
||||
import { layoutMainScrollbarRef, layoutMainScrollbarStyle } from '/@/stores/refs'
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-layouts {
|
||||
display: flex;
|
||||
padding-top: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.user-layouts {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
84
web/src/layouts/frontend/user.vue
Normal file
84
web/src/layouts/frontend/user.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<component :is="memberCenter.state.layoutMode"></component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { initialize } from '/@/api/frontend/index'
|
||||
import { getFirstRoute, routePush } from '/@/utils/router'
|
||||
import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Default from '/@/layouts/frontend/container/default.vue'
|
||||
import Disable from '/@/layouts/frontend/container/disable.vue'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import userMounted from '/@/components/mixins/userMounted'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
|
||||
defineOptions({
|
||||
components: { Default, Disable },
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userInfo = useUserInfo()
|
||||
const siteConfig = useSiteConfig()
|
||||
const memberCenter = useMemberCenter()
|
||||
|
||||
onMounted(async () => {
|
||||
const ret = await userMounted()
|
||||
if (ret.type == 'break') return
|
||||
if (ret.type == 'reload') return (window.location.href = ret.url)
|
||||
|
||||
if (!userInfo.token) return router.push({ name: 'userLogin' })
|
||||
|
||||
/**
|
||||
* 会员中心初始化请求,获取会员中心菜单信息等
|
||||
*/
|
||||
|
||||
const callback = () => {
|
||||
if (ret.type == 'jump') return router.push(ret.url)
|
||||
|
||||
// 预跳转到上次路径
|
||||
if (route.params.to) {
|
||||
const lastRoute = JSON.parse(route.params.to as string)
|
||||
if (lastRoute.path != memberCenterBaseRoutePath) {
|
||||
let query = !isEmpty(lastRoute.query) ? lastRoute.query : {}
|
||||
routePush({ path: lastRoute.path, query: query })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到第一个菜单
|
||||
if (route.name == 'userMainLoading') {
|
||||
let firstRoute = getFirstRoute(memberCenter.state.viewRoutes)
|
||||
if (firstRoute) {
|
||||
router.push({ path: firstRoute.path })
|
||||
} else {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: t('No route found to jump~'),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (siteConfig.userInitialize) {
|
||||
callback()
|
||||
} else {
|
||||
initialize(callback, true)
|
||||
}
|
||||
|
||||
if (document.body.clientWidth < 1024) {
|
||||
memberCenter.setShrink(true)
|
||||
} else {
|
||||
memberCenter.setShrink(false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user