效果:
使用到的技术:
1、支持分页(下拉加载更多),这里是接口支持的分页。推荐:vue基于vant封装上拉加载/下拉刷新组件ListScroller
2、支持搜索,这个也是接口支持的。搜索支持防抖
3、多选(可扩展成支持单选)
4、通过sync修饰符绑定父子组件传参
5、请求函数和请求数据传入子组件中
components/SelectUserPopup.vue
<template> <div class="select-user-popup"> <van-popup v-model="isShow" position="bottom" :style="{ height: '100%' }"> <div class="wrapper"> <div class="search"> <van-icon name="cross" @click="handleClose" /> <van-search v-model="searchValue" :placeholder="params.placeholder||'输入姓名'" @input='handleInput' /> </div> <list-scroller ref="listScrollerRef" @on-pulldown-loading="refrash" @on-pullup-loading="loadMore"> <van-checkbox-group v-model="result"> <van-checkbox :name="item.id" v-for="item in userOptions" :key="item.id"> {{item.name}}{{item.empNo?`(${item.empNo})`:''}} <template #icon="props"> <div :class="props.checked ? 'activeIcon' : 'inactiveIcon'"><span></span></div> </template> </van-checkbox> </van-checkbox-group> </list-scroller> </div> <div class="btns"> <van-button @click="handleClose" native-type="button">取消</van-button> <van-button @click="handleConfirm" native-type="button">确定</van-button> </div> </van-popup> </div> </template> <script> /* 使用: <SelectUserPopup :selected.sync="page1Data.visitStaffList" :show.sync='isShowVisitStaffPopup' :params='visitStaffPopupParams'></SelectUserPopup> isShowVisitStaffPopup: false, // 弹层显隐 visitStaffPopupParams: { queryParams: { page: 1, pageSize: 20, keyWord: '' }, requestFn: getVisitStaffOptionsApi, // 接口函数 placeholder: '输入姓名或工号' } */ import ListScroller from '@/components/ListScroller' import debounce from 'lodash.debounce' export default { watch: { selected: { handler(selected) { this.result = selected // 场景:当进入到弹层中时,将值回显 }, immediate: true } }, computed: { isShow: { get() { this.show && this.handleInit() return this.show }, set(flag) { this.$emit('update:show', flag) } } }, props: { selected: { type: Array, require: true }, // script show: { type: Boolean, require: true }, // css params: { type: Object, require: true } // template }, data() { return { searchValue: '', // 搜索框内容 queryParams: this.params.queryParams, // 接口参数 userOptions: [], // 用户列表 totalRecords: 0, // 总条数 result: [] // 回显选中的值 } }, methods: { // 初始化(打开弹层时执行) handleInit() { this.result = this.selected // 回显选中的值 场景:当选中一项后点击取消,再次进入正常回显 this.searchValue = '' // 清空输入框 this.handleInput('') // 刷新 }, // 【取消】按钮 handleClose() { this.isShow = false }, // 【确认】按钮 handleConfirm() { this.$emit('update:selected', this.result) this.handleClose() }, // 搜索框input事件 handleInput(val) { this.$refs.listScrollerRef.$el.scrollTop = 0 this.queryParams.page = 1 this.queryParams.keyWord = val this.getUserOptions() }, // 拜访人(员工)列表 async getUserOptions() { const { queryParams, params: { requestFn } } = this const { success, data, totalRecords } = await requestFn(queryParams) if (success) { this.totalRecords = totalRecords if (this.queryParams.page === 1) { this.userOptions = data } else { this.userOptions.push(...data) } } }, // 刷新 refrash(cb) { this.queryParams.page = 1 this.getUserOptions().then(() => cb && cb()) }, // 加载更多 loadMore(cb) { const { userOptions, totalRecords } = this if (totalRecords > userOptions.length) { this.queryParams.page++ this.getUserOptions().then(() => { cb && cb() }) } else { cb && cb() } } }, created() { this.getUserOptions() this.handleInput = debounce(this.handleInput, 200) // 搜索框防抖 }, components: { ListScroller } } </script> <style lang="less" scoped> .select-user-popup { /deep/ .van-popup { box-sizing: border-box; .wrapper { height: calc(100% - 144px); padding-top: 44px; .search { position: absolute; top: 0; width: 100%; display: flex; align-items: center; padding: 4px 15px; box-sizing: border-box; > .van-icon { width: 30px; color: #333333; font-size: 20px; } .van-search { flex: 1; padding: 0; height: 36px; border-radius: 18px; overflow: hidden; background-color: #f3f6f9; .van-search__content { .van-icon { color: #8e8e93; } .van-field__control { font-size: 17px; color: #b5b5b5; } } } } .van-checkbox-group { .van-checkbox { margin-top: 20px; padding: 0 15px; .van-checkbox__label { margin-left: 20px; font-size: 16px; word-break: break-all; } // 选中和未选中样式-start .activeIcon { width: 18px; height: 18px; border: 2px solid #198cff; border-radius: 50%; box-sizing: border-box; display: flex; align-items: center; justify-content: center; > span { display: block; width: 10px; height: 10px; background: #198cff; border-radius: 50%; } } .inactiveIcon { width: 18px; height: 18px; border: 2px solid #e0e5f5; border-radius: 50%; box-sizing: border-box; } // 选中和未选中样式-end } } } > .btns { margin-top: 15px; padding: 0 15px; background-color: #fff; display: flex; justify-content: space-between; box-sizing: border-box; > .van-button { width: calc(50% - 8px); height: 38px; line-height: 38px; border-radius: 19px; font-size: 14px; text-align: center; } > .van-button:first-child { background-color: #e0e5f5; color: #374e64; } > .van-button:last-child { background-color: #1288fe; color: #fff; } } } } </style>
css:
<style lang="less" scoped> .select-user-popup { /deep/ .van-popup { box-sizing: border-box; .search { display: flex; align-items: center; padding: 4px 15px; box-sizing: border-box; > .van-icon { width: 30px; color: #333333; font-size: 20px; } .van-search { flex: 1; padding: 0; height: 36px; border-radius: 18px; overflow: hidden; background-color: #f3f6f9; .van-search__content { .van-icon { color: #8e8e93; } .van-field__control { font-size: 17px; color: #b5b5b5; } } } } .van-checkbox-group { .van-checkbox { margin-top: 20px; padding: 0 15px; .van-checkbox__label { margin-left: 20px; font-size: 16px; word-break: break-all; } // 选中和未选中样式-start .activeIcon { width: 18px; height: 18px; border: 2px solid #198cff; border-radius: 50%; box-sizing: border-box; display: flex; align-items: center; justify-content: center; > span { display: block; width: 10px; height: 10px; background: #198cff; border-radius: 50%; } } .inactiveIcon { width: 18px; height: 18px; border: 2px solid #e0e5f5; border-radius: 50%; box-sizing: border-box; } // 选中和未选中样式-end } } .btns { background-color: #fff; display: flex; justify-content: space-between; position: fixed; width: 100%; box-sizing: border-box; bottom: 47px; padding: 0 15px; > .van-button { width: calc(50% - 20px); height: 38px; line-height: 38px; border-radius: 19px; font-size: 14px; text-align: center; } > .van-button:first-child { background-color: #e0e5f5; color: #374e64; } > .van-button:last-child { background-color: #1288fe; color: #fff; } } } } </style>
(父组件)使用:
<template> <van-form class="add" @submit="onSubmit"> <van-field v-model='params.projectName' placeholder="选择项目" readonly is-link @click="$refs.projectRadioRef.handleOpen(params.projectId)" /> <van-field v-model="contactDesc" placeholder="选择联系人" readonly is-link @click="handleOpenContactPopup" /> <van-field v-model="workDesc" placeholder="添加协作人" readonly is-link @click="isShowWorkPopup=true" /> <div style="margin: 16px;"> <van-button native-type="submit" :loading='loading'>提交</van-button> </div> <!-- 项目单选弹框 --> <ProjectRadio @projectRadio='handleProjectRadio' ref='projectRadioRef'></ProjectRadio> <!-- 联系人弹层 --> <SelectUserPopup v-if="contactParams.queryParams.projectId>0" :selected.sync="params.contactIdList" :show.sync='isShowContactPopup' :params='contactParams'> </SelectUserPopup> <!-- 协作人弹层 --> <SelectUserPopup :selected.sync="params.workPerson" :show.sync='isShowWorkPopup' :params='workParams'></SelectUserPopup> </van-form> </template> <script> import { ddNavSetTitle, ddNavSetRight, isDingTalk } from '@/utils/dd' import ProjectRadio from '@/components/ProjectRadio' import SelectUserPopup from '@/components/SelectUserPopup' import { getVisitPersonOptionsApi, getVisitStaffOptionsApi } from '@/api/visitManage' export default { data() { return { isShowContactPopup: false, // 联系人弹层显隐 // 联系人请求参数 contactParams: { queryParams: { page: 1, pageSize: 30, keyWord: '', projectId: -1 }, requestFn: getVisitPersonOptionsApi }, isShowWorkPopup: false, // 协作人弹层显隐 // 协作人请求参数 workParams: { queryParams: { page: 1, pageSize: 30, keyWord: '' }, requestFn: getVisitStaffOptionsApi, // 接口函数 placeholder: '输入姓名或工号' }, params: { projectId: -1, // 项目Id 52883 projectName: '', // 项目名称----仅做回显使用 contactIdList: [], // 联系人 workPerson: [] // 协作人 }, loading: false // 提交loading } }, computed: { // 联系人选中情况 contactDesc() { const { contactIdList } = this.params return contactIdList.length ? `已选${contactIdList.length}个` : '' }, // 协作人选中情况 workDesc() { const { workPerson } = this.params return workPerson.length ? `已选${workPerson.length}个` : '' } }, methods: { // 项目单选弹层【确定】按钮 handleProjectRadio(val) { if (val.length) { const { projectId, projectName } = val[0] if (projectId !== this.params.projectId) this.params.contactIdList = [] // 清空联系人列表 this.params.projectId = projectId this.params.projectName = projectName } else { this.params.contactIdList = [] // 清空联系人列表 this.params.projectId = -1 this.params.projectName = '' } }, // 打开联系人弹层 handleOpenContactPopup() { const { projectId } = this.params if (projectId <= 0) { this.$toast('请先选择项目') } else { this.contactParams.queryParams.projectId = projectId this.isShowContactPopup = true } }, // 提交 onSubmit(values) { console.log('submit', values, this.params) } }, created() { ddNavSetTitle('新建日程') }, components: { ProjectRadio, SelectUserPopup } } </script> <style lang="less" scoped> @import './index.less'; </style>
注意:
1、联系人基于项目id,初始化时SelectUserPopup组件内直接在created中请求数据,但此时projectId的值还没有,所以用v-if做了判断,可以在子组件内做这一块的优化
2、选择项目(单选)组件见此篇:vue移动端封装项目单选组件ProjectRadio(前端懒加载)
页面结构: