Explorar o código

feat: 补货员端补货单功能+批量操作+库存联动+多项优化

补货员端(haha-admin-mp):
- 新增补货单列表页,按状态筛选、分页滚动
- 新增补货单详情页,展示订单信息和商品明细(含图片)
- 补货首页新增"补货单"快捷入口

管理端(haha-admin-web):
- 列表增加门店筛选、批量提交ERP/取消/删除
- 创建弹窗增加"仅显示需补货"开关
- ERP同步失败时显示"重试同步"按钮
- 完成补货单时自动更新设备库存(beforeStock/afterStock)
- 删除冗余的inventory/replenishment代码

后端:
- 新增 GET /replenisher/orders 和 GET /replenisher/orders/{id}
- completeOrder 联动更新 DeviceInventory 库存
- syncToErp 实际更新状态为已同步ERP
- 修复 LayerTemplateMapper 缺少 @Select、Lombok final 字段注入等问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline hai 1 semana
pai
achega
653b3c5512

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

@@ -80,3 +80,19 @@ export async function bindWechat(params: { bindingCode: string; code: string }):
   setUserType('replenisher');
   return response;
 }
+
+// ==================== 补货单相关 ====================
+
+/**
+ * 获取补货员的补货单列表
+ */
+export async function getReplenisherOrders(params: { page?: number; pageSize?: number; status?: number }): Promise<any> {
+  return get('/replenisher/orders', params);
+}
+
+/**
+ * 获取补货单详情(含明细)
+ */
+export async function getReplenisherOrderDetail(id: string): Promise<any> {
+  return get(`/replenisher/orders/${id}`);
+}

+ 18 - 0
haha-admin-mp/src/pages.json

