Просмотр исходного кода

feat: 补货管理页面UI重构 - 预警模式/阈值/拍照设置 + 新统计筛选 + 设备卡片优化 + 补货单详情分区排序

- index: 黄色导航栏、预警模式/阈值/拍照开关设置卡片、4列统计、搜索筛选、库存百分比色块卡片、全选+批量生成补货单
- order-detail: 蓝牙打印机提示、门店/任务编号信息、待下架/待补货商品分区、排序栏、底部三按钮
- api: 新增批量生成补货单接口
skyline 1 день назад
Родитель
Сommit
6bc8669a6a

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

@@ -97,3 +97,10 @@ export async function getReplenisherOrders(params: { page?: number; pageSize?: n
 export async function getReplenisherOrderDetail(id: string): Promise<any> {
   return get(`/replenisher/orders/${id}`);
 }
+
+/**
+ * 批量生成补货单
+ */
+export async function batchGenerateOrders(params: { deviceIds: string[] }): Promise<any> {
+  return post('/replenisher/orders/batch-generate', params);
+}

+ 435 - 190
haha-admin-mp/src/pages/replenish/index.vue

@@ -1,67 +1,99 @@
 <template>
   <view class="page">
-    <NavBar title="补货首页" />
+    <!-- 黄色导航栏 -->
+    <view class="yellow-header">
+      <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
+      <view class="header-content">
+        <text class="header-title">补货管理</text>
+        <view class="header-actions">
+          <view class="action-dot"></view>
+          <view class="action-dot"></view>
+          <view class="action-dot"></view>
+        </view>
+      </view>
+    </view>
+    <view class="header-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
 
-    <!-- 补货员信息 -->
-    <view class="user-card">
-      <view class="user-header">
-        <view class="avatar">
-          <text class="avatar-text">{{ (replenisherInfo.name || '补').charAt(0) }}</text>
+    <!-- 预警模式 -->
+    <view class="settings-card">
+      <view class="setting-row">
+        <view class="setting-label">
+          <text class="label-icon warning-icon">!</text>
+          <text class="label-text">预警模式</text>
+          <text class="label-tip">i</text>
         </view>
-        <view class="user-info">
-          <text class="user-name">{{ replenisherInfo.name || '补货员' }}</text>
-          <text class="user-role">补货员</text>
+      </view>
+      <view class="radio-row">
+        <view class="radio-item" :class="{ active: alertMode === 'standard' }" @click="alertMode = 'standard'">
+          <view class="radio-dot" :class="{ checked: alertMode === 'standard' }"></view>
+          <text>根据标准库存预警</text>
         </view>
-        <view class="user-meta" v-if="replenisherInfo.employeeId">
-          <text class="meta-label">工号</text>
-          <text class="meta-value">{{ replenisherInfo.employeeId }}</text>
+        <view class="radio-item" :class="{ active: alertMode === 'sales' }" @click="alertMode = 'sales'">
+          <view class="radio-dot" :class="{ checked: alertMode === 'sales' }"></view>
+          <text>根据销量预警</text>
         </view>
       </view>
-      <view class="user-stats">
-        <view class="stat-item">
-          <text class="stat-value">{{ deviceList.length }}</text>
-          <text class="stat-label">绑定设备</text>
+
+      <view class="setting-row threshold-row">
+        <view class="setting-label">
+          <text class="label-icon threshold-icon">%</text>
+          <text class="label-text">报警阈值(%)</text>
+          <text class="label-tip">i</text>
+        </view>
+        <view class="threshold-value" @click="editThreshold">
+          <text>{{ alertThreshold }}%</text>
+          <text class="edit-icon">✎</text>
         </view>
-        <view class="stat-item">
-          <text class="stat-value">{{ pendingOrderTotal }}</text>
-          <text class="stat-label">待处理补货单</text>
+      </view>
+
+      <view class="setting-row">
+        <view class="setting-label">
+          <text class="label-icon camera-icon">📷</text>
+          <text class="label-text">补货员上货后必须拍照</text>
         </view>
-        <view class="stat-item">
-          <text class="stat-value">{{ lowStockTotal }}</text>
-          <text class="stat-label">待补货商品</text>
+        <view class="toggle-switch" :class="{ on: requirePhoto }" @click="requirePhoto = !requirePhoto">
+          <view class="toggle-knob"></view>
         </view>
       </view>
     </view>
 
-    <!-- 快捷入口 -->
-    <view class="quick-row">
-      <view class="quick-btn orders" @click="goToOrders">
-        <view class="quick-icon doc-icon"></view>
-        <text class="quick-label">补货单</text>
+    <!-- 统计行 -->
+    <view class="stats-row">
+      <view class="stat-item" @click="activeFilter = 'all'">
+        <text class="stat-num" :class="{ highlight: activeFilter === 'all' }">{{ stats.totalDevices }}</text>
+        <text class="stat-label" :class="{ highlight: activeFilter === 'all' }">全部设备</text>
       </view>
-      <view class="quick-btn scan" @click="handleScanDevice">
-        <view class="quick-icon scan-icon-css"></view>
-        <text class="quick-label">扫码开柜</text>
+      <view class="stat-item" @click="activeFilter = 'pending-order'">
+        <text class="stat-num" :class="{ highlight: activeFilter === 'pending-order' }">{{ stats.pendingOrderDevices }}</text>
+        <text class="stat-label" :class="{ highlight: activeFilter === 'pending-order' }">待出补货单设备</text>
       </view>
-    </view>
-
-    <!-- 筛选标签 -->
-    <view class="filter-tabs">
-      <view class="filter-tab" :class="{ active: activeFilter === 'all' }" @click="activeFilter = 'all'">
-        <text>全部</text>
-        <text class="tab-count">{{ deviceList.length }}</text>
+      <view class="stat-item" @click="activeFilter = 'pending-task'">
+        <text class="stat-num" :class="{ highlight: activeFilter === 'pending-task' }">{{ stats.pendingTasks }}</text>
+        <text class="stat-label" :class="{ highlight: activeFilter === 'pending-task' }">待完成补货单</text>
       </view>
-      <view class="filter-tab" :class="{ active: activeFilter === 'pending' }" @click="activeFilter = 'pending'">
-        <text>有补货单</text>
-        <text class="tab-count">{{ pendingDeviceCount }}</text>
+      <view class="stat-item" @click="activeFilter = 'today'">
+        <text class="stat-num" :class="{ highlight: activeFilter === 'today' }">{{ stats.todayReplenished }}</text>
+        <text class="stat-label" :class="{ highlight: activeFilter === 'today' }">今日已补货(补货记录)</text>
       </view>
-      <view class="filter-tab" :class="{ active: activeFilter === 'submitted' }" @click="activeFilter = 'submitted'">
-        <text>待完成</text>
-        <text class="tab-count">{{ submittedDeviceCount }}</text>
+    </view>
+
+    <!-- 搜索与筛选 -->
+    <view class="search-bar">
+      <view class="search-input-wrap">
+        <text class="search-icon">🔍</text>
+        <input
+          class="search-input"
+          v-model="searchKeyword"
+          placeholder="请输入设备编号/名称"
+          placeholder-class="search-placeholder"
+          confirm-type="search"
+          @confirm="handleSearch"
+        />
       </view>
-      <view class="filter-tab" :class="{ active: activeFilter === 'today' }" @click="activeFilter = 'today'">
-        <text>今日已补</text>
-        <text class="tab-count">{{ todayDeviceCount }}</text>
+      <view class="search-btn" @click="handleSearch">搜索</view>
+      <view class="filter-btn" @click="showFilter = !showFilter">
+        <text>筛选</text>
+        <text class="filter-icon">▽</text>
       </view>
     </view>
 
@@ -72,187 +104,351 @@
 
     <!-- 设备列表 -->
     <view class="device-section" v-else>
-      <view class="section-header">
-        <text class="section-title">我的设备</text>
-        <text class="section-count" v-if="filteredDevices.length">共 {{ filteredDevices.length }} 台</text>
-      </view>
-
       <view class="empty-state" v-if="filteredDevices.length === 0">
-        <view class="empty-icon"><view class="icon-device-empty"></view></view>
         <text class="empty-text">{{ activeFilter === 'all' ? '暂无绑定的设备' : '没有匹配的设备' }}</text>
         <text class="empty-hint">{{ activeFilter === 'all' ? '请联系管理员绑定设备后开始补货' : '当前筛选条件下没有设备' }}</text>
       </view>
 
-      <view class="device-card" v-for="(device, index) in filteredDevices" :key="index" @click="goToOperation(device)">
-        <view class="card-head">
-          <view class="device-info">
-            <text class="device-name">{{ device.name || device.deviceId }}</text>
-            <text class="device-sn">{{ device.deviceId }}</text>
+      <view
+        class="device-card"
+        v-for="(device, index) in filteredDevices"
+        :key="device.deviceId || index"
+        @click="goToOperation(device)"
+      >
+        <view class="card-top">
+          <text class="device-name">{{ device.name || device.deviceId }}</text>
+          <view class="order-tag" v-if="device.replenishOrderGenerated">补货单已生成</view>
+        </view>
+
+        <view class="card-body">
+          <view class="inventory-badge" :class="getInventoryLevel(device.inventoryPercent)">
+            <text class="badge-label">库存</text>
+            <text class="badge-value">{{ device.inventoryPercent || 0 }}%</text>
           </view>
-          <view class="status-tag" :class="device.status === 1 ? 'online' : 'offline'">
-            <view class="status-dot"></view>
-            <text>{{ device.statusLabel || (device.status === 1 ? '在线' : '离线') }}</text>
+          <view class="metrics-row">
+            <view class="metric-item">
+              <text class="metric-value">{{ device.standardStock || 0 }}</text>
+              <text class="metric-label">标准库存</text>
+            </view>
+            <view class="metric-item">
+              <text class="metric-value">{{ device.remainingStock || 0 }}</text>
+              <text class="metric-label">剩余库存</text>
+            </view>
+            <view class="metric-item">
+              <text class="metric-value">{{ device.dailySales || 0 }}</text>
+              <text class="metric-label">日均销量</text>
+              <text class="info-tip">i</text>
+            </view>
+            <view class="metric-item">
+              <text class="metric-value">{{ device.salesRatio || 0 }}</text>
+              <text class="metric-label">销容比</text>
+              <text class="info-tip">i</text>
+            </view>
           </view>
+          <text class="card-arrow">></text>
         </view>
 
-        <view class="device-addr" v-if="device.address">
-          <text>{{ device.address }}</text>
+        <!-- 勾选框 -->
+        <view class="card-checkbox" :class="{ checked: device.selected }" @click.stop="toggleSelect(device)">
+          <text v-if="device.selected" class="check-mark">✓</text>
         </view>
+      </view>
+    </view>
 
-        <view class="stock-row">
-          <view class="stock-chip" v-if="device.pendingOrderCount > 0">
-            <text class="chip-label">待补货单</text>
-            <text class="chip-value accent">{{ device.pendingOrderCount }}</text>
-          </view>
-          <view class="stock-chip">
-            <text class="chip-label">种类</text>
-            <text class="chip-value">{{ device.productCount || 0 }}</text>
-          </view>
-          <view class="stock-chip">
-            <text class="chip-label">总库存</text>
-            <text class="chip-value">{{ device.totalStock || 0 }}</text>
-          </view>
-          <view class="stock-chip warn" v-if="device.lowStockCount > 0">
-            <text class="chip-label">低库存</text>
-            <text class="chip-value">{{ device.lowStockCount }}</text>
-          </view>
-          <view class="stock-chip danger" v-if="device.zeroStockCount > 0">
-            <text class="chip-label">缺货</text>
-            <text class="chip-value">{{ device.zeroStockCount }}</text>
+    <!-- 底部批量操作栏 -->
+    <view class="bottom-bar" v-if="filteredDevices.length > 0">
+      <view class="bottom-left">
+        <view class="select-all-btn" @click="toggleSelectAll">
+          <view class="checkbox-icon" :class="{ checked: allSelected }">
+            <text v-if="allSelected" class="check-mark">✓</text>
           </view>
+          <text>全选</text>
         </view>
+        <text class="selected-count">已选({{ selectedCount }})</text>
+      </view>
+      <view class="batch-btn" @click="handleBatchGenerate">
+        <text>批量生成补货单</text>
+      </view>
+    </view>
 
-        <view class="card-foot">
-          <text class="foot-text">查看商品库存</text>
-          <text class="foot-arrow">></text>
+    <!-- 阈值编辑弹窗 -->
+    <view class="modal-mask" v-if="showThresholdModal" @click="showThresholdModal = false">
+      <view class="modal-content" @click.stop>
+        <text class="modal-title">设置报警阈值</text>
+        <view class="modal-input-wrap">
+          <input
+            class="modal-input"
+            type="number"
+            v-model="tempThreshold"
+            placeholder="请输入百分比"
+          />
+          <text class="modal-suffix">%</text>
+        </view>
+        <view class="modal-actions">
+          <view class="modal-btn cancel" @click="showThresholdModal = false">取消</view>
+          <view class="modal-btn confirm" @click="confirmThreshold">确定</view>
         </view>
       </view>
     </view>
+
+    <CustomTabBar />
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, computed } from 'vue';
+import { ref, computed, onMounted } from 'vue';
 import { onShow } from '@dcloudio/uni-app';
-import NavBar from '@/components/NavBar.vue';
-import { getReplenisherInfo } from '@/utils/auth';
-import { getMyInfo, getDeviceList } from '@/api/replenish';
+import CustomTabBar from '@/components/CustomTabBar.vue';
+import { getDeviceList, batchGenerateOrders } from '@/api/replenish';
+
+const statusBarHeight = ref(0);
+
+// 设置
+const alertMode = ref<'standard' | 'sales'>('standard');
+const alertThreshold = ref(80);
+const requirePhoto = ref(false);
+const showThresholdModal = ref(false);
+const tempThreshold = ref(80);
+
+// 搜索筛选
+const searchKeyword = ref('');
+const showFilter = ref(false);
+const activeFilter = ref('all');
 
-const replenisherInfo = ref<any>({});
+// 数据
 const deviceList = ref<any[]>([]);
 const loading = ref(true);
-const activeFilter = ref('all');
-const pendingOrderTotal = ref(0);
-const lowStockTotal = ref(0);
+
+// 统计
+const stats = ref({
+  totalDevices: 0,
+  pendingOrderDevices: 0,
+  pendingTasks: 0,
+  todayReplenished: 0
+});
 
 const filteredDevices = computed(() => {
+  let list = deviceList.value;
+  // 关键词筛选
+  if (searchKeyword.value) {
+    const kw = searchKeyword.value.toLowerCase();
+    list = list.filter(d =>
+      (d.deviceId || '').toLowerCase().includes(kw) ||
+      (d.name || '').toLowerCase().includes(kw)
+    );
+  }
+  // 标签筛选
   switch (activeFilter.value) {
-    case 'pending': return deviceList.value.filter(d => (d.pendingOrderCount || 0) > 0);
-    case 'submitted': return deviceList.value.filter(d => (d.submittedOrderCount || 0) > 0);
-    case 'today': return deviceList.value.filter(d => d.todayReplenished);
-    default: return deviceList.value;
+    case 'pending-order':
+      return list.filter(d => !d.replenishOrderGenerated);
+    case 'pending-task':
+      return list.filter(d => d.replenishOrderGenerated && !d.todayReplenished);
+    case 'today':
+      return list.filter(d => d.todayReplenished);
+    default:
+      return list;
   }
 });
 
-const pendingDeviceCount = computed(() => deviceList.value.filter(d => (d.pendingOrderCount || 0) > 0).length);
-const submittedDeviceCount = computed(() => deviceList.value.filter(d => (d.submittedOrderCount || 0) > 0).length);
-const todayDeviceCount = computed(() => deviceList.value.filter(d => d.todayReplenished).length);
+const selectedCount = computed(() => deviceList.value.filter(d => d.selected).length);
+const allSelected = computed(() => filteredDevices.value.length > 0 && filteredDevices.value.every(d => d.selected));
 
-const goToOrders = () => uni.navigateTo({ url: '/pages/replenish/orders' });
+const getInventoryLevel = (percent: number): string => {
+  if (percent <= 30) return 'danger';
+  if (percent <= 60) return 'warning';
+  return 'normal';
+};
+
+const editThreshold = () => {
+  tempThreshold.value = alertThreshold.value;
+  showThresholdModal.value = true;
+};
+
+const confirmThreshold = () => {
+  const val = parseInt(tempThreshold.value);
+  if (val >= 0 && val <= 100) {
+    alertThreshold.value = val;
+  }
+  showThresholdModal.value = false;
+};
+
+const toggleSelect = (device: any) => {
+  device.selected = !device.selected;
+};
+
+const toggleSelectAll = () => {
+  const shouldSelect = !allSelected.value;
+  filteredDevices.value.forEach(d => { d.selected = shouldSelect; });
+};
+
+const handleSearch = () => {
+  // 触发筛选
+};
 
 const goToOperation = (device: any) => {
   uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${device.deviceId}` });
 };
 
-const handleScanDevice = () => {
-  uni.scanCode({
-    onlyFromCamera: false,
-    scanType: ['qrCode'],
-    success: (res: any) => {
-      const match = (res.result || '').match(/\/([^/]+)$/);
-      if (!match) { uni.showToast({ title: '无效的设备二维码', icon: 'none' }); return; }
-      const id = match[1];
-      if (!deviceList.value.find((d: any) => d.deviceId === id)) {
-        uni.showToast({ title: '该设备未绑定,请联系管理员', icon: 'none' });
-        return;
-      }
-      uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${id}` });
-    }
-  });
+const handleBatchGenerate = async () => {
+  const selected = deviceList.value.filter(d => d.selected);
+  if (selected.length === 0) {
+    uni.showToast({ title: '请先选择设备', icon: 'none' });
+    return;
+  }
+  try {
+    const ids = selected.map(d => d.deviceId);
+    await batchGenerateOrders({ deviceIds: ids });
+    uni.showToast({ title: `已为${selected.length}台设备生成补货单`, icon: 'success' });
+    // 刷新数据
+    await loadData();
+  } catch (e: any) {
+    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
+  }
 };
 
