封装一个小程序选择器(可多选、单选、搜索)

组件

vue 复制代码
<template>
  <view class="popup" v-show="show">
    <view class="bg" @tap="cancelMultiple"></view>
    <view class="selectMultiple">
      <view class="multipleBody">
        <view class="title">
          <view class="close" @tap="cancelMultiple">
            取消
          </view>
          <view class="name">
            <!-- cancelButton="none" 不展示取消按钮-->
            <uni-search-bar
                @input="updateList"
                @confirm="onSearchConfirm"
                cancelButton="none">
            </uni-search-bar>

          </view>
          <view class="confirm" @tap="confirmMultiple">
            确认
          </view>
        </view>
        <view class="list">
          <view class="mask mask-top"></view>
          <view class="mask mask-bottom"></view>
          <scroll-view class="diet-list" scroll-y="true">
            <view v-for="(item, index) in list" :class="['item', item.selected ? 'checked' : '']" @tap="onChange(index, item)">
              <span style="font-size: 16px;">{{item.label}}</span>
              <view class="icon" v-show="item.selected">
                <icon type="success_no_circle" size="16" color="#2D8DFF"/>
              </view>
            </view>
          </scroll-view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name:"my-curry-multi-select",
  data() {
    return {
      // 选中值
      value: [],
      // 选中列表
      selected: [],
      // 列表数据
      list: [],
      originList: [],
    };
  },
  props: {
    // 是否显示
    show: {
      type: Boolean,
      default: false
    },
    // 标题
    title: {
      type: String,
      default: ''
    },
    //数据列表
    columns: {
      type: Array,
      default: []
    },
    // 默认选中
    defaultIndex: {
      type: Array,
      default: [],
    },
    isMultiSelect: {
      type: Boolean,
      default: true
    },
  },
  watch: {
    // 监听是否显示
    show(val) {
      if(val) {
        this.openMultiple();
      }
    }
  },
  methods: {
    sortListWithSelectedFirst(list) {
      return list.slice().sort((a, b) => {
        const aSelected = a.selected ? 1 : 0;
        const bSelected = b.selected ? 1 : 0;
        return bSelected - aSelected;
      });
    },
    updateList(str) {
      this.list.map(e => {
        this.originList.map(item => {
          if (e.selected && item.value === e.value) {
            item.selected = true
          }
        })
      })

      if (str === null || str === undefined || str === '') {
        this.list = JSON.parse(JSON.stringify(this.originList))
      } else {
        const filtered = this.originList.filter(e => e.label.indexOf(str) > -1 || e.selected);
        this.list = this.sortListWithSelectedFirst(filtered);
      }
    },
    // 新增:处理搜索确认事件
    onSearchConfirm(e) {
      const searchValue = e.value || e;

      // 先更新列表
      this.updateList(searchValue);

      // 如果有搜索内容且搜索结果不为空,自动选择第一个未选中的项目
      if (searchValue && this.list.length > 0) {
        // 找到第一个未选中的项目
        const firstUnselectedIndex = this.list.findIndex(item => !item.selected);

        if (firstUnselectedIndex !== -1) {
          // 自动选择第一个未选中的项目
          this.onChange(firstUnselectedIndex, this.list[firstUnselectedIndex]);
        }
      }
    },
    // 列点击事件
    onChange(index, item) {
      // 单选
      if (!this.isMultiSelect) {
        this.value = [];
        this.selected = [];
        this.value.push(item.value.toString());
        this.selected.push({
          label: item.label,
          value: item.value,
        });
        return this.$emit("confirm", {selected: this.selected, value: this.value});
      }
      // 是否已选中
      if(this.value.indexOf(item.value.toString()) >= 0) {
        this.list[index].selected = false;
      } else {
        this.list[index].selected = true;
      }

      // 筛选已勾选数据
      this.value = [];
      this.selected = [];
      this.list.forEach((col_item, col_index) => {
        if(col_item.selected) {
          this.value.push(col_item.value.toString());
          this.selected.push({
            label: col_item.label,
            value: col_item.value,
          });
        }
      });
      this.list = this.sortListWithSelectedFirst(this.list);
      this.$emit("change", {selected: this.selected, value: this.value});
    },
    // 弹出框开启触发事件
    openMultiple() {
      // 初始化列表数据,默认勾选数据
      this.value = this.defaultIndex;
      this.columns.forEach((item, index) => {
        this.$set(item, "selected", false);
        if(this.value.indexOf(item.value.toString()) >= 0) {
          item.selected = true;
        }
      });
      this.originList = Object.assign([], this.columns);
      this.list = this.sortListWithSelectedFirst(JSON.parse(JSON.stringify(this.originList)));
    },
    // 确认
    confirmMultiple() {
      this.$emit("confirm", {selected: this.selected, value: this.value});
    },
    // 关闭/取消
    cancelMultiple() {
      this.$emit("cancel");
    },
  }
}
</script>

