浏览代码

feat: 补货操作打磨 + 有单设备一键预填 + 一键补满逻辑修正

【补货操作页】
- 设计规范: min-height→height+flex, border→shadow, <button>→<view>
- WCAG: 提交按钮白字改深色(#1e293b), 移除border-left侧边条纹
- 「一键补满」: 补货量=标准库存-剩余库存, 剩余≥标准则跳过
- 颜色修正: 一键补满琥珀→绿色(success), 设备图标琥珀→绿色
- 26rpx→统一24/28rpx, 加载改pulse dots

【有补货单设备一键预填】
- 后端: getDeviceList新增pendingOrderId/pendingOrderNo(首个待处理单)
- 前端: 点击有补货单设备→自动拉详情→预填计划数量→关联orderId

【合并上次提交的补货首页打磨】
- 退出登录、标题居中、emoji替换、字体统一、日均/销容比移除

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 天之前
父节点
当前提交
14793cd3aa

+ 23 - 2
haha-admin-mp/src/pages/replenish/index.vue

@@ -184,7 +184,7 @@
 import { ref, computed, onMounted } from 'vue';
 import { ref, computed, onMounted } from 'vue';
 import { onShow } from '@dcloudio/uni-app';
 import { onShow } from '@dcloudio/uni-app';
 import CustomTabBar from '@/components/CustomTabBar.vue';
 import CustomTabBar from '@/components/CustomTabBar.vue';
-import { getDeviceList, batchGenerateOrders } from '@/api/replenish';
+import { getDeviceList, batchGenerateOrders, getReplenisherOrderDetail } from '@/api/replenish';
 import { clearAuth } from '@/utils/auth';
 import { clearAuth } from '@/utils/auth';
 
 
 const statusBarHeight = ref(0);
 const statusBarHeight = ref(0);
@@ -271,7 +271,28 @@ const handleSearch = () => {
   // 触发筛选
   // 触发筛选
 };
 };
 
 
