refactor(transfer): use virtual scroll to deal with large data

This commit is contained in:
07akioni 2020-02-02 02:06:54 +08:00
parent 1d7be8be55
commit 93ea585a13
11 changed files with 248 additions and 114 deletions

View File

@ -45,10 +45,11 @@ export default {
if (!delay) {
this.vanishTimerId = null
this.show = false
}
} else {
this.vanishTimerId = window.setTimeout(() => {
this.show = false
}, delay)
}
},
updateLightBarTop (el, getLightBarTop = el => el.offsetTop) {
if (!el) return

View File

@ -19,14 +19,44 @@
class="n-transfer-list-body"
@mouseleave="handleSourceListMouseLeave"
>
<n-scrollbar>
<ul ref="sourceList" class="n-transfer-list-content">
<n-transfer-light-bar ref="lightBar" />
<n-scrollbar
v-if="virtualScroll"
:theme="synthesizedTheme"
:container="sourceScrollContainer"
:content="sourceScrollContent"
>
<recycle-scroller
v-if="virtualScroll"
ref="sourceVirtualScroller"
class="n-virtual-scroller n-transfer-list-content"
:items="memorizedSourceOptions"
:item-size="ITEM_SIZE"
key-field="value"
>
<template v-slot:before>
<n-base-light-bar ref="sourceLightBar" :item-size="ITEM_SIZE" :theme="synthesizedTheme" />
</template>
<template v-slot="{ item: option, index }">
<n-transfer-source-list-item
v-for="(option, index) in memorizedSourceOptions"
:key="option.value"
:value="option.value"
:disabled="!!option.disabled"
:label="option.label"
:index="index"
@click="handleSourceCheckboxClick"
@mouseenter="handleSourceOptionMouseEnter"
@mouseleave="handleSourceOptionMouseLeave"
/>
</template>
</recycle-scroller>
</n-scrollbar>
<n-scrollbar v-else>
<div ref="sourceList" class="n-transfer-list-content">
<n-base-light-bar ref="sourceLightBar" :item-size="ITEM_SIZE" :theme="synthesizedTheme" />
<n-transfer-source-list-item
v-for="option in memorizedSourceOptions"
ref="sourceListItems"
:key="option.value"
:index="index"
:value="option.value"
:disabled="!!option.disabled"
:label="option.label"
@ -34,7 +64,7 @@
@mouseenter="handleSourceOptionMouseEnter"
@mouseleave="handleSourceOptionMouseLeave"
/>
</ul>
</div>
</n-scrollbar>
</div>
</div>
@ -65,9 +95,40 @@
class="n-transfer-list-body"
@mouseleave="handleTargetListMouseLeave"
>
<n-scrollbar ref="rightScrollbar">
<ul ref="targetList" class="n-transfer-list-content">
<n-transfer-light-bar ref="secondLightBar" />
<n-scrollbar
v-if="virtualScroll"
:theme="synthesizedTheme"
:container="targetScrollContainer"
:content="targetScrollContent"
>
<recycle-scroller
v-if="virtualScroll"
ref="targetVirtualScroller"
class="n-virtual-scroller n-transfer-list-content"
:items="targetOptions"
:item-size="ITEM_SIZE"
key-field="value"
>
<template v-slot:before>
<n-base-light-bar ref="targetLightBar" :item-size="ITEM_SIZE" :theme="synthesizedTheme" />
</template>
<template v-slot="{ item: option, index }">
<n-transfer-target-list-item
:key="option.value"
:value="option.value"
:disabled="!!option.disabled"
:label="option.label"
:index="index"
@click="handleTargetCheckboxClick"
@mouseenter="handleTargetOptionMouseEnter"
@mouseleave="handleTargetOptionMouseLeave"
/>
</template>
</recycle-scroller>
</n-scrollbar>
<n-scrollbar v-else>
<div ref="targetList" class="n-transfer-list-content">
<n-base-light-bar ref="targetLightBar" :item-size="ITEM_SIZE" :theme="synthesizedTheme" />
<n-transfer-target-list-item
v-for="(option, index) in targetOptions"
ref="targetListItems"
@ -80,7 +141,7 @@
@mouseenter="handleTargetOptionMouseEnter"
@mouseleave="handleTargetOptionMouseLeave"
/>
</ul>
</div>
</n-scrollbar>
</div>
</div>
@ -98,8 +159,11 @@ import cloneDeep from 'lodash/cloneDeep'
import asformitem from '../../../mixins/asformitem'
import withapp from '../../../mixins/withapp'
import themeable from '../../../mixins/themeable'
import NTransferLightBar from './TransferLightBar'
import { RecycleScroller } from 'vue-virtual-scroller'
import debounce from 'lodash-es/debounce'
import NBaseLightBar from '../../../base/LightBar'
const ITEM_SIZE = 34
export default {
name: 'NTransfer',
@ -110,7 +174,8 @@ export default {
NTransferSourceListItem,
NTransferTargetListItem,
NTransferButton,
NTransferLightBar
NBaseLightBar,
RecycleScroller
},
mixins: [withapp, themeable, asformitem()],
model: {
@ -129,6 +194,10 @@ export default {
disabled: {
type: Boolean,
default: false
},
virtualScroll: {
type: Boolean,
default: false
}
},
provide () {
@ -145,10 +214,47 @@ export default {
nextTargetOptionsLength: null,
enableSourceEnterAnimation: false,
enableTargetEnterAnimation: false,
initialized: false
initialized: false,
ITEM_SIZE
}
},
computed: {
sourceScrollContainer () {
if (this.virtualScroll) {
return () => (
this.$refs.sourceVirtualScroller &&
this.$refs.sourceVirtualScroller.$el
)
}
return null
},
sourceScrollContent () {
if (this.virtualScroll) {
return () => (
this.$refs.sourceVirtualScroller &&
this.$refs.sourceVirtualScroller.$refs.wrapper
)
}
return null
},
targetScrollContainer () {
if (this.virtualScroll) {
return () => (
this.$refs.targetVirtualScroller &&
this.$refs.targetVirtualScroller.$el
)
}
return null
},
targetScrollContent () {
if (this.virtualScroll) {
return () => (
this.$refs.targetVirtualScroller &&
this.$refs.targetVirtualScroller.$refs.wrapper
)
}
return null
},
mergedDisabledStatus () {
return {
source: this.memorizedSourceOptions.every(option => option.disabled),
@ -250,36 +356,55 @@ export default {
},
cleanValue (value) {
if (Array.isArray(value)) {
return value.filter((v) => this.valueToOptionMap.has(v))
const valueToOptionMap = this.valueToOptionMap
return value.filter(v => valueToOptionMap.has(v))
} else return null
},
handleSourceHeaderCheckboxChange (value) {
if (this.sourceHeaderCheckboxIndeterminate) {
if (!this.virtualScroll) {
(this.$refs.sourceListItems || []).forEach(listItem => listItem.setChecked(false))
}
this.sourceCheckedValues = []
return
}
if (value) {
if (!this.virtualScroll) {
(this.$refs.sourceListItems || []).forEach(listItem => listItem.setChecked(true))
const newValues = this.memorizedSourceOptions.filter(option => !option.disabled).map(option => option.value).concat(this.sourceCheckedValues)
}
const newValues = this.memorizedSourceOptions
.filter(option => !option.disabled)
.map(option => option.value)
.concat(this.sourceCheckedValues)
this.sourceCheckedValues = Array.from(new Set(newValues))
} else {
if (!this.virtualScroll) {
(this.$refs.sourceListItems || []).forEach(listItem => listItem.setChecked(false))
}
this.sourceCheckedValues = []
}
},
handleTargetHeaderCheckboxChange (value) {
if (this.targetHeaderCheckboxIndeterminate) {
if (!this.virtualScroll) {
(this.$refs.targetListItems || []).forEach(listItem => listItem.setChecked(false))
}
this.targetCheckedValues = []
return
}
if (value) {
if (!this.virtualScroll) {
(this.$refs.targetListItems || []).forEach(listItem => listItem.setChecked(true))
const newValues = this.targetOptions.filter(option => !option.disabled).map(option => option.value).concat(this.targetCheckedValues)
}
const newValues = this.targetOptions
.filter(option => !option.disabled)
.map(option => option.value)
.concat(this.targetCheckedValues)
this.targetCheckedValues = Array.from(new Set(newValues))
} else {
if (!this.virtualScroll) {
(this.$refs.targetListItems || []).forEach(listItem => listItem.setChecked(false))
}
this.targetCheckedValues = []
}
},
@ -304,6 +429,16 @@ export default {
}
},
handleToTargetClick () {
if (this.virtualScroll) {
let newValue = Array.isArray(this.value) ? this.value : []
newValue = this.sourceCheckedValues.concat(newValue)
const sourceCheckedValueSet = this.sourceCheckedValueSet
this.memorizedSourceOptions = this.memorizedSourceOptions
.filter(option => !sourceCheckedValueSet.has(option.value))
this.$emit('change', newValue)
this.sourceCheckedValues = []
return
}
this.enableTargetEnterAnimation = true
const enteredItemEls = Array.from(this.$el.getElementsByClassName('n-transfer-list-item--enter'))
const length = enteredItemEls.length
@ -336,6 +471,17 @@ export default {
this.$emit('change', newValue)
},
handleToSourceClick () {
if (this.virtualScroll) {
let newValue = Array.isArray(this.value) ? this.value : []
const targetValueSet = this.targetCheckedValueSet
newValue = newValue.filter(value => !targetValueSet.has(value))
const valueToOptionMap = this.valueToOptionMap
const newSourceOptions = this.targetCheckedValues.map(value => valueToOptionMap.get(value))
this.memorizedSourceOptions = newSourceOptions.concat(this.memorizedSourceOptions)
this.$emit('change', newValue)
this.targetCheckedValues = []
return
}
this.enableSourceEnterAnimation = true
const enteredItemEls = Array.from(this.$el.getElementsByClassName('n-transfer-list-item--enter'))
const length = enteredItemEls.length
@ -370,24 +516,32 @@ export default {
/** clear check */
this.targetCheckedValues = []
},
handleSourceOptionMouseEnter: debounce(function (e) {
this.$refs.lightBar.updateLightBarPosition(e.target)
}, 128),
handleTargetOptionMouseEnter: debounce(function (e) {
this.$refs.secondLightBar.updateLightBarPosition(e.target)
}, 128),
handleSourceOptionMouseEnter: debounce(function (e, index) {
if (this.virtualScroll) {
this.$refs.sourceLightBar.updateLightBarTop(true, () => index * ITEM_SIZE)
} else {
this.$refs.sourceLightBar.updateLightBarTop(e.target)
}
}, 96),
handleTargetOptionMouseEnter: debounce(function (e, index) {
if (this.virtualScroll) {
this.$refs.targetLightBar.updateLightBarTop(true, () => index * ITEM_SIZE)
} else {
this.$refs.targetLightBar.updateLightBarTop(e.target)
}
}, 96),
handleSourceOptionMouseLeave: debounce(function (e) {
this.$refs.lightBar.hideLightBar()
}, 128),
this.$refs.sourceLightBar.hideLightBar()
}, 96),
handleTargetOptionMouseLeave: debounce(function (e) {
this.$refs.secondLightBar.hideLightBar()
}, 128),
this.$refs.targetLightBar.hideLightBar()
}, 96),
handleSourceListMouseLeave: debounce(function () {
this.$refs.lightBar.hideLightBar()
}, 128),
this.$refs.sourceLightBar.hideLightBar()
}, 96),
handleTargetListMouseLeave: debounce(function () {
this.$refs.secondLightBar.hideLightBar()
}, 128)
this.$refs.targetLightBar.hideLightBar()
}, 96)
}
}
</script>

