uniapp 组件封装踩坑(一)

219 阅读1分钟

省市级三级联动组件

// 支持添加默认id参数 
// 支持默认选项添加到省市区第一个选项 
// 支持确认后再更新选项结果(非双向绑定) 
// 支持 省市区,省市,省

<template>
	<view class="cu-form-group">
		<picker mode="multiSelector" @change="MultiChange" @columnchange="MultiColumnChange" :value="multiIndex" :range="multiArray.slice(0, column_)" range-key="region_name">
			<view class="uni-input">
				<text v-for="(item, index) in column_" :key="index" class="margin-right-sm">
					{{ multiArrayClone[index][multiIndexClone[index]].region_name }}
					<text v-if="index !== column_ - 1">-</text>
				</text>
			</view>
		</picker>
	</view>
</template>
<script>
export default {
	props: {
		multiIndex_: {
			type: Array,
			default: function() {
				return [];
			}
		},
		//自定义第一个选项的名称
		custom_: {
			type: String,
			default: ''
		},
		column_: {
			type: Number,
			default: 3
		}
	},
	components: {},
	data() {
		return {
			multiArray: [],
			multiArrayClone: [],
			multiIndex: [0, 0, 0],
			multiIndexClone: [0, 0, 0],
			region: uni.getStorageSync('region') || {}
		};
	},
	mounted() {
		if (!uni.getStorageSync('region')) {
			uni.showToast({
				title: '初始化省市区失败'
			});
		}
	},
	onLoad() {},
	computed: {},
	watch: {
		custom_: {
			handler: function(nVal, oVal) {
				if (nVal) {
					this.region = this.setDefaultFirstOption();
				}
			},
			immediate: true
		},
		multiIndex_: {
			handler: function(nVal, oVal) {
				if (!nVal.length) {
					const column1 = this.objToArr(this.region);
					const column2 = this.objToArr(column1[0].child);
					const column3 = this.objToArr(column2[0].child);
					this.multiArray = [column1, column2, column3];
					this.multiArrayClone = JSON.parse(JSON.stringify(this.multiArray));
					return;
				}
				const provinceId = nVal[0];
				const cityId = nVal[1];
				const areaId = nVal[2];

				const tempMultiArray = [];
				const column0 = this.objToArr(this.region);

				const index0 = column0.findIndex(item => item.region_id === provinceId);

				const column1 = this.objToArr(column0[index0].child);

				const index1 = column1.findIndex(item => item.region_id === cityId);

				tempMultiArray.push(column0);
				this.multiIndex[0] = index0;

				tempMultiArray.push(column1);
				this.multiIndex[1] = index1;

				const column2 = this.objToArr(tempMultiArray[1][index1].child);
				const index2 = column2.findIndex(item => item.region_id === areaId);

				tempMultiArray.push(column2);
				this.multiIndex[2] = index2;
				this.multiArray = tempMultiArray;
				this.multiArrayClone = JSON.parse(JSON.stringify(this.multiArray));
				this.multiIndexClone = JSON.parse(JSON.stringify(this.multiIndex));
			},
			immediate: true
		}
	},
	methods: {
		objToArr(obj) {
			const arr = Object.values(obj);
			let defaultIndex;
			let list = arr.map((item, index) => {
				if (item.region_name === this.custom_) {
					defaultIndex = index;
				}
				return {
					region_id: item.region_id,
					region_name: item.region_name,
					child: item.child
				};
			});
			if (defaultIndex !== undefined) {
				const temp = list[defaultIndex];
				list.splice(defaultIndex, 1);
				list.unshift(temp);
			}
			return list;
		},
		MultiChange(e) {
			this.multiIndexClone = e.detail.value;
			this.multiArrayClone = JSON.parse(JSON.stringify(this.multiArray));
			const arr = [];
			for (let index = 0; index < this.column_; index++) {
				arr.push(this.multiArrayClone[index][this.multiIndexClone[index]].region_id);
			}

			this.$emit('selecteRegion_', arr);
		},
		MultiColumnChange(e) {
			let data = {
				multiArray: this.multiArray,
				multiIndex: this.multiIndex
			};
			data.multiIndex[e.detail.column] = e.detail.value;
			const column = e.detail.column; // 移动某一列

			if (column === 0) {
				//移动第一列
				const row = data.multiIndex[0]; //移动第一列的某一行
				data.multiArray[1] = this.objToArr(data.multiArray[0][row].child);
				data.multiArray[2] = this.objToArr(data.multiArray[1][0].child);
				data.multiIndex[1] = 0;
				data.multiIndex[2] = 0;
			} else if (column === 1) {
				//移动第二列
				const row = data.multiIndex[1]; //移动第一列的某一行
				data.multiArray[2] = this.objToArr(data.multiArray[1][row].child);
				data.multiIndex[2] = 0;
			}

			this.multiArray = [...data.multiArray];
			this.multiIndex = [...data.multiIndex];
		},
		setDefaultFirstOption() {
			let region = uni.getStorageSync('region');
			region['00'] = {
				region_id: '00',
				region_name: this.custom_,
				child: {
					'00': {
						region_id: '00',
						region_name: this.custom_,
						child: {
							'00': {
								region_id: '00',
								region_name: this.custom_
							}
						}
					}
				}
			};

			for (const provinceK in region) {
				if (region.hasOwnProperty(provinceK)) {
					const province = region[provinceK];
					if (province.child) {
						province.child['00'] = {
							region_id: '00',
							region_name: this.custom_,
							child: {
								'00': {
									region_id: '00',
									region_name: this.custom_
								}
							}
						};
					}

					for (const cityK in province.child) {
						if (province.child.hasOwnProperty(cityK)) {
							const city = province.child[cityK];
							if (city.child) {
								city.child['00'] = {
									region_id: '00',
									region_name: this.custom_
								};
							}
						}
					}
				}
			}
			return region;
		}
	}
};
</script>
<style lang="scss" scoped>
.cu-form-group {
	width: 100%;
}
.title {
	text-align: center;
}
::v-deep .uni-picker-action.uni-picker-action-confirm {
	color: $uni-color-primary !important;
}
</style>

