基于layui框架的Table列表,拖拽排序,我来了

兔白白

前言:layui 框架本身不支持table的拖拽排序,不过,在它红极一时的时候,有过很多优秀的第三方扩展插件,借助于这些扩展插件,我们就能轻松的完成拖拽排序这个操作啦

简单看一下效果图 动动手指就能轻松完成排序了,排序效果直观可见

拖拽功能本身因为有第三方的插件,是比较好实现的,重点在于后端的数据更新
通常更新数据的方案有3种
一、全量更新,直接获取更新后的列表数据,将所有的ID 按照顺序 发送给后端,然后后端进行全部更新(优点,足够简单无脑的,缺点就是,如果数据量比较多,这样更新会性能开销比较大)
二、取中值法,存储排序字段,不再是紧凑连续的,而是每个数据直接会间隔一段距离。

例如表的第一条数据sort是1000,第二条则是2000 ,第三条3000,以此类推,每个数据排序字段间隔1000的差值
然后将数据的排序字段通过对拖拽后,获取到最新的位置前后的数据ID,
例如,将第三条数据 拖拽到1和2直接,则将1和2 发送给后端,后端查询表后,得知他们的排序 分别是 1000,2000
此时 只需要将3的排序值改成(1000+2000)/2 = 1500 即可完成更新
这种更新的方式优点显而易见,只会针对有变动的数据进行更新,缺点也很明显,如果多次更新的话,数值之间没有足够的空间进行二分,就会导致更新失败
这时,我们就需要针对全表进行一次大的排序重置,将数据间的间距重新恢复到初始状态才行,所以该方案适合更新频率不高的场景,或者数据量比较小的也可以(另一个缺点则是新增数据的时候,也需要动态去计算排序值了,无法直接使用一个默认的排序值)

三、单表单列 ,这个我没看懂,有点难复杂,我就不班门弄斧了,原帖在此,有兴趣的可以去看看 拖拽排序后台设计与实现

在这里,我使用的是第二条方案,并且会在无法继续取中间值时,自动触发全表重置,使用起来 还是挺安逸的

不再废话,下面开始手摸手实现

我这边是使用的 webmen-admin 框架,理论上,只要前端是用的layui 都可以使用这个教程的

有请本次友情出场的插件:layui-soul-table

先将插件代码下载到本地,插件文件很多,因为这个插件的功能确实很多,像什么拖拽排序,表头筛选,表头拖拽排序等等... 不过我们先不需要管其他的,先将核心的文件复制一下,也就是 ext 的文件

进入我们自己的项目,在 plugin/admin/public/component/pear/module 目录下 新建一个文件夹 soulTable,将ext 文件夹中的文件拷贝过来

这儿的 module 文件夹 也就是 layui的模块文件夹, 如果有第三方插件 都可以放在这儿

新建一个JS文件,也可以直接 在plugin/admin/public/admin/js/common.js 中追加下方的代码
plugin/admin/public/admin/js/soulTable.js

// 使用 extend 将 刚刚下载的插件 加载进来
layui.extend({
    soulTable:'soulTable/soulTable.slim',// 模块
})

/**
 * 拖拽表格 进行排序
 * @param obj  当前拖拽对象
 * @param tableId  表的主键ID
 * @param updateUrl  更新数据的接口
 * @param weightField  排序字段
 */
function rowDragDoneFunc(obj,tableId,updateUrl,weightField){
    weightField = weightField || 'weight';
    console.log(obj.row,'--obj.row')
    // 获取最新位置 前后数据的id
    var beforId = afterId = 0;
    if(obj.newIndex > 0){
        beforId = obj.cache[obj.newIndex-1][tableId]
    }
    if(obj.newIndex < obj.cache.length-1){
        afterId = obj.cache[obj.newIndex+1][tableId]
    }
    var data = {
        dragDone:1,// 增加数据标识  方便后台接口进行判断
        id:obj.row[tableId],
        field:weightField,
        beforId,afterId
    }
    // 提交数据进行排序更新
    layui.$.post(updateUrl,data,function(res){
        if(res.code){
            layui.layer.msg(res.msg,{icon:5});
        }else{
            obj.row[weightField] =res.data;
            refreshTable();
        }
    })
}

在业务代码中开始使用
app/admin/view/app/demo/index.html
注意:如果上面是新建的 soulTable.js 文件 则需要在 index.html 中 引入这个js文件方可

  table.render({
      elem: "#data-table",
      url: SELECT_API,
      page: true,
      cols: [cols],
      skin: "row",// line 行边框风格  row 列边框风格  nob 无边框
      even:true,// 隔行换色
      size: "lg",
      toolbar: "#table-toolbar",
      autoSort: false,
      where:{field:'weight',order:'desc'},// 默认使用的weight 倒序
      defaultToolbar: [{
      title: "刷新",
      layEvent: "refresh",
      icon: "layui-icon-refresh",
      }, "filter", "print", "exports"],
      // 关键代码 拖拽方法回调 rowDragDoneFunc 就是上面创建的方法
      rowDrag:{
        done:(obj)=>{rowDragDoneFunc(obj,PRIMARY_KEY,UPDATE_API,'weight')},
      },
      done: function () {
          layer.photos({photos: 'div[lay-id="data-table"]', anim: 5});

          // 关键代码 拖拽方法绑定
          soulTable.render(this);
    }})

至此,前端的代码已经添加完毕了,下面需要对后台代码进行一点点小小的改动
app/admin/app/servicers/DragdoneUpService.php