View File

@ -1,39 +0,0 @@
<template>
<!-- <div style="position: absolute; left: 0; right: 0; top: 0; bottom: 0;"> -->
<transition name="n-transfer-list-light-bar--transition">
<div
v-show="show"
class="n-transfer-list-light-bar"
:style="{
top: styleTop
}"
/>
</transition>
<!-- </div> -->
</template>
<script>
export default {
data () {
return {
show: false,
styleTop: null,
vanishTimerId: null
}
},
methods: {
hideLightBar (delay = 300) {
this.vanishTimerId && window.clearTimeout(this.vanishTimerId)
this.vanishTimerId = window.setTimeout(() => {
this.show = false
}, delay)
},
updateLightBarPosition (el) {
this.vanishTimerId && window.clearTimeout(this.vanishTimerId)
this.vanishTimerId = null
this.show = true
this.styleTop = el.offsetTop + 'px'
}
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<li
<div
class="n-transfer-list-item n-transfer-list-item--source"
:class="{
'n-transfer-list-item--disabled': disabled,
@ -13,7 +13,7 @@
<n-simple-checkbox
:theme="NTransfer.synthesizedTheme"
:disabled="disabled"
:checked="checked"
:checked="synthesizedChecked"
/>
</div>
<div
@ -21,7 +21,7 @@
>
{{ label }}
</div>
</li>
</div>
</template>
<script>
@ -47,8 +47,8 @@ export default {
default: false
},
index: {
validator: createValidator(['number']),
required: true
type: Number,
default: null
}
},
inject: {
@ -62,8 +62,20 @@ export default {
enableEnterAnimation: false
}
},
computed: {
synthesizedChecked () {
if (this.NTransfer.virtualScroll) {
return this.NTransfer.sourceCheckedValues.includes(this.value)
} else {
return this.checked
}
}
},
created () {
if (this.NTransfer.initialized && this.NTransfer.enableSourceEnterAnimation) {
if (
this.NTransfer.initialized &&
this.NTransfer.enableSourceEnterAnimation
) {
this.enableEnterAnimation = true
}
},
@ -82,12 +94,12 @@ export default {
},
handleMouseEnter (e) {
if (!this.disabled) {
this.$emit('mouseenter', e)
this.$emit('mouseenter', e, this.index)
}
},
handleMouseLeave (e) {
if (!this.disabled) {
this.$emit('mouseleave', e)
this.$emit('mouseleave', e, this.index)
}
},
leave () {

View File

@ -1,5 +1,5 @@
<template>
<li
<div
class="n-transfer-list-item n-transfer-list-item--target"
:class="{
'n-transfer-list-item--disabled': disabled,
@ -13,7 +13,7 @@
<n-simple-checkbox
:theme="NTransfer.synthesizedTheme"
:disabled="disabled"
:checked="checked"
:checked="synthesizedChecked"
/>
</div>
<div
@ -21,7 +21,7 @@
>
{{ label }}
</div>
</li>
</div>
</template>
<script>
@ -47,8 +47,8 @@ export default {
default: false
},
index: {
validator: createValidator(['number']),
required: true
type: Number,
default: null
}
},
inject: {
@ -62,8 +62,20 @@ export default {
enableEnterAnimation: false
}
},
computed: {
synthesizedChecked () {
if (this.NTransfer.virtualScroll) {
return this.NTransfer.targetCheckedValues.includes(this.value)
} else {
return this.checked
}
}
},
created () {
if (this.NTransfer.initialized && this.NTransfer.enableTargetEnterAnimation) {
if (
this.NTransfer.initialized &&
this.NTransfer.enableTargetEnterAnimation
) {
this.enableEnterAnimation = true
}
},
@ -82,12 +94,12 @@ export default {
},
handleMouseEnter (e) {
if (!this.disabled) {
this.$emit('mouseenter', e)
this.$emit('mouseenter', e, this.index)
}
},
handleMouseLeave (e) {
if (!this.disabled) {
this.$emit('mouseleave', e)
this.$emit('mouseleave', e, this.index)
}
},
leave () {

View File

@ -14,7 +14,7 @@
right: 0;
left: 0;
transition: background-color .3s $--n-ease-in-out-cubic-bezier, top .3s $--n-ease-in-out-cubic-bezier!important;
&.#{block()}-transition-enter, &.#{block()}-leave-to {
&.#{block()}-transition-enter, &.#{block()}-transition-leave-to {
opacity: 0;
}
&.#{block()}-transition-enter-active {

View File

@ -15,8 +15,8 @@ $--card-margin-top: (
);
$--card-margin-bottom: (
'small': 8px,
'medium': 12px,
'small': 12px,
'medium': 16px,
'large': 16px,
'huge': 20px
);

View File

@ -70,6 +70,7 @@
@include b(transfer) {
@include once {
display: flex;
width: 444px;
}
@include b(transfer-list) {
@include once {
@ -78,6 +79,15 @@
height: 311px;
transition: background-color .3s $--n-ease-in-out-cubic-bezier, border-color .3s $--n-ease-in-out-cubic-bezier;
border-radius: 6px;
@include b(virtual-scroller) {
height: 100%;
scrollbar-width: none;
-moz-scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
background-color: map-get($--transfer-list-background-color, 'default');
border: 1px solid $--transfer-list-border-color;
@ -159,16 +169,6 @@
}
}
}
@include b(transfer-list-light-bar) {
@include once {
@include fade-in-transition(transfer-list-light-bar);
height: 34px;
transition: top .15s $--n-ease-in-out-cubic-bezier;
width: 100%;
position: absolute;
}
background-color: map-get($--tranfer-item-lightbar-background-color, 'default');
}
@include b(transfer-list-item) {
@include once {
transition: color .3s $--n-ease-in-out-cubic-bezier;

View File

@ -22,9 +22,6 @@
'default': $--n-secondary-text-color,
'disabled': $--n-disabled-text-color
) !global;
$--tranfer-item-lightbar-background-color: (
'default': change-color($--n-primary-color, $alpha: .3)
) !global;
$--transfer-list-border-color: rgba(255, 255, 255, .1) !global;
$--transfer-header-border-color: rgba(255, 255, 255, .1) !global;
}

View File

@ -23,9 +23,6 @@
'default': $--n-secondary-text-color,
'disabled': $--n-disabled-text-color
) !global;
$--tranfer-item-lightbar-background-color: (
'default': change-color($--n-primary-color, $alpha: .2)
) !global;
$--transfer-list-border-color: $--n-border-color !global;
$--transfer-header-border-color: transparent !global;
}