Ver código fonte

运营端小程序重构

skyline 1 mês atrás
pai
commit
4fc0ce3eab
35 arquivos alterados com 3408 adições e 1034 exclusões
  1. 337 293
      haha-admin-mp/package-lock.json
  2. 24 22
      haha-admin-mp/package.json
  3. 5 43
      haha-admin-mp/src/api/auth.ts
  4. 1 82
      haha-admin-mp/src/api/checkin.ts
  5. 56 0
      haha-admin-mp/src/api/customer.ts
  6. 18 26
      haha-admin-mp/src/api/dashboard.ts
  7. 18 74
      haha-admin-mp/src/api/device.ts
  8. 5 58
      haha-admin-mp/src/api/distribution.ts
  9. 2 0
      haha-admin-mp/src/api/index.ts
  10. 37 50
      haha-admin-mp/src/api/inventory.ts
  11. 1 60
      haha-admin-mp/src/api/invite.ts
  12. 10 59
      haha-admin-mp/src/api/order.ts
  13. 10 53
      haha-admin-mp/src/api/product.ts
  14. 20 46
      haha-admin-mp/src/api/shop.ts
  15. 54 0
      haha-admin-mp/src/api/statistics.ts
  16. 36 0
      haha-admin-mp/src/pages.json
  17. 555 0
      haha-admin-mp/src/pages/customer/detail.vue
  18. 545 0
      haha-admin-mp/src/pages/customer/list.vue
  19. 360 45
      haha-admin-mp/src/pages/device/detail.vue
  20. 1 1
      haha-admin-mp/src/pages/device/list.vue
  21. 9 3
      haha-admin-mp/src/pages/distribution/index.vue
  22. 9 3
      haha-admin-mp/src/pages/distribution/records.vue
  23. 128 58
      haha-admin-mp/src/pages/index/index.vue
  24. 16 15
      haha-admin-mp/src/pages/inventory/query.vue
  25. 516 0
      haha-admin-mp/src/pages/inventory/warning.vue
  26. 106 4
      haha-admin-mp/src/pages/my/my.vue
  27. 2 2
      haha-admin-mp/src/pages/orders/detail.vue
  28. 2 2
      haha-admin-mp/src/pages/orders/list.vue
  29. 2 11
      haha-admin-mp/src/pages/products/list.vue
  30. 2 2
      haha-admin-mp/src/pages/shop/detail.vue
  31. 1 1
      haha-admin-mp/src/pages/shop/list.vue
  32. 389 0
      haha-admin-mp/src/pages/statistics/overview.vue
  33. 2 2
      haha-admin-mp/src/utils/config.ts
  34. 127 0
      haha-admin-mp/src/utils/constants.ts
  35. 2 19
      haha-admin-mp/src/utils/request.ts

Diferenças do arquivo suprimidas por serem muito extensas
+ 337 - 293
haha-admin-mp/package-lock.json


+ 24 - 22
haha-admin-mp/package.json

@@ -10,31 +10,33 @@
     "type-check": "vue-tsc --noEmit"
   },
   "dependencies": {
-    "@dcloudio/uni-app": "3.0.0-4080420251103001",
-    "@dcloudio/uni-app-harmony": "3.0.0-4080420251103001",
-    "@dcloudio/uni-app-plus": "3.0.0-4080420251103001",
-    "@dcloudio/uni-components": "3.0.0-4080420251103001",
-    "@dcloudio/uni-h5": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-alipay": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-baidu": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-harmony": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-jd": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-kuaishou": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-lark": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-qq": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-toutiao": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-weixin": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-xhs": "3.0.0-4080420251103001",
-    "@dcloudio/uni-quickapp-webview": "3.0.0-4080420251103001",
+    "@dcloudio/uni-app": "3.0.0-5000720260410001",
+    "@dcloudio/uni-app-harmony": "3.0.0-5000720260410001",
+    "@dcloudio/uni-app-plus": "3.0.0-5000720260410001",
+    "@dcloudio/uni-components": "3.0.0-5000720260410001",
+    "@dcloudio/uni-h5": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-alipay": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-baidu": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-harmony": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-jd": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-kuaishou": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-lark": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-qq": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
+    "@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
+    "@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
     "pinia": "^3.0.4",
-    "vue": "^3.4.21"
+    "vue": "3.5.33",
+    "vue-i18n": "9.14.4"
   },
   "devDependencies": {
-    "@dcloudio/types": "^3.4.8",
-    "@dcloudio/uni-automator": "3.0.0-4080420251103001",
-    "@dcloudio/uni-cli-shared": "3.0.0-4080420251103001",
-    "@dcloudio/uni-stacktracey": "3.0.0-4080420251103001",
-    "@dcloudio/vite-plugin-uni": "3.0.0-4080420251103001",
+    "@dcloudio/types": "3.4.28",
+    "@dcloudio/uni-automator": "3.0.0-5000720260410001",
+    "@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
+    "@dcloudio/uni-stacktracey": "3.0.0-5000720260410001",
+    "@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001",
+    "@vue/runtime-core": "3.5.33",
     "@vue/tsconfig": "^0.1.3",
     "pngjs": "^7.0.0",
     "sass": "^1.77.0",

+ 5 - 43
haha-admin-mp/src/api/auth.ts

@@ -2,9 +2,8 @@
  * 用户认证相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockLoginResponse, mockUserInfo } from '@/mock';
 import { setToken, setUserInfo } from '@/utils/auth';
+import { post, get } from '@/utils/request';
 
 export interface LoginParams {
   username: string;
@@ -20,29 +19,7 @@ export interface LoginResponse {
  * 登录
  */
 export async function login(params: LoginParams): Promise<LoginResponse> {
-  if (USE_MOCK) {
-    // 模拟登录验证
-    return new Promise((resolve, reject) => {
-      setTimeout(() => {
-        if (params.username === 'admin' && params.password === '123456') {
-          const response = {
-            token: mockLoginResponse.token,
-            userInfo: mockUserInfo
-          };
-          // 保存登录信息
-          setToken(response.token);
-          setUserInfo(response.userInfo);
-          resolve(response);
-        } else {
-          reject(new Error('用户名或密码错误'));
-        }
-      }, 500);
-    });
-  }
-  
-  // 实际API调用
-  const { post } = await import('@/utils/request');
-  const response = await post<LoginResponse>('/auth/login', params);
+  const response = await post<LoginResponse>('/login', params);
   setToken(response.token);
   setUserInfo(response.userInfo);
   return response;
@@ -52,34 +29,19 @@ export async function login(params: LoginParams): Promise<LoginResponse> {
  * 退出登录
  */
 export async function logout(): Promise<void> {
-  if (USE_MOCK) {
-    return Promise.resolve();
-  }
-  
-  const { post } = await import('@/utils/request');
-  await post('/auth/logout');
+  await post('/login/logout');
 }
 
 /**
  * 获取当前用户信息
  */
 export async function getCurrentUser(): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockUserInfo);
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/auth/user-info');
+  return get('/login/info');
 }
 
 /**
  * 修改密码
  */
 export async function changePassword(oldPassword: string, newPassword: string): Promise<void> {
-  if (USE_MOCK) {
-    return Promise.resolve();
-  }
-  
-  const { post } = await import('@/utils/request');
-  await post('/auth/change-password', { oldPassword, newPassword });
+  await post('/user/change-password', { oldPassword, newPassword });
 }

+ 1 - 82
haha-admin-mp/src/api/checkin.ts

@@ -2,8 +2,7 @@
  * 签到功能相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockCheckinRecords, mockCheckinStats, mockTodayCheckin } from '@/mock/checkin';
+import { get, post } from '@/utils/request';
 
 export enum CheckinType {
   INVENTORY_TALLY = 'inventory_tally',
@@ -95,109 +94,29 @@ export interface TodayCheckin {
 }
 
 export async function submitCheckin(params: CheckinParams): Promise<CheckinRecord> {
-  if (USE_MOCK) {
-    const record: CheckinRecord = {
-      id: 'checkin_' + Date.now(),
-      type: params.type,
-      typeName: params.type === CheckinType.INVENTORY_TALLY ? '理货盘点签到' : '补货确认签到',
-      userId: 1,
-      userName: '测试用户',
-      userNo: 'EMP001',
-      shopId: params.shopId,
-      shopName: '测试门店',
-      deviceId: params.deviceId,
-      deviceName: params.deviceId ? '测试设备' : undefined,
-      location: params.location,
-      photos: params.photos,
-      remark: params.remark || '',
-      status: 'synced',
-      createdAt: new Date().toISOString(),
-      syncedAt: new Date().toISOString()
-    };
-    return Promise.resolve(record);
-  }
-  
-  const { post } = await import('@/utils/request');
   return post('/checkin', params);
 }
 
 export async function getCheckinList(params: CheckinQueryParams = {}): Promise<CheckinListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, type, startDate, endDate } = params;
-    let list = [...mockCheckinRecords];
-    
-    if (type) {
-      list = list.filter(item => item.type === type);
-    }
-    
-    if (startDate) {
-      list = list.filter(item => item.createdAt >= startDate);
-    }
-    
-    if (endDate) {
-      list = list.filter(item => item.createdAt <= endDate + ' 23:59:59');
-    }
-    
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-    
-    return Promise.resolve({
-      list: list.slice(startIndex, startIndex + pageSize),
-      total,
-      page,
-      pageSize
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
   return get('/checkin', params);
 }
 
 export async function getCheckinDetail(id: string): Promise<CheckinRecord> {
-  if (USE_MOCK) {
-    const record = mockCheckinRecords.find(item => item.id === id);
-    if (record) {
-      return Promise.resolve(record);
-    }
-    return Promise.reject(new Error('签到记录不存在'));
-  }
-  
-  const { get } = await import('@/utils/request');
   return get(`/checkin/${id}`);
 }
 
 export async function getCheckinStats(): Promise<CheckinStats> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockCheckinStats);
-  }
-  
-  const { get } = await import('@/utils/request');
   return get('/checkin/stats');
 }
 
 export async function getTodayCheckin(): Promise<TodayCheckin> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockTodayCheckin);
-  }
-  
-  const { get } = await import('@/utils/request');
   return get('/checkin/today');
 }
 
 export async function syncOfflineCheckins(records: CheckinRecord[]): Promise<{ success: number; failed: number }> {
-  if (USE_MOCK) {
-    return Promise.resolve({ success: records.length, failed: 0 });
-  }
-  
-  const { post } = await import('@/utils/request');
   return post('/checkin/sync', { records });
 }
 
 export async function exportCheckinRecords(params: CheckinQueryParams): Promise<string> {
-  if (USE_MOCK) {
-    return Promise.resolve('https://example.com/export/checkin_records.xlsx');
-  }
-  
-  const { get } = await import('@/utils/request');
   return get('/checkin/export', params);
 }

+ 56 - 0
haha-admin-mp/src/api/customer.ts

@@ -0,0 +1,56 @@
+/**
+ * 客户管理相关API
+ * 复用后端 UserController 接口
+ */
+
+import { get, put } from '@/utils/request';
+
+export interface CustomerQueryParams {
+  page?: number;
+  pageSize?: number;
+  phone?: string;
+  nickname?: string;
+  status?: number;
+}
+
+export interface CustomerListResponse {
+  list: any[];
+  total: number;
+  pageSize: number;
+  currentPage: number;
+}
+
+/**
+ * 获取客户列表
+ */
+export async function getCustomerList(params: CustomerQueryParams = {}): Promise<CustomerListResponse> {
+  return get('/users/list', params);
+}
+
+/**
+ * 获取客户详情
+ */
+export async function getCustomerDetail(id: number): Promise<any> {
+  return get(`/users/${id}`);
+}
+
+/**
+ * 获取客户统计数据
+ */
+export async function getCustomerStatistics(): Promise<any> {
+  return get('/users/statistics');
+}
+
+/**
+ * 更新客户状态
+ */
+export async function updateCustomerStatus(id: number, status: number): Promise<any> {
+  return put(`/users/${id}/status`, { status });
+}
+
+/**
+ * 更新客户信用分
+ */
+export async function updateCustomerCredit(id: number, creditScore: number): Promise<any> {
+  return put(`/users/${id}/credit`, { creditScore });
+}

+ 18 - 26
haha-admin-mp/src/api/dashboard.ts

@@ -2,41 +2,33 @@
  * 仪表盘相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockDashboardData } from '@/mock';
+import { get } from '@/utils/request';
 
 /**
- * 获取仪表盘统计数据
+ * 获取仪表盘概览数据
  */
-export async function getDashboardData(): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockDashboardData);
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/dashboard/stats');
+export async function getDashboardOverview(): Promise<any> {
+  return get('/dashboard/overview');
 }
 
 /**
- * 获取订单趋势数据
+ * 获取统计数据(按时间维度)
+ * @param type 时间类型:yesterday/week/month/days30
  */
-export async function getOrderTrend(days: number = 7): Promise<any[]> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockDashboardData.orderTrend);
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/dashboard/order-trend', { days });
+export async function getDashboardStatistics(type: string = 'week'): Promise<any> {
+  return get('/dashboard/statistics', { type });
 }
 
 /**
- * 获取热销商品数据
+ * 获取设备状态分布
  */
-export async function getHotProducts(limit: number = 10): Promise<any[]> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockDashboardData.hotProducts.slice(0, limit));
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/dashboard/hot-products', { limit });
+export async function getDeviceStatusDistribution(): Promise<any> {
+  return get('/dashboard/device-status');
+}
+
+/**
+ * 获取快捷入口数据
+ */
+export async function getQuickLinks(): Promise<any> {
+  return get('/dashboard/quick-links');
 }

+ 18 - 74
haha-admin-mp/src/api/device.ts

@@ -2,15 +2,15 @@
  * 设备管理相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockDeviceList, mockDeviceDetail } from '@/mock';
