效果:

所具备的功能:
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>
View Code
使用:
引入、注册:
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>