vue3+ts实现可拖动排序列表,支持快速检索,高亮标记,快速置顶与置底

由于业务需要,这里实现了一个简单的可拖动的排序列表,同时支持快速检索列表项,被检索的列表项可高亮显示并置顶

具体效果如下图:
在这里插入图片描述

代码中使用到element-plus,请根据需要引入;其中还用到了ClickOutside ,主要作用是为了实现点击弹窗外关闭弹窗的功能;拖动动画的实现使用了transition-group组件,ClickOutside 的使用是在 <div v-click-outside="onClickOutside"></div> 中,当用户点击该 div 元素外部时,会触发回调,可在方法中实现关闭弹窗的逻辑

import { ClickOutside as vClickOutside } from "element-plus";

废话不多说,全部代码如下(vue3+ts):

<template>
  <div class="sortable-list-container">
    <el-popover :visible="state.visible" placement="bottom" :width="250" trigger="click">
      <template #default>
        <div v-click-outside="onClickOutside">
          <el-empty v-if="state.deepCopyList.length===0" :image-size="150" description="暂无数据" />
          <div v-else>
            <div class="search-box">
              <el-input
                  placeholder="搜索菜名"
                  v-model="state.searchText"
                  size="default"
                  clearable
                  @input="handleSearchInput"
              />
            </div>
            <div class="action-button">
              <div class="left-action">
                <el-button size="small" type="primary" @click="resetListOrder">默认排序</el-button>
              </div>
              <div class="right-action">
                <el-button size="small" plain @click="upTop">置顶</el-button>
                <el-button size="small" plain @click="downBottom">置底</el-button>
              </div>
            </div>
            <ol class="sortable-list">
              <transition-group
                  name="list"
                  class="list"
                  tag="ol">
                <!-- tag 设置为 ol 有序列表,意味着会生成一个有序列表包裹住所有 li 元素 -->
                <li v-for="(item, index) in state.deepCopyList"
                    :class="item === state.activeItem ? 'list-item item-activeStyle' : 'list-item'"
                    @click="clickItemHandle(item)"
                    @dragenter="dragenter($event, index)"
                    @dragover="dragover($event)"
                    @dragstart="dragstart(index,item)"
                    @dragend="dropFinish"
                    draggable="true"
                    :key="item">
                  <!-- key 唯一且不能为index否则动画不生效 -->
                  <span v-html="highlightedLabel(item, state.searchText)" />
                </li>
              </transition-group>
            </ol>
          </div>
        </div>
        <div style="text-align: right; margin: 0; padding: 5px">
          <el-button round text size="small"  @click="state.visible = false" >取 消</el-button>
          <el-button round text size="small" @click="onSubmitSort" type="primary">确 定</el-button>
        </div>
      </template>
      <template #reference>
        <el-button  @click="openList" size="default" type="primary">
          上菜顺序
        </el-button>
      </template>
    </el-popover>
  </div>
</template>

<script setup lang="ts" name="sortableList">
import {reactive, onMounted, nextTick} from 'vue';
// ClickOutside 点击页面上的某个元素之外时触发特定的回调函数
import { ElMessage, ClickOutside as vClickOutside } from "element-plus";

const list = ["蒸羊羔", "蒸熊掌", "蒸鹿尾儿", "烧花鸭", "烧雏鸡", "烧子鹅", "卤煮咸鸭", "酱鸡", "腊肉", "松花", "小肚儿", "晾肉", "香肠", "什锦苏盘", "熏鸡", "白肚儿", "清蒸八宝猪", "江米酿鸭子", "罐儿野鸡", "罐儿鹌鹑", "卤什件儿", "卤子鹅", "山鸡", "兔脯", "菜蟒", "银鱼", "清蒸哈什蚂", "炒腰丝", "炒鸭腰", "炒鸭条", "清拌鸭丝儿", "黄心管儿", "炯白鲜", "炯黄鲜", "豆豉鱼占鱼", "锅烧鲤鱼", "锅烧绘鱼", "清蒸甲鱼", "抓炒鲤鱼", "抓炒对虾", "软炸里脊", "软炸鸡", "什锦套肠儿", "麻酥油卷儿", "卤煮寒鸦儿", "熠鲜蘑", "熠鱼脯", "熠鱼肚", "熠鱼骨", "爆鱼片儿", "醋爆肉片儿"]
const state = reactive({
  list: list, // 原始列表数据
  deepCopyList: [], // 深克隆数据(所有操作在这个数组上进行),如果数据改变但是未保存 恢复到初始数据
  activeItem: '', // 当前选中(激活)的元素
  dragIndex: '', // 单前拖动项目的索引
  visible: false, // 是否显示弹窗
  searchText: '', // 输入框搜索的内容
  searchTextTimeout: null, // 记录搜索的超时时间,避免频繁触发搜索
});

/**
 * 处理点击当前区域之外位置的事件。 需要引入 ClickOutside
 * import { ClickOutside as vClickOutside } from "element-plus";
 */
const onClickOutside = () => {
  // 关闭弹窗
  state.visible = false;
}

/**
 * 重置列表顺序
 */
const resetListOrder = () => {
  state.deepCopyList = [...state.list];
}

