feat(admin): add msh_single_admin project and harden ignore rules
Introduce the new Vue admin project into version control while tightening gitignore patterns to keep env files, logs, build artifacts, and test outputs out of commits. Made-with: Cursor
This commit is contained in:
60
msh_single_admin/src/layout/components/AppMain.vue
Executable file
60
msh_single_admin/src/layout/components/AppMain.vue
Executable file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<keep-alive :include="cachedViews">
|
||||
<router-view :key="key" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppMain',
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
cachedViews() {
|
||||
return this.$store.state.tagsView.cachedViews;
|
||||
},
|
||||
key() {
|
||||
return this.$route.path;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footers {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #808695;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: #808695;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
background: #f5f5f5;
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 36px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 80px;
|
||||
}
|
||||
}
|
||||
.el-popup-parent--hidden {
|
||||
.fixed-header {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
206
msh_single_admin/src/layout/components/Navbar.vue
Executable file
206
msh_single_admin/src/layout/components/Navbar.vue
Executable file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<hamburger
|
||||
id="hamburger-container"
|
||||
:is-active="sidebar.opened"
|
||||
class="hamburger-container"
|
||||
@toggleClick="toggleSideBar"
|
||||
/>
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav" />
|
||||
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
|
||||
<div class="right-menu">
|
||||
<template v-if="device !== 'mobile'">
|
||||
<search id="header-search" class="right-menu-item" />
|
||||
<!-- <error-log class="errLog-container right-menu-item hover-effect" /> -->
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
<span
|
||||
class="iconfont iconios-notifications-outline right-menu-item"
|
||||
style="font-size: 22px; opacity: 0.5"
|
||||
></span>
|
||||
</template>
|
||||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
|
||||
<div class="avatar-wrapper">{{ JavaInfo.realName }}<i class="el-icon-arrow-down el-icon--right"></i></div>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<router-link to="/">
|
||||
<el-dropdown-item>主页</el-dropdown-item>
|
||||
</router-link>
|
||||
<router-link :to="{ path: '/maintain/user' }" v-if="!isPhone">
|
||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||
</router-link>
|
||||
<router-link :to="{ path: '/maintain/update' }" v-if="!isPhone">
|
||||
<el-dropdown-item>修改密码</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item @click.native="setting = true">
|
||||
<span>布局设置</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="logout">
|
||||
<span>退出</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import TopNav from '@/components/TopNav';
|
||||
import Breadcrumb from '@/components/Breadcrumb';
|
||||
import Hamburger from '@/components/Hamburger';
|
||||
import ErrorLog from '@/components/ErrorLog';
|
||||
import Screenfull from '@/components/Screenfull';
|
||||
import Search from '@/components/HeaderSearch';
|
||||
import { unbindApi } from '@/api/wxApi';
|
||||
import Cookies from 'js-cookie';
|
||||
export default {
|
||||
components: {
|
||||
TopNav,
|
||||
Breadcrumb,
|
||||
Hamburger,
|
||||
ErrorLog,
|
||||
Screenfull,
|
||||
Search,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isPhone: this.$wechat.isPhone(),
|
||||
JavaInfo: JSON.parse(Cookies.get('JavaInfo')),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show(value) {
|
||||
if (value && !this.clickNotClose) {
|
||||
this.addEventClick();
|
||||
}
|
||||
if (value) {
|
||||
addClass(document.body, 'showRightPanel');
|
||||
} else {
|
||||
removeClass(document.body, 'showRightPanel');
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['sidebar', 'avatar', 'device']),
|
||||
topNav: {
|
||||
get() {
|
||||
return this.$store.state.settings.topNav;
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
get() {
|
||||
return this.$store.state.settings.showSettings;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'showSettings',
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onUnbundling() {
|
||||
this.$modalSure('解绑微信吗').then(() => {
|
||||
unbindApi().then(() => {
|
||||
this.$message.success('解绑成功');
|
||||
});
|
||||
});
|
||||
},
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar');
|
||||
},
|
||||
async logout() {
|
||||
await this.$store.dispatch('user/logout');
|
||||
this.$router.push(`/login?redirect=${this.$route.fullPath}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
// box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.topmenu-container {
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.errLog-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
float: right;
|
||||
height: 100%;
|
||||
line-height: 50px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: #5a5e66;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
&.hover-effect {
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 30px;
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.el-icon-caret-bottom {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 25px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
265
msh_single_admin/src/layout/components/Settings/index.vue
Executable file
265
msh_single_admin/src/layout/components/Settings/index.vue
Executable file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div class="drawer-container">
|
||||
<div>
|
||||
<h3 class="drawer-title">主题风格设置</h3>
|
||||
<div class="setting-drawer-block-checbox">
|
||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
|
||||
<img src="@/assets/imgs/dark.svg" alt="dark" />
|
||||
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
||||
<i aria-label="图标: check" class="anticon anticon-check">
|
||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true"
|
||||
focusable="false" class="">
|
||||
<path
|
||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
|
||||
<img src="@/assets/imgs/light.svg" alt="light" />
|
||||
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon"
|
||||
style="display: block">
|
||||
<i aria-label="图标: check" class="anticon anticon-check">
|
||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true"
|
||||
focusable="false" class="">
|
||||
<path
|
||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="drawer-item">
|
||||
<span>主题颜色</span>
|
||||
<theme-picker style="float: right; height: 26px; margin: -3px 8px 0 0" @change="themeChange" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>开启 TopNav</span>
|
||||
<el-switch v-model="topNav" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item" v-if="topNav">
|
||||
<span>开启 Icon</span>
|
||||
<el-switch v-model="navIcon" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>开启 Tags-Views</span>
|
||||
<el-switch v-model="tagsView" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>固定 Header</span>
|
||||
<el-switch v-model="fixedHeader" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>显示 Logo</span>
|
||||
<el-switch v-model="sidebarLogo" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
<el-button type="primary" plain icon="el-icon-document-add" @click="saveSetting">保存配置</el-button>
|
||||
<el-button plain icon=" el-icon-refresh" @click="resetSetting">重置配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ThemePicker from '@/components/ThemePicker';
|
||||
|
||||
export default {
|
||||
components: { ThemePicker },
|
||||
data() {
|
||||
return {
|
||||
sideTheme: this.$store.state.settings.sideTheme,
|
||||
routers: this.$store.state.permission.routes,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
theme: {
|
||||
get() {
|
||||
return this.$store.state.settings.theme;
|
||||
},
|
||||
},
|
||||
fixedHeader: {
|
||||
get() {
|
||||
return this.$store.state.settings.fixedHeader;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'fixedHeader',
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
topNav: {
|
||||
get() {
|
||||
//回调函数 当需要读取当前属性值是执行,根据相关数据计算并返回当前属性的值
|
||||
return this.$store.state.settings.topNav;
|
||||
},
|
||||
set(val) {
|
||||
//监视当前属性值的变化,当属性值发生变化时执行,更新相关的属性数据
|
||||
//val就是topNav的最新属性值
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'topNav',
|
||||
value: val,
|
||||
});
|
||||
if (val) {
|
||||
let key = this.$route.path.split('/')[1];
|
||||
//通过截取当前路由的第一级目录跟顶部一级菜单选中项的val值做匹配
|
||||
key = '/' + key;
|
||||
this.routers.map((item) => {
|
||||
if (key == item.url && item.child) {
|
||||
//如果匹配,就给侧边导航赋值为选中项的子级数组
|
||||
this.$store.commit('permission/SET_SIDEBAR_ROUTERS', item.child);
|
||||
} else if (key == item.url && !item.child) {
|
||||
//如果遍历以后val值等于item的url,但是有没有子级,就把它子级赋值给侧边导航的数组,这里针对dashboard控制台
|
||||
this.$store.commit('permission/SET_SIDEBAR_ROUTERS', [item]);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!val) {
|
||||
//关闭的时候侧边导航的取值还是取默认的routes数组
|
||||
this.$store.commit('permission/SET_SIDEBAR_ROUTERS', this.$store.state.permission.routes);
|
||||
}
|
||||
},
|
||||
},
|
||||
navIcon: {
|
||||
get() {
|
||||
return this.$store.state.settings.navIcon;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
//dispatch:含有异步操作,例如向后台提交数据,写法: this.$store.dispatch('action方法名',值)
|
||||
//commit:同步操作,写法:this.$store.commit('mutations方法名',值)
|
||||
key: 'navIcon',
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
tagsView: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsView;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsView',
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
sidebarLogo: {
|
||||
get() {
|
||||
return this.$store.state.settings.sidebarLogo;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'sidebarLogo',
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
themeChange(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'theme',
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
handleTheme(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'sideTheme',
|
||||
value: val,
|
||||
});
|
||||
this.sideTheme = val;
|
||||
},
|
||||
saveSetting() {
|
||||
this.$modal.loading('正在保存到本地,请稍候...');
|
||||
//将设置写入缓存
|
||||
this.$cache.local.setJSON('layout-setting', {
|
||||
topNav: this.topNav,
|
||||
tagsView: this.tagsView,
|
||||
fixedHeader: this.fixedHeader,
|
||||
sidebarLogo: this.sidebarLogo,
|
||||
dynamicTitle: this.dynamicTitle,
|
||||
sideTheme: this.sideTheme,
|
||||
theme: this.theme,
|
||||
navIcon: this.navIcon,
|
||||
});
|
||||
setTimeout(this.$modal.closeLoading(), 1000);
|
||||
},
|
||||
resetSetting() {
|
||||
this.$modal.loading('正在清除设置缓存并刷新,请稍候...');
|
||||
this.$cache.local.remove('layout-setting');
|
||||
setTimeout('window.location.reload()', 1000);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-switch {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.drawer-container {
|
||||
padding: 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
|
||||
.drawer-title {
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.drawer-item {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.drawer-switch {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.setting-drawer-block-checbox {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.setting-drawer-block-checbox-item {
|
||||
position: relative;
|
||||
margin-right: 16px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.setting-drawer-block-checbox-selectIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 15px;
|
||||
padding-left: 24px;
|
||||
color: #1890ff;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
msh_single_admin/src/layout/components/Sidebar/FixiOSBug.js
Executable file
26
msh_single_admin/src/layout/components/Sidebar/FixiOSBug.js
Executable file
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
computed: {
|
||||
device() {
|
||||
return this.$store.state.app.device;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
|
||||
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
|
||||
this.fixBugIniOS();
|
||||
},
|
||||
methods: {
|
||||
fixBugIniOS() {
|
||||
const $subMenu = this.$refs.subMenu;
|
||||
if ($subMenu) {
|
||||
const handleMouseleave = $subMenu.handleMouseleave;
|
||||
$subMenu.handleMouseleave = (e) => {
|
||||
if (this.device === 'mobile') {
|
||||
return;
|
||||
}
|
||||
handleMouseleave(e);
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
30
msh_single_admin/src/layout/components/Sidebar/Item.vue
Executable file
30
msh_single_admin/src/layout/components/Sidebar/Item.vue
Executable file
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'MenuItem',
|
||||
functional: true,
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
render(h, context) {
|
||||
const { icon, title } = context.props;
|
||||
const vnodes = [];
|
||||
|
||||
if (icon) {
|
||||
const ic = 'el-icon-' + icon;
|
||||
vnodes.push(<i style="color:#ffffff;" class={ic} />);
|
||||
}
|
||||
|
||||
if (title) {
|
||||
vnodes.push(<span slot="title">{title}</span>);
|
||||
}
|
||||
return vnodes;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
35
msh_single_admin/src/layout/components/Sidebar/Link.vue
Executable file
35
msh_single_admin/src/layout/components/Sidebar/Link.vue
Executable file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<!-- eslint-disable vue/require-component-is -->
|
||||
<component v-bind="linkProps(to)">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal } from '@/utils/validate';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
linkProps(url) {
|
||||
if (isExternal(url)) {
|
||||
return {
|
||||
is: 'a',
|
||||
href: url,
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
};
|
||||
}
|
||||
return {
|
||||
is: 'router-link',
|
||||
to: url,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
99
msh_single_admin/src/layout/components/Sidebar/Logo.vue
Executable file
99
msh_single_admin/src/layout/components/Sidebar/Logo.vue
Executable file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img v-if="siteLogoSquare" :src="siteLogoSquare" class="sidebar-logo-small" />
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img v-if="siteLogoLeftTop" :src="siteLogoLeftTop" class="sidebar-logo-big" />
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as systemConfigApi from '@/api/systemConfig.js';
|
||||
export default {
|
||||
name: 'SidebarLogo',
|
||||
props: {
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: 'Vue Element Admin',
|
||||
siteLogoLeftTop: '', //左上角logo大
|
||||
siteLogoSquare: '', //左上角logo小
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getLogo();
|
||||
},
|
||||
methods: {
|
||||
//获取左上角菜单logo
|
||||
getLogo() {
|
||||
systemConfigApi.getSiteLogoApi().then((data) => {
|
||||
this.siteLogoLeftTop = data.siteLogoLeftTop;
|
||||
this.siteLogoSquare = data.siteLogoSquare;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
.sidebar-logo-big {
|
||||
width: auto;
|
||||
height: 40px;
|
||||
vertical-align: middle;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-enter,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
& .sidebar-logo-link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& .sidebar-logo-small {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
line-height: 50px;
|
||||
font-size: 14px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
.sidebar-logo {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
msh_single_admin/src/layout/components/Sidebar/SidebarItem.vue
Executable file
100
msh_single_admin/src/layout/components/Sidebar/SidebarItem.vue
Executable file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template
|
||||
v-if="
|
||||
hasOneShowingChild(item.child, item) &&
|
||||
(!onlyOneChild.child || onlyOneChild.noShowingChildren) &&
|
||||
!item.alwaysShow
|
||||
"
|
||||
>
|
||||
<app-link v-if="onlyOneChild" :to="resolvePath(onlyOneChild.url)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.url)" :class="{ 'submenu-title-noDropdown': !isNest }">
|
||||
<item :icon="onlyOneChild.extra || (item.meta && item.extra)" :title="onlyOneChild.name" />
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.url)" popper-append-to-body>
|
||||
<template slot="title">
|
||||
<item v-if="item" :icon="item && item.extra" :title="item.name" />
|
||||
</template>
|
||||
<sidebar-item
|
||||
v-for="childs in item.child"
|
||||
:key="childs.url"
|
||||
:is-nest="true"
|
||||
:item="childs"
|
||||
:base-path="resolvePath(childs.url)"
|
||||
class="nest-menu"
|
||||
/>
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path';
|
||||
import { isExternal } from '@/utils/validate';
|
||||
import Item from './Item';
|
||||
import AppLink from './Link';
|
||||
import FixiOSBug from './FixiOSBug';
|
||||
|
||||
export default {
|
||||
name: 'SidebarItem',
|
||||
components: { Item, AppLink },
|
||||
mixins: [FixiOSBug],
|
||||
props: {
|
||||
// route object
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
|
||||
// TODO: refactor with render function
|
||||
this.onlyOneChild = null;
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
hasOneShowingChild(child = [], parent) {
|
||||
const showingChildren = child.filter((item) => {
|
||||
if (item.hidden) {
|
||||
return false;
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
this.onlyOneChild = item;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true };
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
resolvePath(routePath) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath;
|
||||
}
|
||||
if (isExternal(this.basePath)) {
|
||||
return this.basePath;
|
||||
}
|
||||
return path.resolve(this.basePath, routePath);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
63
msh_single_admin/src/layout/components/Sidebar/index.vue
Executable file
63
msh_single_admin/src/layout/components/Sidebar/index.vue
Executable file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ 'has-logo': showLogo }"
|
||||
:style="{
|
||||
backgroundColor: settings.sideTheme === 'theme-light' ? variables.menuLightBackground : variables.menuBackground,
|
||||
}"
|
||||
>
|
||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:background-color="
|
||||
settings.sideTheme === 'theme-light' ? variables.menuLightBackground : variables.menuBackground
|
||||
"
|
||||
:text-color="settings.sideTheme === 'theme-light' ? variables.menuLightColor : variables.menuColor"
|
||||
:unique-opened="true"
|
||||
:active-text-color="variables.menuActiveText"
|
||||
:collapse-transition="true"
|
||||
mode="vertical"
|
||||
>
|
||||
<sidebar-item v-for="route in sidebarRouters" :key="route.url" :item="route" :base-path="route.url" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import Logo from './Logo';
|
||||
import SidebarItem from './SidebarItem';
|
||||
import variables from '@/styles/variables.scss';
|
||||
export default {
|
||||
components: { SidebarItem, Logo },
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
...mapGetters(['permission_routes', 'sidebarRouters', 'sidebar']),
|
||||
activeMenu() {
|
||||
const route = this.$route;
|
||||
const { meta, path } = route;
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
showLogo() {
|
||||
return this.$store.state.settings.sidebarLogo;
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.el-submenu__icon-arrow {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
85
msh_single_admin/src/layout/components/TagsView/ScrollPane.vue
Executable file
85
msh_single_admin/src/layout/components/TagsView/ScrollPane.vue
Executable file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const tagAndTagSpacing = 4; // tagAndTagSpacing
|
||||
|
||||
export default {
|
||||
name: 'ScrollPane',
|
||||
data() {
|
||||
return {
|
||||
left: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
scrollWrapper() {
|
||||
return this.$refs.scrollContainer.$refs.wrap;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleScroll(e) {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40;
|
||||
const $scrollWrapper = this.scrollWrapper;
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4;
|
||||
},
|
||||
moveToTarget(currentTag) {
|
||||
const $container = this.$refs.scrollContainer.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = this.scrollWrapper;
|
||||
const tagList = this.$parent.$refs.tag;
|
||||
|
||||
let firstTag = null;
|
||||
let lastTag = null;
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0];
|
||||
lastTag = tagList[tagList.length - 1];
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex = tagList.findIndex((item) => item === currentTag);
|
||||
const prevTag = tagList[currentIndex - 1];
|
||||
const nextTag = tagList[currentIndex + 1];
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing;
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing;
|
||||
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
::v-deep {
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0px;
|
||||
}
|
||||
.el-scrollbar__wrap {
|
||||
/*height: 49px;*/
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
299
msh_single_admin/src/layout/components/TagsView/index.vue
Executable file
299
msh_single_admin/src/layout/components/TagsView/index.vue
Executable file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div id="tags-view-container" class="tags-view-container" v-if="!isPhone">
|
||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
ref="tag"
|
||||
:key="tag.path"
|
||||
:class="isActive(tag) ? 'active' : ''"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
tag="span"
|
||||
class="tags-view-item"
|
||||
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent.native="openMenu(tag, $event)"
|
||||
>
|
||||
{{ tag.title }}
|
||||
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
|
||||
<li @click="closeOthersTags">关闭其他</li>
|
||||
<li @click="closeAllTags(selectedTag)">关闭所有</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollPane from './ScrollPane';
|
||||
import path from 'path';
|
||||
|
||||
export default {
|
||||
components: { ScrollPane },
|
||||
data() {
|
||||
return {
|
||||
fullWidth: document.body.clientWidth,
|
||||
visible: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
selectedTag: {},
|
||||
affixTags: [],
|
||||
isPhone: this.$wechat.isPhone(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visitedViews() {
|
||||
return this.$store.state.tagsView.visitedViews;
|
||||
},
|
||||
routes() {
|
||||
return this.$store.state.permission.routes;
|
||||
},
|
||||
theme() {
|
||||
return this.$store.state.settings.theme;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.addTags();
|
||||
if (!this.isPhone) this.moveToCurrentTag();
|
||||
},
|
||||
visible(value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMenu);
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.initTags();
|
||||
this.addTags();
|
||||
},
|
||||
methods: {
|
||||
handleResize(event) {
|
||||
this.fullWidth = document.body.clientWidth;
|
||||
},
|
||||
isActive(route) {
|
||||
return route.path === this.$route.path;
|
||||
},
|
||||
isAffix(tag) {
|
||||
return tag.meta && tag.meta.affix;
|
||||
},
|
||||
filterAffixTags(routes, basePath = '/') {
|
||||
let tags = [];
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path);
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta },
|
||||
});
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags = this.filterAffixTags(route.children, route.path);
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags];
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
},
|
||||
initTags() {
|
||||
const affixTags = (this.affixTags = this.filterAffixTags(this.routes));
|
||||
for (const tag of affixTags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
this.$store.dispatch('tagsView/addVisitedView', tag);
|
||||
}
|
||||
}
|
||||
},
|
||||
addTags() {
|
||||
const { name } = this.$route;
|
||||
if (name) {
|
||||
this.$store.dispatch('tagsView/addView', this.$route);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
moveToCurrentTag() {
|
||||
const tags = this.$refs.tag;
|
||||
this.$nextTick(() => {
|
||||
for (const tag of tags) {
|
||||
if (tag.to.path === this.$route.path) {
|
||||
this.$refs.scrollPane.moveToTarget(tag);
|
||||
// when query is different then update
|
||||
if (tag.to.fullPath !== this.$route.fullPath) {
|
||||
this.$store.dispatch('tagsView/updateVisitedView', this.$route);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshSelectedTag(view) {
|
||||
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
|
||||
const { fullPath } = view;
|
||||
this.$nextTick(() => {
|
||||
this.$router.replace({
|
||||
path: '/redirect' + fullPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
closeSelectedTag(view) {
|
||||
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
|
||||
if (this.isActive(view)) {
|
||||
this.toLastView(visitedViews, view);
|
||||
}
|
||||
});
|
||||
},
|
||||
closeOthersTags() {
|
||||
this.$router.push(this.selectedTag);
|
||||
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
|
||||
this.moveToCurrentTag();
|
||||
});
|
||||
},
|
||||
closeAllTags(view) {
|
||||
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
|
||||
if (this.affixTags.some((tag) => tag.path === view.path)) {
|
||||
return;
|
||||
}
|
||||
this.toLastView(visitedViews, view);
|
||||
});
|
||||
},
|
||||
toLastView(visitedViews, view) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView) {
|
||||
this.$router.push(latestView.fullPath);
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === 'Dashboard') {
|
||||
// to reload home page
|
||||
this.$router.replace({ path: '/redirect' + view.fullPath });
|
||||
} else {
|
||||
this.$router.push('/');
|
||||
}
|
||||
}
|
||||
},
|
||||
openMenu(tag, e) {
|
||||
const menuMinWidth = 105;
|
||||
const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left
|
||||
const offsetWidth = this.$el.offsetWidth; // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
||||
const left = e.clientX - offsetLeft + 15; // 15: margin right
|
||||
|
||||
if (left > maxLeft) {
|
||||
this.left = maxLeft;
|
||||
} else {
|
||||
this.left = left;
|
||||
}
|
||||
|
||||
this.top = e.clientY;
|
||||
this.visible = true;
|
||||
this.selectedTag = tag;
|
||||
},
|
||||
closeMenu() {
|
||||
this.visible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
padding: 10px 0;
|
||||
width: 100%;
|
||||
// background: #f5f7f9;
|
||||
background: #f5f5f5;
|
||||
// border-bottom: 1px solid #d8dce5;
|
||||
// box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border: 1px solid #fff;
|
||||
color: #495060;
|
||||
background: #fff;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
border-radius: 3px;
|
||||
&:first-of-type {
|
||||
margin-left: 20px;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 20px;
|
||||
}
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
color: #1890ff;
|
||||
border-color: #fff;
|
||||
// &::before {
|
||||
// content: '';
|
||||
// background: #fff;
|
||||
// display: inline-block;
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// border-radius: 50%;
|
||||
// position: relative;
|
||||
// margin-right: 2px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//reset element css of el-icon-close
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
.el-icon-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: 2px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
transform-origin: 100% 50%;
|
||||
&:before {
|
||||
transform: scale(0.6);
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #b4bccc;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
msh_single_admin/src/layout/components/copyright/index.vue
Executable file
82
msh_single_admin/src/layout/components/copyright/index.vue
Executable file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="footers acea-row">
|
||||
<!-- <div class="title mb15" v-text="copyrightNew">
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-link v-for="item in links" :key="item.key" :href="item.href" target="_blank" class="mr15 mb20">{{
|
||||
item.title
|
||||
}}</el-link> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2025 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
import { copyrightInfoApi } from '@/api/authInformation';
|
||||
import { checkPermi } from '@/utils/permission'; // 权限判断函数
|
||||
export default {
|
||||
name: 'i-copyright',
|
||||
data() {
|
||||
return {
|
||||
links: [
|
||||
{
|
||||
title: '官网',
|
||||
key: '1',
|
||||
href: 'https://www.crmeb.com',
|
||||
blankTarget: true,
|
||||
},
|
||||
{
|
||||
title: '社区',
|
||||
key: '2',
|
||||
href: 'https://q.crmeb.net/?categoryId=122&sequence=0',
|
||||
blankTarget: true,
|
||||
},
|
||||
{
|
||||
title: '文档',
|
||||
key: '3',
|
||||
href: 'https://help.crmeb.net/crmeb_java/1748037',
|
||||
blankTarget: true,
|
||||
},
|
||||
],
|
||||
copyright: 'Copyright © 2023 西安众邦网络科技有限公司',
|
||||
copyrightNew: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (checkPermi(['platform:copyright:get:info'])) this.getVersion();
|
||||
},
|
||||
methods: {
|
||||
getVersion() {
|
||||
copyrightInfoApi().then((res) => {
|
||||
const data = res || {};
|
||||
this.copyrightNew = data.companyName ? data.companyName : this.copyright;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.footers {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #808695;
|
||||
justify-content: center;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: #808695;
|
||||
}
|
||||
}
|
||||
.ivu-global-footer {
|
||||
/* margin: 48px 0 24px 0; */
|
||||
/* padding: 0 16px; */
|
||||
margin: 15px 0px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
margin-left: 210px;
|
||||
}
|
||||
</style>
|
||||
5
msh_single_admin/src/layout/components/index.js
Executable file
5
msh_single_admin/src/layout/components/index.js
Executable file
@@ -0,0 +1,5 @@
|
||||
export { default as AppMain } from './AppMain';
|
||||
export { default as Navbar } from './Navbar';
|
||||
export { default as Settings } from './Settings';
|
||||
export { default as Sidebar } from './Sidebar/index.vue';
|
||||
export { default as TagsView } from './TagsView/index.vue';
|
||||
125
msh_single_admin/src/layout/index.vue
Executable file
125
msh_single_admin/src/layout/index.vue
Executable file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: needTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<navbar />
|
||||
<tags-view v-if="needTagsView" />
|
||||
</div>
|
||||
<app-main />
|
||||
<right-panel v-if="showSettings">
|
||||
<settings />
|
||||
</right-panel>
|
||||
<copy-right />
|
||||
</div>
|
||||
<!-- <div class="open-image" @click="clear" v-if="openImage"><img src="@/assets/imgs/pc1.jpg" alt=""></div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RightPanel from '@/components/RightPanel';
|
||||
import copyRight from './components/copyright';
|
||||
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components';
|
||||
import ResizeMixin from './mixin/ResizeHandler';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'Layout',
|
||||
data() {
|
||||
return {
|
||||
openImage: true,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
AppMain,
|
||||
Navbar,
|
||||
RightPanel,
|
||||
Settings,
|
||||
Sidebar,
|
||||
TagsView,
|
||||
copyRight,
|
||||
},
|
||||
mixins: [ResizeMixin],
|
||||
computed: {
|
||||
...mapState({
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
showSettings: (state) => state.settings.showSettings,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === 'mobile',
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.openImage = false;
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/styles/mixin.scss';
|
||||
@import '~@/styles/variables.scss';
|
||||
.open-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999999;
|
||||
}
|
||||
.app-wrapper {
|
||||
@include clearfix;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.mobile.openSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$base-sidebar-width});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
55
msh_single_admin/src/layout/mixin/ResizeHandler.js
Executable file
55
msh_single_admin/src/layout/mixin/ResizeHandler.js
Executable file
@@ -0,0 +1,55 @@
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2025 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
import store from '@/store';
|
||||
|
||||
const { body } = document;
|
||||
const WIDTH = 992; // refer to Bootstrap's responsive design
|
||||
|
||||
export default {
|
||||
watch: {
|
||||
$route() {
|
||||
if (this.device === 'mobile' && this.sidebar.opened) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: false });
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
window.addEventListener('resize', this.$_resizeHandler);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.$_resizeHandler);
|
||||
},
|
||||
mounted() {
|
||||
const isMobile = this.$_isMobile();
|
||||
if (isMobile) {
|
||||
store.dispatch('app/toggleDevice', 'mobile');
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// use $_ for mixins properties
|
||||
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
|
||||
$_isMobile() {
|
||||
const rect = body.getBoundingClientRect();
|
||||
return rect.width - 1 < WIDTH;
|
||||
},
|
||||
$_resizeHandler() {
|
||||
if (!document.hidden) {
|
||||
const isMobile = this.$_isMobile();
|
||||
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop');
|
||||
|
||||
if (isMobile) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user