Browse Source

fix: 营销活动bug调整

sgc 2 months ago
parent
commit
a2b33bbb1d

+ 58 - 10
src/views/operationManagement/activityDetail.vue

@@ -37,7 +37,7 @@
           </div>
 
           <!-- 评论有礼相关字段 -->
-          <template v-if="activityModel.activityType == 1">
+          <template v-if="activityModel.activityType == 2">
             <!-- 用户可参与次数 -->
             <div class="detail-item">
               <div class="detail-label">用户可参与次数</div>
@@ -69,13 +69,13 @@
           </template>
 
           <!-- 营销活动相关字段 -->
-          <template v-if="activityModel.activityType == 2">
+          <template v-if="activityModel.activityType == 1">
             <!-- 报名时间 -->
             <div class="detail-item">
               <div class="detail-label">报名时间</div>
               <div class="detail-value">
                 <span v-if="activityModel.signupStartTime && activityModel.signupEndTime">
-                  {{ activityModel.signupStartTime.split(' ')[0] }} - {{ activityModel.signupEndTime.split(' ')[0] }}
+                  {{ activityModel.signupStartTime.split(" ")[0] }} - {{ activityModel.signupEndTime.split(" ")[0] }}
                 </span>
                 <span v-else>--</span>
               </div>
@@ -90,16 +90,30 @@
             <!-- 活动详情 -->
             <div class="detail-item">
               <div class="detail-label">活动详情</div>
-              <div class="detail-value" style=" word-break: break-word;white-space: pre-wrap">
+              <div class="detail-value" style="word-break: break-word; white-space: pre-wrap">
                 {{ activityModel.activityDetails || "--" }}
               </div>
             </div>
           </template>
-          <!-- 状态 -->
+          <!-- 审核状态 -->
           <div class="detail-item">
-            <div class="detail-label">状态</div>
+            <div class="detail-label">审核状态</div>
             <div class="detail-value">
-              {{ getStatusLabel(activityModel.status) }}
+              {{ getAuditStatusLabel(activityModel.status) }}
+            </div>
+          </div>
+          <!-- 审核时间 -->
+          <div class="detail-item" v-if="activityModel.auditTime">
+            <div class="detail-label">审核时间</div>
+            <div class="detail-value">
+              {{ formatDateTime(activityModel.auditTime) }}
+            </div>
+          </div>
+          <!-- 审核拒绝原因 -->
+          <div class="detail-item" v-if="activityModel.approvalComments">
+            <div class="detail-label">审核拒绝原因</div>
+            <div class="detail-value" style="word-break: break-word; white-space: pre-wrap">
+              {{ activityModel.approvalComments }}
             </div>
           </div>
         </div>
@@ -123,7 +137,9 @@
                   >
                     <template #error>
                       <div class="image-slot">
-                        <el-icon><Picture /></el-icon>
+                        <el-icon>
+                          <Picture />
+                        </el-icon>
                       </div>
                     </template>
                   </el-image>
@@ -145,7 +161,9 @@
                   >
                     <template #error>
                       <div class="image-slot">
-                        <el-icon><Picture /></el-icon>
+                        <el-icon>
+                          <Picture />
+                        </el-icon>
                       </div>
                     </template>
                   </el-image>
@@ -213,7 +231,12 @@ const activityModel = ref<any>({
   // 活动详情图片
   activityDetailImgUrl: null,
   // 状态
-  status: 0
+  status: 0,
+  // 审核时间
+  approvalTime: "",
+  auditTime: "",
+  // 审核拒绝原因
+  approvalComments: ""
 });
 
 // ==================== 生命周期钩子 ====================
