在不少用戶交互的場景中,常常有給用戶提供數據排序的需求,排序的交互嘛,前端有各類插件和方案能夠實現,問題出在如何保存用戶的排序結果。javascript
最常規的方案,就是將排序後的全部數據的順序保存下來或依次從新排序賦值,在數據量小的狀況下,這樣作能夠接受。一旦數據量成千上萬,這個方案就比較扯淡了。php
因此筆者試圖去設計一款通用的排序方案。html
首先是給數據表加一個double類型的字段用於排序,舉個例子:這個字段爲rankValue,表名爲tableItem。前端
那麼,咱們只要設計一個排序接口交給前端,當前端拖拽排序後,分別傳出item_id、prev_item_id、next_item_id這三個參數,便可用於從新排序。java
顧名思義這對應的邏輯是:將item_id 拖拽到 prev_item_id 和 next_item_id 之間時,重算item_id對應的rankValue值,使其符合上下行排序值之間。算法
那麼,這裏有個核心問題來了:如何計算出兩個rankValue之間的中間值?數據庫
計算兩個數字的中間值,第一想法會是直接 (prevRankValue + nextRankValue) / 2 完事兒。api
作固然能作,(10+21)/2=15.5 看着好像是那麼回事,但要是再拖拽幾下:函數
(10+15.5)/2 = 12.75
(10+12.75)/2 = 11.375
複製代碼
... 這就會往奇怪的狀況去了,強迫症患者不能忍,因此咱們須要作一個算法實現這樣的效果:post
(10+15.5)/2 ≈ 13
(10+12.75)/2 ≈ 11
複製代碼
我稱之爲模糊中間數
,即取得兩個數字的模糊中間數,並儘量的忽略精確值。好比0.99與1.2的中間數是1,9000與1002的中間數是950而不是951。
之因此這樣作,就是爲了使咱們的數據在大量的拖拽調序以後,它們對應的排序值不要變得太難看,固然你硬是要在1和2之間插100個數據,那就另說了。
提供一個PHP版本的代碼,供參考:
<?php
/** * 數學相關處理函數庫文件 * @package W2 * @author wanyaxing * @since 1.0 * @version 1.0 */
class W2Math {
/** 取得數字的精確位,正數表示n位小數,負數表示精確到個十百千萬位(10的(n-1)次方) */
public static function getPrecisionOfNumber($number) {
$number = abs($number);
$len = strlen($number);
if (strpos($number,'.')>=0)
{
return $len - strpos($number,'.') + 1;
}
else
{
for ($i=1; $i < $len; $i++)
{
if (substr($number,$len-$i,1) > 0)
{
return 0 - ($i - 1);
}
}
}
}
/** * 取得兩個數字的模糊中間數,並儘量的忽略精確值。好比0.99與1.2的中間數是1 * @param [type] $bigNumber [description] * @param [type] $smallNumber [description] * @param boolean $isShortIfShortAble [description] * @return [type] [description] */
public static function getMiddleBetweenNumbers($bigNumber=null,$smallNumber=null) {
if (!is_null($bigNumber) || !is_null($smallNumber))
{
if (is_null($bigNumber))
{
$precision = min(W2Math::getPrecisionOfNumber($smallNumber),-1);
return $smallNumber + pow(10,abs($precision));
}
else if (is_null($smallNumber))
{
$precision = min(W2Math::getPrecisionOfNumber($bigNumber),-1);
return $bigNumber - pow(10,abs($precision));
}
else if ($bigNumber==$smallNumber)
{
return $bigNumber;
}
else if ($bigNumber<$smallNumber)
{
return null;
}
else
{
$middle = $smallNumber + (($bigNumber - $smallNumber)/2);
$precisionMin = min(W2Math::getPrecisionOfNumber($bigNumber),W2Math::getPrecisionOfNumber($smallNumber),-1);
$precisionMax = max(W2Math::getPrecisionOfNumber($bigNumber),W2Math::getPrecisionOfNumber($smallNumber),$precisionMin);
for ($i=$precisionMin; $i <=$precisionMax ; $i++) {
$tmp = round($middle,$i);
if ($tmp>$smallNumber && $tmp<$bigNumber)
{
return $tmp;
}
}
}
}
return null;
}
}
複製代碼
注意,在這裏getMiddleBetweenNumbers方法接受的參數是嚴格要求大數字在前小數字在後
的,你應該在傳參以前的業務邏輯中保證這個數字順序,固然,此處邏輯僅供參考,你們也能夠直接改代碼,實現兼容方案,這就看你們業務需求了。
繼續提供一份接口方案的核心代碼,供你們參考:
<?php
// ItemHandler.php
class ItemHandler extends AbstractHandler {
/** 取得兩個商品排序的中間排序值 */
public static function getRankValueBetweenItems($prevItemID=null,$nextItemID=null) {
$prevItemModel = ItemHandler::loadModelById($prevItemID);
$nextItemModel = ItemHandler::loadModelById($nextItemID);
if (is_object($prevItemModel) || is_object($nextItemModel) )
{
if (!is_object($prevItemModel))
{
$prevItemModel = ItemHandler::loadModelFirstInList(array('rankValue > ' . $nextItemModel->getRankValue(),'status'=>$nextItemModel->getStatus()),'rankValue asc',1,1);
}
if (!is_object($nextItemModel))
{
$nextItemModel = ItemHandler::loadModelFirstInList(array('rankValue < ' . $prevItemModel->getRankValue(),'status'=>$prevItemModel->getStatus()),'rankValue desc',1,1);
}
$bigNumber = is_object($prevItemModel)?$prevItemModel->getRankValue():null;
$smallNumber = is_object($nextItemModel)?$nextItemModel->getRankValue():null;
return W2Math::getMiddleBetweenNumbers($bigNumber,$smallNumber);
}
return null;
}
}
複製代碼
能夠在此處看到,prev_item_id 和 next_item_id 二者並不都是必傳參數,只傳一個參數也能夠,在接口裏能夠嘗試從數據庫裏取出另外一個數據,若是取不到數也能夠的,那就是當你想要將某數據拖拽到整個護具的第一行或最後一行時。
<?php
// ItemController.php
class ItemController extends AbstractController{
public static function save($tmpModel,$isAdd=false) {
if ($tmpModel->isProperyModified('status') && $tmpModel->properyValue('status')==STATUS_NORMAL)
{
$tmpModel->setRankValue(time());
}
return parent::save($tmpModel,$isAdd);
}
public static function actionResetRankValueOfItem() {
if (static::getAuthIfUserCanDoIt(Utility::getCurrentUserID(),'axapi',null) != 'admin')
{
return HaoResult::init(ERROR_CODE::$NO_AUTH);
}
$itemID = W2HttpRequest::getRequestInt('item_id');
$itemModel = ItemHandler::loadModelById($itemID);
if (!is_object($itemModel))
{
return HaoResult::init(ERROR_CODE::$DATA_EMPTY);
}
$prevItemID = W2HttpRequest::getRequestInt('prev_item_id');//上一個(其rankValue值應該更大)
$nextItemID = W2HttpRequest::getRequestInt('next_item_id');//下一個(其rankValue值應該較小)
$newRankValue = ItemHandler::getRankValueBetweenItems($prevItemID,$nextItemID);
$itemModel->setRankValue($newRankValue);
return static::save($itemModel);
}
}
複製代碼
上文只是核心代碼,注意其中save方法裏,對於初始化的數據,作了一個 $tmpModel->setRankValue(time())
動做,這就是說給初始化的數據,按照時間戳設定排序值,這是一個小技巧,你們也能夠根據業務狀況酌情處理。
再提供一份前端實現的核心代碼,供你們參考
<tbody>
<?php foreach ($requestResult->results() as $detailResult) : ?>
<tr item_id="<?= $detailResult->find('id') ?>" >
<td><?= $detailResult->find('itemName') ?></td>
</tr>
<?php endforeach ?>
</tbody>
複製代碼
<script type="text/javascript">
$(function(){
$LAB
.script('/third/haouploader/js/sortable/Sortable.js')
.wait(function(){
Sortable.create($('#item_list_bg tbody')[0],{
draggable:'tr',
animation: 150,
// Changed sorting within list
onEnd: function (/**Event*/evt) {
if (evt.oldIndex != evt.newIndex)
{
var $this = $(evt.item);
var params = {};
params['item_id'] = $this.attr('item_id');
params['prev_item_id'] = $this.prev().attr('item_id');
params['next_item_id'] = $this.next().attr('item_id');
HaoConnect.post('item/reset_rank_value_of_item',params).then(function(result){
if (result.isResultsOK())
{
console.log('拖拽排序結果保存成功');
}
});
}
}
}
);
resetMenuDiv(result.find('menu'));
});
});
</script>
複製代碼
源於隨手刷到的 知乎 的一個提問 一個基本的用戶排序功能爲何這麼難?,雖然是四五年前的問題了,想起本身的確作過這份研究,也很久沒更新博客了,因此纔有了此篇分享,供你們參考。
原文來自阿星的博客: wanyaxing.com/blog/201907…