Commit 5b19b5f55f0b1022c3bf5e7b7060534b2d9e3676
1 parent
191068ac
support Transfer
support Transfer
Showing
8 changed files
with
198 additions
and
55 deletions
Show diff stats
examples/app.vue
@@ -46,6 +46,7 @@ li + li { border-left: solid 1px #bbb; padding-left: 10px; margin-left: 10px; } | @@ -46,6 +46,7 @@ li + li { border-left: solid 1px #bbb; padding-left: 10px; margin-left: 10px; } | ||
46 | <li><router-link to="/select">Select</router-link></li> | 46 | <li><router-link to="/select">Select</router-link></li> |
47 | <li><router-link to="/backtop">Backtop</router-link></li> | 47 | <li><router-link to="/backtop">Backtop</router-link></li> |
48 | <li><router-link to="/page">Page</router-link></li> | 48 | <li><router-link to="/page">Page</router-link></li> |
49 | + <li><router-link to="/transfer">Transfer</router-link></li> | ||
49 | </ul> | 50 | </ul> |
50 | </nav> | 51 | </nav> |
51 | <router-view></router-view> | 52 | <router-view></router-view> |
examples/main.js
@@ -148,6 +148,10 @@ const router = new VueRouter({ | @@ -148,6 +148,10 @@ const router = new VueRouter({ | ||
148 | { | 148 | { |
149 | path: '/page', | 149 | path: '/page', |
150 | component: require('./routers/page.vue') | 150 | component: require('./routers/page.vue') |
151 | + }, | ||
152 | + { | ||
153 | + path: '/transfer', | ||
154 | + component: require('./routers/transfer.vue') | ||
151 | } | 155 | } |
152 | ] | 156 | ] |
153 | }); | 157 | }); |
examples/routers/transfer.vue
1 | +<!--<template>--> | ||
2 | + <!--<div>--> | ||
3 | + <!--<Transfer--> | ||
4 | + <!--:data="data1"--> | ||
5 | + <!--filterable--> | ||
6 | + <!--:target-keys="targetKeys1"--> | ||
7 | + <!--:render-format="render1"--> | ||
8 | + <!--@on-change="handleChange1"></Transfer>--> | ||
9 | + <!--</div>--> | ||
10 | +<!--</template>--> | ||
11 | +<!--<script>--> | ||
12 | + <!--export default {--> | ||
13 | + <!--data () {--> | ||
14 | + <!--return {--> | ||
15 | + <!--data1: this.getMockData(),--> | ||
16 | + <!--targetKeys1: this.getTargetKeys()--> | ||
17 | + <!--}--> | ||
18 | + <!--},--> | ||
19 | + <!--methods: {--> | ||
20 | + <!--getMockData () {--> | ||
21 | + <!--let mockData = [];--> | ||
22 | + <!--for (let i = 1; i <= 20; i++) {--> | ||
23 | + <!--mockData.push({--> | ||
24 | + <!--key: i.toString(),--> | ||
25 | + <!--label: '内容' + i,--> | ||
26 | + <!--description: '内容' + i + '的描述信息',--> | ||
27 | + <!--disabled: Math.random() * 3 < 1--> | ||
28 | + <!--});--> | ||
29 | + <!--}--> | ||
30 | + <!--return mockData;--> | ||
31 | + <!--},--> | ||
32 | + <!--getTargetKeys () {--> | ||
33 | + <!--return this.getMockData()--> | ||
34 | + <!--.filter(() => Math.random() * 2 > 1)--> | ||
35 | + <!--.map(item => item.key);--> | ||
36 | + <!--},--> | ||
37 | + <!--render1 (item) {--> | ||
38 | + <!--return item.label;--> | ||
39 | + <!--},--> | ||
40 | + <!--handleChange1 (newTargetKeys, direction, moveKeys) {--> | ||
41 | + <!--console.log(newTargetKeys);--> | ||
42 | + <!--console.log(direction);--> | ||
43 | + <!--console.log(moveKeys);--> | ||
44 | + <!--this.targetKeys1 = newTargetKeys;--> | ||
45 | + <!--}--> | ||
46 | + <!--}--> | ||
47 | + <!--}--> | ||
48 | +<!--</script>--> | ||
49 | + | ||
50 | + | ||
1 | <template> | 51 | <template> |
2 | <Transfer | 52 | <Transfer |
3 | - :data="data2" | ||
4 | - :target-keys="targetKeys2" | 53 | + :data="data3" |
54 | + :target-keys="targetKeys3" | ||
55 | + :list-style="listStyle" | ||
56 | + :render-format="render3" | ||
57 | + :operations="['向左移动','向右移动']" | ||
5 | filterable | 58 | filterable |
6 | - :render-format="rf" | ||
7 | - :filter-method="filterMethod" | ||
8 | - @on-change="handleChange2"></Transfer> | 59 | + @on-change="handleChange3"> |
60 | + <div :style="{float: 'right', margin: '5px'}"> | ||
61 | + <Button type="ghost" size="small" @click.native="reloadMockData">刷新</Button> | ||
62 | + </div> | ||
63 | + </Transfer> | ||
9 | </template> | 64 | </template> |
10 | <script> | 65 | <script> |
11 | export default { | 66 | export default { |
12 | data () { | 67 | data () { |
13 | return { | 68 | return { |
14 | - data2: this.getMockData(), | ||
15 | - targetKeys2: this.getTargetKeys() | 69 | + data3: this.getMockData(), |
70 | + targetKeys3: this.getTargetKeys(), | ||
71 | + listStyle: { | ||
72 | + width: '250px', | ||
73 | + height: '300px' | ||
74 | + } | ||
16 | } | 75 | } |
17 | }, | 76 | }, |
18 | methods: { | 77 | methods: { |
@@ -30,18 +89,62 @@ | @@ -30,18 +89,62 @@ | ||
30 | }, | 89 | }, |
31 | getTargetKeys () { | 90 | getTargetKeys () { |
32 | return this.getMockData() | 91 | return this.getMockData() |
33 | - .filter(() => Math.random() * 2 > 1) | ||
34 | - .map(item => item.key); | 92 | + .filter(() => Math.random() * 2 > 1) |
93 | + .map(item => item.key); | ||
35 | }, | 94 | }, |
36 | - handleChange2 (newTargetKeys) { | ||
37 | - this.targetKeys2 = newTargetKeys; | 95 | + handleChange3 (newTargetKeys) { |
96 | + this.targetKeys3 = newTargetKeys; | ||
38 | }, | 97 | }, |
39 | - filterMethod (data, query) { | ||
40 | - return data.label.indexOf(query) > -1; | 98 | + render3 (item) { |
99 | + return item.label + ' - ' + item.description; | ||
41 | }, | 100 | }, |
42 | - rf (data) { | ||
43 | - return '<i class="ivu-icon ivu-icon-alert"></i>' + data.label; | 101 | + reloadMockData () { |
102 | + this.data3 = this.getMockData(); | ||
103 | + this.targetKeys3 = this.getTargetKeys(); | ||
44 | } | 104 | } |
45 | } | 105 | } |
46 | } | 106 | } |
47 | </script> | 107 | </script> |
108 | + | ||
109 | +<!--<template>--> | ||
110 | + <!--<Transfer--> | ||
111 | + <!--:data="data4"--> | ||
112 | + <!--:target-keys="targetKeys4"--> | ||
113 | + <!--:render-format="render4"--> | ||
114 | + <!--@on-change="handleChange4"></Transfer>--> | ||
115 | +<!--</template>--> | ||
116 | +<!--<script>--> | ||
117 | + <!--export default {--> | ||
118 | + <!--data () {--> | ||
119 | + <!--return {--> | ||
120 | + <!--data4: this.getMockData(),--> | ||
121 | + <!--targetKeys4: this.getTargetKeys()--> | ||
122 | + <!--}--> | ||
123 | + <!--},--> | ||
124 | + <!--methods: {--> | ||
125 | + <!--getMockData () {--> | ||
126 | + <!--let mockData = [];--> | ||
127 | + <!--for (let i = 1; i <= 20; i++) {--> | ||
128 | + <!--mockData.push({--> | ||
129 | + <!--key: i.toString(),--> | ||
130 | + <!--label: '内容' + i,--> | ||
131 | + <!--description: '内容' + i + '的描述信息',--> | ||
132 | + <!--disabled: Math.random() * 3 < 1--> | ||
133 | + <!--});--> | ||
134 | + <!--}--> | ||
135 | + <!--return mockData;--> | ||
136 | + <!--},--> | ||
137 | + <!--getTargetKeys () {--> | ||
138 | + <!--return this.getMockData()--> | ||
139 | + <!--.filter(() => Math.random() * 2 > 1)--> | ||
140 | + <!--.map(item => item.key);--> | ||
141 | + <!--},--> | ||
142 | + <!--handleChange4 (newTargetKeys) {--> | ||
143 | + <!--this.targetKeys4 = newTargetKeys;--> | ||
144 | + <!--},--> | ||
145 | + <!--render4 (item) {--> | ||
146 | + <!--return item.label + ' - ' + item.description;--> | ||
147 | + <!--}--> | ||
148 | + <!--}--> | ||
149 | + <!--}--> | ||
150 | +<!--</script>--> |
src/components/transfer/list.vue
1 | <template> | 1 | <template> |
2 | <div :class="classes" :style="style"> | 2 | <div :class="classes" :style="style"> |
3 | <div :class="prefixCls + '-header'"> | 3 | <div :class="prefixCls + '-header'"> |
4 | - <Checkbox :checked.sync="checkedAll" :disabled="checkedAllDisabled" @on-change="toggleSelectAll"></Checkbox> | 4 | + <Checkbox :value="checkedAll" :disabled="checkedAllDisabled" @on-change="toggleSelectAll"></Checkbox> |
5 | <span>{{ title }}</span> | 5 | <span>{{ title }}</span> |
6 | <span :class="prefixCls + '-header-count'">{{ count }}</span> | 6 | <span :class="prefixCls + '-header-count'">{{ count }}</span> |
7 | </div> | 7 | </div> |
@@ -9,21 +9,23 @@ | @@ -9,21 +9,23 @@ | ||
9 | <div :class="prefixCls + '-body-search-wrapper'" v-if="filterable"> | 9 | <div :class="prefixCls + '-body-search-wrapper'" v-if="filterable"> |
10 | <Search | 10 | <Search |
11 | :prefix-cls="prefixCls + '-search'" | 11 | :prefix-cls="prefixCls + '-search'" |
12 | - :query.sync="query" | 12 | + :query="query" |
13 | + @on-query-clear="handleQueryClear" | ||
14 | + @on-query-change="handleQueryChange" | ||
13 | :placeholder="filterPlaceholder"></Search> | 15 | :placeholder="filterPlaceholder"></Search> |
14 | </div> | 16 | </div> |
15 | <ul :class="prefixCls + '-content'"> | 17 | <ul :class="prefixCls + '-content'"> |
16 | <li | 18 | <li |
17 | - v-for="item in showItems | filterBy filterData" | 19 | + v-for="item in filterData" |
18 | :class="itemClasses(item)" | 20 | :class="itemClasses(item)" |
19 | @click.prevent="select(item)"> | 21 | @click.prevent="select(item)"> |
20 | - <Checkbox :checked="isCheck(item)" :disabled="item.disabled"></Checkbox> | ||
21 | - <span>{{{ showLabel(item) }}}</span> | 22 | + <Checkbox :value="isCheck(item)" :disabled="item.disabled"></Checkbox> |
23 | + <span v-html="showLabel(item)"></span> | ||
22 | </li> | 24 | </li> |
23 | <li :class="prefixCls + '-content-not-found'">{{ notFoundText }}</li> | 25 | <li :class="prefixCls + '-content-not-found'">{{ notFoundText }}</li> |
24 | </ul> | 26 | </ul> |
25 | </div> | 27 | </div> |
26 | - <div :class="prefixCls + '-footer'" v-if="showFooter" v-el:footer><slot></slot></div> | 28 | + <div :class="prefixCls + '-footer'" v-if="showFooter"><slot></slot></div> |
27 | </div> | 29 | </div> |
28 | </template> | 30 | </template> |
29 | <script> | 31 | <script> |
@@ -31,6 +33,7 @@ | @@ -31,6 +33,7 @@ | ||
31 | import Checkbox from '../checkbox/checkbox.vue'; | 33 | import Checkbox from '../checkbox/checkbox.vue'; |
32 | 34 | ||
33 | export default { | 35 | export default { |
36 | + name: 'TransferList', | ||
34 | components: { Search, Checkbox }, | 37 | components: { Search, Checkbox }, |
35 | props: { | 38 | props: { |
36 | prefixCls: String, | 39 | prefixCls: String, |
@@ -52,6 +55,11 @@ | @@ -52,6 +55,11 @@ | ||
52 | showFooter: true | 55 | showFooter: true |
53 | }; | 56 | }; |
54 | }, | 57 | }, |
58 | + watch: { | ||
59 | + data () { | ||
60 | + this.updateFilteredData(); | ||
61 | + } | ||
62 | + }, | ||
55 | computed: { | 63 | computed: { |
56 | classes () { | 64 | classes () { |
57 | return [ | 65 | return [ |
@@ -79,6 +87,9 @@ | @@ -79,6 +87,9 @@ | ||
79 | }, | 87 | }, |
80 | checkedAllDisabled () { | 88 | checkedAllDisabled () { |
81 | return this.data.filter(data => !data.disabled).length <= 0; | 89 | return this.data.filter(data => !data.disabled).length <= 0; |
90 | + }, | ||
91 | + filterData () { | ||
92 | + return this.showItems.filter(item => this.filterMethod(item, this.query)); | ||
82 | } | 93 | } |
83 | }, | 94 | }, |
84 | methods: { | 95 | methods: { |
@@ -105,25 +116,23 @@ | @@ -105,25 +116,23 @@ | ||
105 | this.showItems = this.data; | 116 | this.showItems = this.data; |
106 | }, | 117 | }, |
107 | toggleSelectAll (status) { | 118 | toggleSelectAll (status) { |
108 | - this.checkedKeys = status ? | 119 | + const keys = status ? |
109 | this.data.filter(data => !data.disabled || this.checkedKeys.indexOf(data.key) > -1).map(data => data.key) : | 120 | this.data.filter(data => !data.disabled || this.checkedKeys.indexOf(data.key) > -1).map(data => data.key) : |
110 | this.data.filter(data => data.disabled && this.checkedKeys.indexOf(data.key) > -1).map(data => data.key); | 121 | this.data.filter(data => data.disabled && this.checkedKeys.indexOf(data.key) > -1).map(data => data.key); |
122 | + this.$emit('on-checked-keys-change', keys); | ||
123 | + }, | ||
124 | + handleQueryClear () { | ||
125 | + this.query = ''; | ||
111 | }, | 126 | }, |
112 | - filterData (value) { | ||
113 | - return this.filterMethod(value, this.query); | 127 | + handleQueryChange (val) { |
128 | + this.query = val; | ||
114 | } | 129 | } |
115 | }, | 130 | }, |
116 | created () { | 131 | created () { |
117 | this.updateFilteredData(); | 132 | this.updateFilteredData(); |
118 | - | ||
119 | - }, | ||
120 | - compiled () { | ||
121 | - this.showFooter = this.$els.footer.innerHTML !== ''; | ||
122 | }, | 133 | }, |
123 | - watch: { | ||
124 | - data () { | ||
125 | - this.updateFilteredData(); | ||
126 | - } | 134 | + mounted () { |
135 | + this.showFooter = this.$slots.default !== undefined; | ||
127 | } | 136 | } |
128 | }; | 137 | }; |
129 | </script> | 138 | </script> |
src/components/transfer/operation.vue
1 | <template> | 1 | <template> |
2 | <div :class="prefixCls + '-operation'"> | 2 | <div :class="prefixCls + '-operation'"> |
3 | - <i-button type="primary" size="small" :disabled="!rightActive" @click="moveToLeft"> | 3 | + <i-button type="primary" size="small" :disabled="!rightActive" @click.native="moveToLeft"> |
4 | <Icon type="ios-arrow-left"></Icon> {{ operations[0] }} | 4 | <Icon type="ios-arrow-left"></Icon> {{ operations[0] }} |
5 | </i-button> | 5 | </i-button> |
6 | - <i-button type="primary" size="small" :disabled="!leftActive" @click="moveToRight"> | 6 | + <i-button type="primary" size="small" :disabled="!leftActive" @click.native="moveToRight"> |
7 | {{ operations[1] }} <Icon type="ios-arrow-right"></Icon> | 7 | {{ operations[1] }} <Icon type="ios-arrow-right"></Icon> |
8 | </i-button> | 8 | </i-button> |
9 | </div> | 9 | </div> |
@@ -13,6 +13,7 @@ | @@ -13,6 +13,7 @@ | ||
13 | import Icon from '../icon/icon.vue'; | 13 | import Icon from '../icon/icon.vue'; |
14 | 14 | ||
15 | export default { | 15 | export default { |
16 | + name: 'Operation', | ||
16 | components: { iButton, Icon }, | 17 | components: { iButton, Icon }, |
17 | props: { | 18 | props: { |
18 | prefixCls: String, | 19 | prefixCls: String, |
src/components/transfer/search.vue
1 | <template> | 1 | <template> |
2 | <div :class="prefixCls"> | 2 | <div :class="prefixCls"> |
3 | <i-input | 3 | <i-input |
4 | - :value.sync="query" | 4 | + v-model="currentQuery" |
5 | size="small" | 5 | size="small" |
6 | :icon="icon" | 6 | :icon="icon" |
7 | :placeholder="placeholder" | 7 | :placeholder="placeholder" |
@@ -12,12 +12,26 @@ | @@ -12,12 +12,26 @@ | ||
12 | import iInput from '../input/input.vue'; | 12 | import iInput from '../input/input.vue'; |
13 | 13 | ||
14 | export default { | 14 | export default { |
15 | + name: 'Search', | ||
15 | components: { iInput }, | 16 | components: { iInput }, |
16 | props: { | 17 | props: { |
17 | prefixCls: String, | 18 | prefixCls: String, |
18 | placeholder: String, | 19 | placeholder: String, |
19 | query: String | 20 | query: String |
20 | }, | 21 | }, |
22 | + data () { | ||
23 | + return { | ||
24 | + currentQuery: this.query | ||
25 | + }; | ||
26 | + }, | ||
27 | + watch: { | ||
28 | + query (val) { | ||
29 | + this.currentQuery = val; | ||
30 | + }, | ||
31 | + currentQuery (val) { | ||
32 | + this.$emit('on-query-change', val); | ||
33 | + } | ||
34 | + }, | ||
21 | computed: { | 35 | computed: { |
22 | icon () { | 36 | icon () { |
23 | return this.query === '' ? 'ios-search' : 'ios-close'; | 37 | return this.query === '' ? 'ios-search' : 'ios-close'; |
@@ -25,17 +39,19 @@ | @@ -25,17 +39,19 @@ | ||
25 | }, | 39 | }, |
26 | methods: { | 40 | methods: { |
27 | handleClick () { | 41 | handleClick () { |
28 | - if (this.query === '') return; | ||
29 | - this.query = ''; | ||
30 | - } | ||
31 | - }, | ||
32 | - events: { | ||
33 | - 'on-form-blur' () { | ||
34 | - return false; | ||
35 | - }, | ||
36 | - 'on-form-change' () { | ||
37 | - return false; | 42 | + if (this.currentQuery === '') return; |
43 | + this.currentQuery = ''; | ||
44 | + this.$emit('on-query-clear'); | ||
38 | } | 45 | } |
39 | } | 46 | } |
47 | + // todo 事件 | ||
48 | +// events: { | ||
49 | +// 'on-form-blur' () { | ||
50 | +// return false; | ||
51 | +// }, | ||
52 | +// 'on-form-change' () { | ||
53 | +// return false; | ||
54 | +// } | ||
55 | +// } | ||
40 | }; | 56 | }; |
41 | </script> | 57 | </script> |
src/components/transfer/transfer.vue
1 | <template> | 1 | <template> |
2 | <div :class="classes"> | 2 | <div :class="classes"> |
3 | <List | 3 | <List |
4 | - v-ref:left | 4 | + ref="left" |
5 | :prefix-cls="prefixCls + '-list'" | 5 | :prefix-cls="prefixCls + '-list'" |
6 | :data="leftData" | 6 | :data="leftData" |
7 | :render-format="renderFormat" | 7 | :render-format="renderFormat" |
8 | - :checked-keys.sync="leftCheckedKeys" | ||
9 | - :valid-keys-count.sync="leftValidKeysCount" | 8 | + :checked-keys="leftCheckedKeys" |
9 | + @on-checked-keys-change="handleLeftCheckedKeysChange" | ||
10 | + :valid-keys-count="leftValidKeysCount" | ||
10 | :style="listStyle" | 11 | :style="listStyle" |
11 | :title="titles[0]" | 12 | :title="titles[0]" |
12 | :filterable="filterable" | 13 | :filterable="filterable" |
@@ -19,19 +20,20 @@ | @@ -19,19 +20,20 @@ | ||
19 | :operations="operations" | 20 | :operations="operations" |
20 | :left-active="leftValidKeysCount > 0" | 21 | :left-active="leftValidKeysCount > 0" |
21 | :right-active="rightValidKeysCount > 0"></Operation><List | 22 | :right-active="rightValidKeysCount > 0"></Operation><List |
22 | - v-ref:right | 23 | + ref="right" |
23 | :prefix-cls="prefixCls + '-list'" | 24 | :prefix-cls="prefixCls + '-list'" |
24 | :data="rightData" | 25 | :data="rightData" |
25 | :render-format="renderFormat" | 26 | :render-format="renderFormat" |
26 | - :checked-keys.sync="rightCheckedKeys" | ||
27 | - :valid-keys-count.sync="rightValidKeysCount" | 27 | + :checked-keys="rightCheckedKeys" |
28 | + @on-checked-keys-change="handleRightCheckedKeysChange" | ||
29 | + :valid-keys-count="rightValidKeysCount" | ||
28 | :style="listStyle" | 30 | :style="listStyle" |
29 | :title="titles[1]" | 31 | :title="titles[1]" |
30 | :filterable="filterable" | 32 | :filterable="filterable" |
31 | :filter-placeholder="filterPlaceholder" | 33 | :filter-placeholder="filterPlaceholder" |
32 | :filter-method="filterMethod" | 34 | :filter-method="filterMethod" |
33 | :not-found-text="notFoundText"> | 35 | :not-found-text="notFoundText"> |
34 | - <slot></slot> | 36 | + <slot name="right"></slot> |
35 | </List> | 37 | </List> |
36 | </div> | 38 | </div> |
37 | </template> | 39 | </template> |
@@ -177,7 +179,14 @@ | @@ -177,7 +179,14 @@ | ||
177 | 179 | ||
178 | this.$refs[opposite].toggleSelectAll(false); | 180 | this.$refs[opposite].toggleSelectAll(false); |
179 | this.$emit('on-change', newTargetKeys, direction, moveKeys); | 181 | this.$emit('on-change', newTargetKeys, direction, moveKeys); |
180 | - this.$dispatch('on-form-change', newTargetKeys, direction, moveKeys); | 182 | + // todo 事件 |
183 | +// this.$dispatch('on-form-change', newTargetKeys, direction, moveKeys); | ||
184 | + }, | ||
185 | + handleLeftCheckedKeysChange (keys) { | ||
186 | + this.leftCheckedKeys = keys; | ||
187 | + }, | ||
188 | + handleRightCheckedKeysChange (keys) { | ||
189 | + this.rightCheckedKeys = keys; | ||
181 | } | 190 | } |
182 | }, | 191 | }, |
183 | watch: { | 192 | watch: { |
src/index.js
@@ -39,7 +39,7 @@ import Tag from './components/tag'; | @@ -39,7 +39,7 @@ import Tag from './components/tag'; | ||
39 | import Timeline from './components/timeline'; | 39 | import Timeline from './components/timeline'; |
40 | // import TimePicker from './components/time-picker'; | 40 | // import TimePicker from './components/time-picker'; |
41 | import Tooltip from './components/tooltip'; | 41 | import Tooltip from './components/tooltip'; |
42 | -// import Transfer from './components/transfer'; | 42 | +import Transfer from './components/transfer'; |
43 | import Tree from './components/tree'; | 43 | import Tree from './components/tree'; |
44 | import Upload from './components/upload'; | 44 | import Upload from './components/upload'; |
45 | import { Row, Col } from './components/grid'; | 45 | import { Row, Col } from './components/grid'; |
@@ -107,7 +107,7 @@ const iview = { | @@ -107,7 +107,7 @@ const iview = { | ||
107 | TimelineItem: Timeline.Item, | 107 | TimelineItem: Timeline.Item, |
108 | // TimePicker, | 108 | // TimePicker, |
109 | Tooltip, | 109 | Tooltip, |
110 | - // Transfer, | 110 | + Transfer, |
111 | Tree, | 111 | Tree, |
112 | Upload | 112 | Upload |
113 | }; | 113 | }; |