add layout component with header, content, sider and footer

This commit is contained in:
zhigang.li 2017-12-18 18:25:16 +08:00
parent e6508e277f
commit a2eb028782
17 changed files with 509 additions and 4 deletions

View file

@ -6,13 +6,14 @@ nav { margin-bottom: 40px; }
ul { display: flex; flex-wrap: wrap; }
li { display: inline-block; }
li + li { border-left: solid 1px #bbb; padding-left: 10px; margin-left: 10px; }
.container{ padding: 10px 40px; }
.container{ padding: 10px 40px 0; }
.v-link-active { color: #bbb; }
</style>
<template>
<div class="container">
<nav>
<ul>
<li><router-link to="/layout">Layout</router-link></li>
<li><router-link to="/affix">Affix</router-link></li>
<li><router-link to="/grid">Grid</router-link></li>
<li><router-link to="/button">Button</router-link></li>

View file

@ -17,6 +17,10 @@ Vue.config.debug = true;
// 路由配置
const router = new VueRouter({
routes: [
{
path: '/layout',
component: require('./routers/layout.vue')
},
{
path: '/affix',
component: require('./routers/affix.vue')

View file

@ -0,0 +1,61 @@
<template>
<div class="layout-demo-con">
<Layout :style="{minHeight: '100vh'}">
<Sider
v-model="isCollapsed"
collapsed-width="0"
breakpoint="md"
ref="side"
width="200">
<Menu width="auto" theme="dark" active-name="1">
<MenuGroup title="内容管理">
<MenuItem name="1">
<Icon type="document-text"></Icon>
文章管理
</MenuItem>
<MenuItem name="2">
<Icon type="chatbubbles"></Icon>
评论管理
</MenuItem>
</MenuGroup>
<MenuGroup title="统计分析">
<MenuItem name="3">
<Icon type="heart"></Icon>
用户留存
</MenuItem>
<MenuItem name="4">
<Icon type="heart-broken"></Icon>
流失用户
</MenuItem>
</MenuGroup>
</Menu>
</Sider>
<Layout class-name="test-class">
<Header :style="{background: '#eee'}"><Button @click="toggleCollapse">菜单</Button></Header>
<Content :style="{background:'#FFCF9E'}">
<p v-for="i in 100" :key="i">{{ i }}</p>
</Content>
<Footer>sdfsdsdfsdfs</Footer>
</Layout>
</Layout>
</div>
</template>
<script>
export default {
data () {
return {
isCollapsed: false
};
},
methods: {
toggleCollapse () {
this.$refs.side.toggleCollapse();
}
}
};
</script>
<style lang="less" scoped>
.layout-demo-con{
height: 100%;
}
</style>

View file

@ -0,0 +1,3 @@
import Content from '../layout/content.vue';
export default Content;

View file

@ -0,0 +1,3 @@
import Footer from '../layout/footer.vue';
export default Footer;

View file

@ -0,0 +1,3 @@
import Header from '../layout/header.vue';
export default Header;

View file

@ -0,0 +1,28 @@
<template>
<div :class="wrapClasses"><slot></slot></div>
</template>
<script>
const prefixCls = 'ivu-layout';
export default {
name: 'Content',
props: {
className: {
type: String,
default: ''
}
},
data () {
return {
prefixCls: prefixCls
};
},
computed: {
wrapClasses () {
return [
`${prefixCls}-content`,
this.className
];
}
}
};
</script>

View file

@ -0,0 +1,28 @@
<template>
<div :class="wrapClasses"><slot></slot></div>
</template>
<script>
const prefixCls = 'ivu-layout';
export default {
name: 'Footer',
props: {
className: {
type: String,
default: ''
}
},
data () {
return {
prefixCls: prefixCls
};
},
computed: {
wrapClasses () {
return [
`${prefixCls}-footer`,
this.className
];
}
}
};
</script>

View file

@ -0,0 +1,28 @@
<template>
<div :class="wrapClasses"><slot></slot></div>
</template>
<script>
const prefixCls = 'ivu-layout';
export default {
name: 'Header',
props: {
className: {
type: String,
default: ''
}
},
data () {
return {
prefixCls: prefixCls
};
},
computed: {
wrapClasses () {
return [
`${prefixCls}-header`,
this.className
];
}
}
};
</script>

View file

@ -0,0 +1,12 @@
import Layout from './layout.vue';
import Header from './header.vue';
import Sider from './sider.vue';
import Content from './content.vue';
import Footer from './footer.vue';
Layout.Header = Header;
Layout.Sider = Sider;
Layout.Content = Content;
Layout.Footer = Footer;
export default Layout;

View file

@ -0,0 +1,43 @@
<template>
<div :class="wrapClasses"><slot></slot></div>
</template>
<script>
const prefixCls = 'ivu-layout';
export default {
name: 'Layout',
props: {
className: {
type: String,
default: ''
}
},
data () {
return {
prefixCls: prefixCls,
hasSider: false
};
},
computed: {
wrapClasses () {
return [
`${prefixCls}`,
this.className,
{
[`${prefixCls}-has-sider`]: this.hasSider
}
];
}
},
methods: {
findSider () {
return this.$children.some(child => {
return child.$options._componentTag === 'Sider';
});
}
},
mounted () {
this.hasSider = this.findSider();
}
};
</script>

View file

@ -0,0 +1,150 @@
<template>
<div
:class="wrapClasses"
:style="{width: siderWidth + 'px', minWidth: siderWidth + 'px', maxWidth: siderWidth + 'px', flex: '0 0 ' + siderWidth + 'px'}">
<span v-show="showZeroTrigger" @click="toggleCollapse" :class="zeroWidthTriggerClasses">
<i class="ivu-icon ivu-icon-navicon-round"></i>
</span>
<div :class="`${prefixCls}-children`">
<slot></slot>
</div>
<div v-show="!mediaMatched && !hideTrigger" :class="triggerClasses" @click="toggleCollapse" :style="{width: siderWidth + 'px'}">
<i :class="triggerIconClasses"></i>
</div>
</div>
</template>
<script>
import { on, off } from '../../utils/dom';
import { oneOf } from '../../utils/assist';
const prefixCls = 'ivu-layout-sider';
const dimensionMap = {
xs: '480px',
sm: '768px',
md: '992px',
lg: '1200px',
xl: '1600px',
};
if (typeof window !== 'undefined') {
const matchMediaPolyfill = mediaQuery => {
return {
media: mediaQuery,
matches: false,
on() {
},
off() {
},
};
};
window.matchMedia = window.matchMedia || matchMediaPolyfill;
}
export default {
name: 'Sider',
props: {
value: { // if it's collpased now
type: Boolean,
default: false
},
className: {
type: String,
default: ''
},
width: {
type: [Number, String],
default: 200
},
collapsedWidth: {
type: [Number, String],
default: 64
},
hideTrigger: {
type: Boolean,
default: false
},
breakpoint: {
type: String,
default: '',
validator (val) {
return oneOf(val, ['xs', 'sm', 'md', 'lg', 'xl']);
}
},
defaultCollapsed: {
type: Boolean,
default: false
},
reverseArrow: {
type: Boolean,
default: false
}
},
data () {
return {
prefixCls: prefixCls,
mediaMatched: false
};
},
computed: {
wrapClasses () {
return [
`${prefixCls}`,
this.className,
this.siderWidth ? '' : `${prefixCls}-zero-width`,
this.value ? `${prefixCls}-collapsed` : ''
];
},
triggerClasses () {
return [
`${prefixCls}-trigger`,
this.value ? `${prefixCls}-trigger-collapsed` : '',
];
},
zeroWidthTriggerClasses () {
return [
`${prefixCls}-zero-width-trigger`,
this.reverseArrow ? `${prefixCls}-zero-width-trigger-left` : ''
];
},
triggerIconClasses () {
return [
'ivu-icon',
`ivu-icon-chevron-${this.reverseArrow ? 'right' : 'left'}`,
`${prefixCls}-trigger-icon`,
];
},
siderWidth () {
return this.value ? (this.mediaMatched ? 0 : parseInt(this.collapsedWidth)) : parseInt(this.width);
},
showZeroTrigger () {
return this.mediaMatched && !this.hideTrigger || (parseInt(this.collapsedWidth) === 0) && this.value && !this.hideTrigger;
}
},
methods: {
toggleCollapse () {
this.$emit('input', !this.value);
this.$emit('on-collapse', !this.value);
},
matchMedia () {
let matchMedia;
if (window.matchMedia) {
matchMedia = window.matchMedia;
}
let mediaMatched = this.mediaMatched;
this.mediaMatched = matchMedia(`(max-width: ${dimensionMap[this.breakpoint]})`).matches;
if (this.mediaMatched !== mediaMatched) {
this.$emit('input', this.mediaMatched);
this.$emit('on-collapse', this.mediaMatched);
}
},
onWindowResize () {
this.matchMedia();
}
},
mounted () {
on(window, 'resize', this.onWindowResize);
this.matchMedia();
this.$emit('input', this.defaultCollapsed);
},
destroyed () {
off(window, 'resize', this.onWindowResize);
}
};
</script>

View file

@ -0,0 +1,3 @@
import Sider from '../layout/sider.vue';
export default Sider;

View file

@ -17,13 +17,17 @@ import Checkbox from './components/checkbox';
import Circle from './components/circle';
import Collapse from './components/collapse';
import ColorPicker from './components/color-picker';
import Content from './components/content';
import DatePicker from './components/date-picker';
import Dropdown from './components/dropdown';
import Footer from './components/footer';
import Form from './components/form';
import Header from './components/header';
import Icon from './components/icon';
import Input from './components/input';
import InputNumber from './components/input-number';
import Scroll from './components/scroll';
import Layout from './components/layout';
import LoadingBar from './components/loading-bar';
import Menu from './components/menu';
import Message from './components/message';
@ -34,6 +38,7 @@ import Poptip from './components/poptip';
import Progress from './components/progress';
import Radio from './components/radio';
import Rate from './components/rate';
import Sider from './components/sider';
import Slider from './components/slider';
import Spin from './components/spin';
import Steps from './components/steps';
@ -71,21 +76,26 @@ const components = {
Col,
Collapse,
ColorPicker,
Content: Content,
DatePicker,
Dropdown,
DropdownItem: Dropdown.Item,
DropdownMenu: Dropdown.Menu,
Footer: Footer,
Form,
FormItem: Form.Item,
Header: Header,
Icon,
Input,
InputNumber,
Scroll,
Sider: Sider,
Submenu: Menu.Sub,
Layout: Layout,
LoadingBar,
Menu,
MenuGroup: Menu.Group,
MenuItem: Menu.Item,
Submenu: Menu.Sub,
Message,
Modal,
Notice,
@ -122,7 +132,10 @@ const iview = {
iButton: Button,
iCircle: Circle,
iCol: Col,
iContent: Content,
iForm: Form,
iFooter: Footer,
iHeader: Header,
iInput: Input,
iMenu: Menu,
iOption: Option,

View file

@ -15,6 +15,7 @@
@import "input-number";
@import "scroll";
@import "tag";
@import "layout";
@import "loading-bar";
@import "progress";
@import "timeline";

View file

@ -0,0 +1,113 @@
@layout-prefix-cls: ~"@{css-prefix}layout";
.@{layout-prefix-cls} {
display: flex;
flex-direction: column;
flex: auto;
background: @layout-body-background;
&&-has-sider {
flex-direction: row;
> .@{layout-prefix-cls},
> .@{layout-prefix-cls}-content {
overflow-x: hidden;
}
}
&-header,
&-footer {
flex: 0 0 auto;
}
&-header {
background: @layout-header-background;
padding: @layout-header-padding;
height: @layout-header-height;
line-height: @layout-header-height;
}
&-sider {
transition: all .2s @ease-in-out;
position: relative;
background: @layout-sider-background;
min-width: 0;
&-children {
height: 100%;
padding-top: 0.1px;
margin-top: -0.1px;
}
&-has-trigger {
padding-bottom: @layout-trigger-height;
}
&-trigger {
position: fixed;
bottom: 0;
text-align: center;
cursor: pointer;
height: @layout-trigger-height;
line-height: @layout-trigger-height;
color: @layout-trigger-color;
background: @layout-sider-background;
z-index: 1000;
transition: all .2s @ease-in-out;
.ivu-icon {
font-size: 16px;
}
>* {
transition: all .2s;
}
&-collapsed {
.@{layout-prefix-cls}-sider-trigger-icon {
transform: rotateZ(180deg);
}
}
}
&-zero-width {
& > * {
overflow: hidden;
}
&-trigger {
position: absolute;
top: @layout-header-height;
right: -@layout-zero-trigger-width;
text-align: center;
width: @layout-zero-trigger-width;
height: @layout-zero-trigger-height;
line-height: @layout-zero-trigger-height;
background: @layout-sider-background;
color: #fff;
font-size: @layout-zero-trigger-width / 2;
border-radius: 0 @border-radius-base @border-radius-base 0;
cursor: pointer;
transition: background .3s ease;
&:hover {
background: tint(@layout-sider-background, 10%);
}
&&-left {
right: 0;
left: -@layout-zero-trigger-width;
border-radius: @border-radius-base 0 0 @border-radius-base;
}
}
}
}
&-footer {
background: @layout-footer-background;
padding: @layout-footer-padding;
color: @text-color;
font-size: @font-size-base;
}
&-content {
flex: auto;
}
}

View file

@ -89,8 +89,19 @@
@btn-circle-size-small : 24px;
// Layout and Grid
@grid-columns : 24;
@grid-gutter-width : 0;
@grid-columns : 24;
@grid-gutter-width : 0;
@layout-body-background : #f5f7f9;
@layout-header-background : #495060;
@layout-header-height : 64px;
@layout-header-padding : 0 50px;
@layout-footer-padding : 24px 50px;
@layout-footer-background : @layout-body-background;
@layout-sider-background : @layout-header-background;
@layout-trigger-height : 48px;
@layout-trigger-color : #fff;
@layout-zero-trigger-width : 36px;
@layout-zero-trigger-height : 42px;
// Legend
@legend-color : #999;