image.png

可拖拽按钮

<template>
	<view>
		<view
			id="_drag_button"
			class="drag"
			:style="'left: ' + left + 'px; top:' + top + 'px;'"
			@touchstart="touchstart"
			@touchmove.stop.prevent="touchmove"
			@touchend="touchend"
			@click.stop.prevent="click"
			:class="{transition: isDock && !isMove }"
		>
		
			<text>{{ text }}</text>
		</view>
	</view>
</template>

<script>
	export default {
		name: 'uni-drag-button',
		props: {
			isDock:{
				type: Boolean,
				default: false
			},
			existTabBar:{
				type: Boolean,
				default: false
			},
			text: {
				type: Number | String,
				default: 0
			},
			xedge: {
				type: Number,
				default: 10
			},
			yedge: {
				type: Number,
				default: 0
			}
		},
		data() {
			return {
				top:0,
				left:0,
				width: 0,
				height: 0,
				offsetWidth: 0,
				offsetHeight: 0,
				windowWidth: 0,
				windowHeight: 0,
				isMove: true,
			}
		},
		mounted() {
			const sys = uni.getSystemInfoSync();
			
			this.windowWidth = sys.windowWidth;
			this.windowHeight = sys.windowHeight;
			
			// #ifdef APP-PLUS
				this.existTabBar && (this.windowHeight -= 50);
			// #endif
			if (sys.windowTop) {
				this.windowHeight += sys.windowTop;
			}
			console.log(sys)
			
			const query = uni.createSelectorQuery().in(this);
			query.select('#_drag_button').boundingClientRect(data => {
				this.width = data.width;
				this.height = data.height;
				this.offsetWidth = data.width / 2;
				this.offsetHeight = data.height / 2;
				this.left = this.windowWidth - this.width - this.xedge;
				this.top = this.windowHeight - this.height - this.yedge;
			}).exec();
		},
		methods: {
			click() {
				this.$emit('btnClick');
			},
			touchstart(e) {
				this.$emit('btnTouchstart');
			},
			touchmove(e) {
				// 单指触摸
				if (e.touches.length !== 1) {
					return false;
				}
				
				this.isMove = true;
				
				let clientX = e.touches[0].clientX - this.offsetWidth;
				
				let clientY = e.touches[0].clientY - this.offsetHeight;
				// #ifdef H5
					clientY += this.height;
					clientX += this.width;
				// #endif
				let edgeBottom = this.windowHeight - this.height - this.xedge;
				let edgeRight = this.windowWidth - this.width - this.xedge;

				// 上下触及边界
				if (clientY < this.xedge) {
					this.top = this.xedge;
				} else if (clientY > edgeBottom) {
					this.top = edgeBottom;
				} else {
					this.top = clientY
				}
				
				// 左右触及边界
				if(clientX < this.xedge) {
					this.left = this.xedge
				} else if(clientX > edgeRight) {
					this.left = edgeRight
				} else {
					this.left = clientX
				}
			},
			touchend(e) {
				if (this.isDock) {
					let edgeRigth = this.windowWidth - this.width - this.xedge;
					
					if (this.left < this.windowWidth / 2 - this.offsetWidth) {
						this.left = this.xedge;
					} else {
						this.left = edgeRigth;
					}
					
				}
				
				this.isMove = false;
				
				this.$emit('btnTouchend');
			},
		}}