+import { get, post, put } from '@/utils/request';
 
 export interface DeviceQueryParams {
   page?: number;
   pageSize?: number;
-  deviceNo?: string;
-  status?: number;
+  deviceId?: string;
   shopId?: number;
+  status?: number;
+  storeName?: string;
 }
 
 export interface DeviceListResponse {
@@ -24,50 +24,13 @@ export interface DeviceListResponse {
  * 获取设备列表
  */
 export async function getDeviceList(params: DeviceQueryParams = {}): Promise<DeviceListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, status, deviceNo, shopId } = params;
-    let list = [...mockDeviceList];
-    
-    // 状态筛选
-    if (status !== undefined && status !== -1) {
-      list = list.filter(item => item.status === status);
-    }
-    
-    // 设备号搜索
-    if (deviceNo) {
-      list = list.filter(item => item.deviceNo.includes(deviceNo) || item.name.includes(deviceNo));
-    }
-    
-    // 门店筛选
-    if (shopId) {
-      list = list.filter(item => item.shopId === shopId);
-    }
-    
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-    const endIndex = startIndex + pageSize;
-    
-    return Promise.resolve({
-      list: list.slice(startIndex, endIndex),
-      total,
-      page,
-      pageSize
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/devices', params);
+  return get('/devices/list', params);
 }
 
 /**
  * 获取设备详情
  */
 export async function getDeviceDetail(deviceId: number): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockDeviceDetail);
-  }
-  
-  const { get } = await import('@/utils/request');
   return get(`/devices/${deviceId}`);
 }
 
@@ -75,52 +38,33 @@ export async function getDeviceDetail(deviceId: number): Promise<any> {
  * 获取设备统计数据
  */
 export async function getDeviceStats(): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve({
-      totalCount: 50,
-      onlineCount: 48,
-      offlineCount: 2,
-      maintenanceCount: 1,
-      errorCount: 0
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/devices/stats');
+  return get('/devices/statistics');
+}
+
+/**
+ * 远程开门
+ */
+export async function openDeviceDoor(deviceId: number, doorIndex?: string): Promise<void> {
+  await post(`/devices/${deviceId}/open`, { doorIndex: doorIndex || 'A' });
 }
 
 /**
  * 设置设备温度
  */
 export async function setDeviceTemperature(deviceId: number, temperature: number): Promise<void> {
-  if (USE_MOCK) {
-    return Promise.resolve();
-  }
-  
-  const { post } = await import('@/utils/request');
-  await post(`/devices/${deviceId}/temperature`, { temperature });
+  await put(`/devices/${deviceId}/temperature`, { temperature });
 }
 
 /**
  * 设置设备音量
  */
 export async function setDeviceVolume(deviceId: number, volume: number): Promise<void> {
-  if (USE_MOCK) {
-    return Promise.resolve();
-  }
-  
-  const { post } = await import('@/utils/request');
-  await post(`/devices/${deviceId}/volume`, { volume });
+  await put(`/devices/${deviceId}/volume`, { volume });
 }
 
 /**
- * 重启设备
+ * 获取设备门记录
  */
-export async function rebootDevice(deviceId: number): Promise<void> {
-  if (USE_MOCK) {
-    return Promise.resolve();
-  }
-  
-  const { post } = await import('@/utils/request');
-  await post(`/devices/${deviceId}/reboot`);
+export async function getDeviceDoorRecords(deviceId: string, params: { page?: number; pageSize?: number } = {}): Promise<any> {
+  return get(`/devices/${deviceId}/door-records`, params);
 }

+ 5 - 58
haha-admin-mp/src/api/distribution.ts

@@ -2,7 +2,7 @@
  * 分销功能相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
+import { get, post } from '@/utils/request';
 
 export enum WithdrawalStatus {
   PENDING = 'pending',
@@ -50,83 +50,30 @@ export interface WithdrawalQueryParams {
   status?: WithdrawalStatus;
 }
 
-// Mock数据
-const mockStats: DistributionStats = {
-  totalReferrals: 56,
-  validReferrals: 38,
-  commissionBalance: 256.80,
-  frozenAmount: 50.00,
-  totalCommission: 1200.00,
-  totalWithdrawn: 943.20
-};
-
-const mockWithdrawalRecords: WithdrawalRecord[] = [
-  { id: '1', amount: 100, status: WithdrawalStatus.PAID, createTime: '2026-04-10 14:30:00', auditTime: '2026-04-10 16:00:00', payTime: '2026-04-11 09:00:00' },
-  { id: '2', amount: 200, status: WithdrawalStatus.APPROVED, createTime: '2026-04-12 10:20:00', auditTime: '2026-04-12 15:30:00' },
-  { id: '3', amount: 50, status: WithdrawalStatus.PENDING, createTime: '2026-04-15 09:00:00' },
-  { id: '4', amount: 80, status: WithdrawalStatus.REJECTED, createTime: '2026-04-13 11:00:00', auditTime: '2026-04-13 14:00:00', rejectReason: '余额不足' },
-  { id: '5', amount: 150, status: WithdrawalStatus.PAID, createTime: '2026-04-08 16:45:00', auditTime: '2026-04-09 09:00:00', payTime: '2026-04-09 15:00:00' }
-];
-
 /**
  * 绑定分销员
  */
 export async function bindDistributor(distributorCode: string): Promise<{ success: boolean; message: string }> {
-  if (USE_MOCK) {
-    return Promise.resolve({ success: true, message: '绑定成功' });
-  }
-
-  const { post } = await import('@/utils/request');
-  return post('/mp/distribution/bind', { distributorCode });
+  return post('/distribution/bind', { distributorCode });
 }
 
 /**
  * 获取我的分销统计
  */
 export async function getMyStats(): Promise<DistributionStats> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockStats);
-  }
-
-  const { get } = await import('@/utils/request');
-  return get('/mp/distribution/my-stats');
+  return get('/distribution/my-stats');
 }
 
 /**
  * 申请提现
  */
 export async function applyWithdrawal(amount: number): Promise<{ success: boolean; message: string }> {
-  if (USE_MOCK) {
-    return Promise.resolve({ success: true, message: '提现申请已提交' });
-  }
-
-  const { post } = await import('@/utils/request');
-  return post('/mp/distribution/withdrawal', { amount });
+  return post('/distribution/withdrawal', { amount });
 }
 
 /**
  * 获取提现记录列表
  */
 export async function getMyWithdrawalList(params: WithdrawalQueryParams = {}): Promise<WithdrawalListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, status } = params;
-    let list = [...mockWithdrawalRecords];
-
-    if (status) {
-      list = list.filter(item => item.status === status);
-    }
-
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-
-    return Promise.resolve({
-      list: list.slice(startIndex, startIndex + pageSize),
-      total,
-      page,
-      pageSize
-    });
-  }
-
-  const { get } = await import('@/utils/request');
-  return get('/mp/distribution/withdrawal/list', params);
+  return get('/distribution/withdrawal/list', params);
 }

+ 2 - 0
haha-admin-mp/src/api/index.ts

@@ -10,3 +10,5 @@ export * from './shop';
 export * from './product';
 export * from './inventory';
 export * from './checkin';
+export * from './distribution';
+export * from './invite';

+ 37 - 50
haha-admin-mp/src/api/inventory.ts

@@ -2,16 +2,15 @@
  * 库存管理相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockInventoryList, mockInventoryLogList, mockInventoryStats } from '@/mock';
+import { get } from '@/utils/request';
 
 export interface InventoryQueryParams {
   page?: number;
   pageSize?: number;
+  deviceId?: string;
   shopId?: number;
-  deviceId?: number;
-  productName?: string;
-  status?: string; // normal, low, out
+  productId?: number;
+  lowStock?: boolean;
 }
 
 export interface InventoryListResponse {
@@ -21,63 +20,51 @@ export interface InventoryListResponse {
   pageSize: number;
 }
 
+/**
+ * 获取设备库存统计列表
+ */
+export async function getDeviceInventoryStats(params: InventoryQueryParams = {}): Promise<InventoryListResponse> {
+  return get('/inventory/device-stats', params);
+}
+
 /**
  * 获取库存列表
  */
 export async function getInventoryList(params: InventoryQueryParams = {}): Promise<InventoryListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, status, productName } = params;
-    let list = [...mockInventoryList];
-    
-    // 状态筛选
-    if (status && status !== 'all') {
-      list = list.filter(item => item.status === status);
-    }
-    
-    // 商品名称搜索
-    if (productName) {
-      list = list.filter(item => item.productName.includes(productName));
-    }
-    
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-    const endIndex = startIndex + pageSize;
-    
-    return Promise.resolve({
-      list: list.slice(startIndex, endIndex),
-      total,
-      page,
-      pageSize
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/inventory', params);
+  return get('/inventory/list', params);
+}
+
+/**
+ * 查询设备库存详情
+ */
+export async function getDeviceInventory(deviceId: string): Promise<any> {
+  return get(`/inventory/device/${deviceId}`);
+}
+
+/**
+ * 获取低库存列表
+ */
+export async function getLowStockList(): Promise<any> {
+  return get('/inventory/low-stock');
+}
+
+/**
+ * 获取库存统计数据
+ */
+export async function getInventoryStatistics(): Promise<any> {
+  return get('/inventory/statistics');
 }
 
 /**
- * 获取库存日志
+ * 获取库存变动日志
  */
 export async function getInventoryLogs(params: any = {}): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve({
-      list: mockInventoryLogList,
-      total: mockInventoryLogList.length
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
   return get('/inventory/logs', params);
 }
 
 /**
- * 获取库存统计
+ * 获取上货记录列表
  */
-export async function getInventoryStats(): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockInventoryStats);
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/inventory/stats');
+export async function getStockRecords(params: any = {}): Promise<any> {
+  return get('/inventory/records', params);
 }

+ 1 - 60
haha-admin-mp/src/api/invite.ts

@@ -2,8 +2,7 @@
  * 邀请有礼功能相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockInviteRecords, mockInviteStats, mockActivityInfo, mockInviteCode } from '@/mock/invite';
+import { get, post } from '@/utils/request';
 
 export enum InviteStatus {
   PENDING = 'pending',
@@ -107,83 +106,25 @@ export interface PosterResponse {
 }
 
 export async function getInviteCode(): Promise<InviteCodeResponse> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockInviteCode);
-  }
-
-  const { get } = await import('@/utils/request');
   return get('/invite/code');
 }
 
 export async function bindInviteRelation(params: BindInviteParams): Promise<BindInviteResponse> {
-  if (USE_MOCK) {
-    return Promise.resolve({
-      success: true,
-      message: '邀请关系绑定成功',
-      rewardResult: {
-        inviteeCouponGranted: true,
-        inviteeCouponName: '新人专属优惠券',
-        inviteeCouponAmount: 10,
-        inviterCouponPending: true,
-        triggerCondition: '好友完成首单后发放'
-      }
-    });
-  }
-
-  const { post } = await import('@/utils/request');
   return post('/invite/bind', params);
 }
 
 export async function getMyInviteRecords(params: InviteQueryParams = {}): Promise<InviteRecordListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, status } = params;
-    let list = [...mockInviteRecords];
-
-    if (status) {
-      list = list.filter(item => item.status === status);
-    }
-
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-
-    return Promise.resolve({
-      list: list.slice(startIndex, startIndex + pageSize),
-      total,
-      page,
-      pageSize
-    });
-  }
-
-  const { get } = await import('@/utils/request');
   return get('/invite/my-records', params);
 }
 
 export async function getMyInviteStatistics(): Promise<InviteStatistics> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockInviteStats);
-  }
-
-  const { get } = await import('@/utils/request');
   return get('/invite/my-statistics');
 }
 
 export async function getActivityInfo(): Promise<ActivityInfo> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockActivityInfo);
-  }
-
-  const { get } = await import('@/utils/request');
   return get('/invite/activity-info');
 }
 
 export async function generatePoster(): Promise<PosterResponse> {
-  if (USE_MOCK) {
-    return Promise.resolve({
-      posterUrl: 'https://example.com/poster/invite_' + Date.now() + '.png',
-      expireTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
-    });
-  }
-
-  const { get } = await import('@/utils/request');
   return get('/invite/poster');
 }

+ 10 - 59
haha-admin-mp/src/api/order.ts

@@ -2,17 +2,17 @@
  * 订单管理相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockOrderList, mockOrderDetail } from '@/mock';
