Accept Merge Request #61: (feature/hant -> develop)

Merge Request: 派单地图

Created By: @todayswind
Accepted By: @todayswind
URL: https://g-bcrc3009.coding.net/p/allocatr/d/allocatr/git/merge/61?initial=true
This commit is contained in:
todayswind 2025-05-24 19:07:54 +08:00 committed by Coding
commit 5993c3e2e1
6 changed files with 364 additions and 72 deletions

View File

@ -117,9 +117,13 @@ class Dispatch extends Backend
return $this->view->fetch(); return $this->view->fetch();
} }
$params = $this->request->post('row/a'); $params = $this->request->post('row/a');
// $this->success();
if (empty($params)) {
$params = $this->request->post();
if (empty($params)) { if (empty($params)) {
$this->error(__('Parameter %s can not be empty', '')); $this->error(__('Parameter %s can not be empty', ''));
} }
}
$params = $this->preExcludeFields($params); $params = $this->preExcludeFields($params);
if ($this->dataLimit && $this->dataLimitFieldAutoFill) { if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
@ -302,10 +306,21 @@ class Dispatch extends Backend
return $workers; return $workers;
} }
public function map(){ public function map()
{
// 配置信息 // 配置信息
$config = get_addon_config('address'); $config = get_addon_config('address');
$order_id = request()->get('order_id');
$order = (new Order())->where('id', $order_id)->select();
$order = $order[0] ?? false;
if (!$order) {
abort(404);
}
$center = [
$order->lng, $order->lat
];
$this->view->assign('mapkey', $config); $this->view->assign('mapkey', $config);
$this->view->assign('center', $center);
return $this->fetch(); return $this->fetch();
} }

View File

@ -272,7 +272,6 @@ class Worker extends Backend
public function dispatchList() public function dispatchList()
{ {
$area_id = request()->get('area_id'); $area_id = request()->get('area_id');
list($where, $sort, $order, $offset, $limit) = $this->buildparams(); list($where, $sort, $order, $offset, $limit) = $this->buildparams();
@ -288,7 +287,9 @@ class Worker extends Backend
->where('status', '>=', Order::STATUS_DRAFT); ->where('status', '>=', Order::STATUS_DRAFT);
return 'doing_order'; return 'doing_order';
}]) }])
->field(['id', 'name', 'tel', 'area_id', 'lng', 'lat']); ->field(
['id', 'name', 'tel', 'area_id', 'lng', 'lat']
);
if ($area_id) { if ($area_id) {
$code = $this->getSelectAreaCode(substr_replace($area_id, '00', -2)); $code = $this->getSelectAreaCode(substr_replace($area_id, '00', -2));
@ -302,4 +303,83 @@ class Worker extends Backend
} }
public function dispatchMapList()
{
$order_id = request()->get('order_id');
$search = request()->get('search');
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
$order = (new Order())->where('id',$order_id)->field(['id','lng','lat'])->select();
if (!empty($order)) {
$order = $order[0];
}
$worker_distance = Db::query("SELECT id
FROM (
SELECT id,
ST_Distance_Sphere(
point(lng, lat),
point(?, ?)
) AS distance
FROM fa_worker where status = 1
) AS t
WHERE distance < 50000
ORDER BY distance;",[$order->lng,$order->lat]);
$worker_ids = array_column($worker_distance,'id');
$build = model('worker')
->where('status', 1)
->whereIn('id', $worker_ids);
if ($search){
$build->where(function ($q)use($search){
$q->where('name','like','%'.$search.'%')
->whereor('tel','like','%'.$search.'%');
});
}
$build->withCount(['myorder' => function ($query) {
$query->where('status', Order::STATUS_FINISHED);
return 'finish_order';
},])
->withCount(['myorder' => function ($query) {
$query->where('status', '<', Order::STATUS_FINISHED)
->where('status', '>=', Order::STATUS_DRAFT);
return 'doing_order';
}])
->field(
['id', 'name', 'tel', 'area_id', 'lng', 'lat','ST_Distance_Sphere(
point(lng, lat),
point('.$order->lng.','.$order->lat.')
) AS distance']
)->order('distance');
$list = $build
->paginate($limit);
$data = [];
foreach ($list->items() as $item){
$data [] = [
'id' => $item->id,
'name' => $item->name,
'tel' => $item->tel,
'lat' => $item->lat,
'lng' => $item->lng,
'distance' => $item->distance,
'doing_order' => $item->doing_order,
'finish_order' => $item->finish_order,
'star' => $item->star,
];
}
$result = array("total" => $list->total(), "rows" => $data,'order' => $order);
return $result;
}
} }

