Răsfoiți Sursa

端口修改

skyline 2 luni în urmă
părinte
comite
d8dab8cbce

+ 10 - 1
haha-admin-web/src/components/Amap/src/index.vue

@@ -144,7 +144,16 @@ const initMap = async () => {
       center: initialCenter,
       resizeEnable: true,
       viewMode: "2D",
-      keyboardEnable: true
+      keyboardEnable: true,
+      scrollWheel: true,
+      doubleClickZoom: true,
+      zoomEnable: true,
+      // 控制地图显示元素,减少标注重叠
+      features: ['bg', 'road', 'building', 'point'],
+      // 设置标注显示策略:避免重叠
+      showIndoorMap: false,
+      // 使用标准样式,标注更清晰
+      mapStyle: 'amap://styles/normal'
     });
 
     console.log('[地图初始化] 地图实例创建成功:', !!map);

+ 12 - 5
haha-admin-web/src/components/AmapWithSearch/src/index.vue

@@ -351,17 +351,24 @@ const initMap = async () => {
       keyboardEnable: true,
       scrollWheel: true,
       doubleClickZoom: true,
-      zoomEnable: true
+      zoomEnable: true,
+      // 控制地图显示元素,减少标注重叠
+      features: ['bg', 'road', 'building', 'point'],
+      // 设置标注显示策略:避免重叠
+      showIndoorMap: false,
+      // 使用标准样式,标注更清晰
+      mapStyle: 'amap://styles/normal'
     };
     
     // 如果 AMap 已加载,添加图层配置
     if ((window as any).AMap) {
+      // 只使用默认底图,移除路网层避免重叠
       mapOptions.layers = [
-        new (window as any).AMap.TileLayer(), // 默认底图
-        new (window as any).AMap.TileLayer.RoadNet() // 路网层
+        new (window as any).AMap.TileLayer({
+          // 控制标注密度
+          zIndex: 1
+        })
       ];
-      // 暂时不设置自定义样式,避免 API 错误
-      // mapOptions.mapStyle = 'amap://styles/normal';
     }
     
     map = new AMap.Map(mapContainer.value, mapOptions);

+ 75 - 112
haha-admin-web/src/views/marketing/components/DeviceSelector.vue

@@ -1,145 +1,108 @@
 <script setup lang="ts">
-import { ref, watch, onMounted } from "vue";
+import { ref, onMounted } from "vue";
 import { getDeviceList } from "@/api/device";
 
 defineOptions({
   name: "DeviceSelector"
 });
 
-const props = defineProps<{
-  modelValue: string[];
-}>();
+interface Device {
+  id: number;
+  deviceId: string;
+  storeName: string;
+  status: number;
+}
 
-const emit = defineEmits<{
-  (e: "update:modelValue", value: string[]): void;
-}>();
+const props = defineProps({
+  modelValue: {
+    type: Array as () => number[],
+    default: () => []
+  },
+  multiple: {
+    type: Boolean,
+    default: true
+  },
+  placeholder: {
+    type: String,
+    default: "请选择设备"
+  }
+});
 
-const selectedDevices = ref<string[]>([...props.modelValue]);
-const deviceList = ref<any[]>([]);
-const loading = ref(false);
-const dialogVisible = ref(false);
+const emit = defineEmits(["update:modelValue", "change"]);
 
-const selectedDeviceNames = ref<string[]>([]);
+const devices = ref<Device[]>([]);
+const filteredDevices = ref<Device[]>([]);
+const loading = ref(false);
 
