Commit 77f7bb9533b9bcc1a2340711ac6fee2882915dee

Authored by 梁灏
1 parent 306e3f74

add Transfer component

add Transfer component
assets/iview.png

157 KB | W: | H:

159 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
src/components/button/button.vue
... ... @@ -71,7 +71,7 @@
71 71 }
72 72 },
73 73 ready () {
74   - this.showSlot = this.$els.slot.innerHTML !== '';
  74 + this.showSlot = this.$els.slot.innerHTML.replace(/\n/g, '').replace(/<!--[\w\W\r\n]*?-->/gmi, '') !== '';
75 75 }
76 76 }
77 77 </script>
... ...
src/components/transfer/index.js 0 → 100644
  1 +import Transfer from './transfer.vue';
  2 +export default Transfer;
0 3 \ No newline at end of file
... ...
src/components/transfer/list.vue 0 → 100644
  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
... ...
src/components/transfer/operation.vue 0 → 100644
  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
... ...
src/components/transfer/search.vue 0 → 100644
  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
... ...
src/components/transfer/transfer.vue 0 → 100644
  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 &#39;./components/switch&#39;;
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
... ... @@ -26,4 +26,5 @@
26 26 @import "poptip";
27 27 @import "input";
28 28 @import "slider";
29   -@import "cascader";
30 29 \ No newline at end of file
  30 +@import "cascader";
  31 +@import "transfer";
31 32 \ No newline at end of file
... ...
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  
... ...
src/styles/components/transfer.less 0 → 100644
  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
... ... @@ -42,6 +42,7 @@ li + li {
42 42 <li><a v-link="'/tag'">Tag</a></li>
43 43 <li><a v-link="'/input'">Input</a></li>
44 44 <li><a v-link="'/cascader'">Cascader</a></li>
  45 + <li><a v-link="'/transfer'">Transfer</a></li>
45 46 </ul>
46 47 </nav>
47 48 <router-view></router-view>
... ...
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  
... ...
test/routers/transfer.vue 0 → 100644
  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
... ...