update 更换外框架布局
This commit is contained in:
@@ -320,6 +320,99 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.yo-layout-sider {
|
||||
height: 100%;
|
||||
|
||||
background-color: @nav-background;
|
||||
.ant-layout-sider-children {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.logo {
|
||||
font-size: @font-size-lg * 1.5;
|
||||
font-weight: 500;
|
||||
line-height: @layout-header-height + 10px;
|
||||
|
||||
z-index: 11;
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
flex: 0 0 @layout-header-height + 10px;
|
||||
|
||||
height: @layout-header-height + 10px;
|
||||
padding: 0 @padding-md 0 @padding-lg;
|
||||
|
||||
color: @logo-color;
|
||||
box-shadow: @logo-box-shadow;
|
||||
img {
|
||||
max-height: 100%;
|
||||
}
|
||||
span {
|
||||
margin-left: @padding-sm;
|
||||
|
||||
transition: @animation-duration-slow;
|
||||
transition-property: opacity;
|
||||
}
|
||||
}
|
||||
&.ant-layout-sider-collapsed {
|
||||
.logo {
|
||||
span {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.yo-sider-nav {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
flex: 1 1 100%;
|
||||
|
||||
box-shadow: 2px 0 8px @nav-box-shadow-color;
|
||||
&--app {
|
||||
font-size: @font-size-sm;
|
||||
|
||||
margin-top: @padding-sm;
|
||||
padding: 0 @padding-md;
|
||||
|
||||
color: @nav-app-color;
|
||||
}
|
||||
}
|
||||
.swiper-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 100%;
|
||||
.swiper-scrollbar {
|
||||
transition: @animation-duration-slow;
|
||||
transition-property: opacity;
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
.swiper-scrollbar-drag {
|
||||
background-color: @nav-scrollbar-background;
|
||||
}
|
||||
&:hover {
|
||||
.swiper-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.swiper-slide {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
>.ant-spin-nested-loading {
|
||||
height: 100%;
|
||||
.ant-spin-blur {
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.yo-layout--left-menu,
|
||||
.yo-layout--right-menu {
|
||||
position: absolute;
|
||||
@@ -366,99 +459,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout-sider {
|
||||
}
|
||||
>section {
|
||||
>.ant-layout-sider {
|
||||
height: 100%;
|
||||
|
||||
background-color: @nav-background;
|
||||
.ant-layout-sider-children {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.logo {
|
||||
font-size: @font-size-lg * 1.5;
|
||||
font-weight: 500;
|
||||
line-height: @layout-header-height + 10px;
|
||||
|
||||
z-index: 11;
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
flex: 0 0 @layout-header-height + 10px;
|
||||
|
||||
height: @layout-header-height + 10px;
|
||||
padding: 0 @padding-md 0 @padding-lg;
|
||||
|
||||
color: @logo-color;
|
||||
box-shadow: @logo-box-shadow;
|
||||
img {
|
||||
max-height: 100%;
|
||||
}
|
||||
span {
|
||||
margin-left: @padding-sm;
|
||||
|
||||
transition: @animation-duration-slow;
|
||||
transition-property: opacity;
|
||||
}
|
||||
}
|
||||
&.ant-layout-sider-collapsed {
|
||||
.logo {
|
||||
span {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.yo-sider-nav {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
flex: 1 1 100%;
|
||||
|
||||
box-shadow: 2px 0 8px @nav-box-shadow-color;
|
||||
&--app {
|
||||
font-size: @font-size-sm;
|
||||
|
||||
margin-top: @padding-sm;
|
||||
padding: 0 @padding-md;
|
||||
|
||||
color: @nav-app-color;
|
||||
}
|
||||
}
|
||||
.swiper-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 100%;
|
||||
.swiper-scrollbar {
|
||||
transition: @animation-duration-slow;
|
||||
transition-property: opacity;
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
.swiper-scrollbar-drag {
|
||||
background-color: @nav-scrollbar-background;
|
||||
}
|
||||
&:hover {
|
||||
.swiper-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.swiper-slide {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
>.ant-spin-nested-loading {
|
||||
height: 100%;
|
||||
.ant-spin-blur {
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.yo-layout-sider();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,9 +478,16 @@
|
||||
width: 100%;
|
||||
min-width: @container-width;
|
||||
height: 100%;
|
||||
|
||||
@layout-header-height: 54px;
|
||||
.ant-layout-header {
|
||||
line-height: @layout-header-height;
|
||||
|
||||
z-index: 11;
|
||||
|
||||
flex: 0 0 @layout-header-height;
|
||||
|
||||
height: @layout-header-height;
|
||||
padding: 0;
|
||||
|
||||
background-color: @nav-background;
|
||||
@@ -515,19 +527,19 @@
|
||||
}
|
||||
}
|
||||
.user-container {
|
||||
margin: 12px 0;
|
||||
margin: (@layout-header-height - 40px) / 2 0;
|
||||
}
|
||||
.logo {
|
||||
font-size: @font-size-lg * 1.5;
|
||||
font-weight: 500;
|
||||
line-height: @layout-header-height;
|
||||
line-height: @layout-header-height - 10px;
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
|
||||
height: @layout-header-height;
|
||||
margin-right: @padding-lg;
|
||||
height: @layout-header-height 10px;
|
||||
margin: 5px @padding-lg 5px 0;
|
||||
|
||||
color: @logo-color;
|
||||
img {
|
||||
|
||||
@@ -100,6 +100,11 @@ import './assets/style/app.less'
|
||||
import { SETTING_KEY } from './common/storage'
|
||||
|
||||
const settings = JSON.parse(window.localStorage.getItem(SETTING_KEY))
|
||||
Object.assign(settings, {
|
||||
layout: 'top-nav',
|
||||
container: 'container-fluid',
|
||||
navTheme: 'dark'
|
||||
})
|
||||
|
||||
const app = new Vue({
|
||||
data: {
|
||||
|
||||
166
Web/src/views/main-dynamic/_layout/content.vue
Normal file
166
Web/src/views/main-dynamic/_layout/content.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<a-layout-content>
|
||||
<div class="yo-tab-external-mount">
|
||||
<a-tabs @change="onChange" @edit="onClose" hide-add type="editable-card" v-model="actived">
|
||||
<a-tab-pane
|
||||
:closable="pane.closable"
|
||||
:force-render="true"
|
||||
:key="pane.key"
|
||||
v-for="pane in panes"
|
||||
>
|
||||
<a-dropdown :trigger="['contextmenu']" slot="tab">
|
||||
<div @click.middle="() => pane.closable && $emit('close', pane.key)">
|
||||
<a-icon :type="pane.icon" v-if="pane.icon" />
|
||||
{{ pane.title }}
|
||||
<a-tooltip :title="pane.subTitle" placement="bottom" v-if="pane.subTitle">
|
||||
<span class="yo-layout-tab-subtitle">{{`- ${pane.subTitle}`}}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-menu slot="overlay">
|
||||
<template v-if="mode === 'development'">
|
||||
<a-menu-item @click="onCopyComponent(pane)" key="-1">
|
||||
复制组件地址
|
||||
<a-tag color="red">dev</a-tag>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
</template>
|
||||
<a-menu-item @click="onLoadContentWindow(pane.key)" key="0">重新加载</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item
|
||||
:disabled="!pane.closable"
|
||||
@click="() => pane.closable && $emit('close', pane.key)"
|
||||
key="1"
|
||||
>关闭</a-menu-item>
|
||||
<a-menu-item
|
||||
:disabled="!hasOther(pane)"
|
||||
@click="$emit('close-other', pane.key)"
|
||||
key="2"
|
||||
>关闭其他标签页</a-menu-item>
|
||||
<a-menu-item
|
||||
:disabled="!hasRight(pane)"
|
||||
@click="$emit('close-right', pane.key)"
|
||||
key="3"
|
||||
>关闭右侧标签页</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
<!-- <component
|
||||
:is="pane.component"
|
||||
:key="pane.key"
|
||||
:param="pane.param"
|
||||
ref="panes"
|
||||
v-if="pane.loaded"
|
||||
/>-->
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<div class="yo-tab-external-mount-content">
|
||||
<div
|
||||
:class="pane.key === actived ? 'yo-tab-external-tabpane-active' : 'yo-tab-external-tabpane-inactive'"
|
||||
:key="pane.key"
|
||||
class="yo-tab-external-tabpane"
|
||||
v-for="pane in panes"
|
||||
>
|
||||
<component
|
||||
:id="pane.key"
|
||||
:is="pane.component"
|
||||
:key="pane.key"
|
||||
:param="pane.param"
|
||||
ref="panes"
|
||||
v-if="pane.loaded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-content>
|
||||
</template>
|
||||
<script>
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
|
||||
NProgress.configure({ parent: '.ant-layout-content > .yo-tab-external-mount > .yo-tab-external-mount-content' });
|
||||
|
||||
export default {
|
||||
props: {
|
||||
panes: {
|
||||
type: Array,
|
||||
},
|
||||
tabActived: {
|
||||
type: [String, Number],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mode: process.env.VUE_APP_NODE_ENV,
|
||||
actived: '',
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
tabActived() {
|
||||
if (this.tabActived !== this.actived) {
|
||||
this.actived = this.tabActived;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.actived = this.tabActived;
|
||||
},
|
||||
methods: {
|
||||
onLoadContentWindow(key) {
|
||||
NProgress.start();
|
||||
const pane = this.panes.find((p) => p.key === key);
|
||||
|
||||
// 打开之前先销毁
|
||||
// 销毁后重新生成的组件会放置到$refs.panes的最后,所以这里直接根据索引取是错误的
|
||||
// const index = this.panes.indexOf(pane);
|
||||
// const component = this.$refs.panes && this.$refs.panes[index];
|
||||
// if (component) {
|
||||
// component.$destroy();
|
||||
// }
|
||||
|
||||
const i = import(`@/pages${pane.path}`);
|
||||
pane.component = () => i;
|
||||
pane.loaded = false;
|
||||
i.then(() => {})
|
||||
.catch(() => {
|
||||
pane.component = () => import('@/views/error/404');
|
||||
})
|
||||
.finally(() => {
|
||||
pane.loaded = true;
|
||||
NProgress.done();
|
||||
});
|
||||
},
|
||||
onClose(targetKey, action) {
|
||||
if (action === 'remove') {
|
||||
this.$emit('close', targetKey);
|
||||
}
|
||||
},
|
||||
onChange(activeKey) {
|
||||
this.$emit('change', activeKey);
|
||||
},
|
||||
|
||||
onCopyComponent(pane) {
|
||||
try {
|
||||
const copy = document.createElement('textarea');
|
||||
document.body.append(copy);
|
||||
copy.value = `/pages${pane.path}`;
|
||||
copy.select();
|
||||
setTimeout(() => {
|
||||
document.execCommand('copy');
|
||||
copy.remove();
|
||||
this.$message.success('已复制到剪切板');
|
||||
});
|
||||
} catch {
|
||||
this.$message.error('复制错误');
|
||||
}
|
||||
},
|
||||
|
||||
hasOther(pane) {
|
||||
return this.panes.filter((p) => p.key !== pane.key && p.closable).length > 0;
|
||||
},
|
||||
|
||||
hasRight(pane) {
|
||||
return this.panes.length > this.panes.indexOf(pane) + 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
100
Web/src/views/main-dynamic/_layout/header/index.vue
Normal file
100
Web/src/views/main-dynamic/_layout/header/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<a-layout-header class="header">
|
||||
<section
|
||||
v-if="$root.global.settings.layout === 'left-menu' || $root.global.settings.layout === 'right-menu'"
|
||||
>
|
||||
<div class="header-actions">
|
||||
<a
|
||||
@click="$root.global.settings.siderCollapsed = !$root.global.settings.siderCollapsed"
|
||||
class="header-action"
|
||||
>
|
||||
<a-icon :type="$root.global.settings.siderCollapsed ? 'menu-unfold' : 'menu-fold'" />
|
||||
</a>
|
||||
<search :menus="nav.content" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a @click="$emit('reload')" class="header-action">
|
||||
<a-icon type="reload" />
|
||||
</a>
|
||||
<a class="header-action">
|
||||
<a-badge count="5">
|
||||
<a-icon type="bell" />
|
||||
</a-badge>
|
||||
</a>
|
||||
<a @click="$emit('setting')" class="header-action">
|
||||
<a-icon type="setting" />
|
||||
</a>
|
||||
<User />
|
||||
</div>
|
||||
</section>
|
||||
<container v-else-if="$root.global.settings.layout === 'top-nav'">
|
||||
<div class="header-actions">
|
||||
<a @click="showNav = !showNav" class="header-action mr-md">
|
||||
<a-icon type="menu" />
|
||||
</a>
|
||||
<Logo />
|
||||
<search :menus="nav.content" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a @click="$emit('reload')" class="header-action">
|
||||
<a-icon type="reload" />
|
||||
</a>
|
||||
<a class="header-action">
|
||||
<a-badge count="5">
|
||||
<a-icon type="bell" />
|
||||
</a-badge>
|
||||
</a>
|
||||
<a @click="$emit('setting')" class="header-action">
|
||||
<a-icon type="setting" />
|
||||
</a>
|
||||
<User />
|
||||
</div>
|
||||
<a-drawer
|
||||
:body-style="{ padding: 0 }"
|
||||
:closable="false"
|
||||
:get-container="'.ant-layout-content > .yo-tab-external-mount'"
|
||||
:visible="showNav"
|
||||
:wrap-style="{ position: 'absolute' }"
|
||||
@close="showNav = false"
|
||||
placement="left"
|
||||
width="38.2%"
|
||||
>
|
||||
<div @blur="showNav = false" @mouseleave="showNav = false">
|
||||
<Nav :nav="nav" @open="showNav = false" />
|
||||
</div>
|
||||
</a-drawer>
|
||||
</container>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
<script>
|
||||
import Logo from '../logo';
|
||||
import Nav from '../nav';
|
||||
|
||||
import User from './user';
|
||||
import Search from './search';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Logo,
|
||||
Nav,
|
||||
|
||||
User,
|
||||
Search,
|
||||
},
|
||||
props: {
|
||||
nav: {
|
||||
default() {
|
||||
return {
|
||||
content: [],
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showNav: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
149
Web/src/views/main-dynamic/_layout/header/search.js
Normal file
149
Web/src/views/main-dynamic/_layout/header/search.js
Normal file
@@ -0,0 +1,149 @@
|
||||
export default {
|
||||
props: {
|
||||
menus: {
|
||||
type: Array,
|
||||
require: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
searchText: '',
|
||||
searchResult: [],
|
||||
|
||||
timer: null
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
renderSelect(menu) {
|
||||
return menu.map((p) => {
|
||||
return p.children ? this.renderSelectGroup(p) : this.renderSelectOption(p)
|
||||
})
|
||||
},
|
||||
|
||||
renderSelectGroup(menu) {
|
||||
return (
|
||||
<a-select-opt-group key={menu.parents}>
|
||||
<span slot="label">
|
||||
{menu.parents}
|
||||
</span>
|
||||
{this.renderSelect(menu.children)}
|
||||
</a-select-opt-group>
|
||||
)
|
||||
},
|
||||
|
||||
renderSelectOption(menu) {
|
||||
return (<a-select-option key={menu.id} value={
|
||||
JSON.stringify(menu)
|
||||
}>
|
||||
{menu.meta.icon && <a-icon type={menu.meta.icon} />}
|
||||
{menu.meta.title}
|
||||
<small style={{ display: 'block', color: '#aaa' }}>{menu.component}</small>
|
||||
</a-select-option>)
|
||||
},
|
||||
|
||||
|
||||
onSearch(value) {
|
||||
clearTimeout(this.timer)
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.doSearch(value)
|
||||
}, 300)
|
||||
},
|
||||
|
||||
doSearch(value) {
|
||||
this.searchText = value
|
||||
|
||||
const menus = this.$_.concat.apply(this, this.$_.cloneDeep(this.menus.map(p => p.menu)))
|
||||
|
||||
const search = (m) => {
|
||||
if (!value) return []
|
||||
return m.filter((p) => {
|
||||
if (p.children) {
|
||||
p.children = search(p.children)
|
||||
} else {
|
||||
return p.meta.title.indexOf(value) > -1 || (p.component || '').toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||
}
|
||||
return p.children.length
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分层级,只留下 [父级-父级-...] [子级]
|
||||
* *******在更为复杂的目录下会出现父级联动错误的问题
|
||||
*/
|
||||
const unzip = (m) => {
|
||||
const getSeed = (parent, seed) => {
|
||||
if (parent.children) {
|
||||
seed.parents.push(parent.meta.title)
|
||||
seed.children = parent.children
|
||||
parent.children.forEach(p => {
|
||||
getSeed(p, seed)
|
||||
})
|
||||
}
|
||||
return seed
|
||||
}
|
||||
const result = []
|
||||
m.forEach(p => {
|
||||
const r = { parents: [], children: [] }
|
||||
result.push(getSeed(p, r))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const result = unzip(search(menus)).filter(p => p.parents.length).map(p => {
|
||||
return {
|
||||
parents: p.parents.join('-'),
|
||||
children: p.children
|
||||
}
|
||||
})
|
||||
|
||||
this.searchResult = result
|
||||
},
|
||||
|
||||
onSearchSelect(value, node) {
|
||||
this.searchText = '';
|
||||
this.onSearch(this.searchText);
|
||||
|
||||
const menu = JSON.parse(node.componentOptions.propsData.value)
|
||||
|
||||
this.openContentWindow({
|
||||
key: menu.id,
|
||||
title: menu.meta.title,
|
||||
icon: menu.meta.icon,
|
||||
path: menu.component,
|
||||
});
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
render() {
|
||||
|
||||
const props = {
|
||||
dropdownMatchSelectWidth: false,
|
||||
dropdownStyle: { width: '300px' },
|
||||
optionLabelProp: 'value',
|
||||
placeholder: '请输入检索关键字',
|
||||
value: this.searchText
|
||||
}
|
||||
|
||||
const on = {
|
||||
search: this.onSearch,
|
||||
select: this.onSearchSelect
|
||||
}
|
||||
|
||||
return (
|
||||
<a-auto-complete {...{ props, on }}>
|
||||
<template slot="dataSource">
|
||||
{this.renderSelect(this.searchResult)}
|
||||
</template>
|
||||
<a-input allow-clear>
|
||||
<a-icon slot="suffix" type="search" class="certain-category-icon" />
|
||||
</a-input>
|
||||
</a-auto-complete>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
91
Web/src/views/main-dynamic/_layout/header/user.js
Normal file
91
Web/src/views/main-dynamic/_layout/header/user.js
Normal file
@@ -0,0 +1,91 @@
|
||||
let userOpenTimer, userCloseTimer
|
||||
|
||||
let initDropdownHeight
|
||||
|
||||
import { PERVIEW_URL } from '@/util/global';
|
||||
|
||||
import { doLogout } from '@/common/login'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dropdownHeight: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
initDropdownHeight = this.$refs.dropdown.scrollHeight
|
||||
},
|
||||
methods: {
|
||||
onOpen(e) {
|
||||
clearTimeout(userCloseTimer)
|
||||
e.target.classList.add('open')
|
||||
userOpenTimer = setTimeout(() => {
|
||||
e.target.classList.add('drop')
|
||||
this.dropdownHeight = initDropdownHeight
|
||||
}, 300)
|
||||
},
|
||||
|
||||
onClose(e) {
|
||||
clearTimeout(userOpenTimer)
|
||||
e.target.classList.remove('drop')
|
||||
this.dropdownHeight = 0
|
||||
userCloseTimer = setTimeout(() => {
|
||||
e.target.classList.remove('open')
|
||||
}, 300)
|
||||
},
|
||||
|
||||
onAccountSetting() {
|
||||
this.openContentWindow({
|
||||
key: 'account-home',
|
||||
title: '个人中心',
|
||||
icon: 'user',
|
||||
path: '/system/account'
|
||||
})
|
||||
},
|
||||
|
||||
onLogout() {
|
||||
this.$confirm({
|
||||
title: '提示',
|
||||
content: '是否确定退出登录',
|
||||
onOk: async () => {
|
||||
await doLogout()
|
||||
},
|
||||
onCancel() {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div onMouseenter={this.onOpen} onMouseleave={this.onClose} class="user-container" >
|
||||
<div class="user-container-inner">
|
||||
<div class="user--base">
|
||||
{
|
||||
this.$root.global.info && <yo-image width="32" type="avatar" class="user--avatar" icon="user" id={this.$root.global.info.avatar} />
|
||||
}
|
||||
{
|
||||
this.$root.global.info &&
|
||||
<span
|
||||
class="user--name"
|
||||
>{this.$root.global.info.nickName || this.$root.global.info.name}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="user--dropdown" ref="dropdown" style={{ height: `${this.dropdownHeight}px` }}>
|
||||
<ul class="ant-dropdown-menu ant-dropdown-menu-vertical">
|
||||
<li class="ant-dropdown-menu-item" onClick={this.onAccountSetting}>
|
||||
<a-icon type="user" />
|
||||
个人中心
|
||||
</li>
|
||||
<li class="ant-dropdown-menu-item-divider"></li>
|
||||
<li class="ant-dropdown-menu-item" onClick={this.onLogout}>
|
||||
<a-icon type="logout" />
|
||||
退出登录
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
}
|
||||
10
Web/src/views/main-dynamic/_layout/logo.vue
Normal file
10
Web/src/views/main-dynamic/_layout/logo.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<img
|
||||
:src="require('@/assets/image/logo.png')"
|
||||
alt
|
||||
v-if="$root.global.settings.navTheme == 'light'"
|
||||
/>
|
||||
<img :src="require('@/assets/image/logo-w.png')" alt v-else />
|
||||
</div>
|
||||
</template>
|
||||
116
Web/src/views/main-dynamic/_layout/sider/index.vue
Normal file
116
Web/src/views/main-dynamic/_layout/sider/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<section>
|
||||
<a-layout-sider
|
||||
:collapsed="siderCollapsed === undefined ? $root.global.settings.siderCollapsed : siderCollapsed"
|
||||
@collapse="onCollapse"
|
||||
v-if="$root.global.settings.layout === 'left-menu' || $root.global.settings.layout === 'right-menu'"
|
||||
width="200"
|
||||
>
|
||||
<Logo />
|
||||
<div class="yo-sider-nav">
|
||||
<div class="swiper-container" id="layout--swiper-container">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<a-spin :spinning="nav.loading">
|
||||
<a-icon slot="indicator" spin type="loading" />
|
||||
<Menu
|
||||
:menu-style="{ height: '100%', borderRight: 0 }"
|
||||
:nav="nav"
|
||||
@openChange="onMenuOpenChange"
|
||||
mode="inline"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-scrollbar" id="layout--swiper-scrollbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
import Logo from '../logo';
|
||||
import Menu from './menu';
|
||||
|
||||
import Swiper from 'swiper';
|
||||
|
||||
let timer,
|
||||
swiper,
|
||||
siderSwiperOptions = {
|
||||
direction: 'vertical',
|
||||
slidesPerView: 'auto',
|
||||
freeMode: true,
|
||||
scrollbar: {
|
||||
el: '#layout--swiper-scrollbar',
|
||||
},
|
||||
mousewheel: true,
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Logo,
|
||||
Menu,
|
||||
},
|
||||
props: {
|
||||
nav: {
|
||||
default() {
|
||||
return {
|
||||
apps: [],
|
||||
menus: [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
siderCollapsed: undefined,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// swiper不能使用vue版本的组件.效果相当差
|
||||
swiper = new Swiper('#layout--swiper-container', siderSwiperOptions);
|
||||
this.$nextTick(() => {
|
||||
this.onUpdateSwiper();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.$root.global.settings.layout === 'left-menu' || this.$root.global.settings.layout === 'right-menu') {
|
||||
if (!this.$root.global.settings.siderCollapsed) {
|
||||
if (window.innerWidth < 1000) {
|
||||
this.siderCollapsed = true;
|
||||
} else {
|
||||
this.siderCollapsed = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.onUpdateSwiper();
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onUpdateSwiper() {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
// 需要更新两次
|
||||
swiper.update();
|
||||
swiper.update();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
onMenuOpenChange() {
|
||||
this.onUpdateSwiper();
|
||||
},
|
||||
|
||||
onCollapse() {
|
||||
this.onUpdateSwiper();
|
||||
},
|
||||
|
||||
windowTriggerResize() {
|
||||
let e = new Event('resize');
|
||||
window.dispatchEvent(e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
92
Web/src/views/main-dynamic/_layout/sider/menu.js
Normal file
92
Web/src/views/main-dynamic/_layout/sider/menu.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { HmacMD5 } from "crypto-js"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
nav: {
|
||||
default() {
|
||||
return {
|
||||
content: []
|
||||
}
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
menuStyle: {
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
type: Object || String,
|
||||
},
|
||||
mode: {
|
||||
default: 'inline',
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
renderMenu(menu) {
|
||||
return menu.map((p) => {
|
||||
return p.children ? this.renderSubMenu(p) : this.renderMenuItem(p)
|
||||
})
|
||||
},
|
||||
|
||||
renderSubMenu(menu) {
|
||||
return (
|
||||
<a-sub-menu key={menu.id}>
|
||||
<span slot="title">
|
||||
{menu.meta.icon && <a-icon type={menu.meta.icon} />}
|
||||
<span>{menu.meta.title}</span>
|
||||
</span>
|
||||
{this.renderMenu(menu.children)}
|
||||
</a-sub-menu>
|
||||
)
|
||||
},
|
||||
|
||||
renderMenuItem(menu) {
|
||||
return (
|
||||
<a-menu-item key={menu.id} onClick={() => this.onOpenContentWindow(menu)}>
|
||||
{menu.meta.icon && <a-icon type={menu.meta.icon} />}
|
||||
<span>{menu.meta.title}</span>
|
||||
</a-menu-item>
|
||||
)
|
||||
},
|
||||
|
||||
onMenuOpenChange() {
|
||||
this.$emit('openChange')
|
||||
},
|
||||
|
||||
onOpenContentWindow(menu) {
|
||||
this.openContentWindow({
|
||||
key: menu.id,
|
||||
title: menu.meta.title,
|
||||
icon: menu.meta.icon,
|
||||
path: menu.component,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const props = {
|
||||
mode: this.mode,
|
||||
selectable: false,
|
||||
style: this.menuStyle,
|
||||
theme: this.$root.global.settings.navTheme,
|
||||
}
|
||||
|
||||
const on = {
|
||||
openChange: this.onMenuOpenChange,
|
||||
}
|
||||
|
||||
return (<section>
|
||||
{
|
||||
this.nav.content.map(item => {
|
||||
return (
|
||||
<section>
|
||||
<div class="yo-sider-nav--app">{item.app.name}</div>
|
||||
<a-menu {...{ props, on }}>{this.renderMenu(item.menu)}</a-menu>
|
||||
</section>
|
||||
)
|
||||
})
|
||||
}
|
||||
</section>)
|
||||
},
|
||||
}
|
||||
239
Web/src/views/main-dynamic/index.vue
Normal file
239
Web/src/views/main-dynamic/index.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<section>
|
||||
<a-spin :spinning="nav.loading" class="yo-layout--spin">
|
||||
<a-icon :style="{ fontSize: '24px' }" slot="indicator" spin type="loading" />
|
||||
<a-layout
|
||||
:class="{
|
||||
[`yo-layout--top-nav--${$root.global.settings.container}`]: $root.global.settings.layout === 'top-nav',
|
||||
[`yo-layout--${$root.global.settings.layout}`]: true,
|
||||
}"
|
||||
>
|
||||
<Sider :nav="nav" v-if="$root.global.settings.layout === 'left-menu'" />
|
||||
<a-layout>
|
||||
<Header :nav="nav" @reload="onReloadContentWindow" @setting="setting.visible = true" />
|
||||
<a-layout>
|
||||
<Content
|
||||
:panes="panes"
|
||||
:tabActived="tabActived"
|
||||
@change="onChangeContentWindow"
|
||||
@close="onCloseContentWindow"
|
||||
@close-other="onCloseOtherContentWindow"
|
||||
@close-right="onCloseRightContentWindow"
|
||||
ref="content"
|
||||
/>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
<Sider :nav="nav" v-if="$root.global.settings.layout === 'right-menu'" />
|
||||
</a-layout>
|
||||
<Setting :visible="setting.visible" @close="setting.visible = false" />
|
||||
</a-spin>
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
|
||||
import Header from './_layout/header';
|
||||
import Sider from './_layout/sider/index';
|
||||
import Content from './_layout/content';
|
||||
|
||||
import Setting from './setting';
|
||||
|
||||
import { EMPTY_ID, setGlobal } from '@/util/global';
|
||||
|
||||
const getNewID = () => {
|
||||
return Math.random().toString(16).slice(2);
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Header,
|
||||
Sider,
|
||||
Content,
|
||||
Setting,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
panes: new Array(),
|
||||
tabActived: '',
|
||||
|
||||
setting: {
|
||||
visible: false,
|
||||
},
|
||||
nav: {
|
||||
loading: false,
|
||||
content: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
Vue.prototype.openContentWindow = this.onOpenContentWindow;
|
||||
Vue.prototype.closeContentWindow = this.onCloseContentWindow;
|
||||
|
||||
this.nav.loading = true;
|
||||
|
||||
this.$api.getLoginUser().then(async ({ data }) => {
|
||||
// 去除应用和菜单信息,存储基本信息
|
||||
const info = this.$_.cloneDeep(data);
|
||||
delete info.apps;
|
||||
delete info.menus;
|
||||
setGlobal(info);
|
||||
|
||||
this.nav.content = await this.onSetNav(data);
|
||||
this.nav.loading = false;
|
||||
|
||||
this.$root.global.defaultWindow.map((options) => {
|
||||
this.onOpenContentWindow(options);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 打开一个新的标签页
|
||||
* settings: {
|
||||
* title: String,
|
||||
* path: String,
|
||||
* icon: String,
|
||||
* param: Object,
|
||||
* closable: Boolean = true,
|
||||
* }
|
||||
*/
|
||||
onOpenContentWindow(settings) {
|
||||
if (settings.path) {
|
||||
const key = settings.key || getNewID();
|
||||
|
||||
/**
|
||||
* 如果当前标签页已打开,则只需要选中
|
||||
*/
|
||||
const pane = this.panes.find((p) => p.key === key);
|
||||
if (pane) {
|
||||
this.onChangeContentWindow(key);
|
||||
return;
|
||||
}
|
||||
|
||||
const path = settings.path.startsWith('/') ? settings.path : `/${settings.path}`;
|
||||
|
||||
/**
|
||||
* 向标签页队列中添加一个新的标签页
|
||||
*/
|
||||
this.panes.push({
|
||||
key,
|
||||
closable: settings.closable === undefined ? true : settings.closable,
|
||||
icon: settings.icon,
|
||||
title: settings.title || '新建窗口',
|
||||
subTitle: settings.subTitle,
|
||||
component: null,
|
||||
path,
|
||||
param: settings.param,
|
||||
loaded: false,
|
||||
});
|
||||
|
||||
this.onChangeContentWindow(key);
|
||||
|
||||
this.$refs.content.onLoadContentWindow(key);
|
||||
} else {
|
||||
console.warn('wrong component path');
|
||||
}
|
||||
},
|
||||
|
||||
onCloseContentWindow(key) {
|
||||
key = key || this.tabActived;
|
||||
const i = this.$_.findIndex(this.panes, (p) => p.key === key);
|
||||
this.panes.splice(i, 1);
|
||||
|
||||
if (this.panes.length) {
|
||||
if (key === this.tabActived) {
|
||||
const pane = this.panes[i];
|
||||
if (pane) {
|
||||
this.onChangeContentWindow(pane.key);
|
||||
} else {
|
||||
this.onChangeContentWindow(this.panes[i - 1].key);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onCloseOtherContentWindow(key) {
|
||||
const panes = Object.assign([], this.panes);
|
||||
let flag = false;
|
||||
for (let i = panes.length - 1; i >= 0; i--) {
|
||||
const p = panes[i];
|
||||
if (p.key !== key && p.closable) {
|
||||
if (p.key === this.tabActived) {
|
||||
flag = true;
|
||||
}
|
||||
panes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.panes = panes;
|
||||
if (flag) {
|
||||
this.onChangeContentWindow(this.$_.last(panes).key);
|
||||
}
|
||||
},
|
||||
|
||||
onCloseRightContentWindow(key) {
|
||||
const panes = Object.assign([], this.panes);
|
||||
let flag = false;
|
||||
for (let i = panes.length - 1; i >= 0; i--) {
|
||||
const p = panes[i];
|
||||
if (p.key !== key && p.closable) {
|
||||
if (p.key === this.tabActived) {
|
||||
flag = true;
|
||||
}
|
||||
panes.splice(i, 1);
|
||||
} else if (p.key === key) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.panes = panes;
|
||||
if (flag) {
|
||||
this.onChangeContentWindow(this.$_.last(panes).key);
|
||||
}
|
||||
},
|
||||
|
||||
onChangeContentWindow(key) {
|
||||
this.tabActived = key;
|
||||
},
|
||||
|
||||
onReloadContentWindow(key) {
|
||||
if (!key) key = this.tabActived;
|
||||
this.$refs.content.onLoadContentWindow(key);
|
||||
},
|
||||
|
||||
onSetNav(nav) {
|
||||
const getNav = [];
|
||||
nav.apps.forEach((app) => {
|
||||
getNav.push({
|
||||
app,
|
||||
});
|
||||
});
|
||||
|
||||
return this.$api
|
||||
.$queue(getNav.map((p) => this.$api.sysMenuChangeAwait({ application: p.app.code })))
|
||||
.then((menus) => {
|
||||
menus.forEach((menu, i) => {
|
||||
getNav[i].menu = this.serializeMenu(menu.data);
|
||||
});
|
||||
return getNav;
|
||||
});
|
||||
},
|
||||
|
||||
serializeMenu(menus) {
|
||||
const menu = this.$_.cloneDeep(menus);
|
||||
const children = this.$_.groupBy(menu, 'pid');
|
||||
|
||||
const serialize = (m) => {
|
||||
m.map((p) => {
|
||||
if (children[p.id]) {
|
||||
p.children = serialize(children[p.id]);
|
||||
}
|
||||
});
|
||||
return m;
|
||||
};
|
||||
|
||||
return children[EMPTY_ID] ? serialize(children[EMPTY_ID]) : new Array();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,35 +1,11 @@
|
||||
<template>
|
||||
<a-layout-header class="header">
|
||||
<section
|
||||
v-if="$root.global.settings.layout === 'left-menu' || $root.global.settings.layout === 'right-menu'"
|
||||
>
|
||||
<container>
|
||||
<div class="header-actions">
|
||||
<a
|
||||
@click="$root.global.settings.siderCollapsed = !$root.global.settings.siderCollapsed"
|
||||
class="header-action"
|
||||
class="header-action mr-md"
|
||||
>
|
||||
<a-icon :type="$root.global.settings.siderCollapsed ? 'menu-unfold' : 'menu-fold'" />
|
||||
</a>
|
||||
<search :menus="nav.content" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a @click="$emit('reload')" class="header-action">
|
||||
<a-icon type="reload" />
|
||||
</a>
|
||||
<a class="header-action">
|
||||
<a-badge count="5">
|
||||
<a-icon type="bell" />
|
||||
</a-badge>
|
||||
</a>
|
||||
<a @click="$emit('setting')" class="header-action">
|
||||
<a-icon type="setting" />
|
||||
</a>
|
||||
<User />
|
||||
</div>
|
||||
</section>
|
||||
<container v-else-if="$root.global.settings.layout === 'top-nav'">
|
||||
<div class="header-actions">
|
||||
<a @click="showNav = !showNav" class="header-action mr-md">
|
||||
<a-icon type="menu" />
|
||||
</a>
|
||||
<Logo />
|
||||
@@ -44,31 +20,13 @@
|
||||
<a-icon type="bell" />
|
||||
</a-badge>
|
||||
</a>
|
||||
<a @click="$emit('setting')" class="header-action">
|
||||
<a-icon type="setting" />
|
||||
</a>
|
||||
<User />
|
||||
</div>
|
||||
<a-drawer
|
||||
:body-style="{ padding: 0 }"
|
||||
:closable="false"
|
||||
:get-container="'.ant-layout-content > .yo-tab-external-mount'"
|
||||
:visible="showNav"
|
||||
:wrap-style="{ position: 'absolute' }"
|
||||
@close="showNav = false"
|
||||
placement="left"
|
||||
width="38.2%"
|
||||
>
|
||||
<div @blur="showNav = false" @mouseleave="showNav = false">
|
||||
<Nav :nav="nav" @open="showNav = false" />
|
||||
</div>
|
||||
</a-drawer>
|
||||
</container>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
<script>
|
||||
import Logo from '../logo';
|
||||
import Nav from '../nav';
|
||||
|
||||
import User from './user';
|
||||
import Search from './search';
|
||||
@@ -76,7 +34,6 @@ import Search from './search';
|
||||
export default {
|
||||
components: {
|
||||
Logo,
|
||||
Nav,
|
||||
|
||||
User,
|
||||
Search,
|
||||
@@ -91,10 +48,5 @@ export default {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showNav: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,32 +1,29 @@
|
||||
<template>
|
||||
<section>
|
||||
<a-layout-sider
|
||||
:collapsed="siderCollapsed === undefined ? $root.global.settings.siderCollapsed : siderCollapsed"
|
||||
@collapse="onCollapse"
|
||||
v-if="$root.global.settings.layout === 'left-menu' || $root.global.settings.layout === 'right-menu'"
|
||||
width="200"
|
||||
>
|
||||
<Logo />
|
||||
<div class="yo-sider-nav">
|
||||
<div class="swiper-container" id="layout--swiper-container">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<a-spin :spinning="nav.loading">
|
||||
<a-icon slot="indicator" spin type="loading" />
|
||||
<Menu
|
||||
:menu-style="{ height: '100%', borderRight: 0 }"
|
||||
:nav="nav"
|
||||
@openChange="onMenuOpenChange"
|
||||
mode="inline"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
<a-layout-sider
|
||||
:collapsed="siderCollapsed === undefined ? $root.global.settings.siderCollapsed : siderCollapsed"
|
||||
@collapse="onCollapse"
|
||||
class="yo-layout-sider"
|
||||
width="200"
|
||||
>
|
||||
<div class="yo-sider-nav">
|
||||
<div class="swiper-container" id="layout--swiper-container">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<a-spin :spinning="nav.loading">
|
||||
<a-icon slot="indicator" spin type="loading" />
|
||||
<Menu
|
||||
:menu-style="{ height: '100%', borderRight: 0 }"
|
||||
:nav="nav"
|
||||
@openChange="onMenuOpenChange"
|
||||
mode="inline"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
<div class="swiper-scrollbar" id="layout--swiper-scrollbar"></div>
|
||||
</div>
|
||||
<div class="swiper-scrollbar" id="layout--swiper-scrollbar"></div>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
</section>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
<script>
|
||||
import Logo from '../logo';
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
mode: this.mode,
|
||||
selectable: false,
|
||||
style: this.menuStyle,
|
||||
theme: this.$root.global.settings.navTheme,
|
||||
theme: 'light',
|
||||
}
|
||||
|
||||
const on = {
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
[`yo-layout--${$root.global.settings.layout}`]: true,
|
||||
}"
|
||||
>
|
||||
<Sider :nav="nav" v-if="$root.global.settings.layout === 'left-menu'" />
|
||||
<a-layout>
|
||||
<Header :nav="nav" @reload="onReloadContentWindow" @setting="setting.visible = true" />
|
||||
<a-layout>
|
||||
<a-layout class="yo-nav-theme--light">
|
||||
<Sider :nav="nav" />
|
||||
<Content
|
||||
:panes="panes"
|
||||
:tabActived="tabActived"
|
||||
@@ -23,9 +23,7 @@
|
||||
/>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
<Sider :nav="nav" v-if="$root.global.settings.layout === 'right-menu'" />
|
||||
</a-layout>
|
||||
<Setting :visible="setting.visible" @close="setting.visible = false" />
|
||||
</a-spin>
|
||||
</section>
|
||||
</template>
|
||||
@@ -36,8 +34,6 @@ import Header from './_layout/header';
|
||||
import Sider from './_layout/sider/index';
|
||||
import Content from './_layout/content';
|
||||
|
||||
import Setting from './setting';
|
||||
|
||||
import { EMPTY_ID, setGlobal } from '@/util/global';
|
||||
|
||||
const getNewID = () => {
|
||||
@@ -49,16 +45,12 @@ export default {
|
||||
Header,
|
||||
Sider,
|
||||
Content,
|
||||
Setting,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
panes: new Array(),
|
||||
tabActived: '',
|
||||
|
||||
setting: {
|
||||
visible: false,
|
||||
},
|
||||
nav: {
|
||||
loading: false,
|
||||
content: [],
|
||||
|
||||
Reference in New Issue
Block a user