<?php

namespace app\admin\app\servicers;

use support\Db;
use support\exception\ApiException;
use support\facade\Logger;
use support\Request;
use support\Response;

class DragdoneUpService extends BaseService
{
    const sortSpace = 1000;// 排序默认间距

    /**
     * 拖拽排序 后台处理逻辑
     * @param Request $request 接收的参数
     * @param \support\Model $model 被排序的表
     * @return true
     * @throws ApiException
     */
    static function dragDoneUpData(Request $request,\support\Model $model){
        $primary_key = $model->getKeyName();
        // 获取前一条数据的 排序数值
        $params = $request->post();
        $field = $params['field'];
        $selfId = $params[$primary_key];
        $afterWeight = $beforWeight = 0;
        if($params['beforId']){
            $beforWeight = $model->where($primary_key,$params['beforId'])->value($field);
        }else{
            // 不存在 则查询 后一个位置的前一条记录  先查询 后者的排序值
            $afterWeight = $model->where($primary_key,$params['afterId'])->value($field);

            $info = $model->where([[$field,'>',$afterWeight],[$primary_key,'<>',$selfId]])->orderBy($field,'desc')->select($primary_key,$field)->first();
            if($info){
                $beforWeight = $info[$field];
                $params['beforId'] = $info[$primary_key];
            }else{
                $beforWeight = -1 ;// 没有记录
            }
        }
//        Logger::debug(['$beforWeight',$beforWeight]);
        if(!$afterWeight){
            if($params['afterId']){
                $afterWeight = $model->where($primary_key,$params['afterId'])->value($field);
            }else{
                $info = $model->where([[$field,'<',$beforWeight],[$primary_key,'<>',$selfId]])->orderBy($field,'desc')->select($primary_key,$field)->first();
                if($info){
                    $afterWeight = $info[$field];
                    $params['afterId'] = $info[$primary_key];
                }else{
                    $afterWeight = -1 ;// 没有记录
                }
            }
        }

        // 间隔值太小  触发全表重排
        if(abs($afterWeight - $beforWeight)<10  ||
            ($beforWeight<0 && $afterWeight<10) ||
            ($beforWeight<10 && $afterWeight<0)
        ){
            self::dragDoneUpDataDefault($model,$field);
            // 再次获取新的 排序数值
            $beforWeight = $model->where($primary_key,$params['beforId'])->value($field)?:-1;
            $afterWeight = $model->where($primary_key,$params['afterId'])->value($field)?:-1;
        }

        // 如果当前位置末尾没有数据了
        if($afterWeight == -1) $afterWeight = 0;
        if($beforWeight == -1){
            // 如果当前位置前面没有数据了  则更新的排序值 = 最大的排序值+间隔值
            $newWeight = $afterWeight + self::sortSpace;
        }else{
            // 取中间值 则为当前数据的值
            $newWeight = ceil(($beforWeight + $afterWeight) / 2);
        }
//        Logger::debug(['取中间值',$newWeight,$beforWeight,$afterWeight,$params]);
        if($newWeight<2){
//            throw new ApiException('抱歉,排序失败,取值错误');
        }
        // 更新排序
        $model->where($primary_key,$selfId)->update([$field=>$newWeight]);
        return $newWeight;
    }

    /**
     * 全表重排排序
     * @param \support\Model $model
     * @param $field
     * @param $sortSpace
     * @return void
     */
    static protected function dragDoneUpDataDefault(\support\Model $model,$field){
        $tableName = $model->getTable();
        $primary_key = $model->getKeyName();
        $sortSpace = self::sortSpace;
        DB::statement("SET @row_number = 0");
        $sql = " UPDATE $tableName t1
                JOIN (
                    SELECT id, (@row_number := @row_number + 1) AS new_weight
                    FROM (
                        SELECT id, $field
                        FROM $tableName
                        ORDER BY $field asc,$primary_key asc
                    ) AS sorted_table
                ) AS t2 ON t1.id = t2.id
                SET t1.$field = t2.new_weight * $sortSpace";
        return Db::statement($sql);
    }
}

最后,前往我们的业务控制器中, 调用我们上方声明的方法即可
app/admin/app/controller/DemoController.php


    /**
     * 更新
     * @param Request $request
     * @return Response
     * @throws BusinessException
    */
    public function update(Request $request): Response
    {
        if ($request->method() === 'POST') {
            // 通过前端我们安排的间谍 dragDone 来判断本次请求是否是拖拽排序
            // 拖拽排序更新
            if($request->post('dragDone')==1 && DragdoneUpService::dragDoneUpData($request,$this->model)){
                return $this->json(0,'ok');
            }
            return parent::update($request);
        }
        return view('app/demo/update');
    }

好了,到此为止,相信各位看官都能够轻松完成表格的拖拽排序啦~~~
(小插曲,最开始是想给一键菜单里面加个拖拽排序。毕竟 字段不能拖拖 着实难受,不过 在那边试了下 遇到了点挫折,明天我会继续努力的,如果成功了,我再来更新 ^_^)

772 2 1
2个评论

jian1098

正好头疼这个排序问题,每次建表创建时间排前面看着难受,下午试试看

hulang

dragDoneUpDataDefault函数里面的SQL,能否PHP

  • 兔白白 27天前

    这个sql 我只会原生的写法。 用 orm 不知道怎么写 QAQ

  • hulang 27天前

    这,好吧。。。。

兔白白

550
积分
0
获赞数
0
粉丝数
2024-04-18 加入
×
🔝