Vueは移動端の三段連動を実現する_@郝晨光

14713 ワード

最近のプロジェクトで使う必要があるので、自分で手書きで1つ書いて、共有します.
まず、私たちが最後に実現したのは1級、2級、3級乃至多段連動をサポートする多重化可能なコンポーネントであり、例によってまず効果図を見て、1枚目は2級連動で、2枚目は3級連動である.私たちが実現した最終的な効果です.
本文で使用する知識点
  • Vueコンポーネントバインドv-model、参照:Vueコンポーネント(フォームコントロール以外)でv-model双方向データバインド@郝晨光
  • を使用
  • Vueのコンポーネント通信
  • Vueのスロット
  • VueのAPIについて一定の理解がある
  • VueのnextTick法
  • 原生JSの牽引に一定の基礎がある
  • きのうぶんせき
  • は最外層に露出する、外部に表示される選択された文字であり、クリックするとセレクタ
  • がポップアップする.
  • 内部のカスケードセレクタの数は外部で決定する、カスケードセレクタのオプションは外部で決定する
  • .
  • はデータの双方向バインドを実現し、内部修正外部変更、外部修正内部変更
  • を実現する.
  • 確定ボタンは現在の選択を保存し、キャンセルボタンは今回の操作をキャンセルし、前回の選択
  • に戻る.
  • は、できるだけ機能のみを実現するものであり、外部に露出するボタンの様式は、外部操作
  • によって行う.
  • クリック時にアニメーションがセレクタをポップアップし、外部を確定、キャンセルし、クリック時にセレクタ
  • を閉じる.
    ドラフト実装方式
  • カスケードセレクタとオプションの数はいずれも外部によって決定するため、直接3つのコンポーネント(外層ラップ、カスケードセレクタ、カスケードオプション)に分割し、すべて開発者
  • に暴露する.
  • 外層ラップに対してアニメーション表示非表示、およびデータ双方向バインディングを実現し、カスケードセレクタに対してドラッグ&ドロップを実現し、データ修正はタイムリーに発表され、カスケードオプションに対してデータレンダリング
  • を実現する.
    本番
    まず、私たちが最終的に使用したとき、どのように使用したかを見てみましょう.
    
    
    	
    		
                  {{item.name}}
            
    	
    	
    		
                  {{item.name}}
            
    	
    
    
    // script
    //         ,         
    data() {
        address: {
            province: '',
            city: ''
        }
    },
    computed: {
        showAddress() {
    		let province = this.provinceList.find(item => item.id===this.address.province) || {name:'   '};
    		let city = this.cityList.find(item => item.id===this.address.city) || {name:'   '};
    		return `${province.name} - ${city.name}`;
    	}
    }
    

    私たちの最終的な効果にとって、簡単ではないでしょうか.もちろん、もし私がここにh-wrapperを3つ書いたら、私たちは自然に3級連動になりました.
    まず最外層のslectorコンポーネントを定義する必要があります
    最外層のslectorコンポーネントは、外部に表示される文字に露出したり、カスケードセレクタの表示非表示を制御したりするために使用されます.新しい機能を開始するたびに、重要な機能を完了し、重要な機能が完了した後、インタラクティブな効果を含むいくつかのスタイルを変更する必要があります.
  • コンポーネント全体を実施する前に、実は私は最外層のselectorコンポーネントに対してあまり多くのことをしていません.私たちのカスケードセレクタは位置決めが必要なので、外層がどんな様子であっても、私たちの機能に影響を与えません.
  • 最終h-selectorコンポーネントのテンプレート
  • を一緒に見てみましょう.
    
    

    このテンプレートにとって、説明すべき点は少なく、私は注釈に書いています.次に、最終的な論理部分を見てみましょう.
    
    	export default {
    		name: "hSelector",
    		props: {
    			value: { //             
    				type: Object,  //       Object  
    				required: true //    
    			},
    			showValue: { //              
    				type: String,
    			},
    			title: { //           
    				type:String, //        
    				default: '' //       
    			}
    		},
    		data() {
                //   oldValue         ,  Object.assign    ,      value    
    			let oldValue = Object.assign({}, this.value);
    			return {
    				oldValue, //          
    				show: false //       
    			}
    		},
    		computed: {
    			defaultShowValue() { //          ,             ,            
    				let arr = [];
    				for (let i in this.value) {
    					arr.push(i + ':' + this.value[i])
    				}
    				return arr.join('/');
    			}
    		},
    		mounted() {
                // Vue      $on  ,     wrapper   changeSelected  ,  this.changeSelected  
    			this.$on('changeSelected', this.changeSelected);
    		},
            //     
    		methods: {
                //         ,           
    			changeSelected(prop, value) { //       ,prop             ,value      
    				let obj = {};
    				obj[prop] = value;
                    //   Object.assign            ,         
    				let obj2 = Object.assign({}, this.value, obj);
    				this.$emit('input', obj2);
    			},
                //     ,            ,        
    			cancel() {
    				this.$emit('input', this.oldValue);
    				this.show = false;
    			},
                //      ,         ,    oldValue ,      
    			confirm() {
    				this.oldValue = Object.assign({}, this.value);
    				this.$emit('change', this.value);
    				this.show = false;
    			},
                //          
    			showAddress() {
    				this.show = true;
    			}
    		}
    	}
    
    

    まず、mountedメソッドではthis.$を使用しています.onメソッドは、現在のコンポーネント内で公開されていないイベントを購読しています.このイベントは、h-wrapperというコンポーネントに定義されています.後で他の場所には複雑な機能と論理がなく、一つ一つ説明しません.
    次に最も主要な部分を定義します.それは私たちのwrapperコンポーネントです.
  • テンプレート部
  • 
    

    テンプレートの中で、注目しなければならないのは実は2つのstyleと4つのイベントで、もちろん、スロットの位置もあります.私はここで小さなテクニックを使って、元の位置で直接4つのoptionを書いて、その中の1つはオプションの位置が永遠に最も中間の位置にあることを保証するために選択してくださいと表示しています.
  • 論理部分(重中の重)論理に関する部分はすべて注釈に書いてありますので、
  • をよく読んでください.
    
    	export default {
    		name: 'hWrapper',
    		//   prop  ,          value          
    		props: {
    			prop: {
    				type: String,
    				required: true
    			}
    		},
    		//        
    		data() {
    			return {
    				//      
    				style: {
    					transform: 'translate3d(0px,0px,0px)',
    					transition: 'transform .3s',
    				},
    				activeIndex: 0, //        
    				startY: 0, //     
    				startTime: 0,  //     
    				endY: 0, //     
    				endTime: 0,  //     
    				prevY: 0, //         
    				direction: 0, //     
    				maxY: 0,  //       
    				minY: 0, //       
    				optionHeight: 0, //         
    				parentHeight: 0, //       
    				optionLength: 0  //      
    			}
    		},
    		//     
    		computed: {
    			//       ,                
    			propValue() {
    				return this.$parent.value[this.prop];
    			}
    		},
    		//    
    		watch: {
    			//   activeIndex   
    			activeIndex(newValue) {
    				//  activeIndex    ,                    
    				if (this.$children[newValue]) {
    					//       $emit  ,  changeSelected  
    					//                  
    					//       mounted   ,  $on       
    					//               
    					this.$parent.$emit('changeSelected', this.prop, this.$children[newValue].value);
    				}
    			},
    			//             
    			propValue(newValue) {
    				//           ,    change  ,         
    				this.$emit('change', newValue);
    				//             
    				this.formatAddress();
    			}
    		},
    		//       
    		mounted() {
    			//   Vue $nextTick  ,    DOM          
    			this.$nextTick(() => {
    				//     
    				this.formatData();
    				//             
    				this.formatAddress();
    			});
    			//        demo、      resize  ,                   
    			window.addEventListener('resize', () => {
    				this.formatData();
    				this.formatAddress();
    			})
    		},
    		//          
    		updated() {
    			//   Vue $nextTick  ,    DOM          
    			this.$nextTick(() => {
    				//     
    				this.formatData();
    				//             
    				this.formatAddress();
    			})
    		},
    		methods: {
    			//        
    			formatData() {
    				//         ,  this.$children               ( Vue  )
    				let children = this.$children;
    				//          
    				this.optionLength = children.length;
    				// try {
    				// 	this.optionHeight = children[0].$el.offsetHeight;
    				// } catch {
    				//     option   
    				this.optionHeight = this.$refs['wrapper'].children[0].offsetHeight;
    				// }
    				//             ,   option 5 ,     5 option
    				this.parentHeight = this.optionHeight * 5;
    				//          
    				this.maxY = -this.optionHeight * (this.optionLength - 1);
    			},
    			//          
    			formatAddress() {
    				//           
    				let children = this.$children;
    				//                           
    				let index = children.findIndex(item => item.value === this.$parent.value[this.prop]);
    				//       activeIndex,         ,       ,
    				//              ,        
    				this.activeIndex = index > -1 ? index : -1;
    				//         option       ,     
    				this.style.transform = `translate3d(0px,${-this.activeIndex * this.optionHeight}px,0px)`;
    			},
    			//     
    			touchStart(e) {
    				//          
    				this.startY = e.touches[0].pageY;
    				//          
    				this.startTime = e.timeStamp;
    				//       
    				this.style.transition = 'none';
    			},
    			//     
    			touchMove(e) {
    				//          
    				let moveY = e.changedTouches[0].pageY;
    				//          ,     ,moveY - this.startY  ,       
    				this.direction = moveY - this.startY;
    				//       
    				this.style.transform = `translate3d(0px,${this.prevY + this.direction}px,0px)`;
    			},
    			//     
    			touchEnd(e) {
    				//       
    				this.style.transition = 'transform .4s';
    				//       
    				this.endY = e.changedTouches[0].pageY;
    				//       
    				this.endTime = e.timeStamp;
    				//           
    				this.prevY = this.style.transform.split(',')[1].slice(0, -2) * 1;
    				//             
    				let activeIndex = -Math.round(this.prevY / this.optionHeight);
    				//                    
    				let distance = Math.abs(this.endY - this.startY);
    				//                   
    				let interval = this.endTime - this.startTime;
    				//                      
    				//   0     
    				if (this.direction > 0) {
    					//                   
    					activeIndex = this.activeIndex - Math.round(distance / interval) * 2;
    				//	   0     
    				} else if (this.direction < 0) {
    					//                   
    					activeIndex = this.activeIndex + Math.round(distance / interval) * 2;
    				}
    				//            ,       ,     
    				if (distance <= 1) {
    					// e.path              、0          option
    					//           offsetTop            
    					activeIndex = Math.round((e.path[0].offsetTop - this.optionHeight * 2) / this.optionHeight);
    				}
    				//  activeIndex        ,           
    				activeIndex = activeIndex < 0 ? 0 : activeIndex > this.optionLength - 1 ? this.optionLength - 1 : activeIndex;
    				//          
    				this.activeIndex = activeIndex;
    				//          ,     
    				this.style.transform = `translate3d(0px,${-this.activeIndex * this.optionHeight}px,0px)`;
    			},
    			//     
    			transitionEnd() {
    				//           
    				this.prevY = -this.activeIndex * this.optionHeight;
    			}
    		}
    	}
    
    

    論理では、activeIndexというインデックス値を操作することによって、データ中のulのシフトを動的に修正し、現在常に対応するインデックスとoptionの高さで計算された位置がtouchstart、touchmove、touchendの3つのイベントを通じて要素の位置とスライドがwatchを通じて対応する属性をリスニングし、リアルタイムでイベントをトリガし、カスケードセレクタが変化するようにした.内外同期に達するmountedとupdatedフック関数により、現在のカスケードセレクタ属性がslotスロットを介して外部から入力されたoptionオプションをリフレッシュすることを保証します.
    最後にoptionコンポーネント
    optionコンポーネントには、データの表示やカスケードセレクタが値を正しく取得できるようにするだけの内容はありません.
    
    
    
    	export default {
    		name: "hOption",
    		props: {
    			value: {
    				type: [String,Number],
    				required: true
    			}
    		}
    	}
    
    

    スタイルの問題
    まず、このカスケードセレクタのスタイルは、私はあまり処理していませんが、もうとてもきれいです.
    注意:外部に表示される内容は、h-selector-showはあまりスタイル処理をしていません.スタイル設定を完全に外部処理に渡し、必要に応じてスタイルを設定できることを保証します.
    注意:我々が開発したのは移動端のdemoであり、移動端は固定位置決めfixedに対してあまりサポートされていないため、私のスタイルではabsolute絶対位置決めを使用しています.問題は、現在のカスケードセレクタの外層に位置決め要素がないほうがいいということです.いいえ、影響を与えます.
    .h-selector {
    	letter-spacing: 1px;
    	font-size: 16px;
    	width: 100%;
    	height: 100%;
    	.h-selector-show {
    		width: 100%;
    		height: 100%;
    		box-sizing: border-box;
    		padding: 0 20px;
    		white-space: nowrap;
    		overflow: hidden;
    		text-overflow: ellipsis;
    	}
    	.h-selector-container {
    		position: absolute;
    		z-index: 999;
    		left: 0;
    		bottom: 0;
    		width: 100%;
    		background: #fff;
    	}
    	.h-selector-layer {
    		position: absolute;
    		background: rgba(0, 0, 0, 0.3);
    		width: 100%;
    		height: 100%;
    		top: 0;
    		left: 0;
    		z-index: 2;
    	}
    	ul, li {
    		margin: 0;
    		padding: 0;
    		list-style: none;
    	}
    	.h-selector-header {
    		display: flex;
    		align-items: center;
    		height: 40px;
    		justify-content: space-between;
    		padding: 0 30px;
    		.h-selector-header-cancel {
    			color: #e9aa14;
    		}
    		.h-selector-header-confirm {
    			color: #508aff;
    		}
    	}
    	.h-selector-content {
    		display: flex;
    		width: 100%;
    		position: relative;
    	}
    	.h-selector-wrapper {
    		flex: 1;
    		overflow: hidden;
    		& + .h-selector-wrapper {
    			border-left: 1px solid #ddd;
    		}
    	}
    	.h-selector-option {
    		line-height: 60px;
    		height: 60px;
    		text-align: center;
    		white-space: nowrap;
    		overflow: hidden;
    		text-overflow: ellipsis;
    	}
    	.h-selector-bg {
    		height: 100%;
    		width: 100%;
    		position: absolute;
    		top: 0;
    		left: 0;
    		background: linear-gradient(180deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.7)), linear-gradient(0deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4));
    		backface-visibility: hidden;
    		pointer-events: none;
    		background-repeat: no-repeat;
    		background-position: top, bottom;
    		background-size: 100% 100px;
    	}
    	.h-selector-move-enter-to, .h-selector-move-leave {
    		transform: translate3d(0, 0, 0);
    	}
    	.h-selector-move-enter-active, .h-selector-move-leave-active {
    		transition: transform .6s;
    	}
    	.h-selector-move-enter, .h-selector-move-leave-to {
    		transform: translate3d(0, 100%, 0);
    	}
    	.h-selector-fade-enter-to, .h-selector-fade-leave {
    		opacity: 1;
    	}
    	.h-selector-fade-enter-active, .h-selector-fade-leave-active {
    		transition: opacity .4s;
    	}
    	.h-selector-fade-enter, .h-selector-fade-leave-to {
    		opacity: 0;
    	}
    }
    

    最終的に、私達はすでに1種の移動端のカスケードセレクタを作ることに成功して、1級の2級の3級の連動を実現することができて、もちろん、マルチ級も何の問題もなくて、しかし移動端で、私は最大で3級の連動まで提案して、さもなくばユーザーの体験感に影響します
    ***###############################################################################菜鳥一枚、よろしくお願いします