瀏覽代碼

fix: 补货单切换 + orderId类型匹配 + 退出登录优化

【后端】
- GET /replenisher/device/{deviceId}/pending-orders 新增端点
- getDeviceList: MIN(id)→MAX(id)取最新补货单
- ReplenishmentOrderStatusEnum import 新增

【补货操作页】
- 多补货单设备顶部显示picker选择器可切换
- orderId字符串/数字匹配修复(String()比较)
- 共补货数量直接取补货单plannedQuantity之和
- productCode匹配trim+toUpperCase忽略大小写
- 设备名称前加设备编号(用-分割)

【退出登录】
- 文本→SVG图标(icon-logout.svg 44rpx 1.5px描边)
- 移至左侧避免微信胶囊按钮遮挡

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 8 小時之前
父節點
當前提交
41a233e83a

+ 7 - 0
haha-admin-mp/src/api/replenish.ts

@@ -84,6 +84,13 @@ export async function bindWechat(params: { bindingCode: string; code: string }):
 
 // ==================== 补货单相关 ====================
 
+/**
+ * 获取设备待处理补货单列表(用于补货单切换)
+ */
+export async function getDevicePendingOrders(deviceId: string): Promise<any[]> {
+  return get(`/replenisher/device/${deviceId}/pending-orders`);
+}
+
 /**
  * 获取补货员的补货单列表
  */

+ 3 - 4
haha-admin-mp/src/pages/replenish/index.vue

@@ -4,8 +4,8 @@
     <view class="yellow-header">
       <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
       <view class="header-content">
+        <image class="header-logout" src="/static/icon-logout.svg" @click="handleLogout" />
         <text class="header-title">补货管理</text>
-        <view class="header-logout" @click="handleLogout"><text>退出</text></view>
       </view>
     </view>
     <view class="header-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