+import { get, post } from '@/utils/request';
 
 export interface OrderQueryParams {
   page?: number;
   pageSize?: number;
   orderNo?: string;
+  deviceId?: string;
+  payStatus?: string;
   status?: number;
-  shopId?: number;
-  startTime?: string;
-  endTime?: string;
+  startDate?: string;
+  endDate?: string;
 }
 
 export interface OrderListResponse {
@@ -26,75 +26,26 @@ export interface OrderListResponse {
  * 获取订单列表
  */
 export async function getOrderList(params: OrderQueryParams = {}): Promise<OrderListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, status, orderNo } = params;
-    let list = [...mockOrderList];
-    
-    // 状态筛选
-    if (status !== undefined && status !== -1) {
-      list = list.filter(item => item.status === status);
-    }
-    
-    // 订单号搜索
-    if (orderNo) {
-      list = list.filter(item => item.orderNo.includes(orderNo));
-    }
-    
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-    const endIndex = startIndex + pageSize;
-    
-    return Promise.resolve({
-      list: list.slice(startIndex, endIndex),
-      total,
-      page,
-      pageSize
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/orders', params);
+  return get('/order/list', params);
 }
 
 /**
  * 获取订单详情
  */
 export async function getOrderDetail(orderId: number): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockOrderDetail);
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get(`/orders/${orderId}`);
+  return get(`/order/${orderId}`);
 }
 
 /**
  * 获取订单统计
  */
 export async function getOrderStats(): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve({
-      totalCount: 156,
-      completedCount: 120,
-      pendingCount: 15,
-      refundingCount: 8,
-      cancelledCount: 13,
-      totalAmount: 25680
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/orders/stats');
+  return get('/order/statistics');
 }
 
 /**
  * 处理退款
  */
-export async function handleRefund(orderId: number, agree: boolean, reason?: string): Promise<void> {
-  if (USE_MOCK) {
-    return Promise.resolve();
-  }
-  
-  const { post } = await import('@/utils/request');
-  await post(`/orders/${orderId}/refund`, { agree, reason });
+export async function handleRefund(orderId: number, reason: string): Promise<void> {
+  await post(`/order/${orderId}/refund`, { reason });
 }

+ 10 - 53
haha-admin-mp/src/api/product.ts

@@ -2,15 +2,15 @@
  * 商品管理相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockProductList, mockProductCategories } from '@/mock';
+import { get } from '@/utils/request';
 
 export interface ProductQueryParams {
   page?: number;
   pageSize?: number;
   name?: string;
+  barcode?: string;
   category?: string;
-  status?: number;
+  syncStatus?: number;
 }
 
 export interface ProductListResponse {
@@ -24,62 +24,19 @@ export interface ProductListResponse {
  * 获取商品列表
  */
 export async function getProductList(params: ProductQueryParams = {}): Promise<ProductListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, status, name, category } = params;
-    let list = [...mockProductList];
-    
-    // 状态筛选
-    if (status !== undefined && status !== -1) {
-      list = list.filter(item => item.status === status);
-    }
-    
-    // 分类筛选
-    if (category) {
-      list = list.filter(item => item.category === category);
-    }
-    
-    // 名称搜索
-    if (name) {
-      list = list.filter(item => item.name.includes(name));
-    }
-    
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-    const endIndex = startIndex + pageSize;
-    
-    return Promise.resolve({
-      list: list.slice(startIndex, endIndex),
-      total,
-      page,
-      pageSize
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/products', params);
+  return get('/products/list', params);
 }
 
 /**
- * 获取商品分类
+ * 获取商品详情
  */
-export async function getProductCategories(): Promise<any[]> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockProductCategories);
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/products/categories');
+export async function getProductDetail(productId: number): Promise<any> {
+  return get(`/products/${productId}`);
 }
 
 /**
- * 获取商品详情
+ * 获取商品统计数据
  */
-export async function getProductDetail(productId: number): Promise<any> {
-  if (USE_MOCK) {
-    const product = mockProductList.find(item => item.id === productId);
-    return Promise.resolve(product || null);
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get(`/products/${productId}`);
+export async function getProductStatistics(): Promise<any> {
+  return get('/products/statistics');
 }

+ 20 - 46
haha-admin-mp/src/api/shop.ts

@@ -2,13 +2,13 @@
  * 门店管理相关API
  */
 
-import { USE_MOCK } from '@/utils/config';
-import { mockShopList, mockShopDetail } from '@/mock';
+import { get } from '@/utils/request';
 
 export interface ShopQueryParams {
   page?: number;
   pageSize?: number;
   name?: string;
+  city?: string;
   status?: number;
 }
 
@@ -23,59 +23,33 @@ export interface ShopListResponse {
  * 获取门店列表
  */
 export async function getShopList(params: ShopQueryParams = {}): Promise<ShopListResponse> {
-  if (USE_MOCK) {
-    const { page = 1, pageSize = 10, status, name } = params;
-    let list = [...mockShopList];
-    
-    // 状态筛选
-    if (status !== undefined && status !== -1) {
-      list = list.filter(item => item.status === status);
-    }
-    
-    // 名称搜索
-    if (name) {
-      list = list.filter(item => item.name.includes(name));
-    }
-    
-    const total = list.length;
-    const startIndex = (page - 1) * pageSize;
-    const endIndex = startIndex + pageSize;
-    
-    return Promise.resolve({
-      list: list.slice(startIndex, endIndex),
-      total,
-      page,
-      pageSize
-    });
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/shops', params);
+  return get('/shops/list', params);
 }
 
 /**
  * 获取门店详情
  */
 export async function getShopDetail(shopId: number): Promise<any> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockShopDetail);
-  }
-  
-  const { get } = await import('@/utils/request');
   return get(`/shops/${shopId}`);
 }
 
 /**
- * 获取门店下拉列表(用于筛选)
+ * 获取门店统计数据
  */
-export async function getShopOptions(): Promise<any[]> {
-  if (USE_MOCK) {
-    return Promise.resolve(mockShopList.map(item => ({
-      id: item.id,
-      name: item.name
-    })));
-  }
-  
-  const { get } = await import('@/utils/request');
-  return get('/shops/options');
+export async function getShopStatistics(): Promise<any> {
+  return get('/shops/statistics');
+}
+
+/**
+ * 获取启用门店列表(下拉选择)
+ */
+export async function getEnabledShops(): Promise<any[]> {
+  return get('/shops/enabled');
+}
+
+/**
+ * 获取门店关联设备
+ */
+export async function getShopDevices(shopId: number): Promise<any[]> {
+  return get(`/shops/${shopId}/devices`);
 }

+ 54 - 0
haha-admin-mp/src/api/statistics.ts

@@ -0,0 +1,54 @@
+/**
+ * 数据统计相关API
+ * 复用后端 StatisticsController 接口
+ */
+
+import { get } from '@/utils/request';
+
+export interface StatisticsQueryParams {
+  startDate: string;
+  endDate: string;
+  shopId?: number;
+}
+
+/**
+ * 获取经营概览统计
+ */
+export async function getStatisticsOverview(params: { startDate: string; endDate: string; shopId?: number }): Promise<any> {
+  return get('/statistics/overview', params);
+}
+
+/**
+ * 获取品类销售概览
+ */
+export async function getCategoryOverview(params: { startDate: string; endDate: string; shopId?: number }): Promise<any> {
+  return get('/statistics/category/overview', params);
+}
+
+/**
+ * 获取商品销售排行
+ */
+export async function getProductTop(params: { startDate: string; endDate: string; type: string; limit?: number }): Promise<any> {
+  return get('/statistics/product/top', params);
+}
+
+/**
+ * 获取门店业绩排行
+ */
+export async function getShopRanking(params: { startDate: string; endDate: string; type: string; limit?: number }): Promise<any> {
+  return get('/statistics/shop/ranking', params);
+}
+
+/**
+ * 获取设备销售概览
+ */
+export async function getDeviceOverview(params: { startDate: string; endDate: string; shopId?: number }): Promise<any> {
+  return get('/statistics/device/overview', params);
+}
+
+/**
+ * 获取利润概览
+ */
+export async function getProfitOverview(params: { startDate: string; endDate: string }): Promise<any> {
+  return get('/statistics/profit/overview', params);
+}

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

@@ -90,6 +90,42 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/inventory/warning",
+			"style": {
+				"navigationBarTitleText": "库存预警",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/customer/list",
+			"style": {
+				"navigationBarTitleText": "客户管理",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/customer/detail",
+			"style": {
+				"navigationBarTitleText": "客户详情",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/statistics/overview",
+			"style": {
+				"navigationBarTitleText": "数据统计",
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/my/my",
 			"style": {

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

@@ -0,0 +1,555 @@
+<template>
+  <view class="page">
+    <!-- 导航栏 -->
+    <NavBar title="客户详情" :showBack="true" />
+
+    <!-- 客户信息卡片 -->
+    <view class="info-section">
+      <view class="info-card">
+        <view class="info-header">
+          <view class="info-avatar">
+            <text class="avatar-text">{{ getAvatarText(customer.nickname) }}</text>
+          </view>
+          <view class="info-main">
+            <text class="customer-name">{{ customer.nickname || '未命名用户' }}</text>
+            <view class="status-tag" :class="getStatusClass(customer.status)">
+              <text>{{ getStatusText(customer.status) }}</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="info-grid">
+          <view class="info-item">
+            <text class="info-label">手机号</text>
+            <text class="info-value">{{ formatPhone(customer.phone) }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">信用分</text>
+            <text class="info-value" :class="getCreditClass(customer.creditScore)">{{ customer.creditScore || 0 }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">注册时间</text>
+            <text class="info-value">{{ formatDate(customer.createTime) }}</text>
+          </view>
+          <view class="info-item">
+            <text class="info-label">用户ID</text>
+            <text class="info-value id">{{ customer.id }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 操作按钮 -->
+    <view class="action-section">
+      <view class="action-card">
+        <text class="action-title">账户操作</text>
+        <view class="action-list">
+          <view class="action-item" @click="handleToggleStatus">
+            <view class="action-icon" :class="customer.status === 1 ? 'warning' : 'success'">
+              <view class="icon-status"></view>
+            </view>
+            <view class="action-content">
+              <text class="action-name">{{ customer.status === 1 ? '禁用账户' : '启用账户' }}</text>
+              <text class="action-desc">{{ customer.status === 1 ? '禁止该用户下单' : '恢复该用户正常使用' }}</text>
+            </view>
+            <view class="action-arrow"></view>
+          </view>
+
+          <view class="action-item" @click="showCreditModal = true">
+            <view class="action-icon primary">
+              <view class="icon-credit"></view>
+            </view>
+            <view class="action-content">
+              <text class="action-name">调整信用分</text>
+              <text class="action-desc">修改用户信用评分</text>
+            </view>
+            <view class="action-arrow"></view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading">
+      <view class="loading-spinner"></view>
+      <text>加载中...</text>
+    </view>
+
+    <!-- 信用分调整弹窗 -->
+    <view class="modal-overlay" v-if="showCreditModal" @click="showCreditModal = false">
+      <view class="modal-content" @click.stop>
+        <view class="modal-header">
+          <text class="modal-title">调整信用分</text>
+        </view>
+        <view class="modal-body">
+          <text class="modal-label">当前信用分: {{ customer.creditScore || 0 }}</text>
+          <input
+            class="modal-input"
+            v-model="newCreditScore"
+            type="number"
+            placeholder="请输入新的信用分"
+            placeholder-class="input-placeholder"
+          />
+        </view>
+        <view class="modal-footer">
+          <button class="btn-cancel" @click="showCreditModal = false">取消</button>
+          <button class="btn-confirm" @click="handleUpdateCredit">确认</button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import { getCustomerDetail, updateCustomerStatus, updateCustomerCredit } from '@/api/customer';
+import { UserStatus, UserStatusText } from '@/utils/constants';
+import { showConfirm, showToast } from '@/utils/common';
+
+const customer = ref<any>({});
+const loading = ref(false);
+const showCreditModal = ref(false);
+const newCreditScore = ref('');
+
+const getStatusText = (status: number) => UserStatusText[status] || '未知';
+
+const getStatusClass = (status: number) => {
+  return status === UserStatus.ENABLED ? 'enabled' : 'disabled';
+};
+
+const getAvatarText = (nickname: string) => {
+  if (!nickname) return '?';
+  return nickname.charAt(0).toUpperCase();
+};
+
+const formatPhone = (phone: string) => {
+  if (!phone) return '未绑定手机号';
+  if (phone.length === 11) {
+    return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
+  }
+  return phone;
+};
+
+const formatDate = (dateStr: string) => {
+  if (!dateStr) return '-';
+  const date = new Date(dateStr);
+  if (isNaN(date.getTime())) return dateStr;
+  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+};
+
+const getCreditClass = (score: number) => {
+  if (!score || score >= 80) return 'high';
+  if (score >= 60) return 'medium';
+  return 'low';
+};
+
+const loadCustomerDetail = async () => {
+  const pages = getCurrentPages();
+  const currentPage = pages[pages.length - 1];
+  const customerId = (currentPage as any).options?.id;
+  if (!customerId) {
+    showToast('客户ID不存在', 'error');
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const res = await getCustomerDetail(Number(customerId));
+    customer.value = res || {};
+  } catch (error) {
+    console.error('加载客户详情失败', error);
+    showToast('加载客户详情失败', 'error');
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleToggleStatus = async () => {
+  const newStatus = customer.value.status === UserStatus.ENABLED ? UserStatus.DISABLED : UserStatus.ENABLED;
+  const actionText = newStatus === UserStatus.DISABLED ? '禁用' : '启用';
+
+  const confirmed = await showConfirm(`确认${actionText}该客户账户?`);
+  if (!confirmed) return;
+
+  try {
+    await updateCustomerStatus(customer.value.id, newStatus);
+    customer.value.status = newStatus;
+    showToast(`${actionText}成功`, 'success');
+  } catch (error) {
+    console.error('更新客户状态失败', error);
+    showToast('操作失败', 'error');
+  }
+};
+
+const handleUpdateCredit = async () => {
+  const score = parseInt(newCreditScore.value);
+  if (isNaN(score) || score < 0 || score > 100) {
+    showToast('请输入0-100之间的有效分数', 'error');
+    return;
+  }
+
+  try {
+    await updateCustomerCredit(customer.value.id, score);
+    customer.value.creditScore = score;
+    showCreditModal.value = false;
+    newCreditScore.value = '';
+    showToast('信用分更新成功', 'success');
+  } catch (error) {
+    console.error('更新信用分失败', error);
+    showToast('操作失败', 'error');
+  }
+};
+
+onMounted(() => {
+  loadCustomerDetail();
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+  padding-bottom: 40rpx;
+}
+
+/* 信息卡片 */
+.info-section {
+  padding: 16rpx 24rpx;
+}
+
+.info-card {
+  background: #ffffff;
+  border: 1rpx solid #e2e8f0;
+  border-radius: 20rpx;
+  padding: 32rpx;
+}
+
+.info-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.info-avatar {
+  width: 100rpx;
+  height: 100rpx;
+  background: #ecfdf5;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 24rpx;
+  flex-shrink: 0;
+
+  .avatar-text {
+    font-size: 40rpx;
+    font-weight: 700;
+    color: #10b981;
+  }
+}
+
+.info-main {
+  flex: 1;
+
+  .customer-name {
+    display: block;
+    font-size: 34rpx;
+    font-weight: 700;
+    color: #1e293b;
+    margin-bottom: 8rpx;
+  }
+}
+
+.status-tag {
+  display: inline-flex;
+  padding: 4rpx 12rpx;
+  border-radius: 6rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+
+  &.enabled {
+    background: #ecfdf5;
+    color: #10b981;
+  }
+
+  &.disabled {
+    background: #fef2f2;
+    color: #ef4444;
+  }
+}
+
+.info-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 20rpx;
+  background: #f8fafc;
+  border-radius: 12rpx;
+  padding: 20rpx;
+}
+
+.info-item {
+  .info-label {
+    display: block;
+    font-size: 22rpx;
+    color: #94a3b8;
+    margin-bottom: 4rpx;
+  }
+
+  .info-value {
+    font-size: 28rpx;
+    color: #1e293b;
+    font-weight: 500;
+
+    &.high { color: #10b981; }
+    &.medium { color: #f97316; }
+    &.low { color: #ef4444; }
+    &.id { font-size: 22rpx; color: #64748b; font-family: monospace; }
+  }
+}
+
+/* 操作区域 */
+.action-section {
+  padding: 0 24rpx;
+}
+
+.action-card {
+  background: #ffffff;
+  border: 1rpx solid #e2e8f0;
+  border-radius: 20rpx;
+  overflow: hidden;
+
+  .action-title {
+    display: block;
+    padding: 20rpx 24rpx 12rpx;
+    font-size: 24rpx;
+    color: #94a3b8;
+    font-weight: 600;
+  }
+}
+
+.action-list {
+  .action-item {
+    display: flex;
+    align-items: center;
+    padding: 20rpx 24rpx;
+    border-bottom: 1rpx solid #f1f5f9;
+    transition: background 0.15s;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:active {
+      background: #f8fafc;
+    }
+  }
+}
+
+.action-icon {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 14rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+
+  &.warning { background: #fff7ed; }
+  &.success { background: #ecfdf5; }
+  &.primary { background: #f0f9ff; }
+
+  .icon-status {
+    width: 20rpx;
+    height: 20rpx;
+    border-radius: 50%;
+    border: 3rpx solid #f97316;
+    position: relative;
+
+    &::after {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 8rpx;
+      height: 8rpx;
+      background: #f97316;
+      border-radius: 50%;
+    }
+  }
+
+  .icon-credit {
+    width: 20rpx;
+    height: 20rpx;
+    border: 3rpx solid #0ea5e9;
+    border-radius: 50%;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 3rpx;
+      height: 10rpx;
+      background: #0ea5e9;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      top: 4rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 3rpx;
+      height: 3rpx;
+      background: #0ea5e9;
+      border-radius: 50%;
+    }
+  }
+}
+
+.action-content {
+  flex: 1;
+
+  .action-name {
+    display: block;
+    font-size: 30rpx;
+    color: #1e293b;
+    font-weight: 500;
+    margin-bottom: 2rpx;
+  }
+
+  .action-desc {
+    font-size: 24rpx;
+    color: #94a3b8;
+  }
+}
+
+.action-arrow {
+  width: 12rpx;
+  height: 12rpx;
+  border-top: 3rpx solid #cbd5e1;
+  border-right: 3rpx solid #cbd5e1;
+  transform: rotate(45deg);
+  margin-left: 12rpx;
+  flex-shrink: 0;
+}
+
+/* 加载状态 */
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 100rpx 0;
+  color: #94a3b8;
+  font-size: 28rpx;
+
+  .loading-spinner {
+    width: 48rpx;
+    height: 48rpx;
+    border: 4rpx solid #e2e8f0;
+    border-top-color: #10b981;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 16rpx;
+  }
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+/* 弹窗 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 48rpx;
+}
+
+.modal-content {
+  width: 100%;
+  background: #ffffff;
+  border-radius: 20rpx;
+  overflow: hidden;
+
+  .modal-header {
+    padding: 24rpx;
+    border-bottom: 1rpx solid #f1f5f9;
+
+    .modal-title {
+      font-size: 32rpx;
+      font-weight: 600;
+      color: #1e293b;
+    }
+  }
+
+  .modal-body {
+    padding: 24rpx;
+
+    .modal-label {
+      display: block;
+      font-size: 26rpx;
+      color: #64748b;
+      margin-bottom: 16rpx;
+    }
+
+    .modal-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;
+    }
+  }
+
+  .modal-footer {
+    display: flex;
+    padding: 16rpx 24rpx 24rpx;
+    gap: 12rpx;
+
+    button {
+      flex: 1;
+      height: 80rpx;
+      border-radius: 12rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+      border: none;
+
+      &::after {
+        border: none;
+      }
+    }
+
+    .btn-cancel {
+      background: #f8fafc;
+      color: #64748b;
+    }
+
+    .btn-confirm {
+      background: #10b981;
+      color: #fff;
+    }
+  }
+}
+
+.input-placeholder {
+  color: #cbd5e1;
+}
+</style>

+ 545 - 0
haha-admin-mp/src/pages/customer/list.vue

@@ -0,0 +1,545 @@
+<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 === 0 }"
+          @click="changeStatus(0)"
+        >
+          <text>禁用</text>
+        </view>
+      </view>
+      <view class="search-btn" @click="showSearch = true">
+        <view class="search-icon"></view>
+      </view>
+    </view>
+
+    <!-- 客户列表 -->
+    <scroll-view
+      class="customer-scroll"
+      scroll-y
+      @scrolltolower="loadMore"
+    >
+      <view class="customer-list">
+        <view
+          class="customer-card"
+          v-for="customer in customerList"
+          :key="customer.id"
+          @click="goDetail(customer.id)"
+        >
+          <view class="card-avatar">
+            <text class="avatar-text">{{ getAvatarText(customer.nickname) }}</text>
+          </view>
+          <view class="card-content">
+            <view class="card-row-main">
+              <text class="customer-name">{{ customer.nickname || '未命名用户' }}</text>
+              <view class="status-tag" :class="getStatusClass(customer.status)">
+                <text>{{ getStatusText(customer.status) }}</text>
+              </view>
+            </view>
+            <view class="card-row-info">
+              <text class="info-text">{{ formatPhone(customer.phone) }}</text>
+              <text class="info-divider">·</text>
+              <text class="info-text">信用分 {{ customer.creditScore || 0 }}</text>
+            </view>
+          </view>
+          <view class="card-arrow"></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 && customerList.length > 0">
+        <text>— 没有更多了 —</text>
+      </view>
+
+      <view class="empty-state" v-if="!loading && customerList.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="modal-content" @click.stop>
+        <view class="modal-header">
+          <text class="modal-title">搜索客户</text>
+        </view>
+        <view class="modal-body">
+          <input
+            class="search-input"
+            v-model="searchKeyword"
+            placeholder="请输入手机号或昵称"
+            placeholder-class="input-placeholder"
+            @confirm="doSearch"
+          />
+        </view>
+        <view class="modal-footer">
+          <button class="btn-cancel" @click="showSearch = false">取消</button>
+          <button class="btn-confirm" @click="doSearch">搜索</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 自定义TabBar -->
+    <CustomTabBar />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import CustomTabBar from '@/components/CustomTabBar.vue';
+import { getCustomerList } from '@/api/customer';
+import { UserStatus, UserStatusText } from '@/utils/constants';
+
+const customerList = ref<any[]>([]);
+const currentStatus = ref(-1);
+const searchKeyword = ref('');
+const showSearch = ref(false);
+const loading = ref(false);
+const hasMore = ref(true);
+const page = ref(1);
+const pageSize = 10;
+
+const getStatusText = (status: number) => UserStatusText[status] || '未知';
+
+const getStatusClass = (status: number) => {
+  return status === UserStatus.ENABLED ? 'enabled' : 'disabled';
+};
+
+const getAvatarText = (nickname: string) => {
+  if (!nickname) return '?';
+  return nickname.charAt(0).toUpperCase();
+};
+
+const formatPhone = (phone: string) => {
+  if (!phone) return '未绑定手机号';
+  if (phone.length === 11) {
+    return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
+  }
+  return phone;
+};
+
+const changeStatus = (status: number) => {
+  currentStatus.value = status;
+  page.value = 1;
+  hasMore.value = true;
+  loadCustomers();
+};
+
+const loadCustomers = async () => {
+  loading.value = true;
+  try {
+    const params: any = {
+      page: page.value,
+      pageSize
+    };
+    if (currentStatus.value !== -1) {
+      params.status = currentStatus.value;
+    }
+    if (searchKeyword.value) {
+      if (/^\d+$/.test(searchKeyword.value)) {
+        params.phone = searchKeyword.value;
+      } else {
+        params.nickname = searchKeyword.value;
+      }
+    }
+
+    const res = await getCustomerList(params);
+
+    if (page.value === 1) {
+      customerList.value = res.list || [];
+    } else {
+      customerList.value = [...customerList.value, ...(res.list || [])];
+    }
+
+    hasMore.value = customerList.value.length < (res.total || 0);
+  } catch (error) {
+    console.error('加载客户列表失败', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const loadMore = () => {
+  if (loading.value || !hasMore.value) return;
+  page.value++;
+  loadCustomers();
+};
+
+const doSearch = () => {
+  showSearch.value = false;
+  page.value = 1;
+  hasMore.value = true;
+  loadCustomers();
+};
+
+const goDetail = (customerId: number) => {
+  uni.navigateTo({ url: `/pages/customer/detail?id=${customerId}` });
+};
+
+onMounted(() => {
+  loadCustomers();
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+  display: flex;
+  flex-direction: column;
+  padding-bottom: 200rpx;
+}
+
+/* 筛选栏 */
+.filter-header {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 24rpx;
+  gap: 12rpx;
+  background: #ffffff;
+  border-bottom: 1rpx solid #e2e8f0;
+}
+
+.filter-tabs {
+  flex: 1;
+  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;
+    }
+  }
+}
+
+.search-btn {
+  width: 72rpx;
+  height: 72rpx;
+  background: #ecfdf5;
+  border: 2rpx solid #10b981;
+  border-radius: 20rpx;
+  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);
+    }
+  }
+}
+
+/* 客户列表 */
+.customer-scroll {
+  flex: 1;
+  height: 0;
+}
+
+.customer-list {
+  padding: 16rpx 24rpx;
+}
+
+.customer-card {
+  background: #ffffff;
+  border: 1rpx solid #e2e8f0;
+  border-radius: 12rpx;
+  margin-bottom: 10rpx;
+  padding: 14rpx 16rpx;
+  display: flex;
+  align-items: center;
+  transition: transform 0.15s;
+
+  &:active {
+    transform: scale(0.98);
+  }
+}
+
+.card-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  background: #ecfdf5;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+
+  .avatar-text {
+    font-size: 32rpx;
+    font-weight: 600;
+    color: #10b981;
+  }
+}
+
+.card-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.card-row-main {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8rpx;
+
+  .customer-name {
+    font-size: 28rpx;
+    font-weight: 600;
+    color: #1e293b;
+    margin-right: 12rpx;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
+
+.status-tag {
+  padding: 2rpx 10rpx;
+  border-radius: 4rpx;
+  font-size: 20rpx;
+  font-weight: 500;
+  flex-shrink: 0;
+
+  &.enabled {
+    background: #ecfdf5;
+    color: #10b981;
+  }
+
+  &.disabled {
+    background: #fef2f2;
+    color: #ef4444;
+  }
+}
+
+.card-row-info {
+  display: flex;
+  align-items: center;
+
+  .info-text {
+    font-size: 24rpx;
+    color: #64748b;
+  }
+
+  .info-divider {
+    margin: 0 8rpx;
+    color: #cbd5e1;
+    font-size: 24rpx;
+  }
+}
+
+.card-arrow {
+  width: 12rpx;
+  height: 12rpx;
+  border-top: 3rpx solid #cbd5e1;
+  border-right: 3rpx solid #cbd5e1;
+  transform: rotate(45deg);
+  margin-left: 12rpx;
+  flex-shrink: 0;
+}
+
+/* 加载状态 */
+.loading-more {
+  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); }
+}
+
+.no-more {
+  text-align: center;
+  padding: 32rpx;
+  font-size: 24rpx;
+  color: #cbd5e1;
+}
+
+.empty-state {
+  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: 48rpx;
+      height: 48rpx;
+      border: 4rpx solid #cbd5e1;
+      border-radius: 50%;
+      position: relative;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 20rpx;
+        height: 20rpx;
+        background: #cbd5e1;
+        border-radius: 50%;
+      }
+    }
+  }
+
+  .empty-text {
+    font-size: 28rpx;
+    color: #94a3b8;
+  }
+}
+
+/* 搜索弹窗 */
+.search-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 48rpx;
+}
+
+.modal-content {
+  width: 100%;
+  background: #ffffff;
+  border-radius: 20rpx;
+  overflow: hidden;
+
+  .modal-header {
+    padding: 24rpx;
+    border-bottom: 1rpx solid #f1f5f9;
+
+    .modal-title {
+      font-size: 32rpx;
+      font-weight: 600;
+      color: #1e293b;
+    }
+  }
+
+  .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;
+    }
+  }
+
+  .modal-footer {
+    display: flex;
+    padding: 16rpx 24rpx 24rpx;
+    gap: 12rpx;
+
+    button {
+      flex: 1;
+      height: 80rpx;
+      border-radius: 12rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+      border: none;
+
+      &::after {
+        border: none;
+      }
+    }
+
+    .btn-cancel {
+      background: #f8fafc;
+      color: #64748b;
+    }
+
+    .btn-confirm {
+      background: #10b981;
+      color: #fff;
+    }
+  }
+}
+
+.input-placeholder {
+  color: #cbd5e1;
+}
+</style>

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

@@ -97,34 +97,109 @@
         </view>
       </view>
       
-      <!-- 最近订单 -->
-      <view class="info-section" v-if="device.recentOrders && device.recentOrders.length > 0">
-        <text class="section-title">最近订单</text>
-        <view class="order-list">
-          <view class="order-item" v-for="order in device.recentOrders" :key="order.orderNo">
-            <text class="order-no">{{ order.orderNo }}</text>
-            <text class="order-amount">¥{{ formatMoney(order.amount) }}</text>
-            <text class="order-time">{{ order.time }}</text>
+      <!-- 开门记录 -->
+      <view class="info-section" v-if="doorRecords.length > 0">
+        <text class="section-title">最近开门记录</text>
+        <view class="record-list">
+          <view class="record-item" v-for="record in doorRecords" :key="record.id">
+            <view class="record-left">
+              <text class="record-action">{{ record.action || '开门' }}</text>
+              <text class="record-user">{{ record.userName || '未知用户' }}</text>
+            </view>
+            <text class="record-time">{{ record.createTime || record.time }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 远程操作 -->
+      <view class="info-section" v-if="device.status === 1">
+        <text class="section-title">远程操作</text>
+        <view class="operation-grid">
+          <view class="operation-btn" @click="handleOpenDoor">
+            <view class="op-icon green">
+              <view class="icon-door"></view>
+            </view>
+            <text class="op-name">远程开门</text>
+          </view>
+          <view class="operation-btn" @click="showTempModal = true">
+            <view class="op-icon amber">
+              <view class="icon-temp"></view>
+            </view>
+            <text class="op-name">设置温度</text>
+          </view>
+          <view class="operation-btn" @click="showVolumeModal = true">
+            <view class="op-icon blue">
+              <view class="icon-vol"></view>
+            </view>
+            <text class="op-name">设置音量</text>
           </view>
         </view>
       </view>
     </view>
-    
-    <!-- 底部操作 -->
-    <view class="bottom-actions" v-if="device && device.status === 1">
-      <button class="action-btn" @click="handleReboot">重启设备</button>
+
+    <!-- 温度设置弹窗 -->
+    <view class="modal-overlay" v-if="showTempModal" @click="showTempModal = false">
+      <view class="modal-content" @click.stop>
+        <view class="modal-header">
+          <text class="modal-title">设置柜内温度</text>
+        </view>
+        <view class="modal-body">
+          <text class="modal-label">当前温度: {{ device?.temperature }}°C</text>
+          <input
+            class="modal-input"
+            v-model="newTemperature"
+            type="number"
+            placeholder="请输入温度 (0-30°C)"
+            placeholder-class="input-placeholder"
+          />
+        </view>
+        <view class="modal-footer">
+          <button class="btn-cancel" @click="showTempModal = false">取消</button>
+          <button class="btn-confirm" @click="handleSetTemperature">确认</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 音量设置弹窗 -->
+    <view class="modal-overlay" v-if="showVolumeModal" @click="showVolumeModal = false">
+      <view class="modal-content" @click.stop>
+        <view class="modal-header">
+          <text class="modal-title">设置设备音量</text>
+        </view>
+        <view class="modal-body">
+          <text class="modal-label">当前音量: {{ device?.volume }}%</text>
+          <input
+            class="modal-input"
+            v-model="newVolume"
+            type="number"
+            placeholder="请输入音量 (0-100)"
+            placeholder-class="input-placeholder"
+          />
+        </view>
+        <view class="modal-footer">
+          <button class="btn-cancel" @click="showVolumeModal = false">取消</button>
+          <button class="btn-confirm" @click="handleSetVolume">确认</button>
+        </view>
+      </view>
     </view>
+
+    <view class="bottom-spacer"></view>
   </view>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
-import { getDeviceDetail, rebootDevice } from '@/api/device';
-import { DeviceStatusText, DeviceStatusColor } from '@/mock/device';
+import { getDeviceDetail, openDeviceDoor, setDeviceTemperature, setDeviceVolume, getDeviceDoorRecords } from '@/api/device';
+import { DeviceStatusText, DeviceStatusColor } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil, showToast, showConfirm, navigateBack } from '@/utils/common';
 
 const device = ref<any>(null);
+const doorRecords = ref<any[]>([]);
+const showTempModal = ref(false);
+const showVolumeModal = ref(false);
+const newTemperature = ref('');
+const newVolume = ref('');
 const formatMoney = formatMoneyUtil;
 
 const getStatusText = (status: number) => DeviceStatusText[status] || '未知';
@@ -140,30 +215,76 @@ const loadDetail = async () => {
   const pages = getCurrentPages();
   const currentPage = pages[pages.length - 1];
   const id = (currentPage as any).options?.id;
-  
+
   if (!id) {
     showToast('设备ID不存在');
     navigateBack();
     return;
   }
-  
+
   try {
     device.value = await getDeviceDetail(Number(id));
+    loadDoorRecords(id);
   } catch (error) {
     showToast('加载设备详情失败');
     navigateBack();
   }
 };
 
-const handleReboot = async () => {
-  const confirmed = await showConfirm('确认重启该设备?');
-  if (confirmed && device.value) {
-    try {
-      await rebootDevice(device.value.id);
-      showToast('重启指令已发送', 'success');
-    } catch (error) {
-      showToast('重启失败');
-    }
+const loadDoorRecords = async (deviceId: string) => {
+  try {
+    const res = await getDeviceDoorRecords(deviceId, { page: 1, pageSize: 5 });
+    doorRecords.value = res.list || [];
+  } catch (error) {
+    console.error('加载开门记录失败', error);
+  }
+};
+
+const handleOpenDoor = async () => {
+  const confirmed = await showConfirm('确认远程开门?请确保设备前无人遮挡。');
+  if (!confirmed) return;
+
+  try {
+    await openDeviceDoor(device.value.id, 'A');
+    showToast('远程开门指令已发送', 'success');
+  } catch (error) {
+    showToast('远程开门失败', 'error');
+  }
+};
+
+const handleSetTemperature = async () => {
+  const temp = parseInt(newTemperature.value);
+  if (isNaN(temp) || temp < 0 || temp > 30) {
+    showToast('请输入0-30之间的温度值', 'error');
+    return;
+  }
+
+  try {
+    await setDeviceTemperature(device.value.id, temp);
+    device.value.temperature = temp;
+    showTempModal.value = false;
+    newTemperature.value = '';
+    showToast('温度设置成功', 'success');
+  } catch (error) {
+    showToast('温度设置失败', 'error');
+  }
+};
+
+const handleSetVolume = async () => {
+  const vol = parseInt(newVolume.value);
+  if (isNaN(vol) || vol < 0 || vol > 100) {
+    showToast('请输入0-100之间的音量值', 'error');
+    return;
+  }
+
+  try {
+    await setDeviceVolume(device.value.id, vol);
+    device.value.volume = vol;
+    showVolumeModal.value = false;
+    newVolume.value = '';
+    showToast('音量设置成功', 'success');
+  } catch (error) {
+    showToast('音量设置失败', 'error');
   }
 };
 
@@ -507,33 +628,227 @@ onMounted(() => {
   }
 }
 
-.bottom-actions {
+.record-list {
+  .record-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16rpx 0;
+    border-bottom: 1rpx solid #f1f5f9;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .record-left {
+      .record-action {
+        display: block;
+        font-size: 28rpx;
+        color: #1e293b;
+        margin-bottom: 4rpx;
+      }
+
+      .record-user {
+        font-size: 22rpx;
+        color: #94a3b8;
+      }
+    }
+
+    .record-time {
+      font-size: 24rpx;
+      color: #94a3b8;
+    }
+  }
+}
+
+.operation-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16rpx;
+
+  .operation-btn {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 24rpx 0;
+    background: #f8fafc;
+    border-radius: 16rpx;
+    transition: background 0.15s;
+
+    &:active {
+      background: #f1f5f9;
+    }
+
+    .op-icon {
+      width: 72rpx;
+      height: 72rpx;
+      border-radius: 20rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-bottom: 12rpx;
+
+      &.green { background: #ecfdf5; }
+      &.amber { background: #fff7ed; }
+      &.blue { background: #f0f9ff; }
+
+      .icon-door {
+        width: 28rpx;
+        height: 32rpx;
+        border: 3rpx solid #10b981;
+        border-radius: 4rpx;
+        position: relative;
+
+        &::after {
+          content: '';
+          position: absolute;
+          top: 50%;
+          right: -6rpx;
+          transform: translateY(-50%);
+          width: 4rpx;
+          height: 8rpx;
+          background: #10b981;
+          border-radius: 2rpx;
+        }
+      }
+
+      .icon-temp {
+        width: 20rpx;
+        height: 20rpx;
+        border: 3rpx solid #f97316;
+        border-radius: 50%;
+        position: relative;
+
+        &::before {
+          content: '';
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          width: 3rpx;
+          height: 10rpx;
+          background: #f97316;
+        }
+      }
+
+      .icon-vol {
+        width: 0;
+        height: 0;
+        border-left: 12rpx solid #0ea5e9;
+        border-top: 8rpx solid transparent;
+        border-bottom: 8rpx solid transparent;
+        position: relative;
+
+        &::before {
+          content: '';
+          position: absolute;
+          top: -10rpx;
+          left: -4rpx;
+          width: 6rpx;
+          height: 20rpx;
+          background: #0ea5e9;
+          border-radius: 3rpx;
+        }
+      }
+    }
+
+    .op-name {
+      font-size: 24rpx;
+      color: #475569;
+    }
+  }
+}
+
+.bottom-spacer {
+  height: 40rpx;
+}
+
+/* 弹窗 */
+.modal-overlay {
   position: fixed;
-  bottom: 0;
+  top: 0;
   left: 0;
   right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 48rpx;
+}
+
+.modal-content {
+  width: 100%;
   background: #ffffff;
-  padding: 20rpx 32rpx;
-  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
-  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
-  
-  .action-btn {
-    width: 100%;
-    height: 96rpx;
-    background: #10b981;
-    color: #fff;
-    font-size: 32rpx;
-    font-weight: 600;
-    border-radius: 16rpx;
-    border: none;
-    
-    &::after {
+  border-radius: 20rpx;
+  overflow: hidden;
+
+  .modal-header {
+    padding: 24rpx;
+    border-bottom: 1rpx solid #f1f5f9;
+
+    .modal-title {
+      font-size: 32rpx;
+      font-weight: 600;
+      color: #1e293b;
+    }
+  }
+
+  .modal-body {
+    padding: 24rpx;
+
+    .modal-label {
+      display: block;
+      font-size: 26rpx;
+      color: #64748b;
+      margin-bottom: 16rpx;
+    }
+
+    .modal-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;
+    }
+  }
+
+  .modal-footer {
+    display: flex;
+    padding: 16rpx 24rpx 24rpx;
+    gap: 12rpx;
+
+    button {
+      flex: 1;
+      height: 80rpx;
+      border-radius: 12rpx;
+      font-size: 28rpx;
+      font-weight: 500;
       border: none;
+
+      &::after {
+        border: none;
+      }
     }
-    
-    &:active {
-      background: #059669;
+
+    .btn-cancel {
+      background: #f8fafc;
+      color: #64748b;
+    }
+
+    .btn-confirm {
+      background: #10b981;
+      color: #fff;
     }
   }
 }
+
+.input-placeholder {
+  color: #cbd5e1;
+}
 </style>

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

@@ -89,7 +89,7 @@ import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import CustomTabBar from '@/components/CustomTabBar.vue';
 import { getDeviceList, getDeviceStats } from '@/api/device';
-import { DeviceStatusText, DeviceStatusColor } from '@/mock/device';
+import { DeviceStatusText, DeviceStatusColor } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil, formatTime as formatTimeUtil } from '@/utils/common';
 
 const deviceList = ref<any[]>([]);

+ 9 - 3
haha-admin-mp/src/pages/distribution/index.vue

@@ -92,7 +92,7 @@ import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import { getMyStats, getMyWithdrawalList } from '@/api/distribution';
 import type { DistributionStats, WithdrawalRecord } from '@/api/distribution';
-import { WithdrawalStatus, WithdrawalStatusLabel } from '@/api/distribution';
+
 
 const myStats = ref<DistributionStats | null>(null);
 const recentList = ref<WithdrawalRecord[]>([]);
@@ -119,8 +119,14 @@ const loadRecentRecords = async () => {
   }
 };
 
-const getStatusLabel = (status: WithdrawalStatus): string => {
-  return WithdrawalStatusLabel[status] || '未知';
+const getStatusLabel = (status: string): string => {
+  const map: Record<string, string> = {
+    pending: '审核中',
+    approved: '已通过',
+    rejected: '已驳回',
+    paid: '已打款'
+  };
+  return map[status] || '未知';
 };
 
 const goToWithdrawal = () => {

+ 9 - 3
haha-admin-mp/src/pages/distribution/records.vue

@@ -87,7 +87,7 @@ import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import { getMyWithdrawalList } from '@/api/distribution';
 import type { WithdrawalRecord } from '@/api/distribution';
-import { WithdrawalStatus, WithdrawalStatusLabel } from '@/api/distribution';
+
 
 const recordList = ref<WithdrawalRecord[]>([]);
 const currentStatus = ref<string>('');
@@ -138,8 +138,14 @@ const loadRecords = async (reset = false) => {
   }
 };
 
-const getStatusLabel = (status: WithdrawalStatus): string => {
-  return WithdrawalStatusLabel[status] || '未知';
+const getStatusLabel = (status: string): string => {
+  const map: Record<string, string> = {
+    pending: '审核中',
+    approved: '已通过',
+    rejected: '已驳回',
+    paid: '已打款'
+  };
+  return map[status] || '未知';
 };
 </script>
 

+ 128 - 58
haha-admin-mp/src/pages/index/index.vue

@@ -19,12 +19,12 @@
     <view class="stats-section">
       <view class="stats-card main">
         <view class="stats-item" @click="navigateTo('/pages/orders/list')">
-          <text class="stats-value">{{ stats.todayStats?.orderCount || 0 }}</text>
+          <text class="stats-value">{{ overview.todayOrders || 0 }}</text>
           <text class="stats-label">今日订单</text>
         </view>
         <view class="stats-divider"></view>
         <view class="stats-item">
-          <text class="stats-value accent">¥{{ formatMoney(stats.todayStats?.orderAmount || 0) }}</text>
+          <text class="stats-value accent">¥{{ formatMoney(overview.todaySales || 0) }}</text>
           <text class="stats-label">今日销售额</text>
         </view>
       </view>
@@ -33,14 +33,14 @@
         <view class="stats-card mini success" @click="navigateTo('/pages/device/list')">
           <view class="stats-dot green"></view>
           <view class="stats-info">
-            <text class="stats-value">{{ stats.todayStats?.deviceOnline || 0 }}</text>
+            <text class="stats-value">{{ overview.onlineDevices || 0 }}</text>
             <text class="stats-label">在线设备</text>
           </view>
         </view>
         <view class="stats-card mini warning">
           <view class="stats-dot amber"></view>
           <view class="stats-info">
-            <text class="stats-value">{{ stats.todayStats?.deviceOffline || 0 }}</text>
+            <text class="stats-value">{{ overview.offlineDevices || 0 }}</text>
             <text class="stats-label">离线设备</text>
           </view>
         </view>
@@ -83,8 +83,26 @@
           </view>
           <text class="quick-name">库存查询</text>
         </view>
-        <view class="quick-item" @click="navigateTo('/pages/checkin/index')">
+        <view class="quick-item" @click="navigateTo('/pages/customer/list')">
+          <view class="quick-icon green">
+            <view class="icon-user"></view>
+          </view>
+          <text class="quick-name">客户管理</text>
+        </view>
+        <view class="quick-item" @click="navigateTo('/pages/statistics/overview')">
+          <view class="quick-icon blue">
+            <view class="icon-stats"></view>
+          </view>
+          <text class="quick-name">数据统计</text>
+        </view>
+        <view class="quick-item" @click="navigateTo('/pages/inventory/warning')">
           <view class="quick-icon rose">
+            <view class="icon-warning"></view>
+          </view>
+          <text class="quick-name">库存预警</text>
+        </view>
+        <view class="quick-item" @click="navigateTo('/pages/checkin/index')">
+          <view class="quick-icon amber">
             <view class="icon-checkin"></view>
           </view>
           <text class="quick-name">签到打卡</text>
@@ -100,45 +118,19 @@
       </view>
       <view class="chart-card">
         <view class="chart-container">
-          <view 
-            class="chart-bar-wrapper" 
-            v-for="(item, index) in stats.orderTrend || []" 
+          <view
+            class="chart-bar-wrapper"
+            v-for="(item, index) in orderTrend"
             :key="index"
           >
             <text class="chart-value">{{ item.count }}</text>
             <view class="chart-bar-bg">
-              <view 
-                class="chart-bar" 
+              <view
+                class="chart-bar"
                 :style="{ height: getBarHeight(item.count) + '%' }"
               ></view>
             </view>
-            <text class="chart-label">{{ item.date.slice(-2) }}</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <!-- 热销商品 -->
-    <view class="section" v-if="stats.hotProducts && stats.hotProducts.length > 0">
-      <view class="section-header">
-        <text class="section-title">热销商品</text>
-        <text class="section-tag hot">TOP 5</text>
-      </view>
-      <view class="hot-list">
-        <view 
-          class="hot-item" 
-          v-for="(item, index) in stats.hotProducts || []" 
-          :key="item.id"
-        >
-          <view class="hot-rank" :class="{ top: Number(index) < 3 }">
-            <text>{{ Number(index) + 1 }}</text>
-          </view>
-          <view class="hot-info">
-            <text class="hot-name">{{ item.name }}</text>
-            <text class="hot-sales">销量 {{ item.sales }}</text>
-          </view>
-          <view class="hot-progress">
-            <view class="hot-progress-bar" :style="{ width: getProgress(item.sales) + '%' }"></view>
+            <text class="chart-label">{{ item.date }}</text>
           </view>
         </view>
       </view>
@@ -151,7 +143,7 @@
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import { getDashboardData } from '@/api/dashboard';
+import { getDashboardOverview, getDashboardStatistics } from '@/api/dashboard';
 import { formatMoney as formatMoneyUtil } from '@/utils/common';
 import CustomTabBar from '@/components/CustomTabBar.vue';
 
@@ -159,11 +151,15 @@ import CustomTabBar from '@/components/CustomTabBar.vue';
 const systemInfo = uni.getSystemInfoSync();
 const statusBarHeight = ref(systemInfo.statusBarHeight || 20);
 
-const stats = ref<any>({});
+const overview = ref<any>({});
+const statistics = ref<any>({});
+const orderTrend = ref<any[]>([]);
 const maxCount = ref(0);
-const maxSales = ref(0);
 
-const formatMoney = (amount: number) => formatMoneyUtil(amount);
+const formatMoney = (amount: number | string) => {
+  const num = typeof amount === 'string' ? parseFloat(amount) : amount;
+  return formatMoneyUtil(num || 0);
+};
 
 const getTimeGreeting = () => {
   const hour = new Date().getHours();
@@ -178,24 +174,29 @@ const getBarHeight = (count: number) => {
   return Math.max(15, (count / maxCount.value) * 100);
 };
 
-const getProgress = (sales: number) => {
-  if (maxSales.value === 0) return 0;
-  return Math.max(10, (sales / maxSales.value) * 100);
-};
-
 const navigateTo = (url: string) => {
   uni.navigateTo({ url });
 };
 
 const loadData = async () => {
   try {
-    const data = await getDashboardData();
-    stats.value = data;
-    if (data.orderTrend && data.orderTrend.length > 0) {
-      maxCount.value = Math.max(...data.orderTrend.map((item: any) => item.count));
-    }
-    if (data.hotProducts && data.hotProducts.length > 0) {
-      maxSales.value = Math.max(...data.hotProducts.map((item: any) => item.sales));
+    const [overviewRes, statisticsRes] = await Promise.all([
+      getDashboardOverview(),
+      getDashboardStatistics('week')
+    ]);
+    overview.value = overviewRes || {};
+    statistics.value = statisticsRes || {};
+
+    // 构建订单趋势数据
+    const dates = statisticsRes?.dates || [];
+    const ordersData = statisticsRes?.ordersData || [];
+    orderTrend.value = dates.map((date: string, index: number) => ({
+      date: date.slice(5), // MM-dd
+      count: ordersData[index] || 0
+    }));
+
+    if (orderTrend.value.length > 0) {
+      maxCount.value = Math.max(...orderTrend.value.map((item: any) => item.count));
     }
   } catch (error) {
     console.error('获取仪表盘数据失败', error);
@@ -585,10 +586,10 @@ onMounted(() => {
     .icon-checkin {
       width: 28rpx;
       height: 28rpx;
-      border: 3rpx solid #f43f5e;
+      border: 3rpx solid #f97316;
       border-radius: 50%;
       position: relative;
-      
+
       &::before {
         content: '';
         position: absolute;
@@ -597,10 +598,10 @@ onMounted(() => {
         transform: translate(-50%, -50%);
         width: 8rpx;
         height: 8rpx;
-        background: #f43f5e;
+        background: #f97316;
         border-radius: 50%;
       }
-      
+
       &::after {
         content: '';
         position: absolute;
@@ -608,12 +609,81 @@ onMounted(() => {
         right: -6rpx;
         width: 12rpx;
         height: 12rpx;
+        background: #f97316;
+        border-radius: 50%;
+      }
+    }
+
+    .icon-user {
+      width: 24rpx;
+      height: 24rpx;
+      border: 3rpx solid #10b981;
+      border-radius: 50%;
+      position: relative;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: -10rpx;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 12rpx;
+        height: 12rpx;
+        border: 3rpx solid #10b981;
+        border-radius: 50%;
+      }
+    }
+
+    .icon-stats {
+      display: flex;
+      align-items: flex-end;
+      width: 28rpx;
+      height: 24rpx;
+
+      &::before, &::after, & {
+        content: '';
+        width: 8rpx;
+        background: #0ea5e9;
+        border-radius: 4rpx;
+      }
+
+      &::before { height: 10rpx; margin-right: 2rpx; }
+      & { height: 22rpx; margin-right: 2rpx; }
+      &::after { height: 16rpx; }
+    }
+
+    .icon-warning {
+      width: 28rpx;
+      height: 28rpx;
+      border: 3rpx solid #f43f5e;
+      border-radius: 50%;
+      position: relative;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 3rpx;
+        height: 12rpx;
+        background: #f43f5e;
+      }
+
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 4rpx;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 3rpx;
+        height: 3rpx;
         background: #f43f5e;
         border-radius: 50%;
       }
     }
   }
-  
+
   .quick-name {
     font-size: 24rpx;
     color: #475569;

+ 16 - 15
haha-admin-mp/src/pages/inventory/query.vue

@@ -12,13 +12,13 @@
           <view class="alert-dot"></view>
         </view>
         <view class="stat-card error">
-          <text class="stat-value">{{ inventoryStats.outOfStockCount }}</text>
+          <text class="stat-value">{{ inventoryStats.zeroStockCount }}</text>
           <text class="stat-label">缺货商品</text>
           <view class="alert-dot"></view>
         </view>
         <view class="stat-card primary">
-          <text class="stat-value">¥{{ formatMoney(inventoryStats.totalValue) }}</text>
-          <text class="stat-label">库存总值</text>
+          <text class="stat-value">{{ inventoryStats.totalStock }}</text>
+          <text class="stat-label">总库存量</text>
         </view>
       </view>
     </view>
@@ -110,17 +110,15 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
-import { getInventoryList, getInventoryStats } from '@/api/inventory';
+import { getInventoryList, getInventoryStatistics } from '@/api/inventory';
 import { formatMoney as formatMoneyUtil } from '@/utils/common';
 
 const inventoryList = ref<any[]>([]);
 const inventoryStats = ref({
-  totalProducts: 0,
+  totalRecords: 0,
   lowStockCount: 0,
-  outOfStockCount: 0,
-  totalValue: 0,
-  todayInCount: 0,
-  todayOutCount: 0
+  zeroStockCount: 0,
+  totalStock: 0
 });
 const currentStatus = ref('all');
 const loading = ref(false);
@@ -146,7 +144,8 @@ const getProgressWidth = (item: any) => {
 
 const loadStats = async () => {
   try {
-    inventoryStats.value = await getInventoryStats();
+    const stats = await getInventoryStatistics();
+    inventoryStats.value = stats || inventoryStats.value;
   } catch (error) {
     console.error('加载库存统计失败', error);
   }
@@ -162,11 +161,13 @@ const changeStatus = (status: string) => {
 const loadInventory = async () => {
   loading.value = true;
   try {
-    const res = await getInventoryList({
-      page: page.value,
-      pageSize,
-      status: currentStatus.value
-    });
+    const params: any = { page: page.value, pageSize };
+    if (currentStatus.value === 'low') {
+      params.lowStock = true;
+    } else if (currentStatus.value === 'out') {
+      params.stockStatus = 0;
+    }
+    const res = await getInventoryList(params);
     
     if (page.value === 1) {
       inventoryList.value = res.list;

+ 516 - 0
haha-admin-mp/src/pages/inventory/warning.vue

@@ -0,0 +1,516 @@
+<template>
+  <view class="page">
+    <!-- 导航栏 -->
+    <NavBar title="库存预警" :showBack="true" />
+
+    <!-- 预警统计 -->
+    <view class="alert-section">
+      <view class="alert-grid">
+        <view class="alert-card warning">
+          <view class="alert-icon">
+            <view class="icon-alert"></view>
+          </view>
+          <view class="alert-info">
+            <text class="alert-value">{{ stats.lowStockCount || 0 }}</text>
+            <text class="alert-label">低库存商品</text>
+          </view>
+        </view>
+        <view class="alert-card error">
+          <view class="alert-icon">
+            <view class="icon-out"></view>
+          </view>
+          <view class="alert-info">
+            <text class="alert-value">{{ stats.zeroStockCount || 0 }}</text>
+            <text class="alert-label">缺货商品</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 标签切换 -->
+    <view class="tab-section">
+      <view class="tab-list">
+        <view
+          class="tab-item"
+          :class="{ active: currentTab === 'low' }"
+          @click="changeTab('low')"
+        >
+          <text>低库存</text>
+        </view>
+        <view
+          class="tab-item"
+          :class="{ active: currentTab === 'out' }"
+          @click="changeTab('out')"
+        >
+          <text>已缺货</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 预警列表 -->
+    <scroll-view
+      class="warning-scroll"
+      scroll-y
+      @scrolltolower="loadMore"
+    >
+      <view class="warning-list">
+        <view
+          class="warning-card"
+          v-for="item in warningList"
+          :key="item.id"
+        >
+          <view class="card-header">
+            <view class="product-info">
+              <text class="product-name">{{ item.productName }}</text>
+              <text class="location">{{ item.shopName }} · {{ item.deviceName }}</text>
+            </view>
+            <view class="stock-badge" :class="currentTab">
+              <text>{{ item.currentStock || 0 }}</text>
+            </view>
+          </view>
+
+          <view class="card-body">
+            <view class="progress-bar">
+              <view
+                class="progress-fill"
+                :class="currentTab"
+                :style="{ width: getProgressWidth(item) + '%' }"
+              ></view>
+            </view>
+            <view class="stock-detail">
+              <text class="detail-text">预警线: {{ item.minStock || 0 }}</text>
+              <text class="detail-text">上限: {{ item.maxStock || 0 }}</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 && warningList.length > 0">
+        <text>— 没有更多了 —</text>
+      </view>
+
+      <view class="empty-state" v-if="!loading && warningList.length === 0">
+        <view class="empty-icon">
+          <view class="empty-icon-inner"></view>
+        </view>
+        <text class="empty-text">{{ currentTab === 'low' ? '暂无低库存商品' : '暂无缺货商品' }}</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import { getInventoryList, getInventoryStatistics } from '@/api/inventory';
+
+const currentTab = ref('low');
+const warningList = ref<any[]>([]);
+const stats = ref({
+  lowStockCount: 0,
+  zeroStockCount: 0
+});
+const loading = ref(false);
+const hasMore = ref(true);
+const page = ref(1);
+const pageSize = 10;
+
+const getProgressWidth = (item: any) => {
+  if (!item.maxStock || item.maxStock === 0) return 0;
+  return Math.min(100, ((item.currentStock || 0) / item.maxStock) * 100);
+};
+
+const loadStats = async () => {
+  try {
+    const res = await getInventoryStatistics();
+    if (res) {
+      stats.value.lowStockCount = res.lowStockCount || 0;
+      stats.value.zeroStockCount = res.zeroStockCount || 0;
+    }
+  } catch (error) {
+    console.error('加载库存统计失败', error);
+  }
+};
+
+const changeTab = (tab: string) => {
+  currentTab.value = tab;
+  page.value = 1;
+  hasMore.value = true;
+  loadWarnings();
+};
+
+const loadWarnings = async () => {
+  loading.value = true;
+  try {
+    const params: any = { page: page.value, pageSize };
+    if (currentTab.value === 'low') {
+      params.lowStock = true;
+    } else if (currentTab.value === 'out') {
+      params.stockStatus = 0;
+    }
+
+    const res = await getInventoryList(params);
+    const list = res.list || [];
+
+    if (page.value === 1) {
+      warningList.value = list;
+    } else {
+      warningList.value = [...warningList.value, ...list];
+    }
+
+    hasMore.value = warningList.value.length < (res.total || 0);
+  } catch (error) {
+    console.error('加载预警列表失败', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const loadMore = () => {
+  if (loading.value || !hasMore.value) return;
+  page.value++;
+  loadWarnings();
+};
+
+onMounted(() => {
+  loadStats();
+  loadWarnings();
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 预警统计 */
+.alert-section {
+  padding: 16rpx 24rpx;
+  background: #ffffff;
+  border-bottom: 1rpx solid #e2e8f0;
+}
+
+.alert-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 12rpx;
+}
+
+.alert-card {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 20rpx;
+  border-radius: 16rpx;
+  border: 1rpx solid #e2e8f0;
+
+  &.warning {
+    background: #fff7ed;
+    border-color: #f97316;
+  }
+
+  &.error {
+    background: #fef2f2;
+    border-color: #ef4444;
+  }
+}
+
+.alert-icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 16rpx;
+  background: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+
+  .icon-alert {
+    width: 28rpx;
+    height: 28rpx;
+    border: 3rpx solid #f97316;
+    border-radius: 50%;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 3rpx;
+      height: 12rpx;
+      background: #f97316;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 4rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 3rpx;
+      height: 3rpx;
+      background: #f97316;
+      border-radius: 50%;
+    }
+  }
+
+  .icon-out {
+    width: 28rpx;
+    height: 28rpx;
+    border: 3rpx solid #ef4444;
+    border-radius: 50%;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%) rotate(45deg);
+      width: 16rpx;
+      height: 3rpx;
+      background: #ef4444;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%) rotate(-45deg);
+      width: 16rpx;
+      height: 3rpx;
+      background: #ef4444;
+    }
+  }
+}
+
+.alert-info {
+  .alert-value {
+    display: block;
+    font-size: 40rpx;
+    font-weight: 700;
+    color: #1e293b;
+    margin-bottom: 4rpx;
+  }
+
+  .alert-label {
+    font-size: 24rpx;
+    color: #64748b;
+  }
+}
+
+/* 标签切换 */
+.tab-section {
+  padding: 12rpx 24rpx;
+  background: #ffffff;
+}
+
+.tab-list {
+  display: flex;
+  gap: 10rpx;
+
+  .tab-item {
+    flex: 1;
+    padding: 14rpx 0;
+    text-align: center;
+    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;
+      font-weight: 500;
+    }
+  }
+}
+
+/* 预警列表 */
+.warning-scroll {
+  flex: 1;
+  height: 0;
+}
+
+.warning-list {
+  padding: 16rpx 24rpx;
+}
+
+.warning-card {
+  background: #ffffff;
+  border: 1rpx solid #e2e8f0;
+  border-radius: 16rpx;
+  margin-bottom: 12rpx;
+  overflow: hidden;
+  transition: transform 0.15s;
+
+  &:active {
+    transform: scale(0.98);
+  }
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding: 20rpx 24rpx 12rpx;
+
+  .product-info {
+    flex: 1;
+    min-width: 0;
+
+    .product-name {
+      display: block;
+      font-size: 28rpx;
+      font-weight: 600;
+      color: #1e293b;
+      margin-bottom: 4rpx;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .location {
+      font-size: 22rpx;
+      color: #94a3b8;
+    }
+  }
+}
+
+.stock-badge {
+  padding: 8rpx 20rpx;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  font-weight: 700;
+  margin-left: 12rpx;
+  flex-shrink: 0;
+
+  &.low {
+    background: #fff7ed;
+    color: #f97316;
+  }
+
+  &.out {
+    background: #fef2f2;
+    color: #ef4444;
+  }
+}
+
+.card-body {
+  padding: 0 24rpx 20rpx;
+
+  .progress-bar {
+    height: 10rpx;
+    background: #f1f5f9;
+    border-radius: 5rpx;
+    overflow: hidden;
+    margin-bottom: 10rpx;
+
+    .progress-fill {
+      height: 100%;
+      border-radius: 5rpx;
+
+      &.low { background: #f97316; }
+      &.out { background: #ef4444; }
+    }
+  }
+
+  .stock-detail {
+    display: flex;
+    justify-content: space-between;
+
+    .detail-text {
+      font-size: 22rpx;
+      color: #94a3b8;
+    }
+  }
+}
+
+/* 加载状态 */
+.loading-more {
+  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); }
+}
+
+.no-more {
+  text-align: center;
+  padding: 32rpx;
+  font-size: 24rpx;
+  color: #cbd5e1;
+}
+
+.empty-state {
+  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: 48rpx;
+      height: 48rpx;
+      border: 4rpx solid #cbd5e1;
+      border-radius: 50%;
+      position: relative;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 20rpx;
+        height: 20rpx;
+        background: #cbd5e1;
+        border-radius: 50%;
+      }
+    }
+  }
+
+  .empty-text {
+    font-size: 28rpx;
+    color: #94a3b8;
+  }
+}
+</style>

+ 106 - 4
haha-admin-mp/src/pages/my/my.vue

@@ -52,7 +52,7 @@
             </view>
             <view class="menu-arrow"></view>
           </view>
-          
+
           <view class="menu-item" @click="navigateTo('/pages/products/list')">
             <view class="menu-icon purple">
               <view class="icon-product"></view>
@@ -63,7 +63,7 @@
             </view>
             <view class="menu-arrow"></view>
           </view>
-          
+
           <view class="menu-item" @click="navigateTo('/pages/inventory/query')">
             <view class="menu-icon cyan">
               <view class="icon-chart"></view>
@@ -74,6 +74,39 @@
             </view>
             <view class="menu-arrow"></view>
           </view>
+
+          <view class="menu-item" @click="navigateTo('/pages/inventory/warning')">
+            <view class="menu-icon rose">
+              <view class="icon-warning"></view>
+            </view>
+            <view class="menu-content">
+              <text class="menu-name">库存预警</text>
+              <text class="menu-desc">低库存和缺货提醒</text>
+            </view>
+            <view class="menu-arrow"></view>
+          </view>
+
+          <view class="menu-item" @click="navigateTo('/pages/customer/list')">
+            <view class="menu-icon green">
+              <view class="icon-user"></view>
+            </view>
+            <view class="menu-content">
+              <text class="menu-name">客户管理</text>
+              <text class="menu-desc">查看客户信息和信用分</text>
+            </view>
+            <view class="menu-arrow"></view>
+          </view>
+
+          <view class="menu-item" @click="navigateTo('/pages/statistics/overview')">
+            <view class="menu-icon blue">
+              <view class="icon-stats"></view>
+            </view>
+            <view class="menu-content">
+              <text class="menu-name">数据统计</text>
+              <text class="menu-desc">经营数据和销售排行</text>
+            </view>
+            <view class="menu-arrow"></view>
+          </view>
         </view>
       </view>
       
@@ -410,7 +443,7 @@ onMounted(() => {
     border: 3rpx solid #0ea5e9;
     border-radius: 50%;
     position: relative;
-    
+
     &::before {
       content: '';
       position: absolute;
@@ -422,7 +455,7 @@ onMounted(() => {
       background: #0ea5e9;
       border-radius: 50%;
     }
-    
+
     &::after {
       content: '';
       position: absolute;
@@ -435,6 +468,75 @@ onMounted(() => {
       border-radius: 2rpx;
     }
   }
+
+  .icon-warning {
+    width: 24rpx;
+    height: 24rpx;
+    border: 3rpx solid #f43f5e;
+    border-radius: 50%;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 3rpx;
+      height: 10rpx;
+      background: #f43f5e;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 3rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 3rpx;
+      height: 3rpx;
+      background: #f43f5e;
+      border-radius: 50%;
+    }
+  }
+
+  .icon-user {
+    width: 20rpx;
+    height: 20rpx;
+    border: 3rpx solid #10b981;
+    border-radius: 50%;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: -8rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 10rpx;
+      height: 10rpx;
+      border: 3rpx solid #10b981;
+      border-radius: 50%;
+    }
+  }
+
+  .icon-stats {
+    display: flex;
+    align-items: flex-end;
+    width: 22rpx;
+    height: 18rpx;
+
+    &::before, &::after, & {
+      content: '';
+      width: 6rpx;
+      background: #0ea5e9;
+      border-radius: 3rpx;
+    }
+
+    &::before { height: 8rpx; margin-right: 2rpx; }
+    & { height: 16rpx; margin-right: 2rpx; }
+    &::after { height: 12rpx; }
+  }
 }
 
 .menu-content {

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

@@ -92,7 +92,7 @@
 import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import { getOrderDetail, handleRefund as refundOrder } from '@/api/order';
-import { OrderStatusText, OrderStatusColor } from '@/mock/order';
+import { OrderStatusText, OrderStatusColor } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil, showToast, navigateBack } from '@/utils/common';
 
 const order = ref<any>(null);
@@ -131,7 +131,7 @@ const handleRefund = async () => {
   if (!order.value) return;
   
   try {
-    await refundOrder(order.value.id, true);
+    await refundOrder(order.value.id, '商家同意退款');
     showToast('退款处理成功', 'success');
     setTimeout(() => {
       navigateBack();

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

@@ -121,7 +121,7 @@ import { ref, onMounted } 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 '@/mock/order';
+import { OrderStatusText, OrderStatusColor, OrderStatus } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil, showConfirm, showToast } from '@/utils/common';
 
 const orderList = ref<any[]>([]);
@@ -201,7 +201,7 @@ const handleRefund = async (order: any) => {
   const confirmed = await showConfirm(`确认同意订单 ${order.orderNo} 的退款申请?`);
   if (confirmed) {
     try {
-      await refundOrder(order.id, true);
+      await refundOrder(order.id, '商家同意退款');
       showToast('退款处理成功', 'success');
       loadOrders();
     } catch (error) {

+ 2 - 11
haha-admin-mp/src/pages/products/list.vue

@@ -99,8 +99,8 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
-import { getProductList, getProductCategories } from '@/api/product';
-import { ProductStatusText, ProductStatusColor } from '@/mock/product';
+import { getProductList } from '@/api/product';
+import { ProductStatusText, ProductStatusColor } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil } from '@/utils/common';
 
 const productList = ref<any[]>([]);
@@ -120,14 +120,6 @@ const getStatusClass = (status: number) => {
   return 'inactive';
 };
 
-const loadCategories = async () => {
-  try {
-    categories.value = await getProductCategories();
-  } catch (error) {
-    console.error('加载分类失败', error);
-  }
-};
-
 const changeCategory = (category: string) => {
   currentCategory.value = category;
   page.value = 1;
@@ -165,7 +157,6 @@ const loadMore = () => {
 };
 
 onMounted(() => {
-  loadCategories();
   loadProducts();
 });
 </script>

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

@@ -79,8 +79,8 @@
 import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import { getShopDetail } from '@/api/shop';
-import { ShopStatusText, ShopStatusColor } from '@/mock/shop';
-import { DeviceStatusText, DeviceStatusColor } from '@/mock/device';
+import { ShopStatusText, ShopStatusColor } from '@/utils/constants';
+import { DeviceStatusText, DeviceStatusColor } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil, showToast, navigateBack } from '@/utils/common';
 
 const shop = ref<any>(null);

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

@@ -84,7 +84,7 @@
 import { ref, onMounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
 import { getShopList } from '@/api/shop';
-import { ShopStatusText, ShopStatusColor } from '@/mock/shop';
+import { ShopStatusText, ShopStatusColor } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil } from '@/utils/common';
 
 const shopList = ref<any[]>([]);

+ 389 - 0
haha-admin-mp/src/pages/statistics/overview.vue

@@ -0,0 +1,389 @@
+<template>
+  <view class="page">
+    <!-- 导航栏 -->
+    <NavBar title="数据统计" :showBack="true" />
+
+    <!-- 时间筛选 -->
+    <view class="time-filter">
+      <view
+        class="filter-btn"
+        v-for="item in timeOptions"
+        :key="item.value"
+        :class="{ active: currentTime === item.value }"
+        @click="changeTimeRange(item.value)"
+      >
+        <text>{{ item.label }}</text>
+      </view>
+    </view>
+
+    <!-- 核心指标 -->
+    <view class="stats-section">
+      <view class="stats-grid">
+        <view class="stat-card primary">
+          <text class="stat-label">销售额</text>
+          <text class="stat-value">¥{{ formatMoney(overview.totalSales || 0) }}</text>
+          <text class="stat-trend" v-if="overview.salesTrend">
+            环比 {{ getTrendText(overview.salesTrend) }}
+          </text>
+        </view>
+        <view class="stat-card">
+          <text class="stat-label">订单数</text>
+          <text class="stat-value">{{ overview.totalOrders || 0 }}</text>
+        </view>
+        <view class="stat-card">
+          <text class="stat-label">客单价</text>
+          <text class="stat-value">¥{{ formatMoney(overview.avgOrderAmount || 0) }}</text>
+        </view>
+        <view class="stat-card">
+          <text class="stat-label">新增客户</text>
+          <text class="stat-value">{{ overview.newUsers || 0 }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 商品销售排行 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">商品销售TOP10</text>
+      </view>
+      <view class="rank-card">
+        <view
+          class="rank-item"
+          v-for="(item, index) in productTopList"
+          :key="item.productId || index"
+        >
+          <view class="rank-num" :class="{ top: index < 3 }">
+            <text>{{ index + 1 }}</text>
+          </view>
+          <view class="rank-info">
+            <text class="rank-name">{{ item.productName || '未知商品' }}</text>
+            <text class="rank-meta">{{ item.category || '未分类' }} · 销量 {{ item.quantity || 0 }}</text>
+          </view>
+          <view class="rank-value">
+            <text class="rank-amount">¥{{ formatMoney(item.salesAmount || 0) }}</text>
+          </view>
+        </view>
+        <view class="empty-mini" v-if="productTopList.length === 0">
+          <text>暂无数据</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 门店业绩排行 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">门店业绩TOP10</text>
+      </view>
+      <view class="rank-card">
+        <view
+          class="rank-item"
+          v-for="(item, index) in shopRankingList"
+          :key="item.shopId || index"
+        >
+          <view class="rank-num" :class="{ top: index < 3 }">
+            <text>{{ index + 1 }}</text>
+          </view>
+          <view class="rank-info">
+            <text class="rank-name">{{ item.shopName || '未知门店' }}</text>
+            <text class="rank-meta">订单 {{ item.orderCount || 0 }} · 客单 ¥{{ formatMoney(item.avgOrderAmount || 0) }}</text>
+          </view>
+          <view class="rank-value">
+            <text class="rank-amount">¥{{ formatMoney(item.salesAmount || 0) }}</text>
+          </view>
+        </view>
+        <view class="empty-mini" v-if="shopRankingList.length === 0">
+          <text>暂无数据</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading">
+      <view class="loading-spinner"></view>
+      <text>加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import NavBar from '@/components/NavBar.vue';
+import { getStatisticsOverview, getProductTop, getShopRanking } from '@/api/statistics';
+import { formatMoney as formatMoneyUtil } from '@/utils/common';
+
+const loading = ref(false);
+const currentTime = ref('week');
+const overview = ref<any>({});
+const productTopList = ref<any[]>([]);
+const shopRankingList = ref<any[]>([]);
+
+const timeOptions = [
+  { label: '今日', value: 'today' },
+  { label: '本周', value: 'week' },
+  { label: '本月', value: 'month' }
+];
+
+const formatMoney = formatMoneyUtil;
+
+const getDateRange = (type: string) => {
+  const now = new Date();
+  const endDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
+  let startDate = endDate;
+
+  if (type === 'today') {
+    startDate = endDate;
+  } else if (type === 'week') {
+    const weekAgo = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000);
+    startDate = `${weekAgo.getFullYear()}-${String(weekAgo.getMonth() + 1).padStart(2, '0')}-${String(weekAgo.getDate()).padStart(2, '0')}`;
+  } else if (type === 'month') {
+    const monthAgo = new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000);
+    startDate = `${monthAgo.getFullYear()}-${String(monthAgo.getMonth() + 1).padStart(2, '0')}-${String(monthAgo.getDate()).padStart(2, '0')}`;
+  }
+
+  return { startDate, endDate };
+};
+
+const getTrendText = (trend: any) => {
+  if (!trend || !Array.isArray(trend) || trend.length < 2) return '-';
+  const current = trend[trend.length - 1]?.value || 0;
+  const prev = trend[trend.length - 2]?.value || 0;
+  if (prev === 0) return '-';
+  const change = ((current - prev) / prev * 100).toFixed(1);
+  return `${change}%`;
+};
+
+const changeTimeRange = (type: string) => {
+  currentTime.value = type;
+  loadData();
+};
+
+const loadData = async () => {
+  loading.value = true;
+  const { startDate, endDate } = getDateRange(currentTime.value);
+
+  try {
+    const [overviewRes, productRes, shopRes] = await Promise.all([
+      getStatisticsOverview({ startDate, endDate }),
+      getProductTop({ startDate, endDate, type: currentTime.value, limit: 10 }),
+      getShopRanking({ startDate, endDate, type: currentTime.value, limit: 10 })
+    ]);
+
+    overview.value = overviewRes || {};
+    productTopList.value = Array.isArray(productRes) ? productRes : (productRes?.list || []);
+    shopRankingList.value = Array.isArray(shopRes) ? shopRes : (shopRes?.list || []);
+  } catch (error) {
+    console.error('加载统计数据失败', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  loadData();
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f8fafc;
+  padding-bottom: 40rpx;
+}
+
+/* 时间筛选 */
+.time-filter {
+  display: flex;
+  padding: 16rpx 24rpx;
+  gap: 12rpx;
+  background: #ffffff;
+  border-bottom: 1rpx solid #e2e8f0;
+
+  .filter-btn {
+    padding: 12rpx 28rpx;
+    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;
+      font-weight: 500;
+    }
+  }
+}
+
+/* 核心指标 */
+.stats-section {
+  padding: 16rpx 24rpx;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 12rpx;
+}
+
+.stat-card {
+  background: #ffffff;
+  border: 1rpx solid #e2e8f0;
+  border-radius: 16rpx;
+  padding: 24rpx 20rpx;
+  display: flex;
+  flex-direction: column;
+
+  &.primary {
+    background: #ecfdf5;
+    border-color: #10b981;
+  }
+
+  .stat-label {
+    font-size: 22rpx;
+    color: #94a3b8;
+    margin-bottom: 8rpx;
+  }
+
+  .stat-value {
+    font-size: 36rpx;
+    font-weight: 700;
+    color: #1e293b;
+    margin-bottom: 4rpx;
+  }
+
+  .stat-trend {
+    font-size: 20rpx;
+    color: #10b981;
+  }
+}
+
+/* 排行区域 */
+.section {
+  padding: 0 24rpx;
+  margin-bottom: 16rpx;
+}
+
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+  padding: 8rpx 0;
+
+  .section-title {
+    font-size: 30rpx;
+    font-weight: 600;
+    color: #1e293b;
+  }
+}
+
+.rank-card {
+  background: #ffffff;
+  border: 1rpx solid #e2e8f0;
+  border-radius: 16rpx;
+  padding: 8rpx 16rpx;
+}
+
+.rank-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 8rpx;
+  border-bottom: 1rpx solid #f1f5f9;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.rank-num {
+  width: 44rpx;
+  height: 44rpx;
+  border-radius: 12rpx;
+  background: #f1f5f9;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+
+  text {
+    font-size: 24rpx;
+    font-weight: 600;
+    color: #94a3b8;
+  }
+
+  &.top {
+    background: #10b981;
+
+    text {
+      color: #fff;
+    }
+  }
+}
+
+.rank-info {
+  flex: 1;
+  min-width: 0;
+
+  .rank-name {
+    display: block;
+    font-size: 28rpx;
+    color: #1e293b;
+    font-weight: 500;
+    margin-bottom: 4rpx;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .rank-meta {
+    font-size: 22rpx;
+    color: #94a3b8;
+  }
+}
+
+.rank-value {
+  flex-shrink: 0;
+  margin-left: 12rpx;
+
+  .rank-amount {
+    font-size: 28rpx;
+    font-weight: 600;
+    color: #f97316;
+  }
+}
+
+.empty-mini {
+  text-align: center;
+  padding: 40rpx 0;
+  font-size: 26rpx;
+  color: #94a3b8;
+}
+
+/* 加载状态 */
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60rpx 0;
+  color: #94a3b8;
+  font-size: 28rpx;
+
+  .loading-spinner {
+    width: 48rpx;
+    height: 48rpx;
+    border: 4rpx solid #e2e8f0;
+    border-top-color: #10b981;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 16rpx;
+  }
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+</style>

+ 2 - 2
haha-admin-mp/src/utils/config.ts

@@ -3,10 +3,10 @@
  */
 
 // API基础路径
-export const API_BASE_URL = 'http://localhost:7077/api/admin';
+export const API_BASE_URL = 'http://localhost:7070/admin';
 
 // 是否使用Mock数据
-export const USE_MOCK = true;
+export const USE_MOCK = false;
 
 // 应用名称
 export const APP_NAME = '哈哈运营平台';

+ 127 - 0
haha-admin-mp/src/utils/constants.ts

@@ -0,0 +1,127 @@
+/**
+ * 全局枚举常量定义
+ * 从 mock 数据迁移而来,供运行时代码统一引用
+ */
+
+// ==================== 订单状态 ====================
+
+export enum OrderStatus {
+  PENDING = 0,      // 待支付
+  PAID = 1,         // 已支付
+  COMPLETED = 2,    // 已完成
+  CANCELLED = 3,    // 已取消
+  REFUNDING = 4,    // 退款中
+  REFUNDED = 5      // 已退款
+}
+
+export const OrderStatusText: Record<number, string> = {
+  [OrderStatus.PENDING]: '待支付',
+  [OrderStatus.PAID]: '已支付',
+  [OrderStatus.COMPLETED]: '已完成',
+  [OrderStatus.CANCELLED]: '已取消',
+  [OrderStatus.REFUNDING]: '退款中',
+  [OrderStatus.REFUNDED]: '已退款'
+};
+
+export const OrderStatusColor: Record<number, string> = {
+  [OrderStatus.PENDING]: '#faad14',
+  [OrderStatus.PAID]: '#1890ff',
+  [OrderStatus.COMPLETED]: '#52c41a',
+  [OrderStatus.CANCELLED]: '#999999',
+  [OrderStatus.REFUNDING]: '#ff4d4f',
+  [OrderStatus.REFUNDED]: '#999999'
+};
+
+// ==================== 设备状态 ====================
+
+export enum DeviceStatus {
+  ONLINE = 1,       // 在线
+  OFFLINE = 0,      // 离线
+  MAINTENANCE = 2,  // 维护中
+  ERROR = 3         // 故障
+}
+
+export const DeviceStatusText: Record<number, string> = {
+  [DeviceStatus.ONLINE]: '在线',
+  [DeviceStatus.OFFLINE]: '离线',
+  [DeviceStatus.MAINTENANCE]: '维护中',
+  [DeviceStatus.ERROR]: '故障'
+};
+
+export const DeviceStatusColor: Record<number, string> = {
+  [DeviceStatus.ONLINE]: '#52c41a',
+  [DeviceStatus.OFFLINE]: '#999999',
+  [DeviceStatus.MAINTENANCE]: '#faad14',
+  [DeviceStatus.ERROR]: '#ff4d4f'
+};
+
+// ==================== 商品状态 ====================
+
+export enum ProductStatus {
+  ACTIVE = 1,      // 在售
+  INACTIVE = 0,    // 下架
+  OUT_OF_STOCK = 2 // 缺货
+}
+
+export const ProductStatusText: Record<number, string> = {
+  [ProductStatus.ACTIVE]: '在售',
+  [ProductStatus.INACTIVE]: '已下架',
+  [ProductStatus.OUT_OF_STOCK]: '缺货'
+};
+
+export const ProductStatusColor: Record<number, string> = {
+  [ProductStatus.ACTIVE]: '#52c41a',
+  [ProductStatus.INACTIVE]: '#999999',
+  [ProductStatus.OUT_OF_STOCK]: '#ff4d4f'
+};
+
+// ==================== 库存状态 ====================
+
+export enum InventoryStatus {
+  NORMAL = 'normal',
+  LOW = 'low',
+  OUT = 'out'
+}
+
+export const InventoryStatusText: Record<string, string> = {
+  [InventoryStatus.NORMAL]: '正常',
+  [InventoryStatus.LOW]: '低库存',
+  [InventoryStatus.OUT]: '缺货'
+};
+
+// ==================== 门店状态 ====================
+
+export enum ShopStatus {
+  ACTIVE = 1,       // 正常营业
+  INACTIVE = 0,     // 停业
+  MAINTENANCE = 2   // 维护中
+}
+
+export const ShopStatusText: Record<number, string> = {
+  [ShopStatus.ACTIVE]: '营业中',
+  [ShopStatus.INACTIVE]: '已停业',
+  [ShopStatus.MAINTENANCE]: '维护中'
+};
+
+export const ShopStatusColor: Record<number, string> = {
+  [ShopStatus.ACTIVE]: '#52c41a',
+  [ShopStatus.INACTIVE]: '#999999',
+  [ShopStatus.MAINTENANCE]: '#faad14'
+};
+
+// ==================== 用户/客户状态 ====================
+
+export enum UserStatus {
+  DISABLED = 0,   // 禁用
+  ENABLED = 1     // 正常
+}
+
+export const UserStatusText: Record<number, string> = {
+  [UserStatus.DISABLED]: '禁用',
+  [UserStatus.ENABLED]: '正常'
+};
+
+// ==================== 通用分页参数 ====================
+
+export const DEFAULT_PAGE_SIZE = 10;
+export const MAX_PAGE_SIZE = 100;

+ 2 - 19
haha-admin-mp/src/utils/request.ts

@@ -3,7 +3,7 @@
  */
 
 import { getToken } from './auth';
-import { API_BASE_URL, USE_MOCK } from './config';
+import { API_BASE_URL } from './config';
 
 interface RequestOptions {
   url: string;
@@ -26,18 +26,13 @@ interface Response<T = any> {
 export function request<T = any>(options: RequestOptions): Promise<T> {
   const { url, method = 'GET', data, header = {}, showLoading = true, showError = true } = options;
 
-  // 如果使用Mock数据,返回Mock数据
-  if (USE_MOCK) {
-    return handleMockRequest<T>(url, method, data);
-  }
-
   if (showLoading) {
     uni.showLoading({ title: '加载中...', mask: true });
   }
 
   const token = getToken();
   if (token) {
-    header['Authorization'] = `Bearer ${token}`;
+    header['adminAccessToken'] = token;
   }
   header['Content-Type'] = 'application/json';
 
@@ -87,18 +82,6 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
   });
 }
 
-/**
- * 处理Mock请求 - 简单实现,实际Mock数据在mock.ts中
- */
-function handleMockRequest<T>(url: string, method: string, data: any): Promise<T> {
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      // 这里返回空数据,实际由各个API模块处理
-      resolve({} as T);
-    }, 300);
-  });
-}
-
 /**
  * GET请求
  */

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff