Преглед изворни кода

feat(ticket): 新增券管理模块及相关功能页面

- 在菜单配置中添加券管理模块及其子菜单:新建代金券、新建优惠券、券管理详情
- 新增代金券详情页面,展示代金券的基础信息、购买须知、使用规则等内容
- 新增券管理列表页面,支持代金券和优惠券的列表展示、状态管理、库存修改等功能
- 新增新建优惠券页面,用于创建和编辑优惠券信息
- 优化订单详情页面中的文案描述,将“产品名称”改为“订单名称”
congxuesong пре 5 месеци
родитељ
комит
3f923ba8d4

+ 61 - 0
src/assets/json/authMenuList.json

@@ -122,6 +122,67 @@
       ]
     },
     {
+      "path": "/ticketManagement",
+      "name": "ticketManagement",
+      "component": "/ticketManagement/index",
+      "meta": {
+        "icon": "Money",
+        "title": "券管理",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [
+        {
+          "path": "/ticketManagement/newVoucher",
+          "name": "ticketManagementNewVoucher",
+          "component": "/ticketManagement/newVoucher",
+          "meta": {
+            "icon": "Menu",
+            "title": "新建代金券",
+            "activeMenu": "/ticketManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/ticketManagement/newCoupon",
+          "name": "ticketManagementNewCoupon",
+          "component": "/ticketManagement/newCoupon",
+          "meta": {
+            "icon": "Menu",
+            "title": "新建优惠券",
+            "activeMenu": "/ticketManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/ticketManagement/detail",
+          "name": "ticketManagementDetail",
+          "component": "/ticketManagement/detail",
+          "meta": {
+            "icon": "Menu",
+            "title": "券管理详情",
+            "activeMenu": "/ticketManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        }
+      ]
+    },
+    {
       "path": "/voucherManagement",
       "name": "voucherManagement",
       "component": "/voucherManagement/index",

+ 2 - 2
src/views/orderManagement/detail.vue

@@ -11,9 +11,9 @@
         <!-- 基础信息模块 -->
         <div class="model">
           <h3 style="font-weight: bold">基础信息:</h3>
-          <!-- 产品名称 -->
+          <!-- 订单名称 -->
           <div class="detail-item">
-            <div class="detail-label">产品名称</div>
+            <div class="detail-label">订单名称</div>
             <div class="detail-value">
               {{ formData.productName || "--" }}
             </div>

+ 526 - 0
src/views/ticketManagement/detail.vue

@@ -0,0 +1,526 @@
+<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">代金券详情</h2>
+    </div>
+    <div class="content">
+      <!-- 左侧内容区域 -->
+      <div class="contentLeft">
+        <!-- 基础信息模块 -->
+        <div class="model">
+          <h3 style="font-weight: bold">基础信息:</h3>
+          <!-- 代金券名称 -->
+          <div class="detail-item">
+            <div class="detail-label">代金券名称</div>
+            <div class="detail-value">
+              {{ voucherModel.name || "--" }}
+            </div>
+          </div>
+          <!-- 抵扣价格 -->
+          <div class="detail-item">
+            <div class="detail-label">抵扣价格</div>
+            <div class="detail-value">
+              {{ voucherModel.offprice ? `¥${voucherModel.offprice}` : "--" }}
+            </div>
+          </div>
+          <!-- 售卖价格 -->
+          <div class="detail-item">
+            <div class="detail-label">售卖价格</div>
+            <div class="detail-value">
+              {{ voucherModel.price ? `¥${voucherModel.price}` : "--" }}
+            </div>
+          </div>
+          <!-- 开始售卖时间 -->
+          <div class="detail-item">
+            <div class="detail-label">开始售卖时间</div>
+            <div class="detail-value">
+              {{ voucherModel.startDate ? formatDate(voucherModel.startDate) : "--" }}
+            </div>
+          </div>
+          <!-- 结束售卖时间 -->
+          <div class="detail-item">
+            <div class="detail-label">结束售卖时间</div>
+            <div class="detail-value">
+              {{ voucherModel.endDate ? formatDate(voucherModel.endDate) : "--" }}
+            </div>
+          </div>
+        </div>
+        <!-- 购买须知模块 -->
+        <div class="model">
+          <h3 style="font-weight: bold">购买须知:</h3>
+          <!-- 使用时间 -->
+          <div class="detail-item">
+            <div class="detail-label">使用时间</div>
+            <div class="detail-value">
+              {{ getUsageTimeText() }}
+            </div>
+          </div>
+          <!-- 有效期 -->
+          <div class="detail-item">
+            <div class="detail-label">有效期</div>
+            <div class="detail-value">
+              {{ getExpirationText() }}
+            </div>
+          </div>
+          <!-- 不可用日期 -->
+          <div class="detail-item">
+            <div class="detail-label">不可用日期</div>
+            <div class="detail-value">
+              {{ getUnavailableDateText() }}
+            </div>
+          </div>
+          <!-- 库存 -->
+          <div class="detail-item">
+            <div class="detail-label">库存</div>
+            <div class="detail-value">
+              {{ voucherModel.singleQty || "--" }}
+            </div>
+          </div>
+        </div>
+      </div>
+      <!-- 右侧内容区域 -->
+      <div class="contentRight">
+        <!-- 使用规则模块 -->
+        <div class="model">
+          <h3 style="font-weight: bold">使用规则:</h3>
+          <!-- 单次可用数量 -->
+          <div class="detail-item">
+            <div class="detail-label">单次可用数量</div>
+            <div class="detail-value">
+              {{ voucherModel.singleCanUse || "--" }}
+            </div>
+          </div>
+          <!-- 限购数量 -->
+          <div class="detail-item">
+            <div class="detail-label">限购数量</div>
+            <div class="detail-value">
+              {{ voucherModel.purchaseLimitCode || "--" }}
+            </div>
+          </div>
+          <!-- 适用范围 -->
+          <div class="detail-item">
+            <div class="detail-label">适用范围</div>
+            <div class="detail-value">
+              {{ getApplyScopeText() }}
+            </div>
+          </div>
+          <!-- 补充说明 -->
+          <div class="detail-item">
+            <div class="detail-label">补充说明</div>
+            <div class="detail-value">
+              {{ voucherModel.supplement || "--" }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="tsx" name="voucherManagementDetail">
+/**
+ * 代金券管理 - 详情页面
+ * 功能:显示代金券的详细信息
+ */
+import { ref, onMounted } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { ElMessage } from "element-plus";
+import { getVoucherDetail, getHolidayList } from "@/api/modules/voucherManagement";
+
+// ==================== 响应式数据定义 ====================
+
+// 路由相关
+const router = useRouter();
+const route = useRoute();
+
+// 页面ID参数
+const id = ref<string>("");
+
+// ==================== 代金券信息数据模型 ====================
+const voucherModel = ref<any>({
+  // 代金券名称
+  name: "",
+  // 抵扣价格
+  offprice: "",
+  // 售卖价格
+  price: "",
+  // 开始售卖时间
+  startDate: "",
+  // 结束售卖时间
+  endDate: "",
+  // 使用时间 - 开始时间
+  buyUseStartTime: "",
+  // 使用时间 - 结束时间
+  buyUseEndTime: "",
+  // 有效期类型:0-指定天数,1-指定时间段内可用
+  expirationType: "0",
+  // 有效期天数(当expirationType为0时使用)
+  expirationDate: 0,
+  // 有效期时间段(当expirationType为1时使用)
+  validityPeriod: [],
+  // 不可用日期类型:0-全部日期可用,1-限制日期
+  unusedType: "0",
+  // 限制日期 - 星期选择(数组,存储选中的星期值)
+  unavailableWeekdays: [],
+  // 限制日期 - 节日选择(数组,存储选中的节日值)
+  unavailableHolidays: [],
+  // 自定义不可用日期列表(数组,每个元素是一个日期范围 [startDate, endDate])
+  disableDateList: [],
+  // 库存
+  singleQty: "",
+  // 单日可用数量
+  singleCanUse: "",
+  // 限购数量
+  purchaseLimitCode: "",
+  // 适用范围类型:0-全场通用,1-部分不可用
+  applyType: "1",
+  // 适用范围(当applyType为1时使用)
+  applyDesc: "",
+  // 补充说明
+  supplement: ""
+});
+
+// 星期选项列表
+const weekdayList = ref([
+  { name: "周一", id: "0", oName: "星期一" },
+  { name: "周二", id: "1", oName: "星期二" },
+  { name: "周三", id: "2", oName: "星期三" },
+  { name: "周四", id: "3", oName: "星期四" },
+  { name: "周五", id: "4", oName: "星期五" },
+  { name: "周六", id: "5", oName: "星期六" },
+  { name: "周日", id: "6", oName: "星期日" }
+]);
+
+// 节日选项列表
+const holidayList: any = ref([]);
+
+// ==================== 生命周期钩子 ====================
+
+/**
+ * 组件挂载时初始化
+ * 从路由参数中获取ID并加载详情数据
+ */
+onMounted(async () => {
+  id.value = (route.query.id as string) || "";
+  // 加载节日列表
+  await loadHolidayList();
+  if (id.value) {
+    await loadDetailData();
+  } else {
+    ElMessage.warning("缺少代金券ID参数");
+  }
+});
+
+// ==================== 事件处理函数 ====================
+
+/**
+ * 返回上一页
+ */
+const goBack = () => {
+  router.go(-1);
+};
+
+// ==================== 数据加载函数 ====================
+
+/**
+ * 加载节日列表
+ */
+const loadHolidayList = async () => {
+  try {
+    let params = {
+      year: new Date().getFullYear(),
+      page: 1,
+      size: 500,
+      openFlag: 1,
+      holidayName: ""
+    };
+    let res: any = await getHolidayList(params);
+    if (res && res.code == 200) {
+      holidayList.value = res.data.records || [];
+    }
+  } catch (error) {
+    console.error("加载节日列表出错:", error);
+  }
+};
+
+/**
+ * 加载详情数据
+ */
+const loadDetailData = async () => {
+  try {
+    const params = {
+      id: id.value
+    };
+    const res: any = await getVoucherDetail(params);
+    if (res && res.code == 200) {
+      // 合并主数据
+      voucherModel.value = { ...voucherModel.value, ...res.data };
+
+      // 处理有效期时间段:将时间戳字符串转换为日期数组
+      if (voucherModel.value.validityPeriod && voucherModel.value.expirationType == 1) {
+        const periodArray = voucherModel.value.validityPeriod.split(",");
+        voucherModel.value.validityPeriod = periodArray
+          .map((item: string) => {
+            const timestamp = Number(item.trim());
+            if (!isNaN(timestamp)) {
+              // 将时间戳转换为日期字符串 (YYYY-MM-DD)
+              return new Date(timestamp).toISOString().split("T")[0];
+            }
+            return null;
+          })
+          .filter((item: any) => item !== null);
+      } else {
+        voucherModel.value.validityPeriod = [];
+      }
+
+      // 处理不可用日期
+      if (voucherModel.value.unusedType == 1) {
+        // 限制日期类型:格式为 "星期;节日"
+        const listVal = voucherModel.value.unusedDate ? voucherModel.value.unusedDate.split(";") : [];
+        voucherModel.value.unavailableWeekdays = listVal[0] ? listVal[0].split(",").filter((item: string) => item) : [];
+        voucherModel.value.unavailableHolidays = listVal[1] ? listVal[1].split(",").filter((item: string) => item) : [];
+      } else if (voucherModel.value.unusedType === 2) {
+        // 自定义不可用日期类型:格式为 "开始日期,结束日期;开始日期,结束日期"
+        if (voucherModel.value.unusedDate) {
+          const dateRanges = voucherModel.value.unusedDate.split(";");
+          voucherModel.value.disableDateList = dateRanges
+            .map((range: string) => {
+              const dates = range.split(",");
+              if (dates.length === 2 && dates[0] && dates[1]) {
+                return [dates[0].trim(), dates[1].trim()];
+              }
+              return null;
+            })
+            .filter((item: any) => item !== null);
+        } else {
+          voucherModel.value.disableDateList = [];
+        }
+      }
+    } else {
+      ElMessage.error("加载详情数据失败");
+    }
+  } catch (error) {
+    console.error("加载详情数据出错:", error);
+    ElMessage.error("加载详情数据出错");
+  }
+};
+
+// ==================== 工具函数 ====================
+
+/**
+ * 格式化日期
+ * @param date 日期字符串 (YYYY-MM-DD)
+ * @returns 格式化后的日期字符串 (YYYY.MM.DD)
+ */
+const formatDate = (date: string) => {
+  if (!date) return "--";
+  return date.replace(/-/g, ".");
+};
+
+/**
+ * 获取使用时间文本
+ */
+const getUsageTimeText = () => {
+  if (voucherModel.value.buyUseStartTime && voucherModel.value.buyUseEndTime) {
+    const startHour = formatHour(voucherModel.value.buyUseStartTime);
+    const endHour = formatHour(voucherModel.value.buyUseEndTime);
+    return `${startHour}-${endHour}`;
+  }
+  return "--";
+};
+
+/**
+ * 格式化小时
+ * @param hour 小时值(字符串或数字)
+ * @returns 格式化后的小时文本(如:7:00)
+ */
+const formatHour = (hour: string | number) => {
+  if (!hour && hour !== 0) return "";
+  const hourNum = typeof hour === "string" ? parseInt(hour) : hour;
+  return `${hourNum}:00`;
+};
+
+/**
+ * 获取有效期文本
+ */
+const getExpirationText = () => {
+  if (voucherModel.value.expirationType === "0" || voucherModel.value.expirationType == 0) {
+    if (voucherModel.value.expirationDate) {
+      return `购买后${voucherModel.value.expirationDate}天`;
+    }
+    return "--";
+  } else if (voucherModel.value.expirationType === "1" || voucherModel.value.expirationType == 1) {
+    if (
+      voucherModel.value.validityPeriod &&
+      Array.isArray(voucherModel.value.validityPeriod) &&
+      voucherModel.value.validityPeriod.length === 2
+    ) {
+      const startDate = formatDate(voucherModel.value.validityPeriod[0]);
+      const endDate = formatDate(voucherModel.value.validityPeriod[1]);
+      return `${startDate}-${endDate}`;
+    }
+    return "--";
+  }
+  return "--";
+};
+
+/**
+ * 获取不可用日期文本
+ */
+const getUnavailableDateText = () => {
+  if (voucherModel.value.unusedType === "0" || voucherModel.value.unusedType == 0) {
+    return "全部日期可用";
+  } else if (voucherModel.value.unusedType === "1" || voucherModel.value.unusedType == 1) {
+    const weekdays: string[] = [];
+    const holidays: string[] = [];
+
+    // 处理星期
+    if (voucherModel.value.unavailableWeekdays && voucherModel.value.unavailableWeekdays.length > 0) {
+      voucherModel.value.unavailableWeekdays.forEach((day: string) => {
+        const weekday = weekdayList.value.find(w => w.oName === day);
+        if (weekday) {
+          weekdays.push(weekday.name);
+        }
+      });
+    }
+
+    // 处理节日
+    if (voucherModel.value.unavailableHolidays && voucherModel.value.unavailableHolidays.length > 0) {
+      voucherModel.value.unavailableHolidays.forEach((holidayId: string) => {
+        const holiday = holidayList.value.find((h: any) => h.id === holidayId || String(h.id) === String(holidayId));
+        if (holiday) {
+          holidays.push(holiday.festivalName);
+        }
+      });
+    }
+
+    const parts: string[] = [];
+    if (weekdays.length > 0) {
+      parts.push(weekdays.join("、"));
+    }
+    if (holidays.length > 0) {
+      parts.push(holidays.join("、"));
+    }
+
+    return parts.length > 0 ? parts.join("、") : "--";
+  } else if (voucherModel.value.unusedType === "2" || voucherModel.value.unusedType == 2) {
+    if (voucherModel.value.disableDateList && voucherModel.value.disableDateList.length > 0) {
+      const dateStrings = voucherModel.value.disableDateList
+        .filter((date: any) => date && Array.isArray(date) && date.length === 2)
+        .map((date: any) => {
+          const startDate = formatDate(date[0]);
+          const endDate = formatDate(date[1]);
+          return startDate === endDate ? startDate : `${startDate}-${endDate}`;
+        });
+      return dateStrings.length > 0 ? dateStrings.join("、") : "--";
+    }
+    return "--";
+  }
+  return "--";
+};
+
+/**
+ * 获取适用范围文本
+ */
+const getApplyScopeText = () => {
+  if (voucherModel.value.applyType === "1" || voucherModel.value.applyType == 1) {
+    return "全场通用";
+  } else if (voucherModel.value.applyType === "2" || voucherModel.value.applyType == 2) {
+    if (voucherModel.value.applyDesc) {
+      return `除${voucherModel.value.applyDesc}外全场通用`;
+    }
+    return "全场通用";
+  }
+  return "--";
+};
+</script>
+
+<style scoped lang="scss">
+/* 页面容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+
+/* 头部区域 */
+.header {
+  display: flex;
+  align-items: 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;
+}
+
+/* 内容区域布局 */
+.content {
+  display: flex;
+  flex: 1;
+  column-gap: 24px;
+  width: 98%;
+  padding: 0 12px;
+  margin: 24px auto;
+
+  /* 左侧内容区域 */
+  .contentLeft {
+    width: 50%;
+    padding-right: 12px;
+  }
+
+  /* 右侧内容区域 */
+  .contentRight {
+    width: 50%;
+    padding-left: 12px;
+  }
+}
+
+/* 模块容器 */
+.model {
+  margin-bottom: 50px;
+  h3 {
+    padding-bottom: 12px;
+    margin: 0 0 20px;
+    font-size: 16px;
+    color: #303133;
+    border-bottom: 2px solid #e4e7ed;
+  }
+}
+
+/* 详情项样式 */
+.detail-item {
+  display: flex;
+  align-items: flex-start;
+  min-height: 32px;
+  margin-bottom: 24px;
+}
+.detail-label {
+  flex-shrink: 0;
+  min-width: 120px;
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 32px;
+  color: #606266;
+}
+.detail-value {
+  flex: 1;
+  font-size: 14px;
+  line-height: 32px;
+  color: #303133;
+  word-break: break-word;
+}
+.empty-text {
+  color: #909399;
+}
+</style>

+ 600 - 0
src/views/ticketManagement/index.vue

@@ -0,0 +1,600 @@
+<template>
+  <div class="table-box">
+    <ProTable
+      ref="proTable"
+      :columns="columns"
+      :request-api="getTableList"
+      :init-param="initParam"
+      :data-callback="dataCallback"
+      :key="activeName"
+    >
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader="scope">
+        <div class="table-header-btn">
+          <div class="header-actions">
+            <el-tabs v-model="activeName" class="tabs" @tab-click="handleClick">
+              <el-tab-pane v-for="tab in allTabOptions" :key="tab.name" :label="tab.label" :name="tab.name" />
+            </el-tabs>
+            <div class="action-buttons">
+              <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying" v-if="type"> 新建代金券 </el-button>
+              <el-button :icon="Plus" class="button" type="primary" @click="newCoupon" v-if="typeCoupon"> 新建优惠券 </el-button>
+            </div>
+          </div>
+        </div>
+      </template>
+      <template #status="scope">
+        <!-- 代金券:显示状态和审核状态两行 -->
+        <template v-if="activeName === '1'">
+          <p style="margin: 0; line-height: 1.5">状态:{{ getStatusLabel(scope.row.status) || "--" }}</p>
+          <p style="margin: 0; line-height: 1.5">审核状态:{{ getReviewStatusLabel(scope.row.reviewType) || "--" }}</p>
+        </template>
+        <!-- 优惠券:只显示状态一行 -->
+        <template v-else>
+          <span>{{ getStatusLabel(scope.row.status) }}</span>
+        </template>
+      </template>
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button
+          link
+          type="primary"
+          @click="changeTypes(scope.row, 1)"
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.上架)"
+        >
+          上架
+        </el-button>
+        <el-button
+          link
+          type="primary"
+          @click="changeTypes(scope.row, 6)"
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.下架)"
+        >
+          下架
+        </el-button>
+        <el-button
+          link
+          type="primary"
+          @click="changeInventory(scope.row)"
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.修改库存)"
+        >
+          修改库存
+        </el-button>
+        <el-button
+          link
+          type="primary"
+          @click="viewRejectReason(scope.row)"
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.查看拒绝原因)"
+        >
+          查看拒绝原因
+        </el-button>
+        <el-button
+          type="primary"
+          link
+          @click="toDetail(scope.row)"
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.查看详情) && scope.row.dataType != 1"
+        >
+          查看详情
+        </el-button>
+        <el-button
+          link
+          type="primary"
+          @click="editRow(scope.row)"
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.编辑) || scope.row.dataType == 1"
+        >
+          编辑
+        </el-button>
+        <el-button
+          link
+          type="primary"
+          @click="deleteRow(scope.row)"
+          v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.删除) || scope.row.dataType == 1"
+        >
+          删除
+        </el-button>
+      </template>
+    </ProTable>
+    <el-dialog v-model="dialogFormVisible" title="修改库存" width="500">
+      <el-form ref="ruleFormRef" :model="formInventory" :rules="rules" @submit.prevent>
+        <el-form-item label="套餐名">
+          {{ formInventory.name }}
+        </el-form-item>
+        <el-form-item label="剩余库存">
+          {{ formInventory.singleQty }}
+        </el-form-item>
+        <el-form-item label="修改库存" prop="newInventory">
+          <el-input v-model="formInventory.newInventory" placeholder="请输入" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="closeDialog"> 取消 </el-button>
+          <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!-- 查看拒绝原因弹窗 -->
+    <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="voucherManagement">
+import { computed, onActivated, onMounted, reactive, ref, watch } from "vue";
+import { useRouter } from "vue-router";
+import type { FormInstance, FormRules } from "element-plus";
+import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { Plus } from "@element-plus/icons-vue";
+import { delThaliById, getThaliList, updateNum, updateStatus } from "@/api/modules/voucherManagement";
+import { ElMessageBox } from "element-plus/es";
+import { localGet, usePermission } from "@/utils";
+
+const router = useRouter();
+const dialogFormVisible = ref(false);
+const formInventory: any = ref({
+  id: "",
+  name: "",
+  singleQty: "",
+  newInventory: ""
+});
+const rowData = ref<any>();
+const activeName = ref("1");
+// 查看拒绝原因弹窗相关
+const rejectReasonDialogVisible = ref(false);
+const rejectReasonData = ref<any>({
+  name: "",
+  approvalComments: ""
+});
+// 定义表单类型
+interface RuleForm {
+  newInventory: string;
+}
+const ruleFormRef = ref<FormInstance>();
+const rules = reactive<FormRules<RuleForm>>({
+  newInventory: [
+    { required: true, message: "请输入库存数量", trigger: "blur" },
+    {
+      pattern: /^(0|[1-9][0-9]*)$/,
+      message: "请输入整数,不允许输入小数,负数",
+      trigger: "blur"
+    },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (value) {
+          const numValue = Number(value);
+          if (!isNaN(numValue) && numValue > 10000) {
+            callback(new Error("库存不得大于10000"));
+            return;
+          }
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ]
+});
+const statusEnum = [
+  { label: "全部", value: "" },
+  { label: "草稿", value: "0" },
+  { label: "进行中", value: "5" },
+  { label: "未开始", value: "2" },
+  { label: "已下架", value: "6" },
+  { label: "已售罄", value: "4" },
+  { label: "已结束", value: "7" }
+];
+// ProTable 实例(需要在使用它的地方之前定义)
+const proTable = ref<ProTableInstance>();
+
+// 代金券表格列配置
+const voucherColumns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "name",
+    label: "券名称",
+    search: {
+      el: "input"
+    }
+  },
+  {
+    prop: "price",
+    label: "价格",
+    render: (scope: any) => {
+      return scope.row.price ? `¥${scope.row.price}` : "--";
+    }
+  },
+  {
+    prop: "saleNum",
+    label: "已售",
+    render: scope => {
+      return scope.row.saleNum === null || scope.row.saleNum === undefined ? 0 : scope.row.saleNum;
+    }
+  },
+  {
+    prop: "singleQty",
+    label: "剩余库存",
+    render: scope => {
+      return scope.row.singleQty === null || scope.row.singleQty === undefined ? 0 : scope.row.singleQty;
+    }
+  },
+  {
+    prop: "endDate",
+    label: "结束时间"
+  },
+  {
+    prop: "status",
+    label: "状态"
+  },
+  {
+    prop: "reviewType",
+    label: "审核状态",
+    isShow: false,
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: statusEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 330 }
+]);
+
+// 优惠券表格列配置
+const couponColumns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "name",
+    label: "券名称",
+    search: {
+      el: "input"
+    }
+  },
+  {
+    prop: "saleNum",
+    label: "已领",
+    render: scope => {
+      return scope.row.saleNum === null || scope.row.saleNum === undefined ? 0 : scope.row.saleNum;
+    }
+  },
+  {
+    prop: "singleQty",
+    label: "剩余库存",
+    render: scope => {
+      return scope.row.singleQty === null || scope.row.singleQty === undefined ? 0 : scope.row.singleQty;
+    }
+  },
+  {
+    prop: "endDate",
+    label: "结束时间"
+  },
+  {
+    prop: "status",
+    label: "状态"
+  },
+  {
+    prop: "reviewType",
+    label: "审核状态",
+    isShow: false,
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: statusEnum,
+    fieldNames: { label: "label", value: "name" }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 330 }
+]);
+
+// 根据当前选中的tab动态返回列配置
+const columns = computed(() => {
+  return activeName.value === "1" ? voucherColumns : couponColumns;
+});
+
+const allTabOptions = [
+  { label: "代金券", name: "1" },
+  { label: "优惠券", name: "2" }
+];
+
+// 状态枚举:0草稿 1待审核 2未开始 3审核拒绝 4已售罄 5进行中 6已下架 7已结束
+const STATUS = {
+  草稿: 0,
+  待审核: 1,
+  未开始: 2,
+  审核拒绝: 3,
+  已售罄: 4,
+  进行中: 5,
+  已下架: 6,
+  已结束: 7
+} as const;
+
+// 操作按钮权限配置:定义每个操作按钮在哪些状态下显示
+const OPERATION_PERMISSIONS = {
+  // 查看详情:待审核、未开始、审核拒绝、进行中、已售罄、已下架
+  查看详情: [STATUS.待审核, STATUS.未开始, STATUS.审核拒绝, STATUS.进行中, STATUS.已售罄, STATUS.已下架],
+  // 上架:未开始、已下架
+  上架: [STATUS.未开始, STATUS.已下架],
+  // 下架:进行中
+  下架: [STATUS.进行中],
+  // 修改库存:未开始、进行中、已售罄
+  修改库存: [STATUS.未开始, STATUS.进行中, STATUS.已售罄],
+  // 编辑:草稿、审核拒绝、已售罄、已下架、已结束
+  编辑: [STATUS.草稿, STATUS.审核拒绝, STATUS.已售罄, STATUS.已下架, STATUS.已结束],
+  // 删除:草稿、未开始、审核拒绝、已售罄、已结束
+  删除: [STATUS.草稿, STATUS.未开始, STATUS.审核拒绝, STATUS.已售罄, STATUS.已结束],
+  // 查看拒绝原因:审核拒绝
+  查看拒绝原因: [STATUS.审核拒绝]
+} as const;
+
+// 判断按钮是否显示的工具函数
+const canShowButton = (status: number, allowedStatuses: readonly number[]) => {
+  return allowedStatuses.includes(status);
+};
+
+// 获取状态标签
+const getStatusLabel = (status: number) => {
+  const statusMap: Record<number, string> = {
+    0: "草稿",
+    1: "待审核",
+    2: "未开始",
+    3: "审核拒绝",
+    4: "已售罄",
+    5: "进行中",
+    6: "已下架",
+    7: "已结束"
+  };
+  return statusMap[status] || "--";
+};
+
+// 获取审核状态标签
+const getReviewStatusLabel = (reviewType: string | number) => {
+  if (reviewType === null || reviewType === undefined || reviewType === "") {
+    return "";
+  }
+  const reviewStatusMap: Record<string, string> = {
+    "0": "待审核",
+    "1": "审核通过",
+    "2": "审核驳回"
+  };
+  return reviewStatusMap[String(reviewType)] || "";
+};
+
+// 获取当前审核状态
+const currentAuditStatus = computed(() => {
+  if (!proTable.value?.searchParam) return "";
+  return proTable.value.searchParam.reviewType || "";
+});
+
+// 根据审核状态过滤 tabOptions:如果审核状态为空,只显示草稿;审核通过时,显示除草稿外的所有标签页
+const filteredTabOptions = computed(() => {
+  const status = currentAuditStatus.value;
+  if (!status) {
+    return allTabOptions;
+  } else if (status == "1") {
+    // 审核通过时,显示除草稿外的所有标签页
+    return allTabOptions.filter(tab => tab.name != "0");
+  }
+  return [];
+});
+
+const dataType = computed(() => {
+  if (!activeName.value) return 2;
+  return activeName.value === "0" ? 1 : 0;
+});
+
+// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
+const initParam = reactive({
+  storeId: localGet("createdId"),
+  groupType: localGet("businessSection"),
+  status: activeName,
+  dataType: dataType
+});
+const type = ref(false);
+const typeCoupon = ref(false);
+// 页面加载时触发查询
+onMounted(async () => {
+  type.value = await usePermission("新建代金券");
+  typeCoupon.value = await usePermission("新建优惠券");
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+
+// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 list && total 这些字段,可以在这里进行处理成这些字段
+// 或者直接去 hooks/useTable.ts 文件中把字段改为你后端对应的就行
+const dataCallback = (data: any) => {
+  return {
+    list: data.records,
+    total: data.total
+  };
+};
+
+// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
+// 默认不做操作就直接在 ProTable 组件上绑定	:requestApi="getUserList"
+const getTableList = (params: any) => {
+  let newParams = JSON.parse(JSON.stringify(params));
+  return getThaliList(newParams);
+};
+const newGroupBuying = () => {
+  router.push(`/ticketManagement/newVoucher?type=add`);
+};
+const newCoupon = () => {
+  router.push(`/ticketManagement/newCoupon?type=add`);
+};
+// 跳转详情页 - 根据类型跳转不同页面
+const toDetail = row => {
+  if (activeName.value === "1") {
+    // 代金券跳转到代金券详情页
+    router.push(`/ticketManagement/detail?id=${row.id}`);
+  } else {
+    // 优惠券跳转到优惠券详情页
+    router.push(`/couponManagement/detail?id=${row.id}`);
+  }
+};
+const editRow = row => {
+  if (activeName.value === "1") {
+    // 代金券编辑
+    router.push(`/ticketManagement/newVoucher?id=${row.id}&type=edit`);
+  } else {
+    // 优惠券编辑
+    router.push(`/ticketManagement/newCoupon?id=${row.id}&type=edit`);
+  }
+};
+const deleteRow = row => {
+  ElMessageBox.confirm("确定要删除吗?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning"
+  }).then(() => {
+    const params = {
+      id: row.id,
+      groupType: localGet("businessSection")
+    };
+    delThaliById(params).then(() => {
+      ElMessage.success("删除成功");
+      proTable.value?.getTableList();
+    });
+  });
+};
+const handleClick = () => {
+  // tab切换时刷新表格
+  proTable.value?.getTableList();
+};
+const changeTypes = async (row, status) => {
+  rowData.value = row;
+  let res = await updateStatus({ id: row.id, status: status, approvalComments: "" });
+  if (res && res.code == 200) {
+    ElMessage.success("操作成功");
+    proTable.value?.getTableList();
+  }
+};
+const changeInventory = (row: any) => {
+  formInventory.value.id = row.id;
+  formInventory.value.name = row.name;
+  formInventory.value.singleQty = row.singleQty;
+  formInventory.value.newInventory = "";
+  dialogFormVisible.value = true;
+};
+// 弹窗提交
+const handleSubmit = async () => {
+  if (!ruleFormRef.value) return;
+  await ruleFormRef.value.validate(async (valid, fields) => {
+    if (valid) {
+      let res = await updateNum({ id: formInventory.value.id, singleQty: formInventory.value.newInventory });
+      if (res && res.code == 200) {
+        ElMessage.success("修改成功");
+        dialogFormVisible.value = false;
+        proTable.value?.getTableList();
+      }
+    }
+  });
+};
+// 关闭弹窗;
+const closeDialog = () => {
+  dialogFormVisible.value = false;
+  formInventory.value = {
+    id: "",
+    name: "",
+    singleQty: "",
+    newInventory: ""
+  };
+};
+// 查看拒绝原因
+const viewRejectReason = (row: any) => {
+  rejectReasonData.value = {
+    name: row.name || "--",
+    approvalComments: row.approvalComments || "--"
+  };
+  rejectReasonDialogVisible.value = true;
+};
+// 关闭拒绝原因弹窗
+const closeRejectReasonDialog = () => {
+  rejectReasonDialogVisible.value = false;
+  rejectReasonData.value = {
+    name: "",
+    approvalComments: ""
+  };
+};
+</script>
+
+<style lang="scss" scoped>
+// 在组件样式中添加
+.date-range {
+  display: block; // 确保换行生效
+  padding: 0 8px; // 可选:增加内边距
+  word-wrap: break-word; // 长单词内换行
+  white-space: normal; // 允许自然换行
+}
+.table-header-btn {
+  width: 160vh;
+  .header-actions {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    .tabs {
+      flex: 0 0 auto;
+      :deep(.el-tabs__nav-wrap::after) {
+        height: 0;
+      }
+    }
+    .action-buttons {
+      display: flex;
+      flex: 0 0 auto;
+      gap: 10px;
+      .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>

+ 407 - 0
src/views/ticketManagement/newCoupon.vue

@@ -0,0 +1,407 @@
+<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">新建优惠券</h2>
+    </div>
+    <el-form :model="couponModel" ref="ruleFormRef" :rules="rules" label-width="200px" class="formBox">
+      <div class="content">
+        <!-- 左侧内容区域 -->
+        <div class="contentLeft">
+          <!-- 优惠券名称 -->
+          <el-form-item label="优惠券名称" prop="couponName">
+            <el-input maxlength="50" v-model="couponModel.couponName" placeholder="请输入" clearable />
+          </el-form-item>
+          <!-- 面值 -->
+          <el-form-item label="面值(元)" prop="faceValue">
+            <el-input v-model="couponModel.faceValue" maxlength="15" placeholder="请输入" clearable />
+          </el-form-item>
+          <!-- 开始领取时间 -->
+          <el-form-item label="开始领取时间" prop="startCollectionTime">
+            <el-date-picker
+              v-model="couponModel.startCollectionTime"
+              format="YYYY/MM/DD"
+              value-format="YYYY-MM-DD"
+              placeholder="请选择开始领取时间"
+              :disabled-date="disabledStartDate"
+            />
+          </el-form-item>
+          <!-- 结束领取时间 -->
+          <el-form-item label="结束领取时间" prop="endCollectionTime">
+            <el-date-picker
+              v-model="couponModel.endCollectionTime"
+              format="YYYY/MM/DD"
+              value-format="YYYY-MM-DD"
+              placeholder="请选择结束领取时间"
+              :disabled-date="disabledEndDate"
+            />
+          </el-form-item>
+          <!-- 有效期 -->
+          <el-form-item label="有效期" prop="validityPeriod">
+            <el-input v-model="couponModel.validityPeriod" maxlength="15" placeholder="请输入" clearable />
+          </el-form-item>
+          <!-- 库存 -->
+          <el-form-item label="库存" prop="inventory">
+            <el-input v-model="couponModel.inventory" maxlength="15" placeholder="请输入" clearable />
+          </el-form-item>
+          <!-- 用户领取规则 -->
+          <el-form-item label="用户领取规则" prop="collectionRule">
+            <el-radio-group v-model="couponModel.collectionRule" class="ml-4">
+              <el-radio :value="1"> 每日一领 </el-radio>
+              <el-radio :value="2"> 每周一领 </el-radio>
+              <el-radio :value="3"> 每月一领 </el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <!-- 用户是否需要收藏店铺领取 -->
+          <el-form-item label="用户是否需要收藏店铺领取" prop="needFavoriteStore">
+            <el-radio-group v-model="couponModel.needFavoriteStore" class="ml-4">
+              <el-radio :value="1"> 是 </el-radio>
+              <el-radio :value="0"> 否 </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </div>
+        <!-- 右侧内容区域 -->
+        <div class="contentRight">
+          <!-- 是否有低消 -->
+          <el-form-item label="是否有低消" prop="hasMinimumSpend">
+            <el-radio-group v-model="couponModel.hasMinimumSpend" class="ml-4">
+              <el-radio :value="1"> 是 </el-radio>
+              <el-radio :value="0"> 否 </el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <!-- 最低消费金额 -->
+          <el-form-item label="最低消费金额" prop="minimumSpendAmount" v-if="couponModel.hasMinimumSpend === 1">
+            <el-input v-model="couponModel.minimumSpendAmount" maxlength="15" placeholder="请输入" clearable />
+          </el-form-item>
+          <!-- 补充说明 -->
+          <el-form-item label="补充说明" prop="additionalNotes">
+            <el-input
+              maxlength="300"
+              v-model="couponModel.additionalNotes"
+              :rows="4"
+              type="textarea"
+              placeholder="请输入"
+              show-word-limit
+            />
+          </el-form-item>
+        </div>
+      </div>
+    </el-form>
+    <!-- 底部按钮区域 -->
+    <div class="button-container">
+      <el-button @click="() => handleSubmit('draft')"> 存草稿 </el-button>
+      <el-button type="primary" @click="() => handleSubmit()"> 新建优惠券 </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="tsx" name="newCoupon">
+/**
+ * 优惠券管理 - 新增页面
+ * 功能:支持优惠券的新增操作
+ */
+import { ref, reactive, watch, nextTick, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import { useRoute, useRouter } from "vue-router";
+import type { FormInstance } from "element-plus";
+import { getCouponDetail } from "@/api/modules/couponManagement";
+import { validatePositiveNumber, validatePositiveInteger, validateDateRange } from "@/utils/eleValidate";
+// ==================== 响应式数据定义 ====================
+
+// 路由相关
+const router = useRouter();
+const route = useRoute();
+
+// 页面状态
+const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
+const id = ref<string>(""); // 页面ID参数
+// ==================== 表单验证规则 ====================
+const rules = reactive({
+  couponName: [{ required: true, message: "请输入优惠券名称" }],
+  faceValue: [
+    { required: true, message: "请输入面值" },
+    {
+      validator: validatePositiveNumber("面值必须为正数"),
+      trigger: "blur"
+    }
+  ],
+  startCollectionTime: [
+    { required: true, message: "请选择开始领取时间" },
+    {
+      validator: validateDateRange(
+        () => couponModel.value.startCollectionTime,
+        () => couponModel.value.endCollectionTime,
+        "开始领取时间不能早于当前时间",
+        "结束领取时间不能早于当前时间",
+        "开始领取时间必须早于结束领取时间",
+        true
+      ),
+      trigger: "change"
+    }
+  ],
+  endCollectionTime: [
+    { required: true, message: "请选择结束领取时间" },
+    {
+      validator: validateDateRange(
+        () => couponModel.value.startCollectionTime,
+        () => couponModel.value.endCollectionTime,
+        "开始领取时间不能早于当前时间",
+        "结束领取时间不能早于当前时间",
+        "开始领取时间必须早于结束领取时间",
+        true
+      ),
+      trigger: "change"
+    }
+  ],
+  validityPeriod: [
+    { required: true, message: "请输入有效期" },
+    {
+      validator: validatePositiveInteger("有效期必须为正整数", { required: false }),
+      trigger: "blur"
+    }
+  ],
+  inventory: [
+    { required: true, message: "请输入库存" },
+    {
+      validator: validatePositiveInteger("库存必须为正整数", { required: false }),
+      trigger: "blur"
+    }
+  ],
+  minimumSpendAmount: [
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (couponModel.value.hasMinimumSpend === 1) {
+          if (!value || value.toString().trim() === "") {
+            callback(new Error("请输入最低消费金额"));
+            return;
+          }
+          validatePositiveNumber("最低消费金额必须为正数")(rule, value, callback);
+        } else {
+          callback();
+        }
+      },
+      trigger: "blur"
+    }
+  ]
+});
+
+// ==================== 优惠券信息数据模型 ====================
+const couponModel = ref<any>({
+  // 优惠券名称
+  couponName: "",
+  // 面值(元)
+  faceValue: "",
+  // 开始领取时间
+  startCollectionTime: "",
+  // 结束领取时间
+  endCollectionTime: "",
+  // 有效期
+  validityPeriod: "",
+  // 库存
+  inventory: "",
+  // 用户领取规则:1-每日一领,2-每周一领,3-每月一领
+  collectionRule: 1,
+  // 用户是否需要收藏店铺领取:1-是,0-否
+  needFavoriteStore: 1,
+  // 是否有低消:1-是,0-否
+  hasMinimumSpend: 1,
+  // 最低消费金额
+  minimumSpendAmount: "",
+  // 补充说明
+  additionalNotes: ""
+});
+
+// ==================== 监听器 ====================
+
+/**
+ * 监听开始领取时间变化
+ * 当开始时间改变时,重新验证结束时间
+ */
+watch(
+  () => couponModel.value.startCollectionTime,
+  () => {
+    if (couponModel.value.endCollectionTime) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("endCollectionTime");
+      });
+    }
+  }
+);
+
+/**
+ * 监听结束领取时间变化
+ * 当结束时间改变时,重新验证开始时间
+ */
+watch(
+  () => couponModel.value.endCollectionTime,
+  () => {
+    if (couponModel.value.startCollectionTime) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("startCollectionTime");
+      });
+    }
+  }
+);
+
+/**
+ * 监听是否有低消变化
+ * 当选择"否"时,清空最低消费金额
+ */
+watch(
+  () => couponModel.value.hasMinimumSpend,
+  newVal => {
+    if (newVal === 0) {
+      couponModel.value.minimumSpendAmount = "";
+      nextTick(() => {
+        ruleFormRef.value?.clearValidate("minimumSpendAmount");
+      });
+    }
+  }
+);
+
+// ==================== 事件处理函数 ====================
+/**
+ * 组件挂载时初始化
+ * 从路由参数中获取页面类型和ID
+ */
+onMounted(async () => {
+  id.value = (route.query.id as string) || "";
+  type.value = (route.query.type as string) || "";
+  if (type.value != "add") {
+    // TODO: 加载优惠券详情数据
+    // let res: any = await getCouponDetail({ id: id.value });
+  }
+});
+/**
+ * 返回上一页
+ */
+const goBack = () => {
+  router.go(-1);
+};
+
+// ==================== 表单引用 ====================
+const ruleFormRef = ref<FormInstance>(); // 表单引用
+
+/**
+ * 提交数据(新增)
+ * 验证表单,通过后调用相应的API接口
+ */
+const handleSubmit = (submitType?: string) => {
+  // 验证表单
+  ruleFormRef.value!.validate(async (valid: boolean) => {
+    if (!valid) return;
+
+    // 组装提交参数
+    let params: any = { ...couponModel.value };
+
+    console.log("提交参数:", params);
+
+    // TODO: 调用API保存数据
+    // if (submitType === 'draft') {
+    //   await saveCouponDraft(params);
+    // } else {
+    //   await saveCoupon(params);
+    // }
+
+    if (submitType === "draft") {
+      ElMessage.success("草稿保存成功");
+    } else {
+      ElMessage.success("优惠券创建成功");
+      router.go(-1);
+    }
+  });
+};
+
+// ==================== 工具函数 ====================
+
+/**
+ * 禁用开始领取时间的日期
+ * 不能选择早于当前时间的日期
+ */
+const disabledStartDate = (time: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  return time.getTime() < today.getTime();
+};
+
+/**
+ * 禁用结束领取时间的日期
+ * 不能选择早于当前时间的日期,也不能选择早于或等于开始领取时间的日期
+ */
+const disabledEndDate = (time: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  if (time.getTime() < today.getTime()) {
+    return true;
+  }
+  if (couponModel.value.startCollectionTime) {
+    const startDate = new Date(couponModel.value.startCollectionTime);
+    startDate.setHours(0, 0, 0, 0);
+    return time.getTime() <= startDate.getTime();
+  }
+  return false;
+};
+</script>
+
+<style scoped lang="scss">
+/* 页面容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+
+/* 头部区域 */
+.header {
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  border-bottom: 1px solid #e4e7ed;
+}
+.title {
+  flex: 1;
+  margin: 0;
+  font-size: 18px;
+  font-weight: bold;
+  text-align: center;
+}
+
+/* 内容区域布局 */
+.content {
+  display: flex;
+  flex: 1;
+  column-gap: 20px;
+  width: 98%;
+  margin: 20px auto;
+
+  /* 左侧内容区域 */
+  .contentLeft {
+    width: 50%;
+  }
+
+  /* 右侧内容区域 */
+  .contentRight {
+    width: 50%;
+  }
+}
+
+/* 表单容器 */
+.formBox {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  min-height: 100%;
+}
+
+/* 底部按钮容器 - 居中显示 */
+.button-container {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  justify-content: center;
+  padding: 20px 0;
+  margin-top: 20px;
+}
+</style>

