Jelajahi Sumber

商户端小程序订单列表、订单详情页、设备管理,商品模版重构

skyline 1 bulan lalu
induk
melakukan
adeb5fb4fd
28 mengubah file dengan 2654 tambahan dan 1363 penghapusan
  1. 3 3
      haha-admin-mp/src/api/customer.ts
  2. 19 4
      haha-admin-mp/src/api/device.ts
  3. 5 4
      haha-admin-mp/src/api/order.ts
  4. 1 1
      haha-admin-mp/src/api/product.ts
  5. 2 2
      haha-admin-mp/src/api/shop.ts
  6. 6 6
      haha-admin-mp/src/mock/device.ts
  7. 2 2
      haha-admin-mp/src/mock/shop.ts
  8. 2 2
      haha-admin-mp/src/pages.json
  9. 1 1
      haha-admin-mp/src/pages/customer/detail.vue
  10. 2 2
      haha-admin-mp/src/pages/device/detail.vue
  11. 694 246
      haha-admin-mp/src/pages/device/list.vue
  12. 1 1
      haha-admin-mp/src/pages/login/login.vue
  13. 367 338
      haha-admin-mp/src/pages/orders/detail.vue
  14. 432 357
      haha-admin-mp/src/pages/orders/list.vue
  15. 742 334
      haha-admin-mp/src/pages/products/list.vue
  16. 2 2
      haha-admin-mp/src/pages/shop/detail.vue
  17. 1 1
      haha-admin-mp/src/pages/shop/list.vue
  18. 5 3
      haha-admin-mp/src/utils/common.ts
  19. 32 0
      haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java
  20. 11 5
      haha-admin/src/main/java/com/haha/admin/controller/OrderController.java
  21. 7 0
      haha-admin/src/main/java/com/haha/admin/dto/OrderQueryDTO.java
  22. 6 0
      haha-common/src/main/java/com/haha/common/vo/OrderVO.java
  23. 66 0
      haha-mapper/src/main/java/com/haha/mapper/OrderMapper.java
  24. 17 0
      haha-service/src/main/java/com/haha/service/LayerTemplateService.java
  25. 6 2
      haha-service/src/main/java/com/haha/service/OrderService.java
  26. 1 1
      haha-service/src/main/java/com/haha/service/impl/DeviceAlertServiceImpl.java
  27. 145 0
      haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java
  28. 76 46
      haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

+ 3 - 3
haha-admin-mp/src/api/customer.ts

@@ -30,7 +30,7 @@ export async function getCustomerList(params: CustomerQueryParams = {}): Promise
 /**
  * 获取客户详情
  */
