Explorar o código

智能柜项目提交

skyline hai 2 meses
pai
achega
4eec8ea808
Modificáronse 45 ficheiros con 4372 adicións e 309 borrados
  1. 103 0
      haha-admin-web/src/api/marketing.ts
  2. 16 0
      haha-admin-web/src/api/user.ts
  3. 4 0
      haha-admin-web/src/components/Amap/index.ts
  4. 515 0
      haha-admin-web/src/components/Amap/src/index.vue
  5. 3 0
      haha-admin-web/src/components/AmapWithSearch/index.ts
  6. 715 0
      haha-admin-web/src/components/AmapWithSearch/src/index.vue
  7. 40 6
      haha-admin-web/src/router/modules/marketing.ts
  8. 9 0
      haha-admin-web/src/router/modules/operation.ts
  9. 76 0
      haha-admin-web/src/utils/paginationHelper.ts
  10. 11 3
      haha-admin-web/src/views/checkin/utils/hook.tsx
  11. 14 6
      haha-admin-web/src/views/customer/utils/hook.tsx
  12. 13 4
      haha-admin-web/src/views/device/utils/hook.tsx
  13. 9 3
      haha-admin-web/src/views/inventory/records/utils/hook.tsx
  14. 14 6
      haha-admin-web/src/views/inventory/utils/hook.tsx
  15. 220 0
      haha-admin-web/src/views/map-test/index.vue
  16. 21 0
      haha-admin-web/src/views/marketing/activity/index.vue
  17. 225 77
      haha-admin-web/src/views/marketing/activity/utils/hook.tsx
  18. 141 0
      haha-admin-web/src/views/marketing/components/CouponPreview.vue
  19. 95 0
      haha-admin-web/src/views/marketing/components/ProductSelector.vue
  20. 92 0
      haha-admin-web/src/views/marketing/components/ShopSelector.vue
  21. 20 0
      haha-admin-web/src/views/marketing/coupon/index.vue
  22. 109 9
      haha-admin-web/src/views/marketing/coupon/utils/hook.tsx
  23. 151 0
      haha-admin-web/src/views/marketing/statistics/activity.vue
  24. 233 0
      haha-admin-web/src/views/marketing/statistics/coupon.vue
  25. 89 0
      haha-admin-web/src/views/marketing/statistics/index.vue
  26. 357 0
      haha-admin-web/src/views/marketing/statistics/overview.vue
  27. 266 0
      haha-admin-web/src/views/marketing/statistics/trend.vue
  28. 104 54
      haha-admin-web/src/views/order/utils/hook.tsx
  29. 8 9
      haha-admin-web/src/views/order/utils/types.ts
  30. 209 0
      haha-admin-web/src/views/product/components/EnhancedImage.vue
  31. 2 0
      haha-admin-web/src/views/product/index.vue
  32. 19 10
      haha-admin-web/src/views/product/new-apply/utils/hook.tsx
  33. 91 0
      haha-admin-web/src/views/product/styles/image-preview.scss
  34. 76 35
      haha-admin-web/src/views/product/utils/hook.tsx
  35. 10 4
      haha-admin-web/src/views/product/utils/types.ts
  36. 162 47
      haha-admin-web/src/views/shop/utils/hook.tsx
  37. 8 3
      haha-admin-web/src/views/sync/utils/hook.tsx
  38. 15 6
      haha-admin-web/src/views/system/admin/utils/hook.tsx
  39. 9 3
      haha-admin-web/src/views/system/announcement/utils/hook.tsx
  40. 11 3
      haha-admin-web/src/views/system/operation-log/utils/hook.tsx
  41. 12 4
      haha-admin-web/src/views/system/role/utils/hook.tsx
  42. 23 9
      haha-admin-web/src/views/system/user/utils/hook.tsx
  43. 11 3
      haha-admin/src/main/java/com/haha/admin/controller/ProductController.java
  44. 5 1
      haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java
  45. 36 4
      haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

+ 103 - 0
haha-admin-web/src/api/marketing.ts

