Commit b61b627d authored by 周远喜's avatar 周远喜

iview界面整合。

parent 650f9a0e
<template>
<Breadcrumb class="i-layout-header-breadcrumb" v-if="!isLimit" ref="breadcrumb">
<BreadcrumbItem>
<i-menu-head-title :item="topItem" :hide-icon="!showBreadcrumbIcon" />
</BreadcrumbItem>
<BreadcrumbItem v-for="item in items" :key="item.path">
<i-menu-head-title :item="item" :hide-icon="!showBreadcrumbIcon" />
</BreadcrumbItem>
<BreadcrumbItem>
<i-menu-head-title :item="siderMenuObject[activePath]" :hide-icon="!showBreadcrumbIcon" />
</BreadcrumbItem>
</Breadcrumb>
</template>
<script>
import { mapState } from 'vuex';
import menuSider from '@/menu/sider';
import { flattenSiderMenu } from '@/libs/system';
import iMenuHeadTitle from '../menu-head/title';
import { on, off } from 'view-design/src/utils/dom';
import { findComponentUpward, getStyle } from 'view-design/src/utils/assist';
import { throttle } from 'lodash';
export default {
name: 'iHeaderBreadcrumb',
components: { iMenuHeadTitle },
computed: {
...mapState('admin/layout', [
'showBreadcrumbIcon',
'menuCollapse'
]),
...mapState('admin/menu', [
'openNames',
'activePath',
'header',
'headerName'
]),
siderMenuObject () {
let obj = {};
this.allSiderMenu.forEach(item => {
if ('path' in item) {
obj[item.path] = item;
}
});
return obj;
},
items () {
let items = [...this.openNames];
let newItems = [];
items.forEach(i => {
newItems.push(this.siderMenuObject[i]);
});
return newItems;
},
// 第一级,默认是 menu/header.js 中的第一项
topItem () {
return this.header.find(item => item.name === this.headerName);
}
},
data () {
return {
// 得到所有侧边菜单,并转为平级,查询图标及显示对应内容
allSiderMenu: flattenSiderMenu(menuSider, []),
handleResize: () => {},
isLimit: false,
maxWidth: 560,
breadcrumbWidth: 0
}
},
methods: {
handleCheckWidth () {
const $header = findComponentUpward(this, 'Header');
if ($header) {
const headerWidth = parseInt(getStyle($header.$el, 'width'));
this.$nextTick(() => {
this.isLimit = headerWidth - this.maxWidth <= this.breadcrumbWidth;
});
}
},
handleGetWidth () {
this.isLimit = false;
this.$nextTick(() => {
const $breadcrumb = this.$refs.breadcrumb;
if ($breadcrumb) {
this.breadcrumbWidth = parseInt(getStyle($breadcrumb.$el, 'width'));
}
});
}
},
watch: {
topItem: {
handler () {
this.handleGetWidth();
this.handleCheckWidth();
},
deep: true
},
items: {
handler () {
this.handleGetWidth();
this.handleCheckWidth();
},
deep: true
},
activePath: {
handler () {
this.handleGetWidth();
this.handleCheckWidth();
},
deep: true
}
},
mounted () {
this.handleResize = throttle(this.handleCheckWidth, 100, { leading: false });
on(window, 'resize', this.handleResize);
this.handleGetWidth();
this.handleCheckWidth();
},
beforeDestroy () {
off(window, 'resize', this.handleResize);
}
}
</script>
<template>
<span class="i-layout-header-trigger" :class="{ 'i-layout-header-trigger-min': showReload }" @click="handleToggleMenuSide">
<Icon custom="i-icon i-icon-menu-unfold" v-show="menuCollapse || isMobile" />
<Icon custom="i-icon i-icon-menu-fold" v-show="!menuCollapse && !isMobile" />
</span>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
name: 'iHeaderCollapse',
computed: {
...mapState('admin/layout', [
'isMobile',
'isTablet',
'isDesktop',
'menuCollapse',
'showReload'
])
},
methods: {
...mapMutations('admin/layout', [
'updateMenuCollapse'
]),
// 展开/收起侧边栏
handleToggleMenuSide (state) {
if (this.isMobile) {
this.updateMenuCollapse(false);
this.$emit('on-toggle-drawer', state);
} else {
this.updateMenuCollapse(!this.menuCollapse);
}
}
},
watch: {
// 切换页面时,在移动端自动收起侧边栏
// 强行传参 false 是因为有的路由不是在菜单栏发生的,toggle 会使其显示
'$route' () {
if (this.isMobile) this.handleToggleMenuSide(false);
},
// 在平板时自动收起菜单
isTablet (state) {
if (!this.isMobile && state) this.updateMenuCollapse(true);
},
// 在桌面时自动展开菜单
isDesktop (state) {
if (!this.isMobile && state) this.updateMenuCollapse(false);
}
}
}
</script>
<template>
<span class="i-layout-header-trigger i-layout-header-trigger-min" @click="toggleFullscreen">
<Icon custom="i-icon i-icon-full-screen" v-show="!isFullscreen" />
<Icon custom="i-icon i-icon-exit-full-screen" v-show="isFullscreen" />
</span>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
name: 'iHeaderFullscreen',
computed: {
...mapState('admin/layout', [
'isFullscreen'
])
},
methods: {
...mapActions('admin/layout', [
'toggleFullscreen'
])
}
}
</script>
<template>
<span class="i-layout-header-trigger i-layout-header-trigger-min">
<Dropdown :trigger="isMobile ? 'click' : 'hover'" class="i-layout-header-i18n" :class="{ 'i-layout-header-user-mobile': isMobile }" @on-click="handleClick">
<Icon type="md-globe" />
<DropdownMenu slot="list">
<DropdownItem v-for="(item, key) in languages" :key="key" :name="key" :selected="locale === key">
<span>{{ item.language }}</span>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</span>
</template>
<script>
import Languages from '@/i18n/locale';
import { mapState, mapActions } from 'vuex';
export default {
name: 'iHeaderI18n',
data () {
return {
languages: Languages
}
},
computed: {
...mapState('admin/i18n', [
'locale'
]),
...mapState('admin/layout', [
'isMobile'
])
},
methods: {
...mapActions('admin/i18n', [
'setLocale'
]),
handleClick (locale) {
if (locale === this.locale) return;
this.setLocale({ locale, vm: this });
}
}
}
</script>
<template>
<Tooltip :content="tooltipContent" transfer>
<span class="i-layout-header-trigger i-layout-header-trigger-min" @click="handleOpenLog">
<Badge :count="lengthError === 0 ? null : lengthError" :overflow-count="99" :dot="showDot" :offset="showDot ? [26, 2] : [20, 0]">
<Icon custom="i-icon i-icon-record" />
</Badge>
</span>
</Tooltip>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'iHeaderLog',
computed: {
...mapGetters('admin/log', [
'length',
'lengthError'
]),
showDot () {
return !!this.length && this.lengthError === 0;
},
tooltipContent () {
if (!this.length) {
return '没有日志或异常';
} else {
let text = `${this.length} 条日志`;
if (this.lengthError) text += ` | 包含 ${this.lengthError} 个异常`;
return text;
}
}
},
methods: {
handleOpenLog () {
this.$router.push({
name: 'log'
});
}
}
}
</script>
<template>
<i-link class="i-layout-header-logo" :class="{ 'i-layout-header-logo-stick': !isMobile }" to="/">
<img src="@/assets/images/logo-small.png" v-if="isMobile">
<img src="@/assets/images/logo.png" v-else-if="headerTheme === 'light'">
<img src="@/assets/images/logo-dark.png" v-else>
</i-link>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'iHeaderLogo',
computed: {
...mapState('admin/layout', [
'isMobile',
'headerTheme'
])
}
}
</script>
<template>
<span class="i-layout-header-trigger i-layout-header-trigger-min i-layout-header-trigger-in">
<Notification
:wide="isMobile"
:badge-props="badgeProps"
class="i-layout-header-notice"
:class="{ 'i-layout-header-notice-mobile': isMobile }">
<Icon slot="icon" custom="i-icon i-icon-notification" />
<NotificationTab title="通知">
</NotificationTab>
<NotificationTab title="消息">
</NotificationTab>
<NotificationTab title="待办">
</NotificationTab>
</Notification>
</span>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'iHeaderNotice',
data () {
return {
badgeProps: {
offset: [20, 0]
}
}
},
computed: {
...mapState('admin/layout', [
'isMobile'
])
}
}
</script>
<template>
<span class="i-layout-header-trigger" :class="{ 'i-layout-header-trigger-min': showSiderCollapse }" @click="handleReload">
<Icon custom="i-icon i-icon-refresh" />
</span>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'iHeaderReload',
computed: {
...mapState('admin/layout', [
'isMobile',
'showSiderCollapse'
])
},
methods: {
handleReload () {
this.$emit('on-reload');
}
}
}
</script>
<template>
<span v-if="isDesktop" class="i-layout-header-trigger i-layout-header-trigger-min i-layout-header-trigger-in i-layout-header-trigger-nohover">
<input class="i-layout-header-search" type="text" :placeholder="$t('basicLayout.search.placeholder')">
</span>
<span v-else class="i-layout-header-trigger i-layout-header-trigger-min">
<Dropdown trigger="click" class="i-layout-header-search-drop" ref="dropdown">
<Icon type="ios-search" />
<DropdownMenu slot="list">
<div class="i-layout-header-search-drop-main">
<Input size="large" prefix="ios-search" type="text" :placeholder="$t('basicLayout.search.placeholder')" />
<span class="i-layout-header-search-drop-main-cancel" @click="handleCloseSearch">{{ $t('basicLayout.search.cancel') }}</span>
</div>
</DropdownMenu>
</Dropdown>
</span>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'iHeaderSearch',
computed: {
...mapState('admin/layout', [
'isDesktop',
'headerMenu'
])
},
methods: {
handleCloseSearch () {
this.$refs.dropdown.handleClick();
}
}
}
</script>
<template>
<span class="i-layout-header-trigger i-layout-header-trigger-min" @click="showSetting">
<Icon type="md-more" />
<Drawer v-model="visible" width="280">
<Divider size="small">主题风格设置</Divider>
<div class="i-layout-header-setting-item">
<div class="i-layout-header-setting-item-radio" :class="{ 'on': siderTheme === 'dark' }" @click="handleChangeSetting('siderTheme', 'dark')">
<Tooltip content="暗色侧边栏" placement="top" transfer>
<img src="@/assets/svg/nav-theme-dark.svg">
</Tooltip>
</div>
<div class="i-layout-header-setting-item-radio" :class="{ 'on': siderTheme === 'light' }" @click="handleChangeSetting('siderTheme', 'light')">
<Tooltip content="亮色侧边栏" placement="top" transfer>
<img src="@/assets/svg/nav-theme-light.svg">
</Tooltip>
</div>
</div>
<div class="i-layout-header-setting-item">
<div class="i-layout-header-setting-item-radio" :class="{ 'on': headerTheme === 'light' }" @click="handleChangeSetting('headerTheme', 'light')">
<Tooltip content="亮色顶栏" placement="top" transfer>
<img src="@/assets/svg/nav-theme-dark.svg">
</Tooltip>
</div>
<div class="i-layout-header-setting-item-radio" :class="{ 'on': headerTheme === 'dark' }" @click="handleChangeSetting('headerTheme', 'dark')">
<Tooltip content="暗色顶栏" placement="top" transfer>
<img src="@/assets/svg/header-theme-dark.svg">
</Tooltip>
</div>
<div class="i-layout-header-setting-item-radio" :class="{ 'on': headerTheme === 'primary' }" @click="handleChangeSetting('headerTheme', 'primary')">
<Tooltip content="主色顶栏" placement="top" transfer>
<img src="@/assets/svg/header-theme-primary.svg">
</Tooltip>
</div>
</div>
<Divider size="small">导航设置</Divider>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">固定侧边栏</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="siderFix" @on-change="(val) => handleChangeSetting('siderFix', val)" />
</span>
</div>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">固定顶栏</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="headerFix" @on-change="(val) => handleChangeSetting('headerFix', val)" />
</span>
</div>
<div class="i-layout-header-setting-item" :class="{ 'i-layout-header-setting-item-disabled': !headerFix }">
<span class="i-layout-header-setting-item-desc">
下滑时隐藏顶栏
<Tooltip placement="top" content="需开启固定顶栏" transfer>
<Icon type="ios-help-circle-outline" />
</Tooltip>
</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="headerHide" :disabled="!headerFix" @on-change="(val) => handleChangeSetting('headerHide', val)" />
</span>
</div>
<div class="i-layout-header-setting-item" :class="{ 'i-layout-header-setting-item-disabled': !headerFix }">
<span class="i-layout-header-setting-item-desc">
置顶顶栏
<Tooltip placement="top" content="需开启固定顶栏" transfer>
<Icon type="ios-help-circle-outline" />
</Tooltip>
</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="headerStick" :disabled="!headerFix" @on-change="(val) => handleChangeSetting('headerStick', val)" />
</span>
</div>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">侧边栏开启手风琴模式</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="menuAccordion" @on-change="(val) => handleChangeSetting('menuAccordion', val)" />
</span>
</div>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">显示折叠侧边栏按钮</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="showSiderCollapse" @on-change="(val) => handleChangeSetting('showSiderCollapse', val)" />
</span>
</div>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">侧边栏折叠时显示父级菜单名</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="showCollapseMenuTitle" @on-change="(val) => handleChangeSetting('showCollapseMenuTitle', val)" />
</span>
</div>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">
显示全局面包屑导航
<Tooltip placement="top" content="headerMenu 开启时无效" transfer>
<Icon type="ios-help-circle-outline" />
</Tooltip>
</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="showBreadcrumb" @on-change="(val) => handleChangeSetting('showBreadcrumb', val)" />
</span>
</div>
<div class="i-layout-header-setting-item" :class="{ 'i-layout-header-setting-item-disabled': !showBreadcrumb }">
<span class="i-layout-header-setting-item-desc">
全局面包屑显示图标
<Tooltip placement="top" content="需开启全局面包屑导航" transfer>
<Icon type="ios-help-circle-outline" />
</Tooltip>
</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="showBreadcrumbIcon" :disabled="!showBreadcrumb" @on-change="(val) => handleChangeSetting('showBreadcrumbIcon', val)" />
</span>
</div>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">显示重载页面按钮</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="showReload" @on-change="(val) => handleChangeSetting('showReload', val)" />
</span>
</div>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">显示多语言选择</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="showI18n" @on-change="(val) => handleChangeSetting('showI18n', val)" />
</span>
</div>
<Divider size="small">其它设置</Divider>
<div class="i-layout-header-setting-item">
<span class="i-layout-header-setting-item-desc">开启多页签</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="tabs" @on-change="(val) => handleChangeSetting('tabs', val)" />
</span>
</div>
<div class="i-layout-header-setting-item" :class="{ 'i-layout-header-setting-item-disabled': !tabs }">
<span class="i-layout-header-setting-item-desc">
多页签显示图标
<Tooltip placement="top" content="需开启多页签" transfer>
<Icon type="ios-help-circle-outline" />
</Tooltip>
</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="showTabsIcon" :disabled="!tabs" @on-change="(val) => handleChangeSetting('showTabsIcon', val)" />
</span>
</div>
<div class="i-layout-header-setting-item" :class="{ 'i-layout-header-setting-item-disabled': !tabs }">
<span class="i-layout-header-setting-item-desc">
固定多页签
<Tooltip placement="top" content="需开启多页签" transfer>
<Icon type="ios-help-circle-outline" />
</Tooltip>
</span>
<span class="i-layout-header-setting-item-action">
<i-switch size="small" :value="tabsFix" :disabled="!tabs" @on-change="(val) => handleChangeSetting('tabsFix', val)" />
</span>
</div>
<Alert type="warning">
<div slot="desc">
该功能主要实时预览各种布局效果,更多完整配置在 <strong>setting.js</strong> 中设置。建议在生产环境关闭该布局预览功能。
</div>
</Alert>
</Drawer>
</span>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
name: 'iHeaderSetting',
data () {
return {
visible: false
}
},
computed: {
...mapState('admin/layout', [
'siderTheme',
'headerTheme',
'headerStick',
'siderFix',
'headerFix',
'headerHide',
'menuAccordion',
'showSiderCollapse',
'tabs',
'showTabsIcon',
'tabsFix',
'showBreadcrumb',
'showBreadcrumbIcon',
'showReload',
'showI18n',
'showCollapseMenuTitle'
])
},
methods: {
...mapMutations('admin/layout', [
'updateLayoutSetting'
]),
showSetting () {
this.visible = true;
},
handleChangeSetting (key, value) {
this.updateLayoutSetting({
key,
value
});
}
}
}
</script>
<template>
<span class="i-layout-header-trigger i-layout-header-trigger-min">
<Dropdown :trigger="isMobile ? 'click' : 'hover'" class="i-layout-header-user" :class="{ 'i-layout-header-user-mobile': isMobile }" @on-click="handleClick">
<Avatar size="small" :src="info.avatar" v-if="info.avatar" />
<span class="i-layout-header-user-name" v-if="!isMobile">{{ info.name }}</span>
<DropdownMenu slot="list">
<i-link to="/setting/user">
<DropdownItem>
<Icon type="ios-contact-outline" />
<span>{{ $t('basicLayout.user.center') }}</span>
</DropdownItem>
</i-link>
<i-link to="/setting/account">
<DropdownItem>
<Icon type="ios-settings-outline" />
<span>{{ $t('basicLayout.user.setting') }}</span>
</DropdownItem>
</i-link>
<DropdownItem divided name="logout">
<Icon type="ios-log-out" />
<span>{{ $t('basicLayout.user.logOut') }}</span>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</span>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
name: 'iHeaderUser',
computed: {
...mapState('admin/user', [
'info'
]),
...mapState('admin/layout', [
'isMobile',
'logoutConfirm'
])
},
methods: {
...mapActions('admin/account', [
'logout'
]),
handleClick (name) {
if (name === 'logout') {
this.logout({
confirm: this.logoutConfirm,
vm: this
});
}
}
}
}
</script>
// 默认布局使用的多语言
export default {
'zh-CN': {
basicLayout: {
search: {
placeholder: '搜索...',
cancel: '取消'
},
user: {
center: '个人中心',
setting: '设置',
logOut: '退出登录'
},
logout: {
confirmTitle: '退出登录确认',
confirmContent: '您确定退出登录当前账户吗?打开的标签页和个人设置将会保存。'
},
tabs: {
left: '关闭左侧',
right: '关闭右侧',
other: '关闭其它',
all: '全部关闭'
}
}
},
'en-US': {
basicLayout: {
search: {
placeholder: 'Search...',
cancel: 'Cancel'
},
user: {
center: 'My home',
setting: 'Setting',
logOut: 'Log out'
},
logout: {
confirmTitle: 'Logout confirmation',
confirmContent: 'Are you sure you are logged out of your current account? Open tabs and personal settings will be saved.'
},
tabs: {
left: 'Close left',
right: 'Close right',
other: 'Close other',
all: 'Close all'
}
}
}
}
<template>
<Layout class="i-layout">
<Sider v-if="!isMobile && !hideSider" class="i-layout-sider" :class="siderClasses" :width="menuSideWidth">
<i-menu-side :hide-logo="isHeaderStick && headerFix && showHeader" />
</Sider>
<Layout class="i-layout-inside" :class="insideClasses">
<transition name="fade-quick">
<Header class="i-layout-header" :class="headerClasses" :style="headerStyle" v-show="showHeader" v-resize="handleHeaderWidthChange">
<i-header-logo v-if="isMobile && showMobileLogo" />
<i-header-logo v-if="!isMobile && isHeaderStick && headerFix" />
<i-header-collapse v-if="(isMobile || showSiderCollapse) && !hideSider" @on-toggle-drawer="handleToggleDrawer" />
<i-header-reload v-if="!isMobile && showReload" @on-reload="handleReload" />
<i-menu-head v-if="headerMenu && !isMobile" ref="menuHead" />
<i-header-breadcrumb v-if="showBreadcrumb && !headerMenu && !isMobile" ref="breadcrumb" />
<i-header-search v-if="showSearch && !headerMenu && !isMobile && !showBreadcrumb" />
<div class="i-layout-header-right">
<i-header-search v-if="(showSearch && isMobile) || (showSearch && (headerMenu || showBreadcrumb))" />
<i-menu-head v-if="headerMenu && isMobile" />
<i-header-log v-if="isDesktop && showLog" />
<i-header-fullscreen v-if="isDesktop && showFullscreen" />
<i-header-notice v-if="showNotice" />
<i-header-user />
<i-header-i18n v-if="showI18n" />
<i-header-setting v-if="enableSetting && !isMobile" />
</div>
</Header>
</transition>
<Content class="i-layout-content" :class="contentClasses">
<transition name="fade-quick">
<i-tabs v-if="tabs" v-show="showHeader" @on-reload="handleReload" />
</transition>
<div class="i-layout-content-main">
<keep-alive :include="keepAlive">
<router-view v-if="loadRouter" />
</keep-alive>
</div>
</Content>
<i-copyright />
</Layout>
<div v-if="isMobile && !hideSider">
<Drawer v-model="showDrawer" placement="left" :closable="false" :class-name="drawerClasses">
<i-menu-side />
</Drawer>
</div>
</Layout>
</template>
<script>
import iMenuHead from './menu-head';
import iMenuSide from './menu-side';
import iHeaderLogo from './header-logo';
import iHeaderCollapse from './header-collapse';
import iHeaderReload from './header-reload';
import iHeaderBreadcrumb from './header-breadcrumb';
import iHeaderSearch from './header-search';
import iHeaderLog from './header-log';
import iHeaderFullscreen from './header-fullscreen';
import iHeaderNotice from './header-notice';
import iHeaderUser from './header-user';
import iHeaderI18n from './header-i18n';
import iHeaderSetting from './header-setting';
import iTabs from './tabs';
import iCopyright from '@/components/copyright';
import { mapState, mapGetters, mapMutations } from 'vuex';
import Setting from '@/setting';
import { requestAnimation } from '@/libs/util';
export default {
name: 'BasicLayout',
components: { iMenuHead, iMenuSide, iCopyright, iHeaderLogo, iHeaderCollapse, iHeaderReload, iHeaderBreadcrumb, iHeaderSearch, iHeaderUser, iHeaderI18n, iHeaderLog, iHeaderFullscreen, iHeaderSetting, iHeaderNotice, iTabs },
data () {
return {
showDrawer: false,
ticking: false,
headerVisible: true,
oldScrollTop: 0,
isDelayHideSider: false, // hack,当从隐藏侧边栏的 header 切换到正常 header 时,防止 Logo 抖动
loadRouter: true
}
},
computed: {
...mapState('admin/layout', [
'siderTheme',
'headerTheme',
'headerStick',
'tabs',
'tabsFix',
'siderFix',
'headerFix',
'headerHide',
'headerMenu',
'isMobile',
'isTablet',
'isDesktop',
'menuCollapse',
'showMobileLogo',
'showSearch',
'showNotice',
'showFullscreen',
'showSiderCollapse',
'showBreadcrumb',
'showLog',
'showI18n',
'showReload',
'enableSetting'
]),
...mapState('admin/page', [
'keepAlive'
]),
...mapGetters('admin/menu', [
'hideSider'
]),
// 如果开启 headerMenu,且当前 header 的 hideSider 为 true,则将顶部按 headerStick 处理
// 这时,即使没有开启 headerStick,仍然按开启处理
isHeaderStick () {
let state = this.headerStick;
if (this.hideSider) state = true;
return state;
},
showHeader () {
let visible = true;
if (this.headerFix && this.headerHide && !this.headerVisible) visible = false;
return visible;
},
headerClasses () {
return [
`i-layout-header-color-${this.headerTheme}`,
{
'i-layout-header-fix': this.headerFix,
'i-layout-header-fix-collapse': this.headerFix && this.menuCollapse,
'i-layout-header-mobile': this.isMobile,
'i-layout-header-stick': this.isHeaderStick && !this.isMobile,
'i-layout-header-with-menu': this.headerMenu,
'i-layout-header-with-hide-sider': this.hideSider || this.isDelayHideSider
}
];
},
headerStyle () {
const menuWidth = this.isHeaderStick ? 0 : this.menuCollapse ? 80 : Setting.menuSideWidth;
return this.isMobile || !this.headerFix ? {} : {
width: `calc(100% - ${menuWidth}px)`
};
},
siderClasses () {
return {
'i-layout-sider-fix': this.siderFix,
'i-layout-sider-dark': this.siderTheme === 'dark'
};
},
contentClasses () {
return {
'i-layout-content-fix-with-header': this.headerFix,
'i-layout-content-with-tabs': this.tabs,
'i-layout-content-with-tabs-fix': this.tabs && this.tabsFix
};
},
insideClasses () {
return {
'i-layout-inside-fix-with-sider': this.siderFix,
'i-layout-inside-fix-with-sider-collapse': this.siderFix && this.menuCollapse,
'i-layout-inside-with-hide-sider': this.hideSider,
'i-layout-inside-mobile': this.isMobile
};
},
drawerClasses () {
let className = 'i-layout-drawer';
if (this.siderTheme === 'dark') className += ' i-layout-drawer-dark';
return className;
},
menuSideWidth () {
return this.menuCollapse ? 80 : Setting.menuSideWidth;
}
},
watch: {
hideSider () {
this.isDelayHideSider = true;
setTimeout(() => {
this.isDelayHideSider = false;
}, 0);
},
'$route' (to, from) {
if (to.name === from.name) {
// 相同路由,不同参数,跳转时,重载页面
if (Setting.sameRouteForceUpdate) {
this.handleReload();
}
}
}
},
methods: {
...mapMutations('admin/layout', [
'updateMenuCollapse'
]),
...mapMutations('admin/page', [
'keepAlivePush',
'keepAliveRemove'
]),
handleToggleDrawer (state) {
if (typeof state === 'boolean') {
this.showDrawer = state;
} else {
this.showDrawer = !this.showDrawer;
}
},
handleScroll () {
if (!this.headerHide) return;
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop;
if (!this.ticking) {
this.ticking = true;
requestAnimation(() => {
if (this.oldScrollTop > scrollTop) {
this.headerVisible = true;
} else if (scrollTop > 300 && this.headerVisible) {
this.headerVisible = false;
} else if (scrollTop < 300 && !this.headerVisible) {
this.headerVisible = true;
}
this.oldScrollTop = scrollTop;
this.ticking = false;
});
}
},
handleHeaderWidthChange () {
const $breadcrumb = this.$refs.breadcrumb;
if ($breadcrumb) {
$breadcrumb.handleGetWidth();
$breadcrumb.handleCheckWidth();
}
const $menuHead = this.$refs.menuHead;
if ($menuHead) {
// todo $menuHead.handleGetMenuHeight();
}
},
handleReload () {
// 针对缓存的页面也生效
const isCurrentPageCache = this.keepAlive.indexOf(this.$route.name) > -1;
const pageName = this.$route.name;
if (isCurrentPageCache) {
this.keepAliveRemove(pageName);
}
this.loadRouter = false;
this.$nextTick(() => {
this.loadRouter = true;
if (isCurrentPageCache) {
this.keepAlivePush(pageName);
}
});
}
},
mounted () {
document.addEventListener('scroll', this.handleScroll, { passive: true });
},
beforeDestroy () {
document.removeEventListener('scroll', this.handleScroll);
},
created () {
if (this.isTablet && this.showSiderCollapse) this.updateMenuCollapse(true);
}
}
</script>
<template>
<div class="i-layout-menu-head" :class="{ 'i-layout-menu-head-mobile': isMobile }">
<Menu mode="horizontal" :active-name="headerName" v-if="!isMobile && !isMenuLimit" ref="menu">
<MenuItem v-for="item in filterHeader" :to="item.path" :replace="item.replace" :target="item.target" :name="item.name" :key="item.path" @click.native="handleClick(item.path, 'header')">
<i-menu-head-title :item="item" />
</MenuItem>
</Menu>
<div class="i-layout-header-trigger i-layout-header-trigger-min i-layout-header-trigger-in i-layout-header-trigger-no-height" v-else>
<Dropdown trigger="click" :class="{ 'i-layout-menu-head-mobile-drop': isMobile }">
<Icon type="ios-apps" />
<DropdownMenu slot="list">
<i-link v-for="item in filterHeader" :to="item.path" :replace="item.replace" :target="item.target" :key="item.path" @click.native="handleClick(item.path, 'header')">
<DropdownItem>
<i-menu-head-title :item="item" />
</DropdownItem>
</i-link>
</DropdownMenu>
</Dropdown>
</div>
</div>
</template>
<script>
import iMenuHeadTitle from './title';
import { mapState, mapGetters } from 'vuex';
import { getStyle } from 'view-design/src/utils/assist';
import clickItem from '../mixins/click-item';
// import { on, off } from 'view-design/src/utils/dom';
// import { throttle } from 'lodash';
export default {
name: 'iMenuHead',
components: { iMenuHeadTitle },
mixins: [ clickItem ],
computed: {
...mapState('admin/layout', [
'isMobile'
]),
...mapState('admin/menu', [
'headerName'
]),
...mapGetters('admin/menu', [
'filterHeader'
])
},
data () {
return {
handleResize: () => {},
isMenuLimit: false,
menuMaxWidth: 0 // 达到这个值后,menu 就显示不下了
}
},
methods: {
handleGetMenuHeight () {
const menuWidth = parseInt(getStyle(this.$el, 'width'));
const $menu = this.$refs.menu;
if ($menu) {
const menuHeight = parseInt(getStyle(this.$refs.menu.$el, 'height'));
if (menuHeight > 64) {
if (!this.isMenuLimit) {
this.menuMaxWidth = menuWidth;
}
this.isMenuLimit = true;
}
} else if (menuWidth >= this.menuMaxWidth) {
this.isMenuLimit = false;
}
}
},
watch: {
filterHeader () {
this.handleGetMenuHeight();
},
isMobile () {
this.handleGetMenuHeight();
}
},
mounted () {
// this.handleResize = throttle(this.handleGetMenuHeight, 100, { leading: false });
// on(window, 'resize', this.handleResize);
this.handleGetMenuHeight();
},
beforeDestroy () {
// off(window, 'resize', this.handleResize);
}
}
</script>
<template>
<div class="i-layout-menu-head-title">
<span class="i-layout-menu-head-title-icon" v-if="(item.icon || item.custom || item.img) && !hideIcon">
<Icon :type="item.icon" v-if="item.icon" />
<Icon :custom="item.custom" v-else-if="item.custom" />
<img :src="item.img" v-else-if="item.img" />
</span>
<span class="i-layout-menu-head-title-text">{{ tTitle(item.title) }}</span>
</div>
</template>
<script>
/**
* 该组件除了 Menu,也被 Breadcrumb 使用过
* */
import tTitle from '../mixins/translate-title';
export default {
name: 'iMenuHeadTitle',
mixins: [ tTitle ],
props: {
item: {
type: Object,
default () {
return {}
}
},
hideIcon: {
type: Boolean,
default: false
}
}
}
</script>
<template>
<div>
<div class="i-layout-sider-logo" :class="{ 'i-layout-sider-logo-dark': siderTheme === 'dark' }">
<transition name="fade-quick">
<i-link to="/" v-show="!hideLogo">
<img src="@/assets/images/logo-small.png" v-if="menuCollapse">
<img src="@/assets/images/logo.png" v-else-if="siderTheme === 'light'">
<img src="@/assets/images/logo-dark.png" v-else>
</i-link>
</transition>
</div>
<Menu
ref="menu"
class="i-layout-menu-side i-scrollbar-hide"
:theme="siderTheme"
:accordion="menuAccordion"
:active-name="activePath"
:open-names="openNames"
width="auto">
<template v-if="!menuCollapse" v-for="(item, index) in filterSider">
<i-menu-side-item v-if="item.children === undefined || !item.children.length" :menu="item" :key="index" />
<i-menu-side-submenu v-else :menu="item" :key="index" />
</template>
<template v-else>
<Tooltip :content="tTitle(item.title)" placement="right" v-if="item.children === undefined || !item.children.length" :key="index">
<i-menu-side-item :menu="item" hide-title />
</Tooltip>
<i-menu-side-collapse v-else :menu="item" :key="index" top-level />
</template>
</Menu>
</div>
</template>
<script>
import iMenuSideItem from './menu-item';
import iMenuSideSubmenu from './submenu';
import iMenuSideCollapse from './menu-collapse';
import tTitle from '../mixins/translate-title';
import { mapState, mapGetters } from 'vuex';
export default {
name: 'iMenuSide',
mixins: [ tTitle ],
components: { iMenuSideItem, iMenuSideSubmenu, iMenuSideCollapse },
props: {
hideLogo: {
type: Boolean,
default: false
}
},
computed: {
...mapState('admin/layout', [
'siderTheme',
'menuAccordion',
'menuCollapse'
]),
...mapState('admin/menu', [
'activePath',
'openNames'
]),
...mapGetters('admin/menu', [
'filterSider'
])
},
watch: {
'$route': {
handler () {
this.handleUpdateMenuState();
},
immediate: true
},
// 在展开/收起侧边菜单栏时,更新一次 menu 的状态
menuCollapse () {
this.handleUpdateMenuState();
}
},
methods: {
handleUpdateMenuState () {
this.$nextTick(() => {
if (this.$refs.menu) {
this.$refs.menu.updateActiveName();
if (this.menuAccordion) this.$refs.menu.updateOpened();
}
});
}
}
}
</script>
<template>
<Dropdown placement="right-start" :class="dropdownClasses">
<li :class="menuItemClasses" v-if="topLevel">
<i-menu-side-title :menu="menu" hide-title />
</li>
<DropdownItem v-else>
<i-menu-side-title :menu="menu" :selected="openNames.indexOf(menu.path) >= 0" />
<Icon type="ios-arrow-forward" class="i-layout-menu-side-arrow" />
</DropdownItem>
<DropdownMenu slot="list">
<div class="i-layout-menu-side-collapse-title" v-if="showCollapseMenuTitle">
<i-menu-side-title :menu="menu" />
</div>
<template v-for="(item, index) in menu.children">
<i-link :to="item.path" :target="item.target" v-if="item.children === undefined || !item.children.length" :key="index" @click.native="handleClick(item.path)">
<DropdownItem :divided="item.divided" :class="{ 'i-layout-menu-side-collapse-item-selected': item.path === activePath }">
<i-menu-side-title :menu="item" />
</DropdownItem>
</i-link>
<i-menu-side-collapse v-else :menu="item" :key="index" />
</template>
</DropdownMenu>
</Dropdown>
</template>
<script>
import iMenuSideTitle from './menu-title';
import clickItem from '../mixins/click-item';
import { mapState } from 'vuex';
export default {
name: 'iMenuSideCollapse',
components: { iMenuSideTitle },
mixins: [ clickItem ],
props: {
menu: {
type: Object,
default () {
return {}
}
},
// 是否是第一级,区分在于左侧和展开侧
topLevel: {
type: Boolean,
default: false
}
},
computed: {
...mapState('admin/layout', [
'siderTheme',
'showCollapseMenuTitle'
]),
...mapState('admin/menu', [
'activePath',
'openNames'
]),
dropdownClasses () {
return {
'i-layout-menu-side-collapse-top': this.topLevel,
'i-layout-menu-side-collapse-dark': this.siderTheme === 'dark'
}
},
menuItemClasses () {
return [
'ivu-menu-item i-layout-menu-side-collapse-top-item',
{
'ivu-menu-item-selected ivu-menu-item-active': this.openNames.indexOf(this.menu.path) >= 0 // -active 在高亮时,有背景
}
]
}
}
}
</script>
<template>
<div>
<MenuItem :to="menu.path" :replace="menu.replace" :target="menu.target" :name="menu.path" @click.native="handleClick(menu.path)">
<i-menu-side-title :menu="menu" :hide-title="hideTitle" />
</MenuItem>
</div>
</template>
<script>
import iMenuSideTitle from './menu-title';
import clickItem from '../mixins/click-item';
export default {
name: 'iMenuSideItem',
components: { iMenuSideTitle },
mixins: [ clickItem ],
props: {
menu: {
type: Object,
default () {
return {}
}
},
hideTitle: {
type: Boolean,
default: false
}
}
}
</script>
<template>
<span class="i-layout-menu-side-title">
<span class="i-layout-menu-side-title-icon" :class="{ 'i-layout-menu-side-title-icon-single': hideTitle }" v-if="menu.icon || menu.custom || menu.img">
<Icon :type="menu.icon" v-if="menu.icon" />
<Icon :custom="menu.custom" v-else-if="menu.custom" />
<img :src="menu.img" v-else-if="menu.img" />
</span>
<span class="i-layout-menu-side-title-text" :class="{ 'i-layout-menu-side-title-text-selected': selected }" v-if="!hideTitle">{{ tTitle(menu.title) }}</span>
</span>
</template>
<script>
import tTitle from '../mixins/translate-title';
export default {
name: 'iMenuSideTitle',
mixins: [ tTitle ],
props: {
menu: {
type: Object,
default () {
return {}
}
},
hideTitle: {
type: Boolean,
default: false
},
// 用于侧边栏收起 Dropdown 当前高亮
selected: {
type: Boolean,
default: false
}
}
}
</script>
<template>
<Submenu :name="menu.path">
<template slot="title">
<i-menu-side-title :menu="menu" />
</template>
<template v-for="(item, index) in menu.children">
<i-menu-side-item v-if="item.children === undefined || !item.children.length" :menu="item" :key="index" />
<i-menu-side-submenu v-else :menu="item" :key="index" />
</template>
</Submenu>
</template>
<script>
import iMenuSideItem from './menu-item';
import iMenuSideTitle from './menu-title';
export default {
name: 'iMenuSideSubmenu',
components: { iMenuSideItem, iMenuSideTitle },
props: {
menu: {
type: Object,
default () {
return {}
}
}
}
}
</script>
import { findComponentUpward } from 'view-design/src/utils/assist';
import { mapState } from 'vuex';
export default {
computed: {
...mapState('admin/layout', [
'menuSiderReload',
'menuHeaderReload'
])
},
methods: {
handleClick (path, type = 'sider') {
const current = this.$route.path;
if (current === path) {
if (type === 'sider' && this.menuSiderReload) this.handleReload();
else if (type === 'header' && this.menuHeaderReload) this.handleReload();
}
},
handleReload () {
const $layout = findComponentUpward(this, 'BasicLayout');
if ($layout) $layout.handleReload();
}
}
}
export default {
methods: {
tTitle (title) {
if (title && title.indexOf('$t:') === 0) {
return this.$t(title.split('$t:')[1]);
} else {
return title;
}
}
}
}
<template>
<div class="i-layout-tabs" :class="classes" :style="styles">
<div class="i-layout-tabs-main">
<Tabs
type="card"
:value="current"
:animated="false"
closable
@on-click="handleClickTab"
@on-tab-remove="handleClickClose"
>
<TabPane
v-for="page in opened"
:key="page.fullPath"
:label="(h) => tabLabel(h, page)"
:name="page.fullPath"
:closable="page.meta && page.meta.closable"
/>
</Tabs>
<Dropdown class="i-layout-tabs-close" @on-click="handleClose">
<div class="i-layout-tabs-close-main">
<Icon type="ios-arrow-down" />
</div>
<DropdownMenu slot="list">
<DropdownItem name="left">
<Icon type="md-arrow-back" />
{{ $t('basicLayout.tabs.left') }}
</DropdownItem>
<DropdownItem name="right">
<Icon type="md-arrow-forward" />
{{ $t('basicLayout.tabs.right') }}
</DropdownItem>
<DropdownItem name="other">
<Icon type="md-close" />
{{ $t('basicLayout.tabs.other') }}
</DropdownItem>
<DropdownItem name="all">
<Icon type="md-close-circle" />
{{ $t('basicLayout.tabs.all') }}
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import menuSider from '@/menu/sider';
import tTitle from '../mixins/translate-title';
import Setting from '@/setting';
import { getAllSiderMenu } from '@/libs/system';
export default {
name: 'iTabs',
mixins: [ tTitle ],
computed: {
...mapState('admin/page', [
'opened',
'current'
]),
...mapState('admin/layout', [
'showTabsIcon',
'tabsFix',
'tabsReload',
'headerFix',
'headerStick',
'isMobile',
'menuCollapse'
]),
...mapGetters('admin/menu', [
'hideSider'
]),
classes () {
return {
'i-layout-tabs-fix': this.tabsFix
}
},
isHeaderStick () {
return this.hideSider;
},
styles () {
let style = {};
if (this.tabsFix && !this.headerFix) {
style.top = `${64 - this.scrollTop}px`;
}
const menuWidth = this.isHeaderStick ? 0 : this.menuCollapse ? 80 : Setting.menuSideWidth;
if (!this.isMobile && this.tabsFix) {
style.width = `calc(100% - ${menuWidth}px)`;
style.left = `${menuWidth}px`;
}
return style;
}
},
data () {
return {
// 得到所有侧边菜单,并转为平级,查询图标用
allSiderMenu: getAllSiderMenu(menuSider),
scrollTop: 0
}
},
methods: {
...mapActions('admin/page', [
'close',
'closeLeft',
'closeRight',
'closeOther',
'closeAll'
]),
tabLabel (h, page) {
const title = h('span', this.tTitle(page.meta.title) || '未命名');
let slot = [];
if (this.showTabsIcon) {
const fullPathWithoutQuery = page.fullPath.indexOf('?') >= 0 ? page.fullPath.split('?')[0] : page.fullPath;
const currentMenu = this.allSiderMenu.find(menu => menu.path === fullPathWithoutQuery) || {};
let icon;
if (currentMenu.icon) {
icon = h('Icon', {
props: {
type: currentMenu.icon
}
});
} else if (currentMenu.custom) {
icon = h('Icon', {
props: {
custom: currentMenu.custom
}
});
} else if (currentMenu.img) {
icon = h('img', {
attrs: {
src: currentMenu.img
}
});
}
if (icon) slot.push(icon);
slot.push(title);
} else {
slot.push(title);
}
return h('div', {
class: 'i-layout-tabs-title'
}, slot);
},
handleClickTab (tabName) {
if (tabName === this.current) {
if (this.tabsReload) {
this.$emit('on-reload');
}
} else {
const page = this.opened.find(page => page.fullPath === tabName);
const { name, params, query } = page;
if (page) this.$router.push({ name, params, query }, () => {});
}
},
handleClickClose (tagName) {
this.close({
tagName
});
},
handleScroll () {
if (this.tabsFix && !this.headerFix) {
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop;
this.scrollTop = scrollTop > 64 ? 64 : scrollTop;
}
},
handleClose (name) {
const params = {
pageSelect: this.current
};
switch (name) {
case 'left':
this.closeLeft(params);
break;
case 'right':
this.closeRight(params);
break;
case 'other':
this.closeOther(params);
break;
case 'all':
this.closeAll();
break;
}
}
},
mounted () {
document.addEventListener('scroll', this.handleScroll, { passive: true });
this.handleScroll();
},
beforeDestroy () {
document.removeEventListener('scroll', this.handleScroll);
}
}
</script>
<template>
<nuxt/>
</template>
<style lang="less">
html,body,#__layout,#__nuxt{
height: 100%;
}
</style>
<template>
<div class="page-account">
<div v-if="showI18n" class="page-account-header">
<i-header-i18n />
<div class="page-account">
<div v-if="showI18n" class="page-account-header">
<i-header-i18n />
</div>
<div class="page-account-container">
<div class="page-account-top">
<div class="page-account-top-logo">
<img src="@/assets/images/logo.png" alt="logo" />
</div>
<div class="page-account-container">
<div class="page-account-top">
<div class="page-account-top-logo">
<img src="@/assets/images/logo.png" alt="logo">
</div>
<div class="page-account-top-desc">iView Admin Pro 企业级中台前端/设计解决方案</div>
</div>
<Login @on-submit="handleSubmit">
<UserName name="username" value="admin" />
<Password name="password" value="admin" enter-to-submit />
<div class="page-account-auto-login">
<Checkbox v-model="autoLogin" size="large">{{ $t('page.login.remember') }}</Checkbox>
<a href="">{{ $t('page.login.forgot') }}</a>
</div>
<Submit>{{ $t('page.login.submit') }}</Submit>
</Login>
<div class="page-account-other">
<span>{{ $t('page.login.other') }}</span>
<img src="@/assets/svg/icon-social-wechat.svg" alt="wechat">
<img src="@/assets/svg/icon-social-qq.svg" alt="qq">
<img src="@/assets/svg/icon-social-weibo.svg" alt="weibo">
<router-link class="page-account-register" to="./register">{{ $t('page.login.signup') }}</router-link>
</div>
<div class="page-account-top-desc">iView Admin Pro 企业级中台前端/设计解决方案</div>
</div>
<Login @on-submit="handleSubmit">
<UserName name="username" value="admin" />
<Password name="password" value="admin" enter-to-submit />
<div class="page-account-auto-login">
<Checkbox v-model="autoLogin" size="large">{{ $t('page.login.remember') }}</Checkbox>
<a href>{{ $t('page.login.forgot') }}</a>
</div>
<i-copyright />
<Submit>{{ $t('page.login.submit') }}</Submit>
</Login>
<div class="page-account-other">
<span>{{ $t('page.login.other') }}</span>
<img src="@/assets/svg/icon-social-wechat.svg" alt="wechat" />
<img src="@/assets/svg/icon-social-qq.svg" alt="qq" />
<img src="@/assets/svg/icon-social-weibo.svg" alt="weibo" />
<router-link class="page-account-register" to="./register">{{ $t('page.login.signup') }}</router-link>
</div>
</div>
<i-copyright />
</div>
</template>
<script>
import iCopyright from '@/components/copyright';
import { mapActions } from 'vuex';
import mixins from '../mixins';
import iCopyright from "@/components/copyright";
import { mapActions } from "vuex";
import mixins from "../mixins";
export default {
mixins: [ mixins ],
components: { iCopyright },
data () {
return {
autoLogin: true
}
},
methods: {
...mapActions('admin/account', [
'login'
]),
/**
* @description 登录
* 表单校验已有 iView Pro 自动完成,如有需要修改,请阅读 iView Pro 文档
*/
handleSubmit (valid, values) {
if (valid) {
const { username, password } = values;
this.login({
username,
password
})
.then(() => {
// 重定向对象不存在则返回顶层路径
this.$router.replace(this.$route.query.redirect || '/');
});
}
}
}
export default {
layout: "empty",
mixins: [mixins],
components: { iCopyright },
data() {
return {
autoLogin: true
};
},
methods: {
...mapActions("admin/account", ["login"]),
/**
* @description 登录
* 表单校验已有 iView Pro 自动完成,如有需要修改,请阅读 iView Pro 文档
*/
handleSubmit(valid, values) {
if (valid) {
const { username, password } = values;
this.login({
username,
password
}).then(() => {
// 重定向对象不存在则返回顶层路径
this.$router.replace(this.$route.query.redirect || "/");
});
}
}
}
};
</script>
......@@ -42,6 +42,7 @@
import mixins from '../mixins';
export default {
layout: "empty",
mixins: [ mixins ],
components: { iCopyright },
data () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment