newCoupon.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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="couponModel" ref="ruleFormRef" :rules="rules" label-width="200px" class="formBox">
  9. <div class="content">
  10. <!-- 左侧内容区域 -->
  11. <div class="contentLeft">
  12. <!-- 优惠券名称 -->
  13. <el-form-item label="优惠券名称" prop="name">
  14. <el-input maxlength="50" v-model="couponModel.name" placeholder="请输入" clearable />
  15. </el-form-item>
  16. <!-- 面值 -->
  17. <el-form-item label="面值(¥)" prop="nominalValue">
  18. <el-input v-model="couponModel.nominalValue" maxlength="15" placeholder="请输入" clearable />
  19. </el-form-item>
  20. <!-- 开始领取时间 -->
  21. <el-form-item label="开始领取时间" prop="beginGetDate">
  22. <el-date-picker
  23. v-model="couponModel.beginGetDate"
  24. format="YYYY/MM/DD"
  25. value-format="YYYY-MM-DD"
  26. placeholder="请选择开始领取时间"
  27. :disabled-date="disabledStartDate"
  28. />
  29. </el-form-item>
  30. <!-- 结束领取时间 -->
  31. <el-form-item label="结束领取时间" prop="endGetDate">
  32. <el-date-picker
  33. v-model="couponModel.endGetDate"
  34. format="YYYY/MM/DD"
  35. value-format="YYYY-MM-DD"
  36. placeholder="请选择结束领取时间"
  37. :disabled-date="disabledEndDate"
  38. />
  39. </el-form-item>
  40. <!-- 有效期 -->
  41. <el-form-item label="有效期(天)" prop="specifiedDay">
  42. <el-input v-model="couponModel.specifiedDay" maxlength="15" placeholder="请输入" clearable />
  43. </el-form-item>
  44. <!-- 库存 -->
  45. <el-form-item label="库存(张)" prop="singleQty">
  46. <el-input v-model="couponModel.singleQty" maxlength="15" placeholder="请输入" clearable />
  47. </el-form-item>
  48. <!-- 用户领取规则 -->
  49. <el-form-item label="用户领取规则" prop="claimRule">
  50. <el-radio-group v-model="couponModel.claimRule" class="ml-4">
  51. <el-radio v-for="item in claimRuleOptions" :key="item.value" :value="item.value">
  52. {{ item.label }}
  53. </el-radio>
  54. </el-radio-group>
  55. </el-form-item>
  56. <!-- 用户是否需要收藏店铺领取 -->
  57. <el-form-item label="用户是否需要收藏店铺领取" prop="attentionCanReceived">
  58. <el-radio-group v-model="couponModel.attentionCanReceived" class="ml-4">
  59. <el-radio v-for="item in yesNoOptions" :key="item.value" :value="item.value">
  60. {{ item.label }}
  61. </el-radio>
  62. </el-radio-group>
  63. </el-form-item>
  64. </div>
  65. <!-- 右侧内容区域 -->
  66. <div class="contentRight">
  67. <!-- 是否有低消 -->
  68. <el-form-item label="是否有低消" prop="hasMinimumSpend">
  69. <el-radio-group v-model="couponModel.hasMinimumSpend" class="ml-4">
  70. <el-radio v-for="item in yesNoOptions" :key="item.value" :value="item.value">
  71. {{ item.label }}
  72. </el-radio>
  73. </el-radio-group>
  74. </el-form-item>
  75. <!-- 最低消费金额 -->
  76. <el-form-item label="最低消费金额(¥)" prop="minimumSpendingAmount" v-if="couponModel.hasMinimumSpend === 1">
  77. <el-input v-model="couponModel.minimumSpendingAmount" maxlength="15" placeholder="请输入" clearable />
  78. </el-form-item>
  79. <!-- 补充说明 -->
  80. <el-form-item label="补充说明" prop="supplementaryInstruction">
  81. <el-input
  82. maxlength="300"
  83. v-model="couponModel.supplementaryInstruction"
  84. :rows="4"
  85. type="textarea"
  86. placeholder="请输入"
  87. show-word-limit
  88. />
  89. </el-form-item>
  90. </div>
  91. </div>
  92. </el-form>
  93. <!-- 底部按钮区域 -->
  94. <div class="button-container">
  95. <el-button @click="() => handleSubmit('draft')"> 存草稿 </el-button>
  96. <el-button type="primary" @click="() => handleSubmit()"> 提交 </el-button>
  97. </div>
  98. </div>
  99. </template>
  100. <script setup lang="tsx" name="newCoupon">
  101. /**
  102. * 优惠券管理 - 新增页面
  103. * 功能:支持优惠券的新增操作
  104. */
  105. import { ref, reactive, watch, nextTick, onMounted } from "vue";
  106. import { ElMessage } from "element-plus";
  107. import { useRoute, useRouter } from "vue-router";
  108. import type { FormInstance } from "element-plus";
  109. import { getCouponDetail, addDiscountCoupon, editDiscountCoupon } from "@/api/modules/couponManagement";
  110. import { validatePositiveNumber, validatePositiveInteger, validateDateRange } from "@/utils/eleValidate";
  111. import { localGet } from "@/utils";
  112. import { getVoucherDetail } from "@/api/modules/voucherManagement";
  113. // ==================== 响应式数据定义 ====================
  114. // 路由相关
  115. const router = useRouter();
  116. const route = useRoute();
  117. // 页面状态
  118. const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
  119. const id = ref<string>(""); // 页面ID参数
  120. // ==================== 表单验证规则 ====================
  121. const rules = reactive({
  122. name: [{ required: true, message: "请输入优惠券名称" }],
  123. nominalValue: [
  124. { required: true, message: "请输入面值" },
  125. {
  126. validator: validatePositiveNumber("面值必须为正数"),
  127. trigger: "blur"
  128. }
  129. ],
  130. beginGetDate: [
  131. { required: true, message: "请选择开始领取时间" },
  132. {
  133. validator: validateDateRange(
  134. () => couponModel.value.beginGetDate,
  135. () => couponModel.value.endGetDate,
  136. "开始领取时间不能早于当前时间",
  137. "结束领取时间不能早于当前时间",
  138. "开始领取时间必须早于结束领取时间",
  139. true
  140. ),
  141. trigger: "change"
  142. }
  143. ],
  144. endGetDate: [
  145. { required: true, message: "请选择结束领取时间" },
  146. {
  147. validator: validateDateRange(
  148. () => couponModel.value.beginGetDate,
  149. () => couponModel.value.endGetDate,
  150. "开始领取时间不能早于当前时间",
  151. "结束领取时间不能早于当前时间",
  152. "开始领取时间必须早于结束领取时间",
  153. true
  154. ),
  155. trigger: "change"
  156. }
  157. ],
  158. specifiedDay: [
  159. { required: true, message: "请输入有效期" },
  160. {
  161. validator: validatePositiveInteger("有效期必须为正整数", { required: false }),
  162. trigger: "blur"
  163. }
  164. ],
  165. singleQty: [
  166. { required: true, message: "请输入库存" },
  167. {
  168. validator: validatePositiveInteger("库存必须为正整数", { required: false }),
  169. trigger: "blur"
  170. }
  171. ],
  172. minimumSpendingAmount: [
  173. { required: true, message: "请输入最低消费金额" },
  174. {
  175. validator: (rule: any, value: any, callback: any) => {
  176. if (couponModel.value.hasMinimumSpend === 1) {
  177. if (!value || value.toString().trim() === "") {
  178. callback(new Error("请输入最低消费金额"));
  179. return;
  180. }
  181. const strValue = value.toString().trim();
  182. // 检查是否有前导零(除了单独的"0"或"0."开头的小数)
  183. if (strValue.length > 1 && strValue.startsWith("0") && strValue !== "0" && !strValue.startsWith("0.")) {
  184. callback(new Error("最低消费金额必须为正数"));
  185. return;
  186. }
  187. validatePositiveNumber("最低消费金额必须为正数")(rule, value, callback);
  188. } else {
  189. callback();
  190. }
  191. },
  192. trigger: "blur"
  193. }
  194. ]
  195. });
  196. // ==================== 选项配置 ====================
  197. // 用户领取规则选项
  198. const claimRuleOptions = [
  199. { label: "每日一领", value: "day" },
  200. { label: "每周一领", value: "week" },
  201. { label: "每月一领", value: "month" }
  202. ];
  203. // 是/否选项
  204. const yesNoOptions = [
  205. { label: "是", value: 1 },
  206. { label: "否", value: 0 }
  207. ];
  208. // ==================== 优惠券信息数据模型 ====================
  209. const couponModel = ref<any>({
  210. // 优惠券名称
  211. name: "",
  212. // 面值(元)
  213. nominalValue: "",
  214. // 开始领取时间
  215. beginGetDate: "",
  216. // 结束领取时间
  217. endGetDate: "",
  218. // 有效期
  219. specifiedDay: "",
  220. // 库存
  221. singleQty: "",
  222. // 用户领取规则:day-每日一领,week-每周一领,month-每月一领
  223. claimRule: "day",
  224. // 用户是否需要收藏店铺领取:1-是,0-否
  225. attentionCanReceived: 1,
  226. // 是否有低消:1-是,0-否
  227. hasMinimumSpend: 0,
  228. // 最低消费金额
  229. minimumSpendingAmount: "",
  230. // 补充说明
  231. supplementaryInstruction: ""
  232. });
  233. // ==================== 监听器 ====================
  234. // 初始化标志,用于防止初始化时触发验证
  235. const isInitializing = ref(true);
  236. /**
  237. * 监听开始领取时间变化
  238. * 当开始时间改变时,重新验证结束时间
  239. */
  240. watch(
  241. () => couponModel.value.beginGetDate,
  242. () => {
  243. if (isInitializing.value) return;
  244. if (couponModel.value.endGetDate) {
  245. nextTick(() => {
  246. ruleFormRef.value?.validateField("endGetDate");
  247. });
  248. }
  249. }
  250. );
  251. /**
  252. * 监听结束领取时间变化
  253. * 当结束时间改变时,重新验证开始时间
  254. */
  255. watch(
  256. () => couponModel.value.endGetDate,
  257. () => {
  258. if (isInitializing.value) return;
  259. if (couponModel.value.beginGetDate) {
  260. nextTick(() => {
  261. ruleFormRef.value?.validateField("beginGetDate");
  262. });
  263. }
  264. }
  265. );
  266. /**
  267. * 监听是否有低消变化
  268. * 当选择"否"时,清空最低消费金额
  269. */
  270. watch(
  271. () => couponModel.value.hasMinimumSpend,
  272. newVal => {
  273. if (isInitializing.value) return;
  274. if (newVal === 0) {
  275. couponModel.value.minimumSpendingAmount = "";
  276. nextTick(() => {
  277. ruleFormRef.value?.clearValidate("minimumSpendingAmount");
  278. });
  279. }
  280. }
  281. );
  282. /**
  283. * 监听最低消费金额变化
  284. * 当最低消费金额大于0时,自动设置为"是",否则设置为"否"
  285. */
  286. // watch(
  287. // () => couponModel.value.minimumSpendingAmount,
  288. // newVal => {
  289. // if (isInitializing.value) return;
  290. // const amount = Number(newVal);
  291. // if (!isNaN(amount) && amount > 0) {
  292. // couponModel.value.hasMinimumSpend = 1;
  293. // } else {
  294. // couponModel.value.hasMinimumSpend = 0;
  295. // }
  296. // }
  297. // );
  298. // ==================== 事件处理函数 ====================
  299. /**
  300. * 组件挂载时初始化
  301. * 从路由参数中获取页面类型和ID
  302. */
  303. onMounted(async () => {
  304. id.value = (route.query.id as string) || "";
  305. type.value = (route.query.type as string) || "";
  306. if (type.value != "add") {
  307. let res: any = await getCouponDetail({ counponId: id.value });
  308. couponModel.value = { ...couponModel.value, ...res.data };
  309. // 根据最低消费金额设置是否有低消
  310. const amount = Number(couponModel.value.minimumSpendingAmount);
  311. if (!isNaN(amount) && amount > 0) {
  312. couponModel.value.hasMinimumSpend = 1;
  313. } else {
  314. couponModel.value.hasMinimumSpend = 0;
  315. }
  316. }
  317. await nextTick();
  318. ruleFormRef.value?.clearValidate();
  319. isInitializing.value = false;
  320. });
  321. /**
  322. * 返回上一页
  323. */
  324. const goBack = () => {
  325. router.go(-1);
  326. };
  327. // ==================== 表单引用 ====================
  328. const ruleFormRef = ref<FormInstance>(); // 表单引用
  329. /**
  330. * 提交数据(新增)
  331. * 验证表单,通过后调用相应的API接口
  332. */
  333. const handleSubmit = async (submitType?: string) => {
  334. // 组装提交参数
  335. let params: any = { ...couponModel.value };
  336. params.storeId = localGet("createdId");
  337. params.couponId = "";
  338. params.couponStatus = submitType ? 0 : 1;
  339. params.restrictedQuantity = 0;
  340. params.expirationDate = 0;
  341. params.getStatus = 1;
  342. params.type = 1;
  343. if (submitType) {
  344. if (!couponModel.value.name) {
  345. ElMessage.warning("请填写优惠券名称");
  346. return;
  347. }
  348. let res: any = await addDiscountCoupon(params);
  349. if (res && res.code == 200) {
  350. ElMessage.success("保存成功");
  351. goBack();
  352. }
  353. return;
  354. }
  355. // 验证表单
  356. ruleFormRef.value!.validate(async (valid: boolean) => {
  357. if (!valid) return;
  358. if (type.value == "add") {
  359. let res: any = await addDiscountCoupon(params);
  360. if (res && res.code == 200) {
  361. ElMessage.success("创建成功");
  362. }
  363. } else {
  364. params.couponId = id.value;
  365. let res: any = await editDiscountCoupon(params);
  366. if (res && res.code == 200) {
  367. ElMessage.success("修改成功");
  368. }
  369. }
  370. goBack();
  371. });
  372. };
  373. // ==================== 工具函数 ====================
  374. /**
  375. * 禁用开始领取时间的日期
  376. * 不能选择早于当前时间的日期
  377. */
  378. const disabledStartDate = (time: Date) => {
  379. const today = new Date();
  380. today.setHours(0, 0, 0, 0);
  381. return time.getTime() < today.getTime();
  382. };
  383. /**
  384. * 禁用结束领取时间的日期
  385. * 不能选择早于当前时间的日期,也不能选择早于或等于开始领取时间的日期
  386. */
  387. const disabledEndDate = (time: Date) => {
  388. const today = new Date();
  389. today.setHours(0, 0, 0, 0);
  390. if (time.getTime() < today.getTime()) {
  391. return true;
  392. }
  393. if (couponModel.value.beginGetDate) {
  394. const startDate = new Date(couponModel.value.beginGetDate);
  395. startDate.setHours(0, 0, 0, 0);
  396. return time.getTime() <= startDate.getTime();
  397. }
  398. return false;
  399. };
  400. </script>
  401. <style scoped lang="scss">
  402. /* 页面容器 */
  403. .table-box {
  404. display: flex;
  405. flex-direction: column;
  406. height: auto !important;
  407. min-height: 100%;
  408. }
  409. /* 头部区域 */
  410. .header {
  411. display: flex;
  412. align-items: center;
  413. padding: 20px;
  414. border-bottom: 1px solid #e4e7ed;
  415. }
  416. .title {
  417. flex: 1;
  418. margin: 0;
  419. font-size: 18px;
  420. font-weight: bold;
  421. text-align: center;
  422. }
  423. /* 内容区域布局 */
  424. .content {
  425. display: flex;
  426. flex: 1;
  427. column-gap: 20px;
  428. width: 98%;
  429. margin: 20px auto;
  430. /* 左侧内容区域 */
  431. .contentLeft {
  432. width: 50%;
  433. }
  434. /* 右侧内容区域 */
  435. .contentRight {
  436. width: 50%;
  437. }
  438. }
  439. /* 表单容器 */
  440. .formBox {
  441. display: flex;
  442. flex-direction: column;
  443. width: 100%;
  444. min-height: 100%;
  445. }
  446. /* 底部按钮容器 - 居中显示 */
  447. .button-container {
  448. display: flex;
  449. gap: 12px;
  450. align-items: center;
  451. justify-content: center;
  452. padding: 20px 0;
  453. margin-top: 20px;
  454. }
  455. </style>