uniapp下拉组件在官方提供的例子通常是用picker组件来实现,但希望像html 里select组件一样,点击后直接在下方显示。为此我们扩展了select组件。
扩展组件库代码
<template>
<view class="diy-selectdown">
<view class="diy-selectdown__content" :class="direction" :style="[contentStyle, {
transition: `opacity ${duration / 1000}s linear`,
'--hover-color':activeColor,
paddingLeft: menuPositionData.left+'px',
height: active?contentHeight + 'px':'0px'
}]" @tap="maskClick" @touchmove.stop.prevent>
<view :style="{width:menuPositionData.width+'px'}">
<view @tap.stop.prevent class="diy-selectdown__content__popup" :style="[popupStyle]">
<block v-if="!$slots.default && !$slots.$default">
<scroll-view scroll-y="true" :style="{
height: $tools.addUnit(itemHeight*(showLength!=0?showLength:list.length))
}">
<view class="diy-dropdown-items">
<view>
<view class="diy-dropdown-item" hover-class="diy-dropdown-item-hover"
hover-stay-time="150" @tap="cellClick(item[valueName])" :arrow="false"
v-for="(item, index) in list" :key="index" :style="{
color: selectValue == item[valueName] ? activeColor : inactiveColor
}">
{{item[labelName]}}
</view>
</view>
</view>
</scroll-view>
</block>
<slot v-else />
</view>
</view>
<view v-if="isMask" class="diy-selectdown__content__mask"></view>
</view>
</view>
</template>
<script>
export default {
name: 'diy-selectdown',
emits: ["open", "close", "change"],
props: {
triggerId: {
type: String,
default: 'selectdownId'
},
modelValue: {
type: Boolean,
default: false
},
defaultValue: {
type: [String, Number],
default: ''
},
backgroundColor: {
type: String,
default: '#fff'
},
// 列数据
list: {
type: Array,
default () {
return [];
}
},
// 自定义value属性名
valueName: {
type: String,
default: 'value'
},
// 自定义label属性名
labelName: {
type: String,
default: 'label'
},
// 菜单标题和选项的激活态颜色
activeColor: {
type: String,
default: '#19be6b'
},
// 菜单标题和选项的未激活态颜色
inactiveColor: {
type: String,
default: '#606266'
},
// 点击遮罩是否关闭菜单
closeOnClickMask: {
type: Boolean,
default: true
},
// 是否遮罩
isMask: {
type: Boolean,
default: false
},
// 点击当前激活项标题是否关闭菜单
closeOnClickSelf: {
type: Boolean,
default: true
},
direction: {
type: String,
default: 'down'
},
// 过渡时间
duration: {
type: [Number, String],
default: 300
},
// 标题菜单的高度,单位任意,数值默认为rpx单位
itemHeight: {
type: [Number, String],
default: 76
},
// 如果默认显示0个时,显示全部
showLength: {
type: Number,
default: 0
},
// 下拉出来的内容部分的圆角值
borderRadius: {
type: [Number, String],
default: 10
},
offsetTop:{
type:Number,
default:0
}
},
watch: {
modelValue: {
immediate: true,
handler(val) {
if (val) {
setTimeout(() => this.open(), 2);
} else {
setTimeout(() => this.close(), 2);
}
}
},
},
data() {
return {
selectValue: this.defaultValue,
showDropdown: true, // 是否打开下来菜单,
active: false, // 下拉菜单的状态
// 外层内容的样式,初始时处于底层,且透明
contentStyle: {
zIndex: -1,
opacity: 0
},
// 让某个菜单保持高亮的状态
highlightIndex: 99999,
menuPositionData: {
left: 0,
width: 375,
top: 0,
height: 40
},
topheight: 40,
contentHeight: 0
}
},
computed: {
// 下拉出来部分的样式
popupStyle() {
let style = {};
// 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
if (this.direction == 'up') {
style.transform = `translateY(${this.active ? 0 : '100%'})`
style.borderRadius =
`${this.$tools.addUnit(this.borderRadius)} ${this.$tools.addUnit(this.borderRadius)} 0 0 `;
} else {
style.transform = `translateY(${this.active ? 0 : '-100%'})`
style.borderRadius =
`0 0 ${this.$tools.addUnit(this.borderRadius)} ${this.$tools.addUnit(this.borderRadius)}`;
}
style['backgroundColor'] = this.backgroundColor;
style['transition-duration'] = this.duration / 1000 + 's';
style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
return style;
}
},
created() {
// 引用所有子组件(diy-selectdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
this.children = [];
},
methods: {
init() {
// 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
// 以保证数据的正确性
this.menuList = [];
this.children.map(child => {
child.init();
})
},
cellClick(value) {
this.selectValue = value
// 通知父组件(diy-selectdown)收起菜单
this.close();
// 发出事件,抛出当前勾选项的value
this.$emit("change", value);
},
// 点击菜单
menuClick() {
this.open();
},
getStatusBar() {
let promise = new Promise((resolve, reject) => {
uni.getSystemInfo({
success: function(e) {
let customBar
// #ifdef H5
customBar = e.statusBarHeight + e.windowTop;
// #endif
resolve(customBar)
}
})
})
return promise
},
getTopHeight() {
let thiz = this
return new Promise(function(resolve, reject) {
const query = uni.createSelectorQuery().in(this);
query.select('#' + thiz.triggerId).boundingClientRect(res => {
if (res) {
resolve(res)
} else {
resolve({
left: 0,
width: thiz.$u.sys().windowWidth,
top: 0,
height: 40
})
}
}).exec();
})
},
// 打开下拉菜单
async open(index) {
// 重置高亮索引,否则会造成多个菜单同时高亮
// 展开时,设置下拉内容的样式
this.contentStyle = {
zIndex: 999999,
}
let res = await this.getTopHeight();
this.menuPositionData = res;
let topHeight = res.top + res.height;
let statusBar = await this.getStatusBar()
statusBar = statusBar || 0
if (this.direction == 'up') {
this.topheight = statusBar
} else {
this.topheight = statusBar + topHeight
}
this.getContentHeight();
// 标记展开状态以及当前展开项的索引
this.active = true;
},
// 设置下拉菜单处于收起状态
close() {
this.active = false;
this.$emit("update:modelValue", false);
// 下拉内容的样式进行调整,不透明度设置为0
this.contentStyle = {
zIndex: -1,
opacity: 0
}
},
// 点击遮罩
maskClick() {
// 如果不允许点击遮罩,直接返回
if (!this.closeOnClickMask) return;
this.close();
},
// 外部手动设置某个菜单高亮
highlight(index = undefined) {
this.highlightIndex = index !== undefined ? index : 99999;
},
// 获取下拉菜单内容的高度
getContentHeight() {
// 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
// 才能让遮罩占满菜单一下,直到屏幕底部的高度
// this.$u.sys()为uView封装的获取设备信息的方法
let windowHeight = this.$u.sys().windowHeight;
if (this.direction == 'up') {
this.contentHeight = this.menuPositionData.top;
let bottom = windowHeight - this.menuPositionData.top
this.contentStyle.bottom = (this.topheight - this.offsetTop) + 'px'
} else {
this.contentHeight = windowHeight - this.menuPositionData.top;
this.contentStyle.top = (this.topheight + this.offsetTop) + 'px'
}
}
}
}
</script>
<style scoped lang="scss">
.diy-selectdown {
&__content {
--hover-color: #19be6b;
position: fixed;
z-index: 999999;
width: 100%;
left: 0px;
bottom: 0;
padding-top: 2px;
overflow: hidden;
&__mask {
position: absolute;
z-index: 9;
background: rgba(0, 0, 0, .3);
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
&__popup {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 10;
transition: all 0.3s;
transform: translateY(-100%);
overflow: hidden;
}
&.up &__popup {
position: absolute;
bottom: 0;
width: 100%;
transform: translateY(100%);
}
}
.diy-dropdown-item {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
box-sizing: border-box;
width: 100%;
padding: 20rpx;
color: #606266;
background-color: #fff;
text-align: left;
&-hover {
color: var(--hover-color) !important
}
}
}
</style>
组件库调用
经测试我们支持了循环子表单下拉选择、单选择、表单里选择。
<template>
<view class="container container329152">
<u-form-item class="diygw-col-24 diygw-uform-item diygw-form-border" label="下拉1" prop="sctdown1">
<diy-selectinput :id="`sctdown1`" @click="showSctdown1 = true" class="diygw-col-24 solid" valueName="value" labelName="label" :list="sctdown1Datas" placeholder="请选择" v-model="sctdown1" type="select"></diy-selectinput>
<diy-selectdown :trigger-id="`sctdown1`" v-model="showSctdown1" valueName="value" labelName="label" :list="sctdown1Datas" :defaultValue="sctdown1" @change="changeSctdown1"></diy-selectdown>
</u-form-item>
<u-form :model="form" :rules="formRules" :errorType="['message', 'toast']" ref="formRef" class="flex diygw-form diygw-col-24">
<u-form-item class="diygw-col-24" label="下拉" prop="sctdown">
<diy-selectinput :id="`formDatasctdown`" @click="formData.showSctdown = true" class="diygw-col-24" valueName="value" labelName="label" :list="formData.sctdownDatas" placeholder="请选择" v-model="form.sctdown" type="select"></diy-selectinput>
<diy-selectdown :offset-top="15" :trigger-id="`formDatasctdown`" v-model="formData.showSctdown" valueName="value" labelName="label" :list="formData.sctdownDatas" :defaultValue="form.sctdown" @change="changeFormSctdown"></diy-selectdown>
</u-form-item>
<view class="flex flex-wrap diygw-col-24">
<view class="diygw-col-24" v-for="(subformItem, subformIndex) in form.subform" :key="subformIndex">
<u-form class="diygw-col-24" :model="form.subform[subformIndex]" :errorType="['message', 'toast']" ref="subformRef" :rules="subformItemRules">
<u-form-item class="diygw-col-24" label="下拉" prop="sct">
<diy-selectinput @click="formData.subformItemDatas[subformIndex].showSct = true" class="diygw-col-24" valueName="value" labelName="label" :list="formData.subformItemDatas[subformIndex].sctDatas" placeholder="请选择" v-model="subformItem.sct" type="select"></diy-selectinput>
<u-select mode="single-column" valueName="value" labelName="label" :list="formData.subformItemDatas[subformIndex].sctDatas" isDefaultSelect :defaultSelectValue="subformItem.sct" v-model="formData.subformItemDatas[subformIndex].showSct" @confirm="changeSubformItemSct($event, subformIndex, subformItem)"></u-select>
</u-form-item>
</u-form>
<view class="padding-xs diygw-col-24 flex justify-end">
<button @tap="upSubformItem" :data-index="subformIndex" class="diygw-btn flex-sub radius margin-xs"><text class="button-icon diy-icon-fold"></text> 上移</button>
<button @tap="downSubformItem" :data-index="subformIndex" class="diygw-btn flex-sub radius margin-xs"><text class="button-icon diy-icon-unfold"></text> 下移</button>
<button @tap="addSubformItem" :data-index="subformIndex" class="diygw-btn flex-sub radius margin-xs"><text class="button-icon diy-icon-add"></text> 新增</button>
<button @tap="delSubformItem" :data-index="subformIndex" class="diygw-btn flex-sub radius margin-xs"><text class="button-icon diy-icon-close"></text> 删除</button>
</view>
</view>
<view class="padding-xs diygw-col-24">
<button @tap="addSubformItem" class="diygw-btn diygw-col-24 radius" style="background: #07c160; color: #fff"><text class="diy-icon-add"></text>新增</button>
</view>
</view>
</u-form>
<view class="clearfix"></view>
</view>
</template>
<script>
export default {
data() {
return {
//用户全局信息
userInfo: {},
//页面传参
globalOption: {},
//自定义全局变量
globalData: {},
showSctdown1: false,
sctdown1Datas: [
{ value: '1', label: '选项一' },
{ value: '2', label: '选项二' },
{ value: '3', label: '选项三' }
],
sctdown1: '1',
subformItemData: {
showSct: false,
sctDatas: [
{ value: '1', label: '选项一' },
{ value: '2', label: '选项二' },
{ value: '3', label: '选项三' }
]
},
subformItem: {
sct: '1'
},
form: {
sctdown: '1',
subform: []
},
formRules: {},
subformItemRules: {},
formData: {
showSctdown: false,
sctdownDatas: [
{ value: '1', label: '选项一' },
{ value: '2', label: '选项二' },
{ value: '3', label: '选项三' }
],
subformItemDatas: []
}
};
},
onShow() {
this.setCurrentPage(this);
},
onLoad(option) {
this.setCurrentPage(this);
if (option) {
this.setData({
globalOption: this.getOption(option)
});
}
this.init();
},
onReady() {
this.$refs.formRef?.setRules(this.formRules);
this.initSubformData();
},
methods: {
async init() {
await this.initResetform();
},
changeSctdown1(evt) {
this.sctdown1 = evt;
},
changeFormSctdown(evt) {
this.form.sctdown = evt;
},
//初始化显示子表单数据条数
initSubformData() {
for (let i = 0; i < 1; i++) {
this.form.subform.push(JSON.parse(JSON.stringify(this.subformItem)));
this.formData.subformItemDatas.push(JSON.parse(JSON.stringify(this.subformItemData)));
}
this.initSubformValid();
},
//子表单验证
initSubformValid() {
this.$nextTick(() => {
this.$refs['subformRef']?.forEach((subform) => {
subform.setRules(this.subformItemRules);
});
});
},
//上移子表单
upSubformItem(evt) {
let { index } = evt.currentTarget.dataset;
if (index == 0) {
this.navigateTo({
type: 'tip',
tip: '已经是第一个'
});
return false;
}
this.form.subform[index] = this.form.subform.splice(index - 1, 1, this.form.subform[index])[0];
this.formData.subformItemDatas[index] = this.formData.subformItemDatas.splice(index - 1, 1, this.formData.subformItemDatas[index])[0];
this.initSubformValid();
},
//下移子表单
downSubformItem(evt) {
let { index } = evt.currentTarget.dataset;
if (index == this.form.subform.length - 1) {
this.navigateTo({
type: 'tip',
tip: '已经是最后一个'
});
return false;
}
this.form.subform[index] = this.form.subform.splice(index + 1, 1, this.form.subform[index])[0];
this.formData.subformItemDatas[index] = this.formData.subformItemDatas.splice(index + 1, 1, this.formData.subformItemDatas[index])[0];
this.initSubformValid();
},
//删除子表单
delSubformItem(evt) {
let { index } = evt.currentTarget.dataset;
this.form.subform.splice(index, 1);
this.formData.subformItemDatas.splice(index, 1);
this.initSubformValid();
},
//增加子表单
addSubformItem() {
this.form.subform.push(JSON.parse(JSON.stringify(this.subformItem)));
this.formData.subformItemDatas.push(JSON.parse(JSON.stringify(this.subformItemData)));
this.initSubformValid();
},
//验证所有的子表单
checkSubformValid() {
let flag = true;
this.$refs['subformRef']?.forEach((subform) => {
subform.validate((valid) => {
if (!valid) {
flag = false;
return false;
}
});
});
return flag;
},
changeSubformItemSct(evt, subformIndex, subformItem) {
evt.map((val, index) => {
subformItem.sct = val.value;
});
},
initResetform() {
this.initform = JSON.stringify(this.form);
//如果想给表单默认初始值,其中row为某一行数据也可能是API返回的结果集,然后给到this.form
//this.form = this.$tools.changeRowToForm(row,this.form)
},
resetForm() {
this.form = JSON.parse(this.initform);
},
async submitForm(e) {
this.$refs.formRef?.setRules(this.formRules);
this.initSubformValid();
this.$nextTick(async () => {
let subformvalid = await this.checkSubformValid();
let valid = await this.$refs.formRef.validate();
if (valid && subformvalid) {
//保存数据
let param = this.form;
let header = {
'Content-Type': 'application/json'
};
let url = '';
if (!url) {
this.showToast('请先配置表单提交地址', 'none');
return false;
}
let res = await this.$http.post(url, param, header, 'json');
if (res.code == 200) {
this.showToast(res.msg, 'success');
} else {
this.showModal(res.msg, '提示', false);
}
} else {
console.log('验证失败');
}
});
}
}
};
</script>
<style lang="scss" scoped>
.container329152 {
}
</style>