拖拽排序(平均值法)

原先有讨论过一篇有关拖拽排序的问题,求教列表排序的解决方案(拖拽排序),在社区大佬们的帮助下,了解了很多的实现方法。下面介绍一个我本人比较认可的一个方法:平均值法(自己起的名字😂)。

此方法借鉴下面两位大佬的回复:
问答:求教列表排序的解决方案(拖拽排序)
问答:求教列表排序的解决方案(拖拽排序)

表与模型

CREATE TABLE `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
  `rank` decimal(60,30) NOT NULL DEFAULT '0.000000000000000000000000000000' COMMENT '权重,用于排序,越小越靠前'
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8;

迁移

创建迁移文件

$ php artisan make:migration create_sort_table

完整的迁移文件内容

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('sort', function (Blueprint $table) {
            $table->id();
            $table->string('name', 50)->default('');
            $table->decimal('rank', 60, 30)->default(0);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('sort');
    }
};

运行迁移

$ php artisan migrate

模型

$ php artisan make:model "App\\Models\\Sort"

完整的模型文件内容

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Sort extends Model
{
    use HasFactory;

    /**
     * 指示模型是否主动维护时间戳。
     *
     * @var bool
     */
    public $timestamps = false;

    /**
     * 字段黑名单
     *
     * @var array
     */
    protected $guarded = [];

    /**
     * The attributes that should be cast.
     * @var array
     */
    protected $casts = [
        'rank'      => 'string'
    ];
}

逻辑代码

创建控制器

$ php artisan make:controller "SortController"

控制器内容

<?php

namespace App\Http\Controllers;

use App\Models\Sort;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Throwable;

class SortController extends Controller
{
    /**
     * 列表
     * @return JsonResponse
     */
    public function index(): JsonResponse
    {
        $sorts = Sort::orderBy('rank')->get();
        return response()->json($sorts);
    }

    /**
     * 添加
     *
     * @param Request $request
     * @return JsonResponse
     * @throws Throwable
     */
    public function store(Request $request): JsonResponse
    {
        $name = $request->input('name');
        DB::beginTransaction();
        try {
            // 创建
            $sort = Sort::create([
                'name'  => $name
            ]);

            // 修改排序值为 当前 id * 10 的 20 次方,保留30为小数,相同位置最多可拖拽 20 + 30 次
            $sort->rank = bcmul($sort->id, bcpow(10, 20, 30), 30);
            $sort->save();
            DB::commit();
        } catch (Throwable $e) {
            DB::rollBack();
            throw $e;
        }

        return response()->json([
            'id'    => $sort->id
        ]);
    }

    /**
     * 拖拽排序
     *
     * @param Request $request
     * @param int $id
     * @return JsonResponse
     * @throws Throwable
     */
    public function updateRank(Request $request, int $id): JsonResponse
    {
        $params = $request->only(['before_id', 'after_id']);

        // 获取当前需要移动的 sort
        $current = Sort::query()->find($id);
        if (!$current) {
            throw new \Exception();
        }

        // 前一个 rank 值
        $before = Sort::query()->find($params['before_id']);
        $beforeRank = $before->rank ?? 0;

        // 后一个 rank 值
        $after = Sort::query()->find($params['after_id']);
        $afterRank = $after->rank ?? bcadd($before->rank, 2, 30); // 如果后一个不存在则 值=前一个rank + 2

        // 保存 rank
        $current->rank = bcdiv(bcadd($beforeRank, $afterRank, 30), 2, 30);
        $current->save();

        // 判断是否需要重置数据,当右边第三个数已被使用后则重置,最多可拖拽 50 -3 次,便需要重置
        if ((int) substr($current->rank, -3) > 0) {
            DB::beginTransaction();
            try {
                // 这里将循环遍历重置rank
                $sorts = Sort::query()->orderBy('rank')->get();
                foreach ($sorts as $index => $sort) {
                    $sort->rank = bcmul($index + 1, bcpow(10, 20, 30), 30);
                    $sort->save();
                }

                DB::commit();
            } catch (Throwable $e) {
                DB::rollBack();
                throw $e;
            }
        }

        return response()->json();
    }
}

rank字段总长度为60为,其中30位为小数点,添加数据的时候rank是从 1020 开始的,因为 id 是 int(11) 类型的,这样正好保证了 rank 字段不过超过公式 bcmul($sort->id, bcpow(10, 20, 30), 30)的大小。为了防止拖动过快(小并发)保留 3 位作为并发空间,固此代码相同位置最多可以拖拽 47 次后,就需要重置 rank 字段。

这是目前想出来比较合理的拖拽排序的解决方案了,大家有更好的吗?一起交流下

本作品采用《CC 协议》,转载必须注明作者和本文链接
未知的永远是最精彩的!
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 6
抄你码科技有限公司

我粗暴地直接用update实现了排序

$updateItem = collect($myItems)->where(function ($item) {
                return Arr::has($item, 'id');
            })->map(function ($item, $index) use ($parent_id) {
                $myItem = MyItem::where('parent_id', '=', $parent_id)->findOrFail($item['id']);
                $myItem->name = $item['name'];
                $myItem->sort = $index; // 就是这行实现了排序
                $myItem->save();
                return $myItem;
            });
1年前 评论
看上隔壁小花了啦 (楼主) 1年前
抄你码科技有限公司 (作者) 1年前
看上隔壁小花了啦 (楼主) 1年前

我假设一下啊,是不是用链表的思维方式会不会更好一点

1年前 评论
看上隔壁小花了啦 (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!