-async function loadDevices() {
+async function fetchDevices() {
   loading.value = true;
   try {
     const { data } = await getDeviceList({ page: 1, pageSize: 1000 });
-    if (data) {
-      deviceList.value = data.list || [];
-      updateSelectedNames();
-    }
+    devices.value = data?.list || [];
+    filteredDevices.value = devices.value;
+  } catch (error) {
+    console.error("获取设备列表失败:", error);
+    // 模拟数据用于演示
+    devices.value = [
+      { id: 1, deviceId: "DEV001", storeName: "北京旗舰店-A01", status: 1 },
+      { id: 2, deviceId: "DEV002", storeName: "北京旗舰店-A02", status: 1 },
+      { id: 3, deviceId: "DEV003", storeName: "上海体验店-B01", status: 1 },
+      { id: 4, deviceId: "DEV004", storeName: "广州分店-C01", status: 1 }
+    ];
+    filteredDevices.value = devices.value;
   } finally {
     loading.value = false;
   }
 }
 
-function updateSelectedNames() {
-  selectedDeviceNames.value = selectedDevices.value.map(id => {
-    const device = deviceList.value.find(d => d.deviceId === id);
-    return device ? device.name : id;
-  });
-}
-
-function handleSelectionChange(selection: any[]) {
-  selectedDevices.value = selection.map(item => item.deviceId);
-}
-
-function confirmSelection() {
-  emit("update:modelValue", [...selectedDevices.value]);
-  updateSelectedNames();
-  dialogVisible.value = false;
-}
-
-function removeDevice(deviceId: string) {
-  const index = selectedDevices.value.indexOf(deviceId);
-  if (index > -1) {
-    selectedDevices.value.splice(index, 1);
-    emit("update:modelValue", [...selectedDevices.value]);
-    updateSelectedNames();
+function handleFilter(query: string) {
+  if (!query) {
+    filteredDevices.value = devices.value;
+    return;
   }
+  filteredDevices.value = devices.value.filter(device => 
+    device.deviceId.toLowerCase().includes(query.toLowerCase()) ||
+    device.storeName.toLowerCase().includes(query.toLowerCase())
+  );
 }
 
-watch(
-  () => props.modelValue,
-  (newVal) => {
-    selectedDevices.value = [...newVal];
-    updateSelectedNames();
-  }
-);
+function handleChange(value: number[]) {
+  emit("update:modelValue", value);
+  emit("change", value);
+}
 
 onMounted(() => {
-  loadDevices();
+  fetchDevices();
 });
 </script>
 
 <template>
-  <div class="device-selector">
-    <div class="selected-tags flex flex-wrap gap-2 mb-2">
-      <el-tag
-        v-for="(name, index) in selectedDeviceNames"
-        :key="index"
-        closable
-        @close="removeDevice(selectedDevices[index])"
-      >
-        {{ name }}
-      </el-tag>
-      <el-button
-        v-if="selectedDevices.length === 0"
-        type="primary"
-        link
-        @click="dialogVisible = true"
-      >
-        选择设备
-      </el-button>
-    </div>
-    <el-button
-      v-if="selectedDevices.length > 0"
-      type="primary"
-      link
-      @click="dialogVisible = true"
-    >
-      修改选择
-    </el-button>
-
-    <el-dialog
-      v-model="dialogVisible"
-      title="选择设备"
-      width="600px"
-      draggable
-      destroy-on-close
+  <el-select
+    :model-value="modelValue"
+    :multiple="multiple"
+    :placeholder="placeholder"
+    :loading="loading"
+    :filterable="true"
+    :filter-method="handleFilter"
+    @change="handleChange"
+    class="w-full"
+  >
+    <el-option
+      v-for="device in filteredDevices"
+      :key="device.id"
+      :label="device.storeName"
+      :value="device.id"
     >
-      <el-table
-        :data="deviceList"
-        v-loading="loading"
-        max-height="400"
-        @selection-change="handleSelectionChange"
-        :row-key="row => row.deviceId"
-      >
-        <el-table-column type="selection" width="55" />
-        <el-table-column prop="deviceId" label="设备ID" width="120" />
-        <el-table-column prop="name" label="设备名称" min-width="150" />
-        <el-table-column prop="shopName" label="所属门店" min-width="120" />
-        <el-table-column prop="status" label="状态" width="80">
-          <template #default="{ row }">
-            <el-tag :type="row.status === 1 ? 'success' : 'info'">
-              {{ row.status === 1 ? '在线' : '离线' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-      </el-table>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmSelection">
-          确定选择 ({{ selectedDevices.length }}个)
-        </el-button>
-      </template>
-    </el-dialog>
-  </div>
+      <span>{{ device.storeName }}</span>
+      <span class="text-gray-400 text-sm ml-2">{{ device.deviceId }}</span>
+    </el-option>
+  </el-select>
 </template>
 
-<style scoped lang="scss">
-.device-selector {
-  width: 100%;
+<style lang="scss" scoped>
+:deep(.el-select-dropdown__item) {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 }
 </style>

+ 17 - 1
haha-admin-web/src/views/marketing/components/ProductSelector.vue

@@ -31,6 +31,7 @@ const props = defineProps({
 const emit = defineEmits(["update:modelValue", "change"]);
 
 const products = ref<Product[]>([]);
+const filteredProducts = ref<Product[]>([]);
 const loading = ref(false);
 
 async function fetchProducts() {
@@ -39,6 +40,7 @@ async function fetchProducts() {
     // 这里应该调用实际的商品API
     const { data } = await getProductList({ page: 1, pageSize: 1000 });
     products.value = data?.list || [];
+    filteredProducts.value = products.value;
   } catch (error) {
     console.error("获取商品列表失败:", error);
     // 模拟数据用于演示
@@ -49,11 +51,23 @@ async function fetchProducts() {
       { id: 4, name: "矿泉水", price: 2.0, categoryName: "饮品" },
       { id: 5, name: "红牛功能饮料", price: 6.0, categoryName: "功能饮料" }
     ];
+    filteredProducts.value = products.value;
   } finally {
     loading.value = false;
   }
 }
 
+function handleFilter(query: string) {
+  if (!query) {
+    filteredProducts.value = products.value;
+    return;
+  }
+  filteredProducts.value = products.value.filter(product => 
+    product.name.toLowerCase().includes(query.toLowerCase()) ||
+    product.categoryName.toLowerCase().includes(query.toLowerCase())
+  );
+}
+
 function handleChange(value: number[]) {
   emit("update:modelValue", value);
   emit("change", value);
@@ -70,11 +84,13 @@ onMounted(() => {
     :multiple="multiple"
     :placeholder="placeholder"
     :loading="loading"
+    :filterable="true"
+    :filter-method="handleFilter"
     @change="handleChange"
     class="w-full"
   >
     <el-option
-      v-for="product in products"
+      v-for="product in filteredProducts"
       :key="product.id"
       :label="product.name"
       :value="product.id"

+ 17 - 1
haha-admin-web/src/views/marketing/components/ShopSelector.vue

@@ -30,6 +30,7 @@ const props = defineProps({
 const emit = defineEmits(["update:modelValue", "change"]);
 
 const shops = ref<Shop[]>([]);
+const filteredShops = ref<Shop[]>([]);
 const loading = ref(false);
 
 async function fetchShops() {
@@ -38,6 +39,7 @@ async function fetchShops() {
     // 这里应该调用实际的门店API
     const { data } = await getShopList({ page: 1, pageSize: 1000 });
     shops.value = data?.list || [];
+    filteredShops.value = shops.value;
   } catch (error) {
     console.error("获取门店列表失败:", error);
     // 模拟数据用于演示
@@ -47,11 +49,23 @@ async function fetchShops() {
       { id: 3, name: "广州分店", address: "广州市天河区xxx路xxx号" },
       { id: 4, name: "深圳直营店", address: "深圳市南山区xxx路xxx号" }
     ];
+    filteredShops.value = shops.value;
   } finally {
     loading.value = false;
   }
 }
 
+function handleFilter(query: string) {
+  if (!query) {
+    filteredShops.value = shops.value;
+    return;
+  }
+  filteredShops.value = shops.value.filter(shop => 
+    shop.name.toLowerCase().includes(query.toLowerCase()) ||
+    shop.address.toLowerCase().includes(query.toLowerCase())
+  );
+}
+
 function handleChange(value: number[]) {
   emit("update:modelValue", value);
   emit("change", value);
@@ -68,11 +82,13 @@ onMounted(() => {
     :multiple="multiple"
     :placeholder="placeholder"
     :loading="loading"
+    :filterable="true"
+    :filter-method="handleFilter"
     @change="handleChange"
     class="w-full"
   >
     <el-option
-      v-for="shop in shops"
+      v-for="shop in filteredShops"
       :key="shop.id"
       :label="shop.name"
       :value="shop.id"

+ 95 - 55
haha-admin-web/src/views/marketing/coupon/utils/hook.tsx

@@ -12,6 +12,7 @@ import {
 import { distributeCoupons, getCouponDistributeRecords } from "@/api/marketing";
 import ShopSelector from "../../components/ShopSelector.vue";
 import ProductSelector from "../../components/ProductSelector.vue";
+import DeviceSelector from "../../components/DeviceSelector.vue";
 import type { PaginationProps, TableColumnList } from "@pureadmin/table";
 import { deviceDetection } from "@pureadmin/utils";
 import { onMounted, reactive, ref, toRaw } from "vue";
@@ -49,6 +50,7 @@ interface CouponFormData {
   value: number | null;
   totalCount: number | null;
   shopIds: number[];
+  deviceIds: number[];
   productIds: number[];
   validPeriod: string[];
   description: string;
@@ -263,33 +265,39 @@ export function useCoupon() {
     }
     
     .form-block {
-      margin-bottom: 18px;
+      margin-bottom: 20px;
     }
     
     .block-title {
-      font-size: 15px;
+      font-size: 14px;
       font-weight: 600;
       color: #374151;
-      margin-bottom: 12px;
+      margin-bottom: 14px;
       padding-bottom: 8px;
-      border-bottom: 1px solid #f3f4f6;
+      border-bottom: 1px solid #e5e7eb;
     }
     
     .form-grid-2 {
       display: grid;
       grid-template-columns: repeat(2, 1fr);
-      gap: 16px;
+      gap: 20px 24px;
+    }
+    
+    .form-grid-3 {
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 20px 24px;
     }
     
     .type-selector {
-      display: flex;
+      display: grid;
+      grid-template-columns: repeat(4, 1fr);
       gap: 12px;
       margin-bottom: 18px;
     }
     
     .type-btn {
-      flex: 1;
-      padding: 14px 16px;
+      padding: 12px 16px;
       border: 1px solid #e5e7eb;
       border-radius: 6px;
       background: #fff;
@@ -300,6 +308,7 @@ export function useCoupon() {
     
     .type-btn:hover {
       border-color: #3b82f6;
+      background: #f8fafc;
     }
     
     .type-btn.active {
@@ -308,14 +317,14 @@ export function useCoupon() {
     }
     
     .type-btn .name {
-      font-size: 15px;
+      font-size: 14px;
       font-weight: 600;
       color: #374151;
       margin-bottom: 4px;
     }
     
     .type-btn .hint {
-      font-size: 13px;
+      font-size: 12px;
       color: #9ca3af;
     }
     
@@ -326,44 +335,55 @@ export function useCoupon() {
     .scope-grid {
       display: grid;
       grid-template-columns: repeat(3, 1fr);
-      gap: 20px;
+      gap: 16px;
     }
     
     .scope-card {
       background: #f9fafb;
-      border-radius: 6px;
-      padding: 14px;
+      border: 1px solid #e5e7eb;
+      border-radius: 8px;
+      padding: 14px 16px;
     }
     
     .scope-card .card-header {
       display: flex;
       align-items: center;
       justify-content: space-between;
-      margin-bottom: 10px;
+      margin-bottom: 12px;
     }
     
     .scope-card .card-label {
-      font-size: 14px;
+      font-size: 13px;
       font-weight: 500;
       color: #374151;
     }
     
     .scope-card .card-body {
-      min-height: 40px;
+      min-height: 32px;
     }
     
     .coupon-form .el-form-item {
-      margin-bottom: 14px;
+      margin-bottom: 0;
+      align-items: center;
     }
     
     .coupon-form .el-form-item__label {
-      font-size: 14px;
-      color: #6b7280;
+      font-size: 13px;
+      color: #606266;
+      padding-right: 8px;
     }
     
     .coupon-form .el-input__wrapper,
     .coupon-form .el-textarea__inner {
-      font-size: 14px;
+      font-size: 13px;
+    }
+    
+    .coupon-form .form-label-90 .el-form-item__label {
+      width: 90px !important;
+    }
+    
+    .coupon-form .form-label-100 .el-form-item__label {
+      width: 100px !important;
     }
     
     .preview-box {
@@ -375,7 +395,7 @@ export function useCoupon() {
       background: #f9fafb;
       border-radius: 4px;
       margin-top: 12px;
-      font-size: 15px;
+      font-size: 14px;
       color: #374151;
     }
     
@@ -383,13 +403,24 @@ export function useCoupon() {
       font-weight: 600;
     }
     
+    @media (max-width: 1200px) {
+      .form-grid-3,
+      .scope-grid {
+        grid-template-columns: repeat(2, 1fr);
+      }
+      .type-selector {
+        grid-template-columns: repeat(2, 1fr);
+      }
+    }
+    
     @media (max-width: 768px) {
       .form-grid-2,
+      .form-grid-3,
       .scope-grid {
         grid-template-columns: 1fr;
       }
       .type-selector {
-        flex-direction: column;
+        grid-template-columns: 1fr;
       }
     }
   `;
@@ -401,18 +432,20 @@ export function useCoupon() {
         
         <div class="form-block">
           <div class="block-title">基本信息</div>
-          <ElForm label-width="80px" size="default">
-            <ElFormItem label="优惠券名称" required>
-              <ElInput v-model={formData.name} placeholder="请输入优惠券名称" clearable maxlength={50} showWordLimit />
-            </ElFormItem>
-            <ElFormItem label="关联活动">
-              <ElSelect v-model={formData.activityId} placeholder="请选择关联的营销活动(可选)" class="w-full" clearable>
-                <ElOption label="春节大促活动" value="1" />
-                <ElOption label="新品上市活动" value="2" />
-                <ElOption label="会员专享活动" value="3" />
-                <ElOption label="不关联活动" value="" />
-              </ElSelect>
-            </ElFormItem>
+          <ElForm label-width="90px" size="default">
+            <div class="form-grid-2">
+              <ElFormItem label="优惠券名称" required>
+                <ElInput v-model={formData.name} placeholder="请输入优惠券名称" clearable maxlength={50} showWordLimit />
+              </ElFormItem>
+              <ElFormItem label="关联活动">
+                <ElSelect v-model={formData.activityId} placeholder="请选择关联的营销活动(可选)" class="w-full" clearable filterable>
+                  <ElOption label="春节大促活动" value="1" />
+                  <ElOption label="新品上市活动" value="2" />
+                  <ElOption label="会员专享活动" value="3" />
+                  <ElOption label="不关联活动" value="" />
+                </ElSelect>
+              </ElFormItem>
+            </div>
           </ElForm>
         </div>
 
@@ -436,35 +469,41 @@ export function useCoupon() {
           </div>
           
           {formData.type && (
-            <ElForm label-width="80px" size="default">
-              <div class="form-grid-2">
+            <ElForm label-width="90px" size="default">
+              <div class="form-grid-3">
                 <ElFormItem label="优惠值" required>
-                  <ElInputNumber v-model={formData.value} min={0} precision={2} class="w-full" placeholder={formData.type === 2 ? "折扣值如8表示8折" : "优惠金额"} prefix={formData.type !== 2 ? "¥" : ""} />
+                  <ElInputNumber 
+                    v-model={formData.value} 
+                    min={0} 
+                    precision={2} 
+                    class="w-full!" 
+                    placeholder={formData.type === 2 ? "折扣值如8表示8折" : "优惠金额"} 
+                    prefix={formData.type !== 2 ? "¥" : ""} 
+                  />
                 </ElFormItem>
                 <ElFormItem label="发放总量" required>
-                  <ElInputNumber v-model={formData.totalCount} min={1} class="w-full" placeholder="发放总数量" />
+                  <ElInputNumber 
+                    v-model={formData.totalCount} 
+                    min={1} 
+                    class="w-full!" 
+                    placeholder="发放总数量" 
+                  />
+                </ElFormItem>
+                <ElFormItem label="有效期" required>
+                  <ElDatePicker
+                    v-model={formData.validPeriod}
+                    type="daterange"
+                    range-separator="至"
+                    start-placeholder="开始日期"
+                    end-placeholder="结束日期"
+                    class="w-full!"
+                  />
                 </ElFormItem>
               </div>
             </ElForm>
           )}
         </div>
 
-        <div class="form-block">
-          <div class="block-title">有效期</div>
-          <ElForm label-width="80px" size="default">
-            <ElFormItem label="有效期" required>
-              <ElDatePicker
-                v-model={formData.validPeriod}
-                type="daterange"
-                range-separator="至"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                class="w-full"
-              />
-            </ElFormItem>
-          </ElForm>
-        </div>
-
         <div class="form-block">
           <div class="block-title">适用范围</div>
           <div class="scope-grid">
@@ -489,7 +528,7 @@ export function useCoupon() {
                 </ElRadioGroup>
               </div>
               <div class="card-body">
-                {formData.deviceScope === 2 && <span style="color: #9ca3af; font-size: 13px;">选择设备</span>}
+                {formData.deviceScope === 2 && <DeviceSelector v-model={formData.deviceIds} />}
               </div>
             </div>
             <div class="scope-card">
@@ -547,6 +586,7 @@ export function useCoupon() {
       value: null,
       totalCount: null,
       shopIds: [],
+      deviceIds: [],
       productIds: [],
       validPeriod: [],
       description: "",

+ 27 - 0
haha-admin-web/src/views/order/utils/hook.tsx

@@ -71,6 +71,12 @@ export function useOrder(tableRef: Ref) {
       minWidth: 120,
       formatter: ({ deviceId }) => deviceId || "-"
     },
+    {
+      label: "门店名称",
+      prop: "shopName",
+      minWidth: 120,
+      formatter: ({ shopName }) => shopName || "-"
+    },
     {
       label: "订单金额",
       prop: "totalAmount",
@@ -92,6 +98,21 @@ export function useOrder(tableRef: Ref) {
       minWidth: 90,
       formatter: ({ discountAmount }) => discountAmount ? `¥${(discountAmount / 100).toFixed(2)}` : "¥0.00"
     },
+    {
+      label: "支付方式",
+      prop: "payType",
+      minWidth: 90,
+      cellRenderer: ({ row }) => {
+        const payTypeMap: Record<string, { text: string; type: string }> = {
+          "wechat": { text: "微信支付", type: "success" },
+          "alipay": { text: "支付宝", type: "primary" },
+          "cash": { text: "现金", type: "warning" },
+          "free": { text: "免费", type: "info" }
+        };
+        const payType = payTypeMap[row.payType] || { text: row.payType || "-", type: "info" };
+        return <el-tag type={payType.type as any} size="small">{payType.text}</el-tag>;
+      }
+    },
     {
       label: "支付状态",
       prop: "payStatus",
@@ -222,6 +243,7 @@ export function useOrder(tableRef: Ref) {
               <ElDescriptionsItem label="支付订单号">{order.outTradeNo || "-"}</ElDescriptionsItem>
               <ElDescriptionsItem label="用户 ID">{order.userId || "-"}</ElDescriptionsItem>
               <ElDescriptionsItem label="设备 ID">{order.deviceId || "-"}</ElDescriptionsItem>
+              <ElDescriptionsItem label="门店名称">{order.shopName || "-"}</ElDescriptionsItem>
               <ElDescriptionsItem label="活动 ID">{order.activityId || "-"}</ElDescriptionsItem>
               <ElDescriptionsItem label="订单金额">
                 ¥{((order.totalAmount || 0) / 100).toFixed(2)}
@@ -232,6 +254,11 @@ export function useOrder(tableRef: Ref) {
               <ElDescriptionsItem label="优惠金额">
                 ¥{((order.discountAmount || 0) / 100).toFixed(2)}
               </ElDescriptionsItem>
+              <ElDescriptionsItem label="支付方式">
+                <el-tag type={order.payType === "wechat" ? "success" : order.payType === "alipay" ? "primary" : "info"} size="small">
+                  {order.payType === "wechat" ? "微信支付" : order.payType === "alipay" ? "支付宝" : order.payType === "cash" ? "现金" : order.payType === "free" ? "免费" : order.payType || "-"}
+                </el-tag>
+              </ElDescriptionsItem>
               <ElDescriptionsItem label="支付状态">
                 <el-tag type={order.payStatus === "paid" ? "success" : order.payStatus === "refund" ? "danger" : "warning"}>
                   {order.payStatus === "paid" ? "已支付" : order.payStatus === "refund" ? "已退款" : "待支付"}

+ 3 - 0
haha-admin-web/src/views/order/utils/types.ts

@@ -6,10 +6,13 @@ export interface OrderItem {
   activityId?: string;
   userId: number;
   deviceId: string;
+  shopId?: number;
+  shopName?: string;
   totalAmount: number;
   payAmount?: number;
   discountAmount?: number;
   payStatus: string;
+  payType?: string;
   payTime?: string;
   status: number;
   refundStatus?: number;

+ 19 - 19
haha-admin-web/vite.config.ts

@@ -24,97 +24,97 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
       host: "0.0.0.0",
       proxy: {
         "/login": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/login/, "/login")
         },
         "/user": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/user/, "/user")
         },
         "/role": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/role/, "/role")
         },
         "/permission": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/permission/, "/permission")
         },
         "/users": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/users/, "/users")
         },
         "/shops": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/shops/, "/shops")
         },
         "/devices": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/devices/, "/devices")
         },
         "/order": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/order/, "/order")
         },
         "/products": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/products/, "/products")
         },
         "/inventory": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/inventory/, "/inventory")
         },
         "/marketing": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/marketing/, "/marketing")
         },
         "/checkin": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/checkin/, "/checkin")
         },
         "/announcement": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/announcement/, "/announcement")
         },
         "/operation-log": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/operation-log/, "/operation-log")
         },
         "/sync": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/sync/, "/sync")
         },
         "/new-product-apply": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/new-product-apply/, "/new-product-apply")
         },
         "/dashboard": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/dashboard/, "/dashboard")
         },
         "/dict": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/dict/, "/dict")
         },
         "/timed-discount": {
-          target: "http://localhost:8080/admin",
+          target: "http://localhost:7070/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/timed-discount/, "/timed-discount")
         }

+ 1 - 1
haha-admin/src/main/resources/application.yml

@@ -73,7 +73,7 @@ pagehelper:
 
 # 服务器配置
 server:
-  port: 8080
+  port: 7070
   servlet:
     context-path: /admin
 

+ 9 - 0
haha-entity/src/main/java/com/haha/entity/Order.java

@@ -32,6 +32,9 @@ public class Order implements Serializable {
 
     private String payStatus;
 
+    @TableField(exist = false)
+    private String payType;
+
     private String videoUrl;
 
     private BigDecimal confidence;
@@ -61,4 +64,10 @@ public class Order implements Serializable {
 
     @TableField(exist = false)
     private String storeName;
+
+    @TableField(exist = false)
+    private String shopName;
+
+    @TableField(exist = false)
+    private Long shopId;
 }

+ 2 - 2
haha-miniapp/src/main/resources/application.yml

@@ -68,7 +68,7 @@ pagehelper:
 
 # 服务器配置
 server:
-  port: 7070
+  port: 7077
   servlet:
     context-path: /api
 
@@ -80,7 +80,7 @@ wechat:
     mch-id: 1888888888
     mch-key: 88888888888888888888888888888888
     v3-api-key: 88888888888888888888888888888888
-    notify-url: http://localhost:7070/api/callback/wechat/pay
+    notify-url: http://localhost:7077/api/callback/wechat/pay
   # 小程序配置
   miniapp:
     app-id: your_wechat_miniapp_appid

+ 34 - 3
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -7,8 +7,12 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.common.constant.OrderConstants;
 import com.haha.common.exception.BusinessException;
 import com.haha.common.utils.EntityLabelUtils;
+import com.haha.entity.Device;
 import com.haha.entity.Order;
+import com.haha.entity.Shop;
+import com.haha.mapper.DeviceMapper;
 import com.haha.mapper.OrderMapper;
+import com.haha.mapper.ShopMapper;
 import com.haha.service.OrderService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -29,6 +33,9 @@ import java.util.Map;
 @RequiredArgsConstructor
 public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
 
+    private final DeviceMapper deviceMapper;
+    private final ShopMapper shopMapper;
+
     @Override
     public IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId, 
                                  String payStatus, Integer status, String startDate, String endDate) {
@@ -201,8 +208,32 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             order.setStatusLabel(statusLabel.getLabel());
         }
         
-        // TODO: 根据 userId 查询用户信息(昵称、手机号)
-        // TODO: 根据 deviceId 查询设备信息(设备名称、门店名称)
-        // 目前这些字段需要在前端通过其他方式获取,或者后续实现联表查询
+        // 推断支付方式(数据库暂无pay_type字段,根据outTradeNo推断)
+        if (order.getOutTradeNo() != null && !order.getOutTradeNo().isEmpty()) {
+            order.setPayType("wechat"); // 有支付订单号默认为微信支付
+        } else if ("paid".equals(order.getPayStatus())) {
+            order.setPayType("free"); // 已支付但无支付订单号,可能是免费订单
+        }
+        
+        // 根据 deviceId 查询设备信息,获取门店名称
+        if (order.getDeviceId() != null && !order.getDeviceId().isEmpty()) {
+            try {
+                // 通过 deviceId 查询设备
+                Device device = deviceMapper.selectByDeviceId(order.getDeviceId());
+                if (device != null) {
+                    order.setShopId(device.getShopId());
+                    // 通过 shopId 查询门店名称
+                    if (device.getShopId() != null) {
+                        Shop shop = shopMapper.selectById(device.getShopId());
+                        if (shop != null) {
+                            order.setShopName(shop.getName());
+                            order.setStoreName(shop.getName());
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("获取订单关联门店信息失败: orderId={}, deviceId={}", order.getId(), order.getDeviceId(), e);
+            }
+        }
     }
 }