</script>

<style lang="scss" scoped>
	.drag {
		display: flex;
		justify-content: center;
		align-items: center;
		background: #ffffff;
		box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
		color: $uni-text-color-inverse;
		width: 80upx;
		height: 80upx;
		border-radius: 50%;
		font-size: $uni-font-size-sm;
		position: fixed;
		z-index: 999999;
                
		
		&.transition {
			transition: left .3s ease,top .3s ease;
		}
		color: rgba(212, 48, 48, 1);
	}
	
</style>

image.png

遮罩层

<template>
	<view class="popup spec" :class="maskClass" @touchmove.stop.prevent="stopPrevent" @click="toggleSpec">
		<!-- 遮罩层 -->
		<view class="mask"></view>
		<view class="layer attr-content" @click.stop="stopPrevent">
			<slot></slot>
		</view>
	</view>
</template>

<script>
export default {
	props: {
		maskClass: {
			type: String,
			default: 'none'
		}
	},
	methods: {
		toggleSpec() {
			this.$emit("toggle-mask")
		},
		stopPrevent() {}
	}
};
</script>

<style lang="scss" scoped>
/*  弹出层 */
.popup {
	position: fixed;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	z-index: 99;

	&.show {
		display: block;
		.mask {
			animation: showPopup 0.2s linear both;
		}
		.layer {
			animation: showLayer 0.2s linear both;
		}
	}
	&.hide {
		.mask {
			animation: hidePopup 0.2s linear both;
		}
		.layer {
			animation: hideLayer 0.2s linear both;
		}
	}
	&.none {
		display: none;
	}
	.mask {
		position: fixed;
		top: 0;
		width: 100%;
		height: 100%;
		z-index: 1;
		background-color: rgba(0, 0, 0, 0.4);
	}
	.layer {
		position: fixed;
		z-index: 99;
		/* #ifdef H5 */
		bottom: 50px !important;
		/* #endif */
		/* #ifdef MP */
		bottom: 0 !important;
		/* #endif */
		
		width: 100%;
		height: 50vh;
		border-radius: 10upx 10upx 0 0;
		background-color: #fff;
		overflow-y: auto;
		-webkit-overflow-scrolling: touch;
		.btn {
			height: 66upx;
			line-height: 66upx;
			border-radius: 100upx;
			background: $uni-color-primary;
			font-size: $font-base + 2upx;
			color: #fff;
			margin: 30upx auto 20upx;
		}
	}
	@keyframes showPopup {
		0% {
			opacity: 0;
		}
		100% {
			opacity: 1;
		}
	}
	@keyframes hidePopup {
		0% {
			opacity: 1;
		}
		100% {
			opacity: 0;
		}
	}
	@keyframes showLayer {
		0% {
			transform: translateY(120%);
		}
		100% {
			transform: translateY(0%);
		}
	}
	@keyframes hideLayer {
		0% {
			transform: translateY(0);
		}
		100% {
			transform: translateY(120%);
		}
	}
}
</style>

image.png

下拉框选择组件

<template>
	<view class="easy-select" @click.stop="trigger" :style="[easySelectSize]">
		<input type="text" v-model="value" :placeholder="placeholder" disabled clearable>
		<view class="easy-select-suffix" :style="{border: '1px solid rgba(0,0,0,0)'}" :class="[showSuffix]">
			<view class="easy-select-down-tag">^</view>
		</view>
		<view class="easy-select-options" v-if="showOptions" :style="{'min-width': boundingClientRect.width + 'px', top: optionsGroupTop, margin: optionsGroupMargin}">
			<view class="easy-select-options-item" v-for="item in options" :key="item.value" @click.stop="select(item)" :class="{active: currentSelect.label === item.label}">
				<text>{{item.label}}</text>
			</view>
		</view>
	</view>
</template>