-onShow(async () => {
+const loadData = async () => {
   loading.value = true;
   try {
-    const info = getReplenisherInfo();
-    if (info?.id) replenisherInfo.value = info;
-    const detail = await getMyInfo();
-    if (detail) replenisherInfo.value = detail;
-
     const devices = await getDeviceList();
-    deviceList.value = devices || [];
-    lowStockTotal.value = (devices || []).reduce((s: number, d: any) => s + (d.lowStockCount || 0) + (d.zeroStockCount || 0), 0);
-    pendingOrderTotal.value = (devices || []).reduce((s: number, d: any) => s + (d.pendingOrderCount || 0), 0);
+    deviceList.value = (devices || []).map(d => ({ ...d, selected: false }));
+    stats.value = {
+      totalDevices: deviceList.value.length,
+      pendingOrderDevices: deviceList.value.filter(d => !d.replenishOrderGenerated).length,
+      pendingTasks: deviceList.value.filter(d => d.replenishOrderGenerated && !d.todayReplenished).length,
+      todayReplenished: deviceList.value.filter(d => d.todayReplenished).length
+    };
   } catch (e: any) {
     uni.showToast({ title: e.message || '加载失败', icon: 'none' });
   } finally {
     loading.value = false;
   }
+};
+
+onMounted(() => {
+  const systemInfo = uni.getSystemInfoSync();
+  statusBarHeight.value = systemInfo.statusBarHeight || 20;
+});
+
+onShow(async () => {
+  await loadData();
 });
 </script>
 
 <style lang="scss" scoped>
-.page { min-height: 100vh; background: $bg-color-page; padding-bottom: 48rpx; }
+.page { min-height: 100vh; background: $bg-color-page; padding-bottom: 160rpx; }
+
+// ====== 黄色导航栏 ======
+.yellow-header {
+  position: fixed; top: 0; left: 0; right: 0; z-index: 999;
+  background: $primary-color;
+  padding-top: constant(safe-area-inset-top);
+  padding-top: env(safe-area-inset-top);
+}
+.header-content {
+  display: flex; align-items: center; justify-content: space-between;
+  height: 44px; padding: 0 16rpx;
+}
+.header-title { font-size: 34rpx; font-weight: 700; color: #1e293b; }
+.header-actions { display: flex; gap: 6rpx; padding: 8rpx;
+  .action-dot { width: 6rpx; height: 6rpx; border-radius: 50%; background: #1e293b; }
+}
+.header-placeholder { width: 100%; }
 
-// ====== User card ======
-.user-card { 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); }
-.user-header { display: flex; align-items: center; margin-bottom: $spacing-3;
-  .avatar { width: 72rpx; height: 72rpx; background: $primary-color-bg; border-radius: $radius-lg; display: flex; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0;
-    .avatar-text { font-size: 32rpx; font-weight: 700; color: $primary-color-dark; }
+// ====== 设置卡片 ======
+.settings-card {
+  margin: 16rpx 24rpx; padding: 24rpx; background: $bg-color-card;
+  border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+.setting-row {
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 12rpx 0;
+}
+.setting-label {
+  display: flex; align-items: center; gap: 8rpx;
+  .label-icon {
+    width: 32rpx; height: 32rpx; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-size: 18rpx; font-weight: 700; color: #fff;
+  }
+  .warning-icon { background: $warning-color; }
+  .threshold-icon { background: $primary-color; font-size: 16rpx; }
+  .camera-icon { background: $primary-color; font-size: 16rpx; }
+  .label-text { font-size: $font-size-base; font-weight: 600; color: $text-color-primary; }
+  .label-tip {
+    width: 28rpx; height: 28rpx; border-radius: 50%;
+    background: $warning-color-bg; color: $warning-color;
+    font-size: 16rpx; font-weight: 700; font-style: italic;
+    display: flex; align-items: center; justify-content: center;
   }
-  .user-info { flex: 1;
-    .user-name { display: block; font-size: $font-size-lg; font-weight: 600; color: $text-color-primary; }
-    .user-role { font-size: $font-size-sm; color: $primary-color; font-weight: 500; }
+}
+.radio-row { display: flex; gap: 32rpx; padding: 8rpx 0 16rpx;
+  .radio-item { display: flex; align-items: center; gap: 8rpx; font-size: $font-size-sm; color: $text-color-secondary;
+    .radio-dot { width: 28rpx; height: 28rpx; border-radius: 50%; border: 3rpx solid $border-color;
+      &.checked { border-color: $primary-color; background: $primary-color;
+        &::after { content: ''; display: block; width: 10rpx; height: 10rpx; border-radius: 50%; background: #fff; margin: 6rpx auto; }
+      }
+    }
+    &.active { color: $text-color-primary; font-weight: 500; }
   }
-  .user-meta { text-align: right;
-    .meta-label { display: block; font-size: 20rpx; color: $text-color-muted; }
-    .meta-value { font-size: 22rpx; color: $text-color-secondary; }
+}
+.threshold-row { border-top: 1rpx solid $border-color-light;
+  .threshold-value { display: flex; align-items: center; gap: 8rpx; font-size: $font-size-lg; font-weight: 700; color: $text-color-primary;
+    .edit-icon { font-size: 24rpx; color: $primary-color; }
   }
 }
-.user-stats { display: flex; background: $bg-color-page; border-radius: $radius-sm; padding: 16rpx 0;
-  .stat-item { flex: 1; display: flex; flex-direction: column; align-items: center;
-    .stat-value { font-size: 30rpx; font-weight: 700; color: $text-color-primary; }
-    .stat-label { font-size: 22rpx; color: $text-color-muted; margin-top: 4rpx; }
+.toggle-switch {
+  width: 80rpx; height: 44rpx; border-radius: 22rpx;
+  background: $border-color; position: relative; transition: background 0.3s;
+  .toggle-knob {
+    width: 38rpx; height: 38rpx; border-radius: 50%; background: #fff;
+    position: absolute; top: 3rpx; left: 3rpx; transition: left 0.3s;
+    box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.15);
+  }
+  &.on { background: $primary-color;
+    .toggle-knob { left: 39rpx; }
   }
 }
 
-// ====== Filter tabs ======
-.filter-tabs { display: flex; gap: 8rpx; margin: 0 24rpx 16rpx; }
-.filter-tab { display: flex; align-items: center; gap: 6rpx; padding: 12rpx 20rpx; background: $bg-color-card; border-radius: $radius-full; font-size: 24rpx; color: $text-color-secondary; border: 1rpx solid transparent;
-  .tab-count { font-size: 20rpx; color: $text-color-muted; }
-  &.active { background: $primary-color-bg; border-color: $primary-color; color: $primary-color-dark; font-weight: 500;
-    .tab-count { color: $primary-color; }
+// ====== 统计行 ======
+.stats-row {
+  display: flex; padding: 16rpx 24rpx; gap: 0;
+  .stat-item { flex: 1; text-align: center; padding: 12rpx 0;
+    .stat-num { display: block; font-size: 40rpx; font-weight: 800; color: $text-color-primary; line-height: 1.2;
+      &.highlight { color: $primary-color-dark; }
+    }
+    .stat-label { display: block; font-size: 20rpx; color: $text-color-muted; margin-top: 4rpx; line-height: 1.3;
+      &.highlight { color: $primary-color; font-weight: 600; }
+    }
   }
 }
 
-// ====== Quick actions ======
-.quick-row { display: flex; gap: 16rpx; margin: 0 24rpx 16rpx; }
-.quick-btn { display: flex; align-items: center; gap: 12rpx; flex: 1; padding: 20rpx 24rpx; border-radius: $radius-base; background: $bg-color-card; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
-  .quick-label { font-size: $font-size-base; font-weight: 500; color: $text-color-primary; }
-  &:active { opacity: 0.7; }
+// ====== 搜索栏 ======
+.search-bar {
+  display: flex; align-items: center; gap: 12rpx; padding: 0 24rpx; margin-bottom: 16rpx;
+}
+.search-input-wrap {
+  flex: 1; display: flex; align-items: center; background: $bg-color-card;
+  border-radius: $radius-base; padding: 0 16rpx; height: 72rpx;
+  border: 1rpx solid $border-color;
+  .search-icon { font-size: 28rpx; margin-right: 8rpx; }
+  .search-input { flex: 1; font-size: $font-size-base; color: $text-color-primary; }
 }
-.quick-icon { width: 40rpx; height: 40rpx; border-radius: $radius-sm; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
-.doc-icon { position: relative;
-  &::before { content: ''; position: absolute; inset: 4rpx; border: 2.5rpx solid $primary-color; border-radius: 4rpx; }
-  &::after { content: ''; position: absolute; top: 16rpx; left: 12rpx; right: 12rpx; height: 2.5rpx; background: $primary-color-dark; border-radius: 1rpx; box-shadow: 0 8rpx 0 $primary-color-dark; }
+.search-placeholder { color: $text-color-placeholder; }
+.search-btn {
+  padding: 0 28rpx; height: 72rpx; line-height: 72rpx;
+  background: $primary-color; border-radius: $radius-base;
+  font-size: $font-size-base; font-weight: 600; color: #1e293b;
+  &:active { opacity: 0.8; }
 }
-.scan-icon-css { background: $primary-color; position: relative;
-  &::before, &::after { content: ''; position: absolute; width: 12rpx; height: 12rpx; border: 3rpx solid #fff; border-radius: 3rpx; }
-  &::before { top: 8rpx; left: 8rpx; }
-  &::after { bottom: 8rpx; right: 8rpx; }
+.filter-btn {
+  display: flex; align-items: center; gap: 4rpx; padding: 0 20rpx; height: 72rpx; line-height: 72rpx;
+  background: $bg-color-card; border-radius: $radius-base; border: 1rpx solid $border-color;
+  font-size: $font-size-sm; color: $text-color-secondary;
+  .filter-icon { font-size: 18rpx; }
+  &:active { opacity: 0.7; }
 }
 
-// ====== Loading ======
+// ====== 加载 ======
 .load-tip { display: flex; justify-content: center; padding: 100rpx 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;
@@ -260,48 +456,97 @@ onShow(async () => {
 }
 @keyframes pulse { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
 
-// ====== Device ======
+// ====== 设备列表 ======
 .device-section { padding: 0 24rpx; }
-.section-header { display: flex; align-items: center; justify-content: space-between; padding: $spacing-2 0;
-  .section-title { font-size: $font-size-md; font-weight: 600; color: $text-color-primary; }
-  .section-count { font-size: $font-size-sm; color: $text-color-muted; }
-}
 
 .empty-state { display: flex; flex-direction: column; align-items: center; padding: 120rpx 0;
-  .empty-icon { width: 120rpx; height: 120rpx; background: $bg-color-secondary; border-radius: $radius-xl; display: flex; align-items: center; justify-content: center; margin-bottom: $spacing-3; }
-  .icon-device-empty { width: 44rpx; height: 36rpx; border: 3rpx solid $text-color-placeholder; border-radius: 6rpx; position: relative;
-    &::after { content: ''; position: absolute; bottom: -8rpx; left: 50%; transform: translateX(-50%); width: 22rpx; height: 6rpx; background: $text-color-placeholder; border-radius: 0 0 4rpx 4rpx; }
-  }
   .empty-text { font-size: $font-size-base; color: $text-color-secondary; margin-bottom: $spacing-1; }
   .empty-hint { font-size: $font-size-sm; color: $text-color-muted; }
 }
 
-.device-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);
+.device-card {
+  position: relative;
+  margin-bottom: 16rpx; padding: 24rpx; padding-right: 72rpx;
+  background: $bg-color-card; border-radius: $radius-md;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
   &:active { opacity: 0.7; }
 }
-.card-head { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 12rpx;
-  .device-info { flex: 1;
-    .device-name { display: block; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; margin-bottom: 4rpx; }
-    .device-sn { font-size: 22rpx; color: $text-color-muted; }
+.card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx;
+  .device-name { font-size: $font-size-base; font-weight: 600; color: $text-color-primary; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+  .order-tag { padding: 4rpx 16rpx; background: $primary-color; border-radius: 6rpx; font-size: 22rpx; font-weight: 600; color: #1e293b; flex-shrink: 0; margin-left: 12rpx; }
+}
+.card-body { display: flex; align-items: flex-start; gap: 16rpx;
+  .inventory-badge {
+    width: 100rpx; padding: 12rpx 0; border-radius: $radius-sm; text-align: center; flex-shrink: 0;
+    .badge-label { display: block; font-size: 20rpx; color: #fff; }
+    .badge-value { display: block; font-size: 32rpx; font-weight: 800; color: #fff; }
+    &.danger { background: $error-color; }
+    &.warning { background: $accent-color; }
+    &.normal { background: $success-color; }
   }
-  .status-tag { display: flex; align-items: center; gap: 6rpx; padding: 6rpx 14rpx; border-radius: $radius-full; font-size: 22rpx; font-weight: 500; flex-shrink: 0;
-    .status-dot { width: 10rpx; height: 10rpx; border-radius: 50%; }
-    &.online { background: $success-color-bg; color: $success-color; .status-dot { background: $success-color; } }
-    &.offline { background: $bg-color-secondary; color: $text-color-muted; .status-dot { background: $text-color-placeholder; } }
+  .metrics-row { flex: 1; display: flex; flex-wrap: wrap;
+    .metric-item { width: 50%; margin-bottom: 8rpx;
+      .metric-value { font-size: $font-size-base; font-weight: 700; color: $text-color-primary; }
+      .metric-label { font-size: 20rpx; color: $text-color-muted; margin-left: 4rpx; }
+      .info-tip { font-size: 14rpx; color: $warning-color; font-style: italic; margin-left: 2rpx; }
+    }
   }
+  .card-arrow { position: absolute; right: 72rpx; top: 50%; transform: translateY(-50%); font-size: 28rpx; color: $text-color-placeholder; }
 }
-.device-addr { margin-bottom: 12rpx; text { font-size: $font-size-sm; color: $text-color-muted; } }
-.stock-row { display: flex; gap: 12rpx; margin-bottom: 16rpx;
-  .stock-chip { flex: 1; text-align: center; padding: 12rpx 8rpx; background: $bg-color-page; border-radius: $radius-sm;
-    .chip-label { display: block; font-size: 20rpx; color: $text-color-muted; margin-bottom: 4rpx; }
-    .chip-value { font-size: $font-size-base; font-weight: 700; color: $text-color-primary; }
-    &.warn { background: $warning-color-bg; .chip-value { color: $warning-color; } }
-    &.danger { background: $error-color-bg; .chip-value { color: $error-color; } }
-    .chip-value.accent { color: $accent-color; }
+.card-checkbox {
+  position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%);
+  width: 40rpx; height: 40rpx; border-radius: 8rpx; border: 3rpx solid $border-color;
+  display: flex; align-items: center; justify-content: center;
+  &.checked { background: $primary-color; border-color: $primary-color;
+    .check-mark { color: #fff; font-size: 24rpx; font-weight: 700; }
   }
 }
-.card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 12rpx; border-top: 1rpx solid $border-color-light;
-  .foot-text { font-size: $font-size-sm; color: $primary-color; font-weight: 500; }
-  .foot-arrow { font-size: $font-size-sm; color: $text-color-placeholder; }
+
+// ====== 底部批量操作栏 ======
+.bottom-bar {
+  position: fixed; bottom: 0; left: 0; right: 0;
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
+  background: $bg-color-card; border-top: 1rpx solid $border-color-light;
+  z-index: 100;
+}
+.bottom-left { display: flex; align-items: center; gap: 16rpx;
+  .select-all-btn { display: flex; align-items: center; gap: 8rpx; font-size: $font-size-sm; color: $text-color-secondary;
+    .checkbox-icon { width: 36rpx; height: 36rpx; border-radius: 8rpx; border: 3rpx solid $border-color;
+      display: flex; align-items: center; justify-content: center;
+      &.checked { background: $primary-color; border-color: $primary-color;
+        .check-mark { color: #fff; font-size: 22rpx; font-weight: 700; }
+      }
+    }
+  }
+  .selected-count { font-size: $font-size-sm; color: $text-color-muted; }
+}
+.batch-btn {
+  padding: 16rpx 32rpx; background: $primary-color; border-radius: $radius-base;
+  font-size: $font-size-base; font-weight: 600; color: #1e293b;
+  &:active { opacity: 0.8; }
+}
+
+// ====== 阈值弹窗 ======
+.modal-mask {
+  position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 9999;
+  display: flex; align-items: center; justify-content: center;
+}
+.modal-content {
+  width: 560rpx; background: $bg-color-card; border-radius: $radius-lg; padding: 32rpx;
+}
+.modal-title { display: block; font-size: $font-size-lg; font-weight: 700; color: $text-color-primary; text-align: center; margin-bottom: 24rpx; }
+.modal-input-wrap {
+  display: flex; align-items: center; background: $bg-color-page; border-radius: $radius-base;
+  padding: 0 20rpx; height: 80rpx; border: 1rpx solid $border-color;
+  .modal-input { flex: 1; font-size: $font-size-lg; color: $text-color-primary; text-align: center; }
+  .modal-suffix { font-size: $font-size-lg; color: $text-color-muted; margin-left: 8rpx; }
+}
+.modal-actions { display: flex; gap: 16rpx; margin-top: 24rpx;
+  .modal-btn { flex: 1; height: 72rpx; line-height: 72rpx; text-align: center; border-radius: $radius-base; font-size: $font-size-base; font-weight: 600;
+    &.cancel { background: $bg-color-page; color: $text-color-secondary; }
+    &.confirm { background: $primary-color; color: #1e293b; }
+    &:active { opacity: 0.8; }
+  }
 }
 </style>

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

@@ -1,6 +1,20 @@
 <template>
   <view class="page">
-    <NavBar title="补货单详情" />
+    <!-- 黄色导航栏 -->
+    <view class="yellow-header">
+      <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
+      <view class="header-content">
+        <view class="back-btn" @click="goBack">
+          <view class="back-arrow"></view>
+        </view>
+        <text class="header-title">补货单详情</text>
+        <view class="header-actions">
+          <view class="action-dot"></view>
+          <view class="action-dot"></view>
+        </view>
+      </view>
+    </view>
+    <view class="header-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
 
     <view v-if="loading" class="loading-container">
       <view class="loading-spinner"></view>
@@ -8,111 +22,118 @@
     </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 class="printer-bar">
+        <text class="printer-text">暂未连接蓝牙打印机</text>
+        <text class="printer-link" @click="goPrinterConfig">去配置</text>
+      </view>
+
+      <!-- 订单头部信息 -->
+      <view class="order-header-card">
+        <view class="order-title-row">
+          <text class="order-title">{{ order.deviceName || order.deviceId }}</text>
+          <text class="order-status" :class="statusClass(order.status)">{{ statusLabel(order.status) }}</text>
+        </view>
+        <view class="order-info-row">
+          <text class="info-label">门店名称:</text>
+          <text class="info-value">{{ order.shopName || order.deviceName || '-' }}</text>
+        </view>
+        <view class="order-info-row">
+          <text class="info-label">任务编号:</text>
+          <text class="info-value">{{ order.taskNo || order.orderNo || '-' }}</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 class="order-info-row">
+          <text class="info-label">创建时间:</text>
+          <text class="info-value">{{ order.createTime || '-' }}</text>
+        </view>
+        <view class="export-btn" @click="handleExport">
+          <text>导出/打印</text>
+        </view>
+      </view>
+
+      <!-- 待下架商品 -->
+      <view class="section-block">
+        <view class="section-title-row">
+          <text class="section-title">待下架商品</text>
+          <text class="section-count">(共{{ pendingRemovalItems.length }}种商品)</text>
+        </view>
+        <view class="empty-tip" v-if="pendingRemovalItems.length === 0">
+          <text>暂无待下架商品</text>
+        </view>
+        <view
+          class="product-row"
+          v-for="(item, index) in pendingRemovalItems"
+          :key="'remove-' + index"
+        >
+          <image v-if="item.productImage" :src="item.productImage" class="product-img" mode="aspectFill" />
+          <view v-else class="product-img-placeholder">
+            <text class="placeholder-text">无图</text>
           </view>
-          <view class="info-item">
-            <text class="info-label">供应商</text>
-            <text class="info-value">{{ order.supplierName || '-' }}</text>
+          <view class="product-info">
+            <text class="product-name">{{ item.productName || '未知商品' }}</text>
           </view>
-          <view class="info-item">
-            <text class="info-label">仓库</text>
-            <text class="info-value">{{ order.warehouseName || '-' }}</text>
+          <text class="product-qty">{{ item.plannedQuantity || item.quantity || 0 }}</text>
+        </view>
+      </view>
+
+      <!-- 待补货商品 -->
+      <view class="section-block">
+        <view class="section-title-row">
+          <view class="section-title-left">
+            <text class="section-title">待补货商品</text>
+            <text class="section-count">(共{{ replenishItems.length }}个商品)</text>
+          </view>
+          <view class="show-zero-toggle" @click="showZeroQty = !showZeroQty">
+            <text class="toggle-label">展示待补货数量为0商品</text>
+            <view class="toggle-switch" :class="{ on: showZeroQty }">
+              <view class="toggle-knob"></view>
+            </view>
           </view>
-          <view class="info-item">
-            <text class="info-label">总金额</text>
-            <text class="info-value price">¥{{ (order.totalAmount || 0).toFixed(2) }}</text>
+        </view>
+
+        <!-- 排序栏 -->
+        <view class="sort-bar">
+          <view class="sort-item" :class="{ active: sortField === 'name' }" @click="sortField = 'name'">
+            <text>商品名称</text>
+            <text class="sort-icon">{{ sortField === 'name' ? (sortAsc ? '↑' : '↓') : '' }}</text>
           </view>
-          <view class="info-item" v-if="order.expectedArrivalTime">
-            <text class="info-label">预计到货</text>
-            <text class="info-value">{{ order.expectedArrivalTime }}</text>
+          <view class="sort-item" :class="{ active: sortField === 'shelfNum' }" @click="sortField = 'shelfNum'">
+            <text>层数</text>
+            <text class="sort-icon">{{ sortField === 'shelfNum' ? (sortAsc ? '↑' : '↓') : '⇅' }}</text>
           </view>
-          <view class="info-item">
-            <text class="info-label">创建时间</text>
-            <text class="info-value">{{ order.createTime }}</text>
+          <view class="sort-item" :class="{ active: sortField === 'quantity' }" @click="sortField = 'quantity'">
+            <text>待补货数量</text>
+            <text class="sort-icon">{{ sortField === 'quantity' ? (sortAsc ? '↑' : '↓') : '⇅' }}</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"
+          class="product-row"
+          v-for="(item, index) in sortedReplenishItems"
+          :key="'replenish-' + 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>
+          <image v-if="item.productImage" :src="item.productImage" class="product-img" mode="aspectFill" />
+          <view v-else class="product-img-placeholder">
+            <text class="placeholder-text">无图</text>
           </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 class="product-info">
+            <text class="product-name">{{ item.productName || '未知商品' }}</text>
           </view>
+          <text class="product-qty">{{ item.plannedQuantity || item.quantity || 0 }}</text>
         </view>
       </view>
     </template>
 
-    <!-- 执行补货按钮 -->
-    <view class="bottom-bar" v-if="order && canExecute(order)">
-      <view class="action-btn" @click="goReplenish">
-        <text>按补货单执行补货</text>
+    <!-- 底部操作栏 -->
+    <view class="bottom-bar" v-if="order && !loading">
+      <view class="bottom-btn secondary" @click="handleRemoteOpen">
+        <text>远程开门补货</text>
+      </view>
+      <view class="bottom-btn secondary" @click="handleCancel">
+        <text>撤销</text>
+      </view>
+      <view class="bottom-btn primary" @click="handleEdit">
+        <text>修改</text>
       </view>
     </view>
 
@@ -121,30 +142,104 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
-import NavBar from '@/components/NavBar.vue';
+import { ref, computed, onMounted } from 'vue';
 import CustomTabBar from '@/components/CustomTabBar.vue';
 import { getReplenisherOrderDetail } from '@/api/replenish';
 
+const statusBarHeight = ref(0);
 const loading = ref(true);
 const order = ref<any>(null);
 const items = ref<any[]>([]);
+const showZeroQty = ref(false);
+const sortField = ref('name');
+const sortAsc = ref(true);
 
 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'
+  0: '草稿', 1: '待补货', 2: '已同步', 3: '已完成', 4: '已取消'
 };
 
-function statusText(status: number): string { return statusMap[status] || '未知'; }
-function statusColor(status: number): string { return statusColorMap[status] || ''; }
+function statusLabel(status: number): string { return statusMap[status] || '未知'; }
+function statusClass(status: number): string {
+  const map: Record<number, string> = { 0: 'draft', 1: 'pending', 2: 'synced', 3: 'done', 4: 'cancelled' };
+  return map[status] || '';
+}
+
+// 待下架商品(数量为0或标记为下架的商品)
+const pendingRemovalItems = computed(() => {
+  return items.value.filter((item: any) => {
+    const qty = item.plannedQuantity || item.quantity || 0;
+    return item.isRemoval || qty <= 0;
+  });
+});
+
+// 待补货商品
+const replenishItems = computed(() => {
+  let list = items.value.filter((item: any) => {
+    const qty = item.plannedQuantity || item.quantity || 0;
+    return !item.isRemoval && qty > 0;
+  });
+  if (!showZeroQty.value) {
+    list = list.filter((item: any) => (item.plannedQuantity || item.quantity || 0) > 0);
+  }
+  return list;
+});
+
+// 排序
+const sortedReplenishItems = computed(() => {
+  const list = [...replenishItems.value];
+  const dir = sortAsc.value ? 1 : -1;
+  list.sort((a, b) => {
+    if (sortField.value === 'name') {
+      return dir * ((a.productName || '').localeCompare(b.productName || ''));
+    }
+    if (sortField.value === 'shelfNum') {
+      return dir * ((a.shelfNum || 0) - (b.shelfNum || 0));
+    }
+    if (sortField.value === 'quantity') {
+      return dir * ((a.plannedQuantity || a.quantity || 0) - (b.plannedQuantity || b.quantity || 0));
+    }
+    return 0;
+  });
+  return list;
+});
+
+function goBack() {
+  const pages = getCurrentPages();
+  if (pages.length > 1) {
+    uni.navigateBack();
+  } else {
+    uni.switchTab({ url: '/pages/replenish/index' });
+  }
+}
+
+function goPrinterConfig() {
+  uni.showToast({ title: '蓝牙打印机配置功能开发中', icon: 'none' });
+}
+
+function handleExport() {
+  uni.showToast({ title: '导出/打印功能开发中', icon: 'none' });
+}
+
+function handleRemoteOpen() {
+  if (!order.value) return;
+  uni.navigateTo({
+    url: `/pages/replenish/operation?deviceId=${order.value.deviceId}&orderId=${order.value.id}`
+  });
+}
 
-function canExecute(o: any): boolean {
-  return o.status === 0 || o.status === 1;
+function handleCancel() {
+  uni.showModal({
+    title: '确认撤销',
+    content: '确定要撤销此补货单吗?',
+    success: (res) => {
+      if (res.confirm) {
+        uni.showToast({ title: '撤销功能开发中', icon: 'none' });
+      }
+    }
+  });
 }
 
-function goReplenish() {
+function handleEdit() {
   if (!order.value) return;
   const orderData = {
     orderId: order.value.id,
@@ -152,7 +247,7 @@ function goReplenish() {
     items: items.value.map((item: any) => ({
       productCode: item.productCode,
       productName: item.productName,
-      plannedQuantity: item.plannedQuantity || 0,
+      plannedQuantity: item.plannedQuantity || item.quantity || 0,
       shelfNum: item.shelfNum,
       position: item.position
     }))
@@ -164,6 +259,9 @@ function goReplenish() {
 }
 
 onMounted(async () => {
+  const systemInfo = uni.getSystemInfoSync();
+  statusBarHeight.value = systemInfo.statusBarHeight || 20;
+
   try {
     const pages = getCurrentPages();
     const currentPage = pages[pages.length - 1] as any;
@@ -189,197 +287,171 @@ onMounted(async () => {
 .page {
   min-height: 100vh;
   background: $bg-color-page;
-  padding-bottom: 160rpx;
+  padding-bottom: 180rpx;
 }
 
-.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;
-  }
+// ====== 黄色导航栏 ======
+.yellow-header {
+  position: fixed; top: 0; left: 0; right: 0; z-index: 999;
+  background: $primary-color;
+  padding-top: constant(safe-area-inset-top);
+  padding-top: env(safe-area-inset-top);
 }
+.header-content {
+  display: flex; align-items: center; justify-content: space-between;
+  height: 44px; padding: 0 16rpx;
+}
+.back-btn {
+  width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center;
+  &:active { opacity: 0.7; }
+}
+.back-arrow {
+  width: 20rpx; height: 20rpx;
+  border-left: 4rpx solid #1e293b; border-bottom: 4rpx solid #1e293b;
+  transform: rotate(45deg); margin-left: 8rpx;
+}
+.header-title { font-size: 34rpx; font-weight: 700; color: #1e293b; }
+.header-actions { display: flex; gap: 6rpx; padding: 8rpx;
+  .action-dot { width: 6rpx; height: 6rpx; border-radius: 50%; background: #1e293b; }
+}
+.header-placeholder { width: 100%; }
 
-/* 订单头 */
-.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; }
-      }
-    }
-  }
+// ====== 加载 ======
+.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; }
 }
 
-/* 商品明细 */
-.items-section {
-  padding: 0 24rpx;
+// ====== 蓝牙打印机提示 ======
+.printer-bar {
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 16rpx 24rpx; background: #fffbe6; border-bottom: 1rpx solid $border-color-light;
+  .printer-text { font-size: 24rpx; color: $warning-color; }
+  .printer-link { font-size: 24rpx; color: $primary-color-dark; font-weight: 600; }
+}
 
-  .section-title {
-    font-size: 28rpx;
-    font-weight: 700;
-    color: $text-color-primary;
-    padding: 16rpx 0;
+// ====== 订单头部卡片 ======
+.order-header-card {
+  margin: 16rpx 24rpx; padding: 24rpx;
+  background: $bg-color-card; border-radius: $radius-md;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+  position: relative;
+}
+.order-title-row {
+  display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx;
+  .order-title { font-size: 32rpx; font-weight: 700; color: $text-color-primary; }
+  .order-status {
+    padding: 4rpx 16rpx; border-radius: 6rpx; font-size: 22rpx; font-weight: 600;
+    &.draft { background: #f0f0f0; color: #999; }
+    &.pending { background: #fff3e0; color: #ff9800; }
+    &.synced { background: #e3f2fd; color: #2196f3; }
+    &.done { background: #e8f5e9; color: #4caf50; }
+    &.cancelled { background: #ffebee; color: #f44336; }
   }
 }
+.order-info-row {
+  padding: 6rpx 0;
+  .info-label { font-size: 24rpx; color: $text-color-tertiary; }
+  .info-value { font-size: 24rpx; color: $text-color-primary; font-weight: 500; }
+}
+.export-btn {
+  position: absolute; right: 24rpx; bottom: 24rpx;
+  padding: 10rpx 24rpx; background: $primary-color; border-radius: $radius-base;
+  font-size: 22rpx; font-weight: 600; color: #1e293b;
+  &:active { opacity: 0.8; }
+}
 
-.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;
-      }
-    }
+// ====== 区块 ======
+.section-block {
+  padding: 0 24rpx; margin-top: 16rpx;
+}
+.section-title-row {
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 16rpx 0;
+  .section-title { font-size: 28rpx; font-weight: 700; color: $text-color-primary; }
+  .section-count { font-size: 24rpx; color: $text-color-muted; }
+}
+.section-title-left { display: flex; align-items: baseline; gap: 8rpx; }
 
-    .item-info {
-      flex: 1;
+.empty-tip {
+  text-align: center; padding: 40rpx 0;
+  text { font-size: 26rpx; color: $text-color-muted; }
+}
 
-      .item-code {
-        display: block;
-        font-size: 22rpx;
-        color: $text-color-muted;
-      }
+// ====== 商品行 ======
+.product-row {
+  display: flex; align-items: center; padding: 16rpx 0;
+  border-bottom: 1rpx solid $border-color-light;
+  &:last-child { border-bottom: none; }
+}
+.product-img {
+  width: 80rpx; height: 80rpx; border-radius: 10rpx;
+  background: $bg-color-page; margin-right: 16rpx; flex-shrink: 0;
+}
+.product-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; }
+}
+.product-info { flex: 1;
+  .product-name { font-size: 26rpx; color: $text-color-primary; line-height: 1.4; }
+}
+.product-qty {
+  font-size: 30rpx; font-weight: 700; color: $text-color-primary;
+  margin-left: 16rpx; flex-shrink: 0; min-width: 48rpx; text-align: right;
+}
 
-      .item-name {
-        display: block;
-        font-size: 28rpx;
-        font-weight: 600;
-        color: $text-color-primary;
-        margin: 4rpx 0;
-      }
+// ====== 排序栏 ======
+.sort-bar {
+  display: flex; background: $bg-color-card; border-radius: $radius-sm;
+  border: 1rpx solid $border-color; overflow: hidden; margin-bottom: 8rpx;
+}
+.sort-item {
+  flex: 1; display: flex; align-items: center; justify-content: center; gap: 4rpx;
+  padding: 14rpx 0; font-size: 22rpx; color: $text-color-tertiary;
+  border-right: 1rpx solid $border-color-light;
+  &:last-child { border-right: none; }
+  &.active { color: $primary-color-dark; font-weight: 600; }
+  .sort-icon { font-size: 18rpx; }
+  &:active { background: $bg-color-page; }
+}
 
-      .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;
-        }
-      }
-    }
+// ====== 展示0商品开关 ======
+.show-zero-toggle {
+  display: flex; align-items: center; gap: 8rpx;
+  .toggle-label { font-size: 20rpx; color: $text-color-muted; }
+}
+.toggle-switch {
+  width: 64rpx; height: 36rpx; border-radius: 18rpx;
+  background: $border-color; position: relative; transition: background 0.3s;
+  .toggle-knob {
+    width: 30rpx; height: 30rpx; border-radius: 50%; background: #fff;
+    position: absolute; top: 3rpx; left: 3rpx; transition: left 0.3s;
+    box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.15);
   }
-
-  .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; }
-      }
-    }
+  &.on { background: $primary-color;
+    .toggle-knob { left: 31rpx; }
   }
 }
 
-// ====== Bottom bar ======
-.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: $spacing-3 $spacing-4; padding-bottom: calc($spacing-3 + env(safe-area-inset-bottom)); background: $bg-color-card; border-top: 1rpx solid $border-color-light; }
-.action-btn { display: flex; align-items: center; justify-content: center; width: 100%; padding: 26rpx 0; border-radius: $radius-xl; background: $primary-color;
-  text { font-size: $font-size-md; font-weight: 600; color: $text-color-primary; }
+// ====== 底部操作栏 ======
+.bottom-bar {
+  position: fixed; bottom: 0; left: 0; right: 0;
+  display: flex; align-items: center; gap: 16rpx;
+  padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
+  background: $bg-color-card; border-top: 1rpx solid $border-color-light;
+  z-index: 100;
+}
+.bottom-btn {
+  flex: 1; height: 80rpx; line-height: 80rpx; text-align: center;
+  border-radius: $radius-base; font-size: $font-size-base; font-weight: 600;
   &:active { opacity: 0.8; }
+  &.secondary {
+    background: $bg-color-secondary; color: $text-color-secondary;
+  }
+  &.primary {
+    background: $primary-color; color: #1e293b;
+  }
 }
 </style>