Vue3 实现 勾选 + 列表拖拽 + 搜索列表

左侧勾选对应右侧显示
右侧删除对应左侧取消勾选

效果图:
在这里插入图片描述

<template>
  <el-dialog v-model="show" title="选择表头显示内容" draggable :close-on-click-modal="false" 
    @close="handleCloseDialog">
    <div class="view">
      <!-- 左边勾选区域 -->
      <div class="view-left">
        <div class="title">
          可选属性({{items.length}})
        </div>
        <div class="left-content">
          <div class="checkAll">
            <el-checkbox v-model="allChecked" :indeterminate="isIndeterminate" @change="toggleAll">
              全选
            </el-checkbox>
          </div>
          <div class="left-content-select">
            <div v-for="(item, index) in items" :key="index" class="item">
              <label>
                <input type="checkbox" v-model="item.checked" @change="toggleItem(item)" />
                {{ item.label }}
              </label>
            </div>
          </div>
        </div>
      </div>
      <!-- <label>
        <input type="checkbox" v-model="allChecked" @change="toggleAll"> 全选
      </label> -->
      <!-- 右边操作区域 -->
      <div class="view-right">
        <div class="title">
          已选属性<span>({{selectedItems.length}})</span>
        </div>

        <div class="right-content">
          <input type="text" v-model="searchText" placeholder="搜索..." class="search" />
          <draggable v-model="selectedItems" item-key="id" @end="handleDrag"
          :ghost-class="'dragging-ghost'"
          :chosen-class="'dragging-chosen'"
          :drag-class="'dragging-original'"
          class="list">
            <template #item="{ element }">
              <div class="item-right">
                <div v-show="element.label.includes(searchText)" class="item">
                  <div class="icon-left">
                    <el-icon><Grid /></el-icon>
                    <span>{{ element.label }}</span>
                  </div>
                  <div class="icon-right1">
                    <el-icon @click="topItem(element)" class="top">
                      <Top />
                    </el-icon>
                    <el-icon @click="deleteItem(element)" class="delete">
                      <Delete />
                    </el-icon>
                  </div>
                  <!-- <div>
              <button @click="topItem(element)" class="btn">置顶</button>
              <button @click="deleteItem(element)" class="btn">删除</button>
            </div> -->
                </div>
              </div>

            </template>
          </draggable>
        </div>
      </div>
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleCloseDialog">取消</el-button>
        <el-button type="primary" @click="handleSubmitDialog">
          确认
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script setup>
import { computed, ref, toRefs } from 'vue'
import draggable from 'vuedraggable'
import { formPropertySortSave } from '@/api/viewlist'
import { ElMessage } from 'element-plus'
const props = defineProps({
  visible: {
    type: Boolean,
    default: true,
  },
  itemsArr: {
    type: Array,
    default: true,
  }
})

// 创建本地副本
const items = ref(props.itemsArr)

// 监听props变化更新本地值
watch(() => props.itemsArr, (newVal) => {
  items.value = newVal
  console.log(items.value);
})


const selectedItems = ref([])

const searchText = ref('')

let show = toRefs(props).visible

const $emit = defineEmits(['closeShowDialog','reqGetlist','reqGetProjectViews'])
const handleCloseDialog = () => {
  $emit('closeShowDialog')
}
const handleSubmitDialog = async () => { 
  // console.log(selectedItems.value);
  const sortArr = selectedItems.value.map((item, index) => ({
    // ...item,
    formPropertyId:item.formPropertyId,
    sort: index + 1
  }));
  const sortArr1 = JSON.stringify(
    {
    "formPropertyDtos": sortArr
    }
  )
  // console.log(sortArr);
  const res = await formPropertySortSave(sortArr1)
  // console.log(res);
  $emit('closeShowDialog')
  $emit('reqGetlist')
  $emit('reqGetProjectViews')
  ElMessage.success('保存成功!')
}
// 全选计算属性
const allChecked = computed({
  get: () =>
    items.value.length > 0 && items.value.every((item) => item.checked),
  set: (value) => toggleAll(value),
})

// 监听全选框按钮  立即监听 直接将左侧可选属性赋值给右侧已选属性
watch(() => allChecked.value, (newVal) => { 
  selectedItems.value = [...items.value]
},
  {
    immediate:true,
  }
)

