Commit 77f7bb9533b9bcc1a2340711ac6fee2882915dee
1 parent
306e3f74
add Transfer component
add Transfer component
Showing
15 changed files
with
519 additions
and
6 deletions
Show diff stats
assets/iview.png
src/components/button/button.vue
1 | +<template> | |
2 | + <div :class="prefixCls" :style="style"> | |
3 | + <div :class="prefixCls + '-header'"> | |
4 | + <Checkbox :checked.sync="checkedAll" :disabled="checkedAllDisabled" @on-change="toggleSelectAll">{{ title }}</Checkbox> | |
5 | + <span :class="prefixCls + '-header-count'">{{ count }}</span> | |
6 | + </div> | |
7 | + <div :class="bodyClasses"> | |
8 | + <div :class="prefixCls + '-body-search-wrapper'" v-if="filterable"> | |
9 | + <Search | |
10 | + :prefix-cls="prefixCls + '-search'" | |
11 | + :query.sync="query" | |
12 | + :placeholder="filterPlaceholder"></Search> | |
13 | + </div> | |
14 | + <ul :class="prefixCls + '-content'" v-if="showItems.length"> | |
15 | + <li | |
16 | + v-for="item in showItems | filterBy filterData" | |
17 | + :class="[prefixCls + '-content-item', {[prefixCls + '-content-item-disabled']: item.disabled}]" | |
18 | + @click.prevent="select(item)"><Checkbox :checked="isCheck(item)" :disabled="item.disabled">{{ showLabel(item) }}</Checkbox></li> | |
19 | + </ul> | |
20 | + <div :class="prefixCls + '-body-not-found'" v-else>{{ notFoundText }}</div> | |
21 | + </div> | |
22 | + <div :class="prefixCls + '-footer'"> | |
23 | + <slot></slot> | |
24 | + </div> | |
25 | + </div> | |
26 | +</template> | |
27 | +<script> | |
28 | + import Search from './search.vue'; | |
29 | + import Checkbox from '../checkbox/checkbox.vue'; | |
30 | + | |
31 | + export default { | |
32 | + components: { Search, Checkbox }, | |
33 | +// filters: { filterData: this.filterData }, | |
34 | + props: { | |
35 | + prefixCls: String, | |
36 | + data: Array, | |
37 | + renderFormat: Function, | |
38 | + checkedKeys: Array, | |
39 | + style: Object, | |
40 | + title: [String, Number], | |
41 | + filterable: Boolean, | |
42 | + filterPlaceholder: String, | |
43 | + filterMethod: Function, | |
44 | + notFoundText: String, | |
45 | + validKeysCount: Number | |
46 | + }, | |
47 | + data () { | |
48 | + return { | |
49 | + showItems: [], | |
50 | + query: '' | |
51 | + } | |
52 | + }, | |
53 | + computed: { | |
54 | + bodyClasses () { | |
55 | + return [ | |
56 | + `${this.prefixCls}-body`, | |
57 | + { | |
58 | + [`${this.prefixCls}-body-with-search`]: this.filterable | |
59 | + } | |
60 | + ] | |
61 | + }, | |
62 | + count () { | |
63 | + const validKeysCount = this.validKeysCount; | |
64 | + return (validKeysCount > 0 ? `${validKeysCount}/` : '') + `${this.data.length}条`; | |
65 | + }, | |
66 | + checkedAll () { | |
67 | + return this.data.filter(data => !data.disabled).length === this.validKeysCount && this.validKeysCount !== 0; | |
68 | + }, | |
69 | + checkedAllDisabled () { | |
70 | + return this.data.filter(data => !data.disabled).length <= 0; | |
71 | + } | |
72 | + }, | |
73 | + methods: { | |
74 | + showLabel (item) { | |
75 | + return this.renderFormat(item); | |
76 | + }, | |
77 | + isCheck (item) { | |
78 | + return this.checkedKeys.some(key => key === item.key); | |
79 | + }, | |
80 | + select (item) { | |
81 | + if (item.disabled) return; | |
82 | + const index = this.checkedKeys.indexOf(item.key); | |
83 | + index > -1 ? this.checkedKeys.splice(index, 1) : this.checkedKeys.push(item.key); | |
84 | + }, | |
85 | + updateFilteredData () { | |
86 | + this.showItems = this.data.map(item => { | |
87 | + return item; | |
88 | + }) | |
89 | + }, | |
90 | + toggleSelectAll (status) { | |
91 | + this.checkedKeys = status ? | |
92 | + this.data.filter(data => !data.disabled || this.checkedKeys.indexOf(data.key) > -1).map(data => data.key) : | |
93 | + this.data.filter(data => data.disabled && this.checkedKeys.indexOf(data.key) > -1).map(data => data.key); | |
94 | + }, | |
95 | + filterData (value) { | |
96 | + return this.filterMethod(value, this.query); | |
97 | + } | |
98 | + }, | |
99 | + created () { | |
100 | + this.updateFilteredData(); | |
101 | + }, | |
102 | + watch: { | |
103 | + data () { | |
104 | + this.updateFilteredData(); | |
105 | + } | |
106 | + } | |
107 | + } | |
108 | +</script> | |
0 | 109 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div :class="prefixCls + '-operation'"> | |
3 | + <i-button type="primary" size="small" :disabled="!rightActive" @click="moveToLeft"> | |
4 | + <Icon type="ios-arrow-left"></Icon> {{ operations[0] }} | |
5 | + </i-button> | |
6 | + <i-button type="primary" size="small" :disabled="!leftActive" @click="moveToRight"> | |
7 | + {{ operations[1] }} <Icon type="ios-arrow-right"></Icon> | |
8 | + </i-button> | |
9 | + </div> | |
10 | +</template> | |
11 | +<script> | |
12 | + import iButton from '../button/button.vue'; | |
13 | + import Icon from '../icon/icon.vue'; | |
14 | + | |
15 | + export default { | |
16 | + props: { | |
17 | + prefixCls: String, | |
18 | + operations: Array, | |
19 | + leftActive: Boolean, | |
20 | + rightActive: Boolean | |
21 | + }, | |
22 | + methods: { | |
23 | + moveToLeft () { | |
24 | + this.$parent.moveTo('left'); | |
25 | + }, | |
26 | + moveToRight () { | |
27 | + this.$parent.moveTo('right'); | |
28 | + } | |
29 | + } | |
30 | + } | |
31 | +</script> | |
0 | 32 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div :class="prefixCls"> | |
3 | + <i-input | |
4 | + :value.sync="query" | |
5 | + size="small" | |
6 | + :icon="icon" | |
7 | + :placeholder="placeholder" | |
8 | + @on-click="handleClick"></i-input> | |
9 | + </div> | |
10 | +</template> | |
11 | +<script> | |
12 | + import iInput from '../input/input.vue'; | |
13 | + | |
14 | + export default { | |
15 | + props: { | |
16 | + prefixCls: String, | |
17 | + placeholder: String, | |
18 | + query: String | |
19 | + }, | |
20 | + computed: { | |
21 | + icon () { | |
22 | + return this.query === '' ? 'ios-search' : 'ios-close'; | |
23 | + } | |
24 | + }, | |
25 | + methods: { | |
26 | + handleClick () { | |
27 | + if (this.query === '') return; | |
28 | + this.query = ''; | |
29 | + } | |
30 | + } | |
31 | + } | |
32 | +</script> | |
0 | 33 | \ No newline at end of file | ... | ... |
1 | +<template> | |
2 | + <div :class="classes"> | |
3 | + <List | |
4 | + v-ref:left | |
5 | + :prefix-cls="prefixCls + '-list'" | |
6 | + :data="leftData" | |
7 | + :render-format="renderFormat" | |
8 | + :checked-keys.sync="leftCheckedKeys" | |
9 | + :valid-keys-count.sync="leftValidKeysCount" | |
10 | + :style="listStyle" | |
11 | + :title="titles[0]" | |
12 | + :filterable="filterable" | |
13 | + :filter-placeholder="filterPlaceholder" | |
14 | + :filter-method="filterMethod" | |
15 | + :not-found-text="notFoundText"> | |
16 | + <slot></slot> | |
17 | + </List><Operation | |
18 | + :prefix-cls="prefixCls" | |
19 | + :operations="operations" | |
20 | + :left-active="leftValidKeysCount > 0" | |
21 | + :right-active="rightValidKeysCount > 0"></Operation><List | |
22 | + v-ref:right | |
23 | + :prefix-cls="prefixCls + '-list'" | |
24 | + :data="rightData" | |
25 | + :render-format="renderFormat" | |
26 | + :checked-keys.sync="rightCheckedKeys" | |
27 | + :valid-keys-count.sync="rightValidKeysCount" | |
28 | + :style="listStyle" | |
29 | + :title="titles[1]" | |
30 | + :filterable="filterable" | |
31 | + :filter-placeholder="filterPlaceholder" | |
32 | + :filter-method="filterMethod" | |
33 | + :not-found-text="notFoundText"> | |
34 | + <slot></slot> | |
35 | + </List> | |
36 | + </div> | |
37 | +</template> | |
38 | +<script> | |
39 | + import List from './list.vue'; | |
40 | + import Operation from './operation.vue'; | |
41 | + | |
42 | + const prefixCls = 'ivu-transfer'; | |
43 | + | |
44 | + export default { | |
45 | + components: { List, Operation }, | |
46 | + props: { | |
47 | + data: { | |
48 | + type: Array, | |
49 | + default () { | |
50 | + return [] | |
51 | + } | |
52 | + }, | |
53 | + renderFormat: { | |
54 | + type: Function, | |
55 | + default (item) { | |
56 | + return item.label || item.key; | |
57 | + } | |
58 | + }, | |
59 | + targetKeys: { | |
60 | + type: Array, | |
61 | + default () { | |
62 | + return [] | |
63 | + } | |
64 | + }, | |
65 | + selectedKeys: { | |
66 | + type: Array, | |
67 | + default () { | |
68 | + return [] | |
69 | + } | |
70 | + }, | |
71 | + listStyle: { | |
72 | + type: Object, | |
73 | + default () { | |
74 | + return {} | |
75 | + } | |
76 | + }, | |
77 | + titles: { | |
78 | + type: Array, | |
79 | + default () { | |
80 | + return ['源列表', '目的列表'] | |
81 | + } | |
82 | + }, | |
83 | + operations: { | |
84 | + type: Array, | |
85 | + default () { | |
86 | + return [] | |
87 | + } | |
88 | + }, | |
89 | + filterable: { | |
90 | + type: Boolean, | |
91 | + default: false | |
92 | + }, | |
93 | + filterPlaceholder: { | |
94 | + type: String, | |
95 | + default: '请输入搜索内容' | |
96 | + }, | |
97 | + filterMethod: { | |
98 | + type: Function, | |
99 | + default (data, query) { | |
100 | + const type = ('label' in data) ? 'label' : 'key'; | |
101 | + return data[type].indexOf(query) > -1; | |
102 | + } | |
103 | + }, | |
104 | + notFoundText: { | |
105 | + type: String, | |
106 | + default: '列表为空' | |
107 | + } | |
108 | + }, | |
109 | + data () { | |
110 | + return { | |
111 | + prefixCls: prefixCls, | |
112 | + leftData: [], | |
113 | + rightData: [], | |
114 | + leftCheckedKeys: [], | |
115 | + rightCheckedKeys: [] | |
116 | + } | |
117 | + }, | |
118 | + computed: { | |
119 | + classes () { | |
120 | + return [ | |
121 | + `${prefixCls}` | |
122 | + ] | |
123 | + }, | |
124 | + leftValidKeysCount () { | |
125 | + return this.getValidKeys('left').length; | |
126 | + }, | |
127 | + rightValidKeysCount () { | |
128 | + return this.getValidKeys('right').length; | |
129 | + } | |
130 | + }, | |
131 | + methods: { | |
132 | + getValidKeys (direction) { | |
133 | + return this[`${direction}Data`].filter(data => !data.disabled && this[`${direction}CheckedKeys`].indexOf(data.key) > -1).map(data => data.key); | |
134 | + }, | |
135 | + splitData (init = false) { | |
136 | + this.leftData = [...this.data]; | |
137 | + this.rightData = []; | |
138 | + if (this.targetKeys.length > 0) { | |
139 | + this.targetKeys.forEach((targetKey) => { | |
140 | + this.rightData.push( | |
141 | + this.leftData.filter((data, index) => { | |
142 | + if (data.key === targetKey) { | |
143 | + this.leftData.splice(index, 1); | |
144 | + return true; | |
145 | + } | |
146 | + return false; | |
147 | + })[0]); | |
148 | + }); | |
149 | + } | |
150 | + if (init) { | |
151 | + this.splitSelectedKey(); | |
152 | + } | |
153 | + }, | |
154 | + splitSelectedKey () { | |
155 | + const selectedKeys = this.selectedKeys; | |
156 | + if (selectedKeys.length > 0) { | |
157 | + this.leftCheckedKeys = this.leftData | |
158 | + .filter(data => selectedKeys.indexOf(data.key) > -1) | |
159 | + .map(data => data.key); | |
160 | + this.rightCheckedKeys = this.rightData | |
161 | + .filter(data => selectedKeys.indexOf(data.key) > -1) | |
162 | + .map(data => data.key); | |
163 | + } | |
164 | + }, | |
165 | + moveTo (direction) { | |
166 | + const targetKeys = this.targetKeys; | |
167 | + const opposite = direction === 'left' ? 'right' : 'left'; | |
168 | + const moveKeys = this.getValidKeys(opposite); | |
169 | + const newTargetKeys = direction === 'right' ? | |
170 | + moveKeys.concat(targetKeys) : | |
171 | + targetKeys.filter(targetKey => !moveKeys.some(checkedKey => targetKey === checkedKey)); | |
172 | + | |
173 | + this.$refs[opposite].toggleSelectAll(false); | |
174 | + this.$emit('on-change', newTargetKeys, direction, moveKeys); | |
175 | + } | |
176 | + }, | |
177 | + watch: { | |
178 | + targetKeys () { | |
179 | + this.splitData(false); | |
180 | + } | |
181 | + }, | |
182 | + created () { | |
183 | + this.splitData(true); | |
184 | + } | |
185 | + } | |
186 | +</script> | |
0 | 187 | \ No newline at end of file | ... | ... |
src/index.js
... | ... | @@ -27,6 +27,7 @@ import Switch from './components/switch'; |
27 | 27 | import Tag from './components/tag'; |
28 | 28 | import Timeline from './components/timeline'; |
29 | 29 | import Tooltip from './components/tooltip'; |
30 | +import Transfer from './components/transfer'; | |
30 | 31 | import { Row, Col } from './components/layout'; |
31 | 32 | import { Select, Option, OptionGroup } from './components/select'; |
32 | 33 | |
... | ... | @@ -71,7 +72,8 @@ const iview = { |
71 | 72 | Tag, |
72 | 73 | Timeline, |
73 | 74 | TimelineItem: Timeline.Item, |
74 | - Tooltip | |
75 | + Tooltip, | |
76 | + Transfer | |
75 | 77 | }; |
76 | 78 | |
77 | 79 | const install = function (Vue) { | ... | ... |
src/styles/components/index.less
src/styles/components/input.less
... | ... | @@ -19,6 +19,10 @@ |
19 | 19 | z-index: 1; |
20 | 20 | } |
21 | 21 | |
22 | + &-icon + &{ | |
23 | + padding-right: 32px; | |
24 | + } | |
25 | + | |
22 | 26 | &-wrapper-large &-icon{ |
23 | 27 | font-size: 18px; |
24 | 28 | height: @input-height-large; |
... | ... | @@ -29,10 +33,10 @@ |
29 | 33 | font-size: 14px; |
30 | 34 | height: @input-height-small; |
31 | 35 | line-height: @input-height-small; |
32 | - } | |
33 | 36 | |
34 | - &-icon + &{ | |
35 | - padding-right: 32px; | |
37 | + + .@{input-prefix-cls} { | |
38 | + padding-right: 24px; | |
39 | + } | |
36 | 40 | } |
37 | 41 | } |
38 | 42 | ... | ... |
1 | +@transfer-prefix-cls: ~"@{css-prefix}transfer"; | |
2 | +@transfer-item-prefix-cls: ~"@{css-prefix}transfer-list-content-item"; | |
3 | + | |
4 | +.@{transfer-prefix-cls} { | |
5 | + position: relative; | |
6 | + line-height: @line-height-base; | |
7 | + | |
8 | + &-list{ | |
9 | + display: inline-block; | |
10 | + width: 180px; | |
11 | + height: 210px; | |
12 | + font-size: @font-size-small; | |
13 | + vertical-align: middle; | |
14 | + border: 1px solid @border-color-base; | |
15 | + border-radius: @border-radius-base; | |
16 | + position: relative; | |
17 | + padding-top: 35px; | |
18 | + | |
19 | + &-header { | |
20 | + padding: 8px 16px; | |
21 | + border-radius: @border-radius-base @border-radius-base 0 0; | |
22 | + background: #fff; | |
23 | + color: @text-color; | |
24 | + border-bottom: 1px solid @border-color-split; | |
25 | + overflow: hidden; | |
26 | + position: absolute; | |
27 | + top: 0; | |
28 | + left: 0; | |
29 | + width: 100%; | |
30 | + | |
31 | + &-count { | |
32 | + margin: 0 !important; | |
33 | + float: right; | |
34 | + } | |
35 | + } | |
36 | + | |
37 | + &-body{ | |
38 | + height: 100%; | |
39 | + position: relative; | |
40 | + | |
41 | + &-with-search{ | |
42 | + padding-top: 40px; | |
43 | + } | |
44 | + } | |
45 | + | |
46 | + &-content{ | |
47 | + height: 100%; | |
48 | + padding: 4px 0; | |
49 | + overflow: auto; | |
50 | + | |
51 | + &-item{ | |
52 | + overflow: hidden; | |
53 | + white-space: nowrap; | |
54 | + text-overflow: ellipsis; | |
55 | + } | |
56 | + } | |
57 | + &-body-with-search &-content{ | |
58 | + padding: 0; | |
59 | + } | |
60 | + | |
61 | + &-body-search-wrapper{ | |
62 | + padding: 8px; | |
63 | + position: absolute; | |
64 | + top: 0; | |
65 | + left: 0; | |
66 | + right: 0; | |
67 | + } | |
68 | + | |
69 | + &-search{ | |
70 | + position: relative; | |
71 | + } | |
72 | + } | |
73 | + &-operation { | |
74 | + display: inline-block; | |
75 | + overflow: hidden; | |
76 | + margin: 0 16px; | |
77 | + vertical-align: middle; | |
78 | + | |
79 | + .@{btn-prefix-cls} { | |
80 | + display: block; | |
81 | + min-width: @btn-circle-size-small; | |
82 | + | |
83 | + &:first-child { | |
84 | + margin-bottom: 12px; | |
85 | + } | |
86 | + } | |
87 | + } | |
88 | +} | |
89 | +.select-item(@transfer-prefix-cls, @transfer-item-prefix-cls); | |
0 | 90 | \ No newline at end of file | ... | ... |
src/styles/mixins/checkbox.less
... | ... | @@ -11,6 +11,10 @@ |
11 | 11 | line-height: 1; |
12 | 12 | position: relative; |
13 | 13 | |
14 | + &-disabled{ | |
15 | + cursor: @cursor-disabled; | |
16 | + } | |
17 | + | |
14 | 18 | &:hover { |
15 | 19 | .@{checkbox-inner-prefix-cls} { |
16 | 20 | border-color: #bcbcbc; |
... | ... | @@ -56,6 +60,10 @@ |
56 | 60 | z-index: 1; |
57 | 61 | cursor: pointer; |
58 | 62 | opacity: 0; |
63 | + | |
64 | + &[disabled]{ | |
65 | + cursor: @cursor-disabled; | |
66 | + } | |
59 | 67 | } |
60 | 68 | } |
61 | 69 | |
... | ... | @@ -141,6 +149,9 @@ |
141 | 149 | & + & { |
142 | 150 | margin-left: 8px; |
143 | 151 | } |
152 | + &-disabled{ | |
153 | + cursor: @cursor-disabled; | |
154 | + } | |
144 | 155 | } |
145 | 156 | |
146 | 157 | .@{checkbox-prefix-cls}-wrapper + span, | ... | ... |
test/app.vue
test/main.js
... | ... | @@ -97,6 +97,11 @@ router.map({ |
97 | 97 | component: function (resolve) { |
98 | 98 | require(['./routers/cascader.vue'], resolve); |
99 | 99 | } |
100 | + }, | |
101 | + '/transfer': { | |
102 | + component: function (resolve) { | |
103 | + require(['./routers/transfer.vue'], resolve); | |
104 | + } | |
100 | 105 | } |
101 | 106 | }); |
102 | 107 | ... | ... |
1 | +<style> | |
2 | + body{ | |
3 | + height: auto; | |
4 | + } | |
5 | +</style> | |
6 | +<template> | |
7 | + <div style="margin: 50px;"> | |
8 | + <Transfer | |
9 | + :data="data" | |
10 | + filterable | |
11 | + :target-keys.sync="targetKeys" | |
12 | + :selected-keys="selectedKeys" | |
13 | + :operations="['向左移动','向右移动']" | |
14 | + @on-change="change"></Transfer> | |
15 | + </div> | |
16 | +</template> | |
17 | +<script> | |
18 | + import { Transfer } from 'iview'; | |
19 | + | |
20 | + export default { | |
21 | + props: { | |
22 | + | |
23 | + }, | |
24 | + data () { | |
25 | + return { | |
26 | + data: [{"key":"0","label":"content1","description":"description of content1","disabled":true},{"key":"1","label":"content2","description":"description of content2","disabled": false},{"key":"2","label":"content3","description":"description of content3","disabled":false},{"key":"3","label":"content4","description":"description of content4","disabled":false},{"key":"4","label":"content5","description":"description of content5","disabled":true},{"key":"5","label":"content6","description":"description of content6","disabled":false},{"key":"6","label":"content7","description":"description of content7","disabled":false},{"key":"7","label":"content8","description":"description of content8","disabled":false},{"key":"8","label":"content9","description":"description of content9","disabled":true},{"key":"9","label":"content10","description":"description of content10","disabled":false},{"key":"10","label":"content11","description":"description of content11","disabled":false},{"key":"11","label":"content12","description":"description of content12","disabled":false},{"key":"12","label":"content13","description":"description of content13","disabled":true},{"key":"13","label":"content14","description":"description of content14","disabled":false},{"key":"14","label":"content15","description":"description of content15","disabled":false},{"key":"15","label":"content16","description":"description of content16","disabled":false},{"key":"16","label":"content17","description":"description of content17","disabled":false},{"key":"17","label":"content18","description":"description of content18","disabled":true},{"key":"18","label":"content19","description":"description of content19","disabled":false},{"key":"19","label":"content20","description":"description of content20","disabled":false}], | |
27 | + targetKeys: ['1','2','3','5','8'], | |
28 | + selectedKeys: ['0','1','4', '5','6','9'] | |
29 | + } | |
30 | + }, | |
31 | + computed: { | |
32 | + | |
33 | + }, | |
34 | + methods: { | |
35 | + change (newTargetKeys, direction, moveKeys) { | |
36 | +// console.log(newTargetKeys) | |
37 | + this.targetKeys = newTargetKeys; | |
38 | + } | |
39 | + } | |
40 | + } | |
41 | +</script> | |
0 | 42 | \ No newline at end of file | ... | ... |