-const goToOperation = (device: any) => {
+const goToOperation = async (device: any) => {
+  if (device.replenishOrderGenerated && device.pendingOrderId) {
+    try {
+      const data = await getReplenisherOrderDetail(device.pendingOrderId);
+      if (data?.items?.length) {
+        const orderData = {
+          orderId: device.pendingOrderId,
+          orderNo: device.pendingOrderNo || '',
+          items: data.items.map((item: any) => ({
+            productCode: item.productCode,
+            productName: item.productName,
+            plannedQuantity: item.plannedQuantity || item.quantity || 0,
+            shelfNum: item.shelfNum,
+            position: item.position
+          }))
+        };
+        uni.setStorageSync('pendingReplenishOrder', JSON.stringify(orderData));
+        uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${device.deviceId}&orderId=${device.pendingOrderId}` });
+        return;
+      }
+    } catch { /* fallback to manual */ }
+  }
   uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${device.deviceId}` });
   uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${device.deviceId}` });
 };
 };
 
 

+ 138 - 492
haha-admin-mp/src/pages/replenish/operation.vue

@@ -2,108 +2,67 @@
   <view class="page">
   <view class="page">
     <NavBar title="补货操作" showBack />
     <NavBar title="补货操作" showBack />
 
 
-    <view class="loading-container" v-if="loading">
-      <view class="loading-spinner"></view>
-      <text class="loading-text">加载设备信息...</text>
+    <view class="load-tip" v-if="loading">
+      <view class="dot-row"><view class="pulse-dot"></view><view class="pulse-dot"></view><view class="pulse-dot"></view></view>
     </view>
     </view>
 
 
     <template v-else>
     <template v-else>
-      <!-- 补货单提示 -->
       <view class="order-banner" v-if="orderNo">
       <view class="order-banner" v-if="orderNo">
         <text class="order-banner-text">补货单 {{ orderNo }} · 数量已按计划预填</text>
         <text class="order-banner-text">补货单 {{ orderNo }} · 数量已按计划预填</text>
       </view>
       </view>
 
 
-      <!-- 设备信息 -->
-      <view class="device-section">
-        <view class="device-card">
-          <view class="device-header">
-            <view class="device-icon"></view>
-            <view class="device-info">
-              <text class="device-name">{{ deviceInfo.name || deviceInfo.deviceId }}</text>
-              <text class="device-id" v-if="deviceInfo.address">{{ deviceInfo.address }}</text>
-            </view>
-          </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-addr" v-if="deviceInfo.address">{{ deviceInfo.address }}</text>
         </view>
         </view>
       </view>
       </view>
 
 
-      <!-- 商品库存清单 -->
       <view class="inventory-section">
       <view class="inventory-section">
-        <view class="section-header">
+        <view class="section-head">
           <text class="section-title">商品库存清单</text>
           <text class="section-title">商品库存清单</text>
         </view>
         </view>
 
 
-        <!-- 空状态 -->
         <view class="empty-state" v-if="inventoryList.length === 0">
         <view class="empty-state" v-if="inventoryList.length === 0">
+          <view class="empty-icon-box"><view class="empty-doc-icon"></view></view>
           <text class="empty-text">暂无商品库存数据</text>
           <text class="empty-text">暂无商品库存数据</text>
         </view>
         </view>
 
 
-        <!-- 商品列表 -->
-        <view
-          class="inventory-card"
-          v-for="(item, index) in inventoryList"
-          :key="index"
-        >
-          <view class="item-main">
-            <image
-              v-if="item.product_image"
-              :src="item.product_image"
-              class="item-img"
-              mode="aspectFill"
-            />
+        <view class="inventory-card" v-for="(item, index) in inventoryList" :key="index">
+          <view class="item-top">
+            <image v-if="item.product_image" :src="item.product_image" class="item-img" mode="aspectFill" />
             <view v-else class="item-img-placeholder"><text>无图</text></view>
             <view v-else class="item-img-placeholder"><text>无图</text></view>
             <view class="item-info">
             <view class="item-info">
               <text class="item-name">{{ item.product_name || item.productName || '未知商品' }}</text>
               <text class="item-name">{{ item.product_name || item.productName || '未知商品' }}</text>
               <text class="item-code" v-if="item.product_code || item.productCode">{{ item.product_code || item.productCode }}</text>
               <text class="item-code" v-if="item.product_code || item.productCode">{{ item.product_code || item.productCode }}</text>
             </view>
             </view>
-            <view :class="['stock-badge', getStockStatus(item)]">
-              {{ getStockStatusText(item) }}
-            </view>
+            <view class="stock-badge" :class="getStockStatus(item)">{{ getStockStatusText(item) }}</view>
           </view>
           </view>
 
 
           <view class="item-stock">
           <view class="item-stock">
-            <view class="stock-row">
-              <text class="stock-label">标准库存</text>
-              <text class="stock-value muted">{{ item.warning_threshold || item.warningThreshold || '-' }}</text>
-            </view>
-            <view class="stock-row">
-              <text class="stock-label">剩余库存</text>
-              <text :class="['stock-value', { 'low-stock': isLowStock(item), 'out-of-stock': isOutOfStock(item) }]">
-                {{ item.stock || 0 }}
-              </text>
-            </view>
+            <text class="stock-label">标准库存 {{ item.warning_threshold || item.warningThreshold || '-' }}</text>
+            <text class="stock-label">剩余库存 <text :class="{ warn: isLowStock(item), danger: isOutOfStock(item) }">{{ item.stock || 0 }}</text></text>
           </view>
           </view>
 
 
-          <!-- 补货输入 -->
-          <view class="replenish-input-section">
-            <text class="input-label">补货数量</text>
-            <view class="quantity-control">
-              <view class="qty-btn" @click="decrease(item, index)">-</view>
-              <input
-                class="qty-input"
-                type="number"
-                v-model="replenishItems[index].quantity"
-                @blur="validateQuantity(index)"
-              />
-              <view class="qty-btn" @click="increase(item, index)">+</view>
-            </view>
-            <view class="suggest-btn" @click="suggestQuantity(item, index)">
-              一键补满
+          <view class="replenish-row">
+            <text class="replenish-label">补货数量</text>
+            <view class="qty-control">
+              <view class="qty-btn" @click="decrease(item, index)"><text>-</text></view>
+              <input class="qty-input" type="number" v-model="replenishItems[index].quantity" @blur="validateQuantity(index)" />
+              <view class="qty-btn" @click="increase(item, index)"><text>+</text></view>
             </view>
             </view>
+            <view class="fill-btn" @click="suggestQuantity(item, index)"><text>一键补满</text></view>
           </view>
           </view>
         </view>
         </view>
       </view>
       </view>
 
 
-      <!-- 底部提交 -->
-      <view class="bottom-actions" v-if="inventoryList.length > 0">
-        <view class="total-info">
-          <text class="total-label">共补货</text>
-          <text class="total-value">{{ totalReplenishCount }}</text>
-          <text class="total-label">件商品</text>
+      <view class="bottom-bar" v-if="inventoryList.length > 0">
+        <text class="total-label">共补货 <text class="total-num">{{ totalReplenishCount }}</text> 件</text>
+        <view class="submit-btn" :class="{ disabled: submitting || totalReplenishCount === 0 }" @click="handleSubmit">
+          <view class="btn-spinner" v-if="submitting"></view>
+          <text>{{ submitting ? '提交中' : '确认补货' }}</text>
         </view>
         </view>
-        <button class="submit-btn" :loading="submitting" :disabled="submitting || totalReplenishCount === 0" @click="handleSubmit">
-          <text v-if="!submitting">确认补货</text>
-          <text v-else>提交中...</text>
-        </button>
       </view>
       </view>
     </template>
     </template>
   </view>
   </view>
@@ -114,21 +73,6 @@ import { ref, computed, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import NavBar from '@/components/NavBar.vue';
 import { getDeviceInventory, replenishStock } from '@/api/replenish';
 import { getDeviceInventory, replenishStock } from '@/api/replenish';
 
 
-interface InventoryItem {
-  product_id?: number;
-  productId?: number;
-  product_code?: string;
-  productCode?: string;
-  product_name?: string;
-  productName?: string;
-  stock?: number;
-  warning_threshold?: number;
-  warningThreshold?: number;
-  shelf_num?: number;
-  shelfNum?: number;
-  position?: string;
-}
-
 interface ReplenishInput {
 interface ReplenishInput {
   productId: number;
   productId: number;
   productCode?: string;
   productCode?: string;
@@ -140,464 +84,166 @@ interface ReplenishInput {
 
 
 const deviceId = ref('');
 const deviceId = ref('');
 const deviceInfo = ref<any>({});
 const deviceInfo = ref<any>({});
-const inventoryList = ref<InventoryItem[]>([]);
+const inventoryList = ref<any[]>([]);
 const replenishItems = ref<ReplenishInput[]>([]);
 const replenishItems = ref<ReplenishInput[]>([]);
 const loading = ref(true);
 const loading = ref(true);
 const submitting = ref(false);
 const submitting = ref(false);
+const orderId = ref('');
+const orderNo = ref('');
 
 
-const totalReplenishCount = computed(() => {
-  return replenishItems.value.reduce((sum, item) => sum + (item.quantity || 0), 0);
-});
-
-const isLowStock = (item: any): boolean => {
-  const stock = item.stock || 0;
-  const threshold = item.warning_threshold || item.warningThreshold || 5;
-  return stock > 0 && stock <= threshold;
-};
-
-const isOutOfStock = (item: any): boolean => {
-  return (item.stock || 0) === 0;
-};
-
-const getStockStatus = (item: any): string => {
-  if (isOutOfStock(item)) return 'danger';
-  if (isLowStock(item)) return 'warning';
-  return 'normal';
-};
-
-const getStockStatusText = (item: any): string => {
-  if ((item.stock || 0) === 0) return '缺货';
-  const threshold = item.warning_threshold || item.warningThreshold || 5;
-  if ((item.stock || 0) <= threshold) return '低库存';
-  return '正常';
-};
-
-const decrease = (item: any, index: number) => {
-  if (replenishItems.value[index].quantity > 0) {
-    replenishItems.value[index].quantity--;
-  }
-};
-
-const MAX_QUANTITY = 9999;
+const totalReplenishCount = computed(() => replenishItems.value.reduce((s, i) => s + (i.quantity || 0), 0));
 
 
-const increase = (item: any, index: number) => {
-  if (replenishItems.value[index].quantity < MAX_QUANTITY) {
-    replenishItems.value[index].quantity++;
-  }
-};
+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;
+const getStockStatus = (item: any) => isOutOfStock(item) ? 'danger' : isLowStock(item) ? 'warning' : 'normal';
+const getStockStatusText = (item: any) => (item.stock || 0) === 0 ? '缺货' : (item.stock || 0) <= (item.warning_threshold || item.warningThreshold || 5) ? '低库存' : '正常';
 
 
-const suggestQuantity = (item: any, index: number) => {
-  const threshold = item.warning_threshold || item.warningThreshold || 5;
-  const currentStock = item.stock || 0;
-  // 建议补满到预警阈值的3倍(即建议库存水平)
-  const suggestLevel = threshold * 3;
-  const suggest = Math.max(suggestLevel - currentStock, 0);
-  replenishItems.value[index].quantity = suggest;
-};
-
-const validateQuantity = (index: number) => {
-  if (replenishItems.value[index].quantity < 0) {
-    replenishItems.value[index].quantity = 0;
-  }
+const decrease = (_: any, i: number) => { if (replenishItems.value[i].quantity > 0) replenishItems.value[i].quantity--; };
+const increase = (_: any, i: number) => { if (replenishItems.value[i].quantity < 9999) replenishItems.value[i].quantity++; };
+const suggestQuantity = (item: any, i: number) => {
+  const std = item.warning_threshold || item.warningThreshold || 0;
+  const cur = item.stock || 0;
+  replenishItems.value[i].quantity = cur >= std ? 0 : std - cur;
 };
 };
+const validateQuantity = (i: number) => { if (replenishItems.value[i].quantity < 0) replenishItems.value[i].quantity = 0; };
 
 
 const handleSubmit = async () => {
 const handleSubmit = async () => {
-  if (totalReplenishCount.value === 0) {
-    uni.showToast({ title: '请至少补货一件商品', icon: 'none' });
-    return;
-  }
-
-  // 过滤掉数量为0的项
-  const items = replenishItems.value
-    .filter(item => item.quantity > 0)
-    .map(item => ({
-      productId: item.productId,
-      productCode: item.productCode,
-      productName: item.productName,
-      quantity: item.quantity,
-      shelfNum: item.shelfNum,
-      position: item.position
-    }));
-
-  if (items.length === 0) {
-    uni.showToast({ title: '请至少补货一件商品', icon: 'none' });
-    return;
-  }
+  if (submitting.value || totalReplenishCount.value === 0) return;
+  const items = replenishItems.value.filter(i => i.quantity > 0).map(i => ({
+    productId: i.productId, productCode: i.productCode, productName: i.productName,
+    quantity: i.quantity, shelfNum: i.shelfNum, position: i.position
+  }));
+  if (!items.length) { uni.showToast({ title: '请至少补货一件', icon: 'none' }); return; }
 
 
   submitting.value = true;
   submitting.value = true;
   try {
   try {
     const payload: any = { deviceId: deviceId.value, items };
     const payload: any = { deviceId: deviceId.value, items };
     if (orderId.value) payload.orderId = orderId.value;
     if (orderId.value) payload.orderId = orderId.value;
-
     const result = await replenishStock(payload);
     const result = await replenishStock(payload);
-
     uni.removeStorageSync('pendingReplenishOrder');
     uni.removeStorageSync('pendingReplenishOrder');
     uni.showToast({ title: `补货完成,成功${result.success}项`, icon: 'success' });
     uni.showToast({ title: `补货完成,成功${result.success}项`, icon: 'success' });
-    setTimeout(() => {
-      uni.navigateBack();
-    }, 800);
-  } catch (error: any) {
-    uni.showToast({ title: error.message || '补货失败', icon: 'none' });
-  } finally {
-    submitting.value = false;
-  }
+    setTimeout(() => uni.navigateBack(), 800);
+  } catch (e: any) {
+    uni.showToast({ title: e.message || '补货失败', icon: 'none' });
+  } finally { submitting.value = false; }
 };
 };
 
 
-const orderId = ref('');
-const orderNo = ref('');
-
 onMounted(async () => {
 onMounted(async () => {
   const pages = getCurrentPages();
   const pages = getCurrentPages();
-  const currentPage = pages[pages.length - 1];
-  const options = (currentPage as any).$page?.options || (currentPage as any).options || {};
-  deviceId.value = options.deviceId || '';
-  orderId.value = options.orderId || '';
-
-  if (!deviceId.value) {
-    uni.showToast({ title: '缺少设备ID', icon: 'none' });
-    setTimeout(() => uni.navigateBack(), 500);
-    return;
-  }
+  const opts = (pages[pages.length - 1] as any)?.$page?.options || (pages[pages.length - 1] as any)?.options || {};
+  deviceId.value = opts.deviceId || '';
+  orderId.value = opts.orderId || '';
+  if (!deviceId.value) { uni.showToast({ title: '缺少设备ID', icon: 'none' }); setTimeout(() => uni.navigateBack(), 500); return; }
 
 
   try {
   try {
     const data = await getDeviceInventory(deviceId.value);
     const data = await getDeviceInventory(deviceId.value);
     deviceInfo.value = data;
     deviceInfo.value = data;
-
     const list = data.inventoryList || [];
     const list = data.inventoryList || [];
     inventoryList.value = list;
     inventoryList.value = list;
 
 
-    // 加载补货单预填数据
-    const orderStr = uni.getStorageSync('pendingReplenishOrder');
     let orderItems: any[] = [];
     let orderItems: any[] = [];
+    const orderStr = uni.getStorageSync('pendingReplenishOrder');
     if (orderStr) {
     if (orderStr) {
-      try {
-        const orderData = JSON.parse(orderStr);
-        if (orderData.orderId === orderId.value) {
-          orderNo.value = orderData.orderNo || '';
-          orderItems = orderData.items || [];
-        }
-      } catch { /* ignore parse error */ }
+      try { const od = JSON.parse(orderStr); if (od.orderId === orderId.value) { orderNo.value = od.orderNo || ''; orderItems = od.items || []; } } catch { }
     }
     }
 
 
-    // 初始化补货输入,匹配补货单预填数量
-    replenishItems.value = list
-      .filter((item: any) => (item.product_id || item.productId))
-      .map((item: any) => {
-        const code = item.product_code || item.productCode || '';
-        const matched = orderItems.find((oi: any) => oi.productCode === code);
-        return {
-          productId: item.product_id || item.productId,
-          productCode: code,
-          productName: item.product_name || item.productName || '',
-          quantity: matched ? (matched.plannedQuantity || 0) : 0,
-          shelfNum: item.shelf_num || item.shelfNum || null,
-          position: item.position || null
-        };
-      });
-  } catch (error: any) {
-    uni.showToast({ title: error.message || '加载失败', icon: 'none' });
-  } finally {
-    loading.value = false;
-  }
+    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);
+      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 (e: any) {
+    uni.showToast({ title: e.message || '加载失败', icon: 'none' });
+  } finally { loading.value = false; }
 });
 });
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.page {
-  min-height: 100vh;
-  background: $bg-color-page;
-  padding-bottom: 200rpx;
+.page { height: 100vh; background: $bg-color-page; display: flex; flex-direction: column; }
+
+.load-tip { display: flex; justify-content: center; padding: 200rpx 0; }
+.dot-row { display: flex; gap: $spacing-1; }
+.pulse-dot { width: 10rpx; height: 10rpx; background: $text-color-placeholder; border-radius: 50%; animation: pulse 1.2s ease-in-out infinite;
+  &:nth-child(2) { animation-delay: 0.2s; } &:nth-child(3) { animation-delay: 0.4s; }
 }
 }
+@keyframes pulse { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
 
 
-/* 补货单提示 */
-.order-banner { margin: $spacing-2 $spacing-3; padding: $spacing-2 $spacing-3; background: $primary-color-bg; border-radius: $radius-sm; border-left: 4rpx solid $primary-color; }
+.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; }
 .order-banner-text { font-size: $font-size-sm; color: $primary-color-dark; }
 
 
-/* 加载 */
-.loading-container {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 100rpx 0;
-
-  .loading-text {
-    margin-top: 16rpx;
-    font-size: 26rpx;
-    color: $text-color-muted;
-  }
+// ====== Device ======
+.device-card { display: flex; align-items: center; margin: 16rpx 24rpx; padding: $spacing-3; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); }
+.device-icon { width: 48rpx; height: 48rpx; background: $success-color-bg; border-radius: $radius-base; margin-right: 16rpx; flex-shrink: 0; position: relative;
+  &::before { content: ''; position: absolute; top: 8rpx; left: 50%; transform: translateX(-50%); width: 20rpx; height: 16rpx; border: 2.5rpx solid $success-color; border-radius: 4rpx; }
+  &::after { content: ''; position: absolute; bottom: 4rpx; left: 50%; transform: translateX(-50%); width: 12rpx; height: 5rpx; background: $success-color; border-radius: 0 0 3rpx 3rpx; }
 }
 }
-
-/* 设备信息 */
-.device-section {
-  padding: 16rpx 24rpx 0;
-
-  .device-card {
-    background: $bg-color-card;
-    border: 1rpx solid $border-color;
-    border-radius: 16rpx;
-    padding: 24rpx;
-
-    .device-header {
-      display: flex;
-      align-items: center;
-
-      .device-icon {
-        width: 48rpx;
-        height: 48rpx;
-        background: $success-color-bg;
-        border-radius: 12rpx;
-        margin-right: 16rpx;
-        position: relative;
-
-        &::before {
-          content: '';
-          position: absolute;
-          top: 8rpx;
-          left: 50%;
-          transform: translateX(-50%);
-          width: 20rpx;
-          height: 16rpx;
-          border: 3rpx solid $primary-color;
-          border-radius: 4rpx;
-        }
-
-        &::after {
-          content: '';
-          position: absolute;
-          bottom: 4rpx;
-          left: 50%;
-          transform: translateX(-50%);
-          width: 12rpx;
-          height: 6rpx;
-          background: $primary-color;
-          border-radius: 0 0 4rpx 4rpx;
-        }
-      }
-
-      .device-info {
-        flex: 1;
-
-        .device-name {
-          display: block;
-          font-size: 30rpx;
-          font-weight: 600;
-          color: $text-color-primary;
-        }
-
-        .device-id {
-          font-size: 24rpx;
-          color: $text-color-tertiary;
-        }
-      }
-    }
-  }
+.device-info { flex: 1; min-width: 0; }
+.device-name { display: block; font-size: $font-size-md; font-weight: 600; color: $text-color-primary; }
+.device-addr { font-size: $font-size-sm; color: $text-color-muted; }
+
+// ====== Inventory ======
+.inventory-section { padding: 0 24rpx; flex: 1; height: 0; overflow-y: auto; }
+.section-head { padding: $spacing-3 0 $spacing-2; }
+.section-title { font-size: $font-size-md; font-weight: 700; color: $text-color-primary; }
+
+.empty-state { display: flex; flex-direction: column; align-items: center; padding: 80rpx 0; }
+.empty-icon-box { width: 96rpx; height: 96rpx; background: $bg-color-secondary; border-radius: $radius-lg; display: flex; align-items: center; justify-content: center; margin-bottom: $spacing-2; }
+.empty-doc-icon { width: 40rpx; height: 48rpx; border: 2.5rpx solid $text-color-placeholder; border-radius: 4rpx; position: relative;
+  &::after { content: ''; position: absolute; top: 14rpx; left: 8rpx; right: 8rpx; height: 2.5rpx; background: $text-color-placeholder; border-radius: 1rpx; box-shadow: 0 8rpx 0 $text-color-placeholder, 0 16rpx 0 $text-color-placeholder; }
 }
 }
+.empty-text { font-size: $font-size-base; color: $text-color-muted; }
 
 
-/* 库存区域 */
-.inventory-section {
-  padding: 0 24rpx;
-}
+.inventory-card { margin-bottom: 16rpx; padding: $spacing-3; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); }
 
 
-.section-header {
-  padding: 24rpx 0 16rpx;
-
-  .section-title {
-    font-size: 30rpx;
-    font-weight: 700;
-    color: $text-color-primary;
-  }
+.item-top { display: flex; align-items: center; margin-bottom: 12rpx; }
+.item-img { width: 72rpx; height: 72rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 12rpx; }
+.item-img-placeholder { width: 72rpx; height: 72rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 12rpx; display: flex; align-items: center; justify-content: center;
+  text { font-size: 20rpx; color: $text-color-muted; }
 }
 }
-
-.empty-state {
-  text-align: center;
-  padding: 60rpx 0;
-
-  .empty-text {
-    font-size: 26rpx;
-    color: $text-color-muted;
-  }
+.item-info { flex: 1; min-width: 0; overflow: hidden; }
+.item-name { display: block; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.item-code { font-size: $font-size-xs; color: $text-color-muted; }
+
+.stock-badge { padding: 4rpx 12rpx; border-radius: $radius-sm; font-size: 22rpx; font-weight: 500; flex-shrink: 0; margin-left: 8rpx;
+  &.normal { background: $success-color-bg; color: $success-color; }
+  &.warning { background: $warning-color-bg; color: $warning-color; }
+  &.danger { background: $error-color-bg; color: $error-color; }
 }
 }
 
 
-/* 商品卡片 */
-.inventory-card {
-  background: $bg-color-card;
-  border: 1rpx solid $border-color;
-  border-radius: 16rpx;
-  padding: 24rpx;
-  margin-bottom: 16rpx;
-
-  .item-main { display: flex; align-items: center; margin-bottom: 16rpx; }
-  .item-img { width: 80rpx; height: 80rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 16rpx; }
-  .item-img-placeholder { width: 80rpx; height: 80rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 16rpx; display: flex; align-items: center; justify-content: center;
-    text { font-size: 20rpx; color: $text-color-muted; }
-  }
-  .item-info { flex: 1; min-width: 0; overflow: hidden; }
-  .item-name { display: block; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-  .item-code { font-size: 22rpx; color: $text-color-muted; }
-
-  .stock-badge { padding: 4rpx 12rpx; border-radius: $radius-sm; font-size: 20rpx; font-weight: 500; flex-shrink: 0; margin-left: 12rpx;
-    &.normal { background: $success-color-bg; color: $success-color; }
-    &.warning { background: $warning-color-bg; color: $warning-color; }
-    &.danger { background: $error-color-bg; color: $error-color; }
-  }
-
-  .item-stock {
-    display: flex;
-    gap: 24rpx;
-    margin-bottom: 16rpx;
-
-    .stock-row {
-      flex: 1;
-      display: flex;
-      align-items: center;
-
-      .stock-label {
-        font-size: 24rpx;
-        color: $text-color-muted;
-        margin-right: 8rpx;
-      }
-
-      .stock-value {
-        font-size: 28rpx;
-        font-weight: 700;
-        color: $text-color-primary;
-
-        &.muted {
-          color: $text-color-muted;
-        }
-
-        &.low-stock {
-          color: $warning-color;
-        }
-
-        &.out-of-stock {
-          color: $error-color;
-        }
-      }
-    }
-  }
-
-  .replenish-input-section {
-    display: flex;
-    align-items: center;
-    background: $bg-color-page;
-    border-radius: 12rpx;
-    padding: 16rpx;
-
-    .input-label {
-      font-size: 24rpx;
-      color: $text-color-tertiary;
-      margin-right: 16rpx;
-    }
-
-    .quantity-control {
-      display: flex;
-      align-items: center;
-      border: 1rpx solid $border-color;
-      border-radius: 8rpx;
-      overflow: hidden;
-
-      .qty-btn {
-        width: 56rpx;
-        height: 56rpx;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        font-size: 32rpx;
-        font-weight: 500;
-        color: $text-color-tertiary;
-        background: $bg-color-card;
-
-        &:active {
-          background: $bg-color-secondary;
-        }
-      }
-
-      .qty-input {
-        width: 80rpx;
-        height: 56rpx;
-        text-align: center;
-        font-size: 28rpx;
-        font-weight: 600;
-        color: $text-color-primary;
-        border-left: 1rpx solid $border-color;
-        border-right: 1rpx solid $border-color;
-      }
-    }
-
-    .suggest-btn {
-      margin-left: auto;
-      padding: 10rpx 20rpx;
-      background: $success-color-bg;
-      border-radius: 8rpx;
-      font-size: 22rpx;
-      color: $primary-color;
-      font-weight: 500;
-
-      &:active {
-        background: $success-color-bg;
-      }
-    }
-  }
+.item-stock { display: flex; gap: 24rpx; margin-bottom: 12rpx; }
+.stock-label { font-size: $font-size-sm; color: $text-color-muted;
+  text { font-weight: 600; }
+  .warn { color: $warning-color; }
+  .danger { color: $error-color; }
 }
 }
 
 
-/* 底部操作 */
-.bottom-actions {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  background: $bg-color-card;
-  border-top: 1rpx solid $border-color;
-  padding: 20rpx 24rpx;
-  padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
-  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
-  display: flex;
-  align-items: center;
-  gap: 20rpx;
-
-  .total-info {
-    display: flex;
-    align-items: center;
-
-    .total-label {
-      font-size: 24rpx;
-      color: $text-color-tertiary;
-    }
-
-    .total-value {
-      font-size: 36rpx;
-      font-weight: 700;
-      color: $primary-color;
-      margin: 0 8rpx;
-    }
-  }
-
-  .submit-btn {
-    flex: 1;
-    height: 88rpx;
-    line-height: 88rpx;
-    padding: 0;
-    background: $primary-color;
-    color: #fff;
-    font-size: 30rpx;
-    font-weight: 600;
-    border-radius: 12rpx;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-
-    &::after {
-      border: none;
-    }
-
-    &:active {
-      background: $primary-color-dark;
-    }
+.replenish-row { display: flex; align-items: center; background: $bg-color-page; border-radius: $radius-base; padding: 12rpx 16rpx; }
+.replenish-label { font-size: $font-size-sm; color: $text-color-muted; margin-right: 12rpx; }
+.qty-control { display: flex; align-items: center; border: 1rpx solid $border-color; border-radius: $radius-sm; overflow: hidden; }
+.qty-btn { width: 52rpx; height: 52rpx; display: flex; align-items: center; justify-content: center; background: $bg-color-card;
+  text { font-size: 28rpx; color: $text-color-secondary; line-height: 1; }
+  &:active { background: $bg-color-secondary; }
+}
+.qty-input { width: 72rpx; height: 52rpx; text-align: center; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; border-left: 1rpx solid $border-color; border-right: 1rpx solid $border-color; }
+.fill-btn { margin-left: auto; padding: 10rpx 24rpx; background: $success-color-bg; border-radius: $radius-sm;
+  text { font-size: 22rpx; color: $success-color; font-weight: 500; }
+  &:active { opacity: 0.7; }
+}
 
 
-    &[disabled] {
-      opacity: 0.6;
-    }
-  }
+// ====== Bottom ======
+.bottom-bar { display: flex; align-items: center; gap: 20rpx; padding: 20rpx 24rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background: $bg-color-card; border-top: 1rpx solid $border-color-light; }
+.total-label { font-size: $font-size-sm; color: $text-color-muted; flex-shrink: 0; }
+.total-num { font-size: $font-size-xl; font-weight: 700; color: $primary-color-dark; margin: 0 4rpx; }
+.submit-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 12rpx; height: 80rpx; background: $primary-color; border-radius: $radius-base;
+  text { font-size: $font-size-md; font-weight: 600; color: $text-color-primary; }
+  &:active { opacity: 0.8; }
+  &.disabled { opacity: 0.5; pointer-events: none; }
 }
 }
+.btn-spinner { width: 28rpx; height: 28rpx; border: 3rpx solid rgba(30, 41, 59, 0.2); border-top-color: $text-color-primary; border-radius: 50%; animation: spin 0.7s linear infinite; }
+@keyframes spin { to { transform: rotate(360deg); } }
 </style>
 </style>

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

@@ -95,6 +95,33 @@ public class ReplenisherOperationController {
             }
             }
         }
         }
 
 
+        // 批量查询各设备首个待处理补货单(用于一键补货跳转)
+        Map<String, Long> firstOrderIdMap = new java.util.HashMap<>();
+        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")
+                .in("device_id", deviceIds)
+                .in("status", 0, 1, 2)
+                .groupBy("device_id");
+            List<Map<String, Object>> firstRows = replenishmentOrderService.listMaps(firstOrderQw);
+            for (Map<String, Object> row : firstRows) {
+                String did = (String) row.get("device_id");
+                Number oid = (Number) row.get("order_id");
+                if (did != null && oid != null) firstOrderIdMap.put(did, oid.longValue());
+            }
+            // 补充查询订单号
+            if (!firstOrderIdMap.isEmpty()) {
+                var noQw = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<ReplenishmentOrder>()
+                    .select("id", "order_no")
+                    .in("id", firstOrderIdMap.values());
+                List<ReplenishmentOrder> orders = replenishmentOrderService.list(noQw);
+                for (ReplenishmentOrder o : orders) {
+                    firstOrderNoMap.put(String.valueOf(o.getId()), o.getOrderNo());
+                }
+            }
+        }
+
         // 批量统计今日已补货设备(上货类型、当前补货员、今天)
         // 批量统计今日已补货设备(上货类型、当前补货员、今天)
         java.time.LocalDate today = java.time.LocalDate.now();
         java.time.LocalDate today = java.time.LocalDate.now();
         Map<String, Boolean> todayRestockedMap = new java.util.HashMap<>();
         Map<String, Boolean> todayRestockedMap = new java.util.HashMap<>();
@@ -159,6 +186,11 @@ public class ReplenisherOperationController {
             deviceInfo.put("submittedOrderCount", submittedOrderCountMap.getOrDefault(deviceId, 0L));
             deviceInfo.put("submittedOrderCount", submittedOrderCountMap.getOrDefault(deviceId, 0L));
             deviceInfo.put("todayReplenished", todayRestockedMap.containsKey(deviceId));
             deviceInfo.put("todayReplenished", todayRestockedMap.containsKey(deviceId));
             deviceInfo.put("replenishOrderGenerated", pendingOrderCountMap.getOrDefault(deviceId, 0L) > 0);
             deviceInfo.put("replenishOrderGenerated", pendingOrderCountMap.getOrDefault(deviceId, 0L) > 0);
+            if (firstOrderIdMap.containsKey(deviceId)) {
+                Long fid = firstOrderIdMap.get(deviceId);
+                deviceInfo.put("pendingOrderId", fid);
+                deviceInfo.put("pendingOrderNo", firstOrderNoMap.getOrDefault(String.valueOf(fid), ""));
+            }
 
 
             result.add(deviceInfo);
             result.add(deviceInfo);
         }
         }