-export async function getCustomerDetail(id: number): Promise<any> {
+export async function getCustomerDetail(id: string): Promise<any> {
   return get(`/users/${id}`);
 }
 
@@ -44,13 +44,13 @@ export async function getCustomerStatistics(): Promise<any> {
 /**
  * 更新客户状态
  */
-export async function updateCustomerStatus(id: number, status: number): Promise<any> {
+export async function updateCustomerStatus(id: string, status: number): Promise<any> {
   return put(`/users/${id}/status`, { status });
 }
 
 /**
  * 更新客户信用分
  */
-export async function updateCustomerCredit(id: number, creditScore: number): Promise<any> {
+export async function updateCustomerCredit(id: string, creditScore: number): Promise<any> {
   return put(`/users/${id}/credit`, { creditScore });
 }

+ 19 - 4
haha-admin-mp/src/api/device.ts

@@ -11,6 +11,7 @@ export interface DeviceQueryParams {
   shopId?: number;
   status?: number;
   storeName?: string;
+  keyword?: string;
 }
 
 export interface DeviceListResponse {
@@ -30,7 +31,7 @@ export async function getDeviceList(params: DeviceQueryParams = {}): Promise<Dev
 /**
  * 获取设备详情
  */
-export async function getDeviceDetail(deviceId: number): Promise<any> {
+export async function getDeviceDetail(deviceId: string): Promise<any> {
   return get(`/devices/${deviceId}`);
 }
 
@@ -44,24 +45,38 @@ export async function getDeviceStats(): Promise<any> {
 /**
  * 远程开门
  */
-export async function openDeviceDoor(deviceId: number, doorIndex?: string): Promise<void> {
+export async function openDeviceDoor(deviceId: string, doorIndex?: string): Promise<void> {
   await post(`/devices/${deviceId}/open`, { doorIndex: doorIndex || 'A' });
 }
 
 /**
  * 设置设备温度
  */
-export async function setDeviceTemperature(deviceId: number, temperature: number): Promise<void> {
+export async function setDeviceTemperature(deviceId: string, temperature: number): Promise<void> {
   await put(`/devices/${deviceId}/temperature`, { temperature });
 }
 
 /**
  * 设置设备音量
  */
-export async function setDeviceVolume(deviceId: number, volume: number): Promise<void> {
+export async function setDeviceVolume(deviceId: string, volume: number): Promise<void> {
   await put(`/devices/${deviceId}/volume`, { volume });
 }
 
+/**
+ * 获取设备商品配置
+ */
+export async function getDeviceProductConfig(deviceId: string): Promise<any> {
+  return get(`/devices/${deviceId}/product-config`);
+}
+
+/**
+ * 保存设备商品配置
+ */
+export async function saveDeviceProductConfig(deviceId: string, data: any): Promise<any> {
+  return post(`/devices/${deviceId}/product-config`, data);
+}
+
 /**
  * 获取设备门记录
  */

+ 5 - 4
haha-admin-mp/src/api/order.ts

@@ -11,6 +11,7 @@ export interface OrderQueryParams {
   deviceId?: string;
   payStatus?: string;
   status?: number;
+  statusList?: number[];
   startDate?: string;
   endDate?: string;
 }
@@ -36,20 +37,20 @@ export async function getOrderList(params: OrderQueryParams = {}): Promise<Order
 /**
  * 获取订单详情
  */
-export async function getOrderDetail(orderId: number): Promise<any> {
+export async function getOrderDetail(orderId: string): Promise<any> {
   return get(`/order/${orderId}`);
 }
 
 /**
  * 获取订单统计
  */
-export async function getOrderStats(): Promise<any> {
-  return get('/order/statistics');
+export async function getOrderStats(params?: { startDate?: string; endDate?: string }): Promise<any> {
+  return get('/order/statistics', params || {});
 }
 
 /**
  * 处理退款
  */
-export async function handleRefund(orderId: number, reason: string): Promise<void> {
+export async function handleRefund(orderId: string, reason: string): Promise<void> {
   await post(`/order/${orderId}/refund`, { reason });
 }

+ 1 - 1
haha-admin-mp/src/api/product.ts

@@ -30,7 +30,7 @@ export async function getProductList(params: ProductQueryParams = {}): Promise<P
 /**
  * 获取商品详情
  */
-export async function getProductDetail(productId: number): Promise<any> {
+export async function getProductDetail(productId: string): Promise<any> {
   return get(`/products/${productId}`);
 }
 

+ 2 - 2
haha-admin-mp/src/api/shop.ts

@@ -29,7 +29,7 @@ export async function getShopList(params: ShopQueryParams = {}): Promise<ShopLis
 /**
  * 获取门店详情
  */
-export async function getShopDetail(shopId: number): Promise<any> {
+export async function getShopDetail(shopId: string): Promise<any> {
   return get(`/shops/${shopId}`);
 }
 
@@ -50,6 +50,6 @@ export async function getEnabledShops(): Promise<any[]> {
 /**
  * 获取门店关联设备
  */
-export async function getShopDevices(shopId: number): Promise<any[]> {
+export async function getShopDevices(shopId: string): Promise<any[]> {
   return get(`/shops/${shopId}/devices`);
 }

+ 6 - 6
haha-admin-mp/src/mock/device.ts

@@ -30,7 +30,7 @@ export const DeviceStatusColor: Record<number, string> = {
 export const mockDeviceList = [
   {
     id: 1,
-    deviceNo: 'DEV001',
+    deviceId: 'DEV001',
     name: '1号柜机',
     shopId: 1,
     shopName: '万达广场店',
@@ -46,7 +46,7 @@ export const mockDeviceList = [
   },
   {
     id: 2,
-    deviceNo: 'DEV002',
+    deviceId: 'DEV002',
     name: '2号柜机',
     shopId: 2,
     shopName: '人民广场店',
@@ -62,7 +62,7 @@ export const mockDeviceList = [
   },
   {
     id: 3,
-    deviceNo: 'DEV003',
+    deviceId: 'DEV003',
     name: '3号柜机',
     shopId: 3,
     shopName: '火车站店',
@@ -78,7 +78,7 @@ export const mockDeviceList = [
   },
   {
     id: 4,
-    deviceNo: 'DEV004',
+    deviceId: 'DEV004',
     name: '4号柜机',
     shopId: 4,
     shopName: '科技园店',
@@ -94,7 +94,7 @@ export const mockDeviceList = [
   },
   {
     id: 5,
-    deviceNo: 'DEV005',
+    deviceId: 'DEV005',
     name: '5号柜机',
     shopId: 1,
     shopName: '万达广场店',
@@ -113,7 +113,7 @@ export const mockDeviceList = [
 // 设备详情Mock数据
 export const mockDeviceDetail = {
   id: 1,
-  deviceNo: 'DEV001',
+  deviceId: 'DEV001',
   name: '1号柜机',
   shopId: 1,
   shopName: '万达广场店',

+ 2 - 2
haha-admin-mp/src/mock/shop.ts

@@ -106,7 +106,7 @@ export const mockShopDetail = {
   devices: [
     {
       id: 1,
-      deviceNo: 'DEV001',
+      deviceId: 'DEV001',
       name: '1号柜机',
       status: 1,
       todaySales: 680,
@@ -114,7 +114,7 @@ export const mockShopDetail = {
     },
     {
       id: 5,
-      deviceNo: 'DEV005',
+      deviceId: 'DEV005',
       name: '5号柜机',
       status: 1,
       todaySales: 580,

+ 2 - 2
haha-admin-mp/src/pages.json

@@ -75,8 +75,8 @@
 		{
 			"path": "pages/products/list",
 			"style": {
-				"navigationBarTitleText": "商品管理",
-				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTitleText": "配置上架商品",
+				"navigationBarBackgroundColor": "#FFD93D",
 				"navigationBarTextStyle": "black",
 				"navigationStyle": "custom"
 			}

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

@@ -155,7 +155,7 @@ const loadCustomerDetail = async () => {
 
   loading.value = true;
   try {
-    const res = await getCustomerDetail(Number(customerId));
+    const res = await getCustomerDetail(customerId);
     customer.value = res || {};
   } catch (error) {
     console.error('加载客户详情失败', error);

+ 2 - 2
haha-admin-mp/src/pages/device/detail.vue

@@ -13,7 +13,7 @@
             <text>{{ getStatusText(device.status) }}</text>
           </view>
         </view>
-        <text class="device-no">{{ device.deviceNo }}</text>
+        <text class="device-no">{{ device.deviceId }}</text>
       </view>
       
       <!-- 基本信息 -->
@@ -223,7 +223,7 @@ const loadDetail = async () => {
   }
 
   try {
-    device.value = await getDeviceDetail(Number(id));
+    device.value = await getDeviceDetail(id);
     loadDoorRecords(id);
   } catch (error) {
     showToast('加载设备详情失败');

+ 694 - 246
haha-admin-mp/src/pages/device/list.vue

@@ -1,112 +1,197 @@
 <template>
   <view class="page">
-    <!-- 导航栏 -->
-    <NavBar title="设备管理" />
-    
-    <!-- 统计卡片 -->
-    <view class="stats-section">
-      <view class="stats-grid">
-        <view class="stat-card main">
-          <text class="stat-value">{{ deviceStats.totalCount }}</text>
-          <text class="stat-label">设备总数</text>
+    <!-- 黄色头部区域 -->
+    <view class="header-section" :class="{ 'has-shadow': isScrolled }">
+      <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
+      <view class="header-title">
+        <text class="title-text">设备</text>
+      </view>
+
+      <!-- Tab 切换 -->
+      <view class="tab-bar">
+        <view
+          class="tab-item"
+          :class="{ active: activeTab === 'all' }"
+          @click="switchTab('all')"
+        >
+          <text class="tab-text">全部设备</text>
+          <view class="tab-indicator" v-if="activeTab === 'all'"></view>
+        </view>
+        <view
+          class="tab-item"
+          :class="{ active: activeTab === 'online' }"
+          @click="switchTab('online')"
+        >
+          <text class="tab-text">在线设备</text>
+          <view class="tab-indicator" v-if="activeTab === 'online'"></view>
         </view>
-        <view class="stat-card online">
-          <view class="stat-dot green"></view>
-          <text class="stat-value">{{ deviceStats.onlineCount }}</text>
-          <text class="stat-label">在线</text>
+        <view
+          class="tab-item"
+          :class="{ active: activeTab === 'offline' }"
+          @click="switchTab('offline')"
+        >
+          <text class="tab-text">离线设备</text>
+          <view class="tab-indicator" v-if="activeTab === 'offline'"></view>
         </view>
-        <view class="stat-card offline">
-          <view class="stat-dot amber"></view>
-          <text class="stat-value">{{ deviceStats.offlineCount }}</text>
-          <text class="stat-label">离线</text>
+      </view>
+    </view>
+
+    <!-- 搜索区域 -->
+    <view class="search-section">
+      <view class="search-box">
+        <view class="search-icon">
+          <view class="search-icon-circle"></view>
+          <view class="search-icon-line"></view>
         </view>
+        <input
+          class="search-input"
+          v-model="keyword"
+          placeholder="请输入设备编号、设备名称、门店名称"
+          placeholder-class="search-placeholder"
+          confirm-type="search"
+          @confirm="handleSearch"
+        />
+      </view>
+      <view class="search-btn" @click="handleSearch">
+        <text class="search-btn-text">搜索</text>
       </view>
     </view>
-    
+
+    <!-- 统计与操作栏 -->
+    <view class="stats-bar">
+      <text class="stats-total">共{{ totalCount }}条</text>
+      <view class="stats-refresh" @click="refreshNetwork">
+        <text class="refresh-text">如设备离线,可以点此尝试</text>
+        <text class="refresh-link">刷新网络</text>
+      </view>
+      <view class="stats-filter" @click="showFilter">
+        <text class="filter-text">筛选</text>
+        <view class="filter-icon"></view>
+      </view>
+    </view>
+
     <!-- 设备列表 -->
-    <scroll-view 
-      class="device-scroll" 
-      scroll-y 
+    <scroll-view
+      class="device-scroll"
+      scroll-y
       @scrolltolower="loadMore"
+      @scroll="onScroll"
+      refresher-enabled
+      :refresher-triggered="refreshing"
+      @refresherrefresh="onRefresh"
     >
       <view class="device-list">
-        <view 
-          class="device-card" 
-          v-for="device in deviceList" 
+        <view
+          class="device-card"
+          v-for="device in deviceList"
           :key="device.id"
           @click="goDetail(device.id)"
         >
-          <!-- 第一行:名称 + 编号 + 状态 -->
-          <view class="card-row-main">
-            <view class="device-title">
-              <text class="device-name">{{ device.name }}</text>
-              <text class="device-no">{{ device.deviceNo }}</text>
+          <!-- 设备头部:状态 + 名称 -->
+          <view class="card-header">
+            <view class="status-badge" :class="getStatusClass(device.status)">
+              <view class="wifi-icon" v-if="device.status === 1">
+                <view class="wifi-arc wifi-arc-1"></view>
+                <view class="wifi-arc wifi-arc-2"></view>
+                <view class="wifi-arc wifi-arc-3"></view>
+                <view class="wifi-dot"></view>
+              </view>
+              <view class="wifi-icon offline" v-else>
+                <view class="wifi-arc wifi-arc-1"></view>
+                <view class="wifi-arc wifi-arc-2"></view>
+                <view class="wifi-arc wifi-arc-3"></view>
+                <view class="wifi-dot"></view>
+                <view class="wifi-slash"></view>
+              </view>
+              <text class="status-text">{{ getStatusText(device.status) }}</text>
             </view>
-            <view class="status-tag" :class="getStatusClass(device.status)">
-              <text>{{ getStatusText(device.status) }}</text>
+            <view class="device-info">
+              <text class="device-name">{{ device.name }}</text>
+              <view class="device-meta">
+                <text class="meta-label">设备编号</text>
+                <text class="meta-value">{{ device.deviceId }}</text>
+                <text class="meta-divider" v-if="device.shopName">|</text>
+                <text class="meta-shop" v-if="device.shopName">{{ device.shopName }}</text>
+              </view>
             </view>
           </view>
-          
-          <!-- 第二行:门店 + 销售数据 + 库存 -->
-          <view class="card-row-info">
-            <view class="info-left">
-              <text class="info-text">{{ device.shopName }}</text>
-              <text class="info-divider" v-if="device.status === 1">·</text>
-              <text class="info-temp" v-if="device.status === 1">{{ device.temperature }}°C</text>
+
+          <!-- 功能入口 -->
+          <view class="card-actions">
+            <view class="action-item" @click.stop="goProductConfig(device.deviceId)">
+              <view class="action-icon orange">
+                <text class="action-num">2</text>
+              </view>
+              <text class="action-label">配置上架商品</text>
             </view>
-            <view class="info-right">
-              <text class="sales-value">¥{{ formatMoney(device.todaySales) }}</text>
-              <text class="stock-value">{{ device.inventory }}/{{ device.capacity }}</text>
+            <view class="action-item" @click.stop="goRestockerConfig(device.deviceId)">
+              <view class="action-icon orange">
+                <text class="action-num">3</text>
+              </view>
+              <text class="action-label">配置补货员</text>
+            </view>
+            <view class="arrow-icon">
+              <view class="arrow-right"></view>
             </view>
           </view>
         </view>
       </view>
-      
+
+      <!-- 加载状态 -->
       <view class="loading-more" v-if="loading">
         <view class="loading-spinner"></view>
         <text>加载中...</text>
       </view>
-      
+
       <view class="no-more" v-if="!hasMore && deviceList.length > 0">
         <text>— 没有更多了 —</text>
       </view>
-      
+
+      <!-- 空状态 -->
       <view class="empty-state" v-if="!loading && deviceList.length === 0">
         <view class="empty-icon">
-          <view class="empty-icon-inner"></view>
+          <view class="empty-wifi">
+            <view class="wifi-arc-big"></view>
+            <view class="wifi-dot-big"></view>
+          </view>
         </view>
         <text class="empty-text">暂无设备数据</text>
+        <text class="empty-subtext">请检查网络或添加设备</text>
       </view>
     </scroll-view>
-    
+
     <!-- 自定义TabBar -->
     <CustomTabBar />
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
-import NavBar from '@/components/NavBar.vue';
+import { ref, onMounted, computed } from 'vue';
 import CustomTabBar from '@/components/CustomTabBar.vue';
-import { getDeviceList, getDeviceStats } from '@/api/device';
-import { DeviceStatusText, DeviceStatusColor } from '@/utils/constants';
-import { formatMoney as formatMoneyUtil, formatTime as formatTimeUtil } from '@/utils/common';
+import { getDeviceList } from '@/api/device';
+import { DeviceStatusText } from '@/utils/constants';
+
+type TabType = 'all' | 'online' | 'offline';
 
 const deviceList = ref<any[]>([]);
-const deviceStats = ref({
-  totalCount: 0,
-  onlineCount: 0,
-  offlineCount: 0,
-  maintenanceCount: 0,
-  errorCount: 0
-});
 const loading = ref(false);
 const hasMore = ref(true);
 const page = ref(1);
 const pageSize = 10;
+const keyword = ref('');
+const activeTab = ref<TabType>('all');
+const refreshing = ref(false);
+const totalCount = ref(0);
+const isScrolled = ref(false);
+
+const statusBarHeight = ref(44);
 
-const formatMoney = formatMoneyUtil;
-const formatTime = formatTimeUtil;
+// 根据tab获取状态筛选值
+const statusFilter = computed(() => {
+  if (activeTab.value === 'online') return 1;
+  if (activeTab.value === 'offline') return 0;
+  return undefined;
+});
 
 const getStatusText = (status: number) => DeviceStatusText[status] || '未知';
 
@@ -117,33 +202,36 @@ const getStatusClass = (status: number) => {
   return 'offline';
 };
 
-const loadStats = async () => {
-  try {
-    deviceStats.value = await getDeviceStats();
-  } catch (error) {
-    console.error('加载设备统计失败', error);
-  }
-};
+
 
 const loadDevices = async () => {
+  if (loading.value) return;
   loading.value = true;
   try {
-    const res = await getDeviceList({
+    const params: any = {
       page: page.value,
-      pageSize
-    });
-    
+      pageSize,
+      keyword: keyword.value || undefined
+    };
+    if (statusFilter.value !== undefined) {
+      params.status = statusFilter.value;
+    }
+
+    const res = await getDeviceList(params);
+
     if (page.value === 1) {
-      deviceList.value = res.list;
+      deviceList.value = res.list || [];
     } else {
-      deviceList.value = [...deviceList.value, ...res.list];
+      deviceList.value = [...deviceList.value, ...(res.list || [])];
     }
-    
-    hasMore.value = deviceList.value.length < res.total;
+
+    totalCount.value = res.total || 0;
+    hasMore.value = deviceList.value.length < (res.total || 0);
   } catch (error) {
     console.error('加载设备列表失败', error);
   } finally {
     loading.value = false;
+    refreshing.value = false;
   }
 };
 
@@ -153,291 +241,651 @@ const loadMore = () => {
   loadDevices();
 };
 
-const goDetail = (deviceId: number) => {
+const onRefresh = () => {
+  refreshing.value = true;
+  page.value = 1;
+  hasMore.value = true;
+  loadDevices();
+};
+
+const handleSearch = () => {
+  page.value = 1;
+  hasMore.value = true;
+  loadDevices();
+};
+
+const switchTab = (tab: TabType) => {
+  if (activeTab.value === tab) return;
+  activeTab.value = tab;
+  page.value = 1;
+  hasMore.value = true;
+  deviceList.value = [];
+  loadDevices();
+};
+
+const refreshNetwork = () => {
+  uni.showLoading({ title: '刷新中...' });
+  page.value = 1;
+  hasMore.value = true;
+  loadDevices().then(() => {
+    uni.hideLoading();
+    uni.showToast({ title: '刷新成功', icon: 'success' });
+  }).catch(() => {
+    uni.hideLoading();
+  });
+};
+
+const showFilter = () => {
+  uni.showActionSheet({
+    itemList: ['全部状态', '在线', '离线', '维护中', '故障'],
+    success: (res) => {
+      const map = ['all', 'online', 'offline', 'maintenance', 'error'];
+      const tab = map[res.tapIndex];
+      if (tab === 'maintenance' || tab === 'error') {
+        activeTab.value = 'all';
+        // 可以通过额外的筛选参数处理
+        page.value = 1;
+        loadDevices();
+      } else {
+        switchTab(tab as TabType);
+      }
+    }
+  });
+};
+
+const goDetail = (deviceId: string) => {
   uni.navigateTo({ url: `/pages/device/detail?id=${deviceId}` });
 };
 
+const goProductConfig = (deviceId: string) => {
+  uni.navigateTo({ url: `/pages/products/list?deviceId=${deviceId}` });
+};
+
+const goRestockerConfig = (deviceId: string) => {
+  uni.showToast({ title: '配置补货员功能开发中', icon: 'none' });
+};
+
+const onScroll = (e: any) => {
+  const scrollTop = e.detail?.scrollTop || 0;
+  isScrolled.value = scrollTop > 10;
+};
+
 onMounted(() => {
-  loadStats();
+  const systemInfo = uni.getSystemInfoSync();
+  statusBarHeight.value = systemInfo.statusBarHeight || 44;
   loadDevices();
 });
 </script>
 
 <style lang="scss" scoped>
 .page {
-  min-height: 100vh;
-  background: #f8fafc;
+  height: 100vh;
+  background: #f5f5f5;
   display: flex;
   flex-direction: column;
-  padding-bottom: 200rpx;
+  overflow: hidden;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
 }
 
-/* 统计区域 */
-.stats-section {
-  padding: 16rpx 24rpx;
-  background: #ffffff;
-  border-bottom: 1rpx solid #e2e8f0;
+/* ========== 黄色头部区域 ========== */
+.header-section {
+  background: #FFD93D;
+  padding-bottom: 20rpx;
+  transition: box-shadow 0.2s ease;
+  flex-shrink: 0;
+
+  &.has-shadow {
+    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
+  }
 }
 
-.stats-grid {
-  display: grid;
-  grid-template-columns: 1.3fr 1fr 1fr;
-  gap: 12rpx;
+.status-bar {
+  width: 100%;
 }
 
-.stat-card {
-  background: #f8fafc;
-  border: 1rpx solid #e2e8f0;
-  border-radius: 16rpx;
-  padding: 24rpx 16rpx;
+.header-title {
   display: flex;
-  flex-direction: column;
   align-items: center;
   justify-content: center;
+  height: 88rpx;
+  position: relative;
+
+  .title-text {
+    font-size: 36rpx;
+    font-weight: 600;
+    color: #333333;
+  }
+}
+
+/* Tab 切换 */
+.tab-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  margin-top: 8rpx;
+  padding: 0 40rpx;
+}
+
+.tab-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 16rpx 24rpx;
   position: relative;
-  
-  &.main {
-    background: #ecfdf5;
-    border-color: #10b981;
+
+  .tab-text {
+    font-size: 30rpx;
+    color: #666666;
+    font-weight: 500;
   }
-  
-  .stat-dot {
+
+  &.active .tab-text {
+    color: #333333;
+    font-weight: 600;
+  }
+
+  .tab-indicator {
     position: absolute;
-    top: 16rpx;
-    right: 16rpx;
-    width: 10rpx;
-    height: 10rpx;
-    border-radius: 50%;
-    
-    &.green { background: #10b981; }
-    &.amber { background: #f97316; }
+    bottom: 0;
+    width: 48rpx;
+    height: 6rpx;
+    background: #333333;
+    border-radius: 3rpx;
   }
-  
-  .stat-value {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 36rpx;
-    font-weight: 700;
-    color: #1e293b;
-    margin-bottom: 4rpx;
+}
+
+/* ========== 搜索区域 ========== */
+.search-section {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  padding: 20rpx 24rpx;
+  background: #ffffff;
+  border-bottom: 1rpx solid #f0f0f0;
+  flex-shrink: 0;
+}
+
+.search-box {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  background: #f5f5f5;
+  border-radius: 40rpx;
+  padding: 16rpx 24rpx;
+  height: 72rpx;
+}
+
+.search-icon {
+  position: relative;
+  width: 32rpx;
+  height: 32rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+
+  .search-icon-circle {
+    width: 24rpx;
+    height: 24rpx;
+    border: 3rpx solid #999999;
+    border-radius: 50%;
+    position: absolute;
+    top: 0;
+    left: 0;
   }
-  
-  .stat-label {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 22rpx;
-    color: #64748b;
+
+  .search-icon-line {
+    width: 10rpx;
+    height: 3rpx;
+    background: #999999;
+    border-radius: 2rpx;
+    position: absolute;
+    bottom: 2rpx;
+    right: 2rpx;
+    transform: rotate(45deg);
   }
 }
 
-/* 设备列表 */
-.device-scroll {
+.search-input {
   flex: 1;
-  height: 0;
+  font-size: 28rpx;
+  color: #333333;
+  height: 100%;
 }
 
-.device-list {
-  padding: 12rpx 24rpx;
+.search-placeholder {
+  color: #aaaaaa;
+  font-size: 28rpx;
 }
 
-.device-card {
-  background: #ffffff;
-  border: 1rpx solid #e2e8f0;
-  border-radius: 12rpx;
-  margin-bottom: 8rpx;
-  padding: 14rpx 16rpx;
-  transition: transform 0.15s;
-  
+.search-btn {
+  background: #FFD93D;
+  border-radius: 36rpx;
+  padding: 16rpx 36rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+
   &:active {
-    transform: scale(0.98);
+    opacity: 0.85;
+  }
+
+  .search-btn-text {
+    font-size: 28rpx;
+    color: #333333;
+    font-weight: 500;
   }
 }
 
-/* 第一行:名称 + 状态 */
-.card-row-main {
+/* ========== 统计与操作栏 ========== */
+.stats-bar {
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  margin-bottom: 8rpx;
+  justify-content: space-between;
+  padding: 20rpx 24rpx;
+  background: #ffffff;
+  border-bottom: 1rpx solid #f0f0f0;
+  flex-shrink: 0;
+}
+
+.stats-total {
+  font-size: 26rpx;
+  color: #999999;
+  flex-shrink: 0;
 }
 
-.device-title {
+.stats-refresh {
   display: flex;
   align-items: center;
   flex: 1;
+  justify-content: center;
+  margin: 0 16rpx;
   min-width: 0;
-  
-  .device-name {
-    font-size: 28rpx;
-    font-weight: 600;
-    color: #1e293b;
-    margin-right: 12rpx;
+
+  .refresh-text {
+    font-size: 24rpx;
+    color: #999999;
     white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
   }
-  
-  .device-no {
-    font-size: 20rpx;
-    color: #94a3b8;
-    font-family: monospace;
-    flex-shrink: 0;
+
+  .refresh-link {
+    font-size: 24rpx;
+    color: #1890ff;
+    white-space: nowrap;
   }
 }
 
-.status-tag {
-  padding: 4rpx 12rpx;
-  border-radius: 6rpx;
-  font-size: 20rpx;
-  font-weight: 500;
+.stats-filter {
+  display: flex;
+  align-items: center;
+  gap: 6rpx;
   flex-shrink: 0;
-  margin-left: 12rpx;
-  
+
+  &:active {
+    opacity: 0.7;
+  }
+
+  .filter-text {
+    font-size: 26rpx;
+    color: #666666;
+  }
+
+  .filter-icon {
+    width: 0;
+    height: 0;
+    border-left: 8rpx solid transparent;
+    border-right: 8rpx solid transparent;
+    border-top: 10rpx solid #666666;
+    margin-top: 4rpx;
+  }
+}
+
+/* ========== 设备列表滚动区域 ========== */
+.device-scroll {
+  flex: 1;
+  height: 0;
+}
+
+.device-list {
+  padding: 20rpx 24rpx;
+}
+
+/* ========== 设备卡片 ========== */
+.device-card {
+  background: #ffffff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+
+  &:active {
+    transform: scale(0.98);
+    transition: transform 0.15s;
+  }
+}
+
+/* 卡片头部 */
+.card-header {
+  display: flex;
+  align-items: flex-start;
+  gap: 16rpx;
+  margin-bottom: 24rpx;
+}
+
+.status-badge {
+  display: flex;
+  align-items: center;
+  gap: 6rpx;
+  padding: 6rpx 14rpx;
+  border-radius: 8rpx;
+  flex-shrink: 0;
+  margin-top: 4rpx;
+
   &.online {
-    background: #ecfdf5;
-    color: #10b981;
+    background: #e6f7e6;
   }
-  
+
   &.offline {
-    background: #fff7ed;
-    color: #f97316;
+    background: #f5f5f5;
   }
-  
+
   &.maintenance {
-    background: #faf5ff;
-    color: #a855f7;
+    background: #fff7e6;
+  }
+
+  .status-text {
+    font-size: 22rpx;
+    font-weight: 500;
+  }
+
+  &.online .status-text {
+    color: #52c41a;
+  }
+
+  &.offline .status-text {
+    color: #999999;
+  }
+
+  &.maintenance .status-text {
+    color: #faad14;
   }
 }
 
-/* 第二行:门店 + 销售数据 */
-.card-row-info {
+/* WiFi 图标 */
+.wifi-icon {
+  position: relative;
+  width: 28rpx;
+  height: 22rpx;
   display: flex;
-  justify-content: space-between;
-  align-items: center;
+  align-items: flex-end;
+  justify-content: center;
+
+  .wifi-arc {
+    position: absolute;
+    border-radius: 50%;
+    border-style: solid;
+    border-bottom-color: transparent;
+    border-left-color: transparent;
+    border-right-color: transparent;
+  }
+
+  .wifi-arc-1 {
+    width: 24rpx;
+    height: 12rpx;
+    border-width: 3rpx;
+    bottom: 4rpx;
+  }
+
+  .wifi-arc-2 {
+    width: 18rpx;
+    height: 9rpx;
+    border-width: 3rpx;
+    bottom: 4rpx;
+  }
+
+  .wifi-arc-3 {
+    width: 12rpx;
+    height: 6rpx;
+    border-width: 3rpx;
+    bottom: 4rpx;
+  }
+
+  .wifi-dot {
+    width: 5rpx;
+    height: 5rpx;
+    border-radius: 50%;
+    position: absolute;
+    bottom: 0;
+  }
 }
 
-.info-left {
-  display: flex;
-  align-items: center;
+.wifi-icon:not(.offline) {
+  .wifi-arc {
+    border-color: #52c41a;
+  }
+  .wifi-dot {
+    background: #52c41a;
+  }
+}
+
+.wifi-icon.offline {
+  .wifi-arc {
+    border-color: #cccccc;
+  }
+  .wifi-dot {
+    background: #cccccc;
+  }
+
+  .wifi-slash {
+    position: absolute;
+    width: 2rpx;
+    height: 24rpx;
+    background: #ff4d4f;
+    transform: rotate(45deg);
+    top: -2rpx;
+    left: 50%;
+    margin-left: -1rpx;
+  }
+}
+
+.device-info {
   flex: 1;
   min-width: 0;
-  
-  .info-text {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+
+.device-name {
+  font-size: 30rpx;
+  color: #333333;
+  font-weight: 500;
+  line-height: 1.4;
+  word-break: break-all;
+}
+
+.device-meta {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8rpx;
+
+  .meta-label {
+    font-size: 22rpx;
+    color: #999999;
+    background: #f5f5f5;
+    padding: 2rpx 8rpx;
+    border-radius: 4rpx;
+  }
+
+  .meta-value {
+    font-size: 24rpx;
+    color: #666666;
+    font-weight: 500;
+    font-family: monospace;
+  }
+
+  .meta-divider {
     font-size: 22rpx;
-    color: #64748b;
+    color: #dddddd;
+  }
+
+  .meta-shop {
+    font-size: 24rpx;
+    color: #999999;
     white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
+    max-width: 300rpx;
   }
-  
-  .info-divider {
-    margin: 0 8rpx;
-    color: #cbd5e1;
-    font-size: 22rpx;
-  }
-  
-  .info-temp {
-    font-size: 22rpx;
-    color: #10b981;
-    font-weight: 500;
+}
+
+/* 功能入口 */
+.card-actions {
+  display: flex;
+  align-items: center;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #f5f5f5;
+}
+
+.action-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-right: 48rpx;
+
+  &:active {
+    opacity: 0.7;
   }
 }
 
-.info-right {
+.action-icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
   display: flex;
   align-items: center;
-  flex-shrink: 0;
-  margin-left: 16rpx;
-  
-  .sales-value {
-    font-size: 24rpx;
+  justify-content: center;
+  margin-bottom: 10rpx;
+
+  &.orange {
+    background: #fff7e6;
+    border: 2rpx solid #ffd591;
+  }
+
+  .action-num {
+    font-size: 28rpx;
     font-weight: 600;
-    color: #f97316;
-    margin-right: 16rpx;
+    color: #fa8c16;
   }
-  
-  .stock-value {
-    font-size: 22rpx;
-    color: #64748b;
-    background: #f1f5f9;
-    padding: 4rpx 10rpx;
-    border-radius: 4rpx;
+}
+
+.action-label {
+  font-size: 24rpx;
+  color: #666666;
+}
+
+.arrow-icon {
+  margin-left: auto;
+  padding: 16rpx;
+
+  .arrow-right {
+    width: 16rpx;
+    height: 16rpx;
+    border-top: 3rpx solid #cccccc;
+    border-right: 3rpx solid #cccccc;
+    transform: rotate(45deg);
   }
 }
 
-/* 加载状态 */
+/* ========== 加载状态 ========== */
 .loading-more {
   display: flex;
   align-items: center;
   justify-content: center;
   gap: 12rpx;
   padding: 32rpx;
-  color: #94a3b8;
+  color: #999999;
   font-size: 24rpx;
-  
+
   .loading-spinner {
     width: 32rpx;
     height: 32rpx;
-    border: 3rpx solid #e2e8f0;
-    border-top-color: #10b981;
+    border: 3rpx solid #e8e8e8;
+    border-top-color: #FFD93D;
     border-radius: 50%;
     animation: spin 1s linear infinite;
   }
 }
 
 @keyframes spin {
-  to { transform: rotate(360deg); }
+  to {
+    transform: rotate(360deg);
+  }
 }
 
 .no-more {
   text-align: center;
   padding: 32rpx;
   font-size: 24rpx;
-  color: #cbd5e1;
+  color: #cccccc;
 }
 
+/* ========== 空状态 ========== */
 .empty-state {
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 100rpx 0;
-  
+  padding: 120rpx 0;
+
   .empty-icon {
-    width: 120rpx;
-    height: 120rpx;
-    background: #f1f5f9;
-    border-radius: 24rpx;
-    margin-bottom: 20rpx;
+    width: 140rpx;
+    height: 140rpx;
+    background: #f5f5f5;
+    border-radius: 50%;
+    margin-bottom: 24rpx;
     display: flex;
     align-items: center;
     justify-content: center;
-    
-    .empty-icon-inner {
-      width: 32rpx;
-      height: 48rpx;
-      border: 4rpx solid #cbd5e1;
-      border-radius: 6rpx;
-      position: relative;
-      
-      &::after {
-        content: '';
-        position: absolute;
-        bottom: 8rpx;
-        left: 50%;
-        transform: translateX(-50%);
-        width: 10rpx;
-        height: 4rpx;
-        background: #cbd5e1;
-        border-radius: 2rpx;
-      }
-    }
   }
-  
+
+  .empty-wifi {
+    position: relative;
+    width: 60rpx;
+    height: 50rpx;
+    display: flex;
+    align-items: flex-end;
+    justify-content: center;
+  }
+
+  .wifi-arc-big {
+    position: absolute;
+    width: 56rpx;
+    height: 28rpx;
+    border-radius: 50%;
+    border: 4rpx solid #cccccc;
+    border-bottom-color: transparent;
+    border-left-color: transparent;
+    border-right-color: transparent;
+    bottom: 10rpx;
+  }
+
+  .wifi-dot-big {
+    width: 10rpx;
+    height: 10rpx;
+    border-radius: 50%;
+    background: #cccccc;
+  }
+
   .empty-text {
-    font-size: 28rpx;
-    color: #94a3b8;
+    font-size: 30rpx;
+    color: #666666;
+    margin-bottom: 12rpx;
+  }
+
+  .empty-subtext {
+    font-size: 26rpx;
+    color: #999999;
   }
 }
 </style>

+ 1 - 1
haha-admin-mp/src/pages/login/login.vue

@@ -54,7 +54,7 @@
         <view class="login-tips">
           <view class="tips-icon"></view>
           <text class="tips-label">测试账号</text>
-          <text class="tips-value">admin / 123456</text>
+          <text class="tips-value">admin / admin123</text>
         </view>
       </view>
     </view>

+ 367 - 338
haha-admin-mp/src/pages/orders/detail.vue

@@ -1,152 +1,187 @@
 <template>
   <view class="page">
-    <!-- 导航栏 -->
-    <NavBar title="订单详情" :showBack="true" />
-    
-    <view class="detail-card" v-if="order">
-      <!-- 订单状态 -->
+    <!-- 黄色导航栏 -->
+    <NavBar :title="navTitle" bgColor="#FFD100" titleColor="#333333" :showBack="true" />
+
+    <scroll-view class="detail-scroll" scroll-y v-if="order">
+      <!-- 状态区域 -->
       <view class="status-section">
-        <view class="status-icon" :class="getStatusClass(order.status)">
-          <view class="icon-check" v-if="order.status === 2"></view>
-          <view class="icon-close" v-else-if="order.status === 0 || order.status === 3"></view>
-          <view class="icon-clock" v-else-if="order.status === 1"></view>
-          <view class="icon-box" v-else></view>
+        <view class="status-row">
+          <view class="status-tag" :class="getPayStatusClass(order.payStatus)">
+            <text>{{ order.payStatusLabel || getPayStatusText(order.payStatus) }}</text>
+          </view>
+          <text class="order-no-text">订单{{ order.orderNo }}</text>
         </view>
-        <text class="status-text">{{ order.statusText || getStatusText(order.status) }}</text>
-        <text class="pay-status" v-if="order.payStatusLabel">{{ order.payStatusLabel }}</text>
+        <text class="status-desc" :class="getPayStatusClass(order.payStatus)">
+          {{ getStatusDesc(order.status, order.payStatus) }}
+        </text>
       </view>
-      
-      <!-- 门店信息 -->
-      <view class="info-section">
-        <text class="section-title">门店信息</text>
-        <view class="info-item">
-          <text class="label">门店名称</text>
-          <text class="value">{{ order.shopName || '-' }}</text>
-        </view>
-        <view class="info-item">
-          <text class="label">设备编号</text>
-          <text class="value mono">{{ order.deviceId || '-' }}</text>
-        </view>
+
+      <!-- 视频区域 -->
+      <view class="video-section" v-if="order.videoUrl">
+        <video class="order-video" :src="order.videoUrl" controls></video>
       </view>
-      
-      <!-- 商品列表 -->
-      <view class="info-section">
-        <text class="section-title">商品信息</text>
+
+      <!-- 订单明细 -->
+      <view class="section">
+        <text class="section-title">订单明细</text>
         <view class="product-list">
           <view class="product-item" v-for="item in order.products" :key="item.productId">
-            <image 
-              class="product-pic" 
-              :src="item.pic || '/static/images/default-product.png'" 
+            <image
+              class="product-img"
+              :src="item.pic || '/static/images/default-product.png'"
               mode="aspectFill"
             />
-            <view class="product-info">
+            <view class="product-main">
               <text class="product-name">{{ item.productName }}</text>
-              <text class="product-price">¥{{ formatMoney(item.price) }}</text>
-            </view>
-            <view class="product-count">
-              <text>x{{ item.productNum }}</text>
+              <text class="product-meta">销量 x{{ item.productNum }}</text>
+              <text class="product-meta">销售单价 ¥{{ formatMoney(item.price) }}</text>
             </view>
-            <view class="product-amount">
-              <text>¥{{ formatMoney(item.money) }}</text>
+            <view class="product-right">
+              <text class="deal-price">成交单价 ¥{{ formatMoney(item.price) }}</text>
             </view>
           </view>
         </view>
+        <view class="amount-row">
+          <text class="amount-label">实付款</text>
+          <text class="amount-value">合计¥{{ formatMoney(order.paidAmount) }}</text>
+        </view>
       </view>
-      
+
       <!-- 订单信息 -->
-      <view class="info-section">
+      <view class="section">
         <text class="section-title">订单信息</text>
-        <view class="info-item">
-          <text class="label">订单编号</text>
-          <text class="value mono">{{ order.orderNo }}</text>
+        <view class="info-row">
+          <text class="info-label">订单编号</text>
+          <view class="info-value-wrapper">
+            <text class="info-value">{{ order.orderNo }}</text>
+            <view class="copy-icon" @click="copyText(order.orderNo)"></view>
+          </view>
         </view>
-        <view class="info-item" v-if="order.outTradeNo">
-          <text class="label">外部交易号</text>
-          <text class="value mono">{{ order.outTradeNo }}</text>
+        <view class="info-row">
+          <text class="info-label">下单时间</text>
+          <text class="info-value">{{ order.createTime }}</text>
         </view>
-        <view class="info-item">
-          <text class="label">创建时间</text>
-          <text class="value">{{ order.createTime }}</text>
+        <view class="info-row" v-if="order.payTime">
+          <text class="info-label">支付时间</text>
+          <text class="info-value">{{ order.payTime }}</text>
         </view>
-        <view class="info-item" v-if="order.payTime">
-          <text class="label">支付时间</text>
-          <text class="value">{{ order.payTime }}</text>
+        <view class="info-row">
+          <text class="info-label">门店名称</text>
+          <text class="info-value">{{ order.shopName || '-' }}</text>
         </view>
-        <view class="info-item">
-          <text class="label">支付方式</text>
-          <text class="value">{{ order.payType || '-' }}</text>
+        <view class="info-row">
+          <text class="info-label">设备名称</text>
+          <text class="info-value">{{ order.deviceName || '-' }}</text>
         </view>
-        <view class="info-item" v-if="order.userTagLabel">
-          <text class="label">用户标签</text>
-          <view class="tag" :class="getUserTagClass(order.userTag)">
-            <text>{{ order.userTagLabel }}</text>
-          </view>
+        <view class="info-row">
+          <text class="info-label">设备编号</text>
+          <text class="info-value">{{ order.deviceId || '-' }}</text>
         </view>
-      </view>
-      
-      <!-- 金额信息 -->
-      <view class="info-section amount-section">
-        <text class="section-title">金额信息</text>
-        <view class="info-item">
-          <text class="label">商品总额</text>
-          <text class="value">¥{{ formatMoney(order.totalAmount) }}</text>
+        <view class="info-row">
+          <text class="info-label">设备地址</text>
+          <text class="info-value">-</text>
         </view>
-        <view class="info-item" v-if="order.discountAmount && order.discountAmount > 0">
-          <text class="label">优惠金额</text>
-          <text class="value discount">-¥{{ formatMoney(order.discountAmount) }}</text>
+        <view class="info-row">
+          <text class="info-label">联系消费者</text>
+          <view class="info-value-wrapper">
+            <view class="copy-icon" @click="copyText(order.phone || '')"></view>
+            <view class="phone-icon" @click="callPhone(order.phone)"></view>
+          </view>
+        </view>
+        <view class="info-row blacklist-row">
+          <view class="blacklist-label">
+            <text class="info-label">黑名单</text>
+            <text class="blacklist-tip">移入黑名单后,消费者将无法在"此设备所属子商家"的所有设备中消费</text>
+          </view>
+          <switch class="blacklist-switch" :checked="false" @change="toggleBlacklist" />
         </view>
-        <view class="info-item highlight">
-          <text class="label">实付金额</text>
-          <text class="value">¥{{ formatMoney(order.paidAmount) }}</text>
+        <view class="info-row">
+          <text class="info-label">已发送短信催缴次数</text>
+          <text class="info-value">0次</text>
         </view>
       </view>
-    </view>
-    
+    </scroll-view>
+
     <!-- 底部操作 -->
-    <view class="bottom-actions" v-if="order && canRefund(order)">
-      <button class="refund-btn" @click="handleRefund">申请退款</button>
+    <view class="bottom-bar" v-if="order && canRefund(order)">
+      <button class="refund-btn" @click="handleRefund">退款</button>
     </view>
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, computed } from 'vue';
 import { onLoad } from '@dcloudio/uni-app';
 import NavBar from '@/components/NavBar.vue';
 import { getOrderDetail, handleRefund as refundOrder } from '@/api/order';
-import { OrderStatusText } from '@/utils/constants';
-import { formatMoney as formatMoneyUtil, showToast, navigateBack, showConfirm } from '@/utils/common';
+import { formatMoney as formatMoneyUtil, showToast, showConfirm } from '@/utils/common';
 
 const order = ref<any>(null);
 const formatMoney = formatMoneyUtil;
 
-const getStatusText = (status: number) => OrderStatusText[status] || '未知';
+const navTitle = computed(() => {
+  return `${order.value?.deviceId || '订单'}-订单详情`;
+});
+
+const getPayStatusText = (payStatus: string) => {
+  const map: Record<string, string> = {
+    'UNPAID': '未支付',
+    'PAID': '已支付',
+    'REFUND': '已退款'
+  };
+  return map[payStatus] || '未知';
+};
 
-const getStatusClass = (status: number) => {
-  if (status === 2) return 'success';
-  if (status === 0 || status === 3) return 'danger';
-  if (status === 1) return 'warning';
-  return 'pending';
+const getPayStatusClass = (payStatus: string) => {
+  if (payStatus === 'PAID') return 'success';
+  if (payStatus === 'REFUND') return 'danger';
+  return 'warning';
 };
 
-const getUserTagClass = (tag: string) => {
-  if (tag === 'new') return 'tag-new';
-  if (tag === 'frequent') return 'tag-frequent';
-  return 'tag-regular';
+const getStatusDesc = (status: number, payStatus: string) => {
+  if (payStatus === 'PAID') return '交易成功';
+  if (payStatus === 'REFUND') return '已退款';
+  if (status === 1) return '等待支付';
+  if (status === 0) return '已取消';
+  if (status === 3) return '已关闭';
+  return '';
 };
 
 const canRefund = (order: any) => {
   return order.payStatus === 'PAID' && order.status === 2;
 };
 
+const copyText = (text: string) => {
+  if (!text) {
+    showToast('无内容可复制');
+    return;
+  }
+  uni.setClipboardData({
+    data: text,
+    success: () => showToast('已复制', 'success')
+  });
+};
+
+const callPhone = (phone: string) => {
+  if (!phone) {
+    showToast('暂无联系方式');
+    return;
+  }
+  uni.makePhoneCall({ phoneNumber: phone });
+};
+
+const toggleBlacklist = () => {
+  showToast('功能开发中');
+};
+
 const loadDetail = async (id?: string) => {
   if (!id) {
     showToast('订单ID不存在');
     return;
   }
-  
   try {
-    order.value = await getOrderDetail(Number(id));
+    order.value = await getOrderDetail(id);
   } catch (error) {
     showToast('加载订单详情失败');
   }
@@ -154,10 +189,8 @@ const loadDetail = async (id?: string) => {
 
 const handleRefund = async () => {
   if (!order.value) return;
-  
   const confirmed = await showConfirm('确认对该订单进行退款操作?');
   if (!confirmed) return;
-  
   try {
     await refundOrder(order.value.id, '商家申请退款');
     showToast('退款处理成功', 'success');
@@ -183,277 +216,272 @@ onMounted(() => {
 <style lang="scss" scoped>
 .page {
   min-height: 100vh;
-  background: #f8fafc;
-  padding-bottom: 120rpx;
+  background: #f5f5f5;
+  display: flex;
+  flex-direction: column;
+}
+
+.detail-scroll {
+  flex: 1;
+  height: 0;
+  padding-bottom: 140rpx;
 }
 
+/* 状态区域 */
 .status-section {
   background: #ffffff;
-  padding: 48rpx 32rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
+  padding: 24rpx 32rpx;
   margin-bottom: 16rpx;
-  
-  .status-icon {
-    width: 88rpx;
-    height: 88rpx;
-    border-radius: 50%;
+
+  .status-row {
     display: flex;
     align-items: center;
-    justify-content: center;
-    margin-bottom: 20rpx;
-    
-    &.success {
-      background: #ecfdf5;
-      
-      .icon-check {
-        width: 40rpx;
-        height: 24rpx;
-        border-left: 4rpx solid #10b981;
-        border-bottom: 4rpx solid #10b981;
-        transform: rotate(-45deg);
-        margin-top: -8rpx;
+    margin-bottom: 12rpx;
+
+    .status-tag {
+      padding: 6rpx 16rpx;
+      border-radius: 6rpx;
+      font-size: 24rpx;
+      font-weight: 500;
+      margin-right: 16rpx;
+
+      &.success {
+        background: #ecfdf5;
+        color: #10b981;
       }
-    }
-    
-    &.warning {
-      background: #fff7ed;
-      
-      .icon-clock {
-        width: 36rpx;
-        height: 36rpx;
-        border: 4rpx solid #f97316;
-        border-radius: 50%;
-        position: relative;
-        
-        &::before {
-          content: '';
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          transform: translate(-50%, -50%);
-          width: 14rpx;
-          height: 4rpx;
-          background: #f97316;
-          transform-origin: left center;
-        }
-        
-        &::after {
-          content: '';
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          transform: translate(-50%, -100%);
-          width: 4rpx;
-          height: 10rpx;
-          background: #f97316;
-        }
+
+      &.danger {
+        background: #fef2f2;
+        color: #ef4444;
       }
-    }
-    
-    &.danger {
-      background: #fef2f2;
-      
-      .icon-close {
-        width: 32rpx;
-        height: 32rpx;
-        position: relative;
-        
-        &::before,
-        &::after {
-          content: '';
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          width: 24rpx;
-          height: 4rpx;
-          background: #ef4444;
-        }
-        
-        &::before {
-          transform: translate(-50%, -50%) rotate(45deg);
-        }
-        
-        &::after {
-          transform: translate(-50%, -50%) rotate(-45deg);
-        }
+
+      &.warning {
+        background: #fff7ed;
+        color: #f97316;
       }
     }
-    
-    &.processing {
-      background: #f0f9ff;
-      
-      .icon-box {
-        width: 32rpx;
-        height: 32rpx;
-        border: 4rpx solid #0ea5e9;
-        border-radius: 6rpx;
-      }
+
+    .order-no-text {
+      font-size: 30rpx;
+      font-weight: 600;
+      color: #333333;
     }
-    
-    &.pending {
-      background: #faf5ff;
-      
-      .icon-box {
-        width: 32rpx;
-        height: 32rpx;
-        border: 4rpx solid #a855f7;
-        border-radius: 6rpx;
-      }
+  }
+
+  .status-desc {
+    font-size: 26rpx;
+
+    &.success {
+      color: #10b981;
+    }
+
+    &.danger {
+      color: #ef4444;
+    }
+
+    &.warning {
+      color: #f97316;
     }
   }
-  
-  .status-text {
-    font-size: 36rpx;
-    font-weight: 600;
-    color: #1e293b;
-    display: flex;
-    align-items: center;
-    justify-content: center;
+}
+
+/* 视频区域 */
+.video-section {
+  background: #ffffff;
+  padding: 20rpx 32rpx;
+  margin-bottom: 16rpx;
+
+  .order-video {
+    width: 100%;
+    height: 400rpx;
+    border-radius: 12rpx;
+    background: #000000;
   }
-  
-  .pay-status {
-    font-size: 24rpx;
-    color: #64748b;
-    margin-top: 8rpx;
+}
+
+/* 通用卡片 */
+.section {
+  background: #ffffff;
+  margin-bottom: 16rpx;
+  padding: 24rpx 32rpx;
+
+  .section-title {
+    display: block;
+    font-size: 30rpx;
+    font-weight: 600;
+    color: #333333;
+    margin-bottom: 20rpx;
   }
 }
 
-.detail-card {
-  .info-section {
-    background: #ffffff;
-    margin-bottom: 16rpx;
-    padding: 24rpx 32rpx;
-    
-    .section-title {
-      display: block;
-      font-size: 28rpx;
-      font-weight: 600;
-      color: #1e293b;
-      margin-bottom: 20rpx;
-      padding-bottom: 16rpx;
-      border-bottom: 1rpx solid #f1f5f9;
+/* 商品列表 */
+.product-list {
+  .product-item {
+    display: flex;
+    align-items: flex-start;
+    padding: 16rpx 0;
+    border-bottom: 1rpx solid #f5f5f5;
+
+    &:last-child {
+      border-bottom: none;
     }
-    
-    .info-item {
-      display: flex;
-      justify-content: space-between;
-      padding: 14rpx 0;
-      
-      .label {
-        font-size: 28rpx;
-        color: #64748b;
-      }
-      
-      .value {
+
+    .product-img {
+      width: 120rpx;
+      height: 120rpx;
+      border-radius: 8rpx;
+      background: #f5f5f5;
+      margin-right: 20rpx;
+      flex-shrink: 0;
+    }
+
+    .product-main {
+      flex: 1;
+
+      .product-name {
+        display: block;
         font-size: 28rpx;
-        color: #1e293b;
-        
-        &.mono {
-          font-family: monospace;
-        }
+        color: #333333;
+        margin-bottom: 8rpx;
       }
-      
-      &.highlight {
-        .value {
-          color: #f97316;
-          font-weight: 600;
-        }
+
+      .product-meta {
+        display: block;
+        font-size: 24rpx;
+        color: #999999;
+        margin-bottom: 4rpx;
       }
     }
-    
-    .tag {
-      padding: 4rpx 16rpx;
-      border-radius: 8rpx;
-      font-size: 24rpx;
-      font-weight: 500;
-      
-      &.tag-new {
-        background: #fef2f2;
-        color: #ef4444;
+
+    .product-right {
+      display: flex;
+      align-items: flex-end;
+
+      .deal-price {
+        font-size: 24rpx;
+        color: #999999;
       }
-      
-      &.tag-frequent {
-        background: #ecfdf5;
-        color: #10b981;
+    }
+  }
+}
+
+.amount-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx 0 0;
+  border-top: 1rpx solid #f5f5f5;
+  margin-top: 16rpx;
+
+  .amount-label {
+    font-size: 28rpx;
+    color: #666666;
+  }
+
+  .amount-value {
+    font-size: 32rpx;
+    font-weight: 700;
+    color: #333333;
+  }
+}
+
+/* 订单信息 */
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+
+  &:last-child {
+    border-bottom: none;
+  }
+
+  .info-label {
+    font-size: 28rpx;
+    color: #999999;
+  }
+
+  .info-value {
+    font-size: 28rpx;
+    color: #333333;
+  }
+
+  .info-value-wrapper {
+    display: flex;
+    align-items: center;
+    gap: 16rpx;
+
+    .copy-icon {
+      width: 32rpx;
+      height: 32rpx;
+      background: #f5f5f5;
+      border-radius: 4rpx;
+      position: relative;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 16rpx;
+        height: 20rpx;
+        border: 2rpx solid #999999;
+        border-radius: 2rpx;
       }
-      
-      &.tag-regular {
-        background: #f0f9ff;
-        color: #0ea5e9;
+
+      &::after {
+        content: '';
+        position: absolute;
+        top: 4rpx;
+        right: 4rpx;
+        width: 8rpx;
+        height: 8rpx;
+        background: #ffffff;
       }
     }
-    
-    .product-list {
-      .product-item {
-        display: flex;
-        align-items: center;
-        padding: 20rpx 0;
-        border-bottom: 1rpx solid #f1f5f9;
-        
-        &:last-child {
-          border-bottom: none;
-        }
-        
-        .product-pic {
-          width: 100rpx;
-          height: 100rpx;
-          border-radius: 12rpx;
-          margin-right: 16rpx;
-          background: #f1f5f9;
-          flex-shrink: 0;
-        }
-        
-        .product-info {
-          flex: 1;
-          
-          .product-name {
-            display: block;
-            font-size: 28rpx;
-            color: #1e293b;
-            margin-bottom: 8rpx;
-          }
-          
-          .product-price {
-            font-size: 24rpx;
-            color: #94a3b8;
-          }
-        }
-        
-        .product-count {
-          width: 100rpx;
-          text-align: center;
-          font-size: 28rpx;
-          color: #64748b;
-        }
-        
-        .product-amount {
-          width: 120rpx;
-          text-align: right;
-          font-size: 28rpx;
-          color: #1e293b;
-          font-weight: 500;
-        }
+
+    .phone-icon {
+      width: 40rpx;
+      height: 40rpx;
+      background: #10b981;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+
+      &::before {
+        content: '';
+        position: absolute;
+        width: 16rpx;
+        height: 20rpx;
+        background: #ffffff;
+        border-radius: 4rpx 4rpx 8rpx 8rpx;
       }
     }
   }
-  
-  .amount-section {
-    .discount {
-      color: #10b981;
-    }
-    
-    .info-item:last-child {
-      background: #fff7ed;
-      margin: 16rpx -32rpx -24rpx;
-      padding: 20rpx 32rpx;
-      border-radius: 0 0 0 0;
+}
+
+.blacklist-row {
+  align-items: flex-start;
+
+  .blacklist-label {
+    display: flex;
+    flex-direction: column;
+
+    .blacklist-tip {
+      font-size: 22rpx;
+      color: #999999;
+      margin-top: 4rpx;
+      line-height: 1.4;
     }
   }
 }
 
-.bottom-actions {
+/* 底部操作 */
+.bottom-bar {
   position: fixed;
   bottom: 0;
   left: 0;
@@ -462,24 +490,25 @@ onMounted(() => {
   padding: 20rpx 32rpx;
   padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
   box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
-  
+
   .refund-btn {
     width: 100%;
     height: 96rpx;
-    background: #ef4444;
-    color: #fff;
+    background: #FFD100;
+    color: #333333;
     font-size: 32rpx;
     font-weight: 600;
     border-radius: 16rpx;
     border: none;
-    
+
     &::after {
       border: none;
     }
-    
+
     &:active {
-      background: #dc2626;
+      background: #e6bc00;
     }
   }
 }
 </style>
+

+ 432 - 357
haha-admin-mp/src/pages/orders/list.vue

@@ -1,183 +1,264 @@
 <template>
   <view class="page">
-    <!-- 导航栏 -->
-    <NavBar title="订单管理" />
-    
-    <!-- 筛选栏 -->
-    <view class="filter-header">
-      <view class="filter-tabs">
-        <view 
-          class="filter-tab" 
-          :class="{ active: currentStatus === -1 }"
-          @click="changeStatus(-1)"
-        >
-          <text>全部</text>
-        </view>
-        <view 
-          class="filter-tab" 
-          :class="{ active: currentStatus === 1 }"
-          @click="changeStatus(1)"
-        >
-          <text>待支付</text>
-        </view>
-        <view 
-          class="filter-tab" 
-          :class="{ active: currentStatus === 2 }"
-          @click="changeStatus(2)"
-        >
-          <text>已完成</text>
-        </view>
-        <view 
-          class="filter-tab" 
-          :class="{ active: currentStatus === 0 }"
-          @click="changeStatus(0)"
-        >
-          <text>已取消</text>
-        </view>
+    <!-- 黄色导航栏 -->
+    <NavBar title="销售订单" bgColor="#FFD100" titleColor="#333333" :showBack="true" />
+
+    <!-- Tab 栏 -->
+    <view class="tab-bar">
+      <view
+        v-for="tab in tabs"
+        :key="tab.key"
+        class="tab-item"
+        :class="{ active: currentTab === tab.key }"
+        @click="changeTab(tab.key)"
+      >
+        <text>{{ tab.label }}</text>
+        <view class="tab-line" v-if="currentTab === tab.key"></view>
+      </view>
+    </view>
+
+    <!-- 时间筛选 -->
+    <view class="time-filter">
+      <text class="time-label">创建时间: {{ dateRangeFullText }}</text>
+    </view>
+
+    <!-- 统计卡片 -->
+    <view class="stats-bar">
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.totalAmount || '0.00' }}</text>
+        <text class="stat-label">订单总金额</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.wechatAmount || '0.00' }}</text>
+        <text class="stat-label">微信总金额</text>
       </view>
-      <view class="search-btn" @click="showSearch = true">
-        <view class="search-icon"></view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.alipayAmount || '0.00' }}</text>
+        <text class="stat-label">支付宝总金额</text>
       </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.otherAmount || '0.00' }}</text>
+        <text class="stat-label">其他</text>
+      </view>
+    </view>
+
+    <!-- 说明文字 -->
+    <view class="tips-section">
+      <text class="tips-text">已支付订单:消费者支付完成的订单;</text>
+      <text class="tips-text">其他订单:包括未支付订单、刷脸支付、余额支付、扶贫、API商家、刷卡等订单;</text>
     </view>
-    
+
+    <!-- 列表头部 -->
+    <view class="list-header">
+      <text class="list-info">{{ dateRangeShortText }} 共{{ total }}条</text>
+      <view class="filter-btn" @click="showDatePicker = true">
+        <text>筛选</text>
+      </view>
+    </view>
+
     <!-- 订单列表 -->
-    <scroll-view 
-      class="order-scroll" 
-      scroll-y 
-      @scrolltolower="loadMore"
-    >
+    <scroll-view class="order-scroll" scroll-y @scrolltolower="loadMore">
       <view class="order-list">
-        <view 
-          class="order-card" 
-          v-for="order in orderList" 
+        <view
+          class="order-card"
+          v-for="order in orderList"
           :key="order.id"
           @click="goDetail(order.id)"
         >
-          <view class="card-main">
-            <view class="card-left">
-              <view class="card-row">
-                <text class="order-no">{{ order.orderNo }}</text>
-                <view class="status-tag" :class="getStatusClass(order.status)">
-                  <text>{{ getStatusText(order.status) }}</text>
-                </view>
-              </view>
-              <view class="card-row">
-                <text class="order-meta">{{ order.shopName }} · {{ order.deviceName }}</text>
-              </view>
+          <!-- 头部 -->
+          <view class="card-header">
+            <text class="order-time">{{ formatDateTime(order.createTime) }}</text>
+            <text class="order-status" :style="{ color: getStatusColor(order.status) }">
+              {{ order.statusLabel || getStatusText(order.status) }}
+            </text>
+          </view>
+
+          <!-- 商品区域 -->
+          <view class="card-product">
+            <image class="product-img" :src="getFirstProductPic(order)" mode="aspectFill" />
+            <view class="product-right">
+              <text class="product-amount">¥{{ formatMoney(order.paidAmount || order.totalAmount) }}</text>
+              <text class="product-count">共{{ getProductCount(order) }}件</text>
+            </view>
+          </view>
+
+          <!-- 信息区域 -->
+          <view class="card-info">
+            <view class="info-line">
+              <text class="info-label">订单编号:</text>
+              <text class="info-value">{{ order.orderNo }}</text>
+            </view>
+            <view class="info-line">
+              <text class="info-label">设备信息:</text>
+              <text class="info-value">{{ order.deviceId }}</text>
             </view>
-            <view class="card-right">
-              <text class="amount">¥{{ formatMoney(order.totalAmount) }}</text>
-              <text class="time-text">{{ order.createTime }}</text>
+            <view class="info-line">
+              <text class="info-label">门店名称:</text>
+              <text class="info-value">{{ order.shopName || '-' }}</text>
+            </view>
+            <view class="info-line">
+              <text class="info-label">支付方式:</text>
+              <text class="info-value">{{ order.payType || '-' }}</text>
             </view>
           </view>
-          
-          <view class="card-action" v-if="order.payStatus === 'REFUND'">
-            <view class="action-btn" @click.stop="handleRefund(order)">
-              <text>处理退款</text>
+
+          <!-- 操作按钮 -->
+          <view class="card-action">
+            <view class="view-btn" @click.stop="goDetail(order.id)">
+              <text>查看</text>
             </view>
           </view>
         </view>
       </view>
-      
+
+      <!-- 加载状态 -->
       <view class="loading-more" v-if="loading">
         <view class="loading-spinner"></view>
         <text>加载中...</text>
       </view>
-      
       <view class="no-more" v-if="!hasMore && orderList.length > 0">
         <text>— 没有更多了 —</text>
       </view>
-      
       <view class="empty-state" v-if="!loading && orderList.length === 0">
-        <view class="empty-icon">
-          <view class="empty-icon-inner"></view>
-        </view>
         <text class="empty-text">暂无订单数据</text>
       </view>
     </scroll-view>
-    
-    <!-- 搜索弹窗 -->
-    <view class="search-modal" v-if="showSearch" @click="showSearch = false">
+
+    <!-- 日期选择弹窗 -->
+    <view class="date-modal" v-if="showDatePicker" @click="showDatePicker = false">
       <view class="modal-content" @click.stop>
         <view class="modal-header">
-          <text class="modal-title">搜索订单</text>
+          <text class="modal-title">选择时间范围</text>
         </view>
         <view class="modal-body">
-          <input 
-            class="search-input" 
-            v-model="searchOrderNo" 
-            placeholder="请输入订单号"
-            placeholder-class="input-placeholder"
-            @confirm="doSearch"
-          />
+          <view class="date-row">
+            <text>开始日期</text>
+            <picker mode="date" :value="startDate" @change="onStartDateChange">
+              <view class="picker-value">{{ startDate }}</view>
+            </picker>
+          </view>
+          <view class="date-row">
+            <text>结束日期</text>
+            <picker mode="date" :value="endDate" @change="onEndDateChange">
+              <view class="picker-value">{{ endDate }}</view>
+            </picker>
+          </view>
         </view>
         <view class="modal-footer">
-          <button class="btn-cancel" @click="showSearch = false">取消</button>
-          <button class="btn-confirm" @click="doSearch">搜索</button>
+          <button class="btn-cancel" @click="showDatePicker = false">取消</button>
+          <button class="btn-confirm" @click="confirmDateRange">确定</button>
         </view>
       </view>
     </view>
-    
-    <!-- 自定义TabBar -->
+
     <CustomTabBar />
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, computed } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import CustomTabBar from '@/components/CustomTabBar.vue';
-import { getOrderList, handleRefund as refundOrder } from '@/api/order';
-import { OrderStatusText, OrderStatusColor, OrderStatus } from '@/utils/constants';
-import { formatMoney as formatMoneyUtil, showConfirm, showToast } from '@/utils/common';
-
+import { getOrderList, getOrderStats } from '@/api/order';
+import { OrderStatusText, OrderStatusColor } from '@/utils/constants';
+import { formatMoney as formatMoneyUtil, showToast } from '@/utils/common';
+
+const tabs = [
+  { key: 'sales', label: '销售订单', params: { status: 2 } },
+  { key: 'refund', label: '退款订单', params: { payStatus: 'REFUND' } },
+  { key: 'abnormal', label: '异常订单', params: { statusList: [0, 3] } },
+  { key: 'unpaid', label: '未支付', params: { status: 1 } }
+];
+
+const currentTab = ref('sales');
 const orderList = ref<any[]>([]);
-const currentStatus = ref(-1);
-const searchOrderNo = ref('');
-const showSearch = ref(false);
 const loading = ref(false);
 const hasMore = ref(true);
 const page = ref(1);
 const pageSize = 10;
+const total = ref(0);
+
+// 日期范围(默认最近30天)
+const today = new Date();
+const thirtyDaysAgo = new Date(today.getTime() - 29 * 24 * 60 * 60 * 1000);
+const formatDateStr = (d: Date) => {
+  const y = d.getFullYear();
+  const m = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  return `${y}-${m}-${day}`;
+};
+const startDate = ref(formatDateStr(thirtyDaysAgo));
+const endDate = ref(formatDateStr(today));
+const showDatePicker = ref(false);
+
+// 统计
+const stats = ref<any>({});
+
+const dateRangeFullText = computed(() => {
+  return `${startDate.value} 00:00:00 - ${endDate.value} 23:59:59`;
+});
+
+const dateRangeShortText = computed(() => {
+  return `${startDate.value}~${endDate.value}`;
+});
 
 const formatMoney = formatMoneyUtil;
 
 const getStatusText = (status: number) => OrderStatusText[status] || '未知';
 
-const getStatusClass = (status: number) => {
-  const classMap: Record<number, string> = {
-    [OrderStatus.CANCELLED]: 'cancelled',
-    [OrderStatus.PENDING_PAYMENT]: 'pending',
-    [OrderStatus.COMPLETED]: 'completed',
-    [OrderStatus.CLOSED]: 'cancelled'
-  };
-  return classMap[status] || 'pending';
+const getStatusColor = (status: number) => OrderStatusColor[status] || '#999999';
+
+const formatDateTime = (datetime: string) => {
+  if (!datetime) return '-';
+  if (datetime.length > 16) {
+    return datetime.substring(0, 16);
+  }
+  return datetime;
 };
 
-const changeStatus = (status: number) => {
-  currentStatus.value = status;
-  page.value = 1;
-  hasMore.value = true;
-  loadOrders();
+const getFirstProductPic = (_order?: any) => {
+  return '/static/images/default-product.png';
+};
+
+const getProductCount = (_order?: any) => {
+  return 1;
+};
+
+const loadStats = async () => {
+  try {
+    const res = await getOrderStats({ startDate: startDate.value, endDate: endDate.value });
+    stats.value = res || {};
+  } catch (error) {
+    console.error('加载统计失败', error);
+  }
 };
 
 const loadOrders = async () => {
   loading.value = true;
   try {
-    const res = await getOrderList({
+    const tab = tabs.find(t => t.key === currentTab.value);
+    const params: any = {
       page: page.value,
       pageSize,
-      status: currentStatus.value,
-      orderNo: searchOrderNo.value
-    });
-    
+      startDate: startDate.value,
+      endDate: endDate.value,
+      ...tab?.params
+    };
+
+    if (params.status === -1) {
+      delete params.status;
+    }
+
+    const res = await getOrderList(params);
+
     if (page.value === 1) {
-      orderList.value = res.list;
+      orderList.value = res.list || [];
     } else {
-      orderList.value = [...orderList.value, ...res.list];
+      orderList.value = [...orderList.value, ...(res.list || [])];
     }
-    
-    hasMore.value = orderList.value.length < res.total;
+
+    total.value = res.total || 0;
+    hasMore.value = orderList.value.length < total.value;
   } catch (error) {
     console.error('加载订单失败', error);
   } finally {
@@ -191,103 +272,160 @@ const loadMore = () => {
   loadOrders();
 };
 
-const doSearch = () => {
-  showSearch.value = false;
+const changeTab = (key: string) => {
+  currentTab.value = key;
   page.value = 1;
   hasMore.value = true;
   loadOrders();
+  loadStats();
 };
 
-const goDetail = (orderId: number) => {
+const goDetail = (orderId: string) => {
   uni.navigateTo({ url: `/pages/orders/detail?id=${orderId}` });
 };
 
-const handleRefund = async (order: any) => {
-  const confirmed = await showConfirm(`确认同意订单 ${order.orderNo} 的退款申请?`);
-  if (confirmed) {
-    try {
-      await refundOrder(order.id, '商家同意退款');
-      showToast('退款处理成功', 'success');
-      loadOrders();
-    } catch (error) {
-      showToast('退款处理失败', 'error');
-    }
-  }
+const onStartDateChange = (e: any) => {
+  startDate.value = e.detail.value;
+};
+
+const onEndDateChange = (e: any) => {
+  endDate.value = e.detail.value;
+};
+
+const confirmDateRange = () => {
+  showDatePicker.value = false;
+  page.value = 1;
+  hasMore.value = true;
+  loadOrders();
+  loadStats();
 };
 
 onMounted(() => {
   loadOrders();
+  loadStats();
 });
 </script>
 
 <style lang="scss" scoped>
 .page {
   min-height: 100vh;
-  background: #f8fafc;
+  background: #f5f5f5;
   display: flex;
   flex-direction: column;
   padding-bottom: 200rpx;
 }
 
-/* 筛选栏 */
-.filter-header {
+/* Tab 栏 */
+.tab-bar {
   display: flex;
-  align-items: center;
+  background: #ffffff;
+  border-bottom: 1rpx solid #eeeeee;
+
+  .tab-item {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 24rpx 0 16rpx;
+    position: relative;
+
+    text {
+      font-size: 28rpx;
+      color: #666666;
+    }
+
+    &.active {
+      text {
+        color: #333333;
+        font-weight: 600;
+      }
+
+      .tab-line {
+        position: absolute;
+        bottom: 0;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 60rpx;
+        height: 4rpx;
+        background: #FFD100;
+        border-radius: 2rpx;
+      }
+    }
+  }
+}
+
+/* 时间筛选 */
+.time-filter {
   padding: 16rpx 24rpx;
-  gap: 12rpx;
   background: #ffffff;
-  border-bottom: 1rpx solid #e2e8f0;
+
+  .time-label {
+    font-size: 24rpx;
+    color: #999999;
+  }
 }
 
-.filter-tabs {
-  flex: 1;
+/* 统计卡片 */
+.stats-bar {
   display: flex;
-  gap: 10rpx;
-  
-  .filter-tab {
-    padding: 14rpx 24rpx;
-    background: #f8fafc;
-    border: 2rpx solid #e2e8f0;
-    border-radius: 20rpx;
-    font-size: 26rpx;
-    color: #64748b;
-    transition: all 0.15s;
-    
-    &.active {
-      background: #ecfdf5;
-      border-color: #10b981;
-      color: #10b981;
+  background: #ffffff;
+  padding: 20rpx 0;
+
+  .stat-item {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .stat-value {
+      font-size: 36rpx;
+      font-weight: 700;
+      color: #ff4d4f;
+      margin-bottom: 8rpx;
+    }
+
+    .stat-label {
+      font-size: 24rpx;
+      color: #333333;
     }
   }
 }
 
-.search-btn {
-  width: 72rpx;
-  height: 72rpx;
-  background: #ecfdf5;
-  border: 2rpx solid #10b981;
-  border-radius: 20rpx;
+/* 说明文字 */
+.tips-section {
+  background: #ffffff;
+  padding: 16rpx 24rpx;
+  margin-bottom: 16rpx;
+
+  .tips-text {
+    display: block;
+    font-size: 22rpx;
+    color: #999999;
+    line-height: 1.6;
+  }
+}
+
+/* 列表头部 */
+.list-header {
   display: flex;
   align-items: center;
-  justify-content: center;
-  
-  .search-icon {
-    width: 28rpx;
-    height: 28rpx;
-    border: 3rpx solid #10b981;
-    border-radius: 50%;
-    position: relative;
-    
-    &::after {
-      content: '';
-      position: absolute;
-      bottom: -6rpx;
-      right: -6rpx;
-      width: 10rpx;
-      height: 3rpx;
-      background: #10b981;
-      transform: rotate(45deg);
-    }
+  justify-content: space-between;
+  padding: 16rpx 24rpx;
+
+  .list-info {
+    font-size: 26rpx;
+    color: #999999;
+  }
+
+  .filter-btn {
+    display: flex;
+    align-items: center;
+    padding: 8rpx 16rpx;
+    background: #ffffff;
+    border-radius: 8rpx;
+    font-size: 26rpx;
+    color: #666666;
   }
 }
 
@@ -298,126 +436,99 @@ onMounted(() => {
 }
 
 .order-list {
-  padding: 16rpx 24rpx;
+  padding: 0 24rpx;
 }
 
 .order-card {
   background: #ffffff;
-  border: 1rpx solid #e2e8f0;
   border-radius: 12rpx;
-  margin-bottom: 10rpx;
-  overflow: hidden;
-  transition: transform 0.15s;
-  
-  &:active {
-    transform: scale(0.98);
+  margin-bottom: 16rpx;
+  padding: 20rpx;
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16rpx;
+
+    .order-time {
+      font-size: 28rpx;
+      color: #333333;
+    }
+
+    .order-status {
+      font-size: 28rpx;
+      font-weight: 500;
+    }
   }
-}
 
-.card-main {
-  display: flex;
-  padding: 14rpx 16rpx;
-}
+  .card-product {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16rpx;
 
-.card-left {
-  flex: 1;
-  min-width: 0;
-}
+    .product-img {
+      width: 120rpx;
+      height: 120rpx;
+      border-radius: 8rpx;
+      background: #f5f5f5;
+      margin-right: 20rpx;
+    }
 
-.card-right {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  justify-content: center;
-  margin-left: 12rpx;
-  min-width: 140rpx;
-}
+    .product-right {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      align-items: flex-end;
+
+      .product-amount {
+        font-size: 32rpx;
+        font-weight: 600;
+        color: #333333;
+        margin-bottom: 8rpx;
+      }
 
-.card-row {
-  display: flex;
-  align-items: center;
-  margin-bottom: 6rpx;
-  
-  &:last-child {
-    margin-bottom: 0;
+      .product-count {
+        font-size: 24rpx;
+        color: #999999;
+      }
+    }
   }
-}
 
-.order-no {
-  font-size: 24rpx;
-  color: #64748b;
-  font-family: monospace;
-  margin-right: 10rpx;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
+  .card-info {
+    margin-bottom: 16rpx;
 
-.order-meta {
-  font-size: 26rpx;
-  color: #1e293b;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
+    .info-line {
+      display: flex;
+      margin-bottom: 8rpx;
 
-.status-tag {
-  padding: 2rpx 10rpx;
-  border-radius: 4rpx;
-  font-size: 20rpx;
-  font-weight: 500;
-  flex-shrink: 0;
-  
-  &.completed {
-    background: #ecfdf5;
-    color: #10b981;
-  }
-  
-  &.refunding {
-    background: #fff7ed;
-    color: #f97316;
-  }
-  
-  &.processing {
-    background: #f0f9ff;
-    color: #0ea5e9;
-  }
-  
-  &.pending {
-    background: #faf5ff;
-    color: #a855f7;
-  }
-  
-  &.cancelled {
-    background: #fef2f2;
-    color: #ef4444;
+      .info-label {
+        font-size: 26rpx;
+        color: #666666;
+        margin-right: 8rpx;
+      }
+
+      .info-value {
+        font-size: 26rpx;
+        color: #333333;
+      }
+    }
   }
-}
 
-.amount {
-  font-size: 30rpx;
-  font-weight: 700;
-  color: #f97316;
-  margin-bottom: 4rpx;
-}
+  .card-action {
+    display: flex;
+    justify-content: flex-end;
 
-.time-text {
-  font-size: 20rpx;
-  color: #94a3b8;
-}
+    .view-btn {
+      padding: 10rpx 32rpx;
+      border: 1rpx solid #cccccc;
+      border-radius: 8rpx;
 
-.card-action {
-  padding: 8rpx 16rpx 14rpx;
-  display: flex;
-  justify-content: flex-end;
-  
-  .action-btn {
-    padding: 8rpx 20rpx;
-    background: #ef4444;
-    border-radius: 6rpx;
-    font-size: 22rpx;
-    font-weight: 500;
-    color: #fff;
+      text {
+        font-size: 26rpx;
+        color: #666666;
+      }
+    }
   }
 }
 
@@ -428,14 +539,14 @@ onMounted(() => {
   justify-content: center;
   gap: 12rpx;
   padding: 32rpx;
-  color: #94a3b8;
+  color: #999999;
   font-size: 24rpx;
-  
+
   .loading-spinner {
     width: 32rpx;
     height: 32rpx;
-    border: 3rpx solid #e2e8f0;
-    border-top-color: #10b981;
+    border: 3rpx solid #eeeeee;
+    border-top-color: #FFD100;
     border-radius: 50%;
     animation: spin 1s linear infinite;
   }
@@ -449,7 +560,7 @@ onMounted(() => {
   text-align: center;
   padding: 32rpx;
   font-size: 24rpx;
-  color: #cbd5e1;
+  color: #cccccc;
 }
 
 .empty-state {
@@ -457,56 +568,15 @@ onMounted(() => {
   flex-direction: column;
   align-items: center;
   padding: 100rpx 0;
-  
-  .empty-icon {
-    width: 120rpx;
-    height: 120rpx;
-    background: #f1f5f9;
-    border-radius: 24rpx;
-    margin-bottom: 20rpx;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    
-    .empty-icon-inner {
-      width: 48rpx;
-      height: 56rpx;
-      border: 4rpx solid #cbd5e1;
-      border-radius: 8rpx;
-      position: relative;
-      
-      &::before {
-        content: '';
-        position: absolute;
-        top: 14rpx;
-        left: 8rpx;
-        width: 20rpx;
-        height: 4rpx;
-        background: #cbd5e1;
-        border-radius: 2rpx;
-      }
-      
-      &::after {
-        content: '';
-        position: absolute;
-        top: 24rpx;
-        left: 8rpx;
-        width: 14rpx;
-        height: 4rpx;
-        background: #cbd5e1;
-        border-radius: 2rpx;
-      }
-    }
-  }
-  
+
   .empty-text {
     font-size: 28rpx;
-    color: #94a3b8;
+    color: #999999;
   }
 }
 
-/* 搜索弹窗 */
-.search-modal {
+/* 日期弹窗 */
+.date-modal {
   position: fixed;
   top: 0;
   left: 0;
@@ -525,39 +595,48 @@ onMounted(() => {
   background: #ffffff;
   border-radius: 20rpx;
   overflow: hidden;
-  
+
   .modal-header {
     padding: 24rpx;
-    border-bottom: 1rpx solid #f1f5f9;
-    
+    border-bottom: 1rpx solid #f5f5f5;
+
     .modal-title {
       font-size: 32rpx;
       font-weight: 600;
-      color: #1e293b;
+      color: #333333;
     }
   }
-  
+
   .modal-body {
     padding: 24rpx;
-    
-    .search-input {
-      width: 100%;
-      height: 88rpx;
-      background: #f8fafc;
-      border: 2rpx solid #e2e8f0;
-      border-radius: 12rpx;
-      padding: 0 20rpx;
-      font-size: 28rpx;
-      color: #1e293b;
-      box-sizing: border-box;
+
+    .date-row {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 20rpx 0;
+      border-bottom: 1rpx solid #f5f5f5;
+
+      text {
+        font-size: 28rpx;
+        color: #333333;
+      }
+
+      .picker-value {
+        font-size: 28rpx;
+        color: #333333;
+        padding: 8rpx 16rpx;
+        background: #f5f5f5;
+        border-radius: 8rpx;
+      }
     }
   }
-  
+
   .modal-footer {
     display: flex;
     padding: 16rpx 24rpx 24rpx;
     gap: 12rpx;
-    
+
     button {
       flex: 1;
       height: 80rpx;
@@ -565,25 +644,21 @@ onMounted(() => {
       font-size: 28rpx;
       font-weight: 500;
       border: none;
-      
+
       &::after {
         border: none;
       }
     }
-    
+
     .btn-cancel {
-      background: #f8fafc;
-      color: #64748b;
+      background: #f5f5f5;
+      color: #666666;
     }
-    
+
     .btn-confirm {
-      background: #10b981;
-      color: #fff;
+      background: #FFD100;
+      color: #333333;
     }
   }
 }
-
-.input-placeholder {
-  color: #cbd5e1;
-}
 </style>

+ 742 - 334
haha-admin-mp/src/pages/products/list.vue

@@ -1,416 +1,824 @@
 <template>
   <view class="page">
-    <!-- 导航栏 -->
-    <NavBar title="商品管理" :showBack="true" />
-    
-    <!-- 分类筛选 -->
-    <view class="filter-section">
-      <scroll-view class="category-scroll" scroll-x>
-        <view class="category-list">
-          <view 
-            class="category-item" 
-            :class="{ active: currentCategory === '' }"
-            @click="changeCategory('')"
-          >
-            <text>全部</text>
-          </view>
-          <view 
-            class="category-item" 
-            v-for="cat in categories" 
-            :key="cat.id"
-            :class="{ active: currentCategory === cat.name }"
-            @click="changeCategory(cat.name)"
-          >
-            <text>{{ cat.name }}</text>
-          </view>
+    <!-- 黄色头部区域 -->
+    <view class="header-section">
+      <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
+      <view class="header-nav">
+        <view class="back-btn" @click="goBack">
+          <text class="back-icon">←</text>
         </view>
-      </scroll-view>
+        <text class="header-title">{{ deviceId }}配置上架商品</text>
+        <view class="header-right"></view>
+      </view>
     </view>
-    
-    <!-- 商品列表 -->
-    <scroll-view 
-      class="product-scroll" 
-      scroll-y 
-      @scrolltolower="loadMore"
-    >
-      <view class="product-list">
-        <view 
-          class="product-card" 
-          v-for="product in productList" 
-          :key="product.id"
+
+    <!-- 楼层Tab切换 -->
+    <view class="floor-tabs">
+      <view class="floor-grid">
+        <view
+          class="floor-item"
+          v-for="floor in floors"
+          :key="floor.floorNumber"
+          :class="{ active: currentFloor === floor.floorNumber }"
+          @click="scrollToFloor(floor.floorNumber)"
         >
-          <view class="product-image">
-            <image v-if="product.image" :src="product.image" mode="aspectFill" />
-            <view v-else class="image-placeholder">
-              <view class="box-icon"></view>
-            </view>
-          </view>
-          
-          <view class="product-content">
-            <view class="product-info">
-              <text class="product-name">{{ product.name }}</text>
-              <text class="product-category">{{ product.category }}</text>
-            </view>
-            
-            <view class="product-footer">
-              <text class="product-price">¥{{ formatMoney(product.price) }}</text>
-              <view class="status-tag" :class="getStatusClass(product.status)">
-                <text>{{ getStatusText(product.status) }}</text>
+          <text class="floor-text">{{ floor.floorName }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 内容滚动区 -->
+    <scroll-view
+      class="content-scroll"
+      scroll-y
+      :scroll-into-view="scrollIntoViewId"
+      @scroll="onContentScroll"
+    >
+      <!-- 复制模板浮动按钮 -->
+      <view class="template-float" @click="copyTemplate">
+        <text class="template-float-text">复制模板 ❯❯</text>
+      </view>
+
+      <!-- 所有楼层 -->
+      <view
+        class="floor-section"
+        v-for="floor in floors"
+        :key="floor.floorNumber"
+        :id="'floor-' + floor.floorNumber"
+      >
+        <!-- 楼层提示 -->
+        <view class="floor-hint">
+          <text class="hint-text">{{ floor.floorName }}(点击商品图片可修改价格)</text>
+        </view>
+
+        <!-- 货道商品网格 -->
+        <view class="slot-grid">
+          <view
+            class="slot-card"
+            v-for="slot in floor.slots"
+            :key="slot.slotId"
+            :class="{ empty: !slot.productId }"
+          >
+            <!-- 有商品的货道 -->
+            <template v-if="slot.productId">
+              <view class="slot-image-wrap" @click="editPrice(slot)">
+                <image
+                  v-if="slot.productImage"
+                  class="slot-image"
+                  :src="slot.productImage"
+                  mode="aspectFill"
+                />
+                <view v-else class="slot-image-placeholder">
+                  <text class="placeholder-text">暂无图片</text>
+                </view>
+                <!-- 删除按钮 -->
+                <view class="slot-delete" @click.stop="removeProduct(slot)">
+                  <text class="delete-icon">×</text>
+                </view>
               </view>
-            </view>
-            
-            <view class="product-stats">
-              <view class="stat">
-                <text class="stat-label">库存</text>
-                <text class="stat-value">{{ product.totalStock }}</text>
+              <!-- 位置编号+价格条带 -->
+              <view class="slot-meta-bar">
+                <text class="meta-position">{{ slot.slotId }}</text>
+                <text class="meta-price">¥{{ formatPrice(slot.price) }}</text>
               </view>
-              <view class="stat">
-                <text class="stat-label">今日</text>
-                <text class="stat-value">{{ product.todaySales }}</text>
+              <view class="slot-info">
+                <text class="slot-name">{{ slot.productName }}</text>
+                <text class="slot-stock-label">预设库存数量</text>
+                <view class="slot-stock-control">
+                  <view class="stock-btn minus" @click="changeStock(slot, -1)">
+                    <text class="stock-btn-text">−</text>
+                  </view>
+                  <input
+                    class="stock-input"
+                    type="number"
+                    v-model.number="slot.stock"
+                    @blur="validateStock(slot)"
+                  />
+                  <view class="stock-btn plus" @click="changeStock(slot, 1)">
+                    <text class="stock-btn-text">+</text>
+                  </view>
+                </view>
               </view>
-              <view class="stat">
-                <text class="stat-label">月销</text>
-                <text class="stat-value">{{ product.monthSales }}</text>
+            </template>
+
+            <!-- 空货道 -->
+            <template v-else>
+              <view class="slot-add" @click="addProduct(slot)">
+                <text class="add-icon">+</text>
               </view>
-            </view>
+            </template>
           </view>
         </view>
       </view>
-      
-      <view class="loading-more" v-if="loading">
-        <view class="loading-spinner"></view>
-        <text>加载中...</text>
-      </view>
-      
-      <view class="no-more" v-if="!hasMore && productList.length > 0">
-        <text>— 没有更多了 —</text>
+
+      <!-- 底部留白 -->
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+    <!-- 底部保存按钮 -->
+    <view class="footer-section">
+      <view class="save-btn" @click="saveConfig">
+        <text class="save-btn-text">保存</text>
       </view>
-      
-      <view class="empty-state" v-if="!loading && productList.length === 0">
-        <view class="empty-icon">
-          <view class="empty-icon-inner"></view>
+    </view>
+
+    <!-- 价格编辑弹窗 -->
+    <view class="price-modal" v-if="showPriceModal" @click="closePriceModal">
+      <view class="price-modal-content" @click.stop>
+        <view class="price-modal-header">
+          <text class="price-modal-title">修改价格</text>
+          <text class="price-modal-close" @click="closePriceModal">×</text>
+        </view>
+        <view class="price-modal-body">
+          <text class="price-modal-label">商品价格</text>
+          <view class="price-input-wrap">
+            <text class="price-symbol">¥</text>
+            <input
+              class="price-input"
+              type="digit"
+              v-model="editingPrice"
+              placeholder="请输入价格"
+              focus
+            />
+          </view>
+        </view>
+        <view class="price-modal-footer">
+          <view class="price-modal-btn cancel" @click="closePriceModal">
+            <text>取消</text>
+          </view>
+          <view class="price-modal-btn confirm" @click="confirmPrice">
+            <text>确定</text>
+          </view>
         </view>
-        <text class="empty-text">暂无商品数据</text>
       </view>
-    </scroll-view>
+    </view>
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
-import NavBar from '@/components/NavBar.vue';
-import { getProductList } from '@/api/product';
-import { ProductStatusText, ProductStatusColor } from '@/utils/constants';
-import { formatMoney as formatMoneyUtil } from '@/utils/common';
-
-const productList = ref<any[]>([]);
-const categories = ref<any[]>([]);
-const currentCategory = ref('');
-const loading = ref(false);
-const hasMore = ref(true);
-const page = ref(1);
-const pageSize = 10;
-
-const formatMoney = formatMoneyUtil;
-
-const getStatusText = (status: number) => ProductStatusText[status] || '未知';
-
-const getStatusClass = (status: number) => {
-  if (status === 1) return 'active';
-  return 'inactive';
+import { ref, computed, onMounted } from 'vue';
+import { formatMoney } from '@/utils/common';
+import { getDeviceProductConfig, saveDeviceProductConfig } from '@/api/device';
+
+const statusBarHeight = ref(44);
+
+// 获取设备ID
+const deviceId = ref('');
+
+// 楼层数据
+interface SlotItem {
+  slotId: string;
+  productId?: string;
+  productName?: string;
+  productImage?: string;
+  price?: number;
+  stock?: number;
+}
+
+interface FloorItem {
+  floorNumber: number;
+  floorLabel: string;
+  floorName: string;
+  slots: SlotItem[];
+}
+
+const floors = ref<FloorItem[]>([]);
+const currentFloor = ref(1);
+const scrollIntoViewId = ref('');
+
+// 价格编辑弹窗
+const showPriceModal = ref(false);
+const editingPrice = ref('');
+const editingSlot = ref<SlotItem | null>(null);
+
+// 初始化状态栏高度
+const initStatusBar = () => {
+  try {
+    const systemInfo = uni.getSystemInfoSync();
+    statusBarHeight.value = systemInfo.statusBarHeight || 44;
+  } catch (e) {
+    statusBarHeight.value = 44;
+  }
+};
+
+// 生成Mock数据
+const generateMockData = () => {
+  const floorConfigs = [
+    { floorNumber: 1, floorLabel: '第1层\n(顶层)', floorName: '第1层', slotCount: 3 },
+    { floorNumber: 2, floorLabel: '第2层', floorName: '第2层', slotCount: 5 },
+    { floorNumber: 3, floorLabel: '第3层', floorName: '第3层', slotCount: 5 },
+    { floorNumber: 4, floorLabel: '第4层', floorName: '第4层', slotCount: 5 },
+    { floorNumber: 5, floorLabel: '第5层\n(底层)', floorName: '第5层', slotCount: 3 }
+  ];
+
+  const mockProducts = [
+    { id: 'P001', name: '白象红烧牛肉面面饼120g', price: 500, image: '' },
+    { id: 'P002', name: '白象老坛酸菜牛肉面面饼1...', price: 500, image: '' },
+    { id: 'P003', name: '招牌卤肉饭(盖浇)', price: 1500, image: '' },
+    { id: 'P004', name: '土豆牛腩饭(盖浇)', price: 1800, image: '' },
+    { id: 'P005', name: '辣椒炒肉饭(盖浇)', price: 1500, image: '' },
+    { id: 'P006', name: '农家一碗香饭(盖浇)', price: 1500, image: '' },
+    { id: 'P007', name: '白米饭', price: 100, image: '' },
+    { id: 'P008', name: '可口可乐330ml', price: 350, image: '' },
+    { id: 'P009', name: '矿泉水550ml', price: 200, image: '' },
+    { id: 'P010', name: '薯片40g', price: 680, image: '' }
+  ];
+
+  let productIndex = 0;
+
+  floors.value = floorConfigs.map(config => {
+    const slots: SlotItem[] = [];
+    for (let i = 1; i <= config.slotCount; i++) {
+      const slotId = `${config.floorNumber}-${i}`;
+      // 部分货道有商品,部分为空
+      if (productIndex < mockProducts.length && Math.random() > 0.2) {
+        const product = mockProducts[productIndex++];
+        slots.push({
+          slotId,
+          productId: product.id,
+          productName: product.name,
+          productImage: product.image,
+          price: product.price,
+          stock: Math.floor(Math.random() * 10)
+        });
+      } else {
+        slots.push({ slotId });
+      }
+    }
+    return {
+      floorNumber: config.floorNumber,
+      floorLabel: config.floorLabel,
+      floorName: config.floorName,
+      slots
+    };
+  });
+};
+
+// 格式化价格(分转元)
+const formatPrice = (price?: number) => {
+  if (price === undefined || price === null) return '0.00';
+  return formatMoney(price);
+};
+
+// 返回上一页
+const goBack = () => {
+  uni.navigateBack({ delta: 1 });
+};
+
+// 切换楼层(滚动定位)
+const scrollToFloor = (floorNumber: number) => {
+  currentFloor.value = floorNumber;
+  scrollIntoViewId.value = 'floor-' + floorNumber;
+  // 清除scrollIntoViewId,以便下次能再次触发
+  setTimeout(() => {
+    scrollIntoViewId.value = '';
+  }, 300);
+};
+
+// 监听滚动,更新当前楼层
+const onContentScroll = (e: any) => {
+  // 简化处理:根据粗略的楼层高度估算当前楼层
+  const scrollTop = e.detail.scrollTop;
+  const approximateFloorHeight = 400;
+  const floorIndex = Math.floor(scrollTop / approximateFloorHeight);
+  if (floorIndex >= 0 && floorIndex < floors.value.length) {
+    currentFloor.value = floors.value[floorIndex].floorNumber;
+  }
 };
 
-const changeCategory = (category: string) => {
-  currentCategory.value = category;
-  page.value = 1;
-  hasMore.value = true;
-  loadProducts();
+// 修改库存
+const changeStock = (slot: SlotItem, delta: number) => {
+  if (slot.stock === undefined) slot.stock = 0;
+  slot.stock = Math.max(0, slot.stock + delta);
 };
 
-const loadProducts = async () => {
-  loading.value = true;
+// 校验库存
+const validateStock = (slot: SlotItem) => {
+  if (slot.stock === undefined || slot.stock === null || isNaN(slot.stock)) {
+    slot.stock = 0;
+  }
+  slot.stock = Math.max(0, Math.floor(slot.stock));
+};
+
+// 删除商品
+const removeProduct = (slot: SlotItem) => {
+  uni.showModal({
+    title: '提示',
+    content: '确定要移除该商品吗?',
+    success: (res) => {
+      if (res.confirm) {
+        slot.productId = undefined;
+        slot.productName = undefined;
+        slot.productImage = undefined;
+        slot.price = undefined;
+        slot.stock = undefined;
+      }
+    }
+  });
+};
+
+// 添加商品
+const addProduct = (slot: SlotItem) => {
+  uni.showToast({ title: '选择商品功能开发中', icon: 'none' });
+  // TODO: 跳转到商品选择页面
+  // uni.navigateTo({ url: `/pages/products/select?deviceId=${deviceId.value}&slotId=${slot.slotId}` });
+};
+
+// 编辑价格(后端单位:元)
+const editPrice = (slot: SlotItem) => {
+  editingSlot.value = slot;
+  editingPrice.value = slot.price ? String(slot.price) : '';
+  showPriceModal.value = true;
+};
+
+// 关闭价格弹窗
+const closePriceModal = () => {
+  showPriceModal.value = false;
+  editingSlot.value = null;
+  editingPrice.value = '';
+};
+
+// 确认价格(单位:元)
+const confirmPrice = () => {
+  const price = parseFloat(editingPrice.value);
+  if (isNaN(price) || price < 0) {
+    uni.showToast({ title: '请输入有效的价格', icon: 'none' });
+    return;
+  }
+  if (editingSlot.value) {
+    editingSlot.value.price = price;
+  }
+  closePriceModal();
+};
+
+// 复制模板
+const copyTemplate = () => {
+  uni.showToast({ title: '复制模板功能开发中', icon: 'none' });
+};
+
+// 加载商品配置
+const loadProductConfig = async () => {
+  if (!deviceId.value) return;
   try {
-    const res = await getProductList({
-      page: page.value,
-      pageSize,
-      category: currentCategory.value
-    });
-    
-    if (page.value === 1) {
-      productList.value = res.list;
+    uni.showLoading({ title: '加载中...', mask: true });
+    const res = await getDeviceProductConfig(deviceId.value);
+    uni.hideLoading();
+    if (res && res.floors && res.floors.length > 0) {
+      floors.value = res.floors;
     } else {
-      productList.value = [...productList.value, ...res.list];
+      generateMockData();
     }
-    
-    hasMore.value = productList.value.length < res.total;
   } catch (error) {
-    console.error('加载商品列表失败', error);
-  } finally {
-    loading.value = false;
+    uni.hideLoading();
+    uni.showToast({ title: '加载失败', icon: 'none' });
+    generateMockData();
   }
 };
 
-const loadMore = () => {
-  if (loading.value || !hasMore.value) return;
-  page.value++;
-  loadProducts();
+// 保存配置
+const saveConfig = async () => {
+  if (!deviceId.value) return;
+  try {
+    uni.showLoading({ title: '保存中...', mask: true });
+    await saveDeviceProductConfig(deviceId.value, { floors: floors.value });
+    uni.hideLoading();
+    uni.showToast({ title: '保存成功', icon: 'success' });
+  } catch (error) {
+    uni.hideLoading();
+    uni.showToast({ title: '保存失败', icon: 'none' });
+  }
 };
 
+// 页面加载
 onMounted(() => {
-  loadProducts();
+  initStatusBar();
+  // 获取页面参数
+  const pages = getCurrentPages();
+  const currentPage = pages[pages.length - 1] as any;
+  const options = currentPage?.options || {};
+  deviceId.value = options.deviceId || 'B150360';
+  // 加载真实接口数据
+  loadProductConfig();
 });
 </script>
 
 <style lang="scss" scoped>
 .page {
-  min-height: 100vh;
-  background: #f8fafc;
+  height: 100vh;
+  background: #f5f5f5;
   display: flex;
   flex-direction: column;
+  overflow: hidden;
 }
 
-/* 分类筛选 */
-.filter-section {
-  padding: 12rpx 0;
-  background: #ffffff;
-  border-bottom: 1rpx solid #e2e8f0;
-}
-
-.category-scroll {
-  white-space: nowrap;
-}
-
-.category-list {
-  display: inline-flex;
-  padding: 0 24rpx;
-  gap: 10rpx;
-  
-  .category-item {
-    padding: 12rpx 24rpx;
-    background: #f8fafc;
-    border: 2rpx solid #e2e8f0;
-    border-radius: 20rpx;
-    font-size: 26rpx;
-    color: #64748b;
-    transition: all 0.15s;
-    
-    &.active {
-      background: #ecfdf5;
-      border-color: #10b981;
-      color: #10b981;
-    }
+/* 黄色头部区域 */
+.header-section {
+  background: #FFD93D;
+  flex-shrink: 0;
+}
+
+.status-bar {
+  width: 100%;
+}
+
+.header-nav {
+  height: 88rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 20rpx;
+}
+
+.back-btn {
+  width: 60rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #333;
+  font-weight: bold;
+}
+
+.header-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  text-align: center;
+  flex: 1;
+}
+
+.header-right {
+  width: 60rpx;
+}
+
+/* 楼层Tab */
+.floor-tabs {
+  background: #FFD93D;
+  padding: 16rpx 20rpx;
+  flex-shrink: 0;
+}
+
+.floor-grid {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 12rpx;
+}
+
+.floor-item {
+  padding: 20rpx 12rpx;
+  background: rgba(255, 255, 255, 0.5);
+  border-radius: 12rpx;
+  text-align: center;
+  border: 3rpx solid transparent;
+  transition: all 0.2s;
+
+  &.active {
+    background: #fff;
+    border-color: #333;
+    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
   }
 }
 
-/* 商品列表 */
-.product-scroll {
+.floor-text {
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #333;
+  line-height: 1.2;
+}
+
+/* 内容滚动区 */
+.content-scroll {
   flex: 1;
   height: 0;
+  padding: 20rpx;
+  position: relative;
 }
 
-.product-list {
-  padding: 16rpx 24rpx;
+/* 复制模板浮动按钮 */
+.template-float {
+  position: absolute;
+  top: 20rpx;
+  right: 20rpx;
+  z-index: 10;
+  background: rgba(255, 217, 61, 0.9);
+  padding: 10rpx 20rpx;
+  border-radius: 24rpx;
 }
 
-.product-card {
+.template-float-text {
+  font-size: 24rpx;
+  color: #333;
+}
+
+/* 楼层区块 */
+.floor-section {
+  margin-bottom: 30rpx;
+}
+
+/* 楼层提示 */
+.floor-hint {
+  margin-bottom: 16rpx;
+}
+
+.hint-text {
+  font-size: 26rpx;
+  color: #999;
+}
+
+/* 货道网格 */
+.slot-grid {
   display: flex;
-  background: #ffffff;
-  border: 1rpx solid #e2e8f0;
-  border-radius: 16rpx;
-  margin-bottom: 12rpx;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.slot-card {
+  width: calc((100% - 32rpx) / 3);
+  background: #fff;
+  border-radius: 12rpx;
   overflow: hidden;
-  transition: transform 0.15s;
-  
-  &:active {
-    transform: scale(0.98);
-  }
-  
-  .product-image {
-    width: 160rpx;
-    height: 160rpx;
-    flex-shrink: 0;
-    
-    image {
-      width: 100%;
-      height: 100%;
-      object-fit: cover;
-    }
-    
-    .image-placeholder {
-      width: 100%;
-      height: 100%;
-      background: #f8fafc;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      
-      .box-icon {
-        width: 32rpx;
-        height: 32rpx;
-        background: #e2e8f0;
-        border-radius: 6rpx;
-        position: relative;
-        
-        &::before {
-          content: '';
-          position: absolute;
-          top: -8rpx;
-          left: 50%;
-          transform: translateX(-50%);
-          width: 0;
-          height: 0;
-          border-left: 10rpx solid transparent;
-          border-right: 10rpx solid transparent;
-          border-bottom: 10rpx solid #e2e8f0;
-        }
-      }
-    }
-  }
-  
-  .product-content {
-    flex: 1;
-    padding: 16rpx 20rpx;
-    display: flex;
-    flex-direction: column;
-    justify-content: space-between;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
+
+  &.empty {
+    background: #fff;
+    min-height: 320rpx;
   }
 }
 
-.product-info {
-  .product-name {
-    display: block;
-    font-size: 28rpx;
-    font-weight: 600;
-    color: #1e293b;
-    margin-bottom: 4rpx;
-  }
-  
-  .product-category {
-    font-size: 22rpx;
-    color: #94a3b8;
-  }
+/* 有商品货道 */
+.slot-image-wrap {
+  position: relative;
+  width: 100%;
+  height: 200rpx;
+  background: #f8f8f8;
 }
 
-.product-footer {
+.slot-image {
+  width: 100%;
+  height: 100%;
+}
+
+.slot-image-placeholder {
+  width: 100%;
+  height: 100%;
   display: flex;
   align-items: center;
-  gap: 10rpx;
-  margin: 10rpx 0;
-  
-  .product-price {
-    font-size: 28rpx;
-    font-weight: 700;
-    color: #f97316;
-  }
-  
-  .status-tag {
-    padding: 4rpx 12rpx;
-    border-radius: 6rpx;
-    font-size: 20rpx;
-    
-    &.active {
-      background: #ecfdf5;
-      color: #10b981;
-    }
-    
-    &.inactive {
-      background: #fff7ed;
-      color: #f97316;
-    }
-  }
+  justify-content: center;
+  background: #f0f0f0;
 }
 
-.product-stats {
+.placeholder-text {
+  font-size: 24rpx;
+  color: #bbb;
+}
+
+.slot-delete {
+  position: absolute;
+  top: 6rpx;
+  right: 6rpx;
+  width: 36rpx;
+  height: 36rpx;
+  background: rgba(255, 255, 255, 0.9);
+  border-radius: 50%;
   display: flex;
-  gap: 20rpx;
-  
-  .stat {
-    .stat-label {
-      font-size: 20rpx;
-      color: #94a3b8;
-      margin-right: 4rpx;
-    }
-    
-    .stat-value {
-      font-size: 22rpx;
-      color: #475569;
-      font-weight: 500;
-    }
-  }
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.15);
+}
+
+.delete-icon {
+  font-size: 26rpx;
+  color: #999;
+  line-height: 1;
 }
 
-/* 加载状态 */
-.loading-more {
+/* 位置编号+价格条带 */
+.slot-meta-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #ff4d4f;
+  padding: 6rpx 10rpx;
+}
+
+.meta-position {
+  font-size: 20rpx;
+  color: #fff;
+  font-weight: 600;
+}
+
+.meta-price {
+  font-size: 20rpx;
+  color: #fff;
+  font-weight: 600;
+}
+
+.slot-info {
+  padding: 10rpx;
+}
+
+.slot-name {
+  display: block;
+  font-size: 22rpx;
+  color: #333;
+  line-height: 1.4;
+  height: 62rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  margin-bottom: 6rpx;
+}
+
+.slot-stock-label {
+  display: block;
+  font-size: 18rpx;
+  color: #999;
+  margin-bottom: 6rpx;
+}
+
+.slot-stock-control {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.stock-btn {
+  width: 40rpx;
+  height: 40rpx;
   display: flex;
   align-items: center;
   justify-content: center;
-  gap: 12rpx;
-  padding: 32rpx;
-  color: #94a3b8;
-  font-size: 24rpx;
-  
-  .loading-spinner {
-    width: 32rpx;
-    height: 32rpx;
-    border: 3rpx solid #e2e8f0;
-    border-top-color: #10b981;
-    border-radius: 50%;
-    animation: spin 1s linear infinite;
-  }
 }
 
-@keyframes spin {
-  to { transform: rotate(360deg); }
+.stock-btn-text {
+  font-size: 28rpx;
+  color: #999;
+  line-height: 1;
 }
 
-.no-more {
+.stock-input {
+  flex: 1;
+  height: 40rpx;
   text-align: center;
-  padding: 32rpx;
   font-size: 24rpx;
-  color: #cbd5e1;
+  color: #333;
 }
 
-.empty-state {
+/* 空货道 */
+.slot-add {
+  width: 100%;
+  height: 320rpx;
   display: flex;
-  flex-direction: column;
   align-items: center;
-  padding: 100rpx 0;
-  
-  .empty-icon {
-    width: 120rpx;
-    height: 120rpx;
-    background: #f1f5f9;
-    border-radius: 24rpx;
-    margin-bottom: 20rpx;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    
-    .empty-icon-inner {
-      width: 40rpx;
-      height: 28rpx;
-      background: #cbd5e1;
-      border-radius: 6rpx;
-      position: relative;
-      
-      &::before {
-        content: '';
-        position: absolute;
-        top: -10rpx;
-        left: 50%;
-        transform: translateX(-50%);
-        width: 0;
-        height: 0;
-        border-left: 12rpx solid transparent;
-        border-right: 12rpx solid transparent;
-        border-bottom: 10rpx solid #cbd5e1;
-      }
+  justify-content: center;
+  border: 2rpx dashed #e0e0e0;
+  border-radius: 12rpx;
+  box-sizing: border-box;
+}
+
+.add-icon {
+  font-size: 60rpx;
+  color: #ddd;
+  line-height: 1;
+}
+
+/* 底部留白 */
+.bottom-spacer {
+  height: 40rpx;
+}
+
+/* 底部保存 */
+.footer-section {
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  background: #fff;
+  border-top: 1rpx solid #eee;
+  flex-shrink: 0;
+}
+
+.save-btn {
+  height: 88rpx;
+  background: #FFD93D;
+  border-radius: 44rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.save-btn-text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+/* 价格弹窗 */
+.price-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.price-modal-content {
+  width: 560rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  overflow: hidden;
+}
+
+.price-modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.price-modal-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.price-modal-close {
+  font-size: 40rpx;
+  color: #999;
+  line-height: 1;
+}
+
+.price-modal-body {
+  padding: 40rpx 30rpx;
+}
+
+.price-modal-label {
+  display: block;
+  font-size: 28rpx;
+  color: #666;
+  margin-bottom: 20rpx;
+}
+
+.price-input-wrap {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  padding: 20rpx;
+  background: #f5f5f5;
+  border-radius: 12rpx;
+}
+
+.price-symbol {
+  font-size: 32rpx;
+  color: #333;
+  font-weight: 600;
+}
+
+.price-input {
+  flex: 1;
+  font-size: 32rpx;
+  color: #333;
+}
+
+.price-modal-footer {
+  display: flex;
+  border-top: 1rpx solid #eee;
+}
+
+.price-modal-btn {
+  flex: 1;
+  height: 96rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  text {
+    font-size: 30rpx;
+  }
+
+  &.cancel {
+    border-right: 1rpx solid #eee;
+
+    text {
+      color: #666;
     }
   }
-  
-  .empty-text {
-    font-size: 28rpx;
-    color: #94a3b8;
+
+  &.confirm {
+    text {
+      color: #FFD93D;
+      font-weight: 600;
+    }
   }
 }
-</style>
+</style>

+ 2 - 2
haha-admin-mp/src/pages/shop/detail.vue

@@ -62,7 +62,7 @@
           <view class="device-item" v-for="device in shop.devices" :key="device.id">
             <view class="device-info">
               <text class="device-name">{{ device.name }}</text>
-              <text class="device-no">{{ device.deviceNo }}</text>
+              <text class="device-no">{{ device.deviceId }}</text>
             </view>
             <view class="device-status" :class="getDeviceStatusClass(device.status)">
               <view class="status-dot"></view>
@@ -114,7 +114,7 @@ const loadDetail = async () => {
   }
   
   try {
-    shop.value = await getShopDetail(Number(id));
+    shop.value = await getShopDetail(id);
   } catch (error) {
     showToast('加载门店详情失败');
     navigateBack();

+ 1 - 1
haha-admin-mp/src/pages/shop/list.vue

@@ -130,7 +130,7 @@ const loadMore = () => {
   loadShops();
 };
 
-const goDetail = (shopId: number) => {
+const goDetail = (shopId: string) => {
   uni.navigateTo({ url: `/pages/shop/detail?id=${shopId}` });
 };
 

+ 5 - 3
haha-admin-mp/src/utils/common.ts

@@ -3,10 +3,12 @@
  */
 
 /**
- * 格式化金额
+ * 格式化金额(后端返回单位:元)
  */
-export function formatMoney(amount: number): string {
-  return (amount / 100).toFixed(2);
+export function formatMoney(amount: number | string): string {
+  if (amount === null || amount === undefined || amount === '') return '0.00';
+  const num = typeof amount === 'string' ? parseFloat(amount) : amount;
+  return num.toFixed(2);
 }
 
 /**

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

@@ -10,10 +10,12 @@ import com.haha.common.vo.Result;
 import com.haha.entity.Device;
 import com.haha.service.DeviceService;
 import com.haha.service.DoorRecordService;
+import com.haha.service.LayerTemplateService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -27,6 +29,7 @@ public class DeviceController {
 
     private final DeviceService deviceService;
     private final DoorRecordService doorRecordService;
+    private final LayerTemplateService layerTemplateService;
 
     /**
      * 分页查询设备列表
@@ -134,4 +137,33 @@ public class DeviceController {
         IPage<com.haha.entity.DoorRecord> recordPage = doorRecordService.getDeviceRecordsByPage(deviceId, page, pageSize);
         return Result.success("查询成功", PageResult.of(recordPage));
     }
+
+    /**
+     * 获取设备商品配置
+     * @param deviceId 设备编号
+     * @return 商品配置数据
+     */
+    @RequirePermission("device:read")
+    @GetMapping("/{deviceId}/product-config")
+    public Result<Map<String, Object>> getProductConfig(@PathVariable String deviceId) {
+        Map<String, Object> config = layerTemplateService.getProductConfig(deviceId);
+        return Result.success("查询成功", config);
+    }
+
+    /**
+     * 保存设备商品配置
+     * @param deviceId 设备编号
+     * @param data 配置数据
+     * @return 操作结果
+     */
+    @RequirePermission("device:update")
+    @Log(module = "设备管理", operation = OperationType.UPDATE, summary = "保存设备商品配置")
+    @PostMapping("/{deviceId}/product-config")
+    public Result<Void> saveProductConfig(@PathVariable String deviceId, @RequestBody Map<String, Object> data) {
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> floors = (List<Map<String, Object>>) data.get("floors");
+        
+        layerTemplateService.saveProductConfig(deviceId, floors);
+        return Result.success("保存成功", null);
+    }
 }

+ 11 - 5
haha-admin/src/main/java/com/haha/admin/controller/OrderController.java

@@ -35,9 +35,10 @@ public class OrderController {
     @RequirePermission("order:read")
     @GetMapping("/list")
     public Result<PageResult<Order>> list(OrderQueryDTO queryDTO) {
-        log.info("订单列表查询参数:page={}, pageSize={}, orderNo={}, deviceId={}", 
+        log.info("订单列表查询参数:page={}, pageSize={}, orderNo={}, deviceId={}, status={}, statusList={}", 
                 queryDTO.getPage(), queryDTO.getPageSize(), 
-                queryDTO.getOrderNo(), queryDTO.getDeviceId());
+                queryDTO.getOrderNo(), queryDTO.getDeviceId(),
+                queryDTO.getStatus(), queryDTO.getStatusList());
         
         queryDTO.validate();
         
@@ -49,7 +50,8 @@ public class OrderController {
                 queryDTO.getPayStatus(),
                 queryDTO.getStatus(),
                 queryDTO.getStartDate(),
-                queryDTO.getEndDate()
+                queryDTO.getEndDate(),
+                queryDTO.getStatusList()
         );
 
         return Result.success("查询成功", PageResult.of(orderPage));
@@ -72,12 +74,16 @@ public class OrderController {
 
     /**
      * 获取订单统计数据
+     * @param startDate 开始日期
+     * @param endDate 结束日期
      * @return 统计数据
      */
     @RequirePermission("order:read")
     @GetMapping("/statistics")
-    public Result<Map<String, Object>> getStatistics() {
-        Map<String, Object> statistics = orderService.getStatistics();
+    public Result<Map<String, Object>> getStatistics(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        Map<String, Object> statistics = orderService.getStatistics(startDate, endDate);
         return Result.success("查询成功", statistics);
     }
 

+ 7 - 0
haha-admin/src/main/java/com/haha/admin/dto/OrderQueryDTO.java

@@ -3,6 +3,8 @@ package com.haha.admin.dto;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.util.List;
+
 /**
  * 订单查询DTO
  */
@@ -30,6 +32,11 @@ public class OrderQueryDTO extends PageQueryDTO {
      */
     private Integer status;
     
+    /**
+     * 订单状态列表(用于多状态查询,如异常订单)
+     */
+    private List<Integer> statusList;
+    
     /**
      * 开始日期
      */

+ 6 - 0
haha-common/src/main/java/com/haha/common/vo/OrderVO.java

@@ -42,6 +42,12 @@ public class OrderVO {
     /** 门店名称 */
     private String shopName;
 
+    /** 设备名称 */
+    private String deviceName;
+
+    /** 支付方式 */
+    private String payType;
+
     /** 用户消费标签:new-新用户, regular-老用户, frequent-常客 */
     private String userTag;
 

+ 66 - 0
haha-mapper/src/main/java/com/haha/mapper/OrderMapper.java

@@ -27,6 +27,72 @@ public interface OrderMapper extends BaseMapper<Order> {
             "</script>")
     BigDecimal sumCompletedOrderAmount(@Param("startDate") LocalDateTime startDate);
 
+    /**
+     * 按支付渠道统计订单金额
+     *
+     * @param startDate 开始时间(null表示统计全部)
+     * @param endDate 结束时间(null表示统计全部)
+     * @return 每个渠道的金额汇总(channel, amount)
+     */
+    @Select("<script>" +
+            "SELECT pay_channel as channel, COALESCE(SUM(paid_amount), 0) as amount FROM t_order " +
+            "WHERE status = 2 AND pay_status = 'PAID' " +
+            "<if test='startDate != null'>AND create_time &gt;= #{startDate} </if>" +
+            "<if test='endDate != null'>AND create_time &lt;= #{endDate} </if>" +
+            "GROUP BY pay_channel" +
+            "</script>")
+    List<Map<String, Object>> sumAmountByChannel(
+            @Param("startDate") LocalDateTime startDate,
+            @Param("endDate") LocalDateTime endDate);
+
+    /**
+     * 统计指定时间范围内的订单总数
+     *
+     * @param startDate 开始时间(null表示统计全部)
+     * @param endDate 结束时间(null表示统计全部)
+     * @return 订单总数
+     */
+    @Select("<script>" +
+            "SELECT COUNT(*) FROM t_order WHERE 1=1 " +
+            "<if test='startDate != null'>AND create_time &gt;= #{startDate} </if>" +
+            "<if test='endDate != null'>AND create_time &lt;= #{endDate} </if>" +
+            "</script>")
+    Long countByDateRange(
+            @Param("startDate") LocalDateTime startDate,
+            @Param("endDate") LocalDateTime endDate);
+
+    /**
+     * 统计指定时间范围内的已完成订单数
+     *
+     * @param startDate 开始时间(null表示统计全部)
+     * @param endDate 结束时间(null表示统计全部)
+     * @return 已完成订单数
+     */
+    @Select("<script>" +
+            "SELECT COUNT(*) FROM t_order WHERE status = 2 " +
+            "<if test='startDate != null'>AND create_time &gt;= #{startDate} </if>" +
+            "<if test='endDate != null'>AND create_time &lt;= #{endDate} </if>" +
+            "</script>")
+    Long countCompletedByDateRange(
+            @Param("startDate") LocalDateTime startDate,
+            @Param("endDate") LocalDateTime endDate);
+
+    /**
+     * 统计指定时间范围内的已退款订单数
+     *
+     * @param startDate 开始时间(null表示统计全部)
+     * @param endDate 结束时间(null表示统计全部)
+     * @return 已退款订单数
+     */
+    @Select("<script>" +
+            "SELECT COUNT(*) FROM t_order WHERE pay_status = 'REFUND' " +
+            "<if test='startDate != null'>AND create_time &gt;= #{startDate} </if>" +
+            "<if test='endDate != null'>AND create_time &lt;= #{endDate} </if>" +
+            "</script>")
+    Long countRefundedByDateRange(
+            @Param("startDate") LocalDateTime startDate,
+            @Param("endDate") LocalDateTime endDate);
+
     /**
      * 批量查询用户历史已支付订单数
      *

+ 17 - 0
haha-service/src/main/java/com/haha/service/LayerTemplateService.java

@@ -88,4 +88,21 @@ public interface LayerTemplateService extends IService<LayerTemplate> {
      * @return 同步结果统计信息
      */
     Map<String, Object> batchSync(List<String> deviceIds);
+
+    /**
+     * 获取设备商品配置(转换为前端格式)
+     *
+     * @param deviceId 设备编号
+     * @return 商品配置数据,包含floors列表
+     */
+    Map<String, Object> getProductConfig(String deviceId);
+
+    /**
+     * 保存设备商品配置
+     *
+     * @param deviceId 设备编号
+     * @param floors   楼层配置数据(前端格式)
+     * @return 是否成功
+     */
+    boolean saveProductConfig(String deviceId, List<Map<String, Object>> floors);
 }

+ 6 - 2
haha-service/src/main/java/com/haha/service/OrderService.java

@@ -22,10 +22,12 @@ public interface OrderService extends IService<Order> {
      * @param status   订单状态
      * @param startDate 开始日期
      * @param endDate   结束日期
+     * @param statusList 订单状态列表(多状态查询)
      * @return 分页结果
      */
     IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId, 
-                         String payStatus, Integer status, String startDate, String endDate);
+                         String payStatus, Integer status, String startDate, String endDate,
+                         List<Integer> statusList);
     
     /**
      * 获取订单详情(带标签填充)
@@ -46,9 +48,11 @@ public interface OrderService extends IService<Order> {
     /**
      * 获取订单统计数据
      *
+     * @param startDate 开始日期(可选)
+     * @param endDate 结束日期(可选)
      * @return 统计数据
      */
-    Map<String, Object> getStatistics();
+    Map<String, Object> getStatistics(String startDate, String endDate);
     
     /**
      * 订单退款

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

@@ -189,7 +189,7 @@ public class DeviceAlertServiceImpl extends ServiceImpl<DeviceAlertRecordMapper,
             String offlineKey = String.format(RedisConstants.DEVICE_LAST_ONLINE_KEY, deviceId);
             Boolean hasOfflineMark = stringRedisTemplate.hasKey(offlineKey);
             if (!Boolean.TRUE.equals(hasOfflineMark)) {
-                log.debug("[设备告警] 设备无离线标记,跳过恢复上线通知 - deviceId: {}", deviceId);
+                // log.debug("[设备告警] 设备无离线标记,跳过恢复上线通知 - deviceId: {}", deviceId);
                 return;
             }
 

+ 145 - 0
haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java

@@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.haha.common.dto.FloorConfig;
+import com.haha.common.dto.GoodsItem;
 import com.haha.common.dto.LayerTemplateCreateDTO;
 import com.haha.common.dto.LayerTemplateUpdateDTO;
 import com.haha.common.constant.CommonConstants;
@@ -24,8 +25,10 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -694,4 +697,146 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
             return String.format("%.2fmin", costTimeMs / 60000.0);
         }
     }
+
+    /**
+     * 获取设备商品配置(转换为前端格式)
+     */
+    @Override
+    public Map<String, Object> getProductConfig(String deviceId) {
+        log.info("获取设备商品配置: deviceId={}", deviceId);
+
+        LayerTemplate template = getByDeviceId(deviceId);
+        List<Map<String, Object>> floors = new ArrayList<>();
+
+        if (template != null && template.getLeftFloors() != null && !template.getLeftFloors().isEmpty()) {
+            try {
+                List<FloorConfig> leftFloors = objectMapper.readValue(
+                        template.getLeftFloors(), new TypeReference<List<FloorConfig>>() {});
+
+                int totalFloors = template.getShelfNum() != null ? template.getShelfNum() : leftFloors.size();
+
+                for (FloorConfig config : leftFloors) {
+                    int floorNum = config.getFloor() != null ? config.getFloor() : 1;
+                    Map<String, Object> floor = new HashMap<>();
+                    floor.put("floorNumber", floorNum);
+                    floor.put("floorLabel", buildFloorLabel(floorNum, totalFloors));
+                    floor.put("floorName", "第" + floorNum + "层");
+
+                    List<Map<String, Object>> slots = new ArrayList<>();
+                    List<GoodsItem> goods = config.getGoods();
+                    if (goods != null) {
+                        for (int i = 0; i < goods.size(); i++) {
+                            GoodsItem item = goods.get(i);
+                            Map<String, Object> slot = new HashMap<>();
+                            slot.put("slotId", floorNum + "-" + (i + 1));
+                            if (item != null && item.getKey() != null && !item.getKey().isEmpty()) {
+                                slot.put("productId", item.getKey());
+                                slot.put("productName", item.getLabel());
+                                slot.put("productImage", normalizeImageUrl(item.getPic()));
+                                slot.put("stock", item.getStock() != null ? item.getStock() : 0);
+                                if (item.getC_price() != null && !item.getC_price().isEmpty()) {
+                                    slot.put("price", new BigDecimal(item.getC_price()).doubleValue());
+                                }
+                            }
+                            slots.add(slot);
+                        }
+                    }
+                    floor.put("slots", slots);
+                    floors.add(floor);
+                }
+            } catch (Exception e) {
+                log.error("解析层数据失败, deviceId={}", deviceId, e);
+            }
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("deviceId", deviceId);
+        result.put("floors", floors);
+        return result;
+    }
+
+    /**
+     * 保存设备商品配置
+     */
+    @Override
+    public boolean saveProductConfig(String deviceId, List<Map<String, Object>> floors) {
+        log.info("保存设备商品配置: deviceId={}, floors.size={}", deviceId, floors != null ? floors.size() : 0);
+
+        LayerTemplate template = getByDeviceId(deviceId);
+        if (template == null) {
+            throw new BusinessException(404, "设备未配置层模板,无法保存");
+        }
+
+        if (floors == null || floors.isEmpty()) {
+            throw new BusinessException(400, "楼层数据不能为空");
+        }
+
+        List<FloorConfig> leftFloors = new ArrayList<>();
+        for (Map<String, Object> floor : floors) {
+            FloorConfig config = new FloorConfig();
+            Object floorNumber = floor.get("floorNumber");
+            config.setFloor(floorNumber instanceof Number ? ((Number) floorNumber).intValue() : 1);
+
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> slots = (List<Map<String, Object>>) floor.get("slots");
+            List<GoodsItem> goods = new ArrayList<>();
+
+            if (slots != null) {
+                for (Map<String, Object> slot : slots) {
+                    GoodsItem item = new GoodsItem();
+                    String productId = (String) slot.get("productId");
+                    if (productId != null && !productId.isEmpty()) {
+                        item.setKey(productId);
+                        item.setLabel((String) slot.get("productName"));
+                        item.setPic((String) slot.get("productImage"));
+                        Object stock = slot.get("stock");
+                        item.setStock(stock instanceof Number ? ((Number) stock).intValue() : 0);
+                        Object price = slot.get("price");
+                        if (price instanceof Number) {
+                            item.setC_price(price.toString());
+                        }
+                    }
+                    goods.add(item);
+                }
+            }
+            config.setGoods(goods);
+            leftFloors.add(config);
+        }
+
+        LayerTemplateUpdateDTO dto = new LayerTemplateUpdateDTO();
+        dto.setLeftFloors(leftFloors);
+
+        update(template.getId(), dto);
+        return true;
+    }
+
+    /**
+     * 构建楼层标签(顶层/底层标识)
+     */
+    private String buildFloorLabel(int floorNum, int totalFloors) {
+        String label = "第" + floorNum + "层";
+        if (floorNum == 1) {
+            label += "\n(顶层)";
+        } else if (floorNum == totalFloors) {
+            label += "\n(底层)";
+        }
+        return label;
+    }
+
+    /**
+     * 标准化图片URL
+     * 如果图片链接已经是完整的URL(包含http://或https://),则保持不变
+     * 否则添加配置的图片域名前缀
+     */
+    private static final String IMAGE_DOMAIN_PREFIX = "https://img.hahabianli.com/";
+
+    private String normalizeImageUrl(String picUrl) {
+        if (picUrl == null || picUrl.isEmpty()) {
+            return picUrl;
+        }
+        if (picUrl.startsWith("http://") || picUrl.startsWith("https://")) {
+            return picUrl;
+        }
+        return IMAGE_DOMAIN_PREFIX + picUrl;
+    }
 }

+ 76 - 46
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -79,7 +79,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
 
     @Override
     public IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId,
-                                 String payStatus, Integer status, String startDate, String endDate) {
+                                 String payStatus, Integer status, String startDate, String endDate,
+                                 List<Integer> statusList) {
         // 确保分页参数有效(MyBatis-Plus 要求页码从 1 开始)
         if (page < 1) {
             page = 1;
@@ -96,7 +97,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         wrapper.like(orderNo != null && !orderNo.isEmpty(), Order::getOrderNo, orderNo)
                .like(deviceId != null && !deviceId.isEmpty(), Order::getDeviceId, deviceId)
                .eq(payStatus != null && !payStatus.isEmpty(), Order::getPayStatus, payStatus)
-               .eq(status != null, Order::getStatus, status);
+               .eq(status != null, Order::getStatus, status)
+               .in(statusList != null && !statusList.isEmpty(), Order::getStatus, statusList);
 
         // 时间范围查询
         if (startDate != null && !startDate.isEmpty()) {
@@ -159,6 +161,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         vo.setVideoUrl(order.getVideoUrl());
         vo.setConfidence(order.getConfidence());
         vo.setShopName(order.getShopName());
+        vo.setDeviceName(order.getDeviceName());
+        vo.setPayType(order.getPayType());
 
         List<OrderItemVO> products = orderGoodsService.getVOByOrderId(id);
         vo.setProducts(products);
@@ -211,54 +215,80 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
     }
 
     @Override
-    public Map<String, Object> getStatistics() {
+    public Map<String, Object> getStatistics(String startDate, String endDate) {
         Map<String, Object> statistics = new HashMap<>();
-        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
-
-        // 总订单数
-        long totalOrders = this.count();
-        statistics.put("totalOrders", totalOrders);
-
-        // 今日订单数
-        long todayOrders = this.lambdaQuery()
-                .ge(Order::getCreateTime, todayStart)
-                .count();
-        statistics.put("todayOrders", todayOrders);
-
-        // 各状态订单数
-        long unpaidOrders = this.lambdaQuery()
-                .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_UNPAID)
-                .count();
-        statistics.put("unpaidOrders", unpaidOrders);
-
-        long completedOrders = this.lambdaQuery()
-                .eq(Order::getStatus, OrderConstants.STATUS_COMPLETED)
-                .count();
-        statistics.put("completedOrders", completedOrders);
-
-        long cancelledOrders = this.lambdaQuery()
-                .eq(Order::getStatus, OrderConstants.STATUS_CANCELLED)
-                .count();
-        statistics.put("cancelledOrders", cancelledOrders);
-
-        long refundOrders = this.lambdaQuery()
-                .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_REFUND)
-                .count();
-        statistics.put("refundOrders", refundOrders);
-
-        // 总销售额(使用SQL聚合优化性能)
-        BigDecimal totalAmount = baseMapper.sumCompletedOrderAmount(null);
+
+        LocalDateTime startDateTime = null;
+        LocalDateTime endDateTime = null;
+        if (startDate != null && !startDate.isEmpty()) {
+            startDateTime = LocalDate.parse(startDate).atStartOfDay();
+        }
+        if (endDate != null && !endDate.isEmpty()) {
+            endDateTime = LocalDate.parse(endDate).atTime(LocalTime.MAX);
+        }
+
+        // 时间范围内的订单总数
+        Long totalOrders = baseMapper.countByDateRange(startDateTime, endDateTime);
+        statistics.put("totalOrders", totalOrders != null ? totalOrders : 0);
+
+        // 时间范围内的已完成订单数
+        Long completedOrders = baseMapper.countCompletedByDateRange(startDateTime, endDateTime);
+        statistics.put("completedOrders", completedOrders != null ? completedOrders : 0);
+
+        // 时间范围内的已退款订单数
+        Long refundOrders = baseMapper.countRefundedByDateRange(startDateTime, endDateTime);
+        statistics.put("refundOrders", refundOrders != null ? refundOrders : 0);
+
+        // 时间范围内的总销售额(所有已完成订单)
+        BigDecimal totalAmount = baseMapper.sumCompletedOrderAmount(startDateTime);
+        if (endDateTime != null) {
+            // 如果既有开始又有结束时间,需要更精确的统计
+            // 这里复用 sumCompletedOrderAmount 只支持开始时间,我们手动处理结束时间
+            // 由于 sumCompletedOrderAmount 的 SQL 只判断 startDate,如果 endDate 存在需要另写逻辑
+            // 简单处理:直接用 SQL 查询带结束时间的
+            totalAmount = baseMapper.selectObjs(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Order>()
+                    .eq(Order::getStatus, OrderConstants.STATUS_COMPLETED)
+                    .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
+                    .ge(startDateTime != null, Order::getCreateTime, startDateTime)
+                    .le(endDateTime != null, Order::getCreateTime, endDateTime)
+                    .select(Order::getPaidAmount)
+            ).stream()
+                .map(obj -> obj instanceof BigDecimal ? (BigDecimal) obj : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        }
         statistics.put("totalAmount", totalAmount != null ? totalAmount.setScale(2, RoundingMode.HALF_UP).toString() : "0.00");
 
-        // 今日销售额
-        BigDecimal todayAmount = baseMapper.sumCompletedOrderAmount(todayStart);
-        statistics.put("todayAmount", todayAmount != null ? todayAmount.setScale(2, RoundingMode.HALF_UP).toString() : "0.00");
+        // 按支付渠道统计金额
+        List<Map<String, Object>> channelStats = baseMapper.sumAmountByChannel(startDateTime, endDateTime);
+        BigDecimal wechatAmount = BigDecimal.ZERO;
+        BigDecimal alipayAmount = BigDecimal.ZERO;
+        BigDecimal otherAmount = BigDecimal.ZERO;
+
+        if (channelStats != null) {
+            for (Map<String, Object> stat : channelStats) {
+                String channel = (String) stat.get("channel");
+                BigDecimal amount = stat.get("amount") instanceof BigDecimal
+                        ? (BigDecimal) stat.get("amount")
+                        : new BigDecimal(stat.get("amount").toString());
+                if (channel == null) {
+                    otherAmount = otherAmount.add(amount);
+                    continue;
+                }
+                String lowerChannel = channel.toLowerCase();
+                if (lowerChannel.contains("wechat")) {
+                    wechatAmount = wechatAmount.add(amount);
+                } else if (lowerChannel.contains("alipay")) {
+                    alipayAmount = alipayAmount.add(amount);
+                } else {
+                    otherAmount = otherAmount.add(amount);
+                }
+            }
+        }
 
-        // 平均订单金额
-        BigDecimal averageAmount = completedOrders > 0 && totalAmount != null
-            ? totalAmount.divide(BigDecimal.valueOf(completedOrders), 2, RoundingMode.HALF_UP)
-            : BigDecimal.ZERO;
-        statistics.put("averageAmount", averageAmount.toString());
+        statistics.put("wechatAmount", wechatAmount.setScale(2, RoundingMode.HALF_UP).toString());
+        statistics.put("alipayAmount", alipayAmount.setScale(2, RoundingMode.HALF_UP).toString());
+        statistics.put("otherAmount", otherAmount.setScale(2, RoundingMode.HALF_UP).toString());
 
         return statistics;
     }