Jelajahi Sumber

feat(operationManagement): 新增运营活动管理功能

- 新建活动列表页面,支持活动的增删改查操作
- 实现活动状态管理,包括上架、下架、审核等状态控制
- 添加活动详情查看和拒绝原因展示功能
- 开发新建/编辑活动页面,支持活动基本信息配置
- 集成优惠券选择和活动规则级联选择功能
- 实现活动标题图和详情图上传功能,支持图片预览和验证
- 添加表单验证逻辑,确保活动数据完整性和合法性
- 集成API接口,实现活动数据的前后端交互
- 添加权限控制,限制新建活动按钮的显示
- 实现活动时间范围选择和参与次数限制功能
congxuesong 2 minggu lalu
induk
melakukan
c04c346c71

+ 218 - 0
src/api/indexStore.ts

@@ -0,0 +1,218 @@
+import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
+import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
+import { LOGIN_URL } from "@/config";
+import { ElMessage } from "element-plus";
+import { ResultData } from "@/api/interface";
+import { ResultEnum } from "@/enums/httpEnum";
+import { checkStatus } from "./helper/checkStatus";
+import { AxiosCanceler } from "./helper/axiosCancel";
+import { useUserStore } from "@/stores/modules/user";
+import { localGet } from "@/utils";
+import router from "@/routers";
+
+export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
+  loading?: boolean;
+  cancel?: boolean;
+}
+
+const config = {
+  // 默认地址请求地址,可在 .env.** 文件中修改
+  baseURL: import.meta.env.VITE_API_URL as string,
+  // 设置超时时间
+  timeout: ResultEnum.TIMEOUT as number,
+  // 跨域时候允许携带凭证
+  withCredentials: true
+};
+
+const axiosCanceler = new AxiosCanceler();
+
+class RequestHttp {
+  service: AxiosInstance;
+  public constructor(config: AxiosRequestConfig) {
+    // instantiation
+    this.service = axios.create(config);
+
+    /**
+     * @description 请求拦截器
+     * 客户端发送请求 -> [请求拦截器] -> 服务器
+     * token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中
+     */
+    this.service.interceptors.request.use(
+      (config: CustomAxiosRequestConfig) => {
+        const userStore = useUserStore();
+        // 重复请求不需要取消,在 api 服务中通过指定的第三个参数: { cancel: false } 来控制
+        config.cancel ??= true;
+        config.cancel && axiosCanceler.addPending(config);
+        // 当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { loading: false } 来控制
+        config.loading ??= true;
+        config.loading && showFullScreenLoading();
+        if (config.headers && typeof config.headers.set === "function") {
+          config.headers.set("Authorization", userStore.token);
+        }
+        return config;
+      },
+      (error: AxiosError) => {
+        return Promise.reject(error);
+      }
+    );
+
+    /**
+     * @description 响应拦截器
+     *  服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
+     */
+    this.service.interceptors.response.use(
+      (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
+        const { data, config } = response;
+
+        const userStore = useUserStore();
+        axiosCanceler.removePending(config);
+        config.loading && tryHideFullScreenLoading();
+        // 登录失效
+        if (data.code == ResultEnum.OVERDUE) {
+          userStore.setToken("");
+          router.replace(LOGIN_URL);
+          ElMessage.error(data.msg);
+          return Promise.reject(data);
+        }
+        // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
+        if (data.code && data.code !== ResultEnum.SUCCESS) {
+          ElMessage.error(data.msg);
+          return Promise.reject(data);
+        }
+        // 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑)
+        return data;
+      },
+      async (error: AxiosError) => {
+        const { response } = error;
+        tryHideFullScreenLoading();
+        // 请求超时 && 网络错误单独判断,没有 response
+        if (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时!请您稍后重试");
+        if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误!请您稍后重试");
+        // 根据服务器响应的错误状态码,做不同的处理
+        if (response.data.message) {
+          ElMessage.error(response.data.message);
+        } else {
+          if (response) checkStatus(response.status);
+        }
+        // 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面
+        if (!window.navigator.onLine) router.replace("/500");
+        return Promise.reject(error);
+      }
+    );
+  }
+  /**
+   * @description 常用请求方法封装
+   */
+  get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
+    return this.service.get(url, { params, ..._object });
+  }
+  post<T>(url: string, params?: object | string, _object = {}): Promise<ResultData<T>> {
+    console.log(config.baseURL, url, params);
+    return this.service.post(url, params, _object);
+  }
+  put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
+    return this.service.put(url, params, _object);
+  }
+  delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
+    return this.service.delete(url, { params, ..._object });
+  }
+  download(url: string, params?: object, _object = {}): Promise<BlobPart> {
+    return this.service.post(url, params, { ..._object, responseType: "blob" });
+  }
+
+  /**
+   * @description 文件上传方法(基于 XMLHttpRequest)
+   * @param url 上传地址
+   * @param formData FormData 对象
+   * @param onProgress 上传进度回调函数 (progress: number) => void
+   * @param baseURL 可选的基础URL,不传则使用默认baseURL
+   * @returns Promise<ResultData<T>>
+   */
+  upload<T = any>(
+    url: string,
+    formData: FormData,
+    onProgress?: (progress: number) => void,
+    baseURL?: string
+  ): Promise<ResultData<T>> {
+    return new Promise((resolve, reject) => {
+      const xhr = new XMLHttpRequest();
+      const userStore = useUserStore();
+      // 如果传入了 baseURL,使用传入的 baseURL;如果 URL 是完整 URL(以 http 开头),直接使用;否则使用默认 baseURL
+      const fullUrl = baseURL ? `${baseURL}${url}` : url.startsWith("http") ? url : `${config.baseURL}${url}`;
+
+      // 监听上传进度
+      if (onProgress) {
+        xhr.upload.addEventListener("progress", e => {
+          if (e.lengthComputable) {
+            const percentComplete = Math.round((e.loaded / e.total) * 100);
+            onProgress(percentComplete);
+          }
+        });
+      }
+
+      // 监听请求完成
+      xhr.addEventListener("load", () => {
+        if (xhr.status >= 200 && xhr.status < 300) {
+          try {
+            const response = JSON.parse(xhr.responseText);
+            // 统一处理响应,与 axios 拦截器保持一致
+            if (response.code == ResultEnum.OVERDUE) {
+              userStore.setToken("");
+              router.replace(LOGIN_URL);
+              ElMessage.error(response.msg);
+              reject(response);
+              return;
+            }
+            if (response.code && response.code !== ResultEnum.SUCCESS) {
+              ElMessage.error(response.msg);
+              reject(response);
+              return;
+            }
+            resolve(response);
+          } catch (error) {
+            reject(new Error("响应解析失败"));
+          }
+        } else {
+          const errorMsg = `上传失败: ${xhr.status} ${xhr.statusText}`;
+          ElMessage.error(errorMsg);
+          reject(new Error(errorMsg));
+        }
+      });
+
+      // 监听请求错误
+      xhr.addEventListener("error", () => {
+        const errorMsg = "网络错误!请您稍后重试";
+        ElMessage.error(errorMsg);
+        reject(new Error(errorMsg));
+      });
+
+      // 监听请求中止
+      xhr.addEventListener("abort", () => {
+        reject(new Error("上传已取消"));
+      });
+
+      // 打开请求
+      xhr.open("POST", fullUrl, true);
+
+      // 设置请求头
+      const token = userStore.token || localGet("geeker-user")?.token;
+      if (token) {
+        xhr.setRequestHeader("Authorization", token);
+      }
+
+      // 设置超时
+      xhr.timeout = config.timeout;
+      xhr.addEventListener("timeout", () => {
+        const errorMsg = "请求超时!请您稍后重试";
+        ElMessage.error(errorMsg);
+        reject(new Error(errorMsg));
+      });
+
+      // 发送请求
+      xhr.withCredentials = config.withCredentials;
+      xhr.send(formData);
+    });
+  }
+}
+
+export default new RequestHttp(config);

+ 2 - 31
src/api/modules/operationManagement.ts

@@ -1,6 +1,7 @@
 import { ResPage, StoreUser } from "@/api/interface/index";
 import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
+import http_store from "@/api/indexStore";
 
 /**
  * 获取优惠券列表
@@ -80,35 +81,5 @@ export const updateActivityStatus = params => {
  * @returns 级联选择器选项数据
  */
 export const getActivityRuleOptions = (params?: any) => {
-  // 暂时返回假数据,后续替换为真实 API
-  // 真实 API: return http.get<any>(PORT_NONE + `/store-activity/getActivityRuleOptions`, params);
-  return Promise.resolve({
-    code: 200,
-    data: [
-      {
-        value: "当用户",
-        label: "当用户",
-        children: [
-          {
-            value: "核销并评论",
-            label: "核销并评论",
-            disabled: false,
-            children: [{ value: "优惠券", label: "优惠券", disabled: false }]
-          },
-          {
-            value: "核销",
-            label: "核销",
-            disabled: true,
-            children: [{ value: "红包", label: "红包", disabled: true }]
-          },
-          {
-            value: "打卡",
-            label: "打卡",
-            disabled: true,
-            children: []
-          }
-        ]
-      }
-    ]
-  });
+  return http_store.get<any>(PORT_NONE + `/userActionRewardRule/getRewardRuleList`, params);
 };

+ 341 - 0
src/views/operationManagement/activityList.vue

