vue移动端封装项目单选组件ProjectRadio(前端懒加载)
效果:
所具备的功能:
1、切换学年
2、项目单选
3、前端懒加载(前端分页)
4、打开弹框可以回显上一次选中的项目,点击取消不进行操作
5、通过isRadioChange控制,选中后,再次点击可取消
components/ProjectRadio.vue
<template> <div class="public-project-radio"> <van-popup class="project-radio" v-model="isShow" position="bottom" :style="{ height: '100%' }"> <div class="close"> <van-icon name="cross" @click="handleCancel" /> </div> <div class="yearList"> <year-select :yearOptions="yearOptions" :isCheckId="isCheckId" @on-select="selectChange"></year-select> </div> <!-- 搜索 --> <div class="search"> <van-search v-model="searchValue" placeholder="输入项目名称" @input='handleInput' /> </div> <div class="van-list-box"> <van-list ref="scrollContent" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-radio-group v-model="radioResult" @change='handleChange'> <van-radio v-for='ele in personData' :key='ele.value' :name="ele.projectId" @click="handleClick"> {{ele.projectName}} <template #icon="props"> <div :class="props.checked ? 'activeIcon' : 'inactiveIcon'"><span></span></div> </template> </van-radio> </van-radio-group> </van-list> </div> <div class="van-popup-btns"> <van-button native-type="button" @click="handleCancel">取消</van-button> <van-button native-type="button" @click="handleConfirm">确定</van-button> </div> </van-popup> </div> </template> <script> import clonedeep from 'lodash.clonedeep' import { getProjectOptions, getSchoolYearOptions } from '@/api/select' import YearSelect from '@/components/YearSelect' const LOAD_NUM = 20 export default { name: 'ProjectRadio', components: { YearSelect }, data() { return { isShow: false, isRadioChange: false, // 判断单选的状态有没有变化 radioResult: '', // 回显 searchValue: '', // 提供v-model响应参数 personData: [], // 用于循环展示的list数据 initPersonData: [], // 备份:接口数据 searchpersonData: [], // 搜索到的数据 tileList: [], // 平铺数据 yearOptions: [], isCheckId: undefined, finished: false, allProjectList: [] // 所有的学年对应的项目集合 // list: [], } }, created() { this.fetchSchoolYearOptions().then(() => { this.getData() }) }, methods: { onLoad() { if (this.searchValue) { this.loadMoreData(this.searchpersonData) } else { this.loadMoreData(this.initPersonData) } }, // 加载更多数据到select框 loadMoreData(dataList) { const renderedLen = this.personData.length // 已渲染的下拉列表长度 const totalLen = dataList.length // 全部数据源的长度(总全部或者搜索到的全部) let addList = [] // 如果 下拉列表已渲染的数据 < 全部数据 (意味着没有全部渲染完所有数据) if (renderedLen < totalLen) { // 如果 下拉列表已渲染的数据 + 每次想要渲染的数量 <= 全部数据 // (如果小于等于 slice方法第二个参数能取到) if (renderedLen + LOAD_NUM <= totalLen) { addList = dataList.slice(renderedLen, renderedLen + LOAD_NUM) this.finished = false } else { // 如果长度不够 取余数为 slice最后一个参数 addList = dataList.slice( renderedLen, renderedLen + (totalLen % LOAD_NUM) ) this.finished = true } // 把截取到的后30条拼接在循环的列表尾部 this.personData = this.personData.concat(addList) } }, // 单选radio选中后,再次点击需要可以取消选择功能 handleChange() { this.isRadioChange = true }, fetchSchoolYearOptions() { return getSchoolYearOptions().then(({ data }) => { const options = data.map(({ name, id, isCheck }) => { if (isCheck) { this.isCheckId = id } return { name: `${name}学年`, value: id } }) this.yearOptions = options let years = options.map(item => item.value) this.getAllProjectList(years) }) }, // 获取所有的学年对应的项目集合 getAllProjectList(years) { let allProjectList = [] years.forEach(async year => { let res = await getProjectOptions({ isUser: 1, year }) let { projectLetterSelect } = res.data allProjectList = allProjectList.concat(projectLetterSelect) this.allProjectList = allProjectList }) }, selectChange(val) { this.isCheckId = val this.searchValue = '' this.handleInput('') this.getData() document.querySelector('.van-list-box').scrollTop = 0 }, handleClick() { if (!this.isRadioChange) { this.radioResult = '' } this.isRadioChange = false }, // 打开弹框 handleOpen(projectId) { this.radioResult = projectId this.isShow = true }, // 关闭弹框 handleCancel() { this.isShow = false }, // 确定 handleConfirm() { let tileList = clonedeep(this.allProjectList) let result = tileList.filter(item => item.projectId === this.radioResult) this.$emit('projectRadio', result) this.handleCancel() }, // 搜索 handleInput(val) { document.querySelector('.van-list-box').scrollTop = 0 if (val) { this.searchpersonData = this.initPersonData.filter(item => item.projectName.match(val) ) this.personData = this.initPersonData .filter(item => item.projectName.match(val)) .slice(0, LOAD_NUM) } else { this.searchpersonData = [] this.personData = this.initPersonData.slice(0, LOAD_NUM) this.finished = false } }, // 请求数据 async getData() { const { isCheckId } = this let res = await getProjectOptions({ isUser: 1, year: isCheckId }) if (res.code === '200') { let { projectLetterSelect } = res.data let arr = projectLetterSelect.sort((a, b) => a.letter.localeCompare(b.letter) ) // 按字母排序 // 假数据 // for(let i = 0; i <=300; i++){ // arr.push({ // "projectId": 1, // "projectName": `${i}阿坝县中学2023届(高一e网通)`, // "letter": "Z", // "isChecked": false // }) // } this.initPersonData = arr // 存储原始数据 this.personData = arr.slice(0, LOAD_NUM) this.tileList = projectLetterSelect } } } } </script>
css:
<style lang="less" scoped> .public-project-radio { /deep/ .project-radio { box-sizing: border-box; padding-top: 135px; .close { height: 30px; position: fixed; top: 5px; left: 15px; font-size: 16px; z-index: 1005; } .yearList { position: fixed; top: 40px; left: 0px; font-size: 16px; z-index: 1005; .year-select { padding-top: 5px; padding-bottom: 15px; } } // 选中和未选中样式-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 .search { display: flex; align-items: center; padding: 4px 15px; position: fixed; top: 80px; width: 100%; box-sizing: border-box; background-color: #fff; z-index: 1001; > .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 { padding-right: 12px; .van-icon { color: #8e8e93; } .van-field__control { font-size: 17px; color: #b5b5b5; } } } } .van-list-box { height: calc(100% - 100px); overflow: auto; .van-radio-group { color: red; .van-radio { margin-top: 20px; padding: 0 15px; .van-radio__label { margin-left: 20px; font-size: 16px; } } .van-radio:first-child { margin-top: 0; } .van-radio:last-child { margin-bottom: 10px; } } } .van-popup-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: 150px; 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>
使用:
引入、注册:
import ProjectRadio from '@/components/ProjectRadio'
components: { ProjectRadio, SelectUserPopup }
DOM:(通过ref控制子组件的打开)
<van-field v-model='params.projectName' placeholder="选择项目" readonly is-link @click="$refs.projectRadioRef.handleOpen(params.projectId)" /> <!-- 项目单选弹框 --> <ProjectRadio @projectRadio='handleProjectRadio' ref='projectRadioRef'></ProjectRadio>
data:
params: { projectId: -1, // 项目Id 52883 projectName: '', // 项目名称----仅做回显使用 contactIdList: [], // 联系人 }
methods:(联系人options是基于项目id的,所以切换项目时要清空已选的联系人)
// 项目单选弹层【确定】按钮 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 = '' } }
YearSelect.vue
<!-- 学年横向滚动 --> <template> <div class="year-select"> <div v-for="(item, index) in yearOptions" :key="index" :class="{ 'tag-item': true, 'selected-item': currentIndex === index }" @click="() => handleItemClick(item, index)">{{ item.name }}</div> </div> </template> <script> export default { name: 'YearSelect', model: { prop: 'value', event: 'on-change' }, components: {}, props: { yearOptions: { type: Array, default: () => [] }, isCheckId: { type: Number }, value: { type: [String, Number] }, mode: { type: String, default: 'radio' // 'radio', 'checkbox' } }, data() { return { currentIndex: undefined } }, watch: { isCheckId: { handler: function (val, oldVal) { if (val === undefined) { this.currentIndex = undefined } else { this.currentIndex = this.yearOptions.map(n => n.value).indexOf(val) } }, immediate: true } }, computed: {}, mounted() {}, methods: { handleItemClick(item, index) { const { mode } = this if (mode === 'radio') { this.currentIndex = index // console.log(item) this.$emit('on-select', item.value) } } } } </script> <style lang='less' scoped> .year-select { width: 95%; padding-left: 15px; padding-bottom: 5px; overflow-x: scroll; overflow-y: hidden; white-space: nowrap; &::-webkit-scrollbar { display: none; } .tag-item { display: inline; padding: 6px 20px; border-radius: 15px; margin-right: 10px; background: #e0e5f5; font-size: 12px; } .selected-item { background: @theme-color; color: #fff; } } </style>