+ 1232 - 0
src/views/ticketManagement/newVoucher.vue

@@ -0,0 +1,1232 @@
+<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">新建代金券</h2>
+    </div>
+    <el-form :model="voucherModel" ref="ruleFormRef" :rules="rules" label-width="120px" class="formBox">
+      <div class="content">
+        <!-- 左侧内容区域 -->
+        <div class="contentLeft">
+          <!-- 基础信息模块 -->
+          <div class="model">
+            <h3 style="font-weight: bold">基础信息:</h3>
+            <!-- 代金券名称 -->
+            <el-form-item label="代金券名称" prop="name">
+              <el-input maxlength="20" v-model="voucherModel.name" placeholder="请输入" clearable />
+            </el-form-item>
+            <!-- 抵扣价格 -->
+            <el-form-item label="抵扣价格(¥)" prop="offprice">
+              <el-input v-model="voucherModel.offprice" maxlength="15" placeholder="请输入" clearable />
+            </el-form-item>
+            <!-- 售卖价格 -->
+            <el-form-item label="售卖价格(¥)" prop="price">
+              <el-input v-model="voucherModel.price" maxlength="15" placeholder="请输入" clearable />
+            </el-form-item>
+            <!-- 开始售卖时间 -->
+            <el-form-item label="开始售卖时间" prop="startDate">
+              <el-date-picker
+                v-model="voucherModel.startDate"
+                format="YYYY/MM/DD"
+                value-format="YYYY-MM-DD"
+                placeholder="请选择开始售卖时间"
+                :disabled-date="disabledStartDate"
+              />
+            </el-form-item>
+            <!-- 结束售卖时间 -->
+            <el-form-item label="结束售卖时间" prop="endDate">
+              <el-date-picker
+                v-model="voucherModel.endDate"
+                format="YYYY/MM/DD"
+                value-format="YYYY-MM-DD"
+                placeholder="请选择结束售卖时间"
+                :disabled-date="disabledEndDate"
+              />
+            </el-form-item>
+          </div>
+          <!-- 购买须知模块 -->
+          <div class="model">
+            <h3 style="font-weight: bold">购买须知:</h3>
+            <!-- 使用时间 -->
+            <el-form-item label="使用时间" prop="usageTime">
+              <div class="time-range-container">
+                <el-select v-model="voucherModel.buyUseStartTime" placeholder="开始时间" class="time-picker">
+                  <el-option v-for="hour in hourOptions" :key="hour.value" :label="hour.label" :value="hour.value" />
+                </el-select>
+                <span class="time-separator">至</span>
+                <el-select v-model="voucherModel.buyUseEndTime" placeholder="结束时间" class="time-picker">
+                  <el-option v-for="hour in hourOptions" :key="hour.value" :label="hour.label" :value="hour.value" />
+                </el-select>
+              </div>
+            </el-form-item>
+            <!-- 有效期 -->
+            <el-form-item label="有效期" prop="expirationType">
+              <el-radio-group v-model="voucherModel.expirationType" class="ml-4">
+                <el-radio v-for="status in validityPeriodList" :value="status.value" :key="status.value">
+                  {{ status.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="" prop="expirationDate" v-if="voucherModel.expirationType == 0">
+              <div class="expiration-date-container">
+                <span class="expiration-label">用户购买</span>
+                <el-input-number
+                  v-model="voucherModel.expirationDate"
+                  placeholder="请输入"
+                  :min="0"
+                  :max="10000"
+                  class="expiration-input"
+                />
+                <span class="expiration-label">天内有效</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="" prop="validityPeriod" v-else>
+              <el-date-picker
+                v-model="voucherModel.validityPeriod"
+                type="daterange"
+                value-format="x"
+                range-separator="-"
+                start-placeholder="开始时间"
+                end-placeholder="结束时间"
+                :disabled-date="disabledValidityDate"
+              />
+            </el-form-item>
+            <!-- 不可用日期 -->
+            <el-form-item label="不可用日期" prop="unusedType">
+              <el-radio-group v-model="voucherModel.unusedType" class="ml-4">
+                <el-radio v-for="status in unavailableDateTypeList" :value="status.value" :key="status.value">
+                  {{ status.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <template v-if="voucherModel.unusedType == 1">
+              <el-form-item label="" prop="unavailableWeekdays">
+                <div class="unavailable-dates-container">
+                  <!-- 星期选择 -->
+                  <div class="date-select-section">
+                    <div class="section-title">星期</div>
+                    <div class="button-group">
+                      <el-button
+                        v-for="weekday in weekdayList"
+                        :key="weekday.oName"
+                        :type="voucherModel.unavailableWeekdays?.includes(weekday.oName) ? 'primary' : ''"
+                        class="date-select-btn"
+                        @click="toggleWeekday(weekday.oName)"
+                      >
+                        {{ weekday.name }}
+                      </el-button>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+              <el-form-item label="" prop="unavailableHolidays">
+                <div class="unavailable-dates-container">
+                  <!-- 节日选择 -->
+                  <div class="date-select-section">
+                    <div class="section-title">节日</div>
+                    <div class="button-group">
+                      <el-button
+                        v-for="holiday in holidayList"
+                        :key="holiday.id"
+                        :type="voucherModel.unavailableHolidays?.includes(String(holiday.id)) ? 'primary' : ''"
+                        class="date-select-btn"
+                        @click="toggleHoliday(holiday.id)"
+                      >
+                        {{ holiday.festivalName }}
+                      </el-button>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+            </template>
+            <el-form-item label="" prop="customUnavailableDates" v-else-if="voucherModel.unusedType == 2">
+              <div class="date-picker-container">
+                <el-button :icon="Plus" class="add-date-btn" type="primary" @click="addDate"> 添加日期 </el-button>
+                <div v-for="(item, index) in voucherModel.disableDateList" :key="index" class="date-item">
+                  <el-date-picker
+                    v-model="voucherModel.disableDateList[index]"
+                    type="daterange"
+                    value-format="YYYY-MM-DD"
+                    range-separator="-"
+                    start-placeholder="开始时间"
+                    end-placeholder="结束时间"
+                    class="date-picker"
+                    :disabled-date="disabledCustomUnavailableDate"
+                  />
+                  <el-button
+                    :icon="Delete"
+                    type="danger"
+                    circle
+                    size="small"
+                    class="delete-btn"
+                    @click="removeDate(index)"
+                    v-show="voucherModel.disableDateList.length > 1"
+                  />
+                </div>
+              </div>
+            </el-form-item>
+          </div>
+        </div>
+        <!-- 右侧内容区域 -->
+        <div class="contentRight">
+          <!-- 库存模块 -->
+          <div class="model">
+            <h3 style="font-weight: bold">库存:</h3>
+            <el-form-item label="库存" prop="singleQty">
+              <el-input v-model="voucherModel.singleQty" maxlength="15" placeholder="请输入" clearable />
+            </el-form-item>
+          </div>
+          <!-- 使用规则模块 -->
+          <div class="model">
+            <h3 style="font-weight: bold">使用规则:</h3>
+            <!-- 单次可用数量 -->
+            <el-form-item label="单次可用数量" prop="singleCanUse">
+              <el-input v-model="voucherModel.singleCanUse" maxlength="15" placeholder="请输入" clearable />
+            </el-form-item>
+            <!-- 限购数量 -->
+            <el-form-item label="限购数量" prop="purchaseLimitCode">
+              <el-input v-model="voucherModel.purchaseLimitCode" maxlength="15" placeholder="请输入" clearable />
+            </el-form-item>
+            <!-- 适用范围 -->
+            <el-form-item label="适用范围" prop="applyType">
+              <el-radio-group v-model="voucherModel.applyType" class="ml-4">
+                <el-radio v-for="status in applicableScopeList" :value="status.value" :key="status.value">
+                  {{ status.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="" prop="applyDesc" v-if="voucherModel.applyType == 2">
+              <el-input
+                maxlength="50"
+                v-model="voucherModel.applyDesc"
+                :rows="3"
+                type="textarea"
+                placeholder="请输入"
+                show-word-limit
+              />
+            </el-form-item>
+          </div>
+          <!-- 补充说明模块 -->
+          <div class="model">
+            <h3 style="font-weight: bold">补充说明:</h3>
+            <el-form-item label="补充说明" prop="supplement">
+              <el-input
+                maxlength="300"
+                v-model="voucherModel.supplement"
+                :rows="4"
+                type="textarea"
+                placeholder="请输入"
+                show-word-limit
+              />
+            </el-form-item>
+          </div>
+        </div>
+      </div>
+    </el-form>
+    <!-- 底部按钮区域 -->
+    <div class="button-container">
+      <el-button @click="handleSubmit('draft')"> 存草稿 </el-button>
+      <el-button type="primary" @click="handleSubmit()"> 新建代金券 </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="tsx" name="newVoucher">
+/**
+ * 代金券管理 - 新增/编辑页面
+ * 功能:支持代金券的新增和编辑操作
+ */
+import { ref, reactive, watch, nextTick, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import { Plus, Delete } from "@element-plus/icons-vue";
+import { useRoute, useRouter } from "vue-router";
+import type { FormInstance } from "element-plus";
+import { getVoucherDetail, getHolidayList, addOrUpdateCoupon } from "@/api/modules/voucherManagement";
+import {
+  validatePositiveNumber,
+  validatePositiveInteger,
+  validateDateRange,
+  validateDateRangeArray,
+  validateConditionalRequired,
+  validateArrayMinLength,
+  validateDateListArray,
+  validatePriceFormat,
+  validatePriceComparison
+} from "@/utils/eleValidate";
+import { localGet } from "@/utils";
+
+// ==================== 响应式数据定义 ====================
+
+// 路由相关
+const router = useRouter();
+const route = useRoute();
+
+// 页面状态
+const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
+const id = ref<string>(""); // 页面ID参数
+
+// ==================== 表单验证规则 ====================
+const rules = reactive({
+  name: [{ required: true, message: "请输入代金券名称" }],
+  offprice: [
+    { required: true, message: "请输入抵扣价格" },
+    {
+      validator: validatePositiveNumber("抵扣价格必须为正数"),
+      trigger: "blur"
+    },
+    {
+      validator: validatePriceFormat("整数部分最多6位,小数部分最多2位"),
+      trigger: "blur"
+    },
+    {
+      validator: validatePriceComparison(
+        () => voucherModel.value.offprice,
+        () => voucherModel.value.price,
+        "抵扣价格不能低于售卖价格"
+      ),
+      trigger: "blur"
+    }
+  ],
+  price: [
+    { required: true, message: "请输入售卖价格" },
+    {
+      validator: validatePositiveNumber("售卖价格必须为正数"),
+      trigger: "blur"
+    },
+    {
+      validator: validatePriceFormat("整数部分最多6位,小数部分最多2位"),
+      trigger: "blur"
+    },
+    {
+      validator: validatePriceComparison(
+        () => voucherModel.value.offprice,
+        () => voucherModel.value.price,
+        "抵扣价格不能低于售卖价格"
+      ),
+      trigger: "blur"
+    }
+  ],
+  startDate: [
+    { required: true, message: "请选择开始售卖时间" },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value) {
+          callback();
+          return;
+        }
+        const selectedDate = new Date(value);
+        const today = new Date();
+        today.setHours(0, 0, 0, 0);
+        // 验证不能早于今天
+        if (selectedDate < today) {
+          callback(new Error("开始售卖时间不能早于当前时间"));
+          return;
+        }
+        // 验证开始时间必须早于结束时间
+        const endDate = voucherModel.value.endDate;
+        if (endDate) {
+          const end = new Date(endDate);
+          if (selectedDate >= end) {
+            callback(new Error("开始售卖时间必须早于结束售卖时间"));
+            return;
+          }
+        }
+        callback();
+      },
+      trigger: "change"
+    }
+  ],
+  endDate: [
+    { required: true, message: "请选择结束售卖时间" },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value) {
+          callback();
+          return;
+        }
+        const selectedDate = new Date(value);
+        const today = new Date();
+        today.setHours(0, 0, 0, 0);
+        // 验证不能早于今天
+        if (selectedDate < today) {
+          callback(new Error("结束售卖时间不能早于当前时间"));
+          return;
+        }
+        // 验证结束时间必须晚于开始时间
+        const startDate = voucherModel.value.startDate;
+        if (startDate) {
+          const start = new Date(startDate);
+          if (selectedDate <= start) {
+            callback(new Error("开始售卖时间必须早于结束售卖时间"));
+            return;
+          }
+        }
+        callback();
+      },
+      trigger: "change"
+    }
+  ],
+  usageTime: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (!voucherModel.value.buyUseStartTime || !voucherModel.value.buyUseEndTime) {
+          callback(new Error("请选择使用时间"));
+          return;
+        }
+        callback();
+      },
+      trigger: []
+    }
+  ],
+  expirationType: [{ required: true, message: "请选择有效期类型" }],
+  expirationDate: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (voucherModel.value.expirationType == 0) {
+          if (value === null || value === undefined || value === "") {
+            callback(new Error("请输入用户购买天数"));
+            return;
+          }
+          // 先验证是否为正整数
+          validatePositiveInteger("用户购买天数必须为正整数", { required: false, checkLeadingZero: false })(
+            rule,
+            value,
+            (error: any) => {
+              if (error) {
+                callback(error);
+                return;
+              }
+              // 验证最大值
+              const numValue = Number(value);
+              if (!isNaN(numValue) && numValue > 10000) {
+                callback(new Error("有效期不得大于10000"));
+                return;
+              }
+              callback();
+            }
+          );
+        } else {
+          callback();
+        }
+      },
+      trigger: "blur"
+    }
+  ],
+  validityPeriod: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (voucherModel.value.expirationType == 1) {
+          if (!value || value.length !== 2) {
+            callback(new Error("请选择指定时间段"));
+            return;
+          }
+          validateDateRangeArray("开始时间必须早于结束时间", true, "时间不能早于当前时间")(rule, value, callback);
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ],
+  unusedType: [{ required: true, message: "请选择不可用日期类型" }],
+  unavailableWeekdays: [
+    {
+      validator: validateConditionalRequired(() => voucherModel.value.unusedType == 1, "至少需要选择一个星期"),
+      trigger: "change"
+    }
+  ],
+  unavailableHolidays: [
+    {
+      validator: validateConditionalRequired(() => voucherModel.value.unusedType == 1, "至少需要选择一个节日"),
+      trigger: "change"
+    }
+  ],
+  customUnavailableDates: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (voucherModel.value.unusedType == 2) {
+          if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
+            callback(new Error("至少需要添加一个自定义不可用日期"));
+            return;
+          }
+          validateDateListArray(
+            () => voucherModel.value.disableDateList,
+            "日期项未完整填写",
+            "开始时间必须早于结束时间",
+            true
+          )(rule, value, callback);
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ],
+  singleQty: [
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        // 非必填,如果为空则直接通过
+        if (!value || value.toString().trim() === "") {
+          callback();
+          return;
+        }
+        // 先验证是否为正整数
+        validatePositiveInteger("库存必须为正整数", { required: false })(rule, value, (error: any) => {
+          if (error) {
+            callback(error);
+            return;
+          }
+          // 验证最大值
+          if (value) {
+            const numValue = Number(value);
+            if (!isNaN(numValue) && numValue > 10000) {
+              callback(new Error("库存不得大于10000"));
+              return;
+            }
+          }
+          callback();
+        });
+      },
+      trigger: "blur"
+    }
+  ],
+  singleCanUse: [
+    {
+      validator: validatePositiveInteger("单日可用数量必须为正整数", { required: false }),
+      trigger: "blur"
+    },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        // 如果值为空,不进行验证
+        if (!value || value.toString().trim() === "") {
+          callback();
+          return;
+        }
+        const stock = voucherModel.value.singleQty;
+        // 如果库存为空,不进行验证(由库存的验证规则处理)
+        if (!stock || stock.toString().trim() === "") {
+          callback();
+          return;
+        }
+        const useNum = Number(value);
+        const stockNum = Number(stock);
+        // 如果转换失败,不进行验证(由其他验证规则处理)
+        if (isNaN(useNum) || isNaN(stockNum)) {
+          callback();
+          return;
+        }
+        // 验证单日可用数量不能多于库存
+        if (useNum > stockNum) {
+          callback(new Error("单日可用数量不能多于库存"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  purchaseLimitCode: [
+    {
+      validator: validatePositiveInteger("限购数量必须为正整数", { required: false }),
+      trigger: "blur"
+    },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        // 如果值为空,不进行验证
+        if (!value || value.toString().trim() === "") {
+          callback();
+          return;
+        }
+        const stock = voucherModel.value.singleQty;
+        // 如果库存为空,不进行验证(由库存的验证规则处理)
+        if (!stock || stock.toString().trim() === "") {
+          callback();
+          return;
+        }
+        const limitNum = Number(value);
+        const stockNum = Number(stock);
+        // 如果转换失败,不进行验证(由其他验证规则处理)
+        if (isNaN(limitNum) || isNaN(stockNum)) {
+          callback();
+          return;
+        }
+        // 验证限购数量不能多于库存
+        if (limitNum > stockNum) {
+          callback(new Error("限购数量不能多于库存"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  applyDesc: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (voucherModel.value.applyType == 2) {
+          if (!value || value.toString().trim() === "") {
+            callback(new Error("请输入适用范围"));
+            return;
+          }
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ]
+});
+
+// ==================== 代金券信息数据模型 ====================
+const voucherModel = ref<any>({
+  // 代金券名称
+  name: "",
+  // 抵扣价格
+  offprice: "",
+  // 售卖价格
+  price: "",
+  // 开始售卖时间
+  startDate: "",
+  // 结束售卖时间
+  endDate: "",
+  // 使用时间 - 开始时间
+  buyUseStartTime: "",
+  // 使用时间 - 结束时间
+  buyUseEndTime: "",
+  // 使用时间(虚拟字段,用于表单验证)
+  usageTime: null,
+  // 有效期类型:0-指定天数,1-指定时间段内可用
+  expirationType: "0",
+  // 有效期天数(当expirationType为0时使用)
+  expirationDate: 0,
+  // 有效期时间段(当expirationType为1时使用)
+  validityPeriod: [],
+  // 不可用日期类型:0-全部日期可用,1-限制日期
+  unusedType: "0",
+  // 限制日期 - 星期选择(数组,存储选中的星期值)
+  unavailableWeekdays: [],
+  // 限制日期 - 节日选择(数组,存储选中的节日值)
+  unavailableHolidays: [],
+  // 自定义不可用日期列表(数组,每个元素是一个日期范围 [startDate, endDate])
+  disableDateList: [],
+  // 库存
+  singleQty: "",
+  // 单日可用数量
+  singleCanUse: "",
+  // 限购数量
+  purchaseLimitCode: "",
+  // 适用范围类型:0-全场通用,1-部分不可用
+  applyType: "1",
+  // 适用范围(当applyType为1时使用)
+  applyDesc: "",
+  // 补充说明
+  supplement: ""
+});
+
+// ==================== 下拉选项数据 ====================
+
+// 小时选项列表(0-23点)
+const hourOptions = ref(
+  Array.from({ length: 24 }, (_, i) => ({
+    value: String(i),
+    label: `${i}点`
+  }))
+);
+
+// 有效期类型列表
+const validityPeriodList = ref([
+  { value: "0", label: "指定天数" },
+  { value: "1", label: "指定时间段内可用" }
+]);
+
+// 不可用日期类型列表
+const unavailableDateTypeList = ref([
+  { value: "0", label: "全部日期可用" },
+  { value: "1", label: "限制日期" }
+  // { value: '2', label: "自定义不可用日期" }
+]);
+
+// 适用范围类型列表
+const applicableScopeList = ref([
+  { value: "1", label: "全场通用" },
+  { value: "2", label: "部分不可用" }
+]);
+
+// 星期选项列表
+const weekdayList = ref([
+  { name: "周一", id: "0", oName: "星期一" },
+  { name: "周二", id: "1", oName: "星期二" },
+  { name: "周三", id: "2", oName: "星期三" },
+  { name: "周四", id: "3", oName: "星期四" },
+  { name: "周五", id: "4", oName: "星期五" },
+  { name: "周六", id: "5", oName: "星期六" },
+  { name: "周日", id: "6", oName: "星期日" }
+]);
+
+// 节日选项列表
+const holidayList: any = ref([]);
+
+// ==================== 监听器 ====================
+
+/**
+ * 监听开始售卖时间变化
+ * 当开始时间改变时,重新验证结束时间
+ */
+watch(
+  () => voucherModel.value.startDate,
+  () => {
+    if (voucherModel.value.endDate) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("endDate");
+      });
+    }
+  }
+);
+
+/**
+ * 监听结束售卖时间变化
+ * 当结束时间改变时,重新验证开始时间
+ */
+watch(
+  () => voucherModel.value.endDate,
+  () => {
+    if (voucherModel.value.startDate) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("startDate");
+      });
+    }
+  }
+);
+
+/**
+ * 监听抵扣价格变化
+ * 当抵扣价格改变时,重新验证售卖价格
+ */
+watch(
+  () => voucherModel.value.offprice,
+  () => {
+    if (voucherModel.value.price) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("price");
+      });
+    }
+  }
+);
+
+/**
+ * 监听售卖价格变化
+ * 当售卖价格改变时,重新验证抵扣价格
+ */
+watch(
+  () => voucherModel.value.price,
+  () => {
+    if (voucherModel.value.offprice) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("offprice");
+      });
+    }
+  }
+);
+
+/**
+ * 监听库存变化
+ * 当库存改变时,重新验证单日可用数量和限购数量
+ */
+watch(
+  () => voucherModel.value.singleQty,
+  () => {
+    if (voucherModel.value.singleCanUse) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("singleCanUse");
+      });
+    }
+    if (voucherModel.value.purchaseLimitCode) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("purchaseLimitCode");
+      });
+    }
+  }
+);
+
+/**
+ * 监听使用开始时间变化
+ * 更新虚拟字段以支持表单验证
+ */
+watch(
+  () => voucherModel.value.buyUseStartTime,
+  () => {
+    // 更新虚拟字段
+    voucherModel.value.usageTime =
+      voucherModel.value.buyUseStartTime && voucherModel.value.buyUseEndTime
+        ? [voucherModel.value.buyUseStartTime, voucherModel.value.buyUseEndTime]
+        : null;
+  }
+);
+
+/**
+ * 监听使用结束时间变化
+ * 更新虚拟字段以支持表单验证
+ */
+watch(
+  () => voucherModel.value.buyUseEndTime,
+  () => {
+    // 更新虚拟字段
+    voucherModel.value.usageTime =
+      voucherModel.value.buyUseStartTime && voucherModel.value.buyUseEndTime
+        ? [voucherModel.value.buyUseStartTime, voucherModel.value.buyUseEndTime]
+        : null;
+  }
+);
+
+/**
+ * 监听不可用日期类型变化
+ * 当切换到自定义不可用日期时,确保至少有一个日期项
+ */
+watch(
+  () => voucherModel.value.unusedType,
+  newVal => {
+    if (newVal == 2) {
+      // 切换到自定义不可用日期时,如果disableDateList为空,则添加一个默认项
+      if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
+        voucherModel.value.disableDateList = [null];
+      }
+    }
+  },
+  { immediate: true }
+);
+
+// ==================== 生命周期钩子 ====================
+
+/**
+ * 组件挂载时初始化
+ * 从路由参数中获取页面类型和ID
+ */
+onMounted(async () => {
+  id.value = (route.query.id as string) || "";
+  type.value = (route.query.type as string) || "";
+
+  // 加载节日列表
+  let params = {
+    year: new Date().getFullYear(),
+    page: 1,
+    size: 500,
+    openFlag: 1,
+    holidayName: ""
+  };
+  let res: any = await getHolidayList(params);
+  if (res && res.code == 200) {
+    holidayList.value = res.data.records;
+  }
+
+  // 编辑模式下加载数据
+  if (type.value != "add") {
+    let res: any = await getVoucherDetail({ id: id.value });
+    voucherModel.value = { ...voucherModel.value, ...res.data };
+    // 处理有效期时间段:将时间戳字符串转换为数字数组
+    if (voucherModel.value.validityPeriod && voucherModel.value.expirationType == 1) {
+      const periodArray = voucherModel.value.validityPeriod.split(",");
+      voucherModel.value.validityPeriod = periodArray
+        .map((item: string) => Number(item.trim()))
+        .filter((item: number) => !isNaN(item));
+    } else {
+      voucherModel.value.validityPeriod = [];
+    }
+    // 确保星期和节日字段存在;
+    if (voucherModel.value.unusedType == 1) {
+      const listVal = voucherModel.value.unusedDate ? voucherModel.value.unusedDate.split(";") : [];
+      voucherModel.value.unavailableWeekdays = listVal[0] ? listVal[0].split(",").filter((item: string) => item) : [];
+      voucherModel.value.unavailableHolidays = listVal[1] ? listVal[1].split(",").filter((item: string) => item) : [];
+    }
+    // 确保自定义不可用日期字段存在;
+    if (voucherModel.value.unusedType === 2) {
+      if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
+        voucherModel.value.disableDateList = [null];
+      }
+    }
+    console.log(voucherModel.value);
+  }
+});
+
+// ==================== 事件处理函数 ====================
+
+/**
+ * 返回上一页
+ */
+const goBack = () => {
+  router.go(-1);
+};
+
+/**
+ * 切换星期选择
+ * @param value 星期值
+ */
+const toggleWeekday = (value: string) => {
+  if (!voucherModel.value.unavailableWeekdays) {
+    voucherModel.value.unavailableWeekdays = [];
+  }
+  const index = voucherModel.value.unavailableWeekdays.indexOf(value);
+  if (index > -1) {
+    voucherModel.value.unavailableWeekdays.splice(index, 1);
+  } else {
+    voucherModel.value.unavailableWeekdays.push(value);
+  }
+  // 触发表单验证
+  nextTick(() => {
+    ruleFormRef.value?.validateField("unavailableWeekdays");
+  });
+};
+
+/**
+ * 切换节日选择
+ * @param value 节日值
+ */
+const toggleHoliday = (value: string | number) => {
+  if (!voucherModel.value.unavailableHolidays) {
+    voucherModel.value.unavailableHolidays = [];
+  }
+  // 统一转换为字符串进行比较
+  const valueStr = String(value);
+  const index = voucherModel.value.unavailableHolidays.findIndex((item: any) => String(item) === valueStr);
+  if (index > -1) {
+    voucherModel.value.unavailableHolidays.splice(index, 1);
+  } else {
+    voucherModel.value.unavailableHolidays.push(valueStr);
+  }
+  // 触发表单验证
+  nextTick(() => {
+    ruleFormRef.value?.validateField("unavailableHolidays");
+  });
+};
+
+/**
+ * 添加自定义不可用日期
+ */
+const addDate = () => {
+  if (!voucherModel.value.disableDateList) {
+    voucherModel.value.disableDateList = [];
+  }
+  voucherModel.value.disableDateList.push(null);
+};
+
+/**
+ * 删除自定义不可用日期
+ * @param index 要删除的日期索引
+ */
+const removeDate = (index: number) => {
+  if (voucherModel.value.disableDateList.length <= 1) {
+    ElMessage.warning("至少需要保留一个日期项");
+    return;
+  }
+  voucherModel.value.disableDateList.splice(index, 1);
+  // 删除日期项后,重新验证表单以清除旧的验证错误
+  nextTick(() => {
+    ruleFormRef.value?.validateField("customUnavailableDates");
+  });
+};
+
+// ==================== 表单引用 ====================
+const ruleFormRef = ref<FormInstance>(); // 表单引用
+
+/**
+ * 提交数据(新增/编辑)
+ * 验证表单,通过后调用相应的API接口
+ */
+const handleSubmit = async (submitType?: string) => {
+  // 组装提交参数
+  let params: any = { ...voucherModel.value };
+  params.storeId = localGet("createdId");
+  params.status = 1;
+  // 处理有效期:只有当expirationType为1(指定时间段内可用)时才处理validityPeriod
+  if (params.expirationType == 1 && params.validityPeriod && Array.isArray(params.validityPeriod)) {
+    params.validityPeriod = params.validityPeriod.join(",");
+  } else if (params.expirationType == 0) {
+    // 指定天数模式,不需要validityPeriod字段
+    params.validityPeriod = "";
+  }
+  // 处理不可用日期
+  if (params.unusedType == 1) {
+    params.unusedDate = params.unavailableWeekdays.join(",") + ";" + params.unavailableHolidays.join(",");
+  } else if (params.unusedType == 2) {
+    // 处理自定义不可用日期
+    if (params.disableDateList && params.disableDateList.length > 0) {
+      params.unusedDate = params.disableDateList
+        .map((dateRange: any) => (dateRange && dateRange.length === 2 ? dateRange.join(",") : ""))
+        .filter((item: string) => item)
+        .join(";");
+    }
+  }
+  params.dataType = submitType ? 1 : 0;
+  delete params.unavailableWeekdays;
+  delete params.unavailableHolidays;
+  delete params.disableDateList;
+  console.log("提交参数:", params);
+  if (submitType) {
+    if (!voucherModel.value.name) {
+      ElMessage.warning("请填写代金券名称");
+      return;
+    }
+    let res: any = await addOrUpdateCoupon(params);
+    if (res && res.code == 200) {
+      ElMessage.success("保存成功");
+      goBack();
+    }
+    return;
+  }
+  // 验证表单
+  ruleFormRef.value!.validate(async (valid: boolean) => {
+    if (!valid) return;
+    let res: any = await addOrUpdateCoupon(params);
+    if (res && res.code == 200) {
+      ElMessage.success("创建成功,请耐心等待审核");
+      goBack();
+    }
+  });
+};
+
+// ==================== 工具函数 ====================
+
+/**
+ * 禁用开始售卖时间的日期
+ * 不能选择早于当前时间的日期
+ */
+const disabledStartDate = (time: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  return time.getTime() < today.getTime();
+};
+
+/**
+ * 禁用结束售卖时间的日期
+ * 不能选择早于当前时间的日期,也不能选择早于或等于开始售卖时间的日期
+ */
+const disabledEndDate = (time: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  if (time.getTime() < today.getTime()) {
+    return true;
+  }
+  if (voucherModel.value.startDate) {
+    const startDate = new Date(voucherModel.value.startDate);
+    startDate.setHours(0, 0, 0, 0);
+    return time.getTime() <= startDate.getTime();
+  }
+  return false;
+};
+
+/**
+ * 禁用有效期时间段的日期
+ * 不能选择早于当前时间的日期
+ * @param time 日期对象
+ * @returns 是否禁用该日期
+ */
+const disabledValidityDate = (time: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  return time.getTime() < today.getTime();
+};
+
+/**
+ * 禁用自定义不可用日期的日期
+ * 不能选择早于当前时间的日期
+ * @param time 日期对象
+ * @returns 是否禁用该日期
+ */
+const disabledCustomUnavailableDate = (time: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  return time.getTime() < today.getTime();
+};
+</script>
+
+<style scoped lang="scss">
+/* 页面容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+
+/* 头部区域 */
+.header {
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  border-bottom: 1px solid #e4e7ed;
+}
+.title {
+  flex: 1;
+  margin: 0;
+  font-size: 18px;
+  font-weight: bold;
+  text-align: center;
+}
+
+/* 内容区域布局 */
+.content {
+  display: flex;
+  flex: 1;
+  column-gap: 20px;
+  width: 98%;
+  margin: 20px auto;
+
+  /* 左侧内容区域 */
+  .contentLeft {
+    width: 50%;
+  }
+
+  /* 右侧内容区域 */
+  .contentRight {
+    width: 50%;
+  }
+}
+
+/* 模块容器 */
+.model {
+  margin-bottom: 50px;
+}
+
+/* 表单容器 */
+.formBox {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  min-height: 100%;
+}
+
+/* 底部按钮容器 - 居中显示 */
+.button-container {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  justify-content: center;
+  padding: 20px 0;
+  margin-top: 20px;
+}
+
+/* 有效期天数容器 */
+.expiration-date-container {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  width: fit-content;
+  padding: 8px 12px;
+  border-radius: 4px;
+}
+
+/* 有效期标签文字 */
+.expiration-label {
+  font-size: 14px;
+  color: #606266;
+  white-space: nowrap;
+}
+
+/* 有效期输入框 */
+.expiration-input {
+  width: 150px;
+}
+
+/* 不可用日期容器 */
+.unavailable-dates-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  width: 100%;
+}
+
+/* 日期选择区块 */
+.date-select-section {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+/* 区块标题 */
+.section-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+/* 按钮组 */
+.button-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+/* 日期选择按钮 */
+.date-select-btn {
+  min-width: 80px;
+  height: 36px;
+  padding: 8px 16px;
+  margin: 0;
+  font-size: 14px;
+  border-radius: 4px;
+  transition: all 0.1s;
+}
+
+/* 日期选择器容器 */
+.date-picker-container {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: 100%;
+}
+
+/* 添加日期按钮 */
+.add-date-btn {
+  width: fit-content;
+  margin-bottom: 8px;
+}
+
+/* 日期项容器 */
+.date-item {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  padding: 8px;
+  border-radius: 4px;
+  transition: background-color 0.1s;
+  &:hover {
+    background-color: #ecf5ff;
+  }
+}
+
+/* 日期选择器 */
+.date-item .date-picker {
+  flex: 1;
+  max-width: 500px;
+}
+
+/* 删除按钮 */
+.date-item .delete-btn {
+  flex-shrink: 0;
+}
+
+/* 时间范围容器 */
+.time-range-container {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  width: 100%;
+}
+
+/* 时间选择器 */
+.time-picker {
+  flex: 1;
+  max-width: 200px;
+}
+
+/* 时间分隔符 */
+.time-separator {
+  font-size: 14px;
+  color: #606266;
+  white-space: nowrap;
+}
+</style>