newVoucher.vue 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266
  1. <template>
  2. <!-- 代金券管理 - 新增/编辑页面 -->
  3. <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
  4. <div class="header">
  5. <el-button @click="goBack"> 返回 </el-button>
  6. <h2 class="title">{{ type == "add" ? "新建" : "编辑" }}代金券</h2>
  7. </div>
  8. <el-form :model="voucherModel" ref="ruleFormRef" :rules="rules" label-width="120px" class="formBox">
  9. <div class="content">
  10. <!-- 左侧内容区域 -->
  11. <div class="contentLeft">
  12. <!-- 基础信息模块 -->
  13. <div class="model">
  14. <h3 style="font-weight: bold">基础信息:</h3>
  15. <!-- 代金券名称 -->
  16. <el-form-item label="代金券名称" prop="name">
  17. <el-input maxlength="20" v-model="voucherModel.name" placeholder="请输入" clearable />
  18. </el-form-item>
  19. <!-- 抵扣价格 -->
  20. <el-form-item label="抵扣价格(¥)" prop="price">
  21. <el-input v-model="voucherModel.price" maxlength="15" placeholder="请输入" clearable />
  22. </el-form-item>
  23. <!-- 售卖价格 -->
  24. <el-form-item label="售卖价格(¥)" prop="offprice">
  25. <el-input v-model="voucherModel.offprice" maxlength="15" placeholder="请输入" clearable />
  26. </el-form-item>
  27. <!-- 开始售卖时间 -->
  28. <el-form-item label="开始售卖时间" prop="startDate">
  29. <el-date-picker
  30. v-model="voucherModel.startDate"
  31. format="YYYY/MM/DD"
  32. value-format="YYYY-MM-DD"
  33. placeholder="请选择开始售卖时间"
  34. :disabled-date="disabledStartDate"
  35. />
  36. </el-form-item>
  37. <!-- 结束售卖时间 -->
  38. <el-form-item label="结束售卖时间" prop="endDate">
  39. <el-date-picker
  40. v-model="voucherModel.endDate"
  41. format="YYYY/MM/DD"
  42. value-format="YYYY-MM-DD"
  43. placeholder="请选择结束售卖时间"
  44. :disabled-date="disabledEndDate"
  45. />
  46. </el-form-item>
  47. </div>
  48. <!-- 购买须知模块 -->
  49. <div class="model">
  50. <h3 style="font-weight: bold">购买须知:</h3>
  51. <!-- 使用时间 -->
  52. <el-form-item label="使用时间" prop="usageTime">
  53. <div class="time-range-container">
  54. <el-select v-model="voucherModel.buyUseStartTime" placeholder="开始时间" class="time-picker">
  55. <el-option v-for="hour in hourOptions" :key="hour.value" :label="hour.label" :value="hour.value" />
  56. </el-select>
  57. <span class="time-separator">至</span>
  58. <el-select v-model="voucherModel.buyUseEndTime" placeholder="结束时间" class="time-picker">
  59. <el-option v-for="hour in hourOptions" :key="hour.value" :label="hour.label" :value="hour.value" />
  60. </el-select>
  61. </div>
  62. </el-form-item>
  63. <!-- 有效期 -->
  64. <el-form-item label="有效期" prop="expirationType">
  65. <el-radio-group v-model="voucherModel.expirationType" class="ml-4">
  66. <el-radio v-for="status in validityPeriodList" :value="status.value" :key="status.value">
  67. {{ status.label }}
  68. </el-radio>
  69. </el-radio-group>
  70. </el-form-item>
  71. <el-form-item label="" prop="expirationDate" v-if="voucherModel.expirationType == 1">
  72. <div class="expiration-date-container">
  73. <span class="expiration-label">用户购买</span>
  74. <el-input-number
  75. v-model="voucherModel.expirationDate"
  76. placeholder="请输入"
  77. :min="0"
  78. :max="10000"
  79. class="expiration-input"
  80. />
  81. <span class="expiration-label">天内有效</span>
  82. </div>
  83. </el-form-item>
  84. <el-form-item label="" prop="validityPeriod" v-else>
  85. <el-date-picker
  86. v-model="voucherModel.validityPeriod"
  87. type="daterange"
  88. value-format="x"
  89. range-separator="-"
  90. start-placeholder="开始时间"
  91. end-placeholder="结束时间"
  92. :disabled-date="disabledValidityDate"
  93. />
  94. </el-form-item>
  95. <!-- 不可用日期 -->
  96. <el-form-item label="不可用日期" prop="unusedType">
  97. <el-radio-group v-model="voucherModel.unusedType" class="ml-4">
  98. <el-radio v-for="status in unavailableDateTypeList" :value="status.value" :key="status.value">
  99. {{ status.label }}
  100. </el-radio>
  101. </el-radio-group>
  102. </el-form-item>
  103. <template v-if="voucherModel.unusedType == 2">
  104. <el-form-item label="" prop="unavailableWeekdays">
  105. <div class="unavailable-dates-container">
  106. <!-- 星期选择 -->
  107. <div class="date-select-section">
  108. <div class="section-title">星期</div>
  109. <div class="button-group">
  110. <el-button
  111. v-for="weekday in weekdayList"
  112. :key="weekday.oName"
  113. :type="voucherModel.unavailableWeekdays?.includes(weekday.oName) ? 'primary' : ''"
  114. class="date-select-btn"
  115. @click="toggleWeekday(weekday.oName)"
  116. >
  117. {{ weekday.name }}
  118. </el-button>
  119. </div>
  120. </div>
  121. </div>
  122. </el-form-item>
  123. <el-form-item label="" prop="unavailableHolidays">
  124. <div class="unavailable-dates-container">
  125. <!-- 节日选择 -->
  126. <div class="date-select-section">
  127. <div class="section-title">节日</div>
  128. <div class="button-group">
  129. <el-button
  130. v-for="holiday in holidayList"
  131. :key="holiday.id"
  132. :type="voucherModel.unavailableHolidays?.includes(String(holiday.id)) ? 'primary' : ''"
  133. class="date-select-btn"
  134. @click="toggleHoliday(holiday.id)"
  135. >
  136. {{ holiday.festivalName }}
  137. </el-button>
  138. </div>
  139. </div>
  140. </div>
  141. </el-form-item>
  142. </template>
  143. <el-form-item label="" prop="customUnavailableDates" v-else-if="voucherModel.unusedType == 3">
  144. <div class="date-picker-container">
  145. <el-button :icon="Plus" class="add-date-btn" type="primary" @click="addDate"> 添加日期 </el-button>
  146. <div v-for="(item, index) in voucherModel.disableDateList" :key="index" class="date-item">
  147. <el-date-picker
  148. v-model="voucherModel.disableDateList[index]"
  149. type="daterange"
  150. value-format="YYYY-MM-DD"
  151. range-separator="-"
  152. start-placeholder="开始时间"
  153. end-placeholder="结束时间"
  154. class="date-picker"
  155. :disabled-date="disabledCustomUnavailableDate"
  156. />
  157. <el-button
  158. :icon="Delete"
  159. type="danger"
  160. circle
  161. size="small"
  162. class="delete-btn"
  163. @click="removeDate(index)"
  164. v-show="voucherModel.disableDateList.length > 1"
  165. />
  166. </div>
  167. </div>
  168. </el-form-item>
  169. </div>
  170. </div>
  171. <!-- 右侧内容区域 -->
  172. <div class="contentRight">
  173. <!-- 库存模块 -->
  174. <div class="model">
  175. <h3 style="font-weight: bold">库存:</h3>
  176. <el-form-item label="库存" prop="singleQty">
  177. <el-input v-model="voucherModel.singleQty" maxlength="15" placeholder="请输入" clearable />
  178. </el-form-item>
  179. </div>
  180. <!-- 使用规则模块 -->
  181. <div class="model">
  182. <h3 style="font-weight: bold">使用规则:</h3>
  183. <!-- 单次可用数量 -->
  184. <el-form-item label="单次可用数量" prop="singleCanUse">
  185. <el-input v-model="voucherModel.singleCanUse" maxlength="15" placeholder="请输入" clearable />
  186. </el-form-item>
  187. <!-- 限购数量 -->
  188. <el-form-item label="限购数量" prop="purchaseLimitCode">
  189. <el-input v-model="voucherModel.purchaseLimitCode" maxlength="15" placeholder="请输入" clearable />
  190. </el-form-item>
  191. <!-- 适用范围 -->
  192. <el-form-item label="适用范围" prop="applyType">
  193. <el-radio-group v-model="voucherModel.applyType" class="ml-4">
  194. <el-radio v-for="status in applicableScopeList" :value="status.value" :key="status.value">
  195. {{ status.label }}
  196. </el-radio>
  197. </el-radio-group>
  198. </el-form-item>
  199. <el-form-item label="" prop="applyDesc" v-if="voucherModel.applyType == 2">
  200. <el-input
  201. maxlength="50"
  202. v-model="voucherModel.applyDesc"
  203. :rows="3"
  204. type="textarea"
  205. placeholder="请输入"
  206. show-word-limit
  207. />
  208. </el-form-item>
  209. </div>
  210. <!-- 补充说明模块 -->
  211. <div class="model">
  212. <h3 style="font-weight: bold">补充说明:</h3>
  213. <el-form-item label="补充说明" prop="supplement">
  214. <el-input
  215. maxlength="300"
  216. v-model="voucherModel.supplement"
  217. :rows="4"
  218. type="textarea"
  219. placeholder="请输入"
  220. show-word-limit
  221. />
  222. </el-form-item>
  223. </div>
  224. </div>
  225. </div>
  226. </el-form>
  227. <!-- 底部按钮区域 -->
  228. <div class="button-container">
  229. <el-button @click="handleSubmit('draft')"> 存草稿 </el-button>
  230. <el-button type="primary" @click="handleSubmit()"> {{ type == "add" ? "新建" : "编辑" }}代金券 </el-button>
  231. </div>
  232. </div>
  233. </template>
  234. <script setup lang="tsx" name="newVoucher">
  235. /**
  236. * 代金券管理 - 新增/编辑页面
  237. * 功能:支持代金券的新增和编辑操作
  238. */
  239. import { ref, reactive, watch, nextTick, onMounted } from "vue";
  240. import { ElMessage } from "element-plus";
  241. import { Plus, Delete } from "@element-plus/icons-vue";
  242. import { useRoute, useRouter } from "vue-router";
  243. import type { FormInstance } from "element-plus";
  244. import { getVoucherDetail, getHolidayList, addOrUpdateCoupon } from "@/api/modules/voucherManagement";
  245. import {
  246. validatePositiveNumber,
  247. validatePositiveInteger,
  248. validateDateRange,
  249. validateDateRangeArray,
  250. validateConditionalRequired,
  251. validateArrayMinLength,
  252. validateDateListArray,
  253. validatePriceFormat,
  254. validatePriceComparison
  255. } from "@/utils/eleValidate";
  256. import { localGet } from "@/utils";
  257. // ==================== 响应式数据定义 ====================
  258. // 路由相关
  259. const router = useRouter();
  260. const route = useRoute();
  261. // 页面状态
  262. const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
  263. const id = ref<string>(""); // 页面ID参数
  264. // ==================== 表单验证规则 ====================
  265. const rules = reactive({
  266. name: [{ required: true, message: "请输入代金券名称" }],
  267. price: [
  268. { required: true, message: "请输入抵扣价格" },
  269. {
  270. validator: validatePositiveNumber("抵扣价格必须为正数"),
  271. trigger: "blur"
  272. },
  273. {
  274. validator: validatePriceFormat("整数部分最多6位,小数部分最多2位"),
  275. trigger: "blur"
  276. },
  277. {
  278. validator: validatePriceComparison(
  279. () => voucherModel.value.price,
  280. () => voucherModel.value.offprice,
  281. "抵扣价格不能低于售卖价格"
  282. ),
  283. trigger: "blur"
  284. }
  285. ],
  286. offprice: [
  287. { required: true, message: "请输入售卖价格" },
  288. {
  289. validator: validatePositiveNumber("售卖价格必须为正数"),
  290. trigger: "blur"
  291. },
  292. {
  293. validator: validatePriceFormat("整数部分最多6位,小数部分最多2位"),
  294. trigger: "blur"
  295. },
  296. {
  297. validator: validatePriceComparison(
  298. () => voucherModel.value.price,
  299. () => voucherModel.value.offprice,
  300. "抵扣价格不能低于售卖价格"
  301. ),
  302. trigger: "blur"
  303. }
  304. ],
  305. startDate: [
  306. { required: true, message: "请选择开始售卖时间" },
  307. {
  308. validator: (rule: any, value: any, callback: any) => {
  309. if (!value) {
  310. callback();
  311. return;
  312. }
  313. const selectedDate = new Date(value);
  314. const today = new Date();
  315. today.setHours(0, 0, 0, 0);
  316. // 验证不能早于今天
  317. if (selectedDate < today) {
  318. callback(new Error("开始售卖时间不能早于当前时间"));
  319. return;
  320. }
  321. // 验证开始时间必须早于结束时间
  322. const endDate = voucherModel.value.endDate;
  323. if (endDate) {
  324. const end = new Date(endDate);
  325. if (selectedDate >= end) {
  326. callback(new Error("开始售卖时间必须早于结束售卖时间"));
  327. return;
  328. }
  329. }
  330. callback();
  331. },
  332. trigger: "change"
  333. }
  334. ],
  335. endDate: [
  336. { required: true, message: "请选择结束售卖时间" },
  337. {
  338. validator: (rule: any, value: any, callback: any) => {
  339. if (!value) {
  340. callback();
  341. return;
  342. }
  343. const selectedDate = new Date(value);
  344. const today = new Date();
  345. today.setHours(0, 0, 0, 0);
  346. // 验证不能早于今天
  347. if (selectedDate < today) {
  348. callback(new Error("结束售卖时间不能早于当前时间"));
  349. return;
  350. }
  351. // 验证结束时间必须晚于开始时间
  352. const startDate = voucherModel.value.startDate;
  353. if (startDate) {
  354. const start = new Date(startDate);
  355. if (selectedDate <= start) {
  356. callback(new Error("开始售卖时间必须早于结束售卖时间"));
  357. return;
  358. }
  359. }
  360. callback();
  361. },
  362. trigger: "change"
  363. }
  364. ],
  365. usageTime: [
  366. {
  367. required: true,
  368. validator: (rule: any, value: any, callback: any) => {
  369. if (!voucherModel.value.buyUseStartTime || !voucherModel.value.buyUseEndTime) {
  370. callback(new Error("请选择使用时间"));
  371. return;
  372. }
  373. callback();
  374. },
  375. trigger: []
  376. }
  377. ],
  378. expirationType: [{ required: true, message: "请选择有效期类型" }],
  379. expirationDate: [
  380. {
  381. required: true,
  382. validator: (rule: any, value: any, callback: any) => {
  383. if (voucherModel.value.expirationType == 1) {
  384. if (value === null || value === undefined || value === "") {
  385. callback(new Error("请输入用户购买天数"));
  386. return;
  387. }
  388. // 先验证是否为正整数
  389. validatePositiveInteger("用户购买天数必须为正整数", { required: false, checkLeadingZero: false })(
  390. rule,
  391. value,
  392. (error: any) => {
  393. if (error) {
  394. callback(error);
  395. return;
  396. }
  397. // 验证最大值
  398. const numValue = Number(value);
  399. if (!isNaN(numValue) && numValue > 10000) {
  400. callback(new Error("有效期不得大于10000"));
  401. return;
  402. }
  403. callback();
  404. }
  405. );
  406. } else {
  407. callback();
  408. }
  409. },
  410. trigger: "blur"
  411. }
  412. ],
  413. validityPeriod: [
  414. {
  415. required: true,
  416. validator: (rule: any, value: any, callback: any) => {
  417. if (voucherModel.value.expirationType == 2) {
  418. if (!value || value.length !== 2) {
  419. callback(new Error("请选择指定时间段"));
  420. return;
  421. }
  422. validateDateRangeArray("开始时间必须早于结束时间", true, "时间不能早于当前时间")(rule, value, callback);
  423. } else {
  424. callback();
  425. }
  426. },
  427. trigger: "change"
  428. }
  429. ],
  430. unusedType: [{ required: true, message: "请选择不可用日期类型" }],
  431. unavailableWeekdays: [
  432. {
  433. validator: (rule: any, value: any, callback: any) => {
  434. if (voucherModel.value.unusedType == 2) {
  435. const weekdays = voucherModel.value.unavailableWeekdays || [];
  436. const holidays = voucherModel.value.unavailableHolidays || [];
  437. if (weekdays.length === 0 && holidays.length === 0) {
  438. callback(new Error("至少需要选择一个星期或节日"));
  439. return;
  440. }
  441. }
  442. callback();
  443. },
  444. trigger: "change"
  445. }
  446. ],
  447. unavailableHolidays: [
  448. {
  449. validator: (rule: any, value: any, callback: any) => {
  450. if (voucherModel.value.unusedType == 2) {
  451. const weekdays = voucherModel.value.unavailableWeekdays || [];
  452. const holidays = voucherModel.value.unavailableHolidays || [];
  453. if (weekdays.length === 0 && holidays.length === 0) {
  454. callback(new Error("至少需要选择一个星期或节日"));
  455. return;
  456. }
  457. }
  458. callback();
  459. },
  460. trigger: "change"
  461. }
  462. ],
  463. customUnavailableDates: [
  464. {
  465. required: true,
  466. validator: (rule: any, value: any, callback: any) => {
  467. if (voucherModel.value.unusedType == 3) {
  468. if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
  469. callback(new Error("至少需要添加一个自定义不可用日期"));
  470. return;
  471. }
  472. validateDateListArray(
  473. () => voucherModel.value.disableDateList,
  474. "日期项未完整填写",
  475. "开始时间必须早于结束时间",
  476. true
  477. )(rule, value, callback);
  478. } else {
  479. callback();
  480. }
  481. },
  482. trigger: "change"
  483. }
  484. ],
  485. singleQty: [
  486. {
  487. validator: (rule: any, value: any, callback: any) => {
  488. // 非必填,如果为空则直接通过
  489. if (!value || value.toString().trim() === "") {
  490. callback();
  491. return;
  492. }
  493. // 先验证是否为正整数
  494. validatePositiveInteger("库存必须为正整数", { required: false })(rule, value, (error: any) => {
  495. if (error) {
  496. callback(error);
  497. return;
  498. }
  499. // 验证最大值
  500. if (value) {
  501. const numValue = Number(value);
  502. if (!isNaN(numValue) && numValue > 10000) {
  503. callback(new Error("库存不得大于10000"));
  504. return;
  505. }
  506. }
  507. callback();
  508. });
  509. },
  510. trigger: "blur"
  511. }
  512. ],
  513. singleCanUse: [
  514. {
  515. validator: validatePositiveInteger("单日可用数量必须为正整数", { required: false }),
  516. trigger: "blur"
  517. },
  518. {
  519. validator: (rule: any, value: any, callback: any) => {
  520. // 如果值为空,不进行验证
  521. if (!value || value.toString().trim() === "") {
  522. callback();
  523. return;
  524. }
  525. const stock = voucherModel.value.singleQty;
  526. // 如果库存为空,不进行验证(由库存的验证规则处理)
  527. if (!stock || stock.toString().trim() === "") {
  528. callback();
  529. return;
  530. }
  531. const useNum = Number(value);
  532. const stockNum = Number(stock);
  533. // 如果转换失败,不进行验证(由其他验证规则处理)
  534. if (isNaN(useNum) || isNaN(stockNum)) {
  535. callback();
  536. return;
  537. }
  538. // 验证单日可用数量不能多于库存
  539. if (useNum > stockNum) {
  540. callback(new Error("单日可用数量不能多于库存"));
  541. return;
  542. }
  543. callback();
  544. },
  545. trigger: "blur"
  546. }
  547. ],
  548. purchaseLimitCode: [
  549. {
  550. validator: validatePositiveInteger("限购数量必须为正整数", { required: false }),
  551. trigger: "blur"
  552. },
  553. {
  554. validator: (rule: any, value: any, callback: any) => {
  555. // 如果值为空,不进行验证
  556. if (!value || value.toString().trim() === "") {
  557. callback();
  558. return;
  559. }
  560. const stock = voucherModel.value.singleQty;
  561. // 如果库存为空,不进行验证(由库存的验证规则处理)
  562. if (!stock || stock.toString().trim() === "") {
  563. callback();
  564. return;
  565. }
  566. const limitNum = Number(value);
  567. const stockNum = Number(stock);
  568. // 如果转换失败,不进行验证(由其他验证规则处理)
  569. if (isNaN(limitNum) || isNaN(stockNum)) {
  570. callback();
  571. return;
  572. }
  573. // 验证限购数量不能多于库存
  574. if (limitNum > stockNum) {
  575. callback(new Error("限购数量不能多于库存"));
  576. return;
  577. }
  578. callback();
  579. },
  580. trigger: "blur"
  581. }
  582. ],
  583. applyDesc: [
  584. {
  585. required: true,
  586. validator: (rule: any, value: any, callback: any) => {
  587. if (voucherModel.value.applyType == 2) {
  588. if (!value || value.toString().trim() === "") {
  589. callback(new Error("请输入适用范围"));
  590. return;
  591. }
  592. }
  593. callback();
  594. },
  595. trigger: "blur"
  596. }
  597. ]
  598. });
  599. // ==================== 代金券信息数据模型 ====================
  600. const voucherModel = ref<any>({
  601. // 代金券名称
  602. name: "",
  603. // 抵扣价格
  604. price: "",
  605. // 售卖价格
  606. offprice: "",
  607. // 开始售卖时间
  608. startDate: "",
  609. // 结束售卖时间
  610. endDate: "",
  611. // 使用时间 - 开始时间
  612. buyUseStartTime: "",
  613. // 使用时间 - 结束时间
  614. buyUseEndTime: "",
  615. // 使用时间(虚拟字段,用于表单验证)
  616. usageTime: null,
  617. // 有效期类型:1-指定天数,2-指定时间段内可用
  618. expirationType: "1",
  619. // 有效期天数(当expirationType为1时使用)
  620. expirationDate: 0,
  621. // 有效期时间段(当expirationType为2时使用)
  622. validityPeriod: [],
  623. // 不可用日期类型:1-全部日期可用,2-限制日期
  624. unusedType: "1",
  625. // 限制日期 - 星期选择(数组,存储选中的星期值)
  626. unavailableWeekdays: [],
  627. // 限制日期 - 节日选择(数组,存储选中的节日值)
  628. unavailableHolidays: [],
  629. // 自定义不可用日期列表(数组,每个元素是一个日期范围 [startDate, endDate])
  630. disableDateList: [],
  631. // 库存
  632. singleQty: "",
  633. // 单日可用数量
  634. singleCanUse: "",
  635. // 限购数量
  636. purchaseLimitCode: "",
  637. // 适用范围类型:0-全场通用,1-部分不可用
  638. applyType: "1",
  639. // 适用范围(当applyType为1时使用)
  640. applyDesc: "",
  641. // 补充说明
  642. supplement: ""
  643. });
  644. // ==================== 下拉选项数据 ====================
  645. // 小时选项列表(0-24点)
  646. const hourOptions = ref(
  647. Array.from({ length: 25 }, (_, i) => ({
  648. value: String(i),
  649. label: `${i}点`
  650. }))
  651. );
  652. // 有效期类型列表
  653. const validityPeriodList = ref([
  654. { value: "1", label: "指定天数" },
  655. { value: "2", label: "指定时间段内可用" }
  656. ]);
  657. // 不可用日期类型列表
  658. const unavailableDateTypeList = ref([
  659. { value: "1", label: "全部日期可用" },
  660. { value: "2", label: "限制日期" }
  661. // { value: "3", label: "自定义不可用日期" }
  662. ]);
  663. // 适用范围类型列表
  664. const applicableScopeList = ref([
  665. { value: "1", label: "全场通用" },
  666. { value: "2", label: "部分不可用" }
  667. ]);
  668. // 星期选项列表
  669. const weekdayList = ref([
  670. { name: "周一", id: "0", oName: "星期一" },
  671. { name: "周二", id: "1", oName: "星期二" },
  672. { name: "周三", id: "2", oName: "星期三" },
  673. { name: "周四", id: "3", oName: "星期四" },
  674. { name: "周五", id: "4", oName: "星期五" },
  675. { name: "周六", id: "5", oName: "星期六" },
  676. { name: "周日", id: "6", oName: "星期日" }
  677. ]);
  678. // 节日选项列表
  679. const holidayList: any = ref([]);
  680. // ==================== 监听器 ====================
  681. // 初始化标志,用于防止初始化时触发验证
  682. const isInitializing = ref(true);
  683. /**
  684. * 监听开始售卖时间变化
  685. * 当开始时间改变时,重新验证结束时间
  686. */
  687. watch(
  688. () => voucherModel.value.startDate,
  689. () => {
  690. if (isInitializing.value) return;
  691. if (voucherModel.value.endDate) {
  692. nextTick(() => {
  693. ruleFormRef.value?.validateField("endDate");
  694. });
  695. }
  696. }
  697. );
  698. /**
  699. * 监听结束售卖时间变化
  700. * 当结束时间改变时,重新验证开始时间
  701. */
  702. watch(
  703. () => voucherModel.value.endDate,
  704. () => {
  705. if (isInitializing.value) return;
  706. if (voucherModel.value.startDate) {
  707. nextTick(() => {
  708. ruleFormRef.value?.validateField("startDate");
  709. });
  710. }
  711. }
  712. );
  713. /**
  714. * 监听抵扣价格变化
  715. * 当抵扣价格改变时,重新验证售卖价格
  716. */
  717. watch(
  718. () => voucherModel.value.price,
  719. () => {
  720. if (isInitializing.value) return;
  721. if (voucherModel.value.offprice) {
  722. nextTick(() => {
  723. ruleFormRef.value?.validateField("offprice");
  724. });
  725. }
  726. }
  727. );
  728. /**
  729. * 监听售卖价格变化
  730. * 当售卖价格改变时,重新验证抵扣价格
  731. */
  732. watch(
  733. () => voucherModel.value.offprice,
  734. () => {
  735. if (isInitializing.value) return;
  736. if (voucherModel.value.price) {
  737. nextTick(() => {
  738. ruleFormRef.value?.validateField("price");
  739. });
  740. }
  741. }
  742. );
  743. /**
  744. * 监听库存变化
  745. * 当库存改变时,重新验证单日可用数量和限购数量
  746. */
  747. watch(
  748. () => voucherModel.value.singleQty,
  749. () => {
  750. if (isInitializing.value) return;
  751. if (voucherModel.value.singleCanUse) {
  752. nextTick(() => {
  753. ruleFormRef.value?.validateField("singleCanUse");
  754. });
  755. }
  756. if (voucherModel.value.purchaseLimitCode) {
  757. nextTick(() => {
  758. ruleFormRef.value?.validateField("purchaseLimitCode");
  759. });
  760. }
  761. }
  762. );
  763. /**
  764. * 监听使用开始时间变化
  765. * 更新虚拟字段以支持表单验证
  766. */
  767. watch(
  768. () => voucherModel.value.buyUseStartTime,
  769. () => {
  770. // 更新虚拟字段
  771. voucherModel.value.usageTime =
  772. voucherModel.value.buyUseStartTime && voucherModel.value.buyUseEndTime
  773. ? [voucherModel.value.buyUseStartTime, voucherModel.value.buyUseEndTime]
  774. : null;
  775. }
  776. );
  777. /**
  778. * 监听使用结束时间变化
  779. * 更新虚拟字段以支持表单验证
  780. */
  781. watch(
  782. () => voucherModel.value.buyUseEndTime,
  783. () => {
  784. // 更新虚拟字段
  785. voucherModel.value.usageTime =
  786. voucherModel.value.buyUseStartTime && voucherModel.value.buyUseEndTime
  787. ? [voucherModel.value.buyUseStartTime, voucherModel.value.buyUseEndTime]
  788. : null;
  789. }
  790. );
  791. /**
  792. * 监听不可用日期类型变化
  793. * 当切换到自定义不可用日期时,确保至少有一个日期项
  794. */
  795. watch(
  796. () => voucherModel.value.unusedType,
  797. newVal => {
  798. if (newVal == 3) {
  799. // 切换到自定义不可用日期时,如果disableDateList为空,则添加一个默认项
  800. if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
  801. voucherModel.value.disableDateList = [null];
  802. }
  803. }
  804. },
  805. { immediate: true }
  806. );
  807. // ==================== 生命周期钩子 ====================
  808. /**
  809. * 组件挂载时初始化
  810. * 从路由参数中获取页面类型和ID
  811. */
  812. onMounted(async () => {
  813. id.value = (route.query.id as string) || "";
  814. type.value = (route.query.type as string) || "";
  815. // 加载节日列表
  816. let params = {
  817. year: new Date().getFullYear(),
  818. page: 1,
  819. size: 500,
  820. openFlag: 1,
  821. holidayName: ""
  822. };
  823. let res: any = await getHolidayList(params);
  824. if (res && res.code == 200) {
  825. holidayList.value = res.data.records;
  826. }
  827. // 编辑模式下加载数据
  828. if (type.value != "add") {
  829. let res: any = await getVoucherDetail({ id: id.value });
  830. voucherModel.value = { ...voucherModel.value, ...res.data };
  831. // 处理有效期时间段:将时间戳字符串转换为数字数组
  832. if (voucherModel.value.validityPeriod && voucherModel.value.expirationType == 2) {
  833. const periodArray = voucherModel.value.validityPeriod.split(",");
  834. voucherModel.value.validityPeriod = periodArray
  835. .map((item: string) => Number(item.trim()))
  836. .filter((item: number) => !isNaN(item));
  837. } else {
  838. voucherModel.value.validityPeriod = [];
  839. }
  840. // 确保星期和节日字段存在;
  841. if (voucherModel.value.unusedType == 2) {
  842. const listVal = voucherModel.value.unusedDate ? voucherModel.value.unusedDate.split(";") : [];
  843. voucherModel.value.unavailableWeekdays = listVal[0] ? listVal[0].split(",").filter((item: string) => item) : [];
  844. voucherModel.value.unavailableHolidays = listVal[1] ? listVal[1].split(",").filter((item: string) => item) : [];
  845. }
  846. // 确保自定义不可用日期字段存在;
  847. if (voucherModel.value.unusedType === 3) {
  848. if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
  849. voucherModel.value.disableDateList = [null];
  850. }
  851. }
  852. console.log(voucherModel.value);
  853. }
  854. await nextTick();
  855. ruleFormRef.value?.clearValidate();
  856. isInitializing.value = false;
  857. });
  858. // ==================== 事件处理函数 ====================
  859. /**
  860. * 返回上一页
  861. */
  862. const goBack = () => {
  863. router.go(-1);
  864. };
  865. /**
  866. * 切换星期选择
  867. * @param value 星期值
  868. */
  869. const toggleWeekday = (value: string) => {
  870. if (!voucherModel.value.unavailableWeekdays) {
  871. voucherModel.value.unavailableWeekdays = [];
  872. }
  873. const index = voucherModel.value.unavailableWeekdays.indexOf(value);
  874. if (index > -1) {
  875. voucherModel.value.unavailableWeekdays.splice(index, 1);
  876. } else {
  877. voucherModel.value.unavailableWeekdays.push(value);
  878. }
  879. // 触发表单验证(同时验证星期和节日字段)
  880. nextTick(() => {
  881. ruleFormRef.value?.validateField("unavailableWeekdays");
  882. ruleFormRef.value?.validateField("unavailableHolidays");
  883. });
  884. };
  885. /**
  886. * 切换节日选择
  887. * @param value 节日值
  888. */
  889. const toggleHoliday = (value: string | number) => {
  890. if (!voucherModel.value.unavailableHolidays) {
  891. voucherModel.value.unavailableHolidays = [];
  892. }
  893. // 统一转换为字符串进行比较
  894. const valueStr = String(value);
  895. const index = voucherModel.value.unavailableHolidays.findIndex((item: any) => String(item) === valueStr);
  896. if (index > -1) {
  897. voucherModel.value.unavailableHolidays.splice(index, 1);
  898. } else {
  899. voucherModel.value.unavailableHolidays.push(valueStr);
  900. }
  901. // 触发表单验证(同时验证星期和节日字段)
  902. nextTick(() => {
  903. ruleFormRef.value?.validateField("unavailableWeekdays");
  904. ruleFormRef.value?.validateField("unavailableHolidays");
  905. });
  906. };
  907. /**
  908. * 添加自定义不可用日期
  909. */
  910. const addDate = () => {
  911. if (!voucherModel.value.disableDateList) {
  912. voucherModel.value.disableDateList = [];
  913. }
  914. voucherModel.value.disableDateList.push(null);
  915. };
  916. /**
  917. * 删除自定义不可用日期
  918. * @param index 要删除的日期索引
  919. */
  920. const removeDate = (index: number) => {
  921. if (voucherModel.value.disableDateList.length <= 1) {
  922. ElMessage.warning("至少需要保留一个日期项");
  923. return;
  924. }
  925. voucherModel.value.disableDateList.splice(index, 1);
  926. // 删除日期项后,重新验证表单以清除旧的验证错误
  927. nextTick(() => {
  928. ruleFormRef.value?.validateField("customUnavailableDates");
  929. });
  930. };
  931. // ==================== 表单引用 ====================
  932. const ruleFormRef = ref<FormInstance>(); // 表单引用
  933. /**
  934. * 提交数据(新增/编辑)
  935. * 验证表单,通过后调用相应的API接口
  936. */
  937. const handleSubmit = async (submitType?: string) => {
  938. // 组装提交参数
  939. let params: any = { ...voucherModel.value };
  940. params.storeId = localGet("createdId");
  941. params.status = 1;
  942. // 处理有效期:只有当expirationType为2(指定时间段内可用)时才处理validityPeriod
  943. if (params.expirationType == 2 && params.validityPeriod && Array.isArray(params.validityPeriod)) {
  944. params.validityPeriod = params.validityPeriod.join(",");
  945. } else if (params.expirationType == 1) {
  946. // 指定天数模式,不需要validityPeriod字段
  947. params.validityPeriod = "";
  948. }
  949. // 处理不可用日期
  950. if (params.unusedType == 2) {
  951. params.unusedDate = params.unavailableWeekdays.join(",") + ";" + params.unavailableHolidays.join(",");
  952. } else if (params.unusedType == 3) {
  953. // 处理自定义不可用日期
  954. if (params.disableDateList && params.disableDateList.length > 0) {
  955. params.unusedDate = params.disableDateList
  956. .map((dateRange: any) => (dateRange && dateRange.length === 2 ? dateRange.join(",") : ""))
  957. .filter((item: string) => item)
  958. .join(";");
  959. }
  960. }
  961. params.dataType = submitType ? 1 : 0;
  962. delete params.unavailableWeekdays;
  963. delete params.unavailableHolidays;
  964. delete params.disableDateList;
  965. console.log("提交参数:", params);
  966. if (submitType) {
  967. if (!voucherModel.value.name) {
  968. ElMessage.warning("请填写代金券名称");
  969. return;
  970. }
  971. let res: any = await addOrUpdateCoupon(params);
  972. if (res && res.code == 200) {
  973. ElMessage.success("保存成功");
  974. goBack();
  975. }
  976. return;
  977. }
  978. // 验证表单
  979. ruleFormRef.value!.validate(async (valid: boolean) => {
  980. if (!valid) return;
  981. let res: any = await addOrUpdateCoupon(params);
  982. if (res && res.code == 200) {
  983. ElMessage.success("创建成功,请耐心等待审核");
  984. goBack();
  985. }
  986. });
  987. };
  988. // ==================== 工具函数 ====================
  989. /**
  990. * 禁用开始售卖时间的日期
  991. * 不能选择早于当前时间的日期
  992. */
  993. const disabledStartDate = (time: Date) => {
  994. const today = new Date();
  995. today.setHours(0, 0, 0, 0);
  996. return time.getTime() < today.getTime();
  997. };
  998. /**
  999. * 禁用结束售卖时间的日期
  1000. * 不能选择早于当前时间的日期,也不能选择早于或等于开始售卖时间的日期
  1001. */
  1002. const disabledEndDate = (time: Date) => {
  1003. const today = new Date();
  1004. today.setHours(0, 0, 0, 0);
  1005. if (time.getTime() < today.getTime()) {
  1006. return true;
  1007. }
  1008. if (voucherModel.value.startDate) {
  1009. const startDate = new Date(voucherModel.value.startDate);
  1010. startDate.setHours(0, 0, 0, 0);
  1011. return time.getTime() <= startDate.getTime();
  1012. }
  1013. return false;
  1014. };
  1015. /**
  1016. * 禁用有效期时间段的日期
  1017. * 不能选择早于当前时间的日期
  1018. * @param time 日期对象
  1019. * @returns 是否禁用该日期
  1020. */
  1021. const disabledValidityDate = (time: Date) => {
  1022. const today = new Date();
  1023. today.setHours(0, 0, 0, 0);
  1024. return time.getTime() < today.getTime();
  1025. };
  1026. /**
  1027. * 禁用自定义不可用日期的日期
  1028. * 不能选择早于当前时间的日期
  1029. * @param time 日期对象
  1030. * @returns 是否禁用该日期
  1031. */
  1032. const disabledCustomUnavailableDate = (time: Date) => {
  1033. const today = new Date();
  1034. today.setHours(0, 0, 0, 0);
  1035. return time.getTime() < today.getTime();
  1036. };
  1037. </script>
  1038. <style scoped lang="scss">
  1039. /* 页面容器 */
  1040. .table-box {
  1041. display: flex;
  1042. flex-direction: column;
  1043. height: auto !important;
  1044. min-height: 100%;
  1045. }
  1046. /* 头部区域 */
  1047. .header {
  1048. display: flex;
  1049. align-items: center;
  1050. padding: 20px;
  1051. border-bottom: 1px solid #e4e7ed;
  1052. }
  1053. .title {
  1054. flex: 1;
  1055. margin: 0;
  1056. font-size: 18px;
  1057. font-weight: bold;
  1058. text-align: center;
  1059. }
  1060. /* 内容区域布局 */
  1061. .content {
  1062. display: flex;
  1063. flex: 1;
  1064. column-gap: 20px;
  1065. width: 98%;
  1066. margin: 20px auto;
  1067. /* 左侧内容区域 */
  1068. .contentLeft {
  1069. width: 50%;
  1070. }
  1071. /* 右侧内容区域 */
  1072. .contentRight {
  1073. width: 50%;
  1074. }
  1075. }
  1076. /* 模块容器 */
  1077. .model {
  1078. margin-bottom: 50px;
  1079. }
  1080. /* 表单容器 */
  1081. .formBox {
  1082. display: flex;
  1083. flex-direction: column;
  1084. width: 100%;
  1085. min-height: 100%;
  1086. }
  1087. /* 底部按钮容器 - 居中显示 */
  1088. .button-container {
  1089. display: flex;
  1090. gap: 12px;
  1091. align-items: center;
  1092. justify-content: center;
  1093. padding: 20px 0;
  1094. margin-top: 20px;
  1095. }
  1096. /* 有效期天数容器 */
  1097. .expiration-date-container {
  1098. display: flex;
  1099. gap: 12px;
  1100. align-items: center;
  1101. width: fit-content;
  1102. padding: 8px 12px;
  1103. border-radius: 4px;
  1104. }
  1105. /* 有效期标签文字 */
  1106. .expiration-label {
  1107. font-size: 14px;
  1108. color: #606266;
  1109. white-space: nowrap;
  1110. }
  1111. /* 有效期输入框 */
  1112. .expiration-input {
  1113. width: 150px;
  1114. }
  1115. /* 不可用日期容器 */
  1116. .unavailable-dates-container {
  1117. display: flex;
  1118. flex-direction: column;
  1119. gap: 20px;
  1120. width: 100%;
  1121. }
  1122. /* 日期选择区块 */
  1123. .date-select-section {
  1124. display: flex;
  1125. flex-direction: column;
  1126. gap: 12px;
  1127. }
  1128. /* 区块标题 */
  1129. .section-title {
  1130. font-size: 14px;
  1131. font-weight: 500;
  1132. color: #606266;
  1133. }
  1134. /* 按钮组 */
  1135. .button-group {
  1136. display: flex;
  1137. flex-wrap: wrap;
  1138. gap: 10px;
  1139. }
  1140. /* 日期选择按钮 */
  1141. .date-select-btn {
  1142. min-width: 80px;
  1143. height: 36px;
  1144. padding: 8px 16px;
  1145. margin: 0;
  1146. font-size: 14px;
  1147. border-radius: 4px;
  1148. transition: all 0.1s;
  1149. }
  1150. /* 日期选择器容器 */
  1151. .date-picker-container {
  1152. display: flex;
  1153. flex-direction: column;
  1154. gap: 12px;
  1155. width: 100%;
  1156. }
  1157. /* 添加日期按钮 */
  1158. .add-date-btn {
  1159. width: fit-content;
  1160. margin-bottom: 8px;
  1161. }
  1162. /* 日期项容器 */
  1163. .date-item {
  1164. display: flex;
  1165. gap: 12px;
  1166. align-items: center;
  1167. padding: 8px;
  1168. border-radius: 4px;
  1169. transition: background-color 0.1s;
  1170. &:hover {
  1171. background-color: #ecf5ff;
  1172. }
  1173. }
  1174. /* 日期选择器 */
  1175. .date-item .date-picker {
  1176. flex: 1;
  1177. max-width: 500px;
  1178. }
  1179. /* 删除按钮 */
  1180. .date-item .delete-btn {
  1181. flex-shrink: 0;
  1182. }
  1183. /* 时间范围容器 */
  1184. .time-range-container {
  1185. display: flex;
  1186. gap: 12px;
  1187. align-items: center;
  1188. width: 100%;
  1189. }
  1190. /* 时间选择器 */
  1191. .time-picker {
  1192. flex: 1;
  1193. max-width: 200px;
  1194. }
  1195. /* 时间分隔符 */
  1196. .time-separator {
  1197. font-size: 14px;
  1198. color: #606266;
  1199. white-space: nowrap;
  1200. }
  1201. </style>