View File

@ -5,29 +5,73 @@
<title>地图派单示例</title> <title>地图派单示例</title>
<!-- 使用高德测试 Key --> <!-- 使用高德测试 Key -->
<script src="https://webapi.amap.com/maps?v=2.0&key={$mapkey.amapkey|default=''}"></script> <script src="https://webapi.amap.com/maps?v=2.0&key={$mapkey.amapkey|default=''}"></script>
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
<style> <style>
#map { width: 100%; height: 600px; } #map {height: 100vh; }
.info-window { .info-window {
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
} }
.worker-search-bar {
display: flex;
gap: 8px; /* 输入框和按钮的间距 */
margin-bottom: 10px;
}
.worker-input {
flex: 1;
padding: 6px 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
line-height: 14px;
}
.worker-btn {
padding: 6px 12px;
font-size: 14px;
background-color: #007bff;
color: #fff;
border: 1px solid #007bff;
border-radius: 4px;
cursor: pointer;
line-height: 14px;
}
.worker-btn:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.worker-card{
background-color: #c9c5c542;
margin-bottom: 10px;
box-shadow: 0 2px 6px rgba(12, 72, 128, .1);
border-radius: 5px;
}
</style> </style>
</head> </head>
<body> <body>
<div id="map"></div> <div class="row" style="width: 100%">
<div id="map" class="col-md-9"></div>
<div id="list" class="col-md-3 pl-3">
<div class="worker-search-bar mb-2">
<input type="text" id="worker-search" class="worker-input" placeholder="搜索接单员姓名或电话">
<button id="reload" class="worker-btn">刷新</button>
</div>
<div id="worker-list" style="height: 100vh; overflow: auto;"></div>
</div>
</div>
<script> <script>
$(function (){
window._AMapSecurityConfig = { window._AMapSecurityConfig = {
securityJsCode: "{$mapkey.amapsecurityjscode|default=''}", securityJsCode: "{$mapkey.amapsecurityjscode|default=''}",
} }
const taskLocation = [104.065735, 30.657201]; // 天府广场 const taskLocation = {:json_encode($center); };
let workerMarkers = [];
const workers = [
{ name: '张三', phone: '138****1234', position: [104.066, 30.658] },
{ name: '李四', phone: '139****5678', position: [104.064, 30.656] },
{ name: '王五', phone: '136****9876', position: [104.067, 30.659] },
];
const map = new AMap.Map('map', { const map = new AMap.Map('map', {
zoom: 16, zoom: 16,
@ -41,20 +85,32 @@
icon: '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-red.png', icon: '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-red.png',
label: { label: {
content: '<span style="color: red; font-weight: bold;">任务点</span>', content: '<span style="color: red; font-weight: bold;">任务点</span>',
offset: new AMap.Pixel(20, -10) offset: new AMap.Pixel(10, -10)
} }
}); });
map.add(taskMarker); map.add(taskMarker);
function drawWorker(workers) {
// 清除旧的工人标记
if (workerMarkers.length > 0) {
map.remove(workerMarkers);
workerMarkers = [];
}
// 添加新的工人标记
workers.forEach((worker, index) => {
const position = addSlightOffset(worker.lat, worker.lng, index);
// 添加工人标记(蓝色)
workers.forEach(worker => {
const marker = new AMap.Marker({ const marker = new AMap.Marker({
position: worker.position, position,
title: worker.name, title: worker.name,
icon: '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-blue.png', icon: new AMap.Icon({
size: new AMap.Size(32, 32),
image: '/assets/img/worker.png',
imageSize: new AMap.Size(32, 32)
}),
label: { label: {
content: `<span>${worker.name}</span>`, content: `<span>${worker.name}</span>`,
offset: new AMap.Pixel(20, -10) offset: new AMap.Pixel(10, -10)
} }
}); });
@ -62,10 +118,14 @@
content: ` content: `
<div class="info-window"> <div class="info-window">
<strong>${worker.name}</strong><br/> <strong>${worker.name}</strong><br/>
电话:${worker.phone}<br/> 电话:${worker.tel}<br/>
<button onclick="alert('已派单给 ${worker.name}')">派单</button> 距离任务:${worker.distance.toFixed(2)} 米<br/>
正在进行订单数:${worker.doing_order}<br/>
完成订单数:${worker.finish_order}<br/>
星级:${'★'.repeat(worker.star)}<br/>
<button class="btn btn-sm btn-primary assign-btn mt-1" data-id="${worker.id}" data-name="${worker.name}">派单</button>
</div>`, </div>`,
offset: new AMap.Pixel(0, -30) offset: new AMap.Pixel(0, -10)
}); });
marker.on('click', () => { marker.on('click', () => {
@ -73,7 +133,142 @@
}); });
map.add(marker); map.add(marker);
workerMarkers.push(marker); // 存储到全局数组
}); });
}
//点击确定后执行回调赋值
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
function renderWorkerCards(workers) {
const listContainer = document.getElementById('worker-list');
listContainer.innerHTML = ''; // 清空旧内容
if (!workers || workers.length === 0) {
listContainer.innerHTML = `
<div class="col">
<div class="text-center" role="alert">
暂无可用接单员
</div>
</div>`;
return;
}
workers.forEach(worker => {
const maskedPhone = worker.tel.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
const starHtml = '★'.repeat(worker.star).padEnd(5, '☆');
const distanceKm = (worker.distance / 1000).toFixed(2);
const cardHtml = `
<div class="col worker-card" data-lat="${worker.lat}" data-lng="${worker.lng}">
<div class="card shadow-sm">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong class="mb-0">${worker.name}</strong>
<button class="btn btn-sm btn-primary assign-btn" data-id="${worker.id}" data-name="${worker.name}">派单</button>
</div>
<div>
<div>
<small class="text-muted d-block">电话:${maskedPhone}</small>
<small class="text-muted d-block">星级:
<span class="text-warning">${starHtml}</span>
</small>
</div>
<div>
<small class="text-muted d-block">距离:${distanceKm} 公里</small>
<small class="text-muted d-block">完成订单数:${worker.finish_order}</small>
<br>
<small class="text-muted d-block">进行中订单数:${worker.doing_order}</small>
</div>
</div>
</div>
</div>
</div>`;
listContainer.insertAdjacentHTML('beforeend', cardHtml);
});
}
function assignOrder(worker_id) {
// 可发起请求
const params = new URLSearchParams(window.location.search);
const order_id = params.get('order_id');
$.ajax({
url: '/admin/orders/dispatch/add',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ worker_id,order_id }),
success: function (res) {
Toastr.info('派单成功');
close()
},
error: function () {
Toastr.info('派单失败,请重试。');
}
});
}
function reload(){
const params = new URLSearchParams(window.location.search);
const orderId = params.get('order_id');
const keyword = $('#worker-search').val().trim();
// 构造请求参数对象
const queryParams = new URLSearchParams();
if(orderId) queryParams.append('order_id', orderId);
if(keyword) queryParams.append('search', keyword); // 传给后端的搜索关键词参数名,跟后端约定
$.ajax({
url: "/admin/workers/worker/dispatchMapList?" + queryParams.toString(),
type: 'get',
dataType: 'json',
success: function (ret) {
drawWorker(ret.rows);
renderWorkerCards(ret.rows);
}
});
}
reload();
function addSlightOffset(lat, lng, index) {
const offset = 0.00005; // 约5米
return [
parseFloat(lng) + (index % 3 - 1) * offset,
parseFloat(lat) + (Math.floor(index / 3) - 1) * offset
];
}
// 委托方式绑定事件:监听 .assign-btn 点击
$(document).on('click', '.assign-btn', function () {
const worker_id = $(this).data('id');
assignOrder(worker_id);
});
// 事件委托,绑定点击工人卡片的事件
$(document).on('click', '.worker-card', function (e) {
// 避免点击“派单”按钮时触发此事件
if ($(e.target).closest('.assign-btn').length) {
return;
}
const lat = parseFloat($(this).data('lat'));
const lng = parseFloat($(this).data('lng'));
if (!isNaN(lat) && !isNaN(lng)) {
map.setCenter([lng, lat]); // 高德地图的坐标格式是 [经度, 纬度]
map.setZoom(17); // 可选:放大地图
}
});
$('#reload').click(function () {
reload();
});
})
</script> </script>
</body> </body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

View File

@ -246,7 +246,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'cascader'], function
} }
return false; return false;
}, },
refresh: true, callback:function (){
table.bootstrapTable('refresh');
}
}, },
{ {
name: "reminder", name: "reminder",

View File

@ -337,7 +337,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'layer', 'cascader'],
}); });
$('#add_worker').on('click',function (){ $('#add_worker').on('click',function (){
console.log(1); console.log(1);
Fast.api.open('workers/worker/add?type=3', '添加工人',{ Fast.api.open('workers/worker/add?type=2', '添加工人',{
callback: function (value) { callback: function (value) {
console.log(2222,value); console.log(2222,value);
} }