<script>
	/**
	 * easy-select
	 * @author Snoop zhang
	 * @description Select Component
	 * */
	const COMPONENT_NAME = 'easy-select'
	const MAX_OPTIONS_HEIGHT = 137 // 修改务必也修改easy-select-options的css部分
	const OPTIONS_ITEM_HEIGHT = 33 // 修改务必也修改easy-select-options-item的css部分
	const OPTIONS_MARGIN = 10
	const OPTIONS_PADDING = 6 * 2 + 2 // + 2是border
	const OPTIONS_OTHER_HEIGHT = OPTIONS_MARGIN + OPTIONS_PADDING
	const STORAGE_KEY = '_easyWindowHeight'
	const SIZE = {
		'medium': {
			width: '240px',
			height: '40px'
		},
		'small': {
			width: '200px',
			height: '30px'
		},
		'mini': {
			width: '160px',
			height: '30px'
		}
	}
	
	export default {
		name: COMPONENT_NAME,
		props: {
			windowHeight: {
				type: [Number, String],
				default: 0
			},
			placeholder: {
				type: String,
				default: '请选择'
			},
			value: {
				type: String,
				default: '货到付款'
			},
			size: {
				type: String,
				default: 'medium'
			},
			options: {
				type: Array,
				default () {
					return [{
						value: '0',
						label: '货到付款'
					}, {
						value: '1',
						label: '赊账'
					}, {
						value: '2',
						label: '全额付款'
					}, {
						value: '3',
						label: '分期付款'
					}]
				}
			}
		},
		data() {
			return {
				showOptions: false,
				boundingClientRect: {},
				currentSelect: {},
				optionsGroupTop: 'auto',
				optionsGroupMargin: ''
			}
		},
		created() {
			this.$emit('selectOne', this.options[0])
		},
		computed: {
			showSuffix() {
				return this.showOptions ? 'showOptions' : 'no-showOptions'
			},
			easySelectSize() {
				let size = this.size.toLowerCase()
				if (size in SIZE) {
					return {
						width: SIZE[size].width,
						height: SIZE[size].height
					}
				} else {
					return {}
				}
			}
		},
		mounted() {
			const elQuery = uni.createSelectorQuery().in(this)
			elQuery.select('.easy-select').boundingClientRect(data => {
				this.boundingClientRect = data
			}).exec();
			try {
				if (!this.windowHeight) {
					const storageHeihgt = uni.getStorageSync(STORAGE_KEY)
					if (storageHeihgt) {
						this.easyWindowHeight = storageHeihgt
						return
					}
					const res = uni.getSystemInfoSync();
					this.easyWindowHeight = res.windowHeight
					uni.setStorageSync(STORAGE_KEY, this.easyWindowHeight)
				}
			} catch (e) {
			    // error
			}
		},
		methods: {
			trigger(e) {
				const view = uni.createSelectorQuery().in(this)
				view.select('.easy-select').fields({rect: true}, data => {
					let {	top, bottom } = data
					const thresholdHeight = Math.min(MAX_OPTIONS_HEIGHT + OPTIONS_MARGIN, (this.options.length * OPTIONS_ITEM_HEIGHT) +
						OPTIONS_OTHER_HEIGHT)
					bottom = Number(this.windowHeight || this.easyWindowHeight) - (top + this.boundingClientRect.height) // 距离底部的距离等于视口的高度减上top加select组件的高度
					// judge direction
					if (bottom < thresholdHeight) {
						this.optionsGroupDirection = 'up'
						this.optionsGroupTop = -thresholdHeight - 12 + 'px'
						this.optionsGroupMargin = '0'
					} else {
						this.optionsGroupDirection = 'down'
						this.optionsGroupTop = 'auto'
						this.optionsGroupMargin = '10px 0 0 0'
					}
					// if (this.scrollTop < )
					this.showOptions = !this.showOptions
				}).exec();
			},
			select(options) {
				this.showOptions = false
				this.currentSelect = options
				this.$emit('selectOne', options)
			},
			hideOptions() {
				this.showOptions = false
			}
		}
	}
</script>

<style scoped>
	.easy-select {
		position: relative;
		border: 1px solid #dcdfe6;
		border-radius: 4px;
		/* font-size: 28rpx; */
		color: #606266;
		outline: none;
		box-sizing: content-box;
		height: 30px;
	}
	.easy-select input {
		padding: 0 18rpx;
		padding-right: 60rpx;
		overflow: hidden;
		white-space: nowrap;
		text-overflow: ellipsis;
		height: 100% !important;
		min-height: 100% !important;
	}
	.easy-select .easy-select-suffix {
		position: absolute;
		box-sizing: border-box;
		height: 100%;
		right: 5px;
		top: 0;
		display: flex;
		align-items: center;
		transform: rotate(180deg);
		transition: all .3s;
		transform-origin: center;
	}
	.easy-select .showOptions {
		transform: rotate(0) !important;
	}
	.easy-select .no-showOptions {
		transform: rotate(180deg) !important;
	}
	.easy-select .easy-select-options {
		position: absolute;
		padding: 6px 0;
		margin-top: 10px;
		border: 1px solid #e4e7ed;
		border-radius: 4px;
		background-color: #fff;
		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
		box-sizing: border-box;
		transform-origin: center top;
		z-index: 2238;
		overflow: scroll;
		max-height: 274rpx;
	}
	.easy-select .easy-select-options-item {
		padding: 0 20rpx;
		position: relative;
		white-space: nowrap;
		font-size: 14px;
		color: #606266;
		height: 33px;
		line-height: 33px;
		box-sizing: border-box;
	}
	.easy-select .active {
		background-color: #F5F7FA
	}
</style>

image.png