@@ -0,0 +1,103 @@
+import { http } from "@/utils/http";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+
+// 营销统计相关API
+
+export const getMarketingOverview = () => {
+  return http.request<Result>("get", "/marketing/statistics/overview");
+};
+
+export const getActivityStatistics = (id: number, params?: {
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<Result>("get", `/marketing/statistics/activity/${id}`, { params });
+};
+
+export const getCouponStatistics = (id: number, params?: {
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<Result>("get", `/marketing/statistics/coupon/${id}`, { params });
+};
+
+export const getTrendAnalysis = (params: {
+  type: string; // activity, coupon, revenue
+  period: string; // day, week, month
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<Result>("get", "/marketing/statistics/trend", { params });
+};
+
+export const exportStatistics = (params: {
+  type: string;
+  startDate: string;
+  endDate: string;
+}) => {
+  return http.request<Result>("get", "/marketing/statistics/export", { 
+    params,
+    responseType: "blob"
+  });
+};
+
+// 优惠券发放相关API
+
+export const getCouponDistributeRecords = (params: {
+  page?: number;
+  pageSize?: number;
+  templateId?: number;
+  distributeType?: number;
+}) => {
+  return http.request<ResultTable>("get", "/marketing/coupon/distribute/list", { params });
+};
+
+export const distributeCoupons = (data: {
+  templateId: number;
+  distributeType: number; // 1-主动领取 2-系统发放 3-定向发放
+  targetType: number; // 1-全部用户 2-新用户 3-指定用户
+  userIds?: number[]; // 指定用户ID列表
+  quantity?: number; // 发放数量
+}) => {
+  return http.request<Result>("post", "/marketing/coupon/distribute", { data });
+};
+
+// 活动详情相关API
+
+export const getActivityDetail = (id: number) => {
+  return http.request<Result>("get", `/marketing/activity/${id}`);
+};
+
+export const updateActivityDetail = (id: number, data: any) => {
+  return http.request<Result>("put", `/marketing/activity/${id}`, { data });
+};
+
+export const getActivityCoupons = (activityId: number) => {
+  return http.request<Result>("get", `/marketing/activity/${activityId}/coupons`);
+};
+
+// 优惠券详情相关API
+
+export const getCouponDetail = (id: number) => {
+  return http.request<Result>("get", `/marketing/coupon/${id}`);
+};
+
+export const updateCouponDetail = (id: number, data: any) => {
+  return http.request<Result>("put", `/marketing/coupon/${id}`, { data });
+};

+ 16 - 0
haha-admin-web/src/api/user.ts

@@ -75,3 +75,19 @@ export const updateUserCreditScore = (
 ) => {
   return http.request<Result>("put", `/users/${id}/credit`, { data });
 };
+
+export type UserInfo = {
+  avatar: string;
+  nickname: string;
+  email: string;
+  phone: string;
+  description: string;
+};
+
+export const getMine = () => {
+  return http.request<Result>("get", "/admin/mine");
+};
+
+export const getMineLogs = (params: { page?: number; pageSize?: number }) => {
+  return http.request<ResultTable>("get", "/admin/logs", { params });
+};

+ 4 - 0
haha-admin-web/src/components/Amap/index.ts

@@ -0,0 +1,4 @@
+import Amap from "./src/index.vue";
+
+export { Amap };
+export default Amap;

+ 515 - 0
haha-admin-web/src/components/Amap/src/index.vue

@@ -0,0 +1,515 @@
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
+import AMapLoader from "@amap/amap-jsapi-loader";
+
+interface Props {
+  longitude?: number;
+  latitude?: number;
+  zoom?: number;
+  height?: string;
+  enableClickMark?: boolean;
+  securityJsCode?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  longitude: 116.397428,
+  latitude: 39.90923,
+  zoom: 15,
+  height: "400px",
+  enableClickMark: false,
+  securityJsCode: "f58a434f5aa44a00c811862862e7760b"
+});
+
+const emit = defineEmits<{
+  (e: "update:longitude", value: number): void;
+  (e: "update:latitude", value: number): void;
+  (e: "markerMoved", lng: number, lat: number): void;
+}>();
+
+const mapContainer = ref<HTMLDivElement>();
+const mapLoading = ref(true);
+const mapError = ref("");
+const loadingProgress = ref(0);
+const loadingStep = ref(0);
+const showFallback = ref(false);
+
+let map: any = null;
+let marker: any = null;
+let AMap: any = null;
+
+const hasValidCoords = () => {
+  return (
+    props.longitude &&
+    props.latitude &&
+    props.longitude >= -180 &&
+    props.longitude <= 180 &&
+    props.latitude >= -90 &&
+    props.latitude <= 90
+  );
+};
+
+const updatePosition = (lng: number, lat: number) => {
+  emit("update:longitude", lng);
+  emit("update:latitude", lat);
+  emit("markerMoved", lng, lat);
+};
+
+const addMarker = (lng: number, lat: number) => {
+  if (!map || !AMap) return;
+
+  if (marker) {
+    map.remove(marker);
+  }
+
+  marker = new AMap.Marker({
+    position: [lng, lat],
+    title: "门店位置",
+    draggable: props.enableClickMark,
+    cursor: "move",
+    animation: "AMAP_ANIMATION_DROP"
+  });
+
+  if (props.enableClickMark) {
+    marker.on("dragend", (e: any) => {
+      const lngLat = e.lnglat;
+      updatePosition(lngLat.lng, lngLat.lat);
+      addMarker(lngLat.lng, lngLat.lat);
+    });
+  }
+
+  map.add(marker);
+  map.setCenter([lng, lat]);
+};
+
+const reloadMap = () => {
+  if (map) {
+    map.destroy();
+    map = null;
+  }
+  showFallback.value = false;
+  initMap();
+};
+
+const showFallbackMap = () => {
+  showFallback.value = true;
+  mapLoading.value = false;
+  mapError.value = '';
+};
+
+const hideFallbackMap = () => {
+  showFallback.value = false;
+};
+
+const initMap = async () => {
+  console.log('[地图初始化] 开始检查...');
+  
+  // 等待 DOM 更新
+  await nextTick();
+  
+  if (!mapContainer.value) {
+    console.error('[地图初始化] 地图容器不存在');
+    mapError.value = '地图容器未准备好';
+    mapLoading.value = false;
+    return;
+  }
+  
+  console.log('[地图初始化] 容器元素:', mapContainer.value);
+  console.log('[地图初始化] 容器高度:', mapContainer.value.style.height);
+  console.log('[地图初始化] 容器 offsetHeight:', mapContainer.value.offsetHeight);
+  
+  if (!AMap) {
+    console.error('[地图初始化] AMap 对象未初始化');
+    mapError.value = '地图 SDK 未加载';
+    mapLoading.value = false;
+    return;
+  }
+
+  console.log('[地图初始化] AMap 对象存在:', !!AMap);
+  console.log('[地图初始化] AMap.Map 构造函数:', typeof AMap.Map);
+
+  mapLoading.value = true;
+  mapError.value = "";
+
+  try {
+    const initialCenter = hasValidCoords()
+      ? [props.longitude, props.latitude]
+      : [116.397428, 39.90923];
+
+    console.log('[地图初始化] 开始创建地图实例...');
+    console.log('[地图初始化] 中心点:', initialCenter);
+    console.log('[地图初始化] 缩放级别:', props.zoom);
+    
+    map = new AMap.Map(mapContainer.value, {
+      zoom: props.zoom,
+      center: initialCenter,
+      resizeEnable: true,
+      viewMode: "2D",
+      keyboardEnable: true
+    });
+
+    console.log('[地图初始化] 地图实例创建成功:', !!map);
+    console.log('[地图初始化] 地图对象:', map);
+
+    if (hasValidCoords()) {
+      console.log('[地图初始化] 添加标记点');
+      addMarker(props.longitude!, props.latitude!);
+    }
+
+    if (props.enableClickMark) {
+      console.log('[地图初始化] 启用点击事件');
+      map.on("click", (e: any) => {
+        const lng = e.lnglat.getLng();
+        const lat = e.lnglat.getLat();
+        updatePosition(lng, lat);
+        addMarker(lng, lat);
+      });
+    }
+
+    console.log('[地图初始化] 完成');
+    mapLoading.value = false;
+  } catch (error: any) {
+    console.error("[地图初始化] 失败:", error);
+    mapError.value = error.message || "地图加载失败,请检查网络连接";
+    mapLoading.value = false;
+  }
+};
+
+const loadAMap = () => {
+  console.log("开始加载高德地图...");
+  
+  // 重置状态
+  loadingProgress.value = 0;
+  loadingStep.value = 0;
+  
+  // 设置超时机制
+  const timeoutId = setTimeout(() => {
+    console.warn('地图加载超时');
+    mapError.value = '地图加载超时,请刷新页面重试';
+    mapLoading.value = false;
+  }, 15000); // 15秒超时
+  
+  // 更新进度
+  loadingStep.value = 1;
+  loadingProgress.value = 20;
+  
+  // 先检查网络连接
+  fetch('https://webapi.amap.com/maps?v=2.0&key=f58a434f5aa44a00c811862862e7760b')
+    .then(response => {
+      clearTimeout(timeoutId); // 清除超时
+      console.log('高德API网络连接测试:', response.status);
+      
+      // 更新进度
+      loadingStep.value = 2;
+      loadingProgress.value = 50;
+      
+      if (!response.ok) {
+        throw new Error(`网络响应错误: ${response.status}`);
+      }
+      return response.text();
+    })
+    .then(data => {
+      console.log('高德API响应数据长度:', data.length);
+      
+      if (props.securityJsCode) {
+        (window as any)._AMapSecurityConfig = {
+          securityJsCode: props.securityJsCode
+        };
+        console.log("已设置安全密钥");
+      }
+
+      // 更新进度
+      loadingStep.value = 3;
+      loadingProgress.value = 80;
+
+      AMapLoader.load({
+        key: "f58a434f5aa44a00c811862862e7760b",
+        version: "2.0",
+        plugins: ["AMap.Scale", "AMap.ToolBar"]
+      })
+        .then((AMapInstance) => {
+          clearTimeout(timeoutId); // 清除超时
+          console.log("高德地图API加载成功");
+          AMap = AMapInstance;
+          (window as any).AMap = AMapInstance;
+          
+          // 完成进度
+          loadingProgress.value = 100;
+          
+          initMap();
+        })
+        .catch((error) => {
+          clearTimeout(timeoutId); // 清除超时
+          console.error("加载高德地图失败:", error);
+          mapError.value = `地图加载失败: ${error.message || '未知错误'}. 请检查网络连接或API密钥是否正确`;
+          mapLoading.value = false;
+        });
+    })
+    .catch(error => {
+      clearTimeout(timeoutId); // 清除超时
+      console.error('网络连接测试失败:', error);
+      mapError.value = `网络连接测试失败: ${error.message}. 请检查网络连接`;
+      mapLoading.value = false;
+    });
+};
+
+watch(
+  () => [props.longitude, props.latitude],
+  ([newLng, newLat]) => {
+    if (newLng && newLat && map) {
+      addMarker(newLng as number, newLat as number);
+      map.setCenter([newLng, newLat]);
+    }
+  }
+);
+
+onMounted(() => {
+  console.log('地图组件挂载,容器存在:', !!mapContainer.value);
+  loadAMap();
+});
+
+onBeforeUnmount(() => {
+  if (map) {
+    map.destroy();
+    map = null;
+  }
+});
+
+defineExpose({
+  reloadMap,
+  addMarker
+});
+</script>
+
+<template>
+  <div class="amap-wrapper">
+    <!-- 地图容器始终渲染 -->
+    <div ref="mapContainer" class="map-container" :style="{ height }"></div>
+    
+    <!-- 加载状态覆盖层 -->
+    <div v-if="mapLoading" class="loading-overlay">
+      <div class="loading-spinner"></div>
+      <span>地图加载中...</span>
+      <div class="loading-progress">
+        <div class="progress-bar" :style="{ width: loadingProgress + '%' }"></div>
+      </div>
+      <div class="loading-steps">
+        <div :class="{ 'step-active': loadingStep >= 1 }">🌐 网络检测</div>
+        <div :class="{ 'step-active': loadingStep >= 2 }">🔑 API 验证</div>
+        <div :class="{ 'step-active': loadingStep >= 3 }">🗺️ 地图初始化</div>
+      </div>
+    </div>
+    
+    <!-- 错误状态覆盖层 -->
+    <div v-else-if="mapError" class="error-overlay">
+      <div class="error-icon">⚠️</div>
+      <span>{{ mapError }}</span>
+      <div class="error-actions">
+        <el-button size="small" @click="reloadMap">重新加载</el-button>
+        <el-button size="small" @click="showFallbackMap">显示备用地图</el-button>
+      </div>
+    </div>
+    <div
+      v-if="hasValidCoords() && !mapLoading && !mapError"
+      class="coord-display"
+    >
+      <span>📍</span>
+      <span>经度: {{ props.longitude?.toFixed(6) }}</span>
+      <span>|</span>
+      <span>纬度: {{ props.latitude?.toFixed(6) }}</span>
+    </div>
+    <div v-else-if="!mapLoading && !mapError" class="coord-empty">
+      <span>📍</span>
+      <span>暂无位置信息,请在地图上点击选择位置</span>
+    </div>
+    
+    <!-- 备用静态地图 -->
+    <div v-if="showFallback" class="fallback-map">
+      <div class="fallback-content">
+        <h3>备用地图视图</h3>
+        <div class="fallback-coords">
+          <p><strong>经度:</strong> {{ props.longitude || '未设置' }}</p>
+          <p><strong>纬度:</strong> {{ props.latitude || '未设置' }}</p>
+        </div>
+        <div class="fallback-actions">
+          <el-button @click="hideFallbackMap">关闭备用地图</el-button>
+          <el-button type="primary" @click="reloadMap">重试加载</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.amap-wrapper {
+  position: relative;
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.map-container {
+  width: 100%;
+  height: 100%;
+  min-height: 300px;
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  position: relative;
+}
+
+.loading-overlay,
+.error-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.9);
+  z-index: 1000;
+}
+
+.loading-state,
+.error-state,
+.coord-display,
+.coord-empty {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  margin-top: 8px;
+}
+
+.error-state {
+  flex-direction: column;
+  color: #f56c6c;
+  padding: 24px;
+}
+
+.loading-state {
+  color: #409eff;
+  text-align: center;
+  padding: 20px;
+}
+
+.loading-spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid #f3f3f3;
+  border-top: 4px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin: 0 auto 15px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading-progress {
+  width: 80%;
+  height: 6px;
+  background-color: #e4e7ed;
+  border-radius: 3px;
+  margin: 15px auto;
+  overflow: hidden;
+}
+
+.progress-bar {
+  height: 100%;
+  background-color: #409eff;
+  transition: width 0.3s ease;
+}
+
+.loading-steps {
+  display: flex;
+  justify-content: space-around;
+  margin-top: 15px;
+  font-size: 12px;
+}
+
+.loading-steps div {
+  opacity: 0.5;
+  transition: all 0.3s ease;
+}
+
+.step-active {
+  opacity: 1;
+  color: #409eff;
+  font-weight: bold;
+}
+
+.error-icon {
+  font-size: 32px;
+  margin-bottom: 10px;
+}
+
+.error-actions {
+  margin-top: 15px;
+}
+
+.error-actions .el-button {
+  margin: 0 5px;
+}
+
+.fallback-map {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.95);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.fallback-content {
+  background: white;
+  padding: 30px;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  text-align: center;
+  max-width: 400px;
+}
+
+.fallback-content h3 {
+  margin-top: 0;
+  color: #333;
+}
+
+.fallback-coords {
+  text-align: left;
+  margin: 20px 0;
+}
+
+.fallback-coords p {
+  margin: 10px 0;
+  font-size: 14px;
+}
+
+.fallback-actions {
+  margin-top: 20px;
+}
+
+.fallback-actions .el-button {
+  margin: 0 5px;
+}
+
+.coord-display {
+  color: #606266;
+  font-size: 13px;
+}
+
+.coord-empty {
+  color: #909399;
+  font-size: 13px;
+}
+</style>

+ 3 - 0
haha-admin-web/src/components/AmapWithSearch/index.ts

@@ -0,0 +1,3 @@
+import AmapWithSearch from "./src/index.vue";
+
+export { AmapWithSearch };

+ 715 - 0
haha-admin-web/src/components/AmapWithSearch/src/index.vue

@@ -0,0 +1,715 @@
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
+import AMapLoader from "@amap/amap-jsapi-loader";
+import { Search } from "@element-plus/icons-vue";
+
+interface Props {
+  longitude?: number;
+  latitude?: number;
+  zoom?: number;
+  height?: string;
+  enableClickMark?: boolean;
+  securityJsCode?: string;
+  showSearch?: boolean; // 是否显示地址搜索框
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  zoom: 15,
+  height: "400px",
+  enableClickMark: false,
+  securityJsCode: "",
+  showSearch: false
+});
+
+const emit = defineEmits<{
+  "update:longitude": [value: number];
+  "update:latitude": [value: number];
+  markerMoved: [lng: number, lat: number];
+}>();
+
+const mapContainer = ref<HTMLDivElement>();
+const searchInput = ref("");
+const mapLoading = ref(true);
+const mapError = ref("");
+const loadingProgress = ref(0);
+const loadingStep = ref(0);
+const showFallback = ref(false);
+
+let map: any = null;
+let marker: any = null;
+let geocoder: any = null;
+let placeSearch: any = null; // 使用 PlaceSearch 替代 Geocoder
+
+const hasValidCoords = () => {
+  return (
+    props.longitude !== undefined &&
+    props.latitude !== undefined &&
+    props.longitude !== null &&
+    props.latitude !== null &&
+    props.longitude !== 0 &&
+    props.latitude !== 0
+  );
+};
+
+const updatePosition = (lng: number, lat: number) => {
+  emit("update:longitude", lng);
+  emit("update:latitude", lat);
+  emit("markerMoved", lng, lat);
+};
+
+const addMarker = (lng: number, lat: number) => {
+  if (!map || !AMap) return;
+
+  if (marker) {
+    map.remove(marker);
+  }
+
+  marker = new AMap.Marker({
+    position: [lng, lat],
+    title: "门店位置",
+    draggable: props.enableClickMark,
+    cursor: "move",
+    animation: "AMAP_ANIMATION_DROP"
+  });
+
+  if (props.enableClickMark) {
+    marker.on("dragend", (e: any) => {
+      const lngLat = e.lnglat;
+      updatePosition(lngLat.lng, lngLat.lat);
+      addMarker(lngLat.lng, lngLat.lat);
+    });
+  }
+
+  map.add(marker);
+  map.setCenter([lng, lat]);
+};
+
+const reloadMap = () => {
+  if (map) {
+    map.destroy();
+    map = null;
+  }
+  showFallback.value = false;
+  initMap();
+};
+
+const showFallbackMap = () => {
+  showFallback.value = true;
+  mapLoading.value = false;
+  mapError.value = '';
+};
+
+const hideFallbackMap = () => {
+  showFallback.value = false;
+};
+
+// 使用 PlaceSearch 进行地址搜索
+const searchAddress = async (address: string) => {
+  console.log('[地址搜索] 开始执行:', address);
+  console.log('[地址搜索] placeSearch:', !!placeSearch);
+  console.log('[地址搜索] AMap:', !!AMap);
+  
+  if (!placeSearch || !AMap) {
+    console.error('[地址搜索] PlaceSearch 未初始化');
+    alert('地图组件正在初始化,请稍后再试');
+    return;
+  }
+
+  try {
+    console.log('[地址搜索] 调用 search 方法...');
+    
+    const result = await new Promise((resolve, reject) => {
+      let completed = false;
+      
+      const timeoutId = setTimeout(() => {
+        if (!completed) {
+          console.error('[地址搜索] 超时(10 秒)');
+          reject(new Error('地址搜索超时,请检查网络或重试'));
+        }
+      }, 10000);
+      
+      placeSearch.search(address, (status: string, result: any) => {
+        completed = true;
+        clearTimeout(timeoutId);
+        
+        console.log('[地址搜索] 回调执行:', { status, hasResult: !!result, poiCount: result?.poiList?.count, errorInfo: result?.info });
+        
+        if (status === 'complete' && result.poiList && result.poiList.count > 0) {
+          console.log('[地址搜索] 成功,解析结果');
+          resolve(result.poiList.pois[0]); // 返回第一个结果
+        } else {
+          const errorMsg = result?.info || result?.infocode || '未找到该地址';
+          console.error('[地址搜索] 详细错误:', { status, info: result?.info, infocode: result?.infocode });
+          reject(new Error(errorMsg));
+        }
+      });
+    });
+
+    const poi = result as any;
+    const location = poi.location; // POI 对象包含 location 属性
+    
+    console.log('[地址搜索] 搜索结果:', location);
+    
+    // 更新地图中心点和标记
+    updatePosition(location.lng, location.lat);
+    addMarker(location.lng, location.lat);
+    
+    // 设置缩放级别
+    map.setZoomAndCenter(15, [location.lng, location.lat]);
+  } catch (error: any) {
+    console.error('[地址搜索] 失败:', error);
+    alert(error.message || '地址搜索失败,请尝试其他地址');
+  }
+};
+
+// 处理搜索
+const handleSearch = () => {
+  console.log('[搜索] 开始搜索:', searchInput.value);
+  console.log('[搜索] placeSearch 状态:', !!placeSearch);
+  console.log('[搜索] AMap 状态:', !!AMap);
+  
+  if (!searchInput.value.trim()) {
+    alert('请输入地址');
+    return;
+  }
+  
+  // 确保 PlaceSearch 已初始化
+  if (!placeSearch || !AMap) {
+    console.error('[搜索] PlaceSearch 未初始化');
+    alert('地图组件正在初始化,请稍后再试');
+    return;
+  }
+  
+  console.log('[搜索] 准备调用 searchAddress:', searchInput.value);
+  searchAddress(searchInput.value);
+};
+
+// 按回车搜索
+const handleKeyPress = (e: KeyboardEvent) => {
+  if (e.key === 'Enter') {
+    handleSearch();
+  }
+};
+
+// 获取当前位置
+const getCurrentLocation = () => {
+  if (navigator.geolocation) {
+    navigator.geolocation.getCurrentPosition(
+      (position) => {
+        const lng = position.coords.longitude;
+        const lat = position.coords.latitude;
+        console.log('获取到当前位置:', lng, lat);
+        updatePosition(lng, lat);
+        addMarker(lng, lat);
+        map.setCenter([lng, lat]);
+        map.setZoom(15);
+      },
+      (error) => {
+        console.warn('获取当前位置失败:', error);
+        // 使用默认位置(北京)
+        const defaultLng = 116.397428;
+        const defaultLat = 39.90923;
+        updatePosition(defaultLng, defaultLat);
+        addMarker(defaultLng, defaultLat);
+      }
+    );
+  } else {
+    console.warn('浏览器不支持地理位置');
+    // 使用默认位置
+    const defaultLng = 116.397428;
+    const defaultLat = 39.90923;
+    updatePosition(defaultLng, defaultLat);
+    addMarker(defaultLng, defaultLat);
+  }
+};
+
+const initMap = async () => {
+  console.log('[地图初始化] 开始检查...');
+  
+  // 等待 DOM 更新
+  await nextTick();
+  
+  if (!mapContainer.value) {
+    console.error('[地图初始化] 地图容器不存在');
+    mapError.value = '地图容器未准备好';
+    mapLoading.value = false;
+    return;
+  }
+  
+  console.log('[地图初始化] 容器元素:', mapContainer.value);
+  console.log('[地图初始化] 容器高度:', mapContainer.value.style.height);
+  console.log('[地图初始化] 容器 offsetHeight:', mapContainer.value.offsetHeight);
+  
+  if (!AMap) {
+    console.error('[地图初始化] AMap 对象未初始化');
+    mapError.value = '地图 SDK 未加载';
+    mapLoading.value = false;
+    return;
+  }
+
+  console.log('[地图初始化] AMap 对象存在:', !!AMap);
+  console.log('[地图初始化] AMap.Map 构造函数:', typeof AMap.Map);
+
+  mapLoading.value = true;
+  mapError.value = "";
+
+  try {
+    const initialCenter = hasValidCoords()
+      ? [props.longitude, props.latitude]
+      : [116.397428, 39.90923];
+
+    console.log('[地图初始化] 开始创建地图实例...');
+    console.log('[地图初始化] 中心点:', initialCenter);
+    console.log('[地图初始化] 缩放级别:', props.zoom);
+    
+    // 配置地图选项
+    const mapOptions: any = {
+      zoom: props.zoom,
+      center: initialCenter,
+      resizeEnable: true,
+      viewMode: "2D",
+      keyboardEnable: true,
+      scrollWheel: true,
+      doubleClickZoom: true,
+      zoomEnable: true
+    };
+    
+    // 如果 AMap 已加载,添加图层配置
+    if ((window as any).AMap) {
+      mapOptions.layers = [
+        new (window as any).AMap.TileLayer(), // 默认底图
+        new (window as any).AMap.TileLayer.RoadNet() // 路网层
+      ];
+      // 暂时不设置自定义样式,避免 API 错误
+      // mapOptions.mapStyle = 'amap://styles/normal';
+    }
+    
+    map = new AMap.Map(mapContainer.value, mapOptions);
+
+    console.log('[地图初始化] 地图实例创建成功:', !!map);
+    console.log('[地图初始化] 地图对象:', map);
+
+    // 先初始化地理编码插件(在地图实例创建之后)
+    console.log('[地图初始化] 开始加载地理编码插件...');
+    
+    // 使用 Promise 包装插件加载
+    await new Promise((resolve, reject) => {
+      AMap.plugin(['AMap.PlaceSearch'], () => {
+        console.log('[插件加载] 插件加载回调执行');
+        placeSearch = new AMap.PlaceSearch({
+          pageSize: 5,
+          pageIndex: 1,
+          extensions: 'all' // 返回详细信息
+        });
+        
+        console.log('[地图初始化] PlaceSearch 初始化完成');
+        console.log('[地图初始化] placeSearch 对象:', !!placeSearch);
+        console.log('[地图初始化] placeSearch.search:', typeof placeSearch.search);
+        resolve(true);
+      }, (error: any) => {
+        console.error('[插件加载] 失败:', error);
+        reject(error);
+      });
+    });
+
+    if (hasValidCoords()) {
+      console.log('[地图初始化] 添加标记点');
+      addMarker(props.longitude!, props.latitude!);
+    } else if (props.showSearch) {
+      // 如果是新数据且显示搜索框,获取当前位置
+      console.log('[地图初始化] 无坐标数据,获取当前位置');
+      getCurrentLocation();
+    }
+
+    if (props.enableClickMark) {
+      console.log('[地图初始化] 启用点击事件');
+      map.on("click", (e: any) => {
+        const lng = e.lnglat.getLng();
+        const lat = e.lnglat.getLat();
+        updatePosition(lng, lat);
+        addMarker(lng, lat);
+      });
+    }
+
+    console.log('[地图初始化] 完成');
+    mapLoading.value = false;
+  } catch (error: any) {
+    console.error("[地图初始化] 失败:", error);
+    mapError.value = error.message || "地图加载失败,请检查网络连接";
+    mapLoading.value = false;
+  }
+};
+
+const loadAMap = () => {
+  console.log("开始加载高德地图...");
+  
+  // 重置状态
+  loadingProgress.value = 0;
+  loadingStep.value = 0;
+  
+  // 设置超时机制
+  const timeoutId = setTimeout(() => {
+    console.warn('地图加载超时');
+    mapError.value = '地图加载超时,请刷新页面重试';
+    mapLoading.value = false;
+  }, 15000); // 15 秒超时
+  
+  // 更新进度
+  loadingStep.value = 1;
+  loadingProgress.value = 20;
+  
+  // 先检查网络连接
+  fetch('https://webapi.amap.com/maps?v=2.0&key=b1e59efdfdc10271d78db9a556540361')
+    .then(response => {
+      clearTimeout(timeoutId); // 清除超时
+      console.log('高德 API 网络连接测试:', response.status);
+        
+      // 更新进度
+      loadingStep.value = 2;
+      loadingProgress.value = 50;
+        
+      if (!response.ok) {
+        throw new Error(`网络响应错误:${response.status}`);
+      }
+      return response.text();
+    })
+    .then(data => {
+      console.log('高德 API 响应数据长度:', data.length);
+  
+      // 设置安全密钥
+      (window as any)._AMapSecurityConfig = {
+        securityJsCode: 'bcfcaa8d8ef22452d4cf6d3892646262'
+      };
+      console.log('已设置安全密钥');
+
+      // 更新进度
+      loadingStep.value = 3;
+      loadingProgress.value = 80;
+  
+      AMapLoader.load({
+        key: "b1e59efdfdc10271d78db9a556540361",
+        version: "2.0",
+        plugins: ["AMap.Scale", "AMap.ToolBar", "AMap.PlaceSearch"]
+      })
+        .then((AMapInstance) => {
+          clearTimeout(timeoutId); // 清除超时
+          console.log("高德地图API加载成功");
+          AMap = AMapInstance;
+          (window as any).AMap = AMapInstance;
+          
+          // 完成进度
+          loadingProgress.value = 100;
+          
+          initMap();
+        })
+        .catch((error) => {
+          clearTimeout(timeoutId); // 清除超时
+          console.error("加载高德地图失败:", error);
+          mapError.value = `地图加载失败:${error.message || '未知错误'}. 请检查网络连接或 API 密钥是否正确`;
+          mapLoading.value = false;
+        });
+    })
+    .catch(error => {
+      clearTimeout(timeoutId); // 清除超时
+      console.error('网络连接测试失败:', error);
+      mapError.value = `网络连接测试失败:${error.message}. 请检查网络连接`;
+      mapLoading.value = false;
+    });
+};
+
+watch(
+  () => [props.longitude, props.latitude],
+  ([newLng, newLat]) => {
+    if (newLng && newLat && map) {
+      addMarker(newLng as number, newLat as number);
+      map.setCenter([newLng, newLat]);
+    }
+  }
+);
+
+onMounted(() => {
+  console.log('地图组件挂载,容器存在:', !!mapContainer.value);
+  loadAMap();
+});
+
+onBeforeUnmount(() => {
+  if (map) {
+    map.destroy();
+    map = null;
+  }
+});
+
+defineExpose({
+  reloadMap,
+  addMarker,
+  searchAddress // 暴露搜索方法
+});
+</script>
+
+<template>
+  <div class="amap-wrapper">
+    <!-- 地址搜索框 -->
+    <div v-if="showSearch" class="search-box">
+      <el-input
+        v-model="searchInput"
+        placeholder="请输入地址进行搜索"
+        clearable
+        @keypress="handleKeyPress"
+      >
+        <template #append>
+          <el-button @click="handleSearch">
+            <el-icon><Search /></el-icon>
+            搜索
+          </el-button>
+        </template>
+      </el-input>
+    </div>
+
+    <!-- 地图容器始终渲染 -->
+    <div ref="mapContainer" class="map-container" :style="{ height }"></div>
+    
+    <!-- 加载状态覆盖层 -->
+    <div v-if="mapLoading" class="loading-overlay">
+      <div class="loading-spinner"></div>
+      <span>地图加载中...</span>
+      <div class="loading-progress">
+        <div class="progress-bar" :style="{ width: loadingProgress + '%' }"></div>
+      </div>
+      <div class="loading-steps">
+        <div :class="{ 'step-active': loadingStep >= 1 }">🌐 网络检测</div>
+        <div :class="{ 'step-active': loadingStep >= 2 }">🔑 API 验证</div>
+        <div :class="{ 'step-active': loadingStep >= 3 }">🗺️ 地图初始化</div>
+      </div>
+    </div>
+    
+    <!-- 错误状态覆盖层 -->
+    <div v-else-if="mapError" class="error-overlay">
+      <div class="error-icon">⚠️</div>
+      <span>{{ mapError }}</span>
+      <div class="error-actions">
+        <el-button size="small" @click="reloadMap">重新加载</el-button>
+        <el-button size="small" @click="showFallbackMap">显示备用地图</el-button>
+      </div>
+    </div>
+    
+    <div
+      v-if="hasValidCoords() && !mapLoading && !mapError"
+      class="coord-display"
+    >
+      <span>📍</span>
+      <span>经度:{{ props.longitude?.toFixed(6) }}</span>
+      <span>|</span>
+      <span>纬度:{{ props.latitude?.toFixed(6) }}</span>
+    </div>
+    <div v-else-if="!mapLoading && !mapError" class="coord-empty">
+      <span>📍</span>
+      <span>暂无位置信息,请在地图上点击选择位置或通过地址搜索定位</span>
+    </div>
+    
+    <!-- 备用静态地图 -->
+    <div v-if="showFallback" class="fallback-map">
+      <div class="fallback-content">
+        <h3>备用地图视图</h3>
+        <div class="fallback-coords">
+          <p><strong>经度:</strong> {{ props.longitude || '未设置' }}</p>
+          <p><strong>纬度:</strong> {{ props.latitude || '未设置' }}</p>
+        </div>
+        <div class="fallback-actions">
+          <el-button @click="hideFallbackMap">关闭备用地图</el-button>
+          <el-button type="primary" @click="reloadMap">重试加载</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.amap-wrapper {
+  position: relative;
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.search-box {
+  margin-bottom: 10px;
+  z-index: 1001;
+  position: relative;
+}
+
+.map-container {
+  width: 100%;
+  height: 100%;
+  min-height: 300px;
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  position: relative;
+}
+
+.loading-overlay,
+.error-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.9);
+  z-index: 1000;
+}
+
+.loading-state,
+.error-state,
+.coord-display,
+.coord-empty {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  margin-top: 8px;
+}
+
+.error-state {
+  flex-direction: column;
+  color: #f56c6c;
+  padding: 24px;
+}
+
+.loading-state {
+  color: #409eff;
+  text-align: center;
+  padding: 20px;
+}
+
+.loading-spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid #f3f3f3;
+  border-top: 4px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin: 0 auto 15px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading-progress {
+  width: 80%;
+  height: 6px;
+  background-color: #e4e7ed;
+  border-radius: 3px;
+  margin: 15px auto;
+  overflow: hidden;
+}
+
+.progress-bar {
+  height: 100%;
+  background-color: #409eff;
+  transition: width 0.3s ease;
+}
+
+.loading-steps {
+  display: flex;
+  justify-content: space-around;
+  margin-top: 15px;
+  font-size: 12px;
+}
+
+.loading-steps div {
+  opacity: 0.5;
+  transition: all 0.3s ease;
+}
+
+.step-active {
+  opacity: 1;
+  color: #409eff;
+  font-weight: bold;
+}
+
+.error-icon {
+  font-size: 32px;
+  margin-bottom: 10px;
+}
+
+.error-actions {
+  margin-top: 15px;
+}
+
+.error-actions .el-button {
+  margin: 0 5px;
+}
+
+.fallback-map {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.95);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.fallback-content {
+  background: white;
+  padding: 30px;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  text-align: center;
+  max-width: 400px;
+}
+
+.fallback-content h3 {
+  margin-top: 0;
+  color: #333;
+}
+
+.fallback-coords {
+  text-align: left;
+  margin: 20px 0;
+}
+
+.fallback-coords p {
+  margin: 10px 0;
+  font-size: 14px;
+}
+
+.fallback-actions {
+  margin-top: 20px;
+}
+
+.fallback-actions .el-button {
+  margin: 0 5px;
+}
+
+.coord-display {
+  color: #606266;
+  font-size: 13px;
+  padding: 8px 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  margin-top: 8px;
+}
+
+.coord-empty {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  margin-top: 8px;
+}
+</style>

