feat: 新增师傅端功能及服务增项管理模块

- 新增师傅端路由和页面配置,切换登录后默认跳转到师傅大厅
- 新增服务分类图标上传功能和增项服务管理模块
- 新增订单、申诉、结算等模型关联关系及后台控制器
- 新增师傅端TabBar组件和消息页面跳转逻辑
- 新增文件上传接口和服务计算逻辑
- 新增各类SVG图标和空状态提示
这个提交包含在:
YuanWen
2026-11-03 15:45:21 +08:00
父节点 c01e8fc7d9
当前提交 361dffc7d8
共有 82 个文件被更改,包括 4787 次插入 和 531 次删除

查看文件

@@ -661,5 +661,128 @@ class Master extends Base
unset($row);
$this->success('', ['list' => $list]);
}
}
/**
* 计算两点间距离 (km)
*/
protected function calculateDistance($lat1, $lon1, $lat2, $lon2)
{
if (empty($lat1) || empty($lon1) || empty($lat2) || empty($lon2)) {
return 14.40;
}
$earthRadius = 6371;
$dLat = deg2rad((float)$lat2 - (float)$lat1);
$dLon = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad((float)$lat1)) * cos(deg2rad((float)$lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return round($earthRadius * $c, 2);
}
/**
* 师傅端订单抢单详情
* @ApiMethod (GET)
*/
public function orderDetail()
{
$master = $this->requireMaster();
$orderId = (int)$this->request->param('id');
if ($orderId <= 0) {
$this->error('参数错误');
}
$order = Db::name('order')->where('id', $orderId)->find();
if (!$order) {
$this->error('订单不存在');
}
$isMyOrder = ((int)$order['master_id'] === (int)$master['id']);
$inHall = in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true);
if (!$isMyOrder && !$inHall) {
$this->error('该订单已被其他师傅接单或已结束');
}
$now = time();
$quoteCount = (int)Db::name('order_quote')->where('order_id', $orderId)->count();
$myQuote = Db::name('order_quote')
->where('order_id', $orderId)
->where('master_id', $master['id'])
->find();
$maxQuotes = 4;
$remainingQuoteSlots = max(0, $maxQuotes - $quoteCount);
$viewCount = 2 + ($orderId % 3);
$deadline = (int)$order['createtime'] + 3 * 86400;
$countdownSeconds = max(0, $deadline - $now);
$mobile = $order['mobile'];
$address = $order['address'];
if (!$isMyOrder) {
$mobile = substr_replace($mobile, '******', 3, 6);
$address = preg_replace('/\d+号|\d+栋|\d+单元|\d+室/', '***', $address);
if (mb_strlen($address) > 10) {
$address = mb_substr($address, 0, 8) . '****号';
}
}
$userLat = (float)$this->request->param('lat', 0);
$userLng = (float)$this->request->param('lng', 0);
$dist = $this->calculateDistance($userLat, $userLng, (float)$order['lat'], (float)$order['lng']);
$service = Db::name('service')->where('id', $order['service_id'])->find();
$category = Db::name('service_cat')->where('id', $order['cat_id'])->find();
$goodsImages = [];
if (!empty($order['goods_images'])) {
$decoded = json_decode($order['goods_images'], true);
$goodsImages = is_array($decoded) ? $decoded : [$order['goods_images']];
}
$data = [
'id' => (int)$order['id'],
'order_no' => $order['order_no'],
'price_type' => $order['price_type'],
'price_type_text' => $order['price_type'] === 'fixed' ? '一口价' : ($order['price_type'] === 'quote' ? '师傅竞价' : ($order['price_type'] === 'custom' ? '用户定价' : '标准价')),
'status' => (int)$order['status'],
'status_text' => OrderStatus::label($order['status']),
'total_amount' => (float)$order['total_amount'],
'base_amount' => (float)$order['base_amount'],
'user_price' => $order['price_type'] === 'custom' ? (float)$order['base_amount'] : (float)$order['total_amount'],
'service_name' => $order['service_name'] ?: ($service ? $service['name'] : '家庭维修服务'),
'service_image' => $order['service_image'] ?: ($service ? $service['image'] : ''),
'goods_images' => $goodsImages,
'goods_model' => $order['goods_model'] ?: '标准规格',
'goods_num' => (int)$order['goods_num'] ?: 1,
'cat_name' => $category ? $category['name'] : '通用',
'service_type_tag' => $order['price_type'] === 'fixed' ? '一口价' : ($order['price_type'] === 'custom' ? '用户定价' : '维修'),
'remark' => $order['remark'] ?: '无特殊备注',
'contact' => $order['contact'] ? mb_substr($order['contact'], 0, 1) . '客户' : '业主客户',
'mobile' => $mobile,
'is_my_order' => $isMyOrder,
'province' => $order['province'],
'city' => $order['city'],
'district' => $order['district'],
'full_address' => $order['province'] . $order['city'] . $order['district'] . $address,
'lng' => $order['lng'],
'lat' => $order['lat'],
'distance_km' => $dist,
'appoint_time' => $order['appoint_time'],
'appoint_time_text' => $order['appoint_time'] ? date('Y-m-d H:i', (int)$order['appoint_time']) : '接单后与业主预约',
'createtime' => (int)$order['createtime'],
'createtime_text' => date('Y-m-d H:i', (int)$order['createtime']),
'view_count' => $viewCount,
'quote_count' => $quoteCount,
'remaining_quote_slots' => $remainingQuoteSlots,
'countdown_seconds' => $countdownSeconds,
'my_quote' => $myQuote ? [
'id' => (int)$myQuote['id'],
'amount' => (float)$myQuote['amount'],
'description' => $myQuote['description'],
'status' => $myQuote['status'],
] : null,
'can_grab' => ($order['price_type'] !== 'quote' && (int)$order['status'] === OrderStatus::PENDING_ACCEPT && (int)$order['master_id'] === 0),
'can_quote' => ($order['price_type'] === 'quote' && in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true) && (int)$order['master_id'] === 0 && !$myQuote),
];
$this->success('', $data);
}
}