/**
 * 置顶选中的某一项元素
 */
const upTop = () => {
  if (state.activeItem) {
    const index = state.deepCopyList.indexOf(state.activeItem);
    if (index === 0) {
      ElMessage.success("已经置顶了~")
      return;
    }
    //将 当前index的元素通过unshift()方法添加到数组最前面,添加之后长度+1,之后并删除原本index+1 元素
    state.deepCopyList.unshift(state.deepCopyList[index]);
    state.deepCopyList.splice(index + 1, 1);
  }
}

/**
 * 置底选中的某一项元素
 */
const downBottom = () => {
  if (state.activeItem) {
    const index = state.deepCopyList.indexOf(state.activeItem);
    if (index ===  state.deepCopyList.length-1) {
      ElMessage.success("已经置底了~")
      return;
    }
    //置底操作的原理,将 当前index元素 用push() 添加到数组最后 ,并删除原本index 元素
    state.deepCopyList.push(state.deepCopyList[index]);
    state.deepCopyList.splice(index, 1);
  }
}

/**
 * 点击列表选中某元素
 * @param item 选中的元素
 */
const clickItemHandle = (item) => {
  state.activeItem = item;
}

/**
 * 事件在可拖动的元素进入一个有效的放置目标时触发,也就是拖动后鼠标松开时触发
 * @param e
 * @param index
 */
const dragenter = (e, index) => {
  // 避免源对象触发自身的dragenter事件
  e.preventDefault();
  // 如果当前拖动的元素和目标元素不是同一个元素,则进行排序,完成拖放
  if (state.dragIndex !== index) {
    const source =state.deepCopyList[state.dragIndex];
    state.deepCopyList.splice(state.dragIndex, 1);
    state.deepCopyList.splice(index, 0, source);
    // 排序变化后目标对象的索引变成源对象的索引
    state.dragIndex = index;
  }
}
/**
 * 事件在用户开始拖动元素时调用。拖动开始
 * @param index
 * @param item 拖动的元素,用于拖动给时选中该项
 */
const dragstart = (index,item) => {
  state.activeItem = item;
  state.dragIndex = index;
}
/**
 * 事件在可拖动的元素被拖进一个有效的放置目标时触发,也就是拖动结束在dragenter之后触发
 * @param e
 */
const dragover = (e) => {
  e.preventDefault();
}
/**
 * 移动结束后
 */
const dropFinish = () => {

}

/**
 * 处理搜索输入事件
 */
const handleSearchInput = () => {
  clearTimeout(state.searchTextTimeout);
  state.searchTextTimeout = setTimeout(() => {
    // 查找第一个匹配项
    const firstMatch = document.querySelector('.list-item span.match');
    // 如果存在匹配项,则滚动到该元素的顶部位置
    if (firstMatch) {
      //smooth:滚动应该是平滑的动画。instant:滚动应该通过一次跳跃立刻发生。auto:滚动行为由 scroll-behavior 的计算值决定。
      // start、center、end  block 垂直方向对齐
      firstMatch.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  }, 300); // 延迟执行防止频繁搜索
}

/**
 * 生成带有高亮的列表项
 * @param item 列表项
 * @param query 检索字符串
 */
const highlightedLabel = (item, query) => {
  // 如果查询为空,则直接返回标签
  if (!query) return item;
  // 创建正则表达式,使用查询字符串进行全局和不区分大小写的匹配
  const regex = new RegExp(query.trim(), 'gi');
  // 替换标签中匹配的字符串,并返回带有高亮的列表项
  return item.replace(regex, (matched) => `<span class="match" style="background-color: yellow; color: #000000">${matched}</span>`);
}

/**
 * 保存排序
 */
const onSubmitSort = () => {
  // 将修改后的数据保存到原始数据中
  state.list = state.deepCopyList;
  ElMessage.success('保存成功');
  console.log(state.list)
}

/**
 * 打开排序列表
 */
const openList = () => {
  // 深克隆数据,如果数据改变但是未保存 恢复到初始数据
  state.deepCopyList = [...state.list];
  // nextTick 等DOM加载完成后再打开窗口 防止打开窗口时列表出现进入的动画
  nextTick(()=>{
    state.visible = true;
  })
}


/**
 * 页面加载时
 */
onMounted(() => {

});

</script>

<style scoped lang="scss">
.action-button {
  display: flex;
  justify-content: space-between;
  padding-bottom: 10px;
}
.left-action,
.right-action {
  display: flex;
  align-items: center;
}
.search-box {
  padding: 10px 0 15px 0;
}

// 列表样式
.sortable-list {
  list-style-type:decimal;
  list-style-position:inside;
  max-height: 400px;
  min-height: 200px;
  font-size: 14px;
  padding: 0;
  overflow: auto;
}
.list {
  padding: 0;
  // 列表移动动画
  .list-move{
    transition: all 0.5s;
  }
  .list-item {
    border-radius: 4px;
    height: 20px;
    padding: 5px 10px;
    &:hover {
      background: #f5f7fa;
    }
  }
  .item-activeStyle{
    background-color:#ecf5ff;
  }
}
</style>

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值