// 全选/全不选
const toggleAll = (checked) => {
  items.value.forEach((item) => {
    item.checked = checked
  })
  selectedItems.value = checked ? [...items.value] : []
}

// 单个选项切换
const toggleItem = (item) => {
  if (item.checked) {
    if (!selectedItems.value.some((i) => i.id === item.id)) {
      selectedItems.value.push(item)
    }
  } else {
    const index = selectedItems.value.findIndex((i) => i.id === item.id)
    if (index > -1) selectedItems.value.splice(index, 1)
  }
}

// 删除项
const deleteItem = (item) => {
  const index = selectedItems.value.findIndex((i) => i.id === item.id)
  if (index > -1) {
    selectedItems.value.splice(index, 1)
    items.value.find((i) => i.id === item.id).checked = false
  }
}

// 置顶项
const topItem = (item) => {
  const index = selectedItems.value.findIndex((i) => i.id === item.id)
  if (index > -1) {
    selectedItems.value.splice(index, 1)
    selectedItems.value.unshift(item)
  }
}

// 拖拽处理
const handleDrag = (val) => {
  // v-model自动处理顺序,无需额外代码
}

</script>

<style lang="scss" scoped>
.container {
  display: flex;
  gap: 20px;
  padding: 20px;
}

.left,
.right {
  flex: 1;
  border: 1px solid #ddd;
  padding: 15px;
  border-radius: 8px;
}


.btn {
  margin-left: 8px;
  padding: 4px 8px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn:hover {
  background: #0056b3;
}

.view {
  display: flex;

  .title {
    font-size: 16px;
    font-weight: 600;
  }

  .view-left {
    flex: 68%;
    margin-right: 20px;
    .left-content {
      width: 100%;
      // height: 100%;
      height: 370px;
      border: 1px solid #e0e0e0;
      border-radius: 4px;
      margin: 6px auto;
      padding: 10px;

      .checkAll {
        padding-left: 6px;
      }

      .left-content-select {
        display: flex;
        flex-wrap: wrap;

        .item {
          display: flex;
          flex-wrap: wrap;
          justify-content: space-between;
          align-items: center;
          padding: 8px;
          margin: 4px 0;
          background: #ffffff;
          border-radius: 4px;

          input {
            vertical-align: middle;
          }
        }
      }
    }
  }

  .view-right {
    flex: 30%;

    .right-content {
      width: 100%;
      border: 1px solid #e0e0e0;
      border-radius: 4px;
      margin: 6px auto;
      padding: 10px;
      .list{
        height: 300px;
        overflow: auto;
      }
      .search {
        width: 100%;
        padding: 8px;
        margin-bottom: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
      }

      .item-right {
        :hover{
            background: #f0f0f0;
            font-size: #fff;
          }
        // align-items: center;
        .top {
          // margin-left: 8px;
          // cursor: pointer;
        }

        .icon-right {
          // display: flex;
          // flex-direction: flex-end;
        }

        .item {
          display: flex;
          align-items: center;
          // margin: 6px auto;
          padding: 8px;
          cursor: pointer;
          span{
            font-size: 14px;
          }
          .icon-left {
            flex: 7;
            display: flex;
            align-items: center;
            :first-child {
              // width: 18px;
              // margin-right: 6px;
            }
            :nth-child(2) {
              margin-left: 4px;
            }
          }
          .icon-right1 {
            flex: 2;
            :first-child {
              // width: 18px;
              // margin-right: 6px;
            }
            :nth-child(2) {
              margin-left: 8px;
            }
          }
        }
      }

    }
  }
}
/* 被拖动元素的原始位置样式 */
.dragging-original {
  opacity: 0.5;
  background: #1777ff !important;
}

/* 拖动时的占位符样式 */
.dragging-ghost {
  background: #e3f2fd !important; /* 浅蓝色背景 */
  // border: 2px dashed #2196f3 !important;
  opacity: 0.8;
  transform: rotate(3deg);
}

/* 被选中元素的样式 */
.dragging-chosen {
  background: #1777ff !important; /* 浅橙色背景 */
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

/* HTML5拖拽后备样式 */
.dragging-fallback {
  background: #ffebee !important;
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值