@@ -280,7 +280,7 @@ const goToOperation = async (device: any) => {
           orderId: device.pendingOrderId,
           orderNo: device.pendingOrderNo || '',
           items: data.items.map((item: any) => ({
-            productCode: item.productCode,
+            productCode: (item.productCode || '').trim(),
             productName: item.productName,
             plannedQuantity: item.plannedQuantity || item.quantity || 0,
             shelfNum: item.shelfNum,
@@ -369,8 +369,7 @@ onShow(async () => {
   height: 44px; padding: 0 16rpx; position: relative;
 }
 .header-title { font-size: $font-size-xl; font-weight: 700; color: $text-color-primary; }
-.header-logout { position: absolute; right: 24rpx; padding: 8rpx 16rpx;
-  text { font-size: 24rpx; color: $text-color-primary; }
+.header-logout { position: absolute; left: 16rpx; width: 44rpx; height: 44rpx;
   &:active { opacity: 0.7; }
 }
 .header-placeholder { width: 100%; }

+ 111 - 11
haha-admin-mp/src/pages/replenish/operation.vue

@@ -7,14 +7,22 @@
     </view>
 
     <template v-else>
+      <!-- 补货单选择器 -->
+      <view class="order-selector" v-if="pendingOrders.length > 1">
+        <text class="selector-label">补货单</text>
+        <picker mode="selector" :range="pendingOrders" range-key="orderNo" :value="selectedOrderIndex" @change="onOrderChange">
+          <view class="selector-value"><text>{{ pendingOrders[selectedOrderIndex]?.orderNo || '' }}</text><text class="selector-arrow">▾</text></view>
+        </picker>
+        <text class="selector-status">{{ pendingOrders[selectedOrderIndex]?.statusLabel || '' }}</text>
+      </view>
       <view class="order-banner" v-if="orderNo">
-        <text class="order-banner-text">补货单 {{ orderNo }} · 数量已按计划预填</text>
+        <text class="order-banner-text">补货单 {{ orderNo }}</text>
       </view>
 
       <view class="device-card">
         <view class="device-icon"></view>
         <view class="device-info">
-          <text class="device-name">{{ deviceInfo.name || deviceInfo.deviceId }}</text>
+          <text class="device-name">{{ deviceInfo.deviceId }} - {{ deviceInfo.name || '' }}</text>
           <text class="device-addr" v-if="deviceInfo.address">{{ deviceInfo.address }}</text>
         </view>
       </view>
@@ -75,7 +83,7 @@
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
-import { getDeviceInventory, replenishStock } from '@/api/replenish';
+import { getDeviceInventory, replenishStock, getDevicePendingOrders, getReplenisherOrderDetail } from '@/api/replenish';
 
 interface ReplenishInput {
   productId: number;
@@ -96,13 +104,24 @@ const orderId = ref('');
 const orderNo = ref('');
 const showOrderOnly = ref(false);
 const orderItemCodes = ref<Set<string>>(new Set());
+const pendingOrders = ref<any[]>([]);
+const selectedOrderIndex = ref(0);
 
 const isInOrder = (item: any) => {
-  const code = item.product_code || item.productCode || '';
-  return orderItemCodes.value.has(code);
+  const code = (item.product_code || item.productCode || '').trim().toUpperCase();
+  for (const oc of orderItemCodes.value) { if (oc.toUpperCase() === code) return true; }
+  return false;
 };
 
-const totalReplenishCount = computed(() => replenishItems.value.reduce((s, i) => s + (i.quantity || 0), 0));
+// 补货单预填数据
+const orderItemsData = ref<any[]>([]);
+
+const totalReplenishCount = computed(() => {
+  if (orderItemsData.value.length > 0) {
+    return orderItemsData.value.reduce((s, oi) => s + (oi.plannedQuantity || 0), 0);
+  }
+  return replenishItems.value.reduce((s, i) => s + (i.quantity || 0), 0);
+});
 
 const isLowStock = (item: any) => { const s = item.stock || 0; const t = item.warning_threshold || item.warningThreshold || 5; return s > 0 && s <= t; };
 const isOutOfStock = (item: any) => (item.stock || 0) === 0;
@@ -139,6 +158,44 @@ const handleSubmit = async () => {
   } finally { submitting.value = false; }
 };
 
+const loadOrderItems = async (oid: string) => {
+  try {
+    const data = await getReplenisherOrderDetail(oid as any);
+    if (!data?.items?.length) return;
+    orderId.value = oid;
+    orderNo.value = data.order?.orderNo || '';
+    const items = data.items.map((item: any) => ({
+      productCode: (item.productCode || '').trim(),
+      productName: item.productName,
+      plannedQuantity: item.plannedQuantity || item.quantity || 0,
+      shelfNum: item.shelfNum,
+      position: item.position
+    }));
+    orderItemsData.value = items;
+    orderItemCodes.value = new Set(items.map((oi: any) => oi.productCode.toUpperCase()));
+
+    // 重新匹配库存并更新数量
+    const list = inventoryList.value;
+    replenishItems.value = list.filter((it: any) => it.product_id || it.productId).map((it: any) => {
+      const code = (it.product_code || it.productCode || '').trim();
+      const m = items.find((oi: any) => oi.productCode.toUpperCase() === code.toUpperCase());
+      return {
+        productId: it.product_id || it.productId, productCode: code,
+        productName: it.product_name || it.productName || '',
+        quantity: m ? (m.plannedQuantity || 0) : 0,
+        shelfNum: it.shelf_num || it.shelfNum || null, position: it.position || null
+      };
+    });
+  } catch { /* ignore */ }
+};
+
+const onOrderChange = (e: any) => {
+  const idx = e.detail.value;
+  selectedOrderIndex.value = idx;
+  const o = pendingOrders.value[idx];
+  if (o) loadOrderItems(String(o.id));
+};
+
 onMounted(async () => {
   const pages = getCurrentPages();
   const opts = (pages[pages.length - 1] as any)?.$page?.options || (pages[pages.length - 1] as any)?.options || {};
@@ -152,15 +209,51 @@ onMounted(async () => {
     const list = data.inventoryList || [];
     inventoryList.value = list;
 
+    // 加载待处理补货单列表
+    pendingOrders.value = await getDevicePendingOrders(deviceId.value);
+
     let orderItems: any[] = [];
-    const orderStr = uni.getStorageSync('pendingReplenishOrder');
-    if (orderStr) {
-      try { const od = JSON.parse(orderStr); if (od.orderId === orderId.value) { orderNo.value = od.orderNo || ''; orderItems = od.items || []; orderItemCodes.value = new Set(orderItems.map((oi: any) => oi.productCode)); showOrderOnly.value = true; } } catch { }
+    let targetIndex = 0;
+
+    if (pendingOrders.value.length > 0) {
+      // 如果有传入orderId,定位到对应补货单
+      if (orderId.value) {
+        targetIndex = pendingOrders.value.findIndex((o: any) => String(o.id) === String(orderId.value));
+        if (targetIndex < 0) targetIndex = 0;
+      }
+      selectedOrderIndex.value = targetIndex;
+      const targetOrder = pendingOrders.value[targetIndex];
+      if (targetOrder) {
+        // 拉取补货单详情
+        try {
+          const detail = await getReplenisherOrderDetail(targetOrder.id);
+          if (detail?.items) {
+            orderId.value = String(targetOrder.id);
+            orderNo.value = targetOrder.orderNo || '';
+            orderItems = detail.items.map((item: any) => ({
+              productCode: (item.productCode || '').trim(),
+              productName: item.productName,
+              plannedQuantity: item.plannedQuantity || item.quantity || 0,
+              shelfNum: item.shelfNum,
+              position: item.position
+            }));
+            orderItemsData.value = orderItems;
+            orderItemCodes.value = new Set(orderItems.map((oi: any) => oi.productCode.toUpperCase()));
+            showOrderOnly.value = true;
+          }
+        } catch { /* fallback */ }
+      }
+    } else {
+      // 没有待处理补货单时尝试从localStorage加载
+      const orderStr = uni.getStorageSync('pendingReplenishOrder');
+      if (orderStr) {
+        try { const od = JSON.parse(orderStr); if (String(od.orderId) === String(orderId.value)) { orderNo.value = od.orderNo || ''; orderItems = od.items || []; orderItemsData.value = orderItems; orderItemCodes.value = new Set(orderItems.map((oi: any) => (oi.productCode || '').trim().toUpperCase())); showOrderOnly.value = true; } } catch { }
+      }
     }
 
     replenishItems.value = list.filter((it: any) => it.product_id || it.productId).map((it: any) => {
-      const code = it.product_code || it.productCode || '';
-      const m = orderItems.find((oi: any) => oi.productCode === code);
+      const code = (it.product_code || it.productCode || '').trim();
+      const m = orderItems.find((oi: any) => oi.productCode.toUpperCase() === code.toUpperCase());
       return {
         productId: it.product_id || it.productId, productCode: code,
         productName: it.product_name || it.productName || '',
@@ -184,6 +277,13 @@ onMounted(async () => {
 }
 @keyframes pulse { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
 
+.order-selector { display: flex; align-items: center; gap: 12rpx; margin: $spacing-2 $spacing-3; padding: $spacing-2 $spacing-3; background: $primary-color-bg; border-radius: $radius-sm; }
+.selector-label { font-size: $font-size-sm; color: $text-color-muted; flex-shrink: 0; }
+.selector-value { display: flex; align-items: center; gap: 6rpx; padding: 6rpx 16rpx; background: $bg-color-card; border-radius: $radius-sm; border: 1rpx solid $border-color;
+  text { font-size: $font-size-sm; color: $text-color-primary; font-weight: 500; }
+}
+.selector-arrow { font-size: 18rpx; color: $text-color-muted; }
+.selector-status { font-size: 22rpx; color: $primary-color-dark; margin-left: auto; }
 .order-banner { margin: $spacing-2 $spacing-3; padding: $spacing-2 $spacing-3; background: $primary-color-bg; border-radius: $radius-sm; }
 .order-banner-text { font-size: $font-size-sm; color: $primary-color-dark; }
 

+ 1 - 1
haha-admin-mp/src/pages/replenish/order-detail.vue

@@ -144,7 +144,7 @@ function goReplenish() {
   const orderData = {
     orderId: order.value.id, orderNo: order.value.orderNo,
     items: items.value.map((item: any) => ({
-      productCode: item.productCode, productName: item.productName,
+      productCode: (item.productCode || '').trim(), productName: item.productName,
       plannedQuantity: item.plannedQuantity || item.quantity || 0,
       shelfNum: item.shelfNum, position: item.position
     }))

+ 5 - 0
haha-admin-mp/src/static/icon-logout.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#1e293b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
+  <polyline points="16 17 21 12 16 7" />
+  <line x1="21" y1="12" x2="9" y2="12" />
+</svg>

+ 32 - 1
haha-admin/src/main/java/com/haha/admin/controller/ReplenisherOperationController.java

@@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.StpUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.haha.common.enums.ReplenishmentOrderStatusEnum;
 import com.haha.common.exception.BusinessException;
 import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
@@ -100,7 +101,7 @@ public class ReplenisherOperationController {
         Map<String, String> firstOrderNoMap = new java.util.HashMap<>();
         if (!deviceIds.isEmpty()) {
             var firstOrderQw = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<ReplenishmentOrder>()
-                .select("device_id", "MIN(id) as order_id")
+                .select("device_id", "MAX(id) as order_id")
                 .in("device_id", deviceIds)
                 .in("status", 0, 1, 2)
                 .groupBy("device_id");
@@ -198,6 +199,36 @@ public class ReplenisherOperationController {
         return Result.success("查询成功", result);
     }
 
+    /**
+     * 获取设备待处理补货单列表(用于补货单切换)
+     */
+    @GetMapping("/device/{deviceId}/pending-orders")
+    public Result<List<Map<String, Object>>> getDevicePendingOrders(@PathVariable String deviceId) {
+        Replenisher replenisher = getCurrentReplenisher();
+        List<String> boundIds = replenisherService.getBoundDeviceIds(replenisher.getId());
+        if (!boundIds.contains(deviceId)) {
+            return Result.error(403, "您无权查看此设备");
+        }
+
+        var wrapper = new LambdaQueryWrapper<ReplenishmentOrder>()
+            .eq(ReplenishmentOrder::getDeviceId, deviceId)
+            .in(ReplenishmentOrder::getStatus, 0, 1, 2)
+            .orderByDesc(ReplenishmentOrder::getCreateTime);
+        List<ReplenishmentOrder> orders = replenishmentOrderService.list(wrapper);
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ReplenishmentOrder o : orders) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("id", o.getId());
+            item.put("orderNo", o.getOrderNo());
+            item.put("status", o.getStatus());
+            item.put("statusLabel", ReplenishmentOrderStatusEnum.getLabelByCode(o.getStatus()).getLabel());
+            item.put("createTime", o.getCreateTime() != null ? o.getCreateTime().toString() : null);
+            result.add(item);
+        }
+        return Result.success("查询成功", result);
+    }
+
     /**
      * 获取设备库存详情
      *