mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 13:31:04 +00:00
fix(订单配送): 下单配送与发货方式分离,详情只展示配送信息、轨迹按需查询
虚拟发货/商家送货隐藏查看物流;管理端状态优先展示业务子状态。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2463150b5f
commit
e7e6a1a187
@ -10,7 +10,6 @@ use app\model\order\OrderCenter;
|
||||
use app\model\product\product\StoreVisit;
|
||||
use app\model\user\User;
|
||||
use app\services\product\product\StoreProductReplyServices;
|
||||
use app\services\shipping\ExpressServices;
|
||||
use app\jobs\UnpaidOrderSend;
|
||||
use crmeb\services\CacheService;
|
||||
use crmeb\basic\BaseService;
|
||||
@ -609,8 +608,7 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
// 前端 AfterSaleProgress 组件据此渲染,不再硬编码售后类型映射表
|
||||
$this->loadAfterSaleExtension($detail);
|
||||
|
||||
// 加载物流轨迹扩展数据(已发货订单调用物流查询驱动获取 tracks)
|
||||
// 前端 LogisticsTimeline 组件据此渲染物流轨迹卡片,无需点"查看物流"单独查询
|
||||
// 加载配送信息扩展(快递公司/单号);轨迹节点改由「查看物流」页按需查询,详情页不再拉第三方轨迹
|
||||
$this->loadLogisticsExtension($detail, $withRemoteLogistics);
|
||||
|
||||
// 加载订单完成态权益扩展数据(积分奖励/领券入口,系统内置能力,中台自行组装)
|
||||
@ -920,7 +918,54 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
'order' => 50,
|
||||
];
|
||||
|
||||
// 65. 核销码(有核销码的订单:自提订单 或 已支付的非自提订单如酒店预订)
|
||||
// 模块顺序:收货信息(40) → 商品信息(50) → 订单信息 → 价格信息 → 配送信息 → 发票信息
|
||||
// 支付时间/方式已在订单信息,实付在价格信息,不再单独下发「支付详情」
|
||||
// 55. 订单信息(始终存在)
|
||||
$sections[] = [
|
||||
'key' => 'order_info',
|
||||
'component' => 'info_list',
|
||||
'props' => ['fields' => $detail['order_info'] ?? [], 'title' => '订单信息'],
|
||||
'visible' => true,
|
||||
'order' => 55,
|
||||
];
|
||||
|
||||
// 60. 价格明细(始终存在)
|
||||
$sections[] = [
|
||||
'key' => 'pricing',
|
||||
'component' => 'price_detail',
|
||||
'props' => [
|
||||
'items' => $detail['pricing']['items'] ?? [],
|
||||
'pay_price' => $detail['pricing']['pay_price'] ?? '0.00',
|
||||
],
|
||||
'visible' => true,
|
||||
'order' => 60,
|
||||
];
|
||||
|
||||
// 70. 配送信息(express:公司/单号;send:送货人/电话)
|
||||
$logisticsExt = $detail['extensions']['logistics'] ?? null;
|
||||
$sections[] = [
|
||||
'key' => 'logistics',
|
||||
'component' => 'timeline',
|
||||
'props' => [
|
||||
'delivery_type' => $logisticsExt['delivery_type'] ?? '',
|
||||
'delivery_name' => $logisticsExt['delivery_name'] ?? '',
|
||||
'delivery_id' => $logisticsExt['delivery_id'] ?? '',
|
||||
],
|
||||
'visible' => !empty($logisticsExt),
|
||||
'order' => 70,
|
||||
];
|
||||
|
||||
// 75. 发票信息(从 extensions.invoice 取值,由 loadOrderInvoice 组装)
|
||||
$invoiceExt = $detail['extensions']['invoice'] ?? null;
|
||||
$sections[] = [
|
||||
'key' => 'invoice',
|
||||
'component' => 'info_list',
|
||||
'props' => ['fields' => $invoiceExt ?? [], 'title' => '发票信息'],
|
||||
'visible' => !empty($invoiceExt),
|
||||
'order' => 75,
|
||||
];
|
||||
|
||||
// 52. 核销码(自提等;紧挨商品信息,便于核销)
|
||||
// 可见性由 buildGroupedData 中 verification 对象是否构造决定,此处只需检查 code 是否存在
|
||||
$verificationVisible = !empty($detail['verification']['code']);
|
||||
$verification = $detail['verification'] ?? [];
|
||||
@ -933,72 +978,17 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
'status' => $verification['status'] ?? 'pending',
|
||||
],
|
||||
'visible' => $verificationVisible,
|
||||
'order' => 65,
|
||||
'order' => 52,
|
||||
];
|
||||
|
||||
// 70. 自定义表单
|
||||
// 82. 自定义表单(发票信息之后)
|
||||
$customForm = $detail['extensions']['custom_form'] ?? null;
|
||||
$sections[] = [
|
||||
'key' => 'custom_form',
|
||||
'component' => 'custom_form',
|
||||
'props' => ['items' => $customForm ?? []],
|
||||
'visible' => !empty($customForm),
|
||||
'order' => 70,
|
||||
];
|
||||
|
||||
// 75. 物流轨迹
|
||||
$logisticsExt = $detail['extensions']['logistics'] ?? null;
|
||||
$sections[] = [
|
||||
'key' => 'logistics',
|
||||
'component' => 'timeline',
|
||||
'props' => [
|
||||
'tracks' => $logisticsExt['tracks'] ?? [],
|
||||
'delivery_name' => $logisticsExt['delivery_name'] ?? '',
|
||||
'delivery_id' => $logisticsExt['delivery_id'] ?? '',
|
||||
],
|
||||
'visible' => !empty($logisticsExt),
|
||||
'order' => 75,
|
||||
];
|
||||
|
||||
// 80. 价格明细(始终存在)
|
||||
$sections[] = [
|
||||
'key' => 'pricing',
|
||||
'component' => 'price_detail',
|
||||
'props' => [
|
||||
'items' => $detail['pricing']['items'] ?? [],
|
||||
'pay_price' => $detail['pricing']['pay_price'] ?? '0.00',
|
||||
],
|
||||
'visible' => true,
|
||||
'order' => 80,
|
||||
];
|
||||
|
||||
// 85. 订单信息(始终存在)
|
||||
$sections[] = [
|
||||
'key' => 'order_info',
|
||||
'component' => 'info_list',
|
||||
'props' => ['fields' => $detail['order_info'] ?? [], 'title' => '订单信息'],
|
||||
'visible' => true,
|
||||
'order' => 85,
|
||||
];
|
||||
|
||||
// 90. 发票信息(从 extensions.invoice 取值,由 loadOrderInvoice 组装)
|
||||
$invoiceExt = $detail['extensions']['invoice'] ?? null;
|
||||
$sections[] = [
|
||||
'key' => 'invoice',
|
||||
'component' => 'info_list',
|
||||
'props' => ['fields' => $invoiceExt ?? [], 'title' => '发票信息'],
|
||||
'visible' => !empty($invoiceExt),
|
||||
'order' => 90,
|
||||
];
|
||||
|
||||
// 93. 支付详情(已支付订单)
|
||||
$payment = $detail['payment'] ?? null;
|
||||
$sections[] = [
|
||||
'key' => 'payment',
|
||||
'component' => 'info_list',
|
||||
'props' => ['fields' => $payment ?? [], 'title' => '支付详情'],
|
||||
'visible' => !empty($payment),
|
||||
'order' => 93,
|
||||
'order' => 82,
|
||||
];
|
||||
|
||||
// 100. 售后进度
|
||||
@ -1301,6 +1291,10 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
if (!empty($detail['invoice'])) {
|
||||
$extensions['invoice'] = $detail['invoice'];
|
||||
}
|
||||
// 发票功能开关(前端申请开票弹窗依赖 special_invoice 控制专用票可选)
|
||||
$extensions['invoice_func'] = (bool)sys_config('invoice_func_status', 0);
|
||||
$extensions['special_invoice'] = $extensions['invoice_func']
|
||||
&& (bool)sys_config('special_invoice_status', 0);
|
||||
// 上门服务订单:预约信息(供 service_appointment section 渲染)
|
||||
if (!empty($detail['service'])) {
|
||||
$extensions['service'] = $detail['service'];
|
||||
@ -1388,16 +1382,26 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
return null;
|
||||
}
|
||||
$invoice = is_object($invoice) ? $invoice->toArray() : $invoice;
|
||||
// 个人抬头电话在 drawer_phone,企业注册电话在 tell;弹窗联系电话优先 drawer_phone
|
||||
$phone = trim((string)($invoice['drawer_phone'] ?? ''));
|
||||
if ($phone === '') {
|
||||
$phone = trim((string)($invoice['tell'] ?? ''));
|
||||
}
|
||||
return [
|
||||
'name' => $invoice['name'] ?? '', // 兼容查看发票弹窗字段
|
||||
'title' => $invoice['name'] ?? '', // 发票抬头名称
|
||||
'header_type' => (int)($invoice['header_type'] ?? 1), // 1=个人 2=企业
|
||||
'type' => (int)($invoice['type'] ?? 1), // 1=电子发票 2=纸质发票
|
||||
'type' => (int)($invoice['type'] ?? 1), // 1=普通发票 2=专用发票
|
||||
'duty_number' => $invoice['duty_number'] ?? '', // 兼容弹窗字段
|
||||
'tax_number' => $invoice['duty_number'] ?? '', // 税号(企业抬头才有)
|
||||
'email' => $invoice['email'] ?? '', // 接收邮箱
|
||||
'phone' => $invoice['tell'] ?? '', // 联系电话
|
||||
'drawer_phone' => $phone, // 联系电话(弹窗字段)
|
||||
'phone' => $phone, // 联系电话(契约字段)
|
||||
'tell' => $invoice['tell'] ?? '', // 企业电话原值
|
||||
'address' => $invoice['address'] ?? '', // 地址
|
||||
'bank' => $invoice['bank'] ?? '', // 开户银行
|
||||
'bank_account' => $invoice['card_number'] ?? '', // 银行账号
|
||||
'remark' => $invoice['remark'] ?? '', // 发票备注
|
||||
'is_invoice' => (int)($invoice['is_invoice'] ?? 0), // 0=未开票 1=已开票
|
||||
'invoice_number' => $invoice['invoice_number'] ?? '', // 发票号码(已开票时才有)
|
||||
'invoice_time' => (int)($invoice['invoice_time'] ?? 0), // 开票时间
|
||||
@ -1790,78 +1794,46 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载物流轨迹扩展数据
|
||||
* 已发货且填了快递单号的订单,读取物流轨迹(默认允许同步查询第三方;详情首屏可只读缓存)并注入 extend.logistics
|
||||
* 前端 LogisticsTimeline 组件据此渲染物流轨迹卡片,用户无需点"查看物流"单独查询
|
||||
* 数据流:本方法写入 $detail['extend']['logistics']
|
||||
* → buildGroupedData() 合并到 extensions['logistics']
|
||||
* → page_config 中 component='timeline' + key='logistics' → 复合 key 'timeline:logistics'
|
||||
* → 前端 LogisticsTimeline 渲染 delivery_name/delivery_id + OrderTimeline 遍历 tracks
|
||||
* 加载配送信息扩展数据
|
||||
* - express:快递公司 + 运单号(轨迹由「查看物流」按需查询)
|
||||
* - send:送货人 + 联系电话
|
||||
* - fictitious:有人工备注时展示发货备注(无备注时 delivery_id 为自动时间戳,不展示)
|
||||
* 自提 / 上门不展示本区块
|
||||
* @param array $detail 订单详情数据(引用传递)
|
||||
* @param bool $allowRemote 是否允许同步请求第三方物流接口;false 时仅读取已有缓存,避免详情首屏被慢请求阻塞
|
||||
* @param bool $allowRemote 兼容旧调用签名,详情页不再使用
|
||||
* @return void
|
||||
*/
|
||||
protected function loadLogisticsExtension(array &$detail, bool $allowRemote = true): void
|
||||
{
|
||||
// 仅处理有快递单号的订单(已发货且填写了物流单号)
|
||||
unset($allowRemote);
|
||||
$deliveryId = (string)($detail['delivery_id'] ?? '');
|
||||
if ($deliveryId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 到店自提 / 上门服务订单无物流轨迹,仅处理快递配送
|
||||
$deliveryType = (string)($detail['delivery_type'] ?? '');
|
||||
if ($deliveryType !== '' && $deliveryType !== 'express') {
|
||||
return;
|
||||
}
|
||||
|
||||
$orderNo = (string)($detail['order_no'] ?? '');
|
||||
$deliveryName = (string)($detail['delivery_name'] ?? '');
|
||||
// delivery_code 传空字符串而非 null:一号通驱动 query() 第二参数声明为 string,null 会触发 TypeError
|
||||
$deliveryCode = (string)($detail['delivery_code'] ?? '');
|
||||
|
||||
// 物流轨迹查询(失败时 tracks 置空,仍展示快递公司 + 单号,不阻断详情渲染)
|
||||
$tracks = [];
|
||||
try {
|
||||
// 收件人手机号(顺丰等物流查询需要)
|
||||
$phone = '';
|
||||
$contact = $detail['contact'] ?? ($detail['extra_info']['contact'] ?? []);
|
||||
if (is_array($contact) && !empty($contact['phone'])) {
|
||||
$phone = (string)$contact['phone'];
|
||||
}
|
||||
|
||||
// 缓存键名:订单号 + 快递单号(ExpressServices 内部按此缓存,已签收缓存 0 秒,未签收缓存 1800 秒)
|
||||
$cacheName = $orderNo . $deliveryId;
|
||||
|
||||
/** @var ExpressServices $expressServices */
|
||||
$expressServices = $this->app->make(ExpressServices::class);
|
||||
// 详情首屏只读缓存;轨迹按需由前端调用 order/express 异步补齐,避免第三方慢请求阻塞详情
|
||||
$rawTracks = $allowRemote
|
||||
? $expressServices->query($cacheName, $deliveryId, $deliveryCode, $phone)
|
||||
: $expressServices->getCachedTracks($cacheName);
|
||||
|
||||
// 归一化轨迹字段为前端统一格式 {title, time, desc}
|
||||
// 驱动返回格式差异:一号通 {time,status} / 阿里云 {time,context} / 其他兼容
|
||||
foreach ($rawTracks ?? [] as $track) {
|
||||
if (!is_array($track)) {
|
||||
continue;
|
||||
}
|
||||
$tracks[] = [
|
||||
'title' => (string)($track['title'] ?? $track['status'] ?? $track['context'] ?? $track['name'] ?? ''),
|
||||
'time' => (string)($track['time'] ?? $track['ftime'] ?? ''),
|
||||
'desc' => (string)($track['desc'] ?? ''),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 物流查询失败不阻断订单详情渲染(驱动未配置 / 网络异常等场景降级为仅展示快递公司+单号)
|
||||
Log::warning('[OrderCenter] loadLogisticsExtension failed: ' . $e->getMessage());
|
||||
if ($deliveryId === '' && $deliveryName === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$deliveryType = (string)($detail['delivery_type']
|
||||
?? ($detail['extra_info']['delivery']['delivery_type'] ?? ''));
|
||||
if (!in_array($deliveryType, ['express', 'send', 'fictitious'], true)) {
|
||||
return;
|
||||
}
|
||||
// 快递须有运单号
|
||||
if ($deliveryType === 'express' && $deliveryId === '') {
|
||||
return;
|
||||
}
|
||||
// 虚拟发货:仅当存在发货备注时展示(未填备注时后端用 YmdHis 占位,不算备注)
|
||||
if ($deliveryType === 'fictitious') {
|
||||
if ($deliveryId === '' || preg_match('/^\d{14}$/', $deliveryId)) {
|
||||
return;
|
||||
}
|
||||
$deliveryName = $deliveryName !== '' ? $deliveryName : '虚拟发货';
|
||||
}
|
||||
|
||||
// 始终写入 extend.logistics:即使 tracks 查询失败,也展示快递公司 + 单号
|
||||
$detail['extend']['logistics'] = [
|
||||
'delivery_type' => $deliveryType,
|
||||
'delivery_name' => $deliveryName,
|
||||
'delivery_id' => $deliveryId,
|
||||
'tracks' => $tracks,
|
||||
'tracks' => [],
|
||||
];
|
||||
}
|
||||
|
||||
@ -2075,6 +2047,61 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
unset($button);
|
||||
}
|
||||
|
||||
// 已支付且无退款时,按发票开关/已有申请记录追加「申请开票 / 查看发票」(左下角「更多」)
|
||||
return $this->appendInvoiceButtons($buttons, $detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加发票相关底部按钮(对齐旧版订单详情左下角「更多」)
|
||||
*
|
||||
* 条件:已支付且无退款;开启发票功能时可申请;已有开票申请/快照时可查看。
|
||||
* placement=more 由前端 ButtonGroup 收入左下角「更多」菜单,不占右侧主操作位。
|
||||
*
|
||||
* @param array $buttons 已有按钮
|
||||
* @param array $detail 订单详情
|
||||
* @return array
|
||||
*/
|
||||
protected function appendInvoiceButtons(array $buttons, array $detail): array
|
||||
{
|
||||
$payStatus = (int)($detail['pay_status'] ?? 0);
|
||||
$refundStatus = (int)($detail['refund_status'] ?? 0);
|
||||
if ($payStatus !== 1 || $refundStatus !== 0) {
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
$invoiceFunc = (bool)sys_config('invoice_func_status', 0);
|
||||
$hasInvoice = !empty($detail['invoice']);
|
||||
if (!$invoiceFunc && !$hasInvoice) {
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
$orderNo = (string)($detail['order_no'] ?? '');
|
||||
$params = $orderNo !== '' ? ['order_no' => $orderNo] : [];
|
||||
|
||||
// 避免适配器或其它扩展重复注入
|
||||
$existing = [];
|
||||
foreach ($buttons as $btn) {
|
||||
$existing[(string)($btn['action'] ?? '')] = true;
|
||||
}
|
||||
|
||||
if ($hasInvoice && empty($existing['viewInvoice'])) {
|
||||
$buttons[] = [
|
||||
'text' => '查看发票',
|
||||
'action' => 'viewInvoice',
|
||||
'primary' => false,
|
||||
'placement' => 'more',
|
||||
'params' => $params,
|
||||
];
|
||||
} elseif ($invoiceFunc && !$hasInvoice && empty($existing['applyInvoice'])) {
|
||||
$buttons[] = [
|
||||
'text' => '申请开票',
|
||||
'action' => 'applyInvoice',
|
||||
'primary' => false,
|
||||
'placement' => 'more',
|
||||
'params' => $params,
|
||||
];
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
@ -2853,7 +2880,11 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
}
|
||||
// 订单扩展信息
|
||||
$item['extra_info'] = $this->parseExtraInfo($item['extra_info']);
|
||||
$item['business_status'] = $item['extra_info']['business_status'] ?? null;
|
||||
$businessStatus = $item['extra_info']['business_status'] ?? null;
|
||||
if (is_array($businessStatus) && empty($businessStatus['label']) && !empty($businessStatus['text'])) {
|
||||
$businessStatus['label'] = $businessStatus['text'];
|
||||
}
|
||||
$item['business_status'] = is_array($businessStatus) ? $businessStatus : null;
|
||||
// 提取联系人信息到顶层,供列表页展示
|
||||
$item['contact_name'] = $item['extra_info']['contact']['name'] ?? '';
|
||||
$item['contact_phone'] = $item['extra_info']['contact']['phone'] ?? '';
|
||||
|
||||
@ -380,7 +380,7 @@ class NormalOrderAdapter extends BaseOrderTypeAdapter
|
||||
}
|
||||
}
|
||||
|
||||
// 到店自提 / 上门服务无快递物流,列表与详情都不展示「查看物流」
|
||||
// 非快递履约(自提/上门/虚拟发货/商家送货)不展示「查看物流」
|
||||
return $this->filterLogisticsButtons($buttons, $detail);
|
||||
}
|
||||
|
||||
@ -401,12 +401,13 @@ class NormalOrderAdapter extends BaseOrderTypeAdapter
|
||||
}
|
||||
|
||||
/**
|
||||
* 到店自提(shipping_type=2)/ 上门服务(shipping_type=3)去掉「查看物流」
|
||||
* 无真实快递轨迹时去掉「查看物流」:
|
||||
* - 到店自提 / 上门服务
|
||||
* - 虚拟发货(fictitious)/ 商家送货(send)
|
||||
*/
|
||||
protected function filterLogisticsButtons(array $buttons, array $detail): array
|
||||
{
|
||||
$shippingType = (int)($detail['shipping_type'] ?? 0);
|
||||
if (!in_array($shippingType, [2, 3], true)) {
|
||||
if ($this->hasExpressLogistics($detail)) {
|
||||
return $buttons;
|
||||
}
|
||||
return array_values(array_filter(
|
||||
@ -415,6 +416,28 @@ class NormalOrderAdapter extends BaseOrderTypeAdapter
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否具备可查询的快递物流(仅 express 发货)
|
||||
*/
|
||||
protected function hasExpressLogistics(array $detail): bool
|
||||
{
|
||||
$shippingType = (int)($detail['shipping_type'] ?? 0);
|
||||
if (in_array($shippingType, [2, 3], true)) {
|
||||
return false;
|
||||
}
|
||||
$deliveryType = (string)($detail['delivery_type']
|
||||
?? ($detail['extra_info']['delivery']['delivery_type'] ?? ''));
|
||||
if (in_array($deliveryType, ['fictitious', 'send', 'pickup', 'door'], true)) {
|
||||
return false;
|
||||
}
|
||||
// 兼容:发货方式字段被误写成 express,但公司名为「虚拟发货」
|
||||
if ((string)($detail['delivery_name'] ?? '') === '虚拟发货') {
|
||||
return false;
|
||||
}
|
||||
// 仅快递发货且已有运单号才展示查看物流
|
||||
return $deliveryType === 'express' && (string)($detail['delivery_id'] ?? '') !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否送礼订单
|
||||
*/
|
||||
|
||||
@ -90,19 +90,33 @@ class OrderCenterDetailExtendListener implements ListenerInterface
|
||||
}
|
||||
}
|
||||
|
||||
// 发货配送方式以 extra_info.delivery.delivery_type(发货时写入)为准;
|
||||
// 顶层 extra_info.delivery_type 是下单收货方式(express/pickup/door),
|
||||
// 存量订单发货后两者可能不一致,不能让顶层旧值覆盖实际发货方式
|
||||
$deliveryType = (string)(($extraInfo['delivery']['delivery_type'] ?? '') ?: ($extraInfo['delivery_type'] ?? ''));
|
||||
if ($deliveryType === '') {
|
||||
// 下单配送方式(express/pickup/door)与发货方式(express/send/fictitious)语义不同,禁止互相覆盖。
|
||||
// extra_info.delivery_type:下单时写入,供详情「配送方式」展示;
|
||||
// detail.delivery_type / extra_info.delivery.delivery_type:发货后写入,供物流/虚拟发货展示。
|
||||
$orderDeliveryType = (string)($extraInfo['delivery_type'] ?? '');
|
||||
if (!in_array($orderDeliveryType, ['express', 'pickup', 'door'], true)) {
|
||||
// 兼容被误写成发货方式(fictitious/send)的旧数据:按 shipping_type 回退
|
||||
$shippingType = (int)($detail['shipping_type'] ?? 0);
|
||||
$deliveryType = $shippingType === 2 ? 'pickup' : ($shippingType === 3 ? 'door' : 'express');
|
||||
$orderDeliveryType = $shippingType === 2 ? 'pickup' : ($shippingType === 3 ? 'door' : 'express');
|
||||
}
|
||||
$extraInfo['delivery_type'] = $orderDeliveryType;
|
||||
|
||||
$shipDeliveryType = (string)($detail['delivery_type'] ?? '');
|
||||
if (!in_array($shipDeliveryType, ['express', 'send', 'fictitious'], true)) {
|
||||
$nestedShipType = (string)(($extraInfo['delivery']['delivery_type'] ?? ''));
|
||||
$shipDeliveryType = in_array($nestedShipType, ['express', 'send', 'fictitious'], true)
|
||||
? $nestedShipType
|
||||
: '';
|
||||
}
|
||||
if ($shipDeliveryType !== '') {
|
||||
$detail['delivery_type'] = $shipDeliveryType;
|
||||
$extraInfo['delivery'] = array_merge($extraInfo['delivery'] ?? [], [
|
||||
'delivery_type' => $shipDeliveryType,
|
||||
]);
|
||||
} else {
|
||||
// 未发货:不把下单配送方式写回顶层 delivery_type,避免物流区误判为已快递发货
|
||||
$detail['delivery_type'] = $detail['delivery_type'] ?? '';
|
||||
}
|
||||
$extraInfo['delivery_type'] = $deliveryType;
|
||||
$extraInfo['delivery'] = array_merge($extraInfo['delivery'] ?? [], [
|
||||
'delivery_type' => $deliveryType,
|
||||
]);
|
||||
$detail['delivery_type'] = $deliveryType;
|
||||
|
||||
// 前端统一的到店自提/上门服务标识(与中台保持一致计算口径)
|
||||
$isPickup = ($detail['shipping_type'] ?? 0) == 2;
|
||||
|
||||
@ -97,28 +97,37 @@ class OrderCenterListExtendListener
|
||||
$delivery = $extraInfo['delivery'] ?? [];
|
||||
$extDelivery = $mappedExtDeliveries[$orderNo] ?? [];
|
||||
|
||||
$deliveryType = (string)($delivery['delivery_type'] ?? ($extraInfo['delivery_type'] ?? ''));
|
||||
if ($deliveryType === '') {
|
||||
$deliveryType = (string)($extDelivery['delivery_type'] ?? '');
|
||||
}
|
||||
if ($deliveryType === '') {
|
||||
// 下单配送方式(express/pickup/door)与发货方式(express/send/fictitious)分离,禁止互相覆盖
|
||||
$orderDeliveryType = (string)($extraInfo['delivery_type'] ?? '');
|
||||
if (!in_array($orderDeliveryType, ['express', 'pickup', 'door'], true)) {
|
||||
$shippingType = (int)($item['shipping_type'] ?? 0);
|
||||
$deliveryType = $shippingType === 2 ? 'pickup' : ($shippingType === 3 ? 'door' : 'express');
|
||||
$orderDeliveryType = $shippingType === 2 ? 'pickup' : ($shippingType === 3 ? 'door' : 'express');
|
||||
}
|
||||
|
||||
$shipDeliveryType = (string)($delivery['delivery_type'] ?? '');
|
||||
if (!in_array($shipDeliveryType, ['express', 'send', 'fictitious'], true)) {
|
||||
$shipDeliveryType = (string)($extDelivery['delivery_type'] ?? '');
|
||||
}
|
||||
if (!in_array($shipDeliveryType, ['express', 'send', 'fictitious'], true)) {
|
||||
$shipDeliveryType = '';
|
||||
}
|
||||
|
||||
$deliveryName = (string)($delivery['delivery_name'] ?? ($extDelivery['delivery_name'] ?? ''));
|
||||
$deliveryId = (string)($delivery['delivery_id'] ?? ($extDelivery['delivery_id'] ?? ''));
|
||||
|
||||
$item['delivery_type'] = $deliveryType;
|
||||
// 顶层 delivery_type:已发货用发货方式,未发货用下单配送方式(列表按钮/标签依赖)
|
||||
$item['delivery_type'] = $shipDeliveryType !== '' ? $shipDeliveryType : $orderDeliveryType;
|
||||
$item['delivery_name'] = $deliveryName;
|
||||
$item['delivery_id'] = $deliveryId;
|
||||
|
||||
$extraInfo['delivery_type'] = $deliveryType;
|
||||
$extraInfo['delivery'] = array_merge($delivery, [
|
||||
'delivery_type' => $deliveryType,
|
||||
'delivery_name' => $deliveryName,
|
||||
'delivery_id' => $deliveryId,
|
||||
]);
|
||||
$extraInfo['delivery_type'] = $orderDeliveryType;
|
||||
if ($shipDeliveryType !== '') {
|
||||
$extraInfo['delivery'] = array_merge($delivery, [
|
||||
'delivery_type' => $shipDeliveryType,
|
||||
'delivery_name' => $deliveryName,
|
||||
'delivery_id' => $deliveryId,
|
||||
]);
|
||||
}
|
||||
$item['extra_info'] = $extraInfo;
|
||||
}
|
||||
unset($item);
|
||||
|
||||
@ -103,6 +103,10 @@
|
||||
<div>付费会员优惠:</div>
|
||||
<div class="value">¥{{ data.vip_discount || '0.00' }}</div>
|
||||
</li>
|
||||
<li class="item" v-if="Number(data.gift_price) > 0">
|
||||
<div>礼品附加费:</div>
|
||||
<div class="value">¥{{ data.gift_price || '0.00' }}</div>
|
||||
</li>
|
||||
<li class="item">
|
||||
<div>实际支付:</div>
|
||||
<div class="value price">¥{{ data.pay_price || '0.00' }}</div>
|
||||
@ -150,7 +154,7 @@
|
||||
<div>{{ isExpress ? '物流公司:' : deliveryType === 'send' ? '送货人姓名:' : '发货方式:' }}</div>
|
||||
<div class="value">{{ data.delivery_name || '--' }}</div>
|
||||
</li>
|
||||
<li class="item">
|
||||
<li class="item" v-if="deliveryType !== 'fictitious' || data.delivery_id">
|
||||
<div>{{ isExpress ? '物流单号:' : deliveryType === 'send' ? '送货人电话:' : '发货备注:' }}</div>
|
||||
<div class="value">
|
||||
{{ data.delivery_id || '--' }}
|
||||
|
||||
@ -115,10 +115,47 @@ function getLogisticsData() {
|
||||
});
|
||||
}
|
||||
|
||||
/** 详情状态:用户已删除优先展示「已删除」,否则按 status_code / status_text */
|
||||
const CENTER_STATUS_CODES = [
|
||||
'pending_payment',
|
||||
'processing',
|
||||
'pending_review',
|
||||
'completed',
|
||||
'canceled',
|
||||
'after_sale',
|
||||
'refunding',
|
||||
'refunded',
|
||||
'deleted',
|
||||
];
|
||||
|
||||
function getBusinessStatusText(detail) {
|
||||
if (!detail || typeof detail === 'string') return '';
|
||||
const bs = detail.business_status;
|
||||
if (!bs || typeof bs !== 'object') return '';
|
||||
const text = String(bs.label || bs.text || '').trim();
|
||||
if (!text) return '';
|
||||
const code = String(bs.code || '');
|
||||
const genericTexts = [
|
||||
'待支付',
|
||||
'待处理',
|
||||
'进行中',
|
||||
'待评价',
|
||||
'已完成',
|
||||
'已取消',
|
||||
'售后中',
|
||||
'退款中',
|
||||
'已退款',
|
||||
'已删除',
|
||||
];
|
||||
if (!genericTexts.includes(text)) return text;
|
||||
if (code && !CENTER_STATUS_CODES.includes(code)) return text;
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 有业务子状态(待发货/待收货等)时只展示这一项,否则按中台 status_code */
|
||||
function getStatusLabel(detail) {
|
||||
if (detail && Number(detail.is_del) === 1) return '已删除';
|
||||
if (detail?.status_text) return detail.status_text;
|
||||
const businessText = getBusinessStatusText(detail);
|
||||
if (businessText) return businessText;
|
||||
const code = typeof detail === 'string' ? detail : detail?.status_code;
|
||||
const map = {
|
||||
pending_payment: '待支付',
|
||||
@ -131,12 +168,13 @@ function getStatusLabel(detail) {
|
||||
refunded: '已退款',
|
||||
deleted: '已删除',
|
||||
};
|
||||
return map[code] || code || '--';
|
||||
return map[code] || detail?.status_text || code || '--';
|
||||
}
|
||||
|
||||
/* 与列表 getStatusClass 同口径 */
|
||||
function getStatusClass(detail) {
|
||||
if (detail && Number(detail.is_del) === 1) return 'danger';
|
||||
if (getBusinessStatusText(detail)) return 'primary';
|
||||
const code = typeof detail === 'string' ? detail : detail?.status_code;
|
||||
const map = {
|
||||
pending_payment: 'warning',
|
||||
@ -187,6 +225,13 @@ defineExpose({ loadDetail });
|
||||
line-height: 40px !important;
|
||||
}
|
||||
|
||||
.business-status {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.head {
|
||||
padding: 0 30px 24px;
|
||||
|
||||
|
||||
@ -205,9 +205,6 @@
|
||||
<el-table-column label="订单状态" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusClass(scope.row)">{{ getStatusLabel(scope.row) }}</el-tag>
|
||||
<div v-if="Number(scope.row.is_del) !== 1 && scope.row.business_status">
|
||||
<span class="business-status">{{ scope.row.business_status.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="160">
|
||||
@ -469,9 +466,36 @@ function getProductTypeLabel(type) {
|
||||
return item ? item.label : type || '--';
|
||||
}
|
||||
|
||||
const CENTER_STATUS_CODES = [
|
||||
'pending_payment',
|
||||
'processing',
|
||||
'pending_review',
|
||||
'completed',
|
||||
'canceled',
|
||||
'after_sale',
|
||||
'refunding',
|
||||
'refunded',
|
||||
'deleted',
|
||||
];
|
||||
|
||||
function getBusinessStatusText(row) {
|
||||
if (!row || typeof row !== 'object') return '';
|
||||
const bs = row.business_status;
|
||||
if (!bs || typeof bs !== 'object') return '';
|
||||
const text = String(bs.label || bs.text || '').trim();
|
||||
if (!text) return '';
|
||||
const code = String(bs.code || '');
|
||||
const genericTexts = statusCodeOptions.map((o) => o.label).concat(['进行中']);
|
||||
if (!genericTexts.includes(text)) return text;
|
||||
if (code && !CENTER_STATUS_CODES.includes(code)) return text;
|
||||
return '';
|
||||
}
|
||||
|
||||
function getStatusLabel(rowOrCode) {
|
||||
const row = typeof rowOrCode === 'object' && rowOrCode ? rowOrCode : null;
|
||||
if (row && Number(row.is_del) === 1) return '已删除';
|
||||
const businessText = getBusinessStatusText(row);
|
||||
if (businessText) return businessText;
|
||||
const code = row ? row.status_code : rowOrCode;
|
||||
const item = statusCodeOptions.find((opt) => opt.key === code);
|
||||
return item ? item.label : code || '--';
|
||||
@ -480,6 +504,7 @@ function getStatusLabel(rowOrCode) {
|
||||
function getStatusClass(rowOrCode) {
|
||||
const row = typeof rowOrCode === 'object' && rowOrCode ? rowOrCode : null;
|
||||
if (row && Number(row.is_del) === 1) return 'danger';
|
||||
if (getBusinessStatusText(row)) return 'primary';
|
||||
const code = row ? row.status_code : rowOrCode;
|
||||
const map = {
|
||||
pending_payment: 'warning',
|
||||
|
||||
@ -84,7 +84,7 @@
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 配送方式 -->
|
||||
<!-- 配送方式(下单时 express/pickup/door) -->
|
||||
<div class="section">
|
||||
<div class="section-title">配送方式</div>
|
||||
<el-row :gutter="24" class="detail-grid">
|
||||
@ -97,25 +97,23 @@
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 发货/物流信息 -->
|
||||
<!-- 发货/物流信息(发货后 express/send/fictitious) -->
|
||||
<div class="section" v-if="detailData.delivery_name">
|
||||
<div class="section-title">{{ isExpressDelivery ? '物流信息' : '发货信息' }}</div>
|
||||
<div class="section-title">{{ shipInfoSectionTitle }}</div>
|
||||
<el-row :gutter="24" class="detail-grid">
|
||||
<el-col :span="8"
|
||||
><div class="detail-item">
|
||||
<span class="label">{{ isExpressDelivery ? '物流公司:' : deliveryType === 'send' ? '送货人姓名:' : '发货方式:' }}</span
|
||||
<span class="label">{{ shipInfoNameLabel }}</span
|
||||
><span class="value">{{ detailData.delivery_name || '--' }}</span>
|
||||
</div></el-col
|
||||
>
|
||||
<el-col :span="12"
|
||||
<el-col :span="12" v-if="shipDeliveryType !== 'fictitious' || detailData.delivery_id"
|
||||
><div class="detail-item">
|
||||
<span class="label">{{
|
||||
isExpressDelivery ? '物流单号:' : deliveryType === 'send' ? '送货人电话:' : '发货备注:'
|
||||
}}</span
|
||||
<span class="label">{{ shipInfoIdLabel }}</span
|
||||
><span class="value"
|
||||
>{{ detailData.delivery_id || '--'
|
||||
}}<a
|
||||
v-if="isExpressDelivery"
|
||||
v-if="shipDeliveryType === 'express'"
|
||||
v-db-click
|
||||
@click="toggleLogistics"
|
||||
class="track-link"
|
||||
@ -125,8 +123,8 @@
|
||||
</div></el-col
|
||||
>
|
||||
</el-row>
|
||||
<!-- 物流轨迹展开区域 -->
|
||||
<div v-if="logisticsVisible" class="logistics-panel">
|
||||
<!-- 物流轨迹展开区域(仅真实快递) -->
|
||||
<div v-if="logisticsVisible && shipDeliveryType === 'express'" class="logistics-panel">
|
||||
<div class="logistics-header">
|
||||
<img class="logistics-icon" src="../../../../assets/images/expressi.jpg" />
|
||||
<div class="logistics-info">
|
||||
@ -470,21 +468,47 @@ const orderItems = computed(() => {
|
||||
/** 订单状态与列表同口径(待发货/待收货/待到店等业务状态) */
|
||||
const businessStatus = computed(() => getOrderBusinessStatus(detailData.value || {}));
|
||||
|
||||
/** 配送方式:express 快递 / send 送货 / fictitious 无需配送 / pickup 自提 / door 上门 */
|
||||
const deliveryType = computed(
|
||||
() => detailData.value?.extra_info?.delivery_type || detailData.value?.delivery_type || '',
|
||||
);
|
||||
/** 下单配送方式(express/pickup/door),用于「配送方式」区块 */
|
||||
const orderDeliveryType = computed(() => {
|
||||
const t = String(detailData.value?.extra_info?.delivery_type || '');
|
||||
if (['express', 'pickup', 'door'].includes(t)) return t;
|
||||
const shippingType = Number(detailData.value?.shipping_type || 0);
|
||||
if (shippingType === 2) return 'pickup';
|
||||
if (shippingType === 3) return 'door';
|
||||
return 'express';
|
||||
});
|
||||
const deliveryTypeText = computed(
|
||||
() =>
|
||||
({
|
||||
express: '快递配送',
|
||||
send: '送货上门',
|
||||
fictitious: '无需配送',
|
||||
pickup: '到店自提',
|
||||
door: '上门服务',
|
||||
})[deliveryType.value] || '快递配送',
|
||||
})[orderDeliveryType.value] || '快递配送',
|
||||
);
|
||||
const isExpressDelivery = computed(() => deliveryType.value === 'express');
|
||||
|
||||
/** 发货方式(express/send/fictitious);未发货为空 */
|
||||
const shipDeliveryType = computed(() => {
|
||||
const t = String(detailData.value?.delivery_type || '');
|
||||
return ['express', 'send', 'fictitious'].includes(t) && detailData.value?.delivery_name ? t : '';
|
||||
});
|
||||
|
||||
const shipInfoSectionTitle = computed(() => {
|
||||
if (shipDeliveryType.value === 'fictitious') return '发货信息';
|
||||
if (shipDeliveryType.value === 'send') return '送货人信息';
|
||||
return '物流信息';
|
||||
});
|
||||
|
||||
const shipInfoNameLabel = computed(() => {
|
||||
if (shipDeliveryType.value === 'fictitious') return '发货方式:';
|
||||
if (shipDeliveryType.value === 'send') return '送货人姓名:';
|
||||
return '物流公司:';
|
||||
});
|
||||
|
||||
const shipInfoIdLabel = computed(() => {
|
||||
if (shipDeliveryType.value === 'fictitious') return '发货备注:';
|
||||
if (shipDeliveryType.value === 'send') return '送货人电话:';
|
||||
return '物流单号:';
|
||||
});
|
||||
|
||||
/** 售后进度数据(后端注入到 detail.extend.after_sale) */
|
||||
const afterSaleInfo = computed(() => {
|
||||
|
||||
@ -1,55 +1,120 @@
|
||||
<!--
|
||||
物流轨迹组件(timeline:logistics 专用)
|
||||
自包含:卡片标题"物流轨迹" + 物流头部(快递公司+单号) + 内嵌 OrderTimeline 时间线
|
||||
复用 OrderTimeline 基元渲染时间线节点
|
||||
配送信息卡片(timeline:logistics)
|
||||
express:快递公司 + 快递单号;send:送货人 + 联系电话;fictitious:发货方式 + 发货备注
|
||||
-->
|
||||
<template>
|
||||
<view v-if="tracks.length > 0 || deliveryName || deliveryId" class="info-card">
|
||||
<view class="card-title">{{ $t('物流轨迹') }}</view>
|
||||
<!-- 物流头部:快递公司 + 快递单号 -->
|
||||
<view v-if="deliveryName || deliveryId" class="logistics-header">
|
||||
<text>{{ deliveryName }} {{ deliveryId }}</text>
|
||||
<view v-if="deliveryName || deliveryId" class="info-card delivery-info-card">
|
||||
<view class="card-title">{{ $t('配送信息') }}</view>
|
||||
<view v-if="deliveryName" class="info-row acea-row row-between">
|
||||
<text class="info-label">{{ nameLabel }}</text>
|
||||
<text class="info-value">{{ deliveryName }}</text>
|
||||
</view>
|
||||
<view v-if="deliveryId" class="info-row acea-row row-between">
|
||||
<text class="info-label">{{ idLabel }}</text>
|
||||
<view class="info-value acea-row row-middle row-right">
|
||||
<text class="express-no">{{ deliveryId }}</text>
|
||||
<text class="copy" @tap="onCopy">{{ $t('复制') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 时间线节点(复用 OrderTimeline 基元) -->
|
||||
<order-timeline :timeline="tracks" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 物流轨迹组件
|
||||
* 展示快递公司、单号、物流轨迹时间线
|
||||
* 复用 OrderTimeline 基元渲染时间线节点
|
||||
* 配送信息卡片
|
||||
* 通过复合 componentKey='timeline:logistics' 匹配 section
|
||||
*/
|
||||
export default {
|
||||
name: 'LogisticsTimeline',
|
||||
// 复合 key,匹配 section.component='timeline' + section.key='logistics'
|
||||
componentKey: 'timeline:logistics',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import OrderTimeline from './OrderTimeline.vue';
|
||||
import i18n from '@/utils/lang.js';
|
||||
|
||||
const props = defineProps({
|
||||
/** section 对象,含 props.delivery_name / delivery_id / tracks */
|
||||
/** section:props.delivery_type / delivery_name / delivery_id */
|
||||
section: { type: Object, required: true },
|
||||
});
|
||||
|
||||
/** 后端直填 props */
|
||||
const p = computed(() => (props.section && props.section.props) || {});
|
||||
/** 快递公司名称 */
|
||||
const deliveryType = computed(() => p.value.delivery_type || 'express');
|
||||
const deliveryName = computed(() => p.value.delivery_name || '');
|
||||
/** 快递单号 */
|
||||
const deliveryId = computed(() => p.value.delivery_id || '');
|
||||
/** 物流轨迹节点数组 */
|
||||
const tracks = computed(() => p.value.tracks || []);
|
||||
|
||||
const nameLabel = computed(() => {
|
||||
if (deliveryType.value === 'send') return i18n.t('送货人');
|
||||
if (deliveryType.value === 'fictitious') return i18n.t('发货方式');
|
||||
return i18n.t('快递公司');
|
||||
});
|
||||
|
||||
const idLabel = computed(() => {
|
||||
if (deliveryType.value === 'send') return i18n.t('联系电话');
|
||||
if (deliveryType.value === 'fictitious') return i18n.t('发货备注');
|
||||
return i18n.t('快递单号');
|
||||
});
|
||||
|
||||
/** 复制单号 / 电话 / 备注 */
|
||||
function onCopy() {
|
||||
const no = deliveryId.value;
|
||||
if (!no) return;
|
||||
// #ifdef H5
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(no);
|
||||
uni.showToast({ title: i18n.t('已复制'), icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
uni.setClipboardData({
|
||||
data: no,
|
||||
success: () => {
|
||||
uni.showToast({ title: i18n.t('已复制'), icon: 'none' });
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-card { margin: 20rpx; background: #fff; border-radius: 16rpx; padding: 30rpx; }
|
||||
.card-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
|
||||
/* 物流头部:快递公司 + 单号 */
|
||||
.logistics-header { padding: 12rpx 0; font-size: 26rpx; color: #666; border-bottom: 1rpx solid #f5f5f5; margin-bottom: 16rpx; }
|
||||
.delivery-info-card {
|
||||
margin: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12rpx 0;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.info-label {
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info-value {
|
||||
color: #333;
|
||||
text-align: right;
|
||||
max-width: 70%;
|
||||
}
|
||||
.express-no {
|
||||
word-break: break-all;
|
||||
}
|
||||
.copy {
|
||||
margin-left: 12rpx;
|
||||
padding: 2rpx 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--view-theme, #e93323);
|
||||
border: 1rpx solid var(--view-theme, #e93323);
|
||||
border-radius: 6rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user