Commit dfe23efe3d262c0e57943525db9e17171aab49d1
Committed by
GitHub
Merge pull request #2646 from lison16/layout
add layout component with header, content, sider and footer
Showing
18 changed files
with
482 additions
and
4 deletions
Show diff stats
examples/app.vue
... | ... | @@ -6,13 +6,14 @@ nav { margin-bottom: 40px; } |
6 | 6 | ul { display: flex; flex-wrap: wrap; } |
7 | 7 | li { display: inline-block; } |
8 | 8 | li + li { border-left: solid 1px #bbb; padding-left: 10px; margin-left: 10px; } |
9 | -.container{ padding: 10px 40px; } | |
9 | +.container{ padding: 10px 40px 0; } | |
10 | 10 | .v-link-active { color: #bbb; } |
11 | 11 | </style> |
12 | 12 | <template> |
13 | 13 | <div class="container"> |
14 | 14 | <nav> |
15 | 15 | <ul> |
16 | + <li><router-link to="/layout">Layout</router-link></li> | |
16 | 17 | <li><router-link to="/affix">Affix</router-link></li> |
17 | 18 | <li><router-link to="/grid">Grid</router-link></li> |
18 | 19 | <li><router-link to="/button">Button</router-link></li> | ... | ... |
examples/main.js
1 | +<template> | |
2 | + <div class="layout-demo-con"> | |
3 | + <Layout :style="{minHeight: '100vh'}"> | |
4 | + <Sider | |
5 | + v-model="isCollapsed" | |
6 | + collapsed-width="0" | |
7 | + collapsible | |
8 | + ref="side" | |
9 | + width="200"> | |
10 | + <Menu width="auto" theme="dark" active-name="1"> | |
11 | + <MenuGroup title="内容管理"> | |
12 | + <MenuItem name="1"> | |
13 | + <Icon type="document-text"></Icon> | |
14 | + 文章管理 | |
15 | + </MenuItem> | |
16 | + <MenuItem name="2"> | |
17 | + <Icon type="chatbubbles"></Icon> | |
18 | + 评论管理 | |
19 | + </MenuItem> | |
20 | + </MenuGroup> | |
21 | + <MenuGroup title="统计分析"> | |
22 | + <MenuItem name="3"> | |
23 | + <Icon type="heart"></Icon> | |
24 | + 用户留存 | |
25 | + </MenuItem> | |
26 | + <MenuItem name="4"> | |
27 | + <Icon type="heart-broken"></Icon> | |
28 | + 流失用户 | |
29 | + </MenuItem> | |
30 | + </MenuGroup> | |
31 | + </Menu> | |
32 | + </Sider> | |
33 | + <Layout class-name="test-class"> | |
34 | + <Header :style="{background: '#eee'}"><Button @click="toggleCollapse">菜单</Button></Header> | |
35 | + <Content :style="{background:'#FFCF9E'}"> | |
36 | + <p v-for="i in 100" :key="i">{{ i }}</p> | |
37 | + </Content> | |
38 | + <Footer>sdfsdsdfsdfs</Footer> | |
39 | + </Layout> | |
40 | + </Layout> | |
41 | + </div> | |
42 | +</template> | |
43 | +<script> | |
44 | +export default { | |
45 | + data () { | |
46 | + return { | |
47 | + isCollapsed: false | |
48 | + }; | |
49 | + }, | |
50 | + methods: { | |
51 | + toggleCollapse () { | |
52 | + this.$refs.side.toggleCollapse(); | |
53 | + } | |
54 | + } | |
55 | +}; | |
56 | +</script> | |
57 | +<style lang="less" scoped> | |
58 | + .layout-demo-con{ | |
59 | + height: 100%; | |
60 | + } | |
61 | +</style> | |
0 | 62 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div :class="wrapClasses"><slot></slot></div> | |
3 | +</template> | |
4 | +<script> | |
5 | + const prefixCls = 'ivu-layout'; | |
6 | + export default { | |
7 | + name: 'Content', | |
8 | + computed: { | |
9 | + wrapClasses () { | |
10 | + return `${prefixCls}-content`; | |
11 | + } | |
12 | + } | |
13 | + }; | |
14 | +</script> | |
0 | 15 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div :class="wrapClasses"><slot></slot></div> | |
3 | +</template> | |
4 | +<script> | |
5 | + const prefixCls = 'ivu-layout'; | |
6 | + export default { | |
7 | + name: 'Footer', | |
8 | + computed: { | |
9 | + wrapClasses () { | |
10 | + return `${prefixCls}-footer`; | |
11 | + } | |
12 | + } | |
13 | + }; | |
14 | +</script> | |
0 | 15 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div :class="wrapClasses"><slot></slot></div> | |
3 | +</template> | |
4 | +<script> | |
5 | + const prefixCls = 'ivu-layout'; | |
6 | + export default { | |
7 | + name: 'Header', | |
8 | + computed: { | |
9 | + wrapClasses () { | |
10 | + return `${prefixCls}-header`; | |
11 | + } | |
12 | + } | |
13 | + }; | |
14 | +</script> | |
0 | 15 | \ No newline at end of file | ... | ... |
1 | +import Layout from './layout.vue'; | |
2 | +import Header from './header.vue'; | |
3 | +import Sider from './sider.vue'; | |
4 | +import Content from './content.vue'; | |
5 | +import Footer from './footer.vue'; | |
6 | + | |
7 | +Layout.Header = Header; | |
8 | +Layout.Sider = Sider; | |
9 | +Layout.Content = Content; | |
10 | +Layout.Footer = Footer; | |
11 | + | |
12 | +export default Layout; | |
0 | 13 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div :class="wrapClasses"><slot></slot></div> | |
3 | +</template> | |
4 | +<script> | |
5 | + const prefixCls = 'ivu-layout'; | |
6 | + | |
7 | + export default { | |
8 | + name: 'Layout', | |
9 | + data () { | |
10 | + return { | |
11 | + hasSider: false | |
12 | + }; | |
13 | + }, | |
14 | + computed: { | |
15 | + wrapClasses () { | |
16 | + return [ | |
17 | + `${prefixCls}`, | |
18 | + { | |
19 | + [`${prefixCls}-has-sider`]: this.hasSider | |
20 | + } | |
21 | + ]; | |
22 | + } | |
23 | + }, | |
24 | + methods: { | |
25 | + findSider () { | |
26 | + return this.$children.some(child => { | |
27 | + return child.$options.name === 'Sider'; | |
28 | + }); | |
29 | + } | |
30 | + }, | |
31 | + mounted () { | |
32 | + this.hasSider = this.findSider(); | |
33 | + } | |
34 | + }; | |
35 | +</script> | |
0 | 36 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div | |
3 | + :class="wrapClasses" | |
4 | + :style="wrapStyles"> | |
5 | + <span v-show="showZeroTrigger" @click="toggleCollapse" :class="zeroWidthTriggerClasses"> | |
6 | + <i class="ivu-icon ivu-icon-navicon-round"></i> | |
7 | + </span> | |
8 | + <div :class="childClasses"> | |
9 | + <slot></slot> | |
10 | + </div> | |
11 | + <div v-show="showBottomTrigger" :class="triggerClasses" @click="toggleCollapse" :style="{width: siderWidth + 'px'}"> | |
12 | + <i :class="triggerIconClasses"></i> | |
13 | + </div> | |
14 | + </div> | |
15 | +</template> | |
16 | +<script> | |
17 | + import { on, off } from '../../utils/dom'; | |
18 | + import { oneOf, dimensionMap, setMatchMedia } from '../../utils/assist'; | |
19 | + const prefixCls = 'ivu-layout-sider'; | |
20 | + setMatchMedia(); | |
21 | + export default { | |
22 | + name: 'Sider', | |
23 | + props: { | |
24 | + value: { // if it's collpased now | |
25 | + type: Boolean, | |
26 | + default: false | |
27 | + }, | |
28 | + width: { | |
29 | + type: [Number, String], | |
30 | + default: 200 | |
31 | + }, | |
32 | + collapsedWidth: { | |
33 | + type: [Number, String], | |
34 | + default: 64 | |
35 | + }, | |
36 | + hideTrigger: { | |
37 | + type: Boolean, | |
38 | + default: false | |
39 | + }, | |
40 | + breakpoint: { | |
41 | + type: String, | |
42 | + default: 'md', | |
43 | + validator (val) { | |
44 | + return oneOf(val, ['xs', 'sm', 'md', 'lg', 'xl']); | |
45 | + } | |
46 | + }, | |
47 | + collapsible: { | |
48 | + type: Boolean, | |
49 | + default: false | |
50 | + }, | |
51 | + defaultCollapsed: { | |
52 | + type: Boolean, | |
53 | + default: false | |
54 | + }, | |
55 | + reverseArrow: { | |
56 | + type: Boolean, | |
57 | + default: false | |
58 | + } | |
59 | + }, | |
60 | + data () { | |
61 | + return { | |
62 | + prefixCls: prefixCls, | |
63 | + mediaMatched: false, | |
64 | + isCollapsed: false | |
65 | + }; | |
66 | + }, | |
67 | + computed: { | |
68 | + wrapClasses () { | |
69 | + return [ | |
70 | + `${prefixCls}`, | |
71 | + this.siderWidth ? '' : `${prefixCls}-zero-width`, | |
72 | + this.isCollapsed ? `${prefixCls}-collapsed` : '' | |
73 | + ]; | |
74 | + }, | |
75 | + wrapStyles () { | |
76 | + return { | |
77 | + width: `${this.siderWidth}px`, | |
78 | + minWidth: `${this.siderWidth}px`, | |
79 | + maxWidth: `${this.siderWidth}px`, | |
80 | + flex: `0 0 ${this.siderWidth}px` | |
81 | + }; | |
82 | + }, | |
83 | + triggerClasses () { | |
84 | + return [ | |
85 | + `${prefixCls}-trigger`, | |
86 | + this.isCollapsed ? `${prefixCls}-trigger-collapsed` : '', | |
87 | + ]; | |
88 | + }, | |
89 | + childClasses () { | |
90 | + return `${this.prefixCls}-children`; | |
91 | + }, | |
92 | + zeroWidthTriggerClasses () { | |
93 | + return [ | |
94 | + `${prefixCls}-zero-width-trigger`, | |
95 | + this.reverseArrow ? `${prefixCls}-zero-width-trigger-left` : '' | |
96 | + ]; | |
97 | + }, | |
98 | + triggerIconClasses () { | |
99 | + return [ | |
100 | + 'ivu-icon', | |
101 | + `ivu-icon-chevron-${this.reverseArrow ? 'right' : 'left'}`, | |
102 | + `${prefixCls}-trigger-icon`, | |
103 | + ]; | |
104 | + }, | |
105 | + siderWidth () { | |
106 | + return this.collapsible ? (this.isCollapsed ? (this.mediaMatched ? 0 : parseInt(this.collapsedWidth)) : parseInt(this.width)) : this.width; | |
107 | + }, | |
108 | + showZeroTrigger () { | |
109 | + return this.collapsible ? (this.mediaMatched && !this.hideTrigger || (parseInt(this.collapsedWidth) === 0) && this.isCollapsed && !this.hideTrigger) : false; | |
110 | + }, | |
111 | + showBottomTrigger () { | |
112 | + return this.collapsible ? !this.mediaMatched && !this.hideTrigger : false; | |
113 | + } | |
114 | + }, | |
115 | + methods: { | |
116 | + toggleCollapse () { | |
117 | + this.isCollapsed = this.collapsible ? !this.isCollapsed : false; | |
118 | + this.$emit('input', !this.isCollapsed); | |
119 | + this.$emit('on-collapse', !this.isCollapsed); | |
120 | + }, | |
121 | + matchMedia () { | |
122 | + let matchMedia; | |
123 | + if (window.matchMedia) { | |
124 | + matchMedia = window.matchMedia; | |
125 | + } | |
126 | + let mediaMatched = this.mediaMatched; | |
127 | + this.mediaMatched = matchMedia(`(max-width: ${dimensionMap[this.breakpoint]})`).matches; | |
128 | + | |
129 | + if (this.mediaMatched !== mediaMatched) { | |
130 | + this.isCollapsed = this.collapsible ? this.mediaMatched : false; | |
131 | + this.$emit('input', this.mediaMatched); | |
132 | + this.$emit('on-collapse', this.mediaMatched); | |
133 | + } | |
134 | + }, | |
135 | + onWindowResize () { | |
136 | + this.matchMedia(); | |
137 | + } | |
138 | + }, | |
139 | + mounted () { | |
140 | + on(window, 'resize', this.onWindowResize); | |
141 | + this.matchMedia(); | |
142 | + this.$emit('input', this.defaultCollapsed); | |
143 | + if (this.defaultCollapsed) { | |
144 | + this.isCollapsed = true; | |
145 | + } | |
146 | + }, | |
147 | + destroyed () { | |
148 | + off(window, 'resize', this.onWindowResize); | |
149 | + } | |
150 | + }; | |
151 | +</script> | |
0 | 152 | \ No newline at end of file | ... | ... |
src/index.js
... | ... | @@ -17,13 +17,17 @@ import Checkbox from './components/checkbox'; |
17 | 17 | import Circle from './components/circle'; |
18 | 18 | import Collapse from './components/collapse'; |
19 | 19 | import ColorPicker from './components/color-picker'; |
20 | +import Content from './components/content'; | |
20 | 21 | import DatePicker from './components/date-picker'; |
21 | 22 | import Dropdown from './components/dropdown'; |
23 | +import Footer from './components/footer'; | |
22 | 24 | import Form from './components/form'; |
25 | +import Header from './components/header'; | |
23 | 26 | import Icon from './components/icon'; |
24 | 27 | import Input from './components/input'; |
25 | 28 | import InputNumber from './components/input-number'; |
26 | 29 | import Scroll from './components/scroll'; |
30 | +import Layout from './components/layout'; | |
27 | 31 | import LoadingBar from './components/loading-bar'; |
28 | 32 | import Menu from './components/menu'; |
29 | 33 | import Message from './components/message'; |
... | ... | @@ -34,6 +38,7 @@ import Poptip from './components/poptip'; |
34 | 38 | import Progress from './components/progress'; |
35 | 39 | import Radio from './components/radio'; |
36 | 40 | import Rate from './components/rate'; |
41 | +import Sider from './components/sider'; | |
37 | 42 | import Slider from './components/slider'; |
38 | 43 | import Spin from './components/spin'; |
39 | 44 | import Steps from './components/steps'; |
... | ... | @@ -71,21 +76,26 @@ const components = { |
71 | 76 | Col, |
72 | 77 | Collapse, |
73 | 78 | ColorPicker, |
79 | + Content: Content, | |
74 | 80 | DatePicker, |
75 | 81 | Dropdown, |
76 | 82 | DropdownItem: Dropdown.Item, |
77 | 83 | DropdownMenu: Dropdown.Menu, |
84 | + Footer: Footer, | |
78 | 85 | Form, |
79 | 86 | FormItem: Form.Item, |
87 | + Header: Header, | |
80 | 88 | Icon, |
81 | 89 | Input, |
82 | 90 | InputNumber, |
83 | 91 | Scroll, |
92 | + Sider: Sider, | |
93 | + Submenu: Menu.Sub, | |
94 | + Layout: Layout, | |
84 | 95 | LoadingBar, |
85 | 96 | Menu, |
86 | 97 | MenuGroup: Menu.Group, |
87 | 98 | MenuItem: Menu.Item, |
88 | - Submenu: Menu.Sub, | |
89 | 99 | Message, |
90 | 100 | Modal, |
91 | 101 | Notice, |
... | ... | @@ -122,7 +132,10 @@ const iview = { |
122 | 132 | iButton: Button, |
123 | 133 | iCircle: Circle, |
124 | 134 | iCol: Col, |
135 | + iContent: Content, | |
125 | 136 | iForm: Form, |
137 | + iFooter: Footer, | |
138 | + iHeader: Header, | |
126 | 139 | iInput: Input, |
127 | 140 | iMenu: Menu, |
128 | 141 | iOption: Option, | ... | ... |
src/styles/components/index.less
1 | +@layout-prefix-cls: ~"@{css-prefix}layout"; | |
2 | + | |
3 | +.@{layout-prefix-cls} { | |
4 | + display: flex; | |
5 | + flex-direction: column; | |
6 | + flex: auto; | |
7 | + background: @layout-body-background; | |
8 | + | |
9 | + &&-has-sider { | |
10 | + flex-direction: row; | |
11 | + > .@{layout-prefix-cls}, | |
12 | + > .@{layout-prefix-cls}-content { | |
13 | + overflow-x: hidden; | |
14 | + } | |
15 | + } | |
16 | + | |
17 | + &-header, | |
18 | + &-footer { | |
19 | + flex: 0 0 auto; | |
20 | + } | |
21 | + | |
22 | + &-header { | |
23 | + background: @layout-header-background; | |
24 | + padding: @layout-header-padding; | |
25 | + height: @layout-header-height; | |
26 | + line-height: @layout-header-height; | |
27 | + } | |
28 | + | |
29 | + &-sider { | |
30 | + transition: all .2s @ease-in-out; | |
31 | + position: relative; | |
32 | + background: @layout-sider-background; | |
33 | + | |
34 | + min-width: 0; | |
35 | + | |
36 | + &-children { | |
37 | + height: 100%; | |
38 | + padding-top: 0.1px; | |
39 | + margin-top: -0.1px; | |
40 | + } | |
41 | + | |
42 | + &-has-trigger { | |
43 | + padding-bottom: @layout-trigger-height; | |
44 | + } | |
45 | + | |
46 | + &-trigger { | |
47 | + position: fixed; | |
48 | + bottom: 0; | |
49 | + text-align: center; | |
50 | + cursor: pointer; | |
51 | + height: @layout-trigger-height; | |
52 | + line-height: @layout-trigger-height; | |
53 | + color: @layout-trigger-color; | |
54 | + background: @layout-sider-background; | |
55 | + z-index: 1000; | |
56 | + transition: all .2s @ease-in-out; | |
57 | + .ivu-icon { | |
58 | + font-size: 16px; | |
59 | + } | |
60 | + >* { | |
61 | + transition: all .2s; | |
62 | + } | |
63 | + &-collapsed { | |
64 | + .@{layout-prefix-cls}-sider-trigger-icon { | |
65 | + transform: rotateZ(180deg); | |
66 | + } | |
67 | + } | |
68 | + } | |
69 | + | |
70 | + &-zero-width { | |
71 | + & > * { | |
72 | + overflow: hidden; | |
73 | + } | |
74 | + | |
75 | + &-trigger { | |
76 | + position: absolute; | |
77 | + top: @layout-header-height; | |
78 | + right: -@layout-zero-trigger-width; | |
79 | + text-align: center; | |
80 | + width: @layout-zero-trigger-width; | |
81 | + height: @layout-zero-trigger-height; | |
82 | + line-height: @layout-zero-trigger-height; | |
83 | + background: @layout-sider-background; | |
84 | + color: #fff; | |
85 | + font-size: @layout-zero-trigger-width / 2; | |
86 | + border-radius: 0 @border-radius-base @border-radius-base 0; | |
87 | + cursor: pointer; | |
88 | + transition: background .3s ease; | |
89 | + | |
90 | + &:hover { | |
91 | + background: tint(@layout-sider-background, 10%); | |
92 | + } | |
93 | + | |
94 | + &&-left { | |
95 | + right: 0; | |
96 | + left: -@layout-zero-trigger-width; | |
97 | + border-radius: @border-radius-base 0 0 @border-radius-base; | |
98 | + } | |
99 | + } | |
100 | + } | |
101 | + } | |
102 | + | |
103 | + &-footer { | |
104 | + background: @layout-footer-background; | |
105 | + padding: @layout-footer-padding; | |
106 | + color: @text-color; | |
107 | + font-size: @font-size-base; | |
108 | + } | |
109 | + | |
110 | + &-content { | |
111 | + flex: auto; | |
112 | + } | |
113 | +} | |
0 | 114 | \ No newline at end of file | ... | ... |
src/styles/custom.less
... | ... | @@ -89,8 +89,19 @@ |
89 | 89 | @btn-circle-size-small : 24px; |
90 | 90 | |
91 | 91 | // Layout and Grid |
92 | -@grid-columns : 24; | |
93 | -@grid-gutter-width : 0; | |
92 | +@grid-columns : 24; | |
93 | +@grid-gutter-width : 0; | |
94 | +@layout-body-background : #f5f7f9; | |
95 | +@layout-header-background : #495060; | |
96 | +@layout-header-height : 64px; | |
97 | +@layout-header-padding : 0 50px; | |
98 | +@layout-footer-padding : 24px 50px; | |
99 | +@layout-footer-background : @layout-body-background; | |
100 | +@layout-sider-background : @layout-header-background; | |
101 | +@layout-trigger-height : 48px; | |
102 | +@layout-trigger-color : #fff; | |
103 | +@layout-zero-trigger-width : 36px; | |
104 | +@layout-zero-trigger-height : 42px; | |
94 | 105 | |
95 | 106 | // Legend |
96 | 107 | @legend-color : #999; | ... | ... |
src/utils/assist.js
... | ... | @@ -278,3 +278,25 @@ export function removeClass(el, cls) { |
278 | 278 | el.className = trim(curClass); |
279 | 279 | } |
280 | 280 | } |
281 | + | |
282 | +export const dimensionMap = { | |
283 | + xs: '480px', | |
284 | + sm: '768px', | |
285 | + md: '992px', | |
286 | + lg: '1200px', | |
287 | + xl: '1600px', | |
288 | +}; | |
289 | + | |
290 | +export function setMatchMedia () { | |
291 | + if (typeof window !== 'undefined') { | |
292 | + const matchMediaPolyfill = mediaQuery => { | |
293 | + return { | |
294 | + media: mediaQuery, | |
295 | + matches: false, | |
296 | + on() {}, | |
297 | + off() {}, | |
298 | + }; | |
299 | + }; | |
300 | + window.matchMedia = window.matchMedia || matchMediaPolyfill; | |
301 | + } | |
302 | +} | |
281 | 303 | \ No newline at end of file | ... | ... |