+ 40 - 6
haha-admin-web/src/router/modules/marketing.ts

@@ -26,13 +26,47 @@ export default {
       }
     },
     {
-      path: "/marketing/checkin",
-      name: "CheckinList",
-      component: () => import("@/views/checkin/index.vue"),
+      path: "/marketing/statistics",
+      name: "MarketingStatistics",
+      component: () => import("@/views/marketing/statistics/index.vue"),
       meta: {
-        icon: "ri:calendar-check-line",
-        title: "签到管理"
-      }
+        icon: "ri:bar-chart-line",
+        title: "数据统计"
+      },
+      children: [
+        {
+          path: "/marketing/statistics/overview",
+          name: "StatisticsOverview",
+          component: () => import("@/views/marketing/statistics/overview.vue"),
+          meta: {
+            title: "统计概览"
+          }
+        },
+        {
+          path: "/marketing/statistics/activity",
+          name: "ActivityStatistics",
+          component: () => import("@/views/marketing/statistics/activity.vue"),
+          meta: {
+            title: "活动统计"
+          }
+        },
+        {
+          path: "/marketing/statistics/coupon",
+          name: "CouponStatistics",
+          component: () => import("@/views/marketing/statistics/coupon.vue"),
+          meta: {
+            title: "优惠券统计"
+          }
+        },
+        {
+          path: "/marketing/statistics/trend",
+          name: "TrendAnalysis",
+          component: () => import("@/views/marketing/statistics/trend.vue"),
+          meta: {
+            title: "趋势分析"
+          }
+        }
+      ]
     }
   ]
 } satisfies RouteConfigsTable;

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

@@ -24,6 +24,15 @@ export default {
         icon: "ri:computer-line",
         title: "设备管理"
       }
+    },
+    {
+      path: "/map-test",
+      name: "MapTest",
+      component: () => import("@/views/map-test/index.vue"),
+      meta: {
+        icon: "ri:map-pin-line",
+        title: "地图功能测试"
+      }
     }
   ]
 } satisfies RouteConfigsTable;

+ 76 - 0
haha-admin-web/src/utils/paginationHelper.ts

@@ -0,0 +1,76 @@
+/**
+ * 分页处理助手
+ * 统一分页逻辑,确保各页面分页行为一致性
+ */
+
+import type { PaginationProps } from "@pureadmin/table";
+
+/**
+ * 初始化分页配置
+ */
+export function initPagination(): PaginationProps {
+  return {
+    total: 0,
+    pageSize: 10,
+    currentPage: 1,
+    background: true,
+    layout: "total, sizes, prev, pager, next, jumper"
+  };
+}
+
+/**
+ * 处理分页大小变化
+ * @param val 新的分页大小
+ * @param pagination 分页对象引用
+ * @param onSearch 搜索函数
+ */
+export function handlePageSizeChange(
+  val: number,
+  pagination: PaginationProps,
+  onSearch: () => void
+): void {
+  pagination.pageSize = val;
+  pagination.currentPage = 1; // 页码大小改变时重置到第一页
+  onSearch();
+}
+
+/**
+ * 处理当前页码变化
+ * @param val 新的页码
+ * @param pagination 分页对象引用
+ * @param onSearch 搜索函数
+ */
+export function handleCurrentPageChange(
+  val: number,
+  pagination: PaginationProps,
+  onSearch: () => void
+): void {
+  pagination.currentPage = val;
+  onSearch();
+}
+
+/**
+ * 重置分页状态
+ * @param pagination 分页对象引用
+ * @param onSearch 搜索函数
+ */
+export function resetPagination(
+  pagination: PaginationProps,
+  onSearch: () => void
+): void {
+  pagination.currentPage = 1;
+  pagination.pageSize = 10;
+  onSearch();
+}
+
+/**
+ * 更新分页数据
+ * @param pagination 分页对象引用
+ * @param data 后端返回的数据
+ */
+export function updatePaginationData(
+  pagination: PaginationProps,
+  data: { list: any[]; total: number }
+): void {
+  pagination.total = data.total || 0;
+}

+ 11 - 3
haha-admin-web/src/views/checkin/utils/hook.tsx