<style scoped lang="scss">
.popup {
  width: 100%;
  height: 100vh;
  position: fixed;
  z-index: 99999;
  left: 0;
  top: 0;

  .bg {
    width: 100%;
    height: 100%;
    background-color: rgba(black, .5);
  }
}
.selectMultiple {
  transition: none !important;
  will-change: transform;
  width: 100%;
  position: absolute;
  left: 0;
  top: 0;
  height: 50vh;
  background-color: white;
  border-radius: 0 0 20rpx 20rpx;
  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  overflow: hidden;

  .multipleBody {
    width: 100%;
    height: 100%;
    padding: 30rpx;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    .title {
      flex-shrink: 0;
      font-size: 28rpx;
      display: flex;
      flex-direction: row;
      align-items: center; /* 添加这一行 */

      .close {
        width: 80rpx;
        text-align: left;
        opacity: .5;
      }
      .name {
        width: 530rpx;
        text-align: center;
        overflow: hidden;
        display: -webkit-box;
        -webkit-box-orient:vertical;
        -webkit-line-clamp:1;
      }
      .confirm {
        width: 80rpx;
        text-align: right;
        color: #2D8DFF;
      }
    }
    .list {
      flex: 1;
      overflow: hidden;
      width: 100%;
      padding-top: 30rpx;
      position: relative;

      .mask {
        width: 100%;
        height: 60rpx; // 减小渐变区域
        position: absolute;
        left: 0;
        z-index: 2;
        pointer-events: none;

        &.mask-top {
          top: 30rpx;
          background-image: linear-gradient(to bottom, #fff, rgba(#fff, 0));
        }
        &.mask-bottom {
          bottom: 0;
          background-image: linear-gradient(to bottom, rgba(#fff, 0), #fff);
        }
      }

      .diet-list {
        height: 100%;
        max-height: none;
      }

      .item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20rpx 0;
        border-bottom: 1px solid rgba(#000, 0.05);

        span {
          flex: 1;
          font-size: 30rpx;
          text-align: center;
        }

        .icon {
          width: 32rpx;
          height: 32rpx;
        }
        &.checked {
          color: #2D8DFF;
        }
        &:last-child {
          border-bottom: none;
          margin-bottom: 60rpx;
        }
        &:first-child {
          margin-top: 60rpx;
        }
      }
    }
  }
}
</style>

使用

vue 复制代码
<template>
  <view class="container">
    <view>
      <uni-forms ref="taskForm" :model="taskForm" labelWidth="80px">
        <!-- 任务类型单选 -->
        <uni-forms-item label="任务类型" name="type">
          <view class="item">
            <view :class="['select', taskForm.type ? 'selected' : '']"
                  @tap="openTypeSelectionBox(taskForm.type)">
              {{ taskForm.type ? getTaskTypeName(taskForm.type) : '请选择任务类型' }}
            </view>
            <!-- 如果有内容显示关闭图标 -->
            <uni-icons v-if="taskForm.type !== ''" type="clear" size="24" color="#c0c4cc" class="close-btn"
                       @tap="clearType"></uni-icons>
            <!-- 如果没有内容显示下拉图标 -->
            <uni-icons v-else type="pulldown" size="24" color="#c0c4cc" class="close-btn"
                       @tap="openTypeSelectionBox(taskForm.type)"></uni-icons>

            <my-curry-multi-select title="请选择" :show="taskTypeShow" :columns="taskTypeList"
                                   :defaultIndex="defaultTaskTypeIndex"
                                   :isMultiSelect="false"
                                   @confirm="confirmType($event)"
                                   @cancel="taskTypeShow = false"></my-curry-multi-select>
          </view>
        </uni-forms-item>
        <!-- 负责人多选 -->
        <uni-forms-item label="负责人" name="personInChargeIds">
          <view class="item">
            <view :class="['select', (Array.isArray(taskForm.personInChargeIds) && taskForm.personInChargeIds.length > 0) ? 'selected' : '']"
                  @tap="openPersonInChargeIdsMultiSelectionBox(taskForm.personInChargeIds)">
              {{ (Array.isArray(taskForm.personInChargeIds) && taskForm.personInChargeIds.length > 0)
                ? getUserNamesByIds(taskForm.personInChargeIds)
                : '请选择负责人' }}
            </view>
            <uni-icons v-if="Array.isArray(taskForm.personInChargeIds) && taskForm.personInChargeIds.length > 0"
                       type="clear" size="24" color="#c0c4cc" class="close-btn"
                       @tap="clearPersonInChargeIds"></uni-icons>
            <uni-icons v-else type="pulldown" size="24" color="#c0c4cc" class="close-btn"
                       @tap="openPersonInChargeIdsMultiSelectionBox(taskForm.personInChargeIds)"></uni-icons>

            <my-curry-multi-select title="请选择" :show="taskTaskPersonInChargesShow"
                                   :columns="userList"
                                   :defaultIndex="defaultTaskPersonInChargesIndex"
                                   :isMultiSelect="true"
                                   @confirm="confirmPersonInChargeIds($event)"
                                   @cancel="taskTaskPersonInChargesShow = false"></my-curry-multi-select>
          </view>
        </uni-forms-item>

      </uni-forms>
      <button type="primary" @click="submit">提交</button>
    </view>
  </view>
</template>

<script>
import myCurryMultiSelect from "@/components/curry-multi-select/my-curry-multi-select.vue";

export default {
  components: {myCurryMultiSelect},
  data() {
    return {
      // 当前正选择哪个任务类型元素
      defaultTaskTypeIndex: [],
      // 任务类型列表,单选
      taskTypeList: [
        {"label": "指派任务", "value": 1},
        {"label": "设计任务", "value": 2},
        {"label": "代办", "value": 2}
      ],
      // 是否展示任务类型下拉选项
      taskTypeShow: false,

      // 当前正选择哪些任务负责人元素
      defaultTaskPersonInChargesIndex: [],
      // 是否展示任务负责人下拉选项
      taskTaskPersonInChargesShow: false,
      // 用户列表,多选
      userList: [
        { value: 1, label: '张三' },
        { value: 2, label: '李四' },
        { value: 3, label: '王五' },
      ],

      // 表单数据
      taskForm: {
        taskTitle: '',
        insertUser: '',
        type: 2,
        personInChargeIds: [1,2],
        personInChargeId: null,
        taskContent: '',
        requiredCompletionTime: '',
        actualCompletionTime: '',
        weight: '',
        weightScore: '',
        timeoutStatus: '0',
        participants: [],
        taskPictureUrl: [],
        taskFileUrl: [],
        useSchedule: '1',
        standardWorkingHours: '',
      },
      // 表单验证规则
      rules: {
        type: {
          rules: [{required: true, errorMessage: '任务类型不能为空'}]
        },
        personInChargeIds: {
          rules: [
            {
              required: true,
              errorMessage: '负责人不能为空',
              validateFunction: (rule, value) => {
                return Array.isArray(value) && value.length > 0;
              }
            }
          ]
        },
      },
    }
  },
  onLoad() {

  },
  created() {

  },
  onReady() {
    this.$refs.taskForm.setRules(this.rules)
  },
  methods: {
    // =====单选====
    // 打开任务类型选择框
    openTypeSelectionBox(val) {
      console.log('执行了openTypeSelectionBox,展开选择框时val值是:', val)
      this.defaultTaskTypeIndex = val !== '' ? [String(val)] : [];
      console.log('this.defaultTaskTypeIndex',this.defaultTaskTypeIndex)
      this.taskTypeShow = true;
    },
    // 清空类型选择框
    clearType() {
      this.defaultTaskTypeIndex = [];
      this.taskForm.type = '';
    },
    // 获取任务类型名称,把值转换为名称显示出来
    getTaskTypeName(value) {
      const option = this.taskTypeList.find(item => String(item.value) === String(value));
      return option ? option.label : '请选择任务类型';
    },
    // 确认选择任务类型
    confirmType(e) {
      // e是一个数组
      this.taskForm.type = e.value[0];
      this.taskTypeShow = false;
    },


    // =====多选====
    // 打开负责人多选框
    openPersonInChargeIdsMultiSelectionBox(val) {
      console.log('执行了openPersonInChargeIdsMultiSelectionBox,展开选择框时val值是:', val)
      this.defaultTaskPersonInChargesIndex = Array.isArray(val) ? val.map(item => String(item)) : [];
      console.log('this.defaultTaskPersonInChargesIndex', this.defaultTaskPersonInChargesIndex)
      this.taskTaskPersonInChargesShow = true;
    },
    // 清空负责人选择框
    clearPersonInChargeIds() {
      this.defaultTaskPersonInChargesIndex = [];
      this.taskForm.personInChargeIds = null; // 继续保持你的设定
    },

    // 获取任务负责人名称,把值转换为名称显示出来
    getUserNamesByIds(values) {
      if (!Array.isArray(values) || values.length === 0) return '请选择负责人';
      const labels = values.map(value => {
        const option = this.userList.find(item => String(item.value) === String(value));
        return option ? option.label : value;
      });
      return labels.join(',');
    },
    // 确认选择任务负责人
    confirmPersonInChargeIds(e) {
      // e是一个数组
      this.taskForm.personInChargeIds = e.value;
      this.taskTaskPersonInChargesShow = false;
    },

    // 提交表单
    submit() {
      console.log('提交时表单数据是:', this.taskForm)
      // 就是上面这个写法有一个问题,就是提交的时候,选择框的绑定的都是字符串。就是是数值,也是转为字符串的。但是前段字符串,后端用Long也能接收。所以问题不大。
      this.$refs.taskForm.validate().then(res => {
        this.$modal.msgSuccess("修改成功")
      })
    },
  }
}
</script>

<style lang="scss">
.item {
  width: 100%;
  padding: 0;
  position: relative;
  display: flex;
  align-items: center;
  height: 35px;

  .select {
    flex-grow: 1;
    border: 1px solid #dadbde;
    padding: 4px 9px;
    border-radius: 4px;
    font-size: 12px;
    box-sizing: border-box;
    color: #6a6a6a;
    line-height: 25px;
    height: 100%;
    overflow: hidden;

    &.selected {
      color: black;
      font-size: 15px;
    }
  }

  .close-btn {
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    color: red;
    cursor: pointer;
  }
}
</style>

效果

效果:

个人站点链接

我的博客链接:https://blog.yimengtut.online/

相关推荐
扛枪的书生36 分钟前
AD 横向移动-SMB 中继攻击
windows·渗透·kali·域渗透
疯狂的沙粒4 小时前
uniapp开发企业微信小程序时 wx.qy.login 在uniapp中使用的时候,需要导包吗?
前端·javascript·微信小程序·小程序·uni-app
我是菜鸟0713号6 小时前
基于 GitLab CI + Inno Setup 实现 Windows 程序自动化打包发布方案
windows·ci/cd·gitlab
铭记北宸7 小时前
使用微软最近开源的WSL在Windows上优雅的运行Linux
windows·microsoft·开源·wsl
yangshuo12818 小时前
AugmentFree:解除 AugmentCode 限制的终极方案 如何快速清理vscode和AugmentCode缓存—windows端
windows·vscode·缓存·auigment
foDol11 小时前
windows系统下通过visual studio使用clang tooling
ide·windows·visual studio
书山有路勤为径~11 小时前
第三章 windows远程连接ubuntu
linux·windows·ubuntu
疯狂的沙粒12 小时前
uniapp 开发企业微信小程序时,如何在当前页面真正销毁前或者关闭小程序前调用一个api接口
微信小程序·小程序·uni-app
魔术师ID13 小时前
微信小程序学习目录
学习·微信小程序·小程序
码农捻旧16 小时前
MySQL 9.3 超详细下载安装教程(Windows版)附图文说明
数据库·windows·mysql·adb·程序员创富