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>
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>

 

 

posted @ 2022-03-31 09:35  吴小明-  阅读(356)  评论(0编辑  收藏  举报