@@ -273,6 +296,17 @@ const formatDate = (date: string) => {
 };
 
 /**
+ * 格式化日期时间
+ * @param dateTime 日期时间字符串
+ * @returns 格式化后的日期时间字符串
+ */
+const formatDateTime = (dateTime: string) => {
+  if (!dateTime) return "--";
+  // 将 - 替换为 /,并保持时间部分
+  return dateTime.replace(/-/g, "/");
+};
+
+/**
  * 获取活动类型标签
  */
 const getActivityTypeLabel = (activityType: number) => {
@@ -300,6 +334,20 @@ const getStatusLabel = (status: number) => {
 };
 
 /**
+ * 获取审核状态标签(只显示审核相关的状态)
+ */
+const getAuditStatusLabel = (status: number) => {
+  const auditStatusMap: Record<number, string> = {
+    1: "待审核",
+    2: "待审核",
+    3: "审核驳回",
+    5: "审核通过",
+    8: "审核通过"
+  };
+  return auditStatusMap[status] || "--";
+};
+
+/**
  * 获取活动规则显示文本
  */
 const getRuleDisplayText = (ruleValue: any) => {

+ 89 - 52
src/views/operationManagement/activityList.vue

@@ -172,13 +172,13 @@
 <script setup lang="tsx" name="activityList">
 import { reactive, ref, onMounted, computed, nextTick } from "vue";
 import { useRouter } from "vue-router";
-import { ElMessage, ElMessageBox } from "element-plus";
+import { ElMessage, ElMessageBox, ElLoading } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 import type { UploadFile, UploadFiles, UploadProps } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { getActivityList, deleteActivity, updateActivityStatus, uploadActivityResult } from "@/api/modules/operationManagement";
-import { uploadImg } from "@/api/modules/upload";
+import { uploadContractImage } from "@/api/modules/licenseManagement";
 import { localGet, usePermission } from "@/utils";
 
 const router = useRouter();
@@ -290,10 +290,21 @@ const getStatusLabel = (status: number) => {
 // 表格列配置
 const columns = reactive<ColumnProps<any>[]>([
   {
+    prop: "activityName",
+    label: "活动名称",
+    search: {
+      el: "input",
+      props: { placeholder: "请输入" }
+    }
+  },
+  {
+    prop: "id",
+    label: "活动ID"
+  },
+  {
     prop: "activityType",
     label: "活动类型",
     width: 120,
-    isShow: false,
     enum: activityTypeEnum,
     fieldNames: { label: "label", value: "value" },
     search: {
@@ -302,44 +313,40 @@ const columns = reactive<ColumnProps<any>[]>([
       order: 1
     },
     render: (scope: any) => {
-      const type = scope.row.activityType;
-      const typeItem = activityTypeEnum.find(item => item.value === type);
-      return typeItem ? typeItem.label : "-";
+      return scope.row.activityType == 1 ? "营销活动" : "评论有礼";
     }
   },
   {
-    prop: "activityName",
-    label: "活动名称",
-    search: {
-      el: "input",
-      props: { placeholder: "请输入" }
-    }
-  },
-  {
-    prop: "id",
-    label: "活动ID"
-  },
-  {
     prop: "startTime",
-    label: "活动开始时间",
+    label: "活动开始时间/结束时间",
+    width: 300,
     render: (scope: any) => {
-      return scope.row.startTime?.replace(/-/g, "/") || "--";
+      const formatDate = (dateStr: string) => {
+        if (!dateStr) return "--";
+        // 只取日期部分(去掉时分秒)
+        const datePart = dateStr.split(" ")[0];
+        return datePart?.replace(/-/g, "/") || "--";
+      };
+      return `${formatDate(scope.row.startTime)} - ${formatDate(scope.row.endTime)}`;
     }
   },
   {
-    prop: "endTime",
-    label: "活动结束时间",
+    prop: "signupStartTime",
+    label: "报名开始时间/结束时间",
+    width: 300,
     render: (scope: any) => {
-      return scope.row.endTime?.replace(/-/g, "/") || "--";
+      const formatDate = (dateStr: string) => {
+        if (!dateStr) return "--";
+        // 只取日期部分(去掉时分秒)
+        const datePart = dateStr.split(" ")[0];
+        return datePart?.replace(/-/g, "/") || "--";
+      };
+      return `${formatDate(scope.row.signupStartTime)} - ${formatDate(scope.row.signupEndTime)}`;
     }
   },
   {
-    prop: "couponName",
-    label: "优惠券名称"
-  },
-  {
     prop: "status",
-    label: "活动状态",
+    label: "状态",
     render: (scope: any) => {
       // 优先使用返回的 statusName,如果没有则使用 getStatusLabel
       return scope.row.statusName || getStatusLabel(scope.row.status);
@@ -368,21 +375,34 @@ const dataCallback = (data: any) => {
 };
 
 // 获取表格列表
-const getTableList = (params: any) => {
-  // 处理参数:确保 status 是 number 类型,pageNum 和 pageSize 转换为 string
-  const newParams: any = {
-    ...params,
-    pageNum: params.pageNum ? String(params.pageNum) : undefined,
-    pageSize: params.pageSize ? String(params.pageSize) : undefined,
-    status: params.status !== undefined && params.status !== null && params.status !== "" ? Number(params.status) : undefined,
-    storeId: params.storeId ? String(params.storeId) : undefined,
-    // 处理 activityType:如果存在且不为空,转换为 number 类型
-    activityType:
-      params.activityType !== undefined && params.activityType !== null && params.activityType !== ""
-        ? Number(params.activityType)
-        : undefined
-  };
-  return getActivityList(newParams);
+const getTableList = async (params: any) => {
+  // 显示全屏loading
+  const loadingInstance = ElLoading.service({
+    lock: true,
+    text: "加载中...",
+    background: "rgba(0, 0, 0, 0.7)"
+  });
+
+  try {
+    // 处理参数:确保 status 是 number 类型,pageNum 和 pageSize 转换为 string
+    const newParams: any = {
+      ...params,
+      pageNum: params.pageNum ? String(params.pageNum) : undefined,
+      pageSize: params.pageSize ? String(params.pageSize) : undefined,
+      status: params.status !== undefined && params.status !== null && params.status !== "" ? Number(params.status) : undefined,
+      storeId: params.storeId ? String(params.storeId) : undefined,
+      // 处理 activityType:如果存在且不为空,转换为 number 类型
+      activityType:
+        params.activityType !== undefined && params.activityType !== null && params.activityType !== ""
+          ? Number(params.activityType)
+          : undefined
+    };
+    const result = await getActivityList(newParams);
+    return result;
+  } finally {
+    // 关闭loading
+    loadingInstance.close();
+  }
 };
 
 // 新建活动
@@ -496,23 +516,40 @@ const handleResultImageChange: UploadProps["onChange"] = async (uploadFile: Uplo
       return;
     }
 
+    // 确保文件在列表中
+    const existingIndex = resultImageFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+    if (existingIndex === -1) {
+      resultImageFileList.value.push(uploadFile);
+    }
+
     // 自动上传图片
     try {
       uploadFile.status = "uploading";
+      uploadFile.percentage = 0;
       const formData = new FormData();
       formData.append("file", uploadFile.raw);
-      const res: any = await uploadImg(formData);
-      const imageUrl = (res?.data && Array.isArray(res.data) ? res.data[0] : null) || res?.data?.fileUrl || res?.fileUrl;
-      if (imageUrl) {
-        uploadFile.status = "success";
-        uploadFile.url = imageUrl;
-        uploadResultForm.value.resultMediaUrl = imageUrl;
+      formData.append("user", "text");
+      const res: any = await uploadContractImage(formData);
+      if (res?.code === 200 && res.data) {
+        const imageUrl = res.data[0];
+        if (imageUrl) {
+          uploadFile.status = "success";
+          uploadFile.percentage = 100;
+          uploadFile.url = imageUrl;
+          uploadFile.response = { url: imageUrl };
+          uploadResultForm.value.resultMediaUrl = imageUrl;
+          // 触发视图更新
+          resultImageFileList.value = [...resultImageFileList.value];
+        } else {
+          throw new Error("上传失败:未获取到图片URL");
+        }
       } else {
-        throw new Error("上传失败:未获取到图片URL");
+        throw new Error(res?.msg || "图片上传失败");
       }
-    } catch (error) {
+    } catch (error: any) {
       uploadFile.status = "fail";
-      ElMessage.error("图片上传失败,请重试");
+      uploadFile.percentage = 0;
+      ElMessage.error(error?.message || "图片上传失败,请重试");
       const index = resultImageFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
       if (index > -1) {
         resultImageFileList.value.splice(index, 1);

+ 110 - 36
src/views/operationManagement/caseDetail.vue

@@ -31,22 +31,42 @@
               <div class="detail-label">更新时间 : {{ formatTime(detail.achievementList[0].updatedTime) }}</div>
             </div>
             <div class="detail-item">
-              <div class="detail-label">成果描述 : {{ detail.achievementList[0].achievementDesc || '-' }}</div>
+              <div class="detail-label">成果描述 : {{ detail.achievementList[0].achievementDesc || "-" }}</div>
             </div>
-            <div class="detail-item" v-if="detail.achievementList[0].mediaUrlList">
+            <div class="detail-item" v-if="mediaList.length > 0">
               <div class="detail-label">图片与视频 :</div>
               <div class="media-grid">
-                <template v-for="(item, index) in detail.achievementList[0].mediaUrlList" :key="index">
+                <template v-for="(item, index) in mediaList" :key="index">
                   <div v-if="item.type === 'video'" class="media-item video-item" @click="playVideo(item.url)">
-                    <div class="media-placeholder">
+                    <el-image v-if="item.coverUrl" :src="item.coverUrl" fit="cover" class="media-image">
+                      <template #error>
+                        <div class="media-placeholder">
+                          <el-icon class="play-icon">
+                            <VideoPlay />
+                          </el-icon>
+                        </div>
+                      </template>
+                    </el-image>
+                    <div v-else class="media-placeholder">
                       <el-icon class="play-icon">
                         <VideoPlay />
                       </el-icon>
                     </div>
+                    <!-- 视频播放图标覆盖层 -->
+                    <div class="video-overlay">
+                      <el-icon class="play-icon-overlay">
+                        <VideoPlay />
+                      </el-icon>
+                    </div>
                   </div>
-                  <div v-else class="media-item image-item" @click="previewImage(item, index)">
-                    <el-image :src="item" fit="cover" class="media-image"
-                      :preview-src-list="detail.achievementList[0].mediaUrlList" :initial-index="getImageIndex(item)">
+                  <div v-else class="media-item image-item" @click="previewImage(item.url, index)">
+                    <el-image
+                      :src="item.url"
+                      fit="cover"
+                      class="media-image"
+                      :preview-src-list="imageList"
+                      :initial-index="getImageIndex(item.url)"
+                    >
                       <template #error>
                         <div class="image-slot">
                           <el-icon>
@@ -110,24 +130,77 @@ const formatTime = (time: string | null | undefined) => {
   }
 };
 
-type MediaItem = { type: "image" | "video"; url: string };
+type MediaItem = { type: "image" | "video"; url: string; coverUrl?: string };
 
 const mediaList = computed<MediaItem[]>(() => {
   if (!detail.value) return [];
   const list: MediaItem[] = [];
 
+  // 优先处理 achievementList[0].mediaUrlList
+  if (detail.value.achievementList && detail.value.achievementList[0]?.mediaUrlList) {
+    const mediaUrlList = detail.value.achievementList[0].mediaUrlList;
+    const arr = Array.isArray(mediaUrlList) ? mediaUrlList : [mediaUrlList];
+    for (const it of arr) {
+      if (typeof it === "string") {
+        // 检查是否包含 | 分隔符(视频格式:xxx.mp4 | XXX.jpg)
+        if (it.includes("|")) {
+          const parts = it
+            .split("|")
+            .map(s => s.trim())
+            .filter(s => s);
+          if (parts.length >= 2) {
+            // 第一部分是视频URL,第二部分是封面URL
+            list.push({ type: "video", url: parts[0], coverUrl: parts[1] });
+          } else if (parts.length === 1) {
+            // 只有视频URL,没有封面
+            list.push({ type: "video", url: parts[0] });
+          }
+        } else {
+          // 如果是字符串,根据文件扩展名判断类型
+          const isVideo = /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it);
+          list.push({ type: isVideo ? "video" : "image", url: it });
+        }
+      } else if (it?.url) {
+        // 如果是对象,使用 type 字段或根据 url 判断
+        const isVideo = it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.url);
+        list.push({ type: isVideo ? "video" : "image", url: it.url, coverUrl: it.coverUrl });
+      } else if (it?.mediaUrl) {
+        // 处理 mediaUrl 字段
+        const isVideo = it.mediaType === "video" || it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.mediaUrl);
+        list.push({ type: isVideo ? "video" : "image", url: it.mediaUrl, coverUrl: it.coverUrl });
+      }
+    }
+  }
+
   // 处理 mediaList 字段(可能是数组或对象数组)
   const raw = detail.value.mediaList ?? detail.value.images ?? detail.value.videos ?? [];
   const arr = Array.isArray(raw) ? raw : [raw];
   for (const it of arr) {
     if (typeof it === "string") {
-      list.push({ type: "image", url: it });
+      // 检查是否包含 | 分隔符(视频格式:xxx.mp4 | XXX.jpg)
+      if (it.includes("|")) {
+        const parts = it
+          .split("|")
+          .map(s => s.trim())
+          .filter(s => s);
+        if (parts.length >= 2) {
+          // 第一部分是视频URL,第二部分是封面URL
+          list.push({ type: "video", url: parts[0], coverUrl: parts[1] });
+        } else if (parts.length === 1) {
+          // 只有视频URL,没有封面
+          list.push({ type: "video", url: parts[0] });
+        }
+      } else {
+        const isVideo = /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it);
+        list.push({ type: isVideo ? "video" : "image", url: it });
+      }
     } else if (it?.url) {
-      list.push({ type: it.type === "video" ? "video" : "image", url: it.url });
+      const isVideo = it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.url);
+      list.push({ type: isVideo ? "video" : "image", url: it.url, coverUrl: it.coverUrl });
     } else if (it?.mediaUrl) {
       // 处理 mediaUrl 字段
-      const mediaType = it.mediaType === "video" || it.type === "video" ? "video" : "image";
-      list.push({ type: mediaType, url: it.mediaUrl });
+      const isVideo = it.mediaType === "video" || it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.mediaUrl);
+      list.push({ type: isVideo ? "video" : "image", url: it.mediaUrl, coverUrl: it.coverUrl });
     }
   }
 
@@ -178,97 +251,86 @@ onMounted(async () => {
   padding: 16px;
   background: #ffffff;
 }
-
 .header {
   display: flex;
   gap: 16px;
   align-items: center;
   margin-bottom: 16px;
 }
-
 .title {
   margin: 0;
   font-size: 18px;
   font-weight: 600;
 }
-
 .detail-list {
   display: flex;
   flex-direction: column;
   gap: 16px;
 }
-
 .detail-item {
   display: flex;
   flex-direction: column;
   gap: 8px;
 }
-
 .detail-label {
   font-size: 14px;
-  color: #606266;
   font-weight: 500;
+  color: #606266;
 }
-
 .detail-value {
   font-size: 14px;
   color: #303133;
   word-break: break-all;
 }
-
 .result-section {
   margin-top: 24px;
 }
-
 .result-title {
   margin-bottom: 12px;
   font-size: 16px;
   font-weight: 600;
 }
-
 .result-desc {
   font-size: 14px;
-  color: #303133;
   line-height: 1.6;
+  color: #303133;
   word-break: break-all;
   white-space: pre-wrap;
 }
-
 .media-grid {
   display: grid;
-  grid-template-columns: repeat(3, 1fr);
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
   gap: 12px;
   margin-top: 8px;
 }
-
 .media-item {
   position: relative;
-  aspect-ratio: 1;
+  width: 200px;
+  height: 200px;
   overflow: hidden;
   cursor: pointer;
   background: #f5f7fa;
   border-radius: 8px;
 }
-
 .image-item {
-  width: 100%;
-  height: 100%;
+  width: 200px;
+  height: 200px;
 }
-
 .media-image {
   display: block;
   width: 100%;
   height: 100%;
   object-fit: cover;
 }
-
 .video-item {
+  position: relative;
   display: flex;
   align-items: center;
   justify-content: center;
+  width: 200px;
+  height: 200px;
   background: #000000;
 }
-
 .media-placeholder {
   display: flex;
   align-items: center;
@@ -277,11 +339,24 @@ onMounted(async () => {
   height: 100%;
   color: #909399;
 }
-
 .play-icon {
   font-size: 40px;
 }
-
+.video-overlay {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  pointer-events: none;
+  background: rgb(0 0 0 / 30%);
+  border-radius: 8px;
+}
+.play-icon-overlay {
+  font-size: 48px;
+  color: #ffffff;
+  opacity: 0.9;
+}
 .image-slot {
   display: flex;
   align-items: center;
@@ -291,7 +366,6 @@ onMounted(async () => {
   color: #909399;
   background: #f5f7fa;
 }
-
 .dialog-video {
   display: block;
   width: 100%;

+ 29 - 16
src/views/operationManagement/cases.vue

@@ -1,7 +1,6 @@
 <template>
   <div class="table-box button-table">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam"
-      :data-callback="dataCallback">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
       <template #operation="scope">
         <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
       </template>
@@ -12,6 +11,7 @@
 <script setup lang="tsx" name="cases">
 import { reactive, ref } from "vue";
 import { useRouter } from "vue-router";
+import { ElLoading } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { getPersonCaseList } from "@/api/modules/operationManagement";
@@ -64,23 +64,36 @@ const dataCallback = (data: any) => ({
   total: data?.total ?? 0
 });
 
-const getTableList = (params: any) => {
-  // 转换参数格式以匹配新接口
-  const newParams: any = {
-    storeId: params.storeId || localGet("createdId"),
-    pageNum: params.page || params.pageNum || 1,
-    pageSize: params.size || params.pageSize || 10
-  };
+const getTableList = async (params: any) => {
+  // 显示全屏loading
+  const loadingInstance = ElLoading.service({
+    lock: true,
+    text: "加载中...",
+    background: "rgba(0, 0, 0, 0.7)"
+  });
 
-  // 上传情况筛选(未上传0 / 已上传1)
-  newParams.hasResult = params.hasResult;
+  try {
+    // 转换参数格式以匹配新接口
+    const newParams: any = {
+      storeId: params.storeId || localGet("createdId"),
+      pageNum: params.page || params.pageNum || 1,
+      pageSize: params.size || params.pageSize || 10
+    };
 
-  // 所属活动(活动名称)筛选
-  if (params.activityName) {
-    newParams.activityName = params.activityName.trim();
-  }
+    // 上传情况筛选(未上传0 / 已上传1)
+    newParams.hasResult = params.hasResult;
+
+    // 所属活动(活动名称)筛选
+    if (params.activityName) {
+      newParams.activityName = params.activityName.trim();
+    }
 
-  return getPersonCaseList(newParams);
+    const result = await getPersonCaseList(newParams);
+    return result;
+  } finally {
+    // 关闭loading
+    loadingInstance.close();
+  }
 };
 
 const toDetail = (row: any) => {

+ 1 - 1
src/views/operationManagement/couponDetail.vue

@@ -43,7 +43,7 @@
           <div class="detail-item">
             <div class="detail-label">有效期</div>
             <div class="detail-value">
-              {{ couponModel.specifiedDay ? `购买后${couponModel.specifiedDay}天` : "--" }}
+              {{ couponModel.specifiedDay ? `${couponModel.specifiedDay}天` : "--" }}
             </div>
           </div>
           <!-- 库存 -->

+ 165 - 93
src/views/operationManagement/newActivity.vue

@@ -38,7 +38,7 @@
             </el-form-item>
 
             <!-- 评论有礼相关字段 -->
-            <template v-if="activityModel.activityType === 1">
+            <template v-if="activityModel.activityType === 2">
               <!-- 用户可参与次数 -->
               <el-form-item label="用户可参与次数" prop="participationLimit">
                 <el-input v-model="activityModel.participationLimit" placeholder="请输入" maxlength="4" />
@@ -71,7 +71,7 @@
             </template>
 
             <!-- 营销活动相关字段 -->
-            <template v-if="activityModel.activityType === 2">
+            <template v-if="activityModel.activityType === 1">
               <!-- 报名时间 -->
               <el-form-item class="activity-time-item" label="报名时间" prop="signupTimeRange">
                 <el-date-picker
@@ -89,7 +89,12 @@
 
               <!-- 活动限制人数 -->
               <el-form-item label="活动限制人数">
-                <el-input v-model="activityModel.activityLimitPeople" placeholder="请输入" maxlength="6" />
+                <el-input
+                  v-model="activityModel.activityLimitPeople"
+                  placeholder="请输入"
+                  maxlength="20"
+                  @input="handlePositiveIntegerInput('activityLimitPeople', $event)"
+                />
               </el-form-item>
 
               <!-- 活动详情 -->
@@ -152,30 +157,30 @@
                     </template>
                   </el-upload>
                 </div>
-                <div class="upload-hint">请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
+                <div class="upload-hint">请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过5M</div>
               </div>
             </el-form-item>
 
             <!-- 活动详情图 -->
             <el-form-item v-if="activityModel.uploadImgType === 1" label="活动详情图" prop="activityDetailImage">
               <div class="upload-item-wrapper">
-                <div class="upload-area upload-area-vertical" :class="{ 'upload-full': detailFileList.length >= 1 }">
+                <div class="upload-area upload-area-vertical" :class="{ 'upload-full': detailFileList.length >= 9 }">
                   <el-upload
                     v-model:file-list="detailFileList"
                     :accept="'.jpg,.jpeg,.png'"
                     :auto-upload="false"
                     :before-remove="handleBeforeRemove"
                     :disabled="hasUnuploadedImages"
-                    :limit="1"
+                    :limit="9"
                     :on-change="handleDetailUploadChange"
-                    :on-exceed="handleUploadExceed"
+                    :on-exceed="handleDetailUploadExceed"
                     :on-preview="handlePictureCardPreview"
                     :on-remove="handleDetailRemove"
                     :show-file-list="true"
                     list-type="picture-card"
                   >
                     <template #trigger>
-                      <div v-if="detailFileList.length < 1" class="upload-trigger-card el-upload--picture-card">
+                      <div v-if="detailFileList.length < 9" class="upload-trigger-card el-upload--picture-card">
                         <el-icon>
                           <Plus />
                         </el-icon>
@@ -183,7 +188,7 @@
                     </template>
                   </el-upload>
                 </div>
-                <div class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
+                <div class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,最多上传9张,单张图片不得超过5M</div>
               </div>
             </el-form-item>
           </div>
@@ -316,7 +321,7 @@ const rules = reactive({
     { required: true, message: "请输入用户可参与次数", trigger: "blur" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value) {
             callback(new Error("请输入用户可参与次数"));
             return;
@@ -340,7 +345,7 @@ const rules = reactive({
     { required: true, message: "请选择活动规则", trigger: "change" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value || !Array.isArray(value) || value.length < 2) {
             callback(new Error("请选择完整的活动规则(至少选择角色和行为)"));
             return;
@@ -355,7 +360,7 @@ const rules = reactive({
     { required: true, message: "请选择优惠券", trigger: "change" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value) {
             callback(new Error("请选择优惠券"));
             return;
@@ -370,7 +375,7 @@ const rules = reactive({
     { required: true, message: "请输入优惠券发放数量", trigger: "blur" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value) {
             callback(new Error("请输入优惠券发放数量"));
             return;
@@ -394,7 +399,7 @@ const rules = reactive({
     { required: true, message: "请选择报名时间", trigger: "change" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
+        if (activityModel.value.activityType === 1) {
           if (!value || !Array.isArray(value) || value.length !== 2) {
             callback(new Error("请选择报名时间"));
             return;
@@ -420,7 +425,7 @@ const rules = reactive({
     { required: true, message: "请输入活动详情", trigger: ["blur", "change"] },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
+        if (activityModel.value.activityType === 1) {
           if (!value || value.trim() === "") {
             callback(new Error("请输入活动详情"));
             return;
@@ -467,7 +472,9 @@ const rules = reactive({
       required: true,
       validator: (rule: any, value: any, callback: any) => {
         if (activityModel.value.uploadImgType === 1) {
-          if (!detailImageUrl.value) {
+          // 检查是否有成功上传的图片
+          const successFiles = detailFileList.value.filter((f: any) => f.status === "success" && f.url);
+          if (successFiles.length === 0) {
             callback(new Error("请上传活动详情图"));
             return;
           }
@@ -481,8 +488,8 @@ const rules = reactive({
 
 // ==================== 活动信息数据模型 ====================
 const activityModel = ref<any>({
-  // 活动类型:1-评论有礼,2-营销活动
-  activityType: 2,
+  // 活动类型:1-营销活动,2-评论有礼
+  activityType: 1,
   // 活动宣传图(包含标题和详情)
   promotionImages: null,
   // 活动标题图片
@@ -613,15 +620,17 @@ const handleDetailRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) =>
   const file = uploadFile as any;
   const imageUrl = file.url;
 
-  if (imageUrl) {
-    detailImageUrl.value = "";
-    activityModel.value.activityDetailImg = null;
-    activityModel.value.activityDetailImage = null;
-    // 触发表单验证
-    nextTick(() => {
-      ruleFormRef.value?.validateField("activityDetailImage");
-    });
-  }
+  // 更新图片URL列表(移除已删除的图片)
+  const successFiles = uploadFiles.filter((f: any) => f.status === "success" && f.url);
+  const imageUrls = successFiles.map((f: any) => f.url);
+  detailImageUrl.value = imageUrls.length > 0 ? imageUrls.join(",") : "";
+  activityModel.value.activityDetailImg = imageUrls.length > 0 ? { url: imageUrls.join(",") } : null;
+  activityModel.value.activityDetailImage = imageUrls.length > 0 ? imageUrls.join(",") : "";
+
+  // 触发表单验证
+  nextTick(() => {
+    ruleFormRef.value?.validateField("activityDetailImage");
+  });
 
   if (file.url && file.url.startsWith("blob:")) {
     URL.revokeObjectURL(file.url);
@@ -631,13 +640,20 @@ const handleDetailRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) =>
 };
 
 /**
- * 上传文件超出限制提示
+ * 上传文件超出限制提示(标题图)
  */
 const handleUploadExceed: UploadProps["onExceed"] = () => {
   ElMessage.warning("最多只能上传1张图片");
 };
 
 /**
+ * 活动详情图上传超出限制提示
+ */
+const handleDetailUploadExceed: UploadProps["onExceed"] = () => {
+  ElMessage.warning("最多只能上传9张图片");
+};
+
+/**
  * 活动标题图片上传 - 文件变更
  */
 const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
@@ -668,8 +684,8 @@ const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uplo
       return;
     }
 
-    // 检查文件大小,不得超过20M
-    const maxSize = 20 * 1024 * 1024; // 20MB
+    // 检查文件大小,不得超过5M
+    const maxSize = 5 * 1024 * 1024; // 5MB
     if (uploadFile.raw.size > maxSize) {
       // 从文件列表中移除超过大小的文件
       const index = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
@@ -685,7 +701,7 @@ const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uplo
       if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
         URL.revokeObjectURL(uploadFile.url);
       }
-      ElMessage.warning("上传图片不得超过20M");
+      ElMessage.warning("上传图片不得超过5M");
       return;
     }
   }
@@ -737,8 +753,8 @@ const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, upl
       return;
     }
 
-    // 检查文件大小,不得超过20M
-    const maxSize = 20 * 1024 * 1024; // 20MB
+    // 检查文件大小,不得超过5M
+    const maxSize = 5 * 1024 * 1024; // 5MB
     if (uploadFile.raw.size > maxSize) {
       // 从文件列表中移除超过大小的文件
       const index = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
@@ -754,7 +770,7 @@ const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, upl
       if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
         URL.revokeObjectURL(uploadFile.url);
       }
-      ElMessage.warning("上传图片不得超过20M");
+      ElMessage.warning("上传图片不得超过5M");
       return;
     }
   }
@@ -826,9 +842,13 @@ const uploadSingleFile = async (file: UploadFile, uploadType: string) => {
           ruleFormRef.value?.validateField("activityTitleImage");
         });
       } else if (uploadType === "detail") {
-        detailImageUrl.value = imageUrl;
-        activityModel.value.activityDetailImg = { url: imageUrl };
-        activityModel.value.activityDetailImage = imageUrl;
+        // 支持多张图片,将所有成功上传的图片URL组合
+        const successFiles = detailFileList.value.filter((f: any) => f.status === "success" && f.url);
+        const imageUrls = successFiles.map((f: any) => f.url);
+        // 如果有多张图片,用逗号连接;如果只有一张,保持字符串格式
+        detailImageUrl.value = imageUrls.length > 0 ? imageUrls.join(",") : "";
+        activityModel.value.activityDetailImg = imageUrls.length > 0 ? { url: imageUrls.join(",") } : null;
+        activityModel.value.activityDetailImage = imageUrls.length > 0 ? imageUrls.join(",") : "";
         // 触发表单验证
         nextTick(() => {
           ruleFormRef.value?.validateField("activityDetailImage");
@@ -914,13 +934,6 @@ watch(
 
     nextTick(() => {
       if (newVal === 1) {
-        // 切换到评论有礼:清除营销活动相关字段
-        activityModel.value.signupTimeRange = [];
-        activityModel.value.activityLimitPeople = "";
-        activityModel.value.activityDetails = "";
-        // 清除营销活动字段的验证状态
-        ruleFormRef.value?.clearValidate(["signupTimeRange", "activityLimitPeople", "activityDetails"]);
-      } else if (newVal === 2) {
         // 切换到营销活动:清除评论有礼相关字段
         activityModel.value.participationLimit = "";
         activityModel.value.activityRule = [];
@@ -928,6 +941,13 @@ watch(
         activityModel.value.couponQuantity = "";
         // 清除评论有礼字段的验证状态
         ruleFormRef.value?.clearValidate(["participationLimit", "activityRule", "couponId", "couponQuantity"]);
+      } else if (newVal === 2) {
+        // 切换到评论有礼:清除营销活动相关字段
+        activityModel.value.signupTimeRange = [];
+        activityModel.value.activityLimitPeople = "";
+        activityModel.value.activityDetails = "";
+        // 清除营销活动字段的验证状态
+        ruleFormRef.value?.clearValidate(["signupTimeRange", "activityLimitPeople", "activityDetails"]);
       }
     });
   },
@@ -1013,32 +1033,63 @@ onMounted(async () => {
     try {
       const res: any = await getActivityDetail({ id: id.value });
       if (res && res.code == 200) {
-        activityModel.value = { ...activityModel.value, ...res.data };
+        // 先设置 activityType 为数字类型,确保下拉框正确显示
+        if (res.data.activityType !== undefined) {
+          activityModel.value.activityType = Number(res.data.activityType);
+        }
+
+        // 合并其他数据
+        activityModel.value.activityName = res.data.activityName || "";
+
         // 处理活动时间范围
         if (res.data.startTime && res.data.endTime) {
-          activityModel.value.activityTimeRange = [res.data.startTime, res.data.endTime];
+          // 只取日期部分(去掉时分秒)
+          const startDate = res.data.startTime.split(" ")[0];
+          const endDate = res.data.endTime.split(" ")[0];
+          activityModel.value.activityTimeRange = [startDate, endDate];
         }
-        // 加载活动规则(评论有礼)
-        if (res.data.activityRule) {
-          activityModel.value.activityRule = res.data.activityRule.split(",");
-        } else {
-          activityModel.value.activityRule = [];
-        }
-        // 加载报名开始时间(营销活动)
-        if (res.data.signupStartTime) {
-          activityModel.value.signupStartTime = res.data.signupStartTime;
-        }
-        // 加载报名结束时间(营销活动)
-        if (res.data.signupEndTime) {
-          activityModel.value.signupEndTime = res.data.signupEndTime;
-        }
-        // 加载活动限制人数(营销活动)
-        if (res.data.activityLimitPeople !== undefined) {
-          activityModel.value.activityLimitPeople = res.data.activityLimitPeople;
-        }
-        // 加载活动详情(营销活动)
-        if (res.data.activityDetails) {
-          activityModel.value.activityDetails = res.data.activityDetails;
+
+        // 根据活动类型加载对应字段
+        if (activityModel.value.activityType === 1) {
+          // 营销活动:加载报名时间、活动限制人数、活动详情
+          if (res.data.signupStartTime && res.data.signupEndTime) {
+            // 只取日期部分(去掉时分秒)
+            const signupStartDate = res.data.signupStartTime.split(" ")[0];
+            const signupEndDate = res.data.signupEndTime.split(" ")[0];
+            activityModel.value.signupTimeRange = [signupStartDate, signupEndDate];
+          } else {
+            activityModel.value.signupTimeRange = [];
+          }
+          // 加载活动限制人数
+          if (res.data.activityLimitPeople !== undefined && res.data.activityLimitPeople !== null) {
+            activityModel.value.activityLimitPeople = String(res.data.activityLimitPeople);
+          } else {
+            activityModel.value.activityLimitPeople = "";
+          }
+          // 加载活动详情
+          activityModel.value.activityDetails = res.data.activityDetails || "";
+        } else if (activityModel.value.activityType === 2) {
+          // 评论有礼:加载活动规则、优惠券、优惠券发放数量、用户可参与次数
+          // 加载活动规则
+          if (res.data.activityRule) {
+            activityModel.value.activityRule = res.data.activityRule.split(",");
+          } else {
+            activityModel.value.activityRule = [];
+          }
+          // 加载优惠券ID
+          activityModel.value.couponId = res.data.couponId || "";
+          // 加载优惠券发放数量
+          if (res.data.couponQuantity !== undefined && res.data.couponQuantity !== null) {
+            activityModel.value.couponQuantity = String(res.data.couponQuantity);
+          } else {
+            activityModel.value.couponQuantity = "";
+          }
+          // 加载用户可参与次数
+          if (res.data.participationLimit !== undefined && res.data.participationLimit !== null) {
+            activityModel.value.participationLimit = String(res.data.participationLimit);
+          } else {
+            activityModel.value.participationLimit = "";
+          }
         }
         // 加载上传图片方式
         if (res.data.uploadImgType !== undefined) {
@@ -1065,15 +1116,22 @@ onMounted(async () => {
               titleFileList.value = [titleFile];
             }
           }
-          // 如果有详情图片,添加到文件列表
+          // 如果有详情图片,添加到文件列表(支持多张图片,可能是字符串或数组)
           if (res.data.activityDetailImgUrl) {
-            const detailImgUrl = res.data.activityDetailImgUrl;
-            if (detailImgUrl) {
-              detailImageUrl.value = detailImgUrl;
+            let detailImgUrls: string[] = [];
+            // 如果是字符串,可能是逗号分隔的多张图片
+            if (typeof res.data.activityDetailImgUrl === "string") {
+              detailImgUrls = res.data.activityDetailImgUrl.split(",").filter((url: string) => url.trim());
+            } else if (Array.isArray(res.data.activityDetailImgUrl)) {
+              detailImgUrls = res.data.activityDetailImgUrl.filter((url: any) => url);
+            }
+
+            if (detailImgUrls.length > 0) {
+              detailImageUrl.value = detailImgUrls.join(",");
               activityModel.value.activityDetailImg = res.data.activityDetailImgUrl;
-              activityModel.value.activityDetailImage = detailImgUrl;
-              const detailFile = handleImageParam(detailImgUrl);
-              detailFileList.value = [detailFile];
+              activityModel.value.activityDetailImage = detailImgUrls.join(",");
+              // 将多张图片添加到文件列表
+              detailFileList.value = detailImgUrls.map((url: string) => handleImageParam(url));
             }
           }
         }
@@ -1090,6 +1148,24 @@ onMounted(async () => {
 // ==================== 事件处理函数 ====================
 
 /**
+ * 处理正整数输入(只允许输入正整数)
+ */
+const handlePositiveIntegerInput = (field: string, value: string) => {
+  // 移除所有非数字字符
+  let filteredValue = value.replace(/[^\d]/g, "");
+
+  // 限制最大长度为20
+  if (filteredValue.length > 20) {
+    filteredValue = filteredValue.substring(0, 20);
+  }
+
+  // 更新对应字段的值
+  if (field === "activityLimitPeople") {
+    activityModel.value.activityLimitPeople = filteredValue;
+  }
+};
+
+/**
  * 返回上一页
  */
 const goBack = () => {
@@ -1129,17 +1205,6 @@ const handleSubmit = async () => {
 
       // 根据活动类型添加不同的字段,确保只提交对应类型的字段
       if (activityModel.value.activityType === 1) {
-        // 评论有礼:只添加评论有礼相关字段
-        params.participationLimit = activityModel.value.participationLimit;
-        params.activityRule = activityModel.value.activityRule.join(",");
-        params.couponId = activityModel.value.couponId;
-        params.couponQuantity = activityModel.value.couponQuantity;
-        // 确保不包含营销活动的字段
-        delete params.signupStartTime;
-        delete params.signupEndTime;
-        delete params.activityLimitPeople;
-        delete params.activityDetails;
-      } else if (activityModel.value.activityType === 2) {
         // 营销活动:只添加营销活动相关字段
         const [signupStartTime, signupEndTime] = activityModel.value.signupTimeRange || [];
         params.signupStartTime = signupStartTime;
@@ -1151,6 +1216,17 @@ const handleSubmit = async () => {
         delete params.activityRule;
         delete params.couponId;
         delete params.couponQuantity;
+      } else if (activityModel.value.activityType === 2) {
+        // 评论有礼:只添加评论有礼相关字段
+        params.participationLimit = activityModel.value.participationLimit;
+        params.activityRule = activityModel.value.activityRule.join(",");
+        params.couponId = activityModel.value.couponId;
+        params.couponQuantity = activityModel.value.couponQuantity;
+        // 确保不包含营销活动的字段
+        delete params.signupStartTime;
+        delete params.signupEndTime;
+        delete params.activityLimitPeople;
+        delete params.activityDetails;
       }
 
       // 根据上传图片方式设置不同的参数
@@ -1330,18 +1406,14 @@ const handleSubmit = async () => {
 
   // 竖版图片样式
   &.upload-area-vertical {
-    max-width: 300px;
     :deep(.el-upload--picture-card) {
-      width: 100%;
-      height: 400px;
-      aspect-ratio: 3 / 4;
+      width: 150px;
+      height: 150px;
     }
     :deep(.el-upload-list--picture-card) {
-      width: 100%;
       .el-upload-list__item {
-        width: 100%;
-        height: 400px;
-        aspect-ratio: 3 / 4;
+        width: 150px;
+        height: 150px;
         margin: 0;
       }
     }

+ 39 - 27
src/views/operationManagement/personnel.vue

@@ -1,7 +1,6 @@
 <template>
   <div class="table-box button-table">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam"
-      :data-callback="dataCallback">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
       <template #operation="scope">
         <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
         <!-- 待审核状态显示通过和拒绝按钮 -->
@@ -17,7 +16,7 @@
 <script setup lang="tsx" name="personnel">
 import { reactive, ref } from "vue";
 import { useRouter } from "vue-router";
-import { ElMessage, ElMessageBox } from "element-plus";
+import { ElMessage, ElMessageBox, ElLoading } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { getPersonnelList, approvePersonnel, rejectPersonnel } from "@/api/modules/operationManagement";
@@ -45,7 +44,7 @@ const columns = reactive<ColumnProps<any>[]>([
       el: "input",
       props: { placeholder: "请输入活动名称" },
       order: 2
-    },
+    }
   },
   {
     prop: "status",
@@ -74,30 +73,43 @@ const dataCallback = (data: any) => ({
   total: data?.total ?? 0
 });
 
-const getTableList = (params: any) => {
-  // 转换参数格式以匹配新接口
-  const newParams: any = {
-    storeId: params.storeId || localGet("createdId"),
-    pageNum: params.page || params.pageNum || 1,
-    pageSize: params.size || params.pageSize || 10
-  };
-
-  // 如果有状态筛选,添加状态参数(注意:搜索字段名是 status)
-  if (params.status) {
-    newParams.status = params.status;
-  }
+const getTableList = async (params: any) => {
+  // 显示全屏loading
+  const loadingInstance = ElLoading.service({
+    lock: true,
+    text: "加载中...",
+    background: "rgba(0, 0, 0, 0.7)"
+  });
 
-  // 如果有活动类型筛选,添加活动类型参数
-  if (params.activityType) {
-    newParams.activityType = params.activityType;
-  }
+  try {
+    // 转换参数格式以匹配新接口
+    const newParams: any = {
+      storeId: params.storeId || localGet("createdId"),
+      pageNum: params.page || params.pageNum || 1,
+      pageSize: params.size || params.pageSize || 10
+    };
 
-  // 如果有活动名称筛选,添加活动名称参数
-  if (params.activityName) {
-    newParams.activityName = params.activityName;
-  }
+    // 如果有状态筛选,添加状态参数(注意:搜索字段名是 status)
+    if (params.status) {
+      newParams.status = params.status;
+    }
 
-  return getPersonnelList(newParams);
+    // 如果有活动类型筛选,添加活动类型参数
+    if (params.activityType) {
+      newParams.activityType = params.activityType;
+    }
+
+    // 如果有活动名称筛选,添加活动名称参数
+    if (params.activityName) {
+      newParams.activityName = params.activityName;
+    }
+
+    const result = await getPersonnelList(newParams);
+    return result;
+  } finally {
+    // 关闭loading
+    loadingInstance.close();
+  }
 };
 
 const toDetail = (row: any) => {
@@ -141,13 +153,13 @@ const handleReject = async (row: any) => {
         return true;
       }
     });
-    
+
     // 再次验证拒绝原因(防止用户绕过验证)
     if (!rejectReason || rejectReason.trim() === "") {
       ElMessage.warning("拒绝原因不能为空");
       return;
     }
-    
+
     const res: any = await rejectPersonnel({
       id: Number(row.id),
       rejectReason: rejectReason.trim()