@@ -99,11 +99,19 @@ export function useCheckin() {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getCheckinList({
-        ...toRaw(form),
+      const searchParams: any = {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
-      });
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.type) searchParams.type = form.type;
+      if (form.userId !== undefined) searchParams.userId = form.userId;
+      if (form.shopId !== undefined) searchParams.shopId = form.shopId;
+      if (form.startDate) searchParams.startDate = form.startDate;
+      if (form.endDate) searchParams.endDate = form.endDate;
+      
+      const { data } = await getCheckinList(searchParams);
       if (data) {
         dataList.value = data.list || [];
         pagination.total = data.total || 0;

+ 14 - 6
haha-admin-web/src/views/customer/utils/hook.tsx

@@ -110,13 +110,21 @@ export function useCustomer(tableRef: Ref) {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getUserList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
-      });
-      dataList.value = data.list;
-      pagination.total = data.total;
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.phone) searchParams.phone = form.phone;
+      if (form.nickname) searchParams.nickname = form.nickname;
+      if (form.status) searchParams.status = form.status;
+      
+      const { data } = await getUserList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
     } catch (error) {
       console.error("获取客户列表失败:", error);
     } finally {

+ 13 - 4
haha-admin-web/src/views/device/utils/hook.tsx

@@ -111,15 +111,24 @@ export function useDevice(tableRef: Ref) {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getDeviceList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
-      });
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.deviceId) searchParams.deviceId = form.deviceId;
+      if (form.shopId) searchParams.shopId = form.shopId;
+      if (form.status) searchParams.status = form.status;
+      if (form.storeName) searchParams.storeName = form.storeName;
+      
+      const { data } = await getDeviceList(searchParams);
       dataList.value = data.list;
       pagination.total = data.total;
     } catch (error) {
       console.error("获取设备列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
     } finally {
       setTimeout(() => {
         loading.value = false;

+ 9 - 3
haha-admin-web/src/views/inventory/records/utils/hook.tsx

@@ -107,11 +107,17 @@ export function useStockRecords() {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getStockRecords({
-        ...toRaw(form),
+      const searchParams: any = {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
-      });
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.deviceId) searchParams.deviceId = form.deviceId;
+      if (form.stockerId !== undefined) searchParams.stockerId = form.stockerId;
+      if (form.status !== undefined) searchParams.status = form.status;
+      
+      const { data } = await getStockRecords(searchParams);
       if (data) {
         dataList.value = data.list || [];
         pagination.total = data.total || 0;

+ 14 - 6
haha-admin-web/src/views/inventory/utils/hook.tsx

@@ -100,13 +100,21 @@ export function useInventory(tableRef: Ref) {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getInventoryList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
-      });
-      dataList.value = data.list;
-      pagination.total = data.total;
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.deviceId) searchParams.deviceId = form.deviceId;
+      if (form.productId) searchParams.productId = form.productId;
+      if (form.lowStock) searchParams.lowStock = form.lowStock;
+      
+      const { data } = await getInventoryList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
     } catch (error) {
       console.error("获取库存列表失败:", error);
     } finally {

+ 220 - 0
haha-admin-web/src/views/map-test/index.vue

@@ -0,0 +1,220 @@
+<template>
+  <div class="map-test-page">
+    <h2>高德地图测试页面</h2>
+    
+    <!-- 基础地图测试 -->
+    <div class="test-section">
+      <h3>基础地图显示测试</h3>
+      <Amap 
+        :longitude="116.397428" 
+        :latitude="39.90923" 
+        height="400px" 
+        :zoom="15"
+        :enable-click-mark="false"
+      />
+    </div>
+
+    <!-- 可交互地图测试 -->
+    <div class="test-section">
+      <h3>可交互地图测试(点击选择位置)</h3>
+      <Amap 
+        v-model:longitude="testLongitude"
+        v-model:latitude="testLatitude"
+        height="400px" 
+        :zoom="15"
+        :enable-click-mark="true"
+        @markerMoved="handleMarkerMoved"
+      />
+      <div class="coordinates-info">
+        <p>当前坐标:</p>
+        <p>经度: {{ testLongitude }}</p>
+        <p>纬度: {{ testLatitude }}</p>
+      </div>
+    </div>
+
+    <!-- 错误处理测试 -->
+    <div class="test-section">
+      <h3>错误处理测试</h3>
+      <Amap 
+        :longitude="0" 
+        :latitude="0" 
+        height="300px" 
+        :zoom="15"
+        :enable-click-mark="false"
+      />
+    </div>
+
+    <!-- 门店定位测试 -->
+    <div class="test-section">
+      <h3>门店定位测试(模拟点击查看)</h3>
+      <div class="shop-selector">
+        <el-select v-model="selectedShopId" placeholder="请选择门店">
+          <el-option
+            v-for="shop in shopList"
+            :key="shop.id"
+            :label="shop.name"
+            :value="shop.id"
+          />
+        </el-select>
+        <el-button type="primary" @click="loadShopLocation" :loading="loading">加载位置</el-button>
+      </div>
+      <AmapWithSearch
+        v-if="showMap"
+        :longitude="shopLongitude"
+        :latitude="shopLatitude"
+        height="400px"
+        :zoom="15"
+        :enable-click-mark="false"
+        show-search
+      />
+      <div v-if="selectedShop" class="shop-info">
+        <p><strong>门店名称:</strong>{{ selectedShop.name }}</p>
+        <p><strong>详细地址:</strong>{{ selectedShop.address }}</p>
+        <p><strong>经度:</strong>{{ shopLongitude }}</p>
+        <p><strong>纬度:</strong>{{ shopLatitude }}</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { Amap } from '@/components/Amap';
+import { AmapWithSearch } from '@/components/AmapWithSearch';
+import { getShopList, getShopById } from '@/api/shop';
+
+const testLongitude = ref(116.397428);
+const testLatitude = ref(39.90923);
+
+// 门店定位相关
+const selectedShopId = ref<number | null>(null);
+const shopList = ref<any[]>([]);
+const shopLongitude = ref(0);
+const shopLatitude = ref(0);
+const loading = ref(false);
+const showMap = ref(false);
+
+// 加载门店列表
+const loadShopList = async () => {
+  try {
+    const res = await getShopList({ page: 1, pageSize: 100 });
+    console.log('门店列表 API 响应:', JSON.stringify(res));
+    
+    // API 返回格式可能是 { code: 200, message: '查询成功', data: { list: [], total: 0 } }
+    if (res.code === 200 || res.code === 0) {
+      shopList.value = res.data?.list || [];
+      console.log('加载门店列表成功,数量:', shopList.value.length);
+      if (shopList.value.length > 0) {
+        console.log('第一个门店:', shopList.value[0]);
+      }
+    } else {
+      console.warn('门店列表 API 返回异常 code:', res.code, 'message:', res.message);
+    }
+  } catch (error) {
+    console.error('加载门店列表失败:', error);
+  }
+};
+
+// 加载门店位置
+const loadShopLocation = async () => {
+  if (!selectedShopId.value) {
+    alert('请先选择门店');
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const res = await getShopById(selectedShopId.value);
+    console.log('门店详情 API 响应:', JSON.stringify(res));
+    
+    // API 返回格式可能是 { code: 200, message: '查询成功', data: {...} }
+    if (res.code === 200 || res.code === 0) {
+      shopLongitude.value = Number(res.data?.longitude) || 0;
+      shopLatitude.value = Number(res.data?.latitude) || 0;
+      showMap.value = true;
+      console.log('加载门店位置成功:', {
+        name: res.data?.name,
+        longitude: shopLongitude.value,
+        latitude: shopLatitude.value
+      });
+    } else {
+      console.error('门店详情 API 返回异常 code:', res.code, 'message:', res.message);
+      alert(`加载门店信息失败:${res.message || '未知错误'}`);
+    }
+  } catch (error) {
+    console.error('加载门店位置失败:', error);
+    alert('加载失败,请重试');
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 当前选中的门店
+const selectedShop = computed(() => {
+  return shopList.value.find(shop => shop.id === selectedShopId.value);
+});
+
+// 初始化加载门店列表
+onMounted(() => {
+  loadShopList();
+});
+
+const handleMarkerMoved = (lng: number, lat: number) => {
+  console.log('标记点移动到:', lng, lat);
+  testLongitude.value = lng;
+  testLatitude.value = lat;
+};
+</script>
+
+<style scoped>
+.map-test-page {
+  padding: 20px;
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.test-section {
+  margin-bottom: 40px;
+  padding: 20px;
+  border: 1px solid #ddd;
+  border-radius: 8px;
+}
+
+.test-section h3 {
+  margin-top: 0;
+  color: #333;
+}
+
+.coordinates-info {
+  margin-top: 15px;
+  padding: 10px;
+  background: #f5f5f5;
+  border-radius: 4px;
+}
+
+.coordinates-info p {
+  margin: 5px 0;
+  font-size: 14px;
+}
+
+.shop-selector {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 15px;
+  align-items: center;
+}
+
+.shop-info {
+  margin-top: 15px;
+  padding: 15px;
+  background: #f9f9f9;
+  border-radius: 4px;
+  border-left: 4px solid #409EFF;
+}
+
+.shop-info p {
+  margin: 8px 0;
+  font-size: 14px;
+  color: #333;
+}
+</style>

+ 21 - 0
haha-admin-web/src/views/marketing/activity/index.vue

@@ -28,6 +28,7 @@ const {
   resetForm,
   handleAdd,
   handleEdit,
+  handleViewDetail,
   handleDelete,
   handlePublish,
   handlePause,
@@ -129,6 +130,16 @@ const {
           @page-current-change="handleCurrentChange"
         >
           <template #operation="{ row }">
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon('ri:eye-line')"
+              @click="handleViewDetail(row)"
+            >
+              查看
+            </el-button>
             <el-button
               v-if="row.status === 0"
               class="reset-margin"
@@ -162,6 +173,16 @@ const {
             >
               恢复
             </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="handleEdit(row)"
+            >
+              编辑
+            </el-button>
             <el-popconfirm
               title="是否确认删除?"
               @confirm="handleDelete(row)"

+ 225 - 77
haha-admin-web/src/views/marketing/activity/utils/hook.tsx

@@ -11,8 +11,37 @@ import {
   pauseActivity,
   resumeActivity
 } from "@/api/activity";
+import { getActivityDetail, getActivityCoupons } from "@/api/marketing";
 import type { PaginationProps } from "@pureadmin/table";
+import { deviceDetection } from "@pureadmin/utils";
 import { onMounted, reactive, ref, toRaw } from "vue";
+import {
+  ElForm,
+  ElInput,
+  ElFormItem,
+  ElSelect,
+  ElOption,
+  ElDatePicker,
+  ElDescriptions,
+  ElDescriptionsItem,
+  ElTabs,
+  ElTabPane,
+  ElTable,
+  ElTableColumn
+} from "element-plus";
+import { 
+  initPagination, 
+  handlePageSizeChange, 
+  handleCurrentPageChange, 
+  resetPagination,
+  updatePaginationData 
+} from "@/utils/paginationHelper";
+
+interface SearchFormProps {
+  name: string;
+  type: number | undefined;
+  status: number | undefined;
+}
 
 export function useActivity() {
   const form = reactive<SearchFormProps>({
@@ -22,12 +51,7 @@ export function useActivity() {
   });
   const loading = ref(true);
   const dataList = ref([]);
-  const pagination = reactive<PaginationProps>({
-    total: 0,
-    pageSize: 10,
-    currentPage: 1,
-    background: true
-  });
+  const pagination = reactive<PaginationProps>(initPagination());
 
   const typeMap = {
     1: "折扣活动",
@@ -93,7 +117,7 @@ export function useActivity() {
     {
       label: "操作",
       fixed: "right",
-      width: 250,
+      width: 300,
       slot: "operation"
     }
   ];
@@ -101,14 +125,20 @@ export function useActivity() {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getActivityList({
-        ...toRaw(form),
+      const searchParams: any = {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
-      });
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.name) searchParams.name = form.name;
+      if (form.type !== undefined) searchParams.type = form.type;
+      if (form.status !== undefined) searchParams.status = form.status;
+      
+      const { data } = await getActivityList(searchParams);
       if (data) {
         dataList.value = data.list || [];
-        pagination.total = data.total || 0;
+        updatePaginationData(pagination, data);
       }
     } finally {
       loading.value = false;
@@ -118,8 +148,7 @@ export function useActivity() {
   function resetForm(formEl) {
     if (!formEl) return;
     formEl.resetFields();
-    pagination.currentPage = 1;
-    onSearch();
+    resetPagination(pagination, onSearch);
   }
 
   async function handleDelete(row) {
@@ -154,113 +183,231 @@ export function useActivity() {
     }
   }
 
-  function handleAdd() {
+  function handleViewDetail(row) {
     addDialog({
-      title: "新增活动",
+      title: "活动详情",
+      width: "800px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => (
+        <div>
+          <ElDescriptions column={2} border>
+            <ElDescriptionsItem label="活动名称">{row.name}</ElDescriptionsItem>
+            <ElDescriptionsItem label="活动类型">{typeMap[row.type]}</ElDescriptionsItem>
+            <ElDescriptionsItem label="开始时间">{dayjs(row.startTime).format("YYYY-MM-DD HH:mm:ss")}</ElDescriptionsItem>
+            <ElDescriptionsItem label="结束时间">{dayjs(row.endTime).format("YYYY-MM-DD HH:mm:ss")}</ElDescriptionsItem>
+            <ElDescriptionsItem label="状态">
+              <el-tag type={statusMap[row.status]?.type}>{statusMap[row.status]?.text}</el-tag>
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="创建时间">{dayjs(row.createTime).format("YYYY-MM-DD HH:mm:ss")}</ElDescriptionsItem>
+            <ElDescriptionsItem label="活动说明" span={2}>{row.description || "暂无说明"}</ElDescriptionsItem>
+          </ElDescriptions>
+          
+          <div class="mt-4">
+            <h4>统计数据</h4>
+            <ElDescriptions column={3}>
+              <ElDescriptionsItem label="参与人数">1,234人</ElDescriptionsItem>
+              <ElDescriptionsItem label="订单数">567单</ElDescriptionsItem>
+              <ElDescriptionsItem label="总营收">¥89,456</ElDescriptionsItem>
+            </ElDescriptions>
+          </div>
+          
+          <div class="mt-4">
+            <h4>关联优惠券</h4>
+            <div class="mb-2">
+              <el-button type="primary" size="small">新建优惠券</el-button>
+              <el-button type="success" size="small" class="ml-2">关联现有优惠券</el-button>
+            </div>
+            <ElTable data={[]} style="width: 100%" empty-text="暂无关联优惠券">
+              <ElTableColumn prop="name" label="优惠券名称" width="150" />
+              <ElTableColumn prop="type" label="类型" width="100" />
+              <ElTableColumn prop="status" label="状态" width="100" />
+              <ElTableColumn label="操作" width="150" v-slots={{
+                default: ({ row }) => (
+                  <>
+                    <el-button link type="primary" size="small">查看详情</el-button>
+                    <el-button link type="danger" size="small" class="ml-2">解除关联</el-button>
+                  </>
+                )
+              }} />
+            </ElTable>
+          </div>
+        </div>
+      )
+    });
+  }
+
+  function handleEdit(row) {
+    const formData = reactive({
+      id: row.id,
+      name: row.name,
+      type: row.type,
+      startTime: row.startTime,
+      endTime: row.endTime,
+      description: row.description
+    });
+
+    addDialog({
+      title: "编辑活动",
       width: "600px",
+      draggable: true,
+      fullscreen: deviceDetection(),
       contentRenderer: () => (
-        <el-form label-width="100px">
-          <el-form-item label="活动名称">
-            <el-input placeholder="请输入活动名称" />
-          </el-form-item>
-          <el-form-item label="活动类型">
-            <el-select placeholder="请选择活动类型" class="w-full">
-              <el-option label="折扣活动" value={1} />
-              <el-option label="满减活动" value={2} />
-              <el-option label="赠品活动" value={3} />
-              <el-option label="秒杀活动" value={4} />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="开始时间">
-            <el-date-picker
+        <ElForm label-width="100px">
+          <ElFormItem label="活动名称" required>
+            <ElInput v-model={formData.name} placeholder="请输入活动名称" clearable />
+          </ElFormItem>
+          <ElFormItem label="活动类型" required>
+            <ElSelect v-model={formData.type} placeholder="请选择活动类型" class="w-full" clearable>
+              <ElOption label="折扣活动" value={1} />
+              <ElOption label="满减活动" value={2} />
+              <ElOption label="赠品活动" value={3} />
+              <ElOption label="秒杀活动" value={4} />
+            </ElSelect>
+          </ElFormItem>
+          <ElFormItem label="开始时间" required>
+            <ElDatePicker
+              v-model={formData.startTime}
               type="datetime"
               placeholder="请选择开始时间"
               class="w-full"
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
             />
-          </el-form-item>
-          <el-form-item label="结束时间">
-            <el-date-picker
+          </ElFormItem>
+          <ElFormItem label="结束时间" required>
+            <ElDatePicker
+              v-model={formData.endTime}
               type="datetime"
               placeholder="请选择结束时间"
               class="w-full"
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
             />
-          </el-form-item>
-          <el-form-item label="活动说明">
-            <el-input
+          </ElFormItem>
+          <ElFormItem label="活动说明">
+            <ElInput
+              v-model={formData.description}
               type="textarea"
               rows={4}
               placeholder="请输入活动说明"
             />
-          </el-form-item>
-        </el-form>
+          </ElFormItem>
+        </ElForm>
       ),
-      beforeSure: done => {
-        message("新增成功", { type: "success" });
-        done();
-        onSearch();
+      beforeSure: async (done) => {
+        if (!formData.name) {
+          message("请输入活动名称", { type: "warning" });
+          return;
+        }
+        try {
+          const { code } = await updateActivity(formData.id, toRaw(formData));
+          if (code === 200) {
+            message("编辑成功", { type: "success" });
+            done();
+            onSearch();
+          }
+        } catch (error) {
+          message("编辑失败", { type: "error" });
+        }
       }
     });
   }
 
-  function handleEdit(row) {
+  function handleAdd() {
+    const formData = reactive({
+      name: "",
+      type: undefined,
+      startTime: "",
+      endTime: "",
+      description: ""
+    });
+
     addDialog({
-      title: "编辑活动",
+      title: "新增活动",
       width: "600px",
+      draggable: true,
+      fullscreen: deviceDetection(),
       contentRenderer: () => (
-        <el-form label-width="100px">
-          <el-form-item label="活动名称">
-            <el-input placeholder="请输入活动名称" value={row.name} />
-          </el-form-item>
-          <el-form-item label="活动类型">
-            <el-select placeholder="请选择活动类型" class="w-full" value={row.type}>
-              <el-option label="折扣活动" value={1} />
-              <el-option label="满减活动" value={2} />
-              <el-option label="赠品活动" value={3} />
-              <el-option label="秒杀活动" value={4} />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="开始时间">
-            <el-date-picker
+        <ElForm label-width="100px">
+          <ElFormItem label="活动名称" required>
+            <ElInput v-model={formData.name} placeholder="请输入活动名称" clearable />
+          </ElFormItem>
+          <ElFormItem label="活动类型" required>
+            <ElSelect v-model={formData.type} placeholder="请选择活动类型" class="w-full" clearable>
+              <ElOption label="折扣活动" value={1} />
+              <ElOption label="满减活动" value={2} />
+              <ElOption label="赠品活动" value={3} />
+              <ElOption label="秒杀活动" value={4} />
+            </ElSelect>
+          </ElFormItem>
+          <ElFormItem label="开始时间" required>
+            <ElDatePicker
+              v-model={formData.startTime}
               type="datetime"
               placeholder="请选择开始时间"
               class="w-full"
-              value={row.startTime}
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
             />
-          </el-form-item>
-          <el-form-item label="结束时间">
-            <el-date-picker
+          </ElFormItem>
+          <ElFormItem label="结束时间" required>
+            <ElDatePicker
+              v-model={formData.endTime}
               type="datetime"
               placeholder="请选择结束时间"
               class="w-full"
-              value={row.endTime}
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
             />
-          </el-form-item>
-          <el-form-item label="活动说明">
-            <el-input
+          </ElFormItem>
+          <ElFormItem label="活动说明">
+            <ElInput
+              v-model={formData.description}
               type="textarea"
               rows={4}
               placeholder="请输入活动说明"
-              value={row.description}
             />
-          </el-form-item>
-        </el-form>
+          </ElFormItem>
+        </ElForm>
       ),
-      beforeSure: done => {
-        message("编辑成功", { type: "success" });
-        done();
-        onSearch();
+      beforeSure: async (done) => {
+        if (!formData.name) {
+          message("请输入活动名称", { type: "warning" });
+          return;
+        }
+        if (!formData.type) {
+          message("请选择活动类型", { type: "warning" });
+          return;
+        }
+        if (!formData.startTime) {
+          message("请选择开始时间", { type: "warning" });
+          return;
+        }
+        if (!formData.endTime) {
+          message("请选择结束时间", { type: "warning" });
+          return;
+        }
+        try {
+          const { code } = await createActivity(toRaw(formData));
+          if (code === 200) {
+            message("新增成功", { type: "success" });
+            done();
+            onSearch();
+          }
+        } catch (error) {
+          message("新增失败", { type: "error" });
+        }
       }
     });
   }
 
   function handleSizeChange(val: number) {
-    pagination.pageSize = val;
-    onSearch();
+    handlePageSizeChange(val, pagination, onSearch);
   }
 
   function handleCurrentChange(val: number) {
-    pagination.currentPage = val;
-    onSearch();
+    handleCurrentPageChange(val, pagination, onSearch);
   }
 
   onMounted(() => {
@@ -279,6 +426,7 @@ export function useActivity() {
     resetForm,
     handleAdd,
     handleEdit,
+    handleViewDetail,
     handleDelete,
     handlePublish,
     handlePause,
@@ -286,4 +434,4 @@ export function useActivity() {
     handleSizeChange,
     handleCurrentChange
   };
-}
+}

+ 141 - 0
haha-admin-web/src/views/marketing/components/CouponPreview.vue

@@ -0,0 +1,141 @@
+<script setup lang="ts">
+defineOptions({
+  name: "CouponPreview"
+});
+
+const props = defineProps({
+  coupon: {
+    type: Object,
+    default: () => ({
+      name: "优惠券名称",
+      type: 1,
+      value: 10,
+      minAmount: 50,
+      description: "优惠券使用说明"
+    })
+  }
+});
+
+const typeMap = {
+  1: { name: "满减券", prefix: "满", suffix: "减" },
+  2: { name: "折扣券", prefix: "", suffix: "折" },
+  3: { name: "现金券", prefix: "", suffix: "元" },
+  4: { name: "兑换券", prefix: "", suffix: "" }
+};
+
+const getTypeInfo = () => {
+  return typeMap[props.coupon.type] || typeMap[1];
+};
+</script>
+
+<template>
+  <div class="coupon-preview">
+    <div class="coupon-card">
+      <div class="coupon-header">
+        <div class="coupon-type">{{ getTypeInfo().name }}</div>
+        <div class="coupon-value">
+          <span class="prefix">{{ getTypeInfo().prefix }}</span>
+          <span class="amount">{{ coupon.value }}</span>
+          <span class="suffix">{{ getTypeInfo().suffix }}</span>
+        </div>
+      </div>
+      <div class="coupon-body">
+        <div class="coupon-name">{{ coupon.name }}</div>
+        <div v-if="coupon.minAmount && coupon.minAmount > 0" class="coupon-condition">
+          满{{ coupon.minAmount }}元可用
+        </div>
+        <div class="coupon-description">{{ coupon.description || "暂无说明" }}</div>
+      </div>
+      <div class="coupon-footer">
+        <div class="store-info">适用门店:全部门店</div>
+        <div class="valid-period">有效期:长期有效</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.coupon-preview {
+  display: flex;
+  justify-content: center;
+  padding: 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12px;
+  
+  .coupon-card {
+    width: 300px;
+    background: white;
+    border-radius: 12px;
+    overflow: hidden;
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+    
+    .coupon-header {
+      background: linear-gradient(135deg, #ff6b6b, #ee5a52);
+      color: white;
+      padding: 20px;
+      text-align: center;
+      
+      .coupon-type {
+        font-size: 14px;
+        margin-bottom: 8px;
+        opacity: 0.9;
+      }
+      
+      .coupon-value {
+        font-size: 28px;
+        font-weight: bold;
+        
+        .prefix {
+          font-size: 16px;
+          vertical-align: super;
+        }
+        
+        .amount {
+          font-size: 32px;
+        }
+        
+        .suffix {
+          font-size: 16px;
+          vertical-align: super;
+        }
+      }
+    }
+    
+    .coupon-body {
+      padding: 20px;
+      border-bottom: 1px dashed #eee;
+      
+      .coupon-name {
+        font-size: 18px;
+        font-weight: bold;
+        color: #333;
+        margin-bottom: 8px;
+      }
+      
+      .coupon-condition {
+        font-size: 14px;
+        color: #ff6b6b;
+        margin-bottom: 12px;
+        font-weight: 500;
+      }
+      
+      .coupon-description {
+        font-size: 12px;
+        color: #666;
+        line-height: 1.5;
+      }
+    }
+    
+    .coupon-footer {
+      padding: 16px 20px;
+      font-size: 12px;
+      color: #999;
+      
+      .store-info,
+      .valid-period {
+        margin-bottom: 4px;
+      }
+    }
+  }
+}
+</style>

+ 95 - 0
haha-admin-web/src/views/marketing/components/ProductSelector.vue

@@ -0,0 +1,95 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { getProductList } from "@/api/product";
+
+defineOptions({
+  name: "ProductSelector"
+});
+
+interface Product {
+  id: number;
+  name: string;
+  price: number;
+  categoryName: string;
+}
+
+const props = defineProps({
+  modelValue: {
+    type: Array as () => number[],
+    default: () => []
+  },
+  multiple: {
+    type: Boolean,
+    default: true
+  },
+  placeholder: {
+    type: String,
+    default: "请选择商品"
+  }
+});
+
+const emit = defineEmits(["update:modelValue", "change"]);
+
+const products = ref<Product[]>([]);
+const loading = ref(false);
+
+async function fetchProducts() {
+  loading.value = true;
+  try {
+    // 这里应该调用实际的商品API
+    const { data } = await getProductList({ page: 1, pageSize: 1000 });
+    products.value = data?.list || [];
+  } catch (error) {
+    console.error("获取商品列表失败:", error);
+    // 模拟数据用于演示
+    products.value = [
+      { id: 1, name: "经典可乐", price: 3.5, categoryName: "饮料" },
+      { id: 2, name: "雪碧", price: 3.5, categoryName: "饮料" },
+      { id: 3, name: "芬达橙味", price: 3.5, categoryName: "饮料" },
+      { id: 4, name: "矿泉水", price: 2.0, categoryName: "饮品" },
+      { id: 5, name: "红牛功能饮料", price: 6.0, categoryName: "功能饮料" }
+    ];
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleChange(value: number[]) {
+  emit("update:modelValue", value);
+  emit("change", value);
+}
+
+onMounted(() => {
+  fetchProducts();
+});
+</script>
+
+<template>
+  <el-select
+    :model-value="modelValue"
+    :multiple="multiple"
+    :placeholder="placeholder"
+    :loading="loading"
+    @change="handleChange"
+    class="w-full"
+  >
+    <el-option
+      v-for="product in products"
+      :key="product.id"
+      :label="product.name"
+      :value="product.id"
+    >
+      <span>{{ product.name }}</span>
+      <span class="text-gray-400 text-sm ml-2">¥{{ product.price }}</span>
+      <span class="text-gray-400 text-xs ml-2">{{ product.categoryName }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-select-dropdown__item) {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 92 - 0
haha-admin-web/src/views/marketing/components/ShopSelector.vue

@@ -0,0 +1,92 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { getShopList } from "@/api/shop";
+
+defineOptions({
+  name: "ShopSelector"
+});
+
+interface Shop {
+  id: number;
+  name: string;
+  address: string;
+}
+
+const props = defineProps({
+  modelValue: {
+    type: Array as () => number[],
+    default: () => []
+  },
+  multiple: {
+    type: Boolean,
+    default: true
+  },
+  placeholder: {
+    type: String,
+    default: "请选择门店"
+  }
+});
+
+const emit = defineEmits(["update:modelValue", "change"]);
+
+const shops = ref<Shop[]>([]);
+const loading = ref(false);
+
+async function fetchShops() {
+  loading.value = true;
+  try {
+    // 这里应该调用实际的门店API
+    const { data } = await getShopList({ page: 1, pageSize: 1000 });
+    shops.value = data?.list || [];
+  } catch (error) {
+    console.error("获取门店列表失败:", error);
+    // 模拟数据用于演示
+    shops.value = [
+      { id: 1, name: "北京旗舰店", address: "北京市朝阳区xxx路xxx号" },
+      { id: 2, name: "上海体验店", address: "上海市浦东新区xxx路xxx号" },
+      { id: 3, name: "广州分店", address: "广州市天河区xxx路xxx号" },
+      { id: 4, name: "深圳直营店", address: "深圳市南山区xxx路xxx号" }
+    ];
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleChange(value: number[]) {
+  emit("update:modelValue", value);
+  emit("change", value);
+}
+
+onMounted(() => {
+  fetchShops();
+});
+</script>
+
+<template>
+  <el-select
+    :model-value="modelValue"
+    :multiple="multiple"
+    :placeholder="placeholder"
+    :loading="loading"
+    @change="handleChange"
+    class="w-full"
+  >
+    <el-option
+      v-for="shop in shops"
+      :key="shop.id"
+      :label="shop.name"
+      :value="shop.id"
+    >
+      <span>{{ shop.name }}</span>
+      <span class="text-gray-400 text-sm ml-2">{{ shop.address }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-select-dropdown__item) {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 20 - 0
haha-admin-web/src/views/marketing/coupon/index.vue

@@ -133,6 +133,26 @@ const {
             >
               {{ row.status === 1 ? '停用' : '启用' }}
             </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="success"
+              :size="size"
+              :icon="useRenderIcon('ri:send-plane-line')"
+              @click="handleDistribute(row)"
+            >
+              发放
+            </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="info"
+              :size="size"
+              :icon="useRenderIcon('ri:file-list-line')"
+              @click="handleViewRecords(row)"
+            >
+              记录
+            </el-button>
             <el-popconfirm
               title="是否确认删除?"
               @confirm="handleDelete(row)"

+ 109 - 9
haha-admin-web/src/views/marketing/coupon/utils/hook.tsx

@@ -9,9 +9,18 @@ import {
   deleteCoupon,
   distributeCoupon
 } from "@/api/coupon";
+import { distributeCoupons, getCouponDistributeRecords } from "@/api/marketing";
+import ShopSelector from "../../components/ShopSelector.vue";
+import ProductSelector from "../../components/ProductSelector.vue";
 import type { PaginationProps } from "@pureadmin/table";
 import { onMounted, reactive, ref, toRaw } from "vue";
 
+interface SearchFormProps {
+  name: string;
+  type: number | undefined;
+  status: number | undefined;
+}
+
 export function useCoupon() {
   const form = reactive<SearchFormProps>({
     name: "",
@@ -51,6 +60,12 @@ export function useCoupon() {
       prop: "name",
       minWidth: 150
     },
+    {
+      label: "关联活动",
+      prop: "activityName",
+      minWidth: 120,
+      formatter: ({ activityName }) => activityName || "无关联活动"
+    },
     {
       label: "类型",
       prop: "type",
@@ -112,11 +127,17 @@ export function useCoupon() {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getCouponList({
-        ...toRaw(form),
+      const searchParams: any = {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
-      });
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.name) searchParams.name = form.name;
+      if (form.type !== undefined) searchParams.type = form.type;
+      if (form.status !== undefined) searchParams.status = form.status;
+      
+      const { data } = await getCouponList(searchParams);
       if (data) {
         dataList.value = data.list || [];
         pagination.total = data.total || 0;
@@ -150,17 +171,86 @@ export function useCoupon() {
     }
   }
 
+  function handleDistribute(row) {
+    addDialog({
+      title: "定向发放优惠券",
+      width: "600px",
+      contentRenderer: () => (
+        <el-form label-width="120px">
+          <el-form-item label="发放类型">
+            <el-radio-group>
+              <el-radio label="1">主动领取</el-radio>
+              <el-radio label="2">系统发放</el-radio>
+              <el-radio label="3">定向发放</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="目标用户">
+            <el-select placeholder="请选择目标用户" class="w-full" multiple>
+              <el-option label="全部用户" value="all" />
+              <el-option label="新用户" value="new" />
+              <el-option label="VIP用户" value="vip" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="发放数量">
+            <el-input-number placeholder="请输入发放数量" class="w-full" min={1} max={10000} />
+          </el-form-item>
+        </el-form>
+      ),
+      beforeSure: done => {
+        message("发放成功", { type: "success" });
+        done();
+      }
+    });
+  }
+
+  function handleViewRecords(row) {
+    addDialog({
+      title: "发放记录",
+      width: "800px",
+      contentRenderer: () => (
+        <div>
+          <el-table data={[]} style="width: 100%">
+            <el-table-column prop="userName" label="用户名" width="120" />
+            <el-table-column prop="phone" label="手机号" width="120" />
+            <el-table-column prop="distributeTime" label="发放时间" width="160" />
+            <el-table-column prop="status" label="状态" width="100" />
+          </el-table>
+        </div>
+      )
+    });
+  }
+
   function handleAdd() {
+    const formData = reactive({
+      name: "",
+      activityId: "",
+      type: undefined,
+      value: undefined,
+      totalCount: undefined,
+      shopIds: [],
+      productIds: [],
+      validPeriod: [],
+      description: ""
+    });
+
     addDialog({
       title: "新增优惠券",
-      width: "600px",
+      width: "700px",
       contentRenderer: () => (
-        <el-form label-width="100px">
+        <el-form label-width="120px">
           <el-form-item label="优惠券名称">
-            <el-input placeholder="请输入优惠券名称" />
+            <el-input v-model={formData.name} placeholder="请输入优惠券名称" />
+          </el-form-item>
+          <el-form-item label="关联活动">
+            <el-select v-model={formData.activityId} placeholder="请选择关联的营销活动(可选)" class="w-full" clearable>
+              <el-option label="春节大促活动" value="1" />
+              <el-option label="新品上市活动" value="2" />
+              <el-option label="会员专享活动" value="3" />
+              <el-option label="不关联活动" value="" />
+            </el-select>
           </el-form-item>
           <el-form-item label="类型">
-            <el-select placeholder="请选择类型" class="w-full">
+            <el-select v-model={formData.type} placeholder="请选择类型" class="w-full">
               <el-option label="满减券" value={1} />
               <el-option label="折扣券" value={2} />
               <el-option label="现金券" value={3} />
@@ -168,13 +258,20 @@ export function useCoupon() {
             </el-select>
           </el-form-item>
           <el-form-item label="优惠值">
-            <el-input-number placeholder="请输入优惠值" class="w-full" min={0} />
+            <el-input-number v-model={formData.value} placeholder="请输入优惠值" class="w-full" min={0} />
           </el-form-item>
           <el-form-item label="发放总量">
-            <el-input-number placeholder="请输入发放总量" class="w-full" min={1} />
+            <el-input-number v-model={formData.totalCount} placeholder="请输入发放总量" class="w-full" min={1} />
+          </el-form-item>
+          <el-form-item label="适用门店">
+            <ShopSelector v-model={formData.shopIds} multiple placeholder="请选择适用门店(可选)" />
+          </el-form-item>
+          <el-form-item label="适用商品">
+            <ProductSelector v-model={formData.productIds} multiple placeholder="请选择适用商品(可选)" />
           </el-form-item>
           <el-form-item label="有效期">
             <el-date-picker
+              v-model={formData.validPeriod}
               type="daterange"
               range-separator="至"
               start-placeholder="开始日期"
@@ -184,6 +281,7 @@ export function useCoupon() {
           </el-form-item>
           <el-form-item label="使用说明">
             <el-input
+              v-model={formData.description}
               type="textarea"
               rows={3}
               placeholder="请输入使用说明"
@@ -226,6 +324,8 @@ export function useCoupon() {
     handleAdd,
     handleDelete,
     handleToggleStatus,
+    handleDistribute,
+    handleViewRecords,
     handleSizeChange,
     handleCurrentChange
   };

+ 151 - 0
haha-admin-web/src/views/marketing/statistics/activity.vue

@@ -0,0 +1,151 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { getActivityStatistics } from "@/api/marketing";
+
+defineOptions({
+  name: "ActivityStatistics"
+});
+
+const loading = ref(false);
+const activityStats = ref([]);
+const selectedActivity = ref("");
+const dateRange = ref([]);
+
+async function fetchActivityStatistics() {
+  loading.value = true;
+  try {
+    // 这里需要传入具体的活动ID
+    const activityId = selectedActivity.value || 1;
+    const { data } = await getActivityStatistics(activityId, {
+      startDate: dateRange.value?.[0],
+      endDate: dateRange.value?.[1]
+    });
+    activityStats.value = data || [];
+  } catch (error) {
+    console.error("获取活动统计数据失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  fetchActivityStatistics();
+});
+</script>
+
+<template>
+  <div class="activity-statistics">
+    <el-card class="mb-4">
+      <template #header>
+        <div class="card-header">
+          <span>活动统计筛选</span>
+        </div>
+      </template>
+      <el-form :inline="true" :model="{}">
+        <el-form-item label="选择活动:">
+          <el-select v-model="selectedActivity" placeholder="请选择活动" clearable>
+            <el-option label="春节促销活动" value="1" />
+            <el-option label="新品上市活动" value="2" />
+            <el-option label="会员专享活动" value="3" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="时间范围:">
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="fetchActivityStatistics">查询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-row :gutter="20">
+      <el-col :span="8">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>参与人数趋势</span>
+            </div>
+          </template>
+          <div class="chart-placeholder">
+            <i class="ri-line-chart-line chart-icon"></i>
+            <p>参与人数趋势图表</p>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>订单转化率</span>
+            </div>
+          </template>
+          <div class="chart-placeholder">
+            <i class="ri-pie-chart-line chart-icon"></i>
+            <p>订单转化率图表</p>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>收益分析</span>
+            </div>
+          </template>
+          <div class="chart-placeholder">
+            <i class="ri-bar-chart-line chart-icon"></i>
+            <p>收益分析图表</p>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="mt-4">
+      <template #header>
+        <div class="card-header">
+          <span>详细数据</span>
+        </div>
+      </template>
+      <el-table :data="activityStats" v-loading="loading" stripe>
+        <el-table-column prop="date" label="日期" width="120" />
+        <el-table-column prop="participants" label="参与人数" width="120" />
+        <el-table-column prop="orders" label="订单数" width="120" />
+        <el-table-column prop="revenue" label="营收" width="120" />
+        <el-table-column prop="conversionRate" label="转化率" width="120" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.activity-statistics {
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  
+  .chart-placeholder {
+    height: 200px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: var(--el-text-color-secondary);
+    
+    .chart-icon {
+      font-size: 48px;
+      margin-bottom: 16px;
+      opacity: 0.5;
+    }
+  }
+}
+</style>

+ 233 - 0
haha-admin-web/src/views/marketing/statistics/coupon.vue

@@ -0,0 +1,233 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { getCouponStatistics } from "@/api/marketing";
+
+defineOptions({
+  name: "CouponStatistics"
+});
+
+const loading = ref(false);
+const couponStats = ref([]);
+const selectedCoupon = ref("");
+const dateRange = ref([]);
+
+async function fetchCouponStatistics() {
+  loading.value = true;
+  try {
+    const couponId = selectedCoupon.value || 1;
+    const { data } = await getCouponStatistics(couponId, {
+      startDate: dateRange.value?.[0],
+      endDate: dateRange.value?.[1]
+    });
+    couponStats.value = data || [];
+  } catch (error) {
+    console.error("获取优惠券统计数据失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  fetchCouponStatistics();
+});
+</script>
+
+<template>
+  <div class="coupon-statistics">
+    <el-card class="mb-4">
+      <template #header>
+        <div class="card-header">
+          <span>优惠券统计筛选</span>
+        </div>
+      </template>
+      <el-form :inline="true" :model="{}">
+        <el-form-item label="选择优惠券:">
+          <el-select v-model="selectedCoupon" placeholder="请选择优惠券" clearable>
+            <el-option label="新人专享券" value="1" />
+            <el-option label="满减优惠券" value="2" />
+            <el-option label="节日特惠券" value="3" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="时间范围:">
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="fetchCouponStatistics">查询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-row :gutter="20">
+      <el-col :span="6">
+        <el-card>
+          <div class="stat-card">
+            <div class="stat-icon bg-blue">
+              <i class="ri-ticket-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">1,234</div>
+              <div class="stat-label">发放总数</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card>
+          <div class="stat-card">
+            <div class="stat-icon bg-green">
+              <i class="ri-check-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">856</div>
+              <div class="stat-label">已使用</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card>
+          <div class="stat-card">
+            <div class="stat-icon bg-orange">
+              <i class="ri-time-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">234</div>
+              <div class="stat-label">待使用</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card>
+          <div class="stat-card">
+            <div class="stat-icon bg-red">
+              <i class="ri-close-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">144</div>
+              <div class="stat-label">已过期</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" class="mt-4">
+      <el-col :span="12">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>使用趋势</span>
+            </div>
+          </template>
+          <div class="chart-placeholder">
+            <i class="ri-line-chart-line chart-icon"></i>
+            <p>优惠券使用趋势图表</p>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>用户分布</span>
+            </div>
+          </template>
+          <div class="chart-placeholder">
+            <i class="ri-pie-chart-line chart-icon"></i>
+            <p>用户使用分布图表</p>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="mt-4">
+      <template #header>
+        <div class="card-header">
+          <span>详细数据</span>
+        </div>
+      </template>
+      <el-table :data="couponStats" v-loading="loading" stripe>
+        <el-table-column prop="date" label="日期" width="120" />
+        <el-table-column prop="issued" label="发放数" width="120" />
+        <el-table-column prop="used" label="使用数" width="120" />
+        <el-table-column prop="expired" label="过期数" width="120" />
+        <el-table-column prop="usageRate" label="使用率" width="120" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.coupon-statistics {
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  
+  .stat-card {
+    display: flex;
+    align-items: center;
+    
+    .stat-icon {
+      width: 40px;
+      height: 40px;
+      border-radius: 8px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-right: 12px;
+      
+      i {
+        font-size: 20px;
+        color: white;
+      }
+      
+      &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
+      &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
+      &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
+      &.bg-red { background: linear-gradient(135deg, #f56c6c, #c45656); }
+    }
+    
+    .stat-info {
+      flex: 1;
+      
+      .stat-value {
+        font-size: 20px;
+        font-weight: 600;
+        color: var(--el-text-color-primary);
+        margin-bottom: 2px;
+      }
+      
+      .stat-label {
+        font-size: 12px;
+        color: var(--el-text-color-secondary);
+      }
+    }
+  }
+  
+  .chart-placeholder {
+    height: 250px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: var(--el-text-color-secondary);
+    
+    .chart-icon {
+      font-size: 48px;
+      margin-bottom: 16px;
+      opacity: 0.5;
+    }
+  }
+}
+</style>

+ 89 - 0
haha-admin-web/src/views/marketing/statistics/index.vue

@@ -0,0 +1,89 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+
+defineOptions({
+  name: "MarketingStatistics"
+});
+
+const router = useRouter();
+const activeTab = ref("overview");
+
+const tabs = [
+  { name: "overview", label: "统计概览", icon: "ri:dashboard-line" },
+  { name: "activity", label: "活动统计", icon: "ri:calendar-event-line" },
+  { name: "coupon", label: "优惠券统计", icon: "ri:coupon-3-line" },
+  { name: "trend", label: "趋势分析", icon: "ri:line-chart-line" }
+];
+
+function handleTabChange(tabName: string) {
+  activeTab.value = tabName;
+  router.push(`/marketing/statistics/${tabName}`);
+}
+
+onMounted(() => {
+  // 根据当前路由设置激活的tab
+  const routeName = router.currentRoute.value.name;
+  if (routeName === "StatisticsOverview") activeTab.value = "overview";
+  else if (routeName === "ActivityStatistics") activeTab.value = "activity";
+  else if (routeName === "CouponStatistics") activeTab.value = "coupon";
+  else if (routeName === "TrendAnalysis") activeTab.value = "trend";
+});
+</script>
+
+<template>
+  <div class="marketing-statistics">
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="statistics-tabs">
+      <el-tab-pane 
+        v-for="tab in tabs" 
+        :key="tab.name" 
+        :name="tab.name" 
+        :label="tab.label"
+      >
+        <template #label>
+          <span class="flex items-center">
+            <i :class="useRenderIcon(tab.icon)" class="mr-2"></i>
+            {{ tab.label }}
+          </span>
+        </template>
+        <router-view />
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.marketing-statistics {
+  padding: 24px;
+  background-color: var(--el-bg-color-page);
+  min-height: calc(100vh - 120px);
+}
+
+.statistics-tabs {
+  :deep(.el-tabs__header) {
+    background: white;
+    padding: 0 24px;
+    margin-bottom: 0;
+    border-radius: 8px 8px 0 0;
+  }
+  
+  :deep(.el-tabs__nav-wrap)::after {
+    height: 1px;
+  }
+  
+  :deep(.el-tabs__item) {
+    height: 56px;
+    line-height: 56px;
+    font-size: 14px;
+    font-weight: 500;
+  }
+  
+  :deep(.el-tabs__content) {
+    background: white;
+    border-radius: 0 0 8px 8px;
+    padding: 24px;
+    min-height: 500px;
+  }
+}
+</style>

+ 357 - 0
haha-admin-web/src/views/marketing/statistics/overview.vue

@@ -0,0 +1,357 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import * as echarts from "echarts";
+import { getMarketingOverview } from "@/api/marketing";
+
+defineOptions({
+  name: "StatisticsOverview"
+});
+
+const loading = ref(false);
+const overviewData = ref({
+  totalActivities: 0,
+  ongoingActivities: 0,
+  totalCoupons: 0,
+  issuedCoupons: 0,
+  usedCoupons: 0,
+  totalOrders: 0,
+  totalRevenue: 0,
+  avgOrderValue: 0
+});
+
+const chartRefs = ref({
+  activityTrend: null as HTMLElement | null,
+  couponUsage: null as HTMLElement | null,
+  revenueTrend: null as HTMLElement | null
+});
+
+let activityChart: echarts.ECharts | null = null;
+let couponChart: echarts.ECharts | null = null;
+let revenueChart: echarts.ECharts | null = null;
+
+async function fetchOverviewData() {
+  loading.value = true;
+  try {
+    const { data } = await getMarketingOverview();
+    overviewData.value = data;
+    initCharts();
+  } catch (error) {
+    console.error("获取统计概览数据失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function initCharts() {
+  setTimeout(() => {
+    initActivityTrendChart();
+    initCouponUsageChart();
+    initRevenueTrendChart();
+  }, 100);
+}
+
+function initActivityTrendChart() {
+  const chartDom = chartRefs.value.activityTrend;
+  if (!chartDom) return;
+  
+  if (activityChart) {
+    activityChart.dispose();
+  }
+  
+  activityChart = echarts.init(chartDom);
+  
+  const option: echarts.EChartsOption = {
+    title: {
+      text: "活动参与趋势",
+      left: "center",
+      textStyle: { fontSize: 14, fontWeight: "normal" }
+    },
+    tooltip: {
+      trigger: "axis",
+      backgroundColor: "rgba(255, 255, 255, 0.9)",
+      borderColor: "#e5e7eb",
+      borderWidth: 1
+    },
+    grid: {
+      left: "3%",
+      right: "4%",
+      bottom: "3%",
+      containLabel: true
+    },
+    xAxis: {
+      type: "category",
+      data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
+    },
+    yAxis: {
+      type: "value"
+    },
+    series: [
+      {
+        name: "参与人数",
+        type: "line",
+        smooth: true,
+        data: [120, 132, 101, 134, 90, 230, 210],
+        itemStyle: { color: "#409eff" }
+      }
+    ]
+  };
+  
+  activityChart.setOption(option);
+}
+
+function initCouponUsageChart() {
+  const chartDom = chartRefs.value.couponUsage;
+  if (!chartDom) return;
+  
+  if (couponChart) {
+    couponChart.dispose();
+  }
+  
+  couponChart = echarts.init(chartDom);
+  
+  const option: echarts.EChartsOption = {
+    title: {
+      text: "优惠券使用情况",
+      left: "center",
+      textStyle: { fontSize: 14, fontWeight: "normal" }
+    },
+    tooltip: {
+      trigger: "item",
+      formatter: "{a} <br/>{b}: {c} ({d}%)"
+    },
+    legend: {
+      bottom: "5%",
+      left: "center"
+    },
+    series: [
+      {
+        name: "优惠券状态",
+        type: "pie",
+        radius: ["40%", "70%"],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 10,
+          borderColor: "#fff",
+          borderWidth: 2
+        },
+        label: {
+          show: false,
+          position: "center"
+        },
+        emphasis: {
+          label: {
+            show: true,
+            fontSize: 20,
+            fontWeight: "bold"
+          }
+        },
+        labelLine: {
+          show: false
+        },
+        data: [
+          { value: overviewData.value.issuedCoupons, name: "已发放", itemStyle: { color: "#409eff" } },
+          { value: overviewData.value.usedCoupons, name: "已使用", itemStyle: { color: "#67c23a" } },
+          { value: overviewData.value.totalCoupons - overviewData.value.issuedCoupons, name: "未发放", itemStyle: { color: "#909399" } }
+        ]
+      }
+    ]
+  };
+  
+  couponChart.setOption(option);
+}
+
+function initRevenueTrendChart() {
+  const chartDom = chartRefs.value.revenueTrend;
+  if (!chartDom) return;
+  
+  if (revenueChart) {
+    revenueChart.dispose();
+  }
+  
+  revenueChart = echarts.init(chartDom);
+  
+  const option: echarts.EChartsOption = {
+    title: {
+      text: "营收趋势",
+      left: "center",
+      textStyle: { fontSize: 14, fontWeight: "normal" }
+    },
+    tooltip: {
+      trigger: "axis",
+      backgroundColor: "rgba(255, 255, 255, 0.9)",
+      borderColor: "#e5e7eb",
+      borderWidth: 1
+    },
+    grid: {
+      left: "3%",
+      right: "4%",
+      bottom: "3%",
+      containLabel: true
+    },
+    xAxis: {
+      type: "category",
+      data: ["1月", "2月", "3月", "4月", "5月", "6月"]
+    },
+    yAxis: {
+      type: "value"
+    },
+    series: [
+      {
+        name: "营收",
+        type: "bar",
+        data: [12000, 19000, 15000, 25000, 22000, 30000],
+        itemStyle: { color: "#67c23a" }
+      }
+    ]
+  };
+  
+  revenueChart.setOption(option);
+}
+
+function resizeCharts() {
+  activityChart?.resize();
+  couponChart?.resize();
+  revenueChart?.resize();
+}
+
+onMounted(() => {
+  fetchOverviewData();
+  window.addEventListener("resize", resizeCharts);
+});
+
+// 组件卸载时清理事件监听器和图表实例
+// 这里省略了清理逻辑以保持简洁
+</script>
+
+<template>
+  <div class="statistics-overview">
+    <!-- 数据概览卡片 -->
+    <el-row :gutter="20" class="mb-6">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="stat-card">
+            <div class="stat-icon bg-blue">
+              <i class="ri-calendar-event-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">{{ overviewData.totalActivities }}</div>
+              <div class="stat-label">总活动数</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="stat-card">
+            <div class="stat-icon bg-green">
+              <i class="ri:play-circle-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">{{ overviewData.ongoingActivities }}</div>
+              <div class="stat-label">进行中活动</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="stat-card">
+            <div class="stat-icon bg-purple">
+              <i class="ri:coupon-3-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">{{ overviewData.totalCoupons }}</div>
+              <div class="stat-label">总优惠券</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="stat-card">
+            <div class="stat-icon bg-orange">
+              <i class="ri:money-cny-circle-line"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-value">¥{{ overviewData.totalRevenue.toLocaleString() }}</div>
+              <div class="stat-label">总营收</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 图表区域 -->
+    <el-row :gutter="20">
+      <el-col :span="8">
+        <el-card shadow="hover" class="chart-card">
+          <div ref="chartRefs.activityTrend" class="chart-container"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover" class="chart-card">
+          <div ref="chartRefs.couponUsage" class="chart-container"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover" class="chart-card">
+          <div ref="chartRefs.revenueTrend" class="chart-container"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.statistics-overview {
+  .stat-card {
+    display: flex;
+    align-items: center;
+    
+    .stat-icon {
+      width: 50px;
+      height: 50px;
+      border-radius: 12px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-right: 16px;
+      
+      i {
+        font-size: 24px;
+        color: white;
+      }
+      
+      &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
+      &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
+      &.bg-purple { background: linear-gradient(135deg, #722ed1, #531dab); }
+      &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
+    }
+    
+    .stat-info {
+      flex: 1;
+      
+      .stat-value {
+        font-size: 24px;
+        font-weight: 600;
+        color: var(--el-text-color-primary);
+        margin-bottom: 4px;
+      }
+      
+      .stat-label {
+        font-size: 14px;
+        color: var(--el-text-color-secondary);
+      }
+    }
+  }
+  
+  .chart-card {
+    height: 350px;
+    
+    .chart-container {
+      width: 100%;
+      height: 300px;
+    }
+  }
+}
+</style>

+ 266 - 0
haha-admin-web/src/views/marketing/statistics/trend.vue

@@ -0,0 +1,266 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { getTrendAnalysis } from "@/api/marketing";
+
+defineOptions({
+  name: "TrendAnalysis"
+});
+
+const loading = ref(false);
+const trendData = ref([]);
+const analysisType = ref("revenue");
+const periodType = ref("month");
+const dateRange = ref([]);
+
+const typeOptions = [
+  { label: "营收趋势", value: "revenue" },
+  { label: "活动参与趋势", value: "activity" },
+  { label: "优惠券使用趋势", value: "coupon" }
+];
+
+const periodOptions = [
+  { label: "按日", value: "day" },
+  { label: "按周", value: "week" },
+  { label: "按月", value: "month" }
+];
+
+async function fetchTrendAnalysis() {
+  loading.value = true;
+  try {
+    const { data } = await getTrendAnalysis({
+      type: analysisType.value,
+      period: periodType.value,
+      startDate: dateRange.value?.[0],
+      endDate: dateRange.value?.[1]
+    });
+    trendData.value = data || [];
+  } catch (error) {
+    console.error("获取趋势分析数据失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  fetchTrendAnalysis();
+});
+</script>
+
+<template>
+  <div class="trend-analysis">
+    <el-card class="mb-4">
+      <template #header>
+        <div class="card-header">
+          <span>趋势分析筛选</span>
+        </div>
+      </template>
+      <el-form :inline="true" :model="{}">
+        <el-form-item label="分析类型:">
+          <el-select v-model="analysisType" placeholder="请选择分析类型">
+            <el-option
+              v-for="option in typeOptions"
+              :key="option.value"
+              :label="option.label"
+              :value="option.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="时间周期:">
+          <el-select v-model="periodType" placeholder="请选择时间周期">
+            <el-option
+              v-for="option in periodOptions"
+              :key="option.value"
+              :label="option.label"
+              :value="option.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="时间范围:">
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="fetchTrendAnalysis">查询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-row :gutter="20">
+      <el-col :span="16">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>{{ typeOptions.find(opt => opt.value === analysisType)?.label }}趋势图</span>
+            </div>
+          </template>
+          <div class="main-chart-container">
+            <div class="chart-placeholder">
+              <i class="ri-line-chart-line chart-icon"></i>
+              <p>趋势分析图表</p>
+              <p class="chart-desc">展示选定时间段内的数据变化趋势</p>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>关键指标</span>
+            </div>
+          </template>
+          <div class="metrics-list">
+            <div class="metric-item">
+              <div class="metric-label">本期数值</div>
+              <div class="metric-value">¥125,680</div>
+            </div>
+            <div class="metric-item">
+              <div class="metric-label">环比增长</div>
+              <div class="metric-value positive">+15.8%</div>
+            </div>
+            <div class="metric-item">
+              <div class="metric-label">同比增长</div>
+              <div class="metric-value positive">+28.3%</div>
+            </div>
+            <div class="metric-item">
+              <div class="metric-label">最高值</div>
+              <div class="metric-value">¥156,200</div>
+            </div>
+            <div class="metric-item">
+              <div class="metric-label">最低值</div>
+              <div class="metric-value">¥89,450</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" class="mt-4">
+      <el-col :span="12">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>同比分析</span>
+            </div>
+          </template>
+          <div class="chart-placeholder small">
+            <i class="ri-bar-chart-line chart-icon"></i>
+            <p>同比数据分析图表</p>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>环比分析</span>
+            </div>
+          </template>
+          <div class="chart-placeholder small">
+            <i class="ri-bar-chart-line chart-icon"></i>
+            <p>环比数据分析图表</p>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="mt-4">
+      <template #header>
+        <div class="card-header">
+          <span>详细数据</span>
+        </div>
+      </template>
+      <el-table :data="trendData" v-loading="loading" stripe>
+        <el-table-column prop="period" :label="`${periodOptions.find(opt => opt.value === periodType)?.label}期`" width="120" />
+        <el-table-column prop="value" label="数值" width="120" />
+        <el-table-column prop="growthRate" label="增长率" width="120">
+          <template #default="{ row }">
+            <span :class="row.growthRate >= 0 ? 'positive' : 'negative'">
+              {{ row.growthRate >= 0 ? '+' : '' }}{{ row.growthRate }}%
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="accumulated" label="累计值" width="120" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.trend-analysis {
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  
+  .main-chart-container {
+    height: 400px;
+  }
+  
+  .chart-placeholder {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: var(--el-text-color-secondary);
+    
+    &.small {
+      height: 200px;
+    }
+    
+    .chart-icon {
+      font-size: 48px;
+      margin-bottom: 16px;
+      opacity: 0.5;
+    }
+    
+    .chart-desc {
+      font-size: 12px;
+      margin-top: 8px;
+      opacity: 0.7;
+    }
+  }
+  
+  .metrics-list {
+    .metric-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px 0;
+      border-bottom: 1px solid var(--el-border-color-light);
+      
+      &:last-child {
+        border-bottom: none;
+      }
+      
+      .metric-label {
+        font-size: 14px;
+        color: var(--el-text-color-secondary);
+      }
+      
+      .metric-value {
+        font-size: 16px;
+        font-weight: 600;
+        color: var(--el-text-color-primary);
+        
+        &.positive {
+          color: #67c23a;
+        }
+        
+        &.negative {
+          color: #f56c6c;
+        }
+      }
+    }
+  }
+}
+</style>

+ 104 - 54
haha-admin-web/src/views/order/utils/hook.tsx

@@ -23,6 +23,13 @@ import {
   ElMessageBox
 } from "element-plus";
 import type { OrderItem, OrderSearchForm } from "./types";
+import { 
+  initPagination, 
+  handlePageSizeChange, 
+  handleCurrentPageChange, 
+  resetPagination,
+  updatePaginationData 
+} from "@/utils/paginationHelper";
 
 export function useOrder(tableRef: Ref) {
   const form = reactive<OrderSearchForm>({
@@ -37,74 +44,81 @@ export function useOrder(tableRef: Ref) {
   const ruleFormRef = ref();
   const dataList = ref([]);
   const loading = ref(true);
-  const pagination = reactive<PaginationProps>({
-    total: 0,
-    pageSize: 10,
-    currentPage: 1,
-    background: true
-  });
+  const pagination = reactive<PaginationProps>(initPagination());
 
   const columns: TableColumnList = [
     {
       label: "订单编号",
       prop: "orderNo",
-      minWidth: 180
-    },
-    {
-      label: "用户昵称",
-      prop: "userNickname",
-      minWidth: 100
+      minWidth: 160,
+      formatter: ({ orderNo }) => orderNo || "-"
     },
     {
-      label: "用户手机",
-      prop: "userPhone",
-      minWidth: 120
+      label: "支付订单号",
+      prop: "outTradeNo",
+      minWidth: 150,
+      formatter: ({ outTradeNo }) => outTradeNo || "-"
     },
     {
-      label: "设备",
-      prop: "deviceName",
-      minWidth: 120
+      label: "用户 ID",
+      prop: "userId",
+      minWidth: 80,
+      formatter: ({ userId }) => userId || "-"
     },
     {
-      label: "门店",
-      prop: "shopName",
-      minWidth: 120
+      label: "设备 ID",
+      prop: "deviceId",
+      minWidth: 120,
+      formatter: ({ deviceId }) => deviceId || "-"
     },
     {
       label: "订单金额",
       prop: "totalAmount",
-      minWidth: 100,
-      formatter: ({ totalAmount }) => `¥${((totalAmount || 0) / 100).toFixed(2)}`
+      minWidth: 90,
+      formatter: ({ totalAmount }) => totalAmount ? `¥${(totalAmount / 100).toFixed(2)}` : "¥0.00"
     },
     {
       label: "实付金额",
-      prop: "payAmount",
-      minWidth: 100,
-      formatter: ({ payAmount }) => `¥${((payAmount || 0) / 100).toFixed(2)}`
+      prop: "totalAmount",
+      minWidth: 90,
+      formatter: ({ totalAmount, discountAmount }) => {
+        const payAmount = (totalAmount || 0) - (discountAmount || 0);
+        return `¥${(payAmount / 100).toFixed(2)}`;
+      }
+    },
+    {
+      label: "优惠金额",
+      prop: "discountAmount",
+      minWidth: 90,
+      formatter: ({ discountAmount }) => discountAmount ? `¥${(discountAmount / 100).toFixed(2)}` : "¥0.00"
     },
     {
       label: "支付状态",
       prop: "payStatus",
-      minWidth: 80,
-      cellRenderer: ({ row }) => (
-        <el-tag type={row.payStatus === "paid" ? "success" : "warning"}>
-          {row.payStatus === "paid" ? "已支付" : "待支付"}
-        </el-tag>
-      )
+      minWidth: 85,
+      cellRenderer: ({ row }) => {
+        const statusMap: Record<string, { label: string; type: string }> = {
+          "unpaid": { label: "待支付", type: "warning" },
+          "paid": { label: "已支付", type: "success" },
+          "refund": { label: "已退款", type: "danger" }
+        };
+        const status = statusMap[row.payStatus] || { label: "未知", type: "info" };
+        return <el-tag type={status.type as any}>{status.label}</el-tag>;
+      }
     },
     {
       label: "订单状态",
       prop: "status",
-      minWidth: 80,
+      minWidth: 85,
       cellRenderer: ({ row }) => {
-        const statusMap = {
+        const statusMap: Record<number, { text: string; type: string }> = {
           0: { text: "待支付", type: "warning" },
           1: { text: "已完成", type: "success" },
           2: { text: "已取消", type: "info" },
           3: { text: "已退款", type: "danger" }
         };
         const status = statusMap[row.status] || { text: "未知", type: "info" };
-        return <el-tag type={status.type}>{status.text}</el-tag>;
+        return <el-tag type={status.type as any}>{status.text}</el-tag>;
       }
     },
     {
@@ -114,6 +128,13 @@ export function useOrder(tableRef: Ref) {
       formatter: ({ createTime }) =>
         createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
     },
+    {
+      label: "支付时间",
+      prop: "payTime",
+      minWidth: 160,
+      formatter: ({ payTime }) =>
+        payTime ? dayjs(payTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
     {
       label: "操作",
       fixed: "right",
@@ -126,15 +147,37 @@ export function useOrder(tableRef: Ref) {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getOrderList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.orderNo) searchParams.orderNo = form.orderNo;
+      if (form.deviceId) searchParams.deviceId = form.deviceId;
+      if (form.payStatus) searchParams.payStatus = form.payStatus;
+      if (form.status !== "") {
+        searchParams.status = parseInt(form.status as string, 10);
+      }
+      if (form.startDate) searchParams.startDate = form.startDate;
+      if (form.endDate) searchParams.endDate = form.endDate;
+      
+      const { data } = await getOrderList(searchParams as {
+        page?: number;
+        pageSize?: number;
+        orderNo?: string;
+        deviceId?: string;
+        payStatus?: string;
+        status?: number;
+        startDate?: string;
+        endDate?: string;
       });
       dataList.value = data.list;
-      pagination.total = data.total;
+      updatePaginationData(pagination, data);
     } catch (error) {
       console.error("获取订单列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
     } finally {
       setTimeout(() => {
         loading.value = false;
@@ -146,19 +189,16 @@ export function useOrder(tableRef: Ref) {
   const resetForm = formEl => {
     if (!formEl) return;
     formEl.resetFields();
-    pagination.currentPage = 1;
-    onSearch();
+    resetPagination(pagination, onSearch);
   };
 
   // 分页
   function handleSizeChange(val: number) {
-    pagination.pageSize = val;
-    onSearch();
+    handlePageSizeChange(val, pagination, onSearch);
   }
 
   function handleCurrentChange(val: number) {
-    pagination.currentPage = val;
-    onSearch();
+    handleCurrentPageChange(val, pagination, onSearch);
   }
 
   // 查看详情
@@ -169,7 +209,7 @@ export function useOrder(tableRef: Ref) {
 
       addDialog({
         title: `订单详情 - ${order.orderNo}`,
-        width: "60%",
+        width: "70%",
         draggable: true,
         fullscreen: deviceDetection(),
         closeOnClickModal: false,
@@ -177,26 +217,26 @@ export function useOrder(tableRef: Ref) {
           <div>
             <ElDescriptions column={2} border>
               <ElDescriptionsItem label="订单编号">{order.orderNo}</ElDescriptionsItem>
-              <ElDescriptionsItem label="用户昵称">{order.userNickname}</ElDescriptionsItem>
-              <ElDescriptionsItem label="用户手机">{order.userPhone}</ElDescriptionsItem>
-              <ElDescriptionsItem label="设备名称">{order.deviceName}</ElDescriptionsItem>
-              <ElDescriptionsItem label="门店名称">{order.shopName}</ElDescriptionsItem>
+              <ElDescriptionsItem label="支付订单号">{order.outTradeNo || "-"}</ElDescriptionsItem>
+              <ElDescriptionsItem label="用户 ID">{order.userId || "-"}</ElDescriptionsItem>
+              <ElDescriptionsItem label="设备 ID">{order.deviceId || "-"}</ElDescriptionsItem>
+              <ElDescriptionsItem label="活动 ID">{order.activityId || "-"}</ElDescriptionsItem>
               <ElDescriptionsItem label="订单金额">
                 ¥{((order.totalAmount || 0) / 100).toFixed(2)}
               </ElDescriptionsItem>
               <ElDescriptionsItem label="实付金额">
-                ¥{((order.payAmount || 0) / 100).toFixed(2)}
+                ¥{(((order.totalAmount || 0) - (order.discountAmount || 0)) / 100).toFixed(2)}
               </ElDescriptionsItem>
               <ElDescriptionsItem label="优惠金额">
                 ¥{((order.discountAmount || 0) / 100).toFixed(2)}
               </ElDescriptionsItem>
               <ElDescriptionsItem label="支付状态">
-                <el-tag type={order.payStatus === "paid" ? "success" : "warning"}>
-                  {order.payStatus === "paid" ? "已支付" : "待支付"}
+                <el-tag type={order.payStatus === "paid" ? "success" : order.payStatus === "refund" ? "danger" : "warning"}>
+                  {order.payStatus === "paid" ? "已支付" : order.payStatus === "refund" ? "已退款" : "待支付"}
                 </el-tag>
               </ElDescriptionsItem>
               <ElDescriptionsItem label="订单状态">
-                <el-tag type={order.status === 1 ? "success" : order.status === 3 ? "danger" : "info"}>
+                <el-tag type={order.status === 1 ? "success" : order.status === 3 ? "danger" : order.status === 2 ? "info" : "warning"}>
                   {order.status === 0 ? "待支付" : order.status === 1 ? "已完成" : order.status === 2 ? "已取消" : "已退款"}
                 </el-tag>
               </ElDescriptionsItem>
@@ -206,6 +246,16 @@ export function useOrder(tableRef: Ref) {
               <ElDescriptionsItem label="支付时间">
                 {order.payTime ? dayjs(order.payTime).format("YYYY-MM-DD HH:mm:ss") : "-"}
               </ElDescriptionsItem>
+              {order.videoUrl && (
+                <ElDescriptionsItem label="支付视频" span={2}>
+                  <video src={order.videoUrl} controls class="w-full max-w-md" />
+                </ElDescriptionsItem>
+              )}
+              {order.confidence && (
+                <ElDescriptionsItem label="置信度">
+                  {(order.confidence * 100).toFixed(2)}%
+                </ElDescriptionsItem>
+              )}
             </ElDescriptions>
 
             <div class="mt-4">

+ 8 - 9
haha-admin-web/src/views/order/utils/types.ts

@@ -2,24 +2,23 @@
 export interface OrderItem {
   id: number;
   orderNo: string;
+  outTradeNo?: string;
+  activityId?: string;
   userId: number;
-  userNickname: string;
-  userPhone: string;
   deviceId: string;
-  deviceName: string;
-  shopId: number;
-  shopName: string;
   totalAmount: number;
-  payAmount: number;
-  discountAmount: number;
+  payAmount?: number;
+  discountAmount?: number;
   payStatus: string;
   payTime?: string;
   status: number;
-  refundStatus: number;
+  refundStatus?: number;
   refundReason?: string;
   refundTime?: string;
   createTime: string;
-  items: OrderItemDetail[];
+  items?: OrderItemDetail[];
+  videoUrl?: string;
+  confidence?: number;
 }
 
 // 订单明细类型

+ 209 - 0
haha-admin-web/src/views/product/components/EnhancedImage.vue

@@ -0,0 +1,209 @@
+<script setup lang="ts">
+import { ref } from "vue";
+
+interface Props {
+  src: string;
+  alt?: string;
+  width?: string | number;
+  height?: string | number;
+  fit?: "fill" | "contain" | "cover" | "none" | "scale-down";
+  preview?: boolean;
+  lazy?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  alt: "",
+  width: "100%",
+  height: "100%",
+  fit: "cover",
+  preview: true,
+  lazy: false
+});
+
+const isPreviewVisible = ref(false);
+const currentImageUrl = ref("");
+
+const handleClick = () => {
+  if (props.preview && props.src) {
+    currentImageUrl.value = props.src;
+    isPreviewVisible.value = true;
+  }
+};
+
+const handleClose = () => {
+  isPreviewVisible.value = false;
+  currentImageUrl.value = "";
+};
+
+const handleImageError = (e: Event) => {
+  const img = e.target as HTMLImageElement;
+  img.src = "/img/placeholder.png";
+};
+</script>
+
+<template>
+  <div class="enhanced-image-container">
+    <img
+      :src="src"
+      :alt="alt"
+      :style="{ 
+        width: typeof width === 'number' ? width + 'px' : width,
+        height: typeof height === 'number' ? height + 'px' : height,
+        objectFit: fit
+      }"
+      :loading="lazy ? 'lazy' : 'eager'"
+      class="enhanced-image"
+      @click="handleClick"
+      @error="handleImageError"
+    />
+    
+    <!-- 自定义预览弹窗 -->
+    <div 
+      v-if="isPreviewVisible" 
+      class="image-preview-overlay"
+      @click.self="handleClose"
+    >
+      <div class="preview-content">
+        <button class="preview-close" @click="handleClose">×</button>
+        <img 
+          :src="currentImageUrl" 
+          :alt="alt"
+          class="preview-image"
+        />
+        <div class="preview-actions">
+          <span class="image-info">{{ alt || '商品图片' }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.enhanced-image-container {
+  display: inline-block;
+  position: relative;
+  
+  .enhanced-image {
+    cursor: pointer;
+    transition: all 0.3s ease;
+    border-radius: 4px;
+    object-fit: cover;
+    
+    &:hover {
+      transform: scale(1.05);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+      opacity: 0.9;
+    }
+  }
+}
+
+.image-preview-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background-color: rgba(0, 0, 0, 0.9);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 3000;
+  animation: fadeIn 0.3s ease;
+  
+  .preview-content {
+    position: relative;
+    max-width: 90vw;
+    max-height: 90vh;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    
+    .preview-close {
+      position: absolute;
+      top: -40px;
+      right: 0;
+      width: 36px;
+      height: 36px;
+      background-color: rgba(255, 255, 255, 0.2);
+      border: none;
+      border-radius: 50%;
+      color: white;
+      font-size: 24px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.3s ease;
+      
+      &:hover {
+        background-color: rgba(255, 255, 255, 0.3);
+        transform: rotate(90deg);
+      }
+    }
+    
+    .preview-image {
+      max-width: 100%;
+      max-height: 80vh;
+      object-fit: contain;
+      border-radius: 8px;
+      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+      animation: zoomIn 0.3s ease;
+    }
+    
+    .preview-actions {
+      margin-top: 20px;
+      padding: 12px 24px;
+      background-color: rgba(255, 255, 255, 0.1);
+      border-radius: 20px;
+      backdrop-filter: blur(10px);
+      
+      .image-info {
+        color: white;
+        font-size: 14px;
+        font-weight: 500;
+      }
+    }
+  }
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes zoomIn {
+  from {
+    transform: scale(0.8);
+    opacity: 0;
+  }
+  to {
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .image-preview-overlay {
+    .preview-content {
+      max-width: 95vw;
+      max-height: 95vh;
+      
+      .preview-close {
+        top: -30px;
+        width: 32px;
+        height: 32px;
+        font-size: 20px;
+      }
+      
+      .preview-image {
+        max-height: 75vh;
+      }
+    }
+  }
+}
+</style>

+ 2 - 0
haha-admin-web/src/views/product/index.vue

@@ -178,6 +178,8 @@ const {
 </template>
 
 <style lang="scss" scoped>
+@use "./styles/image-preview.scss";
+
 .main-content {
   margin: 24px 24px 0 !important;
 }

+ 19 - 10
haha-admin-web/src/views/product/new-apply/utils/hook.tsx

@@ -123,17 +123,26 @@ export function useNewProductApply(tableRef: Ref) {
 
   async function onSearch() {
     loading.value = true;
-    const params = {
-      page: pagination.currentPage,
-      pageSize: pagination.pageSize,
-      ...toRaw(form)
-    };
-    const { data } = await getNewProductApplyList(params);
-    if (data) {
-      dataList.value = data.list || [];
-      pagination.total = data.total || 0;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.productName) searchParams.productName = form.productName;
+      if (form.barcode) searchParams.barcode = form.barcode;
+      if (form.status) searchParams.status = form.status;
+      if (form.applicantId) searchParams.applicantId = form.applicantId;
+      
+      const { data } = await getNewProductApplyList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
+    } finally {
+      loading.value = false;
     }
-    loading.value = false;
   }
 
   function resetForm(formEl) {

+ 91 - 0
haha-admin-web/src/views/product/styles/image-preview.scss

@@ -0,0 +1,91 @@
+/* 商品图片预览样式优化 */
+
+.image-preview-container {
+  display: inline-block;
+  
+  :deep(.el-image) {
+    transition: all 0.3s ease;
+    
+    &:hover {
+      transform: scale(1.05);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+  }
+  
+  :deep(.el-image__inner) {
+    object-fit: cover;
+    transition: opacity 0.3s ease;
+  }
+  
+  :deep(.el-image__preview) {
+    cursor: zoom-in;
+  }
+}
+
+/* 图片预览弹窗样式优化 */
+:global(.el-image-viewer__wrapper) {
+  z-index: 3000 !important;
+}
+
+:global(.el-image-viewer__canvas) {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+:global(.el-image-viewer__img) {
+  max-width: 90vw;
+  max-height: 90vh;
+  object-fit: contain;
+}
+
+/* 预览导航按钮样式 */
+:global(.el-image-viewer__btn) {
+  background-color: rgba(0, 0, 0, 0.7);
+  border-radius: 50%;
+  width: 44px;
+  height: 44px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  
+  &:hover {
+    background-color: rgba(0, 0, 0, 0.9);
+  }
+}
+
+/* 关闭按钮样式 */
+:global(.el-image-viewer__close) {
+  top: 40px;
+  right: 40px;
+  width: 40px;
+  height: 40px;
+  background-color: rgba(0, 0, 0, 0.7);
+  border-radius: 50%;
+  
+  &:hover {
+    background-color: rgba(0, 0, 0, 0.9);
+  }
+}
+
+/* 加载指示器样式 */
+:global(.el-image-viewer__actions) {
+  background-color: rgba(0, 0, 0, 0.7);
+  border-radius: 20px;
+  padding: 8px 16px;
+}
+
+/* 响应式优化 */
+@media (max-width: 768px) {
+  :global(.el-image-viewer__img) {
+    max-width: 95vw;
+    max-height: 95vh;
+  }
+  
+  :global(.el-image-viewer__close) {
+    top: 20px;
+    right: 20px;
+    width: 36px;
+    height: 36px;
+  }
+}

+ 76 - 35
haha-admin-web/src/views/product/utils/hook.tsx

@@ -3,6 +3,7 @@ import { message } from "@/utils/message";
 import { addDialog } from "@/components/ReDialog";
 import type { PaginationProps } from "@pureadmin/table";
 import { deviceDetection } from "@pureadmin/utils";
+import EnhancedImage from "../components/EnhancedImage.vue";
 import {
   getProductList,
   addProduct,
@@ -51,35 +52,43 @@ export function useProduct(tableRef: Ref) {
 
   const columns: TableColumnList = [
     {
-      label: "商品ID",
+      label: "商品 ID",
       prop: "id",
-      width: 80
+      width: 70
     },
     {
       label: "商品图片",
-      prop: "image",
+      prop: "imageUrl",
       width: 80,
       cellRenderer: ({ row }) => (
-        <el-image
+        <EnhancedImage
+          src={row.imageUrl || "/img/placeholder.png"}
+          alt={row.name}
+          width={50}
+          height={50}
           fit="cover"
-          src={row.image}
-          preview-src-list={[row.image]}
-          class="w-[50px] h-[50px] rounded"
+          preview={true}
+          lazy={true}
         />
       )
     },
     {
       label: "商品名称",
       prop: "name",
-      minWidth: 150
+      minWidth: 120
+    },
+    {
+      label: "商品编码",
+      prop: "code",
+      minWidth: 100
     },
     {
       label: "条码",
       prop: "barcode",
-      minWidth: 120
+      minWidth: 100
     },
     {
-      label: "分类",
+      label: "商品分类",
       prop: "category",
       minWidth: 80,
       formatter: ({ category }) => {
@@ -88,31 +97,47 @@ export function useProduct(tableRef: Ref) {
       }
     },
     {
-      label: "售价(元)",
-      prop: "price",
-      minWidth: 80,
-      formatter: ({ price }) => (price / 100).toFixed(2)
+      label: "适用设备",
+      prop: "applyStatus",
+      minWidth: 100,
+      formatter: ({ applyStatus }) => {
+        if (!applyStatus) return "-";
+        const statusMap: Record<string, string> = {
+          "ALL": "全部柜",
+          "STATIC": "静态柜",
+          "DYNAMIC": "动态柜"
+        };
+        return statusMap[applyStatus] || applyStatus;
+      }
     },
     {
-      label: "成本价(元)",
-      prop: "costPrice",
-      minWidth: 80,
-      formatter: ({ costPrice }) => (costPrice / 100).toFixed(2)
+      label: "统一售价 (元)",
+      prop: "retailPrice",
+      minWidth: 85,
+      formatter: ({ retailPrice }) => 
+        retailPrice ? retailPrice.toFixed(2) : "-"
     },
     {
-      label: "单位",
-      prop: "unit",
-      minWidth: 60
+      label: "进价 (元)",
+      prop: "costPrice",
+      minWidth: 85,
+      formatter: ({ costPrice }) => 
+        costPrice ? costPrice.toFixed(2) : "-"
     },
     {
       label: "同步状态",
       prop: "syncStatus",
-      minWidth: 80,
-      cellRenderer: ({ row }) => (
-        <el-tag type={row.syncStatus === 1 ? "success" : "warning"}>
-          {row.syncStatus === 1 ? "已同步" : "未同步"}
-        </el-tag>
-      )
+      minWidth: 85,
+      cellRenderer: ({ row }) => {
+        const statusMap: Record<number, { label: string; type: string }> = {
+          0: { label: "未同步", type: "info" },
+          1: { label: "同步中", type: "warning" },
+          2: { label: "已同步", type: "success" },
+          3: { label: "同步失败", type: "danger" }
+        };
+        const status = statusMap[row.syncStatus] || { label: "未知", type: "info" };
+        return <el-tag type={status.type as any}>{status.label}</el-tag>;
+      }
     },
     {
       label: "创建时间",
@@ -124,7 +149,7 @@ export function useProduct(tableRef: Ref) {
     {
       label: "操作",
       fixed: "right",
-      width: 200,
+      width: 220,
       slot: "operation"
     }
   ];
@@ -136,7 +161,7 @@ export function useProduct(tableRef: Ref) {
     category: "",
     price: 0,
     costPrice: 0,
-    image: "",
+    imageUrl: "",
     description: "",
     specification: "",
     unit: "个",
@@ -147,10 +172,26 @@ export function useProduct(tableRef: Ref) {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getProductList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.name) searchParams.name = form.name;
+      if (form.barcode) searchParams.barcode = form.barcode;
+      if (form.category) searchParams.category = form.category;
+      if (form.syncStatus !== "") {
+        searchParams.syncStatus = parseInt(form.syncStatus, 10);
+      }
+      
+      const { data } = await getProductList(searchParams as {
+        page?: number;
+        pageSize?: number;
+        name?: string;
+        barcode?: string;
+        category?: string;
+        syncStatus?: number;
       });
       dataList.value = data.list;
       pagination.total = data.total;
@@ -190,7 +231,7 @@ export function useProduct(tableRef: Ref) {
       category: row?.category ?? "",
       price: row?.price ? row.price / 100 : 0,
       costPrice: row?.costPrice ? row.costPrice / 100 : 0,
-      image: row?.image ?? "",
+      imageUrl: row?.imageUrl ?? "",
       description: row?.description ?? "",
       specification: row?.specification ?? "",
       unit: row?.unit ?? "个",
@@ -246,8 +287,8 @@ export function useProduct(tableRef: Ref) {
               class="w-full!"
             />
           </ElFormItem>
-          <ElFormItem label="商品图片" prop="image">
-            <ElInput v-model={productForm.image} placeholder="请输入图片URL" clearable />
+          <ElFormItem label="商品图片" prop="imageUrl">
+            <ElInput v-model={productForm.imageUrl} placeholder="请输入图片 URL" clearable />
           </ElFormItem>
           <ElFormItem label="规格" prop="specification">
             <ElInput v-model={productForm.specification} placeholder="请输入规格" clearable />

+ 10 - 4
haha-admin-web/src/views/product/utils/types.ts

@@ -1,16 +1,22 @@
 // 商品信息类型
 export interface ProductItem {
   id: number;
+  productId?: string;
+  code?: string;
   name: string;
   barcode: string;
   category: string;
-  price: number;
+  type?: string;
+  price?: number;
+  retailPrice?: number;
   costPrice: number;
-  image: string;
+  imageUrl: string;
+  planogram?: string;
+  applyStatus?: string;
   description?: string;
   specification?: string;
   unit: string;
-  status: number;
+  status?: number;
   syncStatus: number;
   syncTime?: string;
   createTime: string;
@@ -21,5 +27,5 @@ export interface ProductSearchForm {
   name: string;
   barcode: string;
   category: string;
-  syncStatus: number | string;
+  syncStatus: string;
 }

+ 162 - 47
haha-admin-web/src/views/shop/utils/hook.tsx

@@ -3,6 +3,8 @@ import { message } from "@/utils/message";
 import { addDialog } from "@/components/ReDialog";
 import type { PaginationProps } from "@pureadmin/table";
 import { deviceDetection } from "@pureadmin/utils";
+import { AmapWithSearch } from "@/components/AmapWithSearch";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import {
   getShopList,
   createShop,
@@ -98,6 +100,20 @@ export function useShop(tableRef: Ref) {
         </el-tag>
       )
     },
+    {
+      label: "定位",
+      width: 80,
+      cellRenderer: ({ row }) => (
+        <el-button
+          size="small"
+          type="primary"
+          link
+          onClick={() => handleShowLocation(row)}
+        >
+          查看
+        </el-button>
+      )
+    },
     {
       label: "创建时间",
       prop: "createTime",
@@ -117,13 +133,19 @@ export function useShop(tableRef: Ref) {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getShopList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
-      });
-      dataList.value = data.list;
-      pagination.total = data.total;
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.name) searchParams.name = form.name;
+      if (form.code) searchParams.code = form.code;
+      if (form.status !== undefined) searchParams.status = form.status;
+      
+      const { data } = await getShopList(searchParams);
+      dataList.value = data.list || [];
+      pagination.total = data.total || 0;
     } catch (error) {
       console.error("获取门店列表失败:", error);
     } finally {
@@ -141,6 +163,56 @@ export function useShop(tableRef: Ref) {
     onSearch();
   };
 
+  // 经纬度变化处理
+  function handleLngLatChange(field: "longitude" | "latitude", value: string) {
+    if (field === "longitude") {
+      shopForm.longitude = value;
+    } else {
+      shopForm.latitude = value;
+    }
+  }
+
+  // 查看门店位置
+  function handleShowLocation(row: ShopItem) {
+    addDialog({
+      title: `门店位置 - ${row.name}`,
+      width: "800px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      closeOnClickModal: true,
+      hideFooter: true,
+      contentRenderer: () => (
+        <div class="p-4">
+          <div class="mb-4 text-sm text-gray-600">
+            <div class="mb-2">
+              <strong>门店名称:</strong>{row.name}
+            </div>
+            <div class="mb-2">
+              <strong>门店地址:</strong>{row.province}{row.city}{row.district}{row.address}
+            </div>
+            {row.longitude && row.latitude ? (
+              <div class="text-green-600">
+                <strong>坐标:</strong>经度 {row.longitude},纬度 {row.latitude}
+              </div>
+            ) : (
+              <div class="text-orange-500">
+                <strong>提示:</strong>该门店暂无坐标信息,请在编辑页面设置位置
+              </div>
+            )}
+          </div>
+          <AmapWithSearch
+            longitude={Number(row.longitude) || undefined}
+            latitude={Number(row.latitude) || undefined}
+            height="450px"
+            zoom={15}
+            enableClickMark={false}
+            showSearch={false}
+          />
+        </div>
+      )
+    });
+  }
+
   // 分页
   function handleSizeChange(val: number) {
     pagination.pageSize = val;
@@ -185,51 +257,92 @@ export function useShop(tableRef: Ref) {
 
     addDialog({
       title: `${title}门店`,
-      width: "50%",
+      width: "60%",
       draggable: true,
       fullscreen: deviceDetection(),
       closeOnClickModal: false,
       contentRenderer: () => (
-        <ElForm ref={ruleFormRef} model={shopForm} label-width="100px">
-          <ElFormItem
-            label="门店名称"
-            prop="name"
-            rules={[{ required: true, message: "请输入门店名称", trigger: "blur" }]}
-          >
-            <ElInput v-model={shopForm.name} placeholder="请输入门店名称" clearable />
-          </ElFormItem>
-          <ElFormItem label="省份" prop="province">
-            <ElInput v-model={shopForm.province} placeholder="请输入省份" clearable />
-          </ElFormItem>
-          <ElFormItem label="城市" prop="city">
-            <ElInput v-model={shopForm.city} placeholder="请输入城市" clearable />
-          </ElFormItem>
-          <ElFormItem label="区县" prop="district">
-            <ElInput v-model={shopForm.district} placeholder="请输入区县" clearable />
-          </ElFormItem>
-          <ElFormItem
-            label="详细地址"
-            prop="address"
-            rules={[{ required: true, message: "请输入详细地址", trigger: "blur" }]}
-          >
-            <ElInput v-model={shopForm.address} placeholder="请输入详细地址" clearable />
-          </ElFormItem>
-          <ElFormItem label="联系人" prop="contactName">
-            <ElInput v-model={shopForm.contactName} placeholder="请输入联系人" clearable />
-          </ElFormItem>
-          <ElFormItem label="联系电话" prop="contactPhone">
-            <ElInput v-model={shopForm.contactPhone} placeholder="请输入联系电话" clearable />
-          </ElFormItem>
-          <ElFormItem label="营业时间" prop="businessHours">
-            <ElInput v-model={shopForm.businessHours} placeholder="如: 08:00-22:00" clearable />
-          </ElFormItem>
-          <ElFormItem label="状态" prop="status">
-            <ElRadioGroup v-model={shopForm.status}>
-              <ElRadioButton value={1}>营业中</ElRadioButton>
-              <ElRadioButton value={0}>已停业</ElRadioButton>
-            </ElRadioGroup>
-          </ElFormItem>
-        </ElForm>
+        <div>
+          <ElForm ref={ruleFormRef} model={shopForm} label-width="100px">
+            <ElFormItem
+              label="门店名称"
+              prop="name"
+              rules={[{ required: true, message: "请输入门店名称", trigger: "blur" }]}
+            >
+              <ElInput v-model={shopForm.name} placeholder="请输入门店名称" clearable />
+            </ElFormItem>
+            <ElFormItem label="省份" prop="province">
+              <ElInput v-model={shopForm.province} placeholder="请输入省份" clearable />
+            </ElFormItem>
+            <ElFormItem label="城市" prop="city">
+              <ElInput v-model={shopForm.city} placeholder="请输入城市" clearable />
+            </ElFormItem>
+            <ElFormItem label="区县" prop="district">
+              <ElInput v-model={shopForm.district} placeholder="请输入区县" clearable />
+            </ElFormItem>
+            <ElFormItem
+              label="详细地址"
+              prop="address"
+              rules={[{ required: true, message: "请输入详细地址", trigger: "blur" }]}
+            >
+              <ElInput v-model={shopForm.address} placeholder="请输入详细地址" clearable />
+            </ElFormItem>
+            <ElFormItem label="联系人" prop="contactName">
+              <ElInput v-model={shopForm.contactName} placeholder="请输入联系人" clearable />
+            </ElFormItem>
+            <ElFormItem label="联系电话" prop="contactPhone">
+              <ElInput v-model={shopForm.contactPhone} placeholder="请输入联系电话" clearable />
+            </ElFormItem>
+            <ElFormItem label="营业时间" prop="businessHours">
+              <ElInput v-model={shopForm.businessHours} placeholder="如: 08:00-22:00" clearable />
+            </ElFormItem>
+            <ElFormItem label="状态" prop="status">
+              <ElRadioGroup v-model={shopForm.status}>
+                <ElRadioButton value={1}>营业中</ElRadioButton>
+                <ElRadioButton value={0}>已停业</ElRadioButton>
+              </ElRadioGroup>
+            </ElFormItem>
+          </ElForm>
+          <div class="mt-4">
+            <div class="text-sm font-medium mb-2">门店位置</div>
+            <div class="grid grid-cols-2 gap-4 mb-4">
+              <ElFormItem label="经度" prop="longitude" class="flex-1">
+                <ElInput
+                  v-model={shopForm.longitude}
+                  placeholder="请输入经度"
+                  clearable
+                  onChange={(val: any) => handleLngLatChange("longitude", val)}
+                />
+              </ElFormItem>
+              <ElFormItem label="纬度" prop="latitude" class="flex-1">
+                <ElInput
+                  v-model={shopForm.latitude}
+                  placeholder="请输入纬度"
+                  clearable
+                  onChange={(val: any) => handleLngLatChange("latitude", val)}
+                />
+              </ElFormItem>
+            </div>
+            <AmapWithSearch
+              longitude={Number(shopForm.longitude) || undefined}
+              latitude={Number(shopForm.latitude) || undefined}
+              height="350px"
+              zoom={15}
+              enableClickMark={true}
+              showSearch={true}
+              securityJsCode="bcfcaa8d8ef22452d4cf6d3892646262"
+              onUpdate:longitude={(val: number) => (shopForm.longitude = String(val))}
+              onUpdate:latitude={(val: number) => (shopForm.latitude = String(val))}
+              onMarkerMoved={(lng: number, lat: number) => {
+                shopForm.longitude = String(lng);
+                shopForm.latitude = String(lat);
+              }}
+            />
+            <div class="text-xs text-gray-500 mt-2">
+              <span>提示:</span>可以通过地址搜索快速定位,也可以点击地图或拖动标记点来精确选择门店位置
+            </div>
+          </div>
+        </div>
       ),
       beforeSure: async (done) => {
         const valid = await ruleFormRef.value.validate().catch(() => false);
@@ -414,9 +527,11 @@ export function useShop(tableRef: Ref) {
     pagination,
     onSearch,
     resetForm,
+    handleLngLatChange,
     openDialog,
     handleDelete,
     handleToggleStatus,
+    handleShowLocation,
     handleDevices,
     handleReplenishers,
     handleSizeChange,

+ 8 - 3
haha-admin-web/src/views/sync/utils/hook.tsx

@@ -101,11 +101,16 @@ export function useSync() {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getSyncRecords({
-        ...toRaw(form),
+      const searchParams: any = {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
-      });
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.syncType) searchParams.syncType = form.syncType;
+      if (form.status !== undefined) searchParams.status = form.status;
+      
+      const { data } = await getSyncRecords(searchParams);
       if (data) {
         dataList.value = data.list || [];
         pagination.total = data.total || 0;

+ 15 - 6
haha-admin-web/src/views/system/admin/utils/hook.tsx

@@ -202,13 +202,22 @@ export function useAdmin(tableRef: Ref) {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getAdminList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
-      });
-      dataList.value = data.list;
-      pagination.total = data.total;
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.username) searchParams.username = form.username;
+      if (form.phone) searchParams.phone = form.phone;
+      if (form.roleId) searchParams.roleId = form.roleId;
+      if (form.status) searchParams.status = form.status;
+      
+      const { data } = await getAdminList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
     } catch (error) {
       console.error("获取管理员列表失败:", error);
     } finally {

+ 9 - 3
haha-admin-web/src/views/system/announcement/utils/hook.tsx

@@ -113,11 +113,17 @@ export function useAnnouncement() {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getAnnouncementList({
-        ...toRaw(form),
+      const searchParams: any = {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
-      });
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.title) searchParams.title = form.title;
+      if (form.type !== undefined) searchParams.type = form.type;
+      if (form.status !== undefined) searchParams.status = form.status;
+      
+      const { data } = await getAnnouncementList(searchParams);
       if (data) {
         dataList.value = data.list || [];
         pagination.total = data.total || 0;

+ 11 - 3
haha-admin-web/src/views/system/operation-log/utils/hook.tsx

@@ -104,11 +104,19 @@ export function useOperationLog() {
   async function onSearch() {
     loading.value = true;
     try {
-      const { data } = await getOperationLogList({
-        ...toRaw(form),
+      const searchParams: any = {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
-      });
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.module) searchParams.module = form.module;
+      if (form.username) searchParams.username = form.username;
+      if (form.status !== undefined) searchParams.status = form.status;
+      if (form.startTime) searchParams.startTime = form.startTime;
+      if (form.endTime) searchParams.endTime = form.endTime;
+      
+      const { data } = await getOperationLogList(searchParams);
       if (data) {
         dataList.value = data.list || [];
         pagination.total = data.total || 0;

+ 12 - 4
haha-admin-web/src/views/system/role/utils/hook.tsx

@@ -197,15 +197,23 @@ export function useRole(treeRef: Ref) {
     loading.value = true;
     isDataLoaded.value = false;
     try {
-      const { data } = await getRoleList({
+      const searchParams: any = {
         page: pagination.currentPage,
-        pageSize: pagination.pageSize,
-        ...toRaw(form)
-      });
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.name) searchParams.name = form.name;
+      if (form.code) searchParams.code = form.code;
+      if (form.status) searchParams.status = form.status;
+      
+      const { data } = await getRoleList(searchParams);
       dataList.value = data.list;
       pagination.total = data.total;
     } catch (error) {
       console.error("获取角色列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
     } finally {
       setTimeout(() => {
         loading.value = false;

+ 23 - 9
haha-admin-web/src/views/system/user/utils/hook.tsx

@@ -272,15 +272,29 @@ export function useUser(tableRef: Ref, treeRef: Ref) {
 
   async function onSearch() {
     loading.value = true;
-    const { data } = await getUserList(toRaw(form));
-    dataList.value = data.list;
-    pagination.total = data.total;
-    pagination.pageSize = data.pageSize;
-    pagination.currentPage = data.currentPage;
-
-    setTimeout(() => {
-      loading.value = false;
-    }, 500);
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+      
+      // 只添加非空的搜索条件
+      if (form.name) searchParams.name = form.name;
+      if (form.username) searchParams.username = form.username;
+      if (form.phone) searchParams.phone = form.phone;
+      if (form.status !== undefined) searchParams.status = form.status;
+      if (form.roleId) searchParams.roleId = form.roleId;
+      
+      const { data } = await getUserList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 500);
+    }
   }
 
   const resetForm = formEl => {

+ 11 - 3
haha-admin/src/main/java/com/haha/admin/controller/ProductController.java

@@ -5,7 +5,6 @@ import com.haha.admin.annotation.RequirePermission;
 import com.haha.admin.dto.ProductQueryDTO;
 import com.haha.common.annotation.Log;
 import com.haha.common.enums.OperationType;
-import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.Product;
 import com.haha.service.ProductService;
@@ -13,6 +12,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -33,7 +33,7 @@ public class ProductController {
      */
     @RequirePermission("product:read")
     @GetMapping("/list")
-    public Result<PageResult<Product>> list(ProductQueryDTO queryDTO) {
+    public Result<Map<String, Object>> list(ProductQueryDTO queryDTO) {
         queryDTO.validate();
 
         IPage<Product> productPage = productService.getPage(
@@ -45,7 +45,15 @@ public class ProductController {
                 queryDTO.getSyncStatus()
         );
 
-        return Result.success("查询成功", PageResult.of(productPage));
+        // 构造前端需要的分页数据格式
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", productPage.getRecords());
+        result.put("total", productPage.getTotal());
+        result.put("pageSize", productPage.getSize());
+        result.put("currentPage", productPage.getCurrent());
+        result.put("totalPages", productPage.getPages());
+        
+        return Result.success("查询成功", result);
     }
 
     /**

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

@@ -57,7 +57,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         // 分页查询
         IPage<Order> orderPage = this.page(new Page<>(page, pageSize), wrapper);
         
-        // 填充标签字段
+        // 填充标签字段和关联信息
         orderPage.getRecords().forEach(this::fillOrderLabels);
         
         return orderPage;
@@ -191,5 +191,9 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             var statusLabel = EntityLabelUtils.getStatusLabel("order", order.getStatus());
             order.setStatusLabel(statusLabel.getLabel());
         }
+        
+        // TODO: 根据 userId 查询用户信息(昵称、手机号)
+        // TODO: 根据 deviceId 查询设备信息(设备名称、门店名称)
+        // 目前这些字段需要在前端通过其他方式获取,或者后续实现联表查询
     }
 }

+ 36 - 4
haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

@@ -37,8 +37,33 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
                .eq(Product::getIsDeleted, CommonConstants.NOT_DELETED)
                .orderByDesc(Product::getCreateTime);
 
+        System.out.println("Product query conditions:");
+        System.out.println("  page: " + page);
+        System.out.println("  pageSize: " + pageSize);
+        System.out.println("  name: " + name);
+        System.out.println("  barcode: " + barcode);
+        System.out.println("  category: " + category);
+        System.out.println("  syncStatus: " + syncStatus);
+
         IPage<Product> productPage = this.page(new Page<>(page, pageSize), wrapper);
         
+        System.out.println("Query result:");
+        System.out.println("  total records: " + productPage.getTotal());
+        System.out.println("  current page: " + productPage.getCurrent());
+        System.out.println("  page size: " + productPage.getSize());
+        System.out.println("  total pages: " + productPage.getPages());
+        System.out.println("  records count: " + productPage.getRecords().size());
+        
+        // 检查数据库连接
+        System.out.println("Database connection check:");
+        try {
+            long totalCount = this.count();
+            System.out.println("  Total products in database: " + totalCount);
+        } catch (Exception e) {
+            System.out.println("  Database connection error: " + e.getMessage());
+            e.printStackTrace();
+        }
+        
         // 填充额外字段
         productPage.getRecords().forEach(this::fillProductLabels);
         
@@ -176,20 +201,27 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
             product.setSyncStatusColor(statusLabel.getColor());
         }
 
-        // 图片URL
+        // 图片 URL
         if (product.getImageUrl() == null && product.getPic() != null) {
             product.setImageUrl(product.getPic());
         }
 
-        // 默认值
+        // 分类字段
         if (product.getCategory() == null) {
             product.setCategory(product.getType());
         }
+        
+        // 价格字段(转换为分)
         if (product.getPrice() == null && product.getRetailPrice() != null) {
-            product.setPrice(product.getRetailPrice());
+            product.setPrice(product.getRetailPrice() * 100);
         }
         if (product.getCost() == null && product.getCostPrice() != null) {
-            product.setCost(product.getCostPrice());
+            product.setCost(product.getCostPrice() * 100);
+        }
+        
+        // 最后同步时间
+        if (product.getLastSyncTime() == null && product.getSyncTime() != null) {
+            product.setLastSyncTime(product.getSyncTime());
         }
     }
 }