@@ -0,0 +1,341 @@
+<template>
+  <div class="table-box button-table">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <template #tableHeader="scope">
+        <div class="action-buttons">
+          <el-button :icon="Plus" class="button" type="primary" @click="newActivity" v-if="type"> 新建活动 </el-button>
+        </div>
+      </template>
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <!-- 上架按钮 -->
+        <el-button
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.上架)"
+          link
+          type="primary"
+          @click="changeStatus(scope.row, 1)"
+        >
+          上架
+        </el-button>
+        <!-- 下架按钮 -->
+        <el-button
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.下架)"
+          link
+          type="primary"
+          @click="changeStatus(scope.row, 6)"
+        >
+          下架
+        </el-button>
+        <!-- 编辑按钮 -->
+        <el-button
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.编辑)"
+          link
+          type="primary"
+          @click="editRow(scope.row)"
+        >
+          编辑
+        </el-button>
+        <!-- 查看详情按钮 -->
+        <el-button
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.查看详情)"
+          link
+          type="primary"
+          @click="toDetail(scope.row)"
+        >
+          查看详情
+        </el-button>
+        <!-- 删除按钮 -->
+        <el-button
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.删除)"
+          link
+          type="primary"
+          @click="deleteRow(scope.row)"
+        >
+          删除
+        </el-button>
+        <!-- 查看拒绝原因按钮 -->
+        <el-button
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.查看拒绝原因)"
+          link
+          type="primary"
+          @click="viewRejectReason(scope.row)"
+        >
+          查看拒绝原因
+        </el-button>
+      </template>
+    </ProTable>
+    <!-- 查看拒绝原因弹窗 -->
+    <el-dialog v-model="rejectReasonDialogVisible" title="查看拒绝原因" width="600px">
+      <div class="reject-reason-content">
+        <div class="reject-reason-item">
+          <div class="reject-reason-label">活动名称:</div>
+          <div class="reject-reason-value">
+            {{ rejectReasonData.name || "--" }}
+          </div>
+        </div>
+        <div class="reject-reason-item">
+          <div class="reject-reason-label">拒绝原因:</div>
+          <div class="reject-reason-value reject-reason-text">
+            {{ rejectReasonData.approvalComments || "暂无拒绝原因" }}
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="closeRejectReasonDialog"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="tsx" name="activityList">
+import { reactive, ref, onMounted, computed } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { getActivityList, deleteActivity, updateActivityStatus } from "@/api/modules/operationManagement";
+import { localGet, usePermission } from "@/utils";
+
+const router = useRouter();
+const proTable = ref<ProTableInstance>();
+const type = ref(false);
+
+// 查看拒绝原因弹窗相关
+const rejectReasonDialogVisible = ref(false);
+const rejectReasonData = ref<any>({
+  name: "",
+  approvalComments: ""
+});
+
+// 活动状态枚举
+const ACTIVITY_STATUS = {
+  待审核: 1,
+  未开始: 2,
+  审核驳回: 3,
+  进行中: 5,
+  已下架: 6,
+  已结束: 7
+} as const;
+
+// 活动状态选项(用于搜索)
+const statusEnum = [
+  { label: "待审核", value: "1" },
+  { label: "未开始", value: "2" },
+  { label: "审核驳回", value: "3" },
+  { label: "进行中", value: "5" },
+  { label: "已下架", value: "6" },
+  { label: "已结束", value: "7" }
+];
+
+// 操作按钮权限配置
+const OPERATION_PERMISSIONS = {
+  查看详情: [
+    ACTIVITY_STATUS.待审核,
+    ACTIVITY_STATUS.未开始,
+    ACTIVITY_STATUS.进行中,
+    ACTIVITY_STATUS.已下架,
+    ACTIVITY_STATUS.已结束
+  ],
+  上架: [ACTIVITY_STATUS.已下架],
+  下架: [ACTIVITY_STATUS.进行中],
+  编辑: [ACTIVITY_STATUS.未开始, ACTIVITY_STATUS.审核驳回, ACTIVITY_STATUS.已下架, ACTIVITY_STATUS.已结束],
+  删除: [ACTIVITY_STATUS.未开始, ACTIVITY_STATUS.已结束, ACTIVITY_STATUS.已下架],
+  查看拒绝原因: [ACTIVITY_STATUS.审核驳回]
+} as const;
+
+// 判断按钮是否显示的工具函数
+const canShowButton = (status: number, allowedStatuses: readonly number[]) => {
+  return allowedStatuses.includes(status);
+};
+
+// 获取状态标签
+const getStatusLabel = (status: number) => {
+  const statusMap: Record<number, string> = {
+    1: "待审核",
+    2: "未开始",
+    3: "审核驳回",
+    5: "进行中",
+    6: "已下架",
+    7: "已结束"
+  };
+  return statusMap[status] || "--";
+};
+
+// 表格列配置
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "activityName",
+    label: "活动名称"
+    // search: {
+    //   el: "input",
+    //   props: { placeholder: "请输入" }
+    // }
+  },
+  {
+    prop: "id",
+    label: "活动ID"
+  },
+  {
+    prop: "startTime",
+    label: "活动开始时间",
+    render: (scope: any) => {
+      return scope.row.startTime?.replace(/-/g, "/") || "--";
+    }
+  },
+  {
+    prop: "endTime",
+    label: "活动结束时间",
+    render: (scope: any) => {
+      return scope.row.endTime?.replace(/-/g, "/") || "--";
+    }
+  },
+  {
+    prop: "couponName",
+    label: "优惠券名称"
+  },
+  {
+    prop: "status",
+    label: "状态",
+    render: (scope: any) => {
+      return getStatusLabel(scope.row.status);
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: statusEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 300 }
+]);
+
+// 初始化请求参数
+const initParam = reactive({
+  storeId: localGet("createdId")
+});
+
+// 数据回调处理
+const dataCallback = (data: any) => {
+  return {
+    list: data?.records || [],
+    total: data?.total || 0
+  };
+};
+
+// 获取表格列表
+const getTableList = (params: any) => {
+  return getActivityList(params);
+};
+
+// 新建活动
+const newActivity = () => {
+  router.push(`/operationManagement/newActivity?type=add`);
+};
+
+// 跳转详情页
+const toDetail = (row: any) => {
+  router.push(`/operationManagement/activityDetail?id=${row.id}`);
+};
+
+// 编辑行数据
+const editRow = (row: any) => {
+  router.push(`/operationManagement/newActivity?id=${row.id}&type=edit`);
+};
+
+// 删除行数据
+const deleteRow = (row: any) => {
+  ElMessageBox.confirm("确定要删除吗?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning"
+  })
+    .then(() => {
+      const params = {
+        id: row.id
+      };
+      return deleteActivity(params);
+    })
+    .then(() => {
+      ElMessage.success("删除成功");
+      proTable.value?.getTableList();
+    })
+    .catch(() => {
+      // 用户取消删除,不做任何操作
+    });
+};
+
+// 修改状态(上架/下架)
+const changeStatus = async (row: any, status: number) => {
+  const res = await updateActivityStatus({ id: row.id, status: status });
+  if (res && res.code == 200) {
+    ElMessage.success("操作成功");
+    proTable.value?.getTableList();
+  }
+};
+
+// 查看拒绝原因
+const viewRejectReason = (row: any) => {
+  rejectReasonData.value = {
+    name: row.activityName,
+    approvalComments: row.approvalComments
+  };
+  rejectReasonDialogVisible.value = true;
+};
+
+// 关闭拒绝原因弹窗
+const closeRejectReasonDialog = () => {
+  rejectReasonDialogVisible.value = false;
+};
+
+// 页面加载时触发查询
+onMounted(async () => {
+  type.value = await usePermission("新建运营活动");
+  proTable.value?.getTableList();
+});
+</script>
+
+<style lang="scss" scoped>
+.action-buttons {
+  display: flex;
+  flex: 0 0 auto;
+  gap: 10px;
+  margin-right: 20px;
+  .button {
+    margin-bottom: 0;
+  }
+}
+.reject-reason-content {
+  padding: 20px 0;
+  .reject-reason-item {
+    display: flex;
+    margin-bottom: 20px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .reject-reason-label {
+      flex-shrink: 0;
+      min-width: 100px;
+      font-size: 14px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .reject-reason-value {
+      flex: 1;
+      font-size: 14px;
+      color: #303133;
+      word-break: break-word;
+      &.reject-reason-text {
+        min-height: 80px;
+        padding: 12px;
+        line-height: 1.6;
+        white-space: pre-wrap;
+        background-color: #f5f7fa;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+</style>

+ 1086 - 0
src/views/operationManagement/newActivity.vue

@@ -0,0 +1,1086 @@
+<template>
+  <!-- 运营活动管理 - 新增/编辑页面 -->
+  <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
+    <div class="header">
+      <el-button @click="goBack"> 返回 </el-button>
+      <h2 class="title">{{ type == "add" ? "新建" : "编辑" }}运营活动</h2>
+    </div>
+    <div class="form-wrapper">
+      <div class="form-wrapper-main">
+        <el-form ref="ruleFormRef" :model="activityModel" :rules="rules" class="formBox" label-width="140px">
+          <div class="form-content">
+            <!-- 活动名称 -->
+            <el-form-item label="活动名称" prop="activityName">
+              <el-input v-model="activityModel.activityName" class="form-input" clearable maxlength="50" placeholder="请输入" />
+            </el-form-item>
+
+            <!-- 活动时间 -->
+            <el-form-item class="activity-time-item" label="活动时间" prop="activityTimeRange">
+              <el-date-picker
+                v-model="activityModel.activityTimeRange"
+                :disabled-date="disabledDate"
+                class="form-input"
+                end-placeholder="结束日期"
+                format="YYYY/MM/DD"
+                range-separator="-"
+                start-placeholder="开始日期"
+                type="daterange"
+                value-format="YYYY-MM-DD"
+              />
+            </el-form-item>
+
+            <!-- 用户可参与次数 -->
+            <el-form-item label="用户可参与次数" prop="participationLimit">
+              <el-input v-model="activityModel.participationLimit" placeholder="请输入" />
+            </el-form-item>
+
+            <!-- 活动规则 -->
+            <el-form-item label="活动规则" prop="activityRule">
+              <el-cascader
+                v-model="activityModel.activityRule"
+                :options="ruleCascaderOptions"
+                :props="cascaderProps"
+                class="form-input"
+                clearable
+                placeholder="请选择"
+                style="width: 100%"
+              />
+            </el-form-item>
+
+            <!-- 优惠券 -->
+            <el-form-item label="优惠券" prop="couponId">
+              <el-select v-model="activityModel.couponId" class="form-input" clearable filterable placeholder="请选择">
+                <el-option v-for="item in couponList" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+
+            <!-- 优惠券发放数量 -->
+            <el-form-item label="优惠券发放数量" prop="couponQuantity">
+              <el-input v-model="activityModel.couponQuantity" placeholder="请输入" />
+            </el-form-item>
+
+            <!-- 活动标题图 -->
+            <el-form-item label="活动标题图" prop="activityTitleImage">
+              <div class="upload-item-wrapper">
+                <div class="upload-area upload-area-horizontal-21-9" :class="{ 'upload-full': titleFileList.length >= 1 }">
+                  <el-upload
+                    v-model:file-list="titleFileList"
+                    :accept="'.jpg,.jpeg,.png'"
+                    :auto-upload="false"
+                    :before-remove="handleBeforeRemove"
+                    :disabled="hasUnuploadedImages"
+                    :limit="1"
+                    :on-change="handleTitleUploadChange"
+                    :on-exceed="handleUploadExceed"
+                    :on-preview="handlePictureCardPreview"
+                    :on-remove="handleTitleRemove"
+                    :show-file-list="true"
+                    list-type="picture-card"
+                  >
+                    <template #trigger>
+                      <div v-if="titleFileList.length < 1" class="upload-trigger-card el-upload--picture-card">
+                        <el-icon>
+                          <Plus />
+                        </el-icon>
+                      </div>
+                    </template>
+                  </el-upload>
+                </div>
+                <div class="upload-hint">请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
+              </div>
+            </el-form-item>
+
+            <!-- 活动详情图 -->
+            <el-form-item label="活动详情图" prop="activityDetailImage">
+              <div class="upload-item-wrapper">
+                <div class="upload-area upload-area-vertical" :class="{ 'upload-full': detailFileList.length >= 1 }">
+                  <el-upload
+                    v-model:file-list="detailFileList"
+                    :accept="'.jpg,.jpeg,.png'"
+                    :auto-upload="false"
+                    :before-remove="handleBeforeRemove"
+                    :disabled="hasUnuploadedImages"
+                    :limit="1"
+                    :on-change="handleDetailUploadChange"
+                    :on-exceed="handleUploadExceed"
+                    :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">
+                        <el-icon>
+                          <Plus />
+                        </el-icon>
+                      </div>
+                    </template>
+                  </el-upload>
+                </div>
+                <div class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
+              </div>
+            </el-form-item>
+          </div>
+        </el-form>
+      </div>
+
+      <!-- 底部按钮区域 -->
+      <div class="button-container">
+        <el-button @click="goBack"> 取消 </el-button>
+        <el-button type="primary" @click="handleSubmit()"> 提交审核 </el-button>
+      </div>
+    </div>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :initial-index="imageViewerInitialIndex"
+      :url-list="imageViewerUrlList"
+      @close="imageViewerVisible = false"
+    />
+  </div>
+</template>
+
+<script lang="tsx" name="newActivity" setup>
+/**
+ * 运营活动管理 - 新增/编辑页面
+ * 功能:支持运营活动的新增和编辑操作
+ */
+import { computed, nextTick, onMounted, reactive, ref } from "vue";
+import type { FormInstance, UploadFile, UploadProps } from "element-plus";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import { useRoute, useRouter } from "vue-router";
+import {
+  addActivity,
+  getActivityDetail,
+  getActivityRuleOptions,
+  getCouponList,
+  updateActivity
+} from "@/api/modules/operationManagement";
+import { uploadContractImage } from "@/api/modules/licenseManagement";
+import { localGet } from "@/utils";
+
+// ==================== 响应式数据定义 ====================
+
+// 路由相关
+const router = useRouter();
+const route = useRoute();
+
+// 页面状态
+const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
+const id = ref<string>(""); // 页面ID参数
+
+// 表单引用
+const ruleFormRef = ref<FormInstance>();
+
+// 优惠券列表
+const couponList = ref<any[]>([]);
+
+// 文件上传相关
+const titleFileList = ref<UploadFile[]>([]);
+const detailFileList = ref<UploadFile[]>([]);
+const titleImageUrl = ref<string>("");
+const detailImageUrl = ref<string>("");
+const pendingUploadFiles = ref<UploadFile[]>([]);
+const uploading = ref(false);
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
+
+// 活动规则级联选择器选项
+const ruleCascaderOptions = ref<any[]>([]);
+
+// 级联选择器配置
+const cascaderProps = {
+  expandTrigger: "hover" as const,
+  emitPath: true,
+  disabled: (data: any) => {
+    // 除了 "当用户 > 核销并评论 > 优惠券" 这个路径,其余选项不可选择
+    if (data.disabled !== undefined) {
+      return data.disabled;
+    }
+    return false;
+  }
+};
+
+// 是否有未上传的图片
+const hasUnuploadedImages = computed(() => {
+  return (
+    titleFileList.value.some(file => file.status === "ready" || file.status === "uploading") ||
+    detailFileList.value.some(file => file.status === "ready" || file.status === "uploading")
+  );
+});
+
+// ==================== 表单验证规则 ====================
+const rules = reactive({
+  activityName: [{ required: true, message: "请输入活动名称", trigger: "blur" }],
+  activityTimeRange: [
+    { required: true, message: "请选择活动时间", trigger: "change" },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value || !Array.isArray(value) || value.length !== 2) {
+          callback(new Error("请选择活动时间"));
+          return;
+        }
+        const [startTime, endTime] = value;
+        if (!startTime || !endTime) {
+          callback(new Error("请选择完整的活动时间"));
+          return;
+        }
+        const start = new Date(startTime);
+        const end = new Date(endTime);
+        const today = new Date();
+        today.setHours(0, 0, 0, 0);
+        if (start < today) {
+          callback(new Error("活动开始时间不能早于当前时间"));
+          return;
+        }
+        if (start >= end) {
+          callback(new Error("活动开始时间必须早于活动结束时间"));
+          return;
+        }
+        callback();
+      },
+      trigger: "change"
+    }
+  ],
+  participationLimit: [
+    { required: true, message: "请输入用户可参与次数", trigger: "blur" },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        const numValue = Number(value);
+        if (isNaN(numValue) || !Number.isInteger(numValue) || numValue <= 0) {
+          callback(new Error("用户可参与次数必须为正整数"));
+          return;
+        }
+        if (numValue >= 10000) {
+          callback(new Error("用户可参与次数必须小于10000"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  activityRule: [
+    { required: true, message: "请选择活动规则", trigger: "change" },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value || !Array.isArray(value) || value.length < 2) {
+          callback(new Error("请选择完整的活动规则(至少选择角色和行为)"));
+          return;
+        }
+        callback();
+      },
+      trigger: "change"
+    }
+  ],
+  couponId: [{ required: true, message: "请选择优惠券", trigger: "change" }],
+  couponQuantity: [
+    { required: true, message: "请输入优惠券发放数量", trigger: "blur" },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        const numValue = Number(value);
+        if (isNaN(numValue) || !Number.isInteger(numValue) || numValue <= 0) {
+          callback(new Error("优惠券发放数量必须为正整数"));
+          return;
+        }
+        if (numValue >= 10000) {
+          callback(new Error("优惠券发放数量必须小于10000"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  activityTitleImage: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (!titleImageUrl.value) {
+          callback(new Error("请上传活动标题图"));
+          return;
+        }
+        callback();
+      },
+      trigger: ["change", "blur"]
+    }
+  ],
+  activityDetailImage: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (!detailImageUrl.value) {
+          callback(new Error("请上传活动详情图"));
+          return;
+        }
+        callback();
+      },
+      trigger: ["change", "blur"]
+    }
+  ]
+});
+
+// ==================== 活动信息数据模型 ====================
+const activityModel = ref<any>({
+  // 活动宣传图(包含标题和详情)
+  promotionImages: null,
+  // 活动标题图片
+  activityTitleImg: null,
+  // 活动详情图片
+  activityDetailImg: null,
+  // 活动标题图(用于表单验证)
+  activityTitleImage: null,
+  // 活动详情图(用于表单验证)
+  activityDetailImage: null,
+  // 活动名称
+  activityName: "",
+  // 活动时间范围
+  activityTimeRange: [],
+  // 用户可参与次数
+  participationLimit: "",
+  // 活动规则(级联选择器的值数组)
+  activityRule: [],
+  // 优惠券ID
+  couponId: "",
+  // 优惠券发放数量
+  couponQuantity: ""
+});
+
+// ==================== 日期选择器禁用规则 ====================
+
+// 禁用日期(不能早于今天)
+const disabledDate = (time: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  return time.getTime() < today.getTime();
+};
+
+// ==================== 图片参数转换函数 ====================
+
+/**
+ * 图片URL转换为upload组件的参数
+ * @param imageUrl 图片URL
+ * @returns UploadFile对象
+ */
+const handleImageParam = (imageUrl: string): UploadFile => {
+  // 使用split方法以'/'为分隔符将URL拆分成数组
+  const parts = imageUrl.split("/");
+  // 取数组的最后一项,即图片名称
+  const imageName = parts[parts.length - 1];
+  return {
+    uid: Date.now() + Math.random(),
+    name: imageName,
+    status: "success",
+    percentage: 100,
+    url: imageUrl
+  } as unknown as UploadFile;
+};
+
+// ==================== 文件上传相关函数 ====================
+
+/**
+ * 检查文件是否在排队中(未上传)
+ */
+const isFilePending = (file: any): boolean => {
+  if (file.status === "ready") {
+    return true;
+  }
+  if (pendingUploadFiles.value.some(item => item.uid === file.uid)) {
+    return true;
+  }
+  return false;
+};
+
+/**
+ * 图片上传 - 删除前确认
+ */
+const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
+  if (isFilePending(uploadFile)) {
+    ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
+    return false;
+  }
+  try {
+    await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+    return true;
+  } catch {
+    return false;
+  }
+};
+
+/**
+ * 活动标题图片上传 - 移除图片回调
+ */
+const handleTitleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
+  const file = uploadFile as any;
+  const imageUrl = file.url;
+
+  if (imageUrl) {
+    titleImageUrl.value = "";
+    activityModel.value.activityTitleImg = null;
+    activityModel.value.activityTitleImage = null;
+    // 触发表单验证
+    nextTick(() => {
+      ruleFormRef.value?.validateField("activityTitleImage");
+    });
+  }
+
+  if (file.url && file.url.startsWith("blob:")) {
+    URL.revokeObjectURL(file.url);
+  }
+  titleFileList.value = [...uploadFiles];
+  ElMessage.success("图片已删除");
+};
+
+/**
+ * 活动详情图片上传 - 移除图片回调
+ */
+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");
+    });
+  }
+
+  if (file.url && file.url.startsWith("blob:")) {
+    URL.revokeObjectURL(file.url);
+  }
+  detailFileList.value = [...uploadFiles];
+  ElMessage.success("图片已删除");
+};
+
+/**
+ * 上传文件超出限制提示
+ */
+const handleUploadExceed: UploadProps["onExceed"] = () => {
+  ElMessage.warning("最多只能上传1张图片");
+};
+
+/**
+ * 活动标题图片上传 - 文件变更
+ */
+const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
+  if (uploadFile.raw) {
+    const fileType = uploadFile.raw.type.toLowerCase();
+    const fileName = uploadFile.name.toLowerCase();
+    const validTypes = ["image/jpeg", "image/jpg", "image/png"];
+    const validExtensions = [".jpg", ".jpeg", ".png"];
+
+    const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
+
+    if (!isValidType) {
+      // 从文件列表中移除不符合类型的文件
+      const index = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        titleFileList.value.splice(index, 1);
+      }
+      // 从 uploadFiles 中移除
+      const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (uploadIndex > -1) {
+        uploadFiles.splice(uploadIndex, 1);
+      }
+      // 如果文件有 blob URL,释放它
+      if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
+        URL.revokeObjectURL(uploadFile.url);
+      }
+      ElMessage.warning("只支持上传 JPG、JPEG 和 PNG 格式的图片");
+      return;
+    }
+
+    // 检查文件大小,不得超过20M
+    const maxSize = 20 * 1024 * 1024; // 20MB
+    if (uploadFile.raw.size > maxSize) {
+      // 从文件列表中移除超过大小的文件
+      const index = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        titleFileList.value.splice(index, 1);
+      }
+      // 从 uploadFiles 中移除
+      const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (uploadIndex > -1) {
+        uploadFiles.splice(uploadIndex, 1);
+      }
+      // 如果文件有 blob URL,释放它
+      if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
+        URL.revokeObjectURL(uploadFile.url);
+      }
+      ElMessage.warning("上传图片不得超过20M");
+      return;
+    }
+  }
+
+  const existingIndex = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+  if (existingIndex === -1) {
+    titleFileList.value.push(uploadFile);
+  }
+
+  const readyFiles = titleFileList.value.filter(file => file.status === "ready");
+  if (readyFiles.length) {
+    readyFiles.forEach(file => {
+      if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
+        pendingUploadFiles.value.push(file);
+      }
+    });
+  }
+  processUploadQueue("title");
+};
+
+/**
+ * 活动详情图片上传 - 文件变更
+ */
+const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
+  if (uploadFile.raw) {
+    const fileType = uploadFile.raw.type.toLowerCase();
+    const fileName = uploadFile.name.toLowerCase();
+    const validTypes = ["image/jpeg", "image/jpg", "image/png"];
+    const validExtensions = [".jpg", ".jpeg", ".png"];
+
+    const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
+
+    if (!isValidType) {
+      // 从文件列表中移除不符合类型的文件
+      const index = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        detailFileList.value.splice(index, 1);
+      }
+      // 从 uploadFiles 中移除
+      const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (uploadIndex > -1) {
+        uploadFiles.splice(uploadIndex, 1);
+      }
+      // 如果文件有 blob URL,释放它
+      if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
+        URL.revokeObjectURL(uploadFile.url);
+      }
+      ElMessage.warning("只支持上传 JPG、JPEG 和 PNG 格式的图片");
+      return;
+    }
+
+    // 检查文件大小,不得超过20M
+    const maxSize = 20 * 1024 * 1024; // 20MB
+    if (uploadFile.raw.size > maxSize) {
+      // 从文件列表中移除超过大小的文件
+      const index = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        detailFileList.value.splice(index, 1);
+      }
+      // 从 uploadFiles 中移除
+      const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (uploadIndex > -1) {
+        uploadFiles.splice(uploadIndex, 1);
+      }
+      // 如果文件有 blob URL,释放它
+      if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
+        URL.revokeObjectURL(uploadFile.url);
+      }
+      ElMessage.warning("上传图片不得超过20M");
+      return;
+    }
+  }
+
+  const existingIndex = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+  if (existingIndex === -1) {
+    detailFileList.value.push(uploadFile);
+  }
+
+  const readyFiles = detailFileList.value.filter(file => file.status === "ready");
+  if (readyFiles.length) {
+    readyFiles.forEach(file => {
+      if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
+        pendingUploadFiles.value.push(file);
+      }
+    });
+  }
+  processUploadQueue("detail");
+};
+
+/**
+ * 处理上传队列 - 逐个上传文件
+ */
+const processUploadQueue = async (type: string) => {
+  if (uploading.value || pendingUploadFiles.value.length === 0) {
+    return;
+  }
+  const file = pendingUploadFiles.value.shift();
+  if (file) {
+    await uploadSingleFile(file, type);
+    processUploadQueue(type);
+  }
+};
+
+/**
+ * 单文件上传图片
+ */
+const uploadSingleFile = async (file: UploadFile, uploadType: string) => {
+  if (!file.raw) {
+    return;
+  }
+  const rawFile = file.raw as File;
+  const formData = new FormData();
+  formData.append("file", rawFile);
+  formData.append("user", "text");
+  file.status = "uploading";
+  file.percentage = 0;
+  uploading.value = true;
+
+  try {
+    const result: any = await uploadContractImage(formData);
+    if (result?.code === 200 && result.data) {
+      let imageUrl = result.data[0];
+      if (!imageUrl) {
+        throw new Error("上传成功但未获取到图片URL");
+      }
+
+      file.status = "success";
+      file.percentage = 100;
+      file.url = imageUrl;
+      file.response = { url: imageUrl };
+
+      if (uploadType === "title") {
+        titleImageUrl.value = imageUrl;
+        activityModel.value.activityTitleImg = { url: imageUrl };
+        activityModel.value.activityTitleImage = imageUrl;
+        // 触发表单验证
+        nextTick(() => {
+          ruleFormRef.value?.validateField("activityTitleImage");
+        });
+      } else if (uploadType === "detail") {
+        detailImageUrl.value = imageUrl;
+        activityModel.value.activityDetailImg = { url: imageUrl };
+        activityModel.value.activityDetailImage = imageUrl;
+        // 触发表单验证
+        nextTick(() => {
+          ruleFormRef.value?.validateField("activityDetailImage");
+        });
+      }
+    } else {
+      throw new Error(result?.msg || "图片上传失败");
+    }
+  } catch (error: any) {
+    // 上传失败时保持进度条为 0
+    file.percentage = 0;
+    // 不要设置 file.status = "fail",直接移除文件,避免显示错误占位图
+    if (file.url && file.url.startsWith("blob:")) {
+      URL.revokeObjectURL(file.url);
+    }
+    // 从文件列表中移除失败的文件
+    const index =
+      uploadType === "title"
+        ? titleFileList.value.findIndex((f: any) => f.uid === file.uid)
+        : detailFileList.value.findIndex((f: any) => f.uid === file.uid);
+    if (index > -1) {
+      if (uploadType === "title") {
+        titleFileList.value.splice(index, 1);
+      } else {
+        detailFileList.value.splice(index, 1);
+      }
+    }
+    // Error message handled by global upload method, except for specific business logic errors
+    if (error?.message && error.message.includes("未获取到图片URL")) {
+      ElMessage.error(error.message);
+    }
+  } finally {
+    uploading.value = false;
+    // 触发视图更新
+    if (uploadType === "title") {
+      titleFileList.value = [...titleFileList.value];
+    } else {
+      detailFileList.value = [...detailFileList.value];
+    }
+  }
+};
+
+/**
+ * 图片预览
+ */
+const handlePictureCardPreview = (file: any) => {
+  if (isFilePending(file)) {
+    ElMessage.warning("图片尚未上传,请等待上传完成后再预览");
+    return;
+  }
+  if (file.status === "uploading" && file.url) {
+    imageViewerUrlList.value = [file.url];
+    imageViewerInitialIndex.value = 0;
+    imageViewerVisible.value = true;
+    return;
+  }
+  const allFiles = [...titleFileList.value, ...detailFileList.value];
+  const urlList = allFiles
+    .filter((item: any) => item.status === "success" && (item.url || item.response?.data))
+    .map((item: any) => item.url || item.response?.data);
+  const currentIndex = urlList.findIndex((url: string) => url === (file.url || file.response?.data));
+  if (currentIndex < 0) {
+    ElMessage.warning("图片尚未上传完成,无法预览");
+    return;
+  }
+  imageViewerUrlList.value = urlList;
+  imageViewerInitialIndex.value = currentIndex;
+  imageViewerVisible.value = true;
+};
+
+// ==================== 生命周期钩子 ====================
+
+/**
+ * 组件挂载时初始化
+ */
+onMounted(async () => {
+  id.value = (route.query.id as string) || "";
+  type.value = (route.query.type as string) || "";
+
+  // 加载优惠券列表
+  try {
+    const params = {
+      storeId: localGet("createdId"),
+      groupType: localGet("businessSection"),
+      couponType: "2",
+      couponStatus: "1",
+      couponsFromType: 1,
+      pageNum: 1,
+      pageSize: 99999
+    };
+    const res: any = await getCouponList(params);
+    if (res && res.code == 200) {
+      couponList.value = res.data?.discountList?.records || [];
+    }
+  } catch (error) {
+    console.error("加载优惠券列表失败:", error);
+  }
+
+  // 加载活动规则级联选择器选项
+  try {
+    const res: any = await getActivityRuleOptions();
+    console.log("ruleCascaderOptions:", res.data);
+    if (res && res.code == 200) {
+      ruleCascaderOptions.value = res.data || [];
+    }
+  } catch (error) {
+    console.error("加载活动规则选项失败:", error);
+  }
+
+  // 编辑模式下加载数据
+  if (type.value != "add" && id.value) {
+    try {
+      const res: any = await getActivityDetail({ id: id.value });
+      if (res && res.code == 200) {
+        activityModel.value = { ...activityModel.value, ...res.data };
+        // 处理活动时间范围
+        if (res.data.startTime && res.data.endTime) {
+          activityModel.value.activityTimeRange = [res.data.startTime, res.data.endTime];
+        }
+        // 如果有标题图片,添加到文件列表
+        if (res.data.activityTitleImgUrl) {
+          const titleImgUrl = res.data.activityTitleImgUrl;
+          if (titleImgUrl) {
+            titleImageUrl.value = titleImgUrl;
+            activityModel.value.activityTitleImg = res.data.activityTitleImgUrl;
+            activityModel.value.activityTitleImage = titleImgUrl;
+            const titleFile = handleImageParam(titleImgUrl);
+            titleFileList.value = [titleFile];
+          }
+        }
+        // 如果有详情图片,添加到文件列表
+        if (res.data.activityDetailImgUrl) {
+          const detailImgUrl = res.data.activityDetailImgUrl;
+          if (detailImgUrl) {
+            detailImageUrl.value = detailImgUrl;
+            activityModel.value.activityDetailImg = res.data.activityDetailImgUrl;
+            activityModel.value.activityDetailImage = detailImgUrl;
+            const detailFile = handleImageParam(detailImgUrl);
+            detailFileList.value = [detailFile];
+          }
+        }
+        // 加载活动规则
+        if (res.data.activityRule) {
+          activityModel.value.activityRule = res.data.activityRule.split(",");
+        } else {
+          activityModel.value.activityRule = [];
+        }
+      }
+    } catch (error) {
+      console.error("加载活动详情失败:", error);
+      ElMessage.error("加载活动详情失败");
+    }
+  }
+  await nextTick();
+  ruleFormRef.value?.clearValidate();
+});
+
+// ==================== 事件处理函数 ====================
+
+/**
+ * 返回上一页
+ */
+const goBack = () => {
+  router.go(-1);
+};
+
+/**
+ * 提交表单
+ */
+const handleSubmit = async () => {
+  if (!ruleFormRef.value) return;
+
+  // 如果有未上传的图片,阻止提交
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再提交");
+    return;
+  }
+
+  await ruleFormRef.value.validate(async valid => {
+    if (valid) {
+      const [startTime, endTime] = activityModel.value.activityTimeRange || [];
+      const params: any = {
+        activityName: activityModel.value.activityName,
+        startTime: startTime,
+        endTime: endTime,
+        participationLimit: activityModel.value.participationLimit,
+        activityRule: activityModel.value.activityRule.join(","),
+        couponId: activityModel.value.couponId,
+        couponQuantity: activityModel.value.couponQuantity,
+        activityTitleImg: {
+          imgUrl: titleImageUrl.value,
+          imgSort: 0,
+          storeId: localGet("createdId")
+        },
+        activityDetailImg: {
+          imgUrl: detailImageUrl.value,
+          imgSort: 0,
+          storeId: localGet("createdId")
+        },
+        storeId: localGet("createdId"),
+        groupType: localGet("businessSection"),
+        status: 1 // 1-待审核
+      };
+
+      try {
+        let res: any;
+        if (type.value == "add") {
+          res = await addActivity(params);
+        } else {
+          params.id = id.value;
+          res = await updateActivity(params);
+        }
+
+        if (res && res.code == 200) {
+          ElMessage.success(type.value == "add" ? "新增成功" : "编辑成功");
+          goBack();
+        } else {
+          ElMessage.error(res?.msg || "操作失败");
+        }
+      } catch (error) {
+        console.error("提交失败:", error);
+        ElMessage.error("操作失败");
+      }
+    }
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+/* 页面容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+
+/* 头部区域 */
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20px 24px;
+  background-color: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+  box-shadow: 0 2px 4px rgb(0 0 0 / 2%);
+}
+.title {
+  flex: 1;
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  text-align: center;
+}
+
+/* 表单内容区域 */
+.form-content {
+  padding: 24px;
+  background-color: #ffffff;
+}
+
+/* 表单包装器 */
+.form-wrapper {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background-color: #ffffff;
+  .form-wrapper-main {
+    width: 100%;
+    max-width: 800px;
+  }
+}
+
+/* 上传项容器 */
+.upload-item-wrapper {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  align-items: flex-start;
+  width: 100%;
+}
+.upload-hint {
+  margin-top: 4px;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #909399;
+}
+
+/* 规则表格容器 */
+.rule-table-container {
+  margin-top: 20px;
+}
+
+/* 底部按钮区域 */
+.button-container {
+  display: flex;
+  gap: 20px;
+  justify-content: center;
+  padding-bottom: 15px;
+  background-color: #ffffff;
+}
+.upload-area {
+  width: 100%;
+  :deep(.el-upload--picture-card) {
+    width: 100%;
+    height: 180px;
+  }
+  :deep(.el-upload-list--picture-card) {
+    width: 100%;
+    .el-upload-list__item {
+      width: 100%;
+      height: 180px;
+      margin: 0;
+    }
+  }
+  :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  :deep(.el-upload-list--picture-card .el-icon--close-tip) {
+    display: none !important;
+  }
+  &.upload-full {
+    :deep(.el-upload--picture-card) {
+      display: none !important;
+    }
+  }
+
+  // 21:9横向图片样式
+  &.upload-area-horizontal-21-9 {
+    :deep(.el-upload--picture-card) {
+      width: 100%;
+      height: auto;
+      aspect-ratio: 21 / 9;
+    }
+    :deep(.el-upload-list--picture-card) {
+      width: 100%;
+      .el-upload-list__item {
+        width: 100%;
+        height: auto;
+        aspect-ratio: 21 / 9;
+        margin: 0;
+      }
+    }
+  }
+
+  // 竖版图片样式
+  &.upload-area-vertical {
+    max-width: 300px;
+    :deep(.el-upload--picture-card) {
+      width: 100%;
+      height: 400px;
+      aspect-ratio: 3 / 4;
+    }
+    :deep(.el-upload-list--picture-card) {
+      width: 100%;
+      .el-upload-list__item {
+        width: 100%;
+        height: 400px;
+        aspect-ratio: 3 / 4;
+        margin: 0;
+      }
+    }
+  }
+}
+.upload-trigger-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  font-size: 28px;
+  color: #8c939d;
+}
+
+/* el-upload 图片预览铺满容器 */
+:deep(.el-upload-list--picture-card) {
+  .el-upload-list__item {
+    margin: 0;
+    overflow: hidden;
+    .el-upload-list__item-thumbnail {
+      width: 100%;
+      height: 100%;
+      object-fit: fill;
+    }
+  }
+  .el-upload-list__item[data-status="ready"],
+  .el-upload-list__item.is-ready {
+    position: relative;
+    pointer-events: none;
+    cursor: not-allowed;
+    opacity: 0.6;
+    &::after {
+      position: absolute;
+      inset: 0;
+      z-index: 1;
+      content: "";
+      background-color: rgb(0 0 0 / 30%);
+    }
+    .el-upload-list__item-actions {
+      pointer-events: none;
+      opacity: 0.5;
+    }
+  }
+  .el-upload-list__item:hover .el-upload-list__item-status-label {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  .el-upload-list__item.is-success:focus .el-upload-list__item-status-label {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  .el-icon--close-tip {
+    display: none !important;
+  }
+}
+</style>