@@ -421,6 +421,24 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/replenish/orders",
+      "style": {
+        "navigationBarTitleText": "补货单",
+        "navigationBarBackgroundColor": "#ffffff",
+        "navigationBarTextStyle": "black",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/replenish/order-detail",
+      "style": {
+        "navigationBarTitleText": "补货单详情",
+        "navigationBarBackgroundColor": "#ffffff",
+        "navigationBarTextStyle": "black",
+        "navigationStyle": "custom"
+      }
+    },
     {
       "style": {
         "navigationBarBackgroundColor": "#ffffff",

+ 43 - 0
haha-admin-mp/src/pages/replenish/index.vue

@@ -34,6 +34,14 @@
       </view>
     </view>
 
+    <!-- 快捷入口 -->
+    <view class="quick-actions">
+      <view class="action-btn" @click="goToOrders">
+        <text class="action-icon">📋</text>
+        <text class="action-label">补货单</text>
+      </view>
+    </view>
+
     <!-- 加载中 -->
     <view class="loading-container" v-if="loading">
       <view class="loading-spinner"></view>
@@ -141,6 +149,10 @@ const navigateToMy = () => {
   uni.switchTab({ url: '/pages/my/my' });
 };
 
+const goToOrders = () => {
+  uni.navigateTo({ url: '/pages/replenish/orders' });
+};
+
 const goToOperation = (device: any) => {
   uni.navigateTo({
     url: `/pages/replenish/operation?deviceId=${device.deviceId}`
@@ -311,6 +323,37 @@ onMounted(async () => {
   }
 }
 
+/* 快捷入口 */
+.quick-actions {
+  display: flex;
+  padding: 0 24rpx 16rpx;
+  gap: 16rpx;
+
+  .action-btn {
+    display: flex;
+    align-items: center;
+    gap: 8rpx;
+    background: $bg-color-card;
+    border: 1rpx solid $border-color;
+    border-radius: 12rpx;
+    padding: 16rpx 28rpx;
+
+    .action-icon {
+      font-size: 28rpx;
+    }
+
+    .action-label {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: $text-color-primary;
+    }
+
+    &:active {
+      background: $bg-color-secondary;
+    }
+  }
+}
+
 /* 加载 */
 .loading-container {
   display: flex;

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

@@ -0,0 +1,346 @@
+<template>
+  <view class="page">
+    <NavBar title="补货单详情" />
+
+    <view v-if="loading" class="loading-container">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <template v-else-if="order">
+      <!-- 订单头信息 -->
+      <view class="header-card">
+        <view class="header-row">
+          <text class="order-no">{{ order.orderNo }}</text>
+          <text :class="['status-tag', statusColor(order.status)]">
+            {{ statusText(order.status) }}
+          </text>
+        </view>
+        <view class="info-grid">
+          <view class="info-item">
+            <text class="info-label">设备</text>
+            <text class="info-value">{{ order.deviceName || order.deviceId }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">供应商</text>
+            <text class="info-value">{{ order.supplierName || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">仓库</text>
+            <text class="info-value">{{ order.warehouseName || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">总金额</text>
+            <text class="info-value price">¥{{ (order.totalAmount || 0).toFixed(2) }}</text>
+          </view>
+          <view class="info-item" v-if="order.expectedArrivalTime">
+            <text class="info-label">预计到货</text>
+            <text class="info-value">{{ order.expectedArrivalTime }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">创建时间</text>
+            <text class="info-value">{{ order.createTime }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 商品明细 -->
+      <view class="items-section">
+        <view class="section-title">补货明细(共 {{ items.length }} 种)</view>
+
+        <view
+          class="item-card"
+          v-for="(item, index) in items"
+          :key="index"
+        >
+          <view class="item-main">
+            <!-- 商品图片 -->
+            <image
+              v-if="item.productImage"
+              :src="item.productImage"
+              class="item-img"
+              mode="aspectFill"
+            />
+            <view v-else class="item-img-placeholder">
+              <text class="placeholder-text">无图</text>
+            </view>
+
+            <view class="item-info">
+              <text class="item-code">{{ item.productCode || '-' }}</text>
+              <text class="item-name">{{ item.productName || '未知商品' }}</text>
+              <view class="item-meta">
+                <text class="meta-text" v-if="item.shelfNum">层{{ item.shelfNum }}</text>
+                <text class="meta-text" v-if="item.position">{{ item.position }}</text>
+              </view>
+            </view>
+          </view>
+
+          <view class="item-detail">
+            <view class="detail-row">
+              <text class="detail-label">计划数量</text>
+              <text class="detail-value quantity">{{ item.plannedQuantity }}</text>
+            </view>
+            <view class="detail-row" v-if="item.actualQuantity != null">
+              <text class="detail-label">实际数量</text>
+              <text class="detail-value actual">{{ item.actualQuantity }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">单价</text>
+              <text class="detail-value">
+                {{ item.unitPrice != null ? '¥' + Number(item.unitPrice).toFixed(2) : '-' }}
+              </text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">小计</text>
+              <text class="detail-value price">
+                ¥{{ ((item.plannedQuantity || 0) * (item.unitPrice || 0)).toFixed(2) }}
+              </text>
+            </view>
+            <view class="detail-row" v-if="item.beforeStock != null">
+              <text class="detail-label">补前库存</text>
+              <text class="detail-value">{{ item.beforeStock }}</text>
+            </view>
+            <view class="detail-row" v-if="item.afterStock != null">
+              <text class="detail-label">补后库存</text>
+              <text class="detail-value after">{{ item.afterStock }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </template>
+
+    <CustomTabBar />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import CustomTabBar from '@/components/CustomTabBar.vue';
+import { getReplenisherOrderDetail } from '@/api/replenish';
+
+const loading = ref(true);
+const order = ref<any>(null);
+const items = ref<any[]>([]);
+
+const statusMap: Record<number, string> = {
+  0: '草稿', 1: '已提交', 2: '已同步', 3: '已完成', 4: '已取消'
+};
+const statusColorMap: Record<number, string> = {
+  0: 'draft', 1: 'submitted', 2: 'synced', 3: 'done', 4: 'cancelled'
+};
+
+function statusText(status: number): string { return statusMap[status] || '未知'; }
+function statusColor(status: number): string { return statusColorMap[status] || ''; }
+
+onMounted(async () => {
+  try {
+    const pages = getCurrentPages();
+    const currentPage = pages[pages.length - 1] as any;
+    const id = currentPage?.options?.id;
+    if (!id) {
+      uni.showToast({ title: '参数错误', icon: 'none' });
+      return;
+    }
+    const data = await getReplenisherOrderDetail(id);
+    order.value = data?.order;
+    items.value = data?.items || [];
+  } catch (e: any) {
+    uni.showToast({ title: e.message || '加载失败', icon: 'none' });
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: $bg-color-page;
+  padding-bottom: 160rpx;
+}
+
+.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;
+  }
+}
+
+/* 订单头 */
+.header-card {
+  background: $bg-color-card;
+  margin: 16rpx 24rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  border: 1rpx solid $border-color;
+
+  .header-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16rpx;
+    padding-bottom: 12rpx;
+    border-bottom: 1rpx solid $bg-color-secondary;
+
+    .order-no {
+      font-size: 30rpx;
+      font-weight: 700;
+      color: $text-color-primary;
+    }
+
+    .status-tag {
+      padding: 6rpx 18rpx;
+      border-radius: 8rpx;
+      font-size: 22rpx;
+      font-weight: 600;
+
+      &.draft { background: #f0f0f0; color: #999; }
+      &.submitted { background: #fff3e0; color: #ff9800; }
+      &.synced { background: #e3f2fd; color: #2196f3; }
+      &.done { background: #e8f5e9; color: #4caf50; }
+      &.cancelled { background: #ffebee; color: #f44336; }
+    }
+  }
+
+  .info-grid {
+    display: flex;
+    flex-wrap: wrap;
+
+    .info-item {
+      width: 50%;
+      padding: 8rpx 0;
+
+      .info-label {
+        display: block;
+        font-size: 22rpx;
+        color: $text-color-tertiary;
+      }
+
+      .info-value {
+        font-size: 26rpx;
+        color: $text-color-primary;
+        font-weight: 500;
+
+        &.price { color: $error-color; }
+      }
+    }
+  }
+}
+
+/* 商品明细 */
+.items-section {
+  padding: 0 24rpx;
+
+  .section-title {
+    font-size: 28rpx;
+    font-weight: 700;
+    color: $text-color-primary;
+    padding: 16rpx 0;
+  }
+}
+
+.item-card {
+  background: $bg-color-card;
+  border: 1rpx solid $border-color;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  margin-bottom: 16rpx;
+
+  .item-main {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 16rpx;
+
+    .item-img {
+      width: 80rpx;
+      height: 80rpx;
+      border-radius: 10rpx;
+      background: $bg-color-page;
+      margin-right: 16rpx;
+      flex-shrink: 0;
+    }
+
+    .item-img-placeholder {
+      width: 80rpx;
+      height: 80rpx;
+      border-radius: 10rpx;
+      background: $bg-color-page;
+      margin-right: 16rpx;
+      flex-shrink: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      .placeholder-text {
+        font-size: 20rpx;
+        color: $text-color-muted;
+      }
+    }
+
+    .item-info {
+      flex: 1;
+
+      .item-code {
+        display: block;
+        font-size: 22rpx;
+        color: $text-color-muted;
+      }
+
+      .item-name {
+        display: block;
+        font-size: 28rpx;
+        font-weight: 600;
+        color: $text-color-primary;
+        margin: 4rpx 0;
+      }
+
+      .item-meta {
+        display: flex;
+        gap: 12rpx;
+
+        .meta-text {
+          font-size: 20rpx;
+          color: $text-color-tertiary;
+          background: $bg-color-page;
+          padding: 2rpx 10rpx;
+          border-radius: 6rpx;
+        }
+      }
+    }
+  }
+
+  .item-detail {
+    background: $bg-color-page;
+    border-radius: 10rpx;
+    padding: 12rpx 16rpx;
+
+    .detail-row {
+      display: flex;
+      justify-content: space-between;
+      padding: 6rpx 0;
+
+      .detail-label {
+        font-size: 24rpx;
+        color: $text-color-tertiary;
+      }
+
+      .detail-value {
+        font-size: 24rpx;
+        color: $text-color-primary;
+
+        &.quantity { font-weight: 700; color: $primary-color; }
+        &.actual { font-weight: 700; color: #4caf50; }
+        &.after { font-weight: 700; color: #4caf50; }
+        &.price { color: $error-color; }
+      }
+    }
+  }
+}
+</style>

+ 315 - 0
haha-admin-mp/src/pages/replenish/orders.vue

@@ -0,0 +1,315 @@
+<template>
+  <view class="page">
+    <NavBar title="补货单" />
+
+    <!-- 状态筛选 -->
+    <view class="filter-bar">
+      <view
+        v-for="item in statusTabs"
+        :key="item.value"
+        :class="['filter-tag', { active: currentStatus === item.value }]"
+        @click="switchStatus(item.value)"
+      >
+        {{ item.label }}
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading-container" v-if="loading">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-else-if="list.length === 0">
+      <text class="empty-text">暂无补货单</text>
+    </view>
+
+    <!-- 补货单列表 -->
+    <scroll-view
+      class="list-scroll"
+      scroll-y
+      @scrolltolower="loadMore"
+      v-else
+    >
+      <view
+        class="order-card"
+        v-for="order in list"
+        :key="order.id"
+        @click="goToDetail(order)"
+      >
+        <view class="card-header">
+          <text class="order-no">{{ order.orderNo }}</text>
+          <text :class="['status-tag', statusColor(order.status)]">
+            {{ statusText(order.status) }}
+          </text>
+        </view>
+
+        <view class="card-body">
+          <view class="info-row">
+            <text class="info-label">设备</text>
+            <text class="info-value">{{ order.deviceName || order.deviceId }}</text>
+          </view>
+          <view class="info-row" v-if="order.supplierName">
+            <text class="info-label">供应商</text>
+            <text class="info-value">{{ order.supplierName }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">商品数</text>
+            <text class="info-value">{{ order.itemCount || 0 }} 种</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">总数量 / 金额</text>
+            <text class="info-value">
+              {{ order.totalQuantity || 0 }} 件 /
+              ¥{{ (order.totalAmount || 0).toFixed(2) }}
+            </text>
+          </view>
+          <view class="info-row" v-if="order.expectedArrivalTime">
+            <text class="info-label">预计到货</text>
+            <text class="info-value">{{ order.expectedArrivalTime }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">创建时间</text>
+            <text class="info-value">{{ order.createTime }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="load-more" v-if="hasMore">
+        <text class="load-more-text">上拉加载更多</text>
+      </view>
+      <view class="load-more" v-else-if="list.length > 0">
+        <text class="load-more-text">- 没有更多了 -</text>
+      </view>
+    </scroll-view>
+
+    <CustomTabBar />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import CustomTabBar from '@/components/CustomTabBar.vue';
+import { getReplenisherOrders } from '@/api/replenish';
+
+const loading = ref(true);
+const list = ref<any[]>([]);
+const currentStatus = ref<number | undefined>(undefined);
+const page = ref(1);
+const pageSize = 10;
+const total = ref(0);
+const hasMore = ref(true);
+
+const statusTabs = [
+  { label: '全部', value: undefined },
+  { label: '草稿', value: 0 },
+  { label: '已提交', value: 1 },
+  { label: '已完成', value: 3 }
+];
+
+const statusMap: Record<number, string> = {
+  0: '草稿',
+  1: '已提交',
+  2: '已同步',
+  3: '已完成',
+  4: '已取消'
+};
+
+const statusColorMap: Record<number, string> = {
+  0: 'draft',
+  1: 'submitted',
+  2: 'synced',
+  3: 'done',
+  4: 'cancelled'
+};
+
+function statusText(status: number): string {
+  return statusMap[status] || '未知';
+}
+
+function statusColor(status: number): string {
+  return statusColorMap[status] || '';
+}
+
+async function loadData(reset = false) {
+  if (reset) {
+    page.value = 1;
+    list.value = [];
+    hasMore.value = true;
+  }
+  loading.value = true;
+  try {
+    const data = await getReplenisherOrders({
+      page: page.value,
+      pageSize,
+      status: currentStatus.value
+    });
+    const records = data?.records || [];
+    if (reset) {
+      list.value = records;
+    } else {
+      list.value.push(...records);
+    }
+    total.value = data?.total || 0;
+    hasMore.value = list.value.length < total.value;
+  } catch (e: any) {
+    uni.showToast({ title: e.message || '加载失败', icon: 'none' });
+  } finally {
+    loading.value = false;
+  }
+}
+
+function switchStatus(status: number | undefined) {
+  currentStatus.value = status;
+  loadData(true);
+}
+
+function loadMore() {
+  if (!hasMore.value || loading.value) return;
+  page.value++;
+  loadData(false);
+}
+
+function goToDetail(order: any) {
+  uni.navigateTo({
+    url: `/pages/replenish/order-detail?id=${order.id}`
+  });
+}
+
+onMounted(() => {
+  loadData(true);
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: $bg-color-page;
+  padding-bottom: 160rpx;
+}
+
+.filter-bar {
+  display: flex;
+  padding: 16rpx 24rpx;
+  gap: 16rpx;
+  background: $bg-color-card;
+  border-bottom: 1rpx solid $border-color;
+  overflow-x: auto;
+  white-space: nowrap;
+
+  .filter-tag {
+    padding: 10rpx 24rpx;
+    border-radius: 10rpx;
+    font-size: 24rpx;
+    color: $text-color-tertiary;
+    background: $bg-color-page;
+    flex-shrink: 0;
+
+    &.active {
+      background: $primary-color;
+      color: #fff;
+      font-weight: 600;
+    }
+  }
+}
+
+.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;
+  }
+}
+
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 100rpx 0;
+
+  .empty-text {
+    font-size: 28rpx;
+    color: $text-color-tertiary;
+  }
+}
+
+.list-scroll {
+  height: calc(100vh - 200rpx);
+  padding: 16rpx 24rpx;
+}
+
+.order-card {
+  background: $bg-color-card;
+  border: 1rpx solid $border-color;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+
+  &:active {
+    background: $bg-color-secondary;
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16rpx;
+    padding-bottom: 12rpx;
+    border-bottom: 1rpx solid $bg-color-secondary;
+
+    .order-no {
+      font-size: 28rpx;
+      font-weight: 600;
+      color: $text-color-primary;
+    }
+
+    .status-tag {
+      padding: 4rpx 16rpx;
+      border-radius: 8rpx;
+      font-size: 22rpx;
+      font-weight: 500;
+
+      &.draft { background: #f0f0f0; color: #999; }
+      &.submitted { background: #fff3e0; color: #ff9800; }
+      &.synced { background: #e3f2fd; color: #2196f3; }
+      &.done { background: #e8f5e9; color: #4caf50; }
+      &.cancelled { background: #ffebee; color: #f44336; }
+    }
+  }
+
+  .card-body {
+    .info-row {
+      display: flex;
+      justify-content: space-between;
+      padding: 6rpx 0;
+
+      .info-label {
+        font-size: 24rpx;
+        color: $text-color-tertiary;
+      }
+
+      .info-value {
+        font-size: 24rpx;
+        color: $text-color-primary;
+      }
+    }
+  }
+}
+
+.load-more {
+  text-align: center;
+  padding: 24rpx 0;
+
+  .load-more-text {
+    font-size: 24rpx;
+    color: $text-color-muted;
+  }
+}
+</style>

+ 0 - 9
haha-admin-web/src/router/modules/inventory.ts

@@ -35,15 +35,6 @@ export default {
         title: "上货记录"
       }
     },
-    {
-      path: "/inventory/replenishment",
-      name: "ReplenishmentOrders",
-      component: () => import("@/views/inventory/replenishment/index.vue"),
-      meta: {
-        icon: "ri:shopping-bag-3-line",
-        title: "补货单管理"
-      }
-    },
     {
       path: "/inventory/replenishment-order/detail",
       name: "ReplenishmentOrderDetail",

+ 0 - 234
haha-admin-web/src/views/inventory/replenishment/index.vue

@@ -1,234 +0,0 @@
-<script setup lang="ts">
-import { ref } from "vue";
-import { useReplenishment } from "./utils/hook";
-import { PureTableBar } from "@/components/RePureTableBar";
-import { useRenderIcon } from "@/components/ReIcon/src/hooks";
-import Refresh from "~icons/ep/refresh";
-import AddFill from "~icons/ri/add-circle-line";
-import Eye from "~icons/ri/eye-line";
-import Send from "~icons/ri/send-plane-line";
-import Cloud from "~icons/ri/cloud-line";
-import Check from "~icons/ri/checkbox-circle-line";
-import Close from "~icons/ri/close-circle-line";
-import Delete from "~icons/ri/delete-bin-line";
-
-defineOptions({
-  name: "ReplenishmentManage"
-});
-
-const formRef = ref();
-
-const {
-  form,
-  loading,
-  columns,
-  dataList,
-  pagination,
-  deviceOptions,
-  statusMap,
-  onSearch,
-  resetForm,
-  handleCreate,
-  handleViewDetail,
-  handleDelete,
-  handleSubmit,
-  handleSyncErp,
-  handleComplete,
-  handleCancel,
-  handleSizeChange,
-  handleCurrentChange
-} = useReplenishment();
-</script>
-
-<template>
-  <div class="main">
-    <el-form
-      ref="formRef"
-      :inline="true"
-      :model="form"
-      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
-    >
-      <el-form-item label="补货单号:" prop="orderNo">
-        <el-input
-          v-model="form.orderNo"
-          placeholder="请输入补货单号"
-          clearable
-          class="w-[180px]!"
-        />
-      </el-form-item>
-      <el-form-item label="设备:" prop="deviceId">
-        <el-select
-          v-model="form.deviceId"
-          placeholder="请选择设备"
-          clearable
-          filterable
-          class="w-[180px]!"
-        >
-          <el-option
-            v-for="item in deviceOptions"
-            :key="item.deviceId"
-            :label="`${item.deviceId} — ${item.shopName || ''}`"
-            :value="item.deviceId"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="状态:" prop="status">
-        <el-select
-          v-model="form.status"
-          placeholder="请选择状态"
-          clearable
-          class="w-[140px]!"
-        >
-          <el-option
-            v-for="(v, k) in statusMap"
-            :key="k"
-            :label="v.text"
-            :value="Number(k)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="创建时间:" prop="timeRange">
-        <el-date-picker
-          v-model="form.timeRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          value-format="YYYY-MM-DD"
-          class="w-[240px]!"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button
-          type="primary"
-          :icon="useRenderIcon('ri/search-line')"
-          :loading="loading"
-          @click="onSearch"
-        >
-          搜索
-        </el-button>
-        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
-          重置
-        </el-button>
-      </el-form-item>
-    </el-form>
-
-    <PureTableBar
-      title="补货单管理"
-      :columns="columns"
-      @refresh="onSearch"
-    >
-      <template #buttons>
-        <el-button
-          type="primary"
-          :icon="useRenderIcon(AddFill)"
-          @click="handleCreate"
-        >
-          创建补货单
-        </el-button>
-      </template>
-      <template v-slot="{ size, dynamicColumns }">
-        <pure-table
-          row-key="id"
-          adaptive
-          :adaptiveConfig="{ offsetBottom: 108 }"
-          align-whole="center"
-          table-layout="auto"
-          :loading="loading"
-          :size="size"
-          :data="dataList"
-          :columns="dynamicColumns"
-          :pagination="{ ...pagination, size }"
-          :header-cell-style="{
-            background: 'var(--el-fill-color-light)',
-            color: 'var(--el-text-color-primary)'
-          }"
-          @page-size-change="handleSizeChange"
-          @page-current-change="handleCurrentChange"
-        >
-          <template #operation="{ row }">
-            <el-button
-              class="reset-margin"
-              link
-              type="primary"
-              :size="size"
-              :icon="useRenderIcon(Eye)"
-              @click="handleViewDetail(row)"
-            >
-              查看详情
-            </el-button>
-            <el-button
-              v-if="row.status === 0"
-              class="reset-margin"
-              link
-              type="success"
-              :size="size"
-              :icon="useRenderIcon(Send)"
-              @click="handleSubmit(row)"
-            >
-              提交
-            </el-button>
-            <el-button
-              v-if="row.status === 1"
-              class="reset-margin"
-              link
-              type="warning"
-              :size="size"
-              :icon="useRenderIcon(Cloud)"
-              @click="handleSyncErp(row)"
-            >
-              同步ERP
-            </el-button>
-            <el-button
-              v-if="row.status === 2"
-              class="reset-margin"
-              link
-              type="success"
-              :size="size"
-              :icon="useRenderIcon(Check)"
-              @click="handleComplete(row)"
-            >
-              完成
-            </el-button>
-            <el-button
-              v-if="row.status === 0 || row.status === 1"
-              class="reset-margin"
-              link
-              type="danger"
-              :size="size"
-              :icon="useRenderIcon(Close)"
-              @click="handleCancel(row)"
-            >
-              取消
-            </el-button>
-            <el-popconfirm
-              v-if="row.status === 0"
-              title="是否确认删除?"
-              @confirm="handleDelete(row)"
-            >
-              <template #reference>
-                <el-button
-                  class="reset-margin"
-                  link
-                  type="danger"
-                  :size="size"
-                  :icon="useRenderIcon(Delete)"
-                >
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </pure-table>
-      </template>
-    </PureTableBar>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.search-form {
-  :deep(.el-form-item) {
-    margin-bottom: 12px;
-  }
-}
-</style>

+ 0 - 615
haha-admin-web/src/views/inventory/replenishment/utils/hook.tsx

@@ -1,615 +0,0 @@
-import dayjs from "dayjs";
-import type { PaginationProps } from "@pureadmin/table";
-import { deviceDetection } from "@pureadmin/utils";
-import { message } from "@/utils/message";
-import { addDialog } from "@/components/ReDialog";
-import {
-  getOrderList,
-  getOrderDetail,
-  getOrderItems,
-  createOrder,
-  deleteOrder,
-  submitOrder,
-  syncToErp,
-  completeOrder,
-  cancelOrder,
-  getReplenishmentSuggestions
-} from "@/api/replenishmentOrder";
-import { getDeviceList } from "@/api/device";
-import { reactive, ref, onMounted } from "vue";
-import {
-  ElMessageBox,
-  ElTag,
-  ElInput,
-  ElInputNumber,
-  ElDatePicker,
-  ElDescriptions,
-  ElDescriptionsItem,
-  ElTable,
-  ElTableColumn,
-  ElButton,
-  ElDivider,
-  ElCard
-} from "element-plus";
-import type {
-  ReplenishmentSearchForm,
-  ReplenishmentOrderRow,
-  ReplenishmentSuggestion,
-  SuggestionItem
-} from "./types";
-
-export function useReplenishment() {
-  const form = reactive<ReplenishmentSearchForm>({
-    orderNo: "",
-    deviceId: "",
-    status: undefined,
-    timeRange: []
-  });
-  const formRef = ref();
-  const dataList = ref<ReplenishmentOrderRow[]>([]);
-  const loading = ref(true);
-  const deviceOptions = ref<any[]>([]);
-  const pagination = reactive<PaginationProps>({
-    total: 0,
-    pageSize: 10,
-    currentPage: 1,
-    background: true
-  });
-
-  const statusMap: Record<number, { text: string; type: string }> = {
-    0: { text: "草稿", type: "info" },
-    1: { text: "已提交", type: "warning" },
-    2: { text: "已同步ERP", type: "primary" },
-    3: { text: "已完成", type: "success" },
-    4: { text: "已取消", type: "danger" }
-  };
-
-  const columns: TableColumnList = [
-    { label: "补货单号", prop: "orderNo", minWidth: 180 },
-    { label: "门店名称", prop: "shopName", minWidth: 130 },
-    { label: "设备ID", prop: "deviceId", minWidth: 130 },
-    { label: "设备名称", prop: "deviceName", minWidth: 130 },
-    { label: "补货数量", prop: "totalQuantity", width: 100 },
-    {
-      label: "补货金额",
-      prop: "totalAmount",
-      width: 110,
-      formatter: ({ totalAmount }: any) =>
-        totalAmount != null ? `¥${Number(totalAmount).toFixed(2)}` : "-"
-    },
-    { label: "供应商", prop: "supplierName", minWidth: 120 },
-    {
-      label: "状态",
-      prop: "status",
-      width: 100,
-      cellRenderer: ({ row }: any) => {
-        const s = statusMap[row.status];
-        return s ? <ElTag type={s.type as any}>{s.text}</ElTag> : "-";
-      }
-    },
-    { label: "创建人", prop: "creatorName", width: 100 },
-    { label: "明细数", prop: "itemCount", width: 80 },
-    {
-      label: "创建时间",
-      prop: "createTime",
-      minWidth: 160,
-      formatter: ({ createTime }: any) =>
-        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
-    },
-    { label: "操作", fixed: "right", width: 280, slot: "operation" }
-  ];
-
-  // ==================== 搜索 / 分页 ====================
-
-  async function onSearch() {
-    loading.value = true;
-    try {
-      const params: any = {
-        page: pagination.currentPage,
-        pageSize: pagination.pageSize
-      };
-      if (form.orderNo) params.orderNo = form.orderNo;
-      if (form.deviceId) params.deviceId = form.deviceId;
-      if (form.status !== undefined && form.status !== null && form.status !== "")
-        params.status = form.status;
-      if (form.timeRange?.length === 2) {
-        params.startTime = form.timeRange[0];
-        params.endTime = form.timeRange[1];
-      }
-      const { data } = await getOrderList(params);
-      if (data) {
-        dataList.value = (data.list || []) as ReplenishmentOrderRow[];
-        pagination.total = Number(data.total) || 0;
-      }
-    } catch (e) {
-      console.error("获取补货单列表失败:", e);
-    } finally {
-      setTimeout(() => { loading.value = false; }, 300);
-    }
-  }
-
-  function resetForm(formEl: any) {
-    if (!formEl) return;
-    formEl.resetFields();
-    (form as any).timeRange = [];
-    pagination.currentPage = 1;
-    onSearch();
-  }
-
-  function handleSizeChange(val: number) {
-    pagination.pageSize = val;
-    onSearch();
-  }
-
-  function handleCurrentChange(val: number) {
-    pagination.currentPage = val;
-    onSearch();
-  }
-
-  async function fetchDeviceOptions() {
-    try {
-      const { data } = await getDeviceList({ page: 1, pageSize: 1000 });
-      deviceOptions.value = data?.list || [];
-    } catch (e) {
-      console.error("获取设备列表失败:", e);
-    }
-  }
-
-  // ==================== 创建补货单弹窗 ====================
-
-  function handleCreate() {
-    const selectedDevices = ref<any[]>([]);
-    const suggestions = ref<ReplenishmentSuggestion[]>([]);
-    const suggestionsLoading = ref(false);
-    const commonForm = reactive({
-      supplierName: "",
-      warehouseName: "",
-      expectedArrivalTime: ""
-    });
-    const innerDeviceOptions = ref<any[]>([]);
-
-    async function loadDevices() {
-      try {
-        const { data } = await getDeviceList({ page: 1, pageSize: 1000 });
-        innerDeviceOptions.value = data?.list || [];
-      } catch (e) {
-        console.error("加载设备失败:", e);
-      }
-    }
-
-    function handleSelectionChange(selection: any[]) {
-      selectedDevices.value = selection;
-    }
-
-    async function fetchSuggestions() {
-      if (selectedDevices.value.length === 0) {
-        message("请先勾选设备", { type: "warning" });
-        return;
-      }
-      suggestionsLoading.value = true;
-      try {
-        const ids = selectedDevices.value.map((d: any) => d.deviceId as string);
-        const { data } = await getReplenishmentSuggestions(ids);
-        const list: ReplenishmentSuggestion[] = Array.isArray(data) ? data : [];
-        if (list.length === 0) {
-          message("所选设备当前均无需补货", { type: "info" });
-        }
-        suggestions.value = list;
-      } catch (e) {
-        console.error("获取补货建议失败:", e);
-        message("获取补货建议失败", { type: "error" });
-      } finally {
-        suggestionsLoading.value = false;
-      }
-    }
-
-    async function doSubmit(done: () => void) {
-      const validSuggestions = suggestions.value.filter(
-        s => s.items.some(i => i.suggestedQuantity > 0)
-      );
-      if (validSuggestions.length === 0) {
-        message("没有有效的补货项,请检查补货数量", { type: "warning" });
-        return;
-      }
-
-      const payloads = validSuggestions.map(s => ({
-          deviceId: s.deviceId,
-          shopId: s.shopId ? Number(s.shopId) : undefined,
-          supplierName: commonForm.supplierName || undefined,
-          warehouseName: commonForm.warehouseName || undefined,
-          expectedArrivalTime: commonForm.expectedArrivalTime
-            ? dayjs(commonForm.expectedArrivalTime).format("YYYY-MM-DD HH:mm:ss")
-            : undefined,
-          items: s.items
-            .filter(i => i.suggestedQuantity > 0)
-            .map(i => ({
-              productId: Number(i.productId),
-              productCode: i.productCode,
-              productName: i.productName,
-              plannedQuantity: i.suggestedQuantity,
-              unitPrice: i.unitPrice != null ? i.unitPrice : undefined,
-              shelfNum: i.shelfNum,
-              position: i.position
-            }))
-        }));
-
-        const results = await Promise.allSettled(
-          payloads.map(p => createOrder(p))
-        );
-
-        const successCount = results.filter(r => r.status === "fulfilled").length;
-        const failCount = results.filter(r => r.status === "rejected").length;
-
-        if (failCount === 0) {
-          message(`成功创建 ${successCount} 个补货单`, { type: "success" });
-        } else {
-          message(`创建完成:${successCount} 成功, ${failCount} 失败`, { type: "warning" });
-        }
-        done();
-        onSearch();
-    }
-
-    const createContentRenderer = () => {
-      const hasSuggestions = suggestions.value.length > 0;
-
-      return (
-        <div style="max-height: 65vh; overflow-y: auto;">
-          {/* ===== 第一步:设备列表 ===== */}
-          <div style="margin-bottom:8px;font-size:14px;font-weight:600;color:#303133;">
-            第一步:勾选需要补货的设备
-          </div>
-          <ElTable
-            data={innerDeviceOptions.value}
-            max-height={220}
-            onSelectionChange={handleSelectionChange}
-          >
-            <ElTableColumn type="selection" width="55" />
-            <ElTableColumn prop="deviceId" label="设备ID" minWidth={140} />
-            <ElTableColumn prop="name" label="设备名称" minWidth={140} />
-            <ElTableColumn prop="shopName" label="所属门店" minWidth={140} />
-          </ElTable>
-
-          <div style="margin-top:12px;display:flex;align-items:center;gap:12px;">
-            <ElButton
-              type="primary"
-              loading={suggestionsLoading.value}
-              onClick={fetchSuggestions}
-              disabled={selectedDevices.value.length === 0}
-            >
-              补货建议
-            </ElButton>
-            <span style="color:#909399;font-size:13px;">
-              已勾选 {selectedDevices.value.length} 台设备
-              {hasSuggestions
-                ? `,共 ${suggestions.value.reduce((s: number, sug) => s + sug.items.length, 0)} 项待补商品`
-                : ""}
-            </span>
-          </div>
-
-          {/* ===== 第二步:补货明细 ===== */}
-          {hasSuggestions && (
-            <div style="margin-top:24px;">
-              <div style="margin-bottom:12px;font-size:14px;font-weight:600;color:#303133;">
-                第二步:确认补货明细,填写供应商等信息
-              </div>
-
-              <div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px;padding:12px;background:#fafafa;border-radius:6px;">
-                <div style="display:flex;align-items:center;gap:6px;">
-                  <span style="font-size:13px;color:#606266;white-space:nowrap;">供应商</span>
-                  <ElInput v-model={commonForm.supplierName} placeholder="选填" clearable size="small" style="width:150px;" />
-                </div>
-                <div style="display:flex;align-items:center;gap:6px;">
-                  <span style="font-size:13px;color:#606266;white-space:nowrap;">仓库</span>
-                  <ElInput v-model={commonForm.warehouseName} placeholder="选填" clearable size="small" style="width:150px;" />
-                </div>
-                <div style="display:flex;align-items:center;gap:6px;">
-                  <span style="font-size:13px;color:#606266;white-space:nowrap;">预计到货</span>
-                  <ElDatePicker v-model={commonForm.expectedArrivalTime} type="datetime" placeholder="选填"
-                    value-format="YYYY-MM-DD HH:mm:ss" size="small" style="width:190px;" />
-                </div>
-              </div>
-
-              {suggestions.value.map((sug: ReplenishmentSuggestion) => {
-                const deviceTotalQty = sug.items.reduce(
-                  (sum: number, i) => sum + (i.suggestedQuantity || 0), 0
-                );
-                const deviceTotalAmt = sug.items.reduce(
-                  (sum: number, i) => sum + (i.suggestedQuantity || 0) * (i.unitPrice || 0), 0
-                );
-
-                const itemRows = sug.items.map((item: SuggestionItem, idx: number) => (
-                  <tr key={idx}>
-                    <td style="padding:6px 10px;border:1px solid #ebeef5;font-size:13px;">{item.productCode}</td>
-                    <td style="padding:6px 10px;border:1px solid #ebeef5;font-size:13px;">{item.productName || "-"}</td>
-                    <td style="padding:6px 10px;border:1px solid #ebeef5;text-align:center;font-size:13px;">{item.templateStock}</td>
-                    <td style="padding:6px 10px;border:1px solid #ebeef5;text-align:center;font-size:13px;">
-                      <span style={`font-weight:500;color:${item.currentStock <= 0 ? '#f56c6c' : '#67c23a'}`}>{item.currentStock}</span>
-                    </td>
-                    <td style="padding:4px 6px;border:1px solid #ebeef5;">
-                      <ElInputNumber v-model={item.suggestedQuantity} min={1} max={9999} size="small" controls-position="right" style="width:100px;" />
-                    </td>
-                    <td style="padding:4px 6px;border:1px solid #ebeef5;">
-                      <ElInput v-model={item.unitPrice} placeholder="单价" size="small" style="width:90px;" />
-                    </td>
-                    <td style="padding:6px 10px;border:1px solid #ebeef5;text-align:right;font-size:13px;font-weight:500;">
-                      ¥{((item.suggestedQuantity || 0) * (item.unitPrice || 0)).toFixed(2)}
-                    </td>
-                  </tr>
-                ));
-
-                return (
-                  <ElCard key={sug.deviceId} style="margin-bottom:12px;" shadow="hover">
-                    {{
-                      header: () => (
-                        <div style="display:flex;justify-content:space-between;align-items:center;">
-                          <span style="font-size:14px;font-weight:600;">
-                            {sug.deviceId}{sug.deviceName ? ` (${sug.deviceName})` : ""}
-                            <span style="margin:0 8px;color:#dcdfe6;">|</span>
-                            {sug.shopName || "未绑定门店"}
-                          </span>
-                          <span style="color:#409eff;font-size:13px;">{deviceTotalQty} 件 / ¥{deviceTotalAmt.toFixed(2)}</span>
-                        </div>
-                      ),
-                      default: () => (
-                        <table style="width:100%;border-collapse:collapse;">
-                          <thead>
-                            <tr style="background:#f5f7fa;">
-                              <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">商品编码</th>
-                              <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">商品名称</th>
-                              <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">标准库存</th>
-                              <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">当前库存</th>
-                              <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">补货数量</th>
-                              <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">单价</th>
-                              <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">小计</th>
-                            </tr>
-                          </thead>
-                          <tbody>{itemRows}</tbody>
-                        </table>
-                      )
-                    }}
-                  </ElCard>
-                );
-              })}
-
-              {/* 提示 */}
-              <div style="margin-top:16px;padding:10px 16px;background:#ecf5ff;border-radius:6px;color:#409eff;font-size:13px;">
-                确认补货明细无误后,点击「确认」按钮即可创建补货单
-              </div>
-            </div>
-          )}
-        </div>
-      );
-    };
-
-    loadDevices();
-    addDialog({
-      title: "创建补货单",
-      width: "80%",
-      draggable: true,
-      closeOnClickModal: false,
-      fullscreen: deviceDetection(),
-      contentRenderer: createContentRenderer,
-      beforeSure: async (done: () => void) => {
-        // 确保用户已获取补货建议
-        if (suggestions.value.length === 0) {
-          message("请先勾选设备并点击「补货建议」生成补货明细", { type: "warning" });
-          return;
-        }
-        await doSubmit(done);
-      }
-    });
-  }
-
-  // ==================== 查看详情弹窗 ====================
-
-  function handleViewDetail(row: ReplenishmentOrderRow) {
-    const orderDetail = ref<any>(null);
-    const items = ref<any[]>([]);
-    const detailLoading = ref(true);
-
-    async function loadDetail() {
-      detailLoading.value = true;
-      try {
-        const [orderRes, itemsRes] = await Promise.all([
-          getOrderDetail(row.id),
-          getOrderItems(row.id)
-        ]);
-        orderDetail.value = orderRes.data || (orderRes as any);
-        items.value = Array.isArray(itemsRes.data) ? itemsRes.data : [];
-      } catch (e) {
-        console.error("获取补货单详情失败:", e);
-      } finally {
-        detailLoading.value = false;
-      }
-    }
-
-    const detailRenderer = () => {
-      if (detailLoading.value) {
-        return <div style="text-align:center;padding:40px;">加载中...</div>;
-      }
-      const o = orderDetail.value || {};
-      const s = statusMap[o.status as number];
-      return (
-        <div style="max-height:60vh;overflow-y:auto;">
-          <ElDescriptions column={2} border size="small">
-            <ElDescriptionsItem label="补货单号" span={2}>
-              {o.orderNo || "-"}
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="设备ID">{o.deviceId || "-"}</ElDescriptionsItem>
-            <ElDescriptionsItem label="设备名称">{o.deviceName || "-"}</ElDescriptionsItem>
-            <ElDescriptionsItem label="门店">{o.shopName || "-"}</ElDescriptionsItem>
-            <ElDescriptionsItem label="状态">
-              {s ? <ElTag type={s.type as any} size="small">{s.text}</ElTag> : "-"}
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="总数量">{o.totalQuantity ?? "-"}</ElDescriptionsItem>
-            <ElDescriptionsItem label="总金额">
-              {o.totalAmount != null ? `¥${Number(o.totalAmount).toFixed(2)}` : "-"}
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="供应商">
-              {o.supplierName || "-"}
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="仓库">
-              {o.warehouseName || "-"}
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="预计到货">
-              {o.expectedArrivalTime
-                ? dayjs(o.expectedArrivalTime).format("YYYY-MM-DD HH:mm:ss")
-                : "-"}
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="创建人">{o.creatorName || "-"}</ElDescriptionsItem>
-            <ElDescriptionsItem label="创建时间">
-              {o.createTime
-                ? dayjs(o.createTime).format("YYYY-MM-DD HH:mm:ss")
-                : "-"}
-            </ElDescriptionsItem>
-            <ElDescriptionsItem label="备注" span={2}>
-              {o.remark || "-"}
-            </ElDescriptionsItem>
-          </ElDescriptions>
-
-          <ElDivider contentPosition="left">补货明细</ElDivider>
-          <ElTable data={items.value} size="small" max-height={300}>
-            <ElTableColumn prop="productCode" label="商品编码" minWidth={120} />
-            <ElTableColumn prop="productName" label="商品名称" minWidth={130} />
-            <ElTableColumn prop="plannedQuantity" label="计划数量" width={90} />
-            <ElTableColumn prop="actualQuantity" label="实际数量" width={90} />
-            <ElTableColumn prop="beforeStock" label="补货前库存" width={110} />
-            <ElTableColumn prop="afterStock" label="补货后库存" width={110} />
-            <ElTableColumn
-              prop="unitPrice"
-              label="单价"
-              width={90}
-              formatter={(r: any) =>
-                r.unitPrice != null ? `¥${Number(r.unitPrice).toFixed(2)}` : "-"
-              }
-            />
-            <ElTableColumn
-              prop="totalPrice"
-              label="小计"
-              width={90}
-              formatter={(r: any) =>
-                r.totalPrice != null
-                  ? `¥${Number(r.totalPrice).toFixed(2)}`
-                  : "-"
-              }
-            />
-            <ElTableColumn prop="shelfNum" label="层号" width={70} />
-            <ElTableColumn prop="position" label="货道" width={80} />
-          </ElTable>
-        </div>
-      );
-    };
-
-    loadDetail();
-    addDialog({
-      title: `补货单详情 - ${row.orderNo}`,
-      width: "70%",
-      draggable: true,
-      closeOnClickModal: false,
-      fullscreen: deviceDetection(),
-      contentRenderer: detailRenderer,
-      hideFooter: true
-    });
-  }
-
-  // ==================== 生命周期操作 ====================
-
-  async function handleSubmit(row: ReplenishmentOrderRow) {
-    try {
-      await ElMessageBox.confirm(`确认提交补货单 ${row.orderNo}?`, "提示", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "warning"
-      });
-      await submitOrder(row.id);
-      message("提交成功", { type: "success" });
-      onSearch();
-    } catch {
-      // cancelled
-    }
-  }
-
-  async function handleDelete(row: ReplenishmentOrderRow) {
-    try {
-      await deleteOrder(row.id);
-      message("删除成功", { type: "success" });
-      onSearch();
-    } catch (e) {
-      console.error("删除补货单失败:", e);
-    }
-  }
-
-  async function handleSyncErp(row: ReplenishmentOrderRow) {
-    try {
-      await ElMessageBox.confirm(
-        `确认将补货单 ${row.orderNo} 同步到ERP?`,
-        "提示",
-        { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
-      );
-      await syncToErp(row.id);
-      message("同步成功", { type: "success" });
-      onSearch();
-    } catch {
-      // cancelled
-    }
-  }
-
-  async function handleComplete(row: ReplenishmentOrderRow) {
-    try {
-      await ElMessageBox.confirm(
-        `确认完成补货单 ${row.orderNo}?`,
-        "提示",
-        { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
-      );
-      await completeOrder(row.id);
-      message("完成", { type: "success" });
-      onSearch();
-    } catch {
-      // cancelled
-    }
-  }
-
-  async function handleCancel(row: ReplenishmentOrderRow) {
-    try {
-      await ElMessageBox.confirm(
-        `确认取消补货单 ${row.orderNo}?取消后不可恢复。`,
-        "提示",
-        { confirmButtonText: "确定", cancelButtonText: "取消", type: "error" }
-      );
-      await cancelOrder(row.id);
-      message("已取消", { type: "success" });
-      onSearch();
-    } catch {
-      // cancelled
-    }
-  }
-
-  // ==================== 初始化 ====================
-
-  onMounted(async () => {
-    await fetchDeviceOptions();
-    onSearch();
-  });
-
-  return {
-    form,
-    formRef,
-    loading,
-    columns,
-    dataList,
-    pagination,
-    deviceOptions,
-    statusMap,
-    onSearch,
-    resetForm,
-    handleSizeChange,
-    handleCurrentChange,
-    handleCreate,
-    handleViewDetail,
-    handleDelete,
-    handleSubmit,
-    handleSyncErp,
-    handleComplete,
-    handleCancel
-  };
-}

+ 0 - 52
haha-admin-web/src/views/inventory/replenishment/utils/types.ts

@@ -1,52 +0,0 @@
-/** 搜索表单 */
-export interface ReplenishmentSearchForm {
-  orderNo: string;
-  deviceId: string;
-  status: number | undefined;
-  timeRange: string[];
-}
-
-/** 补货单列表行 */
-export interface ReplenishmentOrderRow {
-  id: string;
-  orderNo: string;
-  deviceId: string;
-  deviceName: string;
-  shopId: string;
-  shopName: string;
-  status: number;
-  statusLabel: string;
-  statusColor: string;
-  totalQuantity: number;
-  totalAmount: number;
-  supplierName: string;
-  warehouseName: string;
-  expectedArrivalTime: string;
-  remark: string;
-  creatorName: string;
-  itemCount: number;
-  createTime: string;
-}
-
-/** 单条补货建议 */
-export interface SuggestionItem {
-  productId: string;
-  productCode: string;
-  productName: string;
-  productImage: string;
-  templateStock: number;
-  currentStock: number;
-  suggestedQuantity: number;
-  shelfNum: number;
-  position: string;
-  unitPrice: number;
-}
-
-/** 按设备分组的补货建议 */
-export interface ReplenishmentSuggestion {
-  deviceId: string;
-  deviceName: string;
-  shopId: string;
-  shopName: string;
-  items: SuggestionItem[];
-}

+ 32 - 4
haha-admin-web/src/views/replenishment-order/components/CreateDialog.vue

@@ -1,8 +1,9 @@
 <script setup lang="ts">
-import { ref, reactive } from "vue";
+import { ref, reactive, computed } from "vue";
 import { message } from "@/utils/message";
 import { createOrder, getReplenishmentSuggestions } from "@/api/replenishmentOrder";
 import { getDeviceList } from "@/api/device";
+import { getDeviceInventoryStats } from "@/api/inventory";
 import type { ReplenishmentOrderItem } from "../utils/types";
 
 const emit = defineEmits<{
@@ -16,6 +17,14 @@ const suggestionsLoading = ref(false);
 // 设备列表
 const deviceList = ref<any[]>([]);
 const selectedDevices = ref<any[]>([]);
+const showLowStockOnly = ref(false);
+const lowStockDeviceIds = ref<Set<string>>(new Set());
+
+// 需要补货的设备(库存不足)
+const filteredDeviceList = computed(() => {
+  if (!showLowStockOnly.value) return deviceList.value;
+  return deviceList.value.filter((d: any) => lowStockDeviceIds.value.has(d.deviceId));
+});
 
 // 补货建议结果(按设备分组)
 const suggestions = ref<any[]>([]);
@@ -37,6 +46,7 @@ function resetState() {
   selectedDevices.value = [];
   suggestions.value = [];
   suggestionsLoading.value = false;
+  showLowStockOnly.value = false;
   commonForm.supplierName = "";
   commonForm.warehouseName = "";
   commonForm.expectedArrivalTime = "";
@@ -44,8 +54,19 @@ function resetState() {
 
 async function loadDevices() {
   try {
-    const { data } = await getDeviceList({ page: 1, pageSize: 1000 });
-    deviceList.value = data?.list || [];
+    const [deviceRes, statsRes] = await Promise.all([
+      getDeviceList({ page: 1, pageSize: 1000 }),
+      getDeviceInventoryStats({ page: 1, pageSize: 1000 })
+    ]);
+    deviceList.value = deviceRes.data?.list || [];
+
+    // 从库存统计中提取需要补货的设备ID
+    const stats = statsRes.data?.list || [];
+    lowStockDeviceIds.value = new Set(
+      stats
+        .filter((s: any) => s.stock_status === "缺货" || s.stock_status === "低库存")
+        .map((s: any) => s.device_id)
+    );
   } catch (e) {
     console.error("加载设备列表失败:", e);
   }
@@ -160,10 +181,17 @@ defineExpose({ open });
       <!-- 第一步:设备列表 -->
       <div style="margin-bottom:8px;font-size:14px;font-weight:600;color:#303133;">
         第一步:勾选需要补货的设备
+        <el-switch
+          v-model="showLowStockOnly"
+          active-text="仅显示需补货"
+          inactive-text="全部设备"
+          size="small"
+          style="margin-left:16px;"
+        />
       </div>
       <el-table
         ref="deviceTableRef"
-        :data="deviceList"
+        :data="filteredDeviceList"
         max-height="420"
         @selection-change="handleSelectionChange"
       >

+ 49 - 89
haha-admin-web/src/views/replenishment-order/index.vue

@@ -11,6 +11,7 @@ import Upload from "~icons/ep/upload";
 import Check from "~icons/ep/check";
 import Close from "~icons/ep/close";
 import View from "~icons/ep/view";
+import RefreshRight from "~icons/ep/refresh-right";
 import CreateDialog from "./components/CreateDialog.vue";
 
 defineOptions({
@@ -28,21 +29,28 @@ const {
   columns,
   dataList,
   pagination,
+  shopOptions,
+  selectedRows,
   onSearch,
   resetForm,
   handleDelete,
   handleSubmitToErp,
+  handleRetrySync,
   handleComplete,
   handleCancel,
   handleSizeChange,
-  handleCurrentChange
+  handleCurrentChange,
+  handleSelectionChange,
+  batchSubmitToErp,
+  batchCancel,
+  batchDelete
 } = useReplenishmentOrder();
 
 function handleCreate() {
   createDialogRef.value?.open();
 }
 
-function handleViewDetail(row) {
+function handleViewDetail(row: any) {
   router.push({
     path: "/inventory/replenishment-order/detail",
     query: { id: row.id }
@@ -63,47 +71,28 @@ function onCreated() {
       class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
     >
       <el-form-item label="补货单号:" prop="orderNo">
-        <el-input
-          v-model="form.orderNo"
-          placeholder="请输入补货单号"
-          clearable
-          class="w-[180px]!"
-        />
+        <el-input v-model="form.orderNo" placeholder="请输入补货单号" clearable class="w-[180px]!" />
+      </el-form-item>
+      <el-form-item label="门店:" prop="shopId">
+        <el-select v-model="form.shopId" placeholder="请选择门店" clearable class="w-[160px]!">
+          <el-option v-for="s in shopOptions" :key="s.id" :label="s.name" :value="s.id" />
+        </el-select>
       </el-form-item>
       <el-form-item label="设备ID:" prop="deviceId">
-        <el-input
-          v-model="form.deviceId"
-          placeholder="请输入设备ID"
-          clearable
-          class="w-[180px]!"
-        />
+        <el-input v-model="form.deviceId" placeholder="请输入设备ID" clearable class="w-[180px]!" />
       </el-form-item>
       <el-form-item label="状态:" prop="status">
-        <el-select
-          v-model="form.status"
-          placeholder="请选择"
-          clearable
-          class="w-[160px]!"
-        >
+        <el-select v-model="form.status" placeholder="请选择" clearable class="w-[140px]!">
           <el-option label="草稿" :value="0" />
-          <el-option label="已提交" :value="1" />
+          <el-option label="已提交ERP" :value="1" />
           <el-option label="已同步ERP" :value="2" />
           <el-option label="已完成" :value="3" />
           <el-option label="已取消" :value="4" />
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button
-          type="primary"
-          :icon="useRenderIcon(Search)"
-          :loading="loading"
-          @click="onSearch"
-        >
-          搜索
-        </el-button>
-        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
-          重置
-        </el-button>
+        <el-button type="primary" :icon="useRenderIcon(Search)" :loading="loading" @click="onSearch">搜索</el-button>
+        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">重置</el-button>
       </el-form-item>
     </el-form>
 
@@ -113,12 +102,28 @@ function onCreated() {
       @refresh="onSearch"
     >
       <template #buttons>
+        <el-button type="primary" @click="handleCreate">新建补货单</el-button>
+        <el-button
+          type="warning"
+          :disabled="!selectedRows.some((r: any) => r.status === 0)"
+          @click="batchSubmitToErp"
+        >
+          批量提交ERP
+        </el-button>
+        <el-button
+          type="danger"
+          :disabled="!selectedRows.some((r: any) => r.status === 0 || r.status === 1)"
+          @click="batchCancel"
+        >
+          批量取消
+        </el-button>
         <el-button
-          type="primary"
-          :icon="useRenderIcon('ep:plus')"
-          @click="handleCreate"
+          type="danger"
+          :disabled="!selectedRows.some((r: any) => r.status === 0)"
+          :icon="useRenderIcon(Delete)"
+          @click="batchDelete"
         >
-          新建补货单
+          批量删除
         </el-button>
       </template>
 
@@ -137,63 +142,18 @@ function onCreated() {
           :columns="dynamicColumns"
           :pagination="pagination"
           :paginationSmall="size === 'small'"
-          :header-cell-style="{
-            background: 'var(--el-fill-color-light)',
-            color: 'var(--el-text-color-primary)'
-          }"
+          :header-cell-style="{ background: 'var(--el-fill-color-light)', color: 'var(--el-text-color-primary)' }"
+          @selection-change="handleSelectionChange"
           @page-size-change="handleSizeChange"
           @page-current-change="handleCurrentChange"
         >
           <template #operation="{ row }">
-            <el-button
-              link
-              type="primary"
-              :size="size"
-              :icon="useRenderIcon(View)"
-              @click="handleViewDetail(row)"
-            >
-              详情
-            </el-button>
-            <el-button
-              v-if="row.status === 0"
-              link
-              type="warning"
-              :size="size"
-              :icon="useRenderIcon(Upload)"
-              @click="handleSubmitToErp(row)"
-            >
-              提交ERP
-            </el-button>
-            <el-button
-              v-if="row.status === 2"
-              link
-              type="success"
-              :size="size"
-              :icon="useRenderIcon(Check)"
-              @click="handleComplete(row)"
-            >
-              完成
-            </el-button>
-            <el-button
-              v-if="row.status === 0 || row.status === 1"
-              link
-              type="danger"
-              :size="size"
-              :icon="useRenderIcon(Close)"
-              @click="handleCancel(row)"
-            >
-              取消
-            </el-button>
-            <el-button
-              v-if="row.status === 0"
-              link
-              type="danger"
-              :size="size"
-              :icon="useRenderIcon(Delete)"
-              @click="handleDelete(row)"
-            >
-              删除
-            </el-button>
+            <el-button link type="primary" :size="size" :icon="useRenderIcon(View)" @click="handleViewDetail(row)">详情</el-button>
+            <el-button v-if="row.status === 0" link type="warning" :size="size" :icon="useRenderIcon(Upload)" @click="handleSubmitToErp(row)">提交ERP</el-button>
+            <el-button v-if="row.status === 1" link type="primary" :size="size" :icon="useRenderIcon(RefreshRight)" @click="handleRetrySync(row)">重试同步</el-button>
+            <el-button v-if="row.status === 2" link type="success" :size="size" :icon="useRenderIcon(Check)" @click="handleComplete(row)">完成</el-button>
+            <el-button v-if="row.status === 0 || row.status === 1" link type="danger" :size="size" :icon="useRenderIcon(Close)" @click="handleCancel(row)">取消</el-button>
+            <el-button v-if="row.status === 0" link type="danger" :size="size" :icon="useRenderIcon(Delete)" @click="handleDelete(row)">删除</el-button>
           </template>
         </pure-table>
       </template>

+ 125 - 114
haha-admin-web/src/views/replenishment-order/utils/hook.tsx

@@ -8,8 +8,10 @@ import {
   completeOrder,
   cancelOrder
 } from "@/api/replenishmentOrder";
+import { getShopList } from "@/api/shop";
 import type { PaginationProps } from "@pureadmin/table";
 import { onMounted, reactive, ref } from "vue";
+import { ElMessageBox } from "element-plus";
 
 export function useReplenishmentOrder() {
   const form = reactive<{
@@ -29,6 +31,8 @@ export function useReplenishmentOrder() {
   });
   const loading = ref(true);
   const dataList = ref([]);
+  const shopOptions = ref<any[]>([]);
+  const selectedRows = ref<any[]>([]);
   const pagination = reactive<PaginationProps>({
     total: 0,
     pageSize: 10,
@@ -38,94 +42,51 @@ export function useReplenishmentOrder() {
 
   const statusMap: Record<number, { text: string; type: string }> = {
     0: { text: "草稿", type: "info" },
-    1: { text: "已提交", type: "warning" },
+    1: { text: "已提交ERP", type: "warning" },
     2: { text: "已同步ERP", type: "primary" },
     3: { text: "已完成", type: "success" },
     4: { text: "已取消", type: "danger" }
   };
 
   const columns: TableColumnList = [
+    { type: "selection", width: 45, fixed: "left" },
+    { label: "补货单号", prop: "orderNo", minWidth: 160 },
+    { label: "门店名称", prop: "shopName", minWidth: 120, formatter: ({ shopName }: any) => shopName || "-" },
+    { label: "设备ID", prop: "deviceId", minWidth: 120 },
+    { label: "设备名称", prop: "deviceName", minWidth: 120, formatter: ({ deviceName }: any) => deviceName || "-" },
     {
-      label: "补货单号",
-      prop: "orderNo",
-      minWidth: 160
-    },
-    {
-      label: "设备ID",
-      prop: "deviceId",
-      minWidth: 120
-    },
-    {
-      label: "状态",
-      prop: "status",
-      minWidth: 110,
-      cellRenderer: ({ row }) => {
-        const item = statusMap[row.status] || { text: "未知", type: "info" };
-        return <el-tag type={item.type}>{item.text}</el-tag>;
+      label: "状态", prop: "status", minWidth: 110,
+      cellRenderer: ({ row }: any) => {
+        const s = statusMap[row.status] || { text: "未知", type: "info" };
+        return <el-tag type={s.type}>{s.text}</el-tag>;
       }
     },
+    { label: "总数量", prop: "totalQuantity", width: 90 },
     {
-      label: "总数量",
-      prop: "totalQuantity",
-      width: 90
+      label: "总金额", prop: "totalAmount", minWidth: 100,
+      formatter: ({ totalAmount }: any) => totalAmount != null ? `¥${Number(totalAmount).toFixed(2)}` : "-"
     },
+    { label: "供应商", prop: "supplierName", minWidth: 110, formatter: ({ supplierName }: any) => supplierName || "-" },
+    { label: "仓库", prop: "warehouseName", minWidth: 110, formatter: ({ warehouseName }: any) => warehouseName || "-" },
+    { label: "ERP单号", prop: "erpOrderNo", minWidth: 140, formatter: ({ erpOrderNo }: any) => erpOrderNo || "-" },
+    { label: "创建人", prop: "creatorName", minWidth: 100 },
     {
-      label: "总金额",
-      prop: "totalAmount",
-      minWidth: 100,
-      formatter: ({ totalAmount }) =>
-        totalAmount != null ? `¥${Number(totalAmount).toFixed(2)}` : "-"
+      label: "创建时间", prop: "createTime", minWidth: 160,
+      formatter: ({ createTime }: any) => createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : ""
     },
-    {
-      label: "供应商",
-      prop: "supplierName",
-      minWidth: 110,
-      formatter: ({ supplierName }) => supplierName || "-"
-    },
-    {
-      label: "仓库",
-      prop: "warehouseName",
-      minWidth: 110,
-      formatter: ({ warehouseName }) => warehouseName || "-"
-    },
-    {
-      label: "ERP单号",
-      prop: "erpOrderNo",
-      minWidth: 140,
-      formatter: ({ erpOrderNo }) => erpOrderNo || "-"
-    },
-    {
-      label: "创建人",
-      prop: "creatorName",
-      minWidth: 100
-    },
-    {
-      label: "创建时间",
-      prop: "createTime",
-      minWidth: 160,
-      formatter: ({ createTime }) =>
-        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : ""
-    },
-    {
-      label: "操作",
-      fixed: "right",
-      width: 280,
-      slot: "operation"
-    }
+    { label: "操作", fixed: "right", width: 300, slot: "operation" }
   ];
 
+  // ==================== 搜索 / 分页 ====================
+
   async function onSearch() {
     loading.value = true;
     try {
-      const params: any = {
-        page: pagination.currentPage,
-        pageSize: pagination.pageSize
-      };
+      const params: any = { page: pagination.currentPage, pageSize: pagination.pageSize };
       if (form.orderNo) params.orderNo = form.orderNo;
       if (form.deviceId) params.deviceId = form.deviceId;
       if (form.shopId) params.shopId = form.shopId;
-      if (form.status !== "" && form.status !== undefined)
-        params.status = form.status;
+      if (form.status !== "" && form.status !== undefined) params.status = form.status;
       if (form.startTime) params.startTime = form.startTime;
       if (form.endTime) params.endTime = form.endTime;
 
@@ -134,31 +95,44 @@ export function useReplenishmentOrder() {
         dataList.value = data.list || [];
         pagination.total = Number(data.total) || 0;
       }
+      selectedRows.value = [];
     } finally {
       loading.value = false;
     }
   }
 
-  function resetForm(formEl) {
+  function resetForm(formEl: any) {
     if (!formEl) return;
     formEl.resetFields();
+    (form as any).shopId = undefined;
     pagination.currentPage = 1;
     onSearch();
   }
 
-  async function handleDelete(row) {
+  function handleSizeChange(val: number) { pagination.pageSize = val; onSearch(); }
+  function handleCurrentChange(val: number) { pagination.currentPage = val; onSearch(); }
+
+  async function fetchShopOptions() {
+    try {
+      const { data } = await getShopList({ page: 1, pageSize: 1000 });
+      shopOptions.value = data?.list || [];
+    } catch (e) { console.error("加载门店失败:", e); }
+  }
+
+  function handleSelectionChange(selection: any[]) {
+    selectedRows.value = selection;
+  }
+
+  // ==================== 单行操作 ====================
+
+  async function handleDelete(row: any) {
     try {
       const { code } = await deleteOrder(row.id);
-      if (code === 200) {
-        message("删除成功", { type: "success" });
-        onSearch();
-      }
-    } catch (e) {
-      // error handled by interceptor
-    }
+      if (code === 200) { message("删除成功", { type: "success" }); onSearch(); }
+    } catch { /* handled */ }
   }
 
-  async function handleSubmitToErp(row) {
+  async function handleSubmitToErp(row: any) {
     try {
       await submitOrder(row.id);
       try {
@@ -168,63 +142,100 @@ export function useReplenishmentOrder() {
         message("已提交,但ERP同步失败", { type: "warning" });
       }
       onSearch();
-    } catch (e) {
-      // error handled by interceptor
-    }
+    } catch { /* handled */ }
+  }
+
+  async function handleRetrySync(row: any) {
+    try {
+      await syncToErp(row.id);
+      message("ERP同步成功", { type: "success" });
+      onSearch();
+    } catch { /* handled */ }
   }
 
-  async function handleComplete(row) {
+  async function handleComplete(row: any) {
     try {
       const { code } = await completeOrder(row.id);
-      if (code === 200) {
-        message("完成补货单", { type: "success" });
-        onSearch();
-      }
-    } catch (e) {
-      // error handled by interceptor
-    }
+      if (code === 200) { message("完成补货单", { type: "success" }); onSearch(); }
+    } catch { /* handled */ }
   }
 
-  async function handleCancel(row) {
+  async function handleCancel(row: any) {
     try {
       const { code } = await cancelOrder(row.id);
-      if (code === 200) {
-        message("取消成功", { type: "success" });
-        onSearch();
-      }
-    } catch (e) {
-      // error handled by interceptor
+      if (code === 200) { message("取消成功", { type: "success" }); onSearch(); }
+    } catch { /* handled */ }
+  }
+
+  // ==================== 批量操作 ====================
+
+  async function batchSubmitToErp() {
+    const drafts = selectedRows.value.filter((r: any) => r.status === 0);
+    if (drafts.length === 0) { message("请选择草稿状态的补货单", { type: "warning" }); return; }
+    try {
+      await ElMessageBox.confirm(`确认批量提交 ${drafts.length} 个补货单并同步ERP?`, "批量提交", {
+        confirmButtonText: "确定", cancelButtonText: "取消", type: "warning"
+      });
+    } catch { return; }
+
+    let ok = 0, fail = 0;
+    for (const row of drafts) {
+      try {
+        await submitOrder(row.id);
+        await syncToErp(row.id);
+        ok++;
+      } catch { fail++; }
     }
+    message(`批量提交完成:${ok} 成功, ${fail} 失败`, { type: fail > 0 ? "warning" : "success" });
+    onSearch();
   }
 
-  function handleSizeChange(val: number) {
-    pagination.pageSize = val;
+  async function batchCancel() {
+    const cancelable = selectedRows.value.filter((r: any) => r.status === 0 || r.status === 1);
+    if (cancelable.length === 0) { message("请选择草稿或已提交状态的补货单", { type: "warning" }); return; }
+    try {
+      await ElMessageBox.confirm(`确认批量取消 ${cancelable.length} 个补货单?`, "批量取消", {
+        confirmButtonText: "确定", cancelButtonText: "取消", type: "error"
+      });
+    } catch { return; }
+
+    let ok = 0, fail = 0;
+    for (const row of cancelable) {
+      try { await cancelOrder(row.id); ok++; } catch { fail++; }
+    }
+    message(`批量取消完成:${ok} 成功, ${fail} 失败`, { type: fail > 0 ? "warning" : "success" });
     onSearch();
   }
 
-  function handleCurrentChange(val: number) {
-    pagination.currentPage = val;
+  async function batchDelete() {
+    const drafts = selectedRows.value.filter((r: any) => r.status === 0);
+    if (drafts.length === 0) { message("只能删除草稿状态的补货单", { type: "warning" }); return; }
+    try {
+      await ElMessageBox.confirm(`确认删除 ${drafts.length} 个草稿补货单?删除后不可恢复。`, "批量删除", {
+        confirmButtonText: "确定", cancelButtonText: "取消", type: "error"
+      });
+    } catch { return; }
+
+    let ok = 0, fail = 0;
+    for (const row of drafts) {
+      try { await deleteOrder(row.id); ok++; } catch { fail++; }
+    }
+    message(`批量删除完成:${ok} 成功, ${fail} 失败`, { type: fail > 0 ? "warning" : "success" });
     onSearch();
   }
 
-  onMounted(() => {
+  // ==================== 初始化 ====================
+
+  onMounted(async () => {
+    await fetchShopOptions();
     onSearch();
   });
 
   return {
-    form,
-    loading,
-    columns,
-    dataList,
-    pagination,
-    statusMap,
-    onSearch,
-    resetForm,
-    handleDelete,
-    handleSubmitToErp,
-    handleComplete,
-    handleCancel,
-    handleSizeChange,
-    handleCurrentChange
+    form, loading, columns, dataList, pagination, shopOptions, selectedRows, statusMap,
+    onSearch, resetForm, handleSizeChange, handleCurrentChange,
+    handleDelete, handleSubmitToErp, handleRetrySync, handleComplete, handleCancel,
+    handleSelectionChange,
+    batchSubmitToErp, batchCancel, batchDelete
   };
 }

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

@@ -1,15 +1,22 @@
 package com.haha.admin.controller;
 
 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.exception.BusinessException;
+import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.Device;
 import com.haha.entity.DeviceInventory;
 import com.haha.entity.Replenisher;
+import com.haha.entity.ReplenishmentOrder;
+import com.haha.entity.ReplenishmentOrderItem;
 import com.haha.entity.dto.ReplenishDTO;
 import com.haha.service.DeviceInventoryService;
 import com.haha.service.DeviceService;
 import com.haha.service.ReplenisherService;
+import com.haha.service.ReplenishmentOrderService;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -34,6 +41,7 @@ public class ReplenisherOperationController {
     private final ReplenisherService replenisherService;
     private final DeviceInventoryService deviceInventoryService;
     private final DeviceService deviceService;
+    private final ReplenishmentOrderService replenishmentOrderService;
 
     /**
      * 获取当前补货员信息
@@ -204,6 +212,71 @@ public class ReplenisherOperationController {
         return Result.success(String.format("补货完成,成功%d项,失败%d项", successCount, dto.getItems().size() - successCount), result);
     }
 
+    /**
+     * 获取补货员关联的补货单列表
+     */
+    @GetMapping("/orders")
+    public Result<PageResult<ReplenishmentOrder>> getOrders(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "10") int pageSize,
+            @RequestParam(required = false) Integer status) {
+        Replenisher replenisher = getCurrentReplenisher();
+        List<String> deviceIds = replenisherService.getBoundDeviceIds(replenisher.getId());
+
+        if (deviceIds.isEmpty()) {
+            return Result.success("查询成功", new PageResult<>());
+        }
+
+        LambdaQueryWrapper<ReplenishmentOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.in(ReplenishmentOrder::getDeviceId, deviceIds);
+        if (status != null) {
+            wrapper.eq(ReplenishmentOrder::getStatus, status);
+        }
+        wrapper.orderByDesc(ReplenishmentOrder::getCreateTime);
+
+        Page<ReplenishmentOrder> pageObj = new Page<>(page, pageSize);
+        IPage<ReplenishmentOrder> result = replenishmentOrderService.page(pageObj, wrapper);
+
+        // 填充状态标签和设备名称
+        for (ReplenishmentOrder order : result.getRecords()) {
+            var statusLabel = com.haha.common.enums.ReplenishmentOrderStatusEnum.getLabelByCode(order.getStatus());
+            order.setStatusLabel(statusLabel.getLabel());
+            order.setStatusColor(statusLabel.getColor());
+            // 查设备名称
+            Device dev = deviceService.getDeviceBySn(order.getDeviceId());
+            if (dev != null) {
+                order.setDeviceName(dev.getName());
+            }
+        }
+
+        return Result.success("查询成功", PageResult.of(result));
+    }
+
+    /**
+     * 获取补货单详情(含商品明细和图片)
+     */
+    @GetMapping("/orders/{id}")
+    public Result<Map<String, Object>> getOrderDetail(@PathVariable Long id) {
+        Replenisher replenisher = getCurrentReplenisher();
+        ReplenishmentOrder order = replenishmentOrderService.getDetailWithItems(id);
+        if (order == null) {
+            return Result.error(404, "补货单不存在");
+        }
+
+        // 验证该补货单关联的设备是否绑定到当前补货员
+        List<String> deviceIds = replenisherService.getBoundDeviceIds(replenisher.getId());
+        if (!deviceIds.contains(order.getDeviceId())) {
+            return Result.error(403, "您无权查看此补货单");
+        }
+
+        List<ReplenishmentOrderItem> items = replenishmentOrderService.getItems(id);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("order", order);
+        result.put("items", items);
+        return Result.success("查询成功", result);
+    }
+
     /**
      * 获取当前登录的补货员
      */

+ 45 - 1
haha-service/src/main/java/com/haha/service/impl/ReplenishmentOrderServiceImpl.java

@@ -346,11 +346,55 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
             throw new BusinessException(400, "仅已同步ERP的补货单可完成");
         }
 
+        // 同步更新设备库存
+        List<ReplenishmentOrderItem> items = orderItemMapper.selectByOrderId(id);
+        for (ReplenishmentOrderItem item : items) {
+            if (item.getPlannedQuantity() == null || item.getPlannedQuantity() <= 0) continue;
+
+            // 查找或创建库存记录
+            LambdaQueryWrapper<DeviceInventory> wrapper = new LambdaQueryWrapper<DeviceInventory>()
+                    .eq(DeviceInventory::getDeviceId, order.getDeviceId())
+                    .eq(DeviceInventory::getProductCode, item.getProductCode());
+            DeviceInventory inv = deviceInventoryMapper.selectOne(wrapper);
+            if (inv == null) {
+                inv = new DeviceInventory();
+                inv.setDeviceId(order.getDeviceId());
+                inv.setProductId(item.getProductId());
+                inv.setProductCode(item.getProductCode());
+                inv.setProductName(item.getProductName());
+                inv.setStock(0);
+                inv.setShelfNum(item.getShelfNum());
+                inv.setPosition(item.getPosition());
+                inv.setWarningThreshold(5);
+                inv.setCreateTime(LocalDateTime.now());
+                inv.setUpdateTime(LocalDateTime.now());
+            }
+
+            int beforeStock = inv.getStock() != null ? inv.getStock() : 0;
+            int afterStock = beforeStock + item.getPlannedQuantity();
+            inv.setStock(afterStock);
+            inv.setLastRestockTime(LocalDateTime.now());
+            inv.setUpdateTime(LocalDateTime.now());
+            if (inv.getId() == null) {
+                deviceInventoryMapper.insert(inv);
+            } else {
+                deviceInventoryMapper.updateById(inv);
+            }
+
+            item.setBeforeStock(beforeStock);
+            item.setAfterStock(afterStock);
+            item.setActualQuantity(item.getPlannedQuantity());
+            orderItemMapper.updateById(item);
+
+            log.info("更新设备库存: deviceId={}, productCode={}, {} -> {}",
+                    order.getDeviceId(), item.getProductCode(), beforeStock, afterStock);
+        }
+
         order.setStatus(ReplenishmentOrderStatusEnum.COMPLETED.getCode());
         order.setUpdateTime(LocalDateTime.now());
         updateById(order);
 
-        log.info("完成补货单: id={}, orderNo={}", id, order.getOrderNo());
+        log.info("完成补货单: id={}, orderNo={}, items={}", id, order.getOrderNo(), items.size());
 
         fillStatusLabel(order);
         return order;