diff --git a/backend/app/services/order/OrderCenterServices.php b/backend/app/services/order/OrderCenterServices.php index 9a3817be9..36add57c3 100644 --- a/backend/app/services/order/OrderCenterServices.php +++ b/backend/app/services/order/OrderCenterServices.php @@ -500,6 +500,17 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac // 确保 buildGroupedData 和前端引擎都能拿到正确的状态文案 $detail['status_text'] = OrderCenter::$statusMap[$detail['status_code'] ?? ''] ?? ($detail['status_code'] ?? ''); + // 用户已删除:详情状态统一展示「已删除」(is_del 为软删标记,不改写 status_code) + if ((int)($detail['is_del'] ?? 0) === 1) { + $detail['status_text'] = '已删除'; + $detail['business_status'] = [ + 'code' => 'deleted', + 'label' => '已删除', + 'text' => '已删除', + 'type' => 'danger', + ]; + } + // 构建操作按钮(委托给产品类型 Handler) $detail['order_buttons'] = $this->getOrderButtonsFromHandler($detail); // 构建订单生命周期进度条(不同订单类型节点不同,委托给产品类型 Handler) @@ -2622,7 +2633,8 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac /** * 构建关键词搜索闭包(后台订单列表/统计共用) * 按 search_field 限定搜索字段:order_no=订单号前缀、contact_name=收货人、 - * contact_phone=联系电话、uid=用户ID精确匹配,all/其它=订单号+联系人+电话 + * contact_phone=联系电话、uid=用户ID精确匹配; + * all/其它=用户相关(收货人+电话,纯数字兼查 UID),订单号走独立 order_no 条件 * 收货人/电话存于 extra_info JSON,走 JSON 路径匹配 * @param string $keyword 关键词(调用方需先 trim) * @param string $searchField 搜索字段标识 @@ -2651,10 +2663,12 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac $q->where('uid', '=', (int) $keyword); break; default: - // 全部字段:订单号前缀 + 收货人 + 电话 - $q->whereOr('order_no', 'LIKE', "{$escaped}%") - ->whereOrRaw("extra_info->>'$.contact.name' LIKE ?", ["%{$escaped}%"]) + // 用户搜索「全部」:收货人 + 电话;纯数字时兼查 UID(订单号由独立筛选项承担) + $q->whereRaw("extra_info->>'$.contact.name' LIKE ?", ["%{$escaped}%"]) ->whereOrRaw("extra_info->>'$.contact.phone' LIKE ?", ["%{$escaped}%"]); + if (ctype_digit((string) $keyword)) { + $q->whereOr('uid', '=', (int) $keyword); + } } }); }; @@ -2764,20 +2778,25 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac } } + // 扩展表子状态(如待发货/待收货)经 _order_nos 两步筛选;不可因无 extra_conditions 而丢弃 + $orderNos = $where['_order_nos'] ?? []; + if (empty($extraConditions)) { - // 无扩展条件:直接走 DAO 方法 + // 无闭包扩展条件:直接走 DAO 方法 $finalWhere = $searchWhere; + if (!empty($orderNos)) { + $finalWhere[] = ['order_no', 'IN', $orderNos]; + } if (!empty($afterSaleOrderNos)) { $finalWhere[] = ['order_no', 'IN', $afterSaleOrderNos]; } $list = $this->dao->selectList($finalWhere, $fields, $page, $limit, 'id DESC'); $total = $this->dao->count($finalWhere); } else { - // 有扩展条件:用 DAO 提供的查询构建器,避免直接 Db::table + // 有闭包扩展条件:用 DAO 提供的查询构建器,避免直接 Db::table $query = $this->dao->getDb()->where($searchWhere); // 扩展表条件用两步查询(先查 order_no,再 IN),不 JOIN - $orderNos = $where['_order_nos'] ?? []; if (!empty($orderNos)) { $query->whereIn('order_no', $orderNos); } diff --git a/backend/modules/product/normal/controller/admin/OrderController.php b/backend/modules/product/normal/controller/admin/OrderController.php index a1e956357..14040c837 100644 --- a/backend/modules/product/normal/controller/admin/OrderController.php +++ b/backend/modules/product/normal/controller/admin/OrderController.php @@ -1243,6 +1243,23 @@ class OrderController extends AuthController ]); $where['order_category'] = 'goods'; $where['product_type'] = 'normal'; + $where['is_deleted'] = !empty($where['is_deleted']); + + // 订单类型映射:与 list 接口一致 + $type = $where['type'] ?? ''; + if (!empty($type)) { + $activityType = $this->mapOrderType($type); + if ($activityType !== '') { + $where['activity_type'] = $activityType; + } + } + unset($where['type']); + + // 业务状态(待发货/待收货等)与列表同口径翻译 + $businessStatus = (string)$this->request->get('business_status', ''); + if ($businessStatus !== '') { + $this->translateBusinessStatus($businessStatus, $where); + } try { $result = $centerServices->getAdminList($where, (int)$where['page'], (int)$where['limit']); @@ -1259,12 +1276,18 @@ class OrderController extends AuthController $exportData = []; foreach ($list as $item) { - $items = $item['items'] ?? []; - $storeName = ''; - if (!empty($items)) { - $first = $items[0]; - $storeName = $first['store_name'] ?? $first['title'] ?? ''; + // 列表接口无顶层 items,商品名在中台 title 或 extra_info.items[].store_name + $goodsItems = $item['extra_info']['items'] ?? ($item['items'] ?? []); + $goodsNames = []; + foreach ((array)$goodsItems as $goods) { + $name = trim((string)($goods['store_name'] ?? $goods['title'] ?? $goods['name'] ?? '')); + if ($name !== '') { + $goodsNames[] = $name; + } } + $storeName = $goodsNames + ? implode(' / ', $goodsNames) + : (string)($item['title'] ?? $item['subtitle'] ?? ''); $exportData[] = [ 'order_no' => $item['order_no'] ?? '', diff --git a/frontend/admin/src/pages/order/orderCenter/detail.vue b/frontend/admin/src/pages/order/orderCenter/detail.vue index e89acd9d1..a82578f8a 100644 --- a/frontend/admin/src/pages/order/orderCenter/detail.vue +++ b/frontend/admin/src/pages/order/orderCenter/detail.vue @@ -14,10 +14,11 @@
  • 订单状态
    - {{ - getStatusLabel(detailData.status_code) - }} - + {{ getStatusLabel(detailData) }} + 退款中
    @@ -114,7 +115,11 @@ function getLogisticsData() { }); } -function getStatusLabel(code) { +/** 详情状态:用户已删除优先展示「已删除」,否则按 status_code / status_text */ +function getStatusLabel(detail) { + if (detail && Number(detail.is_del) === 1) return '已删除'; + if (detail?.status_text) return detail.status_text; + const code = typeof detail === 'string' ? detail : detail?.status_code; const map = { pending_payment: '待支付', processing: '待处理', @@ -124,12 +129,15 @@ function getStatusLabel(code) { after_sale: '售后中', refunding: '退款中', refunded: '已退款', + deleted: '已删除', }; return map[code] || code || '--'; } /* 与列表 getStatusClass 同口径 */ -function getStatusClass(code) { +function getStatusClass(detail) { + if (detail && Number(detail.is_del) === 1) return 'danger'; + const code = typeof detail === 'string' ? detail : detail?.status_code; const map = { pending_payment: 'warning', processing: 'primary', @@ -139,6 +147,7 @@ function getStatusClass(code) { after_sale: 'warning', refunding: 'warning', refunded: 'info', + deleted: 'danger', }; return map[code] || 'info'; } diff --git a/frontend/admin/src/pages/order/orderCenter/index.vue b/frontend/admin/src/pages/order/orderCenter/index.vue index 48c94f608..33387fd9a 100644 --- a/frontend/admin/src/pages/order/orderCenter/index.vue +++ b/frontend/admin/src/pages/order/orderCenter/index.vue @@ -204,8 +204,8 @@ @@ -469,12 +469,18 @@ function getProductTypeLabel(type) { return item ? item.label : type || '--'; } -function getStatusLabel(code) { +function getStatusLabel(rowOrCode) { + const row = typeof rowOrCode === 'object' && rowOrCode ? rowOrCode : null; + if (row && Number(row.is_del) === 1) return '已删除'; + const code = row ? row.status_code : rowOrCode; const item = statusCodeOptions.find((opt) => opt.key === code); return item ? item.label : code || '--'; } -function getStatusClass(code) { +function getStatusClass(rowOrCode) { + const row = typeof rowOrCode === 'object' && rowOrCode ? rowOrCode : null; + if (row && Number(row.is_del) === 1) return 'danger'; + const code = row ? row.status_code : rowOrCode; const map = { pending_payment: 'warning', processing: 'primary', @@ -484,6 +490,7 @@ function getStatusClass(code) { after_sale: 'warning', refunding: 'warning', refunded: 'info', + deleted: 'danger', }; return map[code] || 'info'; } diff --git a/frontend/admin/src/pages/plugin/normal/normalOrder.js b/frontend/admin/src/pages/plugin/normal/normalOrder.js index 206258177..b70e51a69 100644 --- a/frontend/admin/src/pages/plugin/normal/normalOrder.js +++ b/frontend/admin/src/pages/plugin/normal/normalOrder.js @@ -189,7 +189,8 @@ export function getOrderExpress(orderNo) { * 供详情弹窗等场景复用(列表/详情状态展示保持一致) */ export function getOrderBusinessStatus(row = {}) { - if (row.is_del === 1) { + // is_del 可能为数字 1 或字符串 "1",与列表口径一致按真值判断 + if (Number(row.is_del) === 1) { return { code: 'deleted', text: '已删除', type: 'danger' }; } if (row.status_code === 'pending_payment') { diff --git a/frontend/admin/src/pages/plugin/normal/order/List.vue b/frontend/admin/src/pages/plugin/normal/order/List.vue index 625b9cc04..26a569f28 100644 --- a/frontend/admin/src/pages/plugin/normal/order/List.vue +++ b/frontend/admin/src/pages/plugin/normal/order/List.vue @@ -122,46 +122,73 @@ - -
    -

    请上传 Excel 文件(xls/xlsx),格式如下:

    - - - - - - - - - - - -

    - 第一行为表头,从第二行开始为数据 -

    + +
    +
    + 请按下方格式编辑 Excel(xls / xlsx)后上传,系统将按行完成发货。 +
    + +
    +
    Excel 格式示例
    + + + + + + + + + + + +
    第一行为表头,从第二行开始填写数据
    +
    + - +
    + +
    + + +
    +
    仅支持 .xls、.xlsx
    +
    -
    -
    @@ -197,7 +224,9 @@ import { getPrintPickList, getOrderExpress, exportOrderList as exportNormalOrder, + batchShipment, } from '../normalOrder'; +import { getCookies } from '@/libs/util'; import OrderStatistics from './components/OrderStatistics'; import OrderSearch from './components/OrderSearch'; @@ -276,17 +305,24 @@ const editOrderData = ref(null); const batchShipmentModal = ref(false); const batchResult = ref(null); const showBatchErrors = ref(false); -const templateData = [{}]; +const batchUploading = ref(false); +const batchUploadRef = ref(null); +const templateData = [ + { + order_no: 'CP1234567890', + delivery_name: '顺丰速运', + delivery_id: 'SF1234567890', + }, +]; // 批量删除 const selectedIds = ref([]); -const adminApi = computed(() => { - return import.meta.env.VITE_APP_API_URL + '/adminapi/'; -}); -const headers = computed(() => { - return { 'Authori-zation': 'Bearer ' + localStorage.getItem('token') }; -}); +// 先上传到文件服务拿到路径,再调批量发货接口(与商品导入同口径) +const batchUploadUrl = computed(() => setting.apiBaseURL + '/file/upload/1'); +const batchUploadHeaders = computed(() => ({ + 'Authori-zation': 'Bearer ' + (getCookies('token') || ''), +})); /* ===== 数据加载 ===== */ @@ -616,29 +652,61 @@ function openRecord(row) { function openBatchShipment() { batchResult.value = null; showBatchErrors.value = false; + batchUploading.value = false; batchShipmentModal.value = true; } -function onBatchUploadSuccess(res) { - batchResult.value = res; - if (res.code === 200) { - ElMessage.success(res.msg || '批量发货完成'); - getList(); - } else { - ElMessage.error(res.msg || '批量发货失败'); +function onBatchDialogClosed() { + batchResult.value = null; + showBatchErrors.value = false; + batchUploading.value = false; +} + +/** + * 文件上传成功后,用返回路径调用批量发货接口 + */ +function onBatchFileUploadSuccess(res) { + if (res.status !== 200 || !res.data || !res.data.src) { + batchUploading.value = false; + ElMessage.error(res.msg || '文件上传失败'); + return; } + batchShipment({ file: res.data.src }) + .then((result) => { + batchResult.value = result; + ElMessage.success(result.msg || '批量发货完成'); + getList(); + getStatistics(); + }) + .catch((err) => { + ElMessage.error((err && err.msg) || '批量发货失败'); + }) + .finally(() => { + batchUploading.value = false; + }); } function onBatchUploadError(err) { - ElMessage.error('批量发货失败:' + (err.msg || '文件上传异常')); + batchUploading.value = false; + let msg = '文件上传失败'; + try { + const res = JSON.parse(err?.message || ''); + if (res && res.msg) msg = res.msg; + } catch (e) { + // 非 JSON 响应,保留默认提示 + } + ElMessage.error(msg); } function onBeforeUpload(file) { - var suffix = file.name.split('.').pop().toLowerCase(); + const suffix = (file.name.split('.').pop() || '').toLowerCase(); if (!['xls', 'xlsx'].includes(suffix)) { - ElMessage.error('文件格式不正确,请上传xls或xlsx格式'); + ElMessage.error('文件格式不正确,请上传 xls 或 xlsx 格式'); return false; } + batchResult.value = null; + showBatchErrors.value = false; + batchUploading.value = true; return true; } @@ -750,9 +818,12 @@ function handleExport() { page: 1, limit: 200, ...searchParams.value, - status_code: statusTab.value === 'deleted' ? '' : statusTab.value, is_deleted: statusTab.value === 'deleted' ? 1 : '', }; + // 与列表同口径:业务子状态走 business_status,勿把 pending_shipped 等塞进 status_code + if (statusTab.value && statusTab.value !== 'deleted') { + params.business_status = statusTab.value; + } doExport(params); }) .catch(() => {}); @@ -805,13 +876,6 @@ onMounted(() => { margin-top: 10px; } - .batch-result { - padding: 10px; - background: #f5f7fa; - border-radius: 4px; - font-size: 14px; - } - .logistics-box { .logistics-header { display: flex; @@ -843,3 +907,136 @@ onMounted(() => { } } + + + diff --git a/frontend/admin/src/pages/plugin/normal/order/components/OrderSearch.vue b/frontend/admin/src/pages/plugin/normal/order/components/OrderSearch.vue index 6c4365a3d..67517e14c 100644 --- a/frontend/admin/src/pages/plugin/normal/order/components/OrderSearch.vue +++ b/frontend/admin/src/pages/plugin/normal/order/components/OrderSearch.vue @@ -33,18 +33,17 @@ value-format="YYYY-MM-DD" > - +