CouponTemplateForm.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <template>
  2. <Dialog v-model="dialogVisible" :title="dialogTitle">
  3. <el-form
  4. ref="formRef"
  5. v-loading="formLoading"
  6. :model="formData"
  7. :rules="formRules"
  8. label-width="140px"
  9. >
  10. <el-form-item label="优惠券名称" prop="name">
  11. <el-input v-model="formData.name" placeholder="请输入优惠券名称" />
  12. </el-form-item>
  13. <el-form-item label="优惠劵类型" prop="productScope">
  14. <el-radio-group v-model="formData.productScope">
  15. <el-radio
  16. v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
  17. :key="dict.value"
  18. :label="dict.value"
  19. >
  20. {{ dict.label }}
  21. </el-radio>
  22. </el-radio-group>
  23. </el-form-item>
  24. <el-form-item
  25. label="商品"
  26. v-if="formData.productScope === PromotionProductScopeEnum.SPU.scope"
  27. prop="productSpuIds"
  28. >
  29. <div class="flex items-center gap-1 flex-wrap">
  30. <div class="select-box spu-pic" v-for="(spu, index) in productSpus" :key="spu.id">
  31. <el-image :src="spu.picUrl" />
  32. <Icon icon="ep:circle-close-filled" class="del-icon" @click="handleRemoveSpu(index)" />
  33. </div>
  34. <div class="select-box" @click="openSpuTableSelect">
  35. <Icon icon="ep:plus" />
  36. </div>
  37. </div>
  38. </el-form-item>
  39. <el-form-item
  40. label="分类"
  41. v-if="formData.productScope === PromotionProductScopeEnum.CATEGORY.scope"
  42. prop="productCategoryIds"
  43. >
  44. <ProductCategorySelect v-model="formData.productCategoryIds" />
  45. </el-form-item>
  46. <el-form-item label="优惠类型" prop="discountType">
  47. <el-radio-group v-model="formData.discountType">
  48. <el-radio
  49. v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
  50. :key="dict.value"
  51. :label="dict.value"
  52. >
  53. {{ dict.label }}
  54. </el-radio>
  55. </el-radio-group>
  56. </el-form-item>
  57. <el-form-item
  58. v-if="formData.discountType === PromotionDiscountTypeEnum.PRICE.type"
  59. label="优惠券面额"
  60. prop="discountPrice"
  61. >
  62. <el-input-number
  63. v-model="formData.discountPrice"
  64. placeholder="请输入优惠金额,单位:元"
  65. class="!w-400px mr-2"
  66. :precision="2"
  67. :min="0"
  68. />
  69. </el-form-item>
  70. <el-form-item
  71. v-if="formData.discountType === PromotionDiscountTypeEnum.PERCENT.type"
  72. label="优惠券折扣"
  73. prop="discountPercent"
  74. >
  75. <el-input-number
  76. v-model="formData.discountPercent"
  77. placeholder="优惠券折扣不能小于 1 折,且不可大于 9.9 折"
  78. class="!w-400px mr-2"
  79. :precision="1"
  80. :min="1"
  81. :max="9.9"
  82. />
  83. </el-form-item>
  84. <el-form-item
  85. v-if="formData.discountType === PromotionDiscountTypeEnum.PERCENT.type"
  86. label="最多优惠"
  87. prop="discountLimitPrice"
  88. >
  89. <el-input-number
  90. v-model="formData.discountLimitPrice"
  91. placeholder="请输入最多优惠"
  92. class="!w-400px mr-2"
  93. :precision="2"
  94. :min="0"
  95. />
  96. </el-form-item>
  97. <el-form-item label="满多少元可以使用" prop="usePrice">
  98. <el-input-number
  99. v-model="formData.usePrice"
  100. placeholder="无门槛请设为 0"
  101. class="!w-400px mr-2"
  102. :precision="2"
  103. :min="0"
  104. />
  105. </el-form-item>
  106. <el-form-item label="领取方式" prop="takeType">
  107. <el-radio-group v-model="formData.takeType">
  108. <el-radio :key="1" :label="1">直接领取</el-radio>
  109. <el-radio :key="2" :label="2">指定发放</el-radio>
  110. </el-radio-group>
  111. </el-form-item>
  112. <el-form-item v-if="formData.takeType === 1" label="发放数量" prop="totalCount">
  113. <el-input-number
  114. v-model="formData.totalCount"
  115. placeholder="发放数量,没有之后不能领取或发放,-1 为不限制"
  116. class="!w-400px mr-2"
  117. :precision="0"
  118. :min="-1"
  119. />
  120. </el-form-item>
  121. <el-form-item v-if="formData.takeType === 1" label="每人限领个数" prop="takeLimitCount">
  122. <el-input-number
  123. v-model="formData.takeLimitCount"
  124. placeholder="设置为 -1 时,可无限领取"
  125. class="!w-400px mr-2"
  126. :precision="0"
  127. :min="-1"
  128. />
  129. </el-form-item>
  130. <el-form-item label="有效期类型" prop="validityType">
  131. <el-radio-group v-model="formData.validityType">
  132. <el-radio
  133. v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE)"
  134. :key="dict.value"
  135. :label="dict.value"
  136. >
  137. {{ dict.label }}
  138. </el-radio>
  139. </el-radio-group>
  140. </el-form-item>
  141. <el-form-item
  142. v-if="formData.validityType === CouponTemplateValidityTypeEnum.DATE.type"
  143. label="固定日期"
  144. prop="validTimes"
  145. >
  146. <el-date-picker
  147. v-model="formData.validTimes"
  148. style="width: 240px"
  149. value-format="x"
  150. type="datetimerange"
  151. :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
  152. />
  153. </el-form-item>
  154. <el-form-item
  155. v-if="formData.validityType === CouponTemplateValidityTypeEnum.TERM.type"
  156. label="领取日期"
  157. prop="fixedStartTerm"
  158. >
  159. <el-input-number
  160. v-model="formData.fixedStartTerm"
  161. placeholder="0 为今天生效"
  162. class="mx-2"
  163. :precision="0"
  164. :min="0"
  165. />
  166. <el-input-number
  167. v-model="formData.fixedEndTerm"
  168. placeholder="请输入结束天数"
  169. class="mx-2"
  170. :precision="0"
  171. :min="0"
  172. />
  173. 天有效
  174. </el-form-item>
  175. </el-form>
  176. <template #footer>
  177. <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
  178. <el-button @click="dialogVisible = false">取 消</el-button>
  179. </template>
  180. </Dialog>
  181. <SpuTableSelect ref="spuTableSelectRef" multiple @change="handleSpuSelected" />
  182. </template>
  183. <script lang="ts" setup>
  184. import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
  185. import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
  186. import * as ProductSpuApi from '@/api/mall/product/spu'
  187. import {
  188. CouponTemplateValidityTypeEnum,
  189. PromotionDiscountTypeEnum,
  190. PromotionProductScopeEnum
  191. } from '@/utils/constants'
  192. import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue'
  193. import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
  194. defineOptions({ name: 'CouponTemplateForm' })
  195. const { t } = useI18n() // 国际化
  196. const message = useMessage() // 消息弹窗
  197. const dialogVisible = ref(false) // 弹窗的是否展示
  198. const dialogTitle = ref('') // 弹窗的标题
  199. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  200. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  201. const formData = ref({
  202. id: undefined,
  203. name: undefined,
  204. discountType: PromotionDiscountTypeEnum.PRICE.type,
  205. discountPrice: undefined,
  206. discountPercent: undefined,
  207. discountLimitPrice: undefined,
  208. usePrice: undefined,
  209. takeType: 1,
  210. totalCount: undefined,
  211. takeLimitCount: undefined,
  212. validityType: CouponTemplateValidityTypeEnum.DATE.type,
  213. validTimes: [],
  214. validStartTime: undefined,
  215. validEndTime: undefined,
  216. fixedStartTerm: undefined,
  217. fixedEndTerm: undefined,
  218. productScope: PromotionProductScopeEnum.ALL.scope,
  219. productScopeValues: [], // 商品范围:值为 品类编号列表 或 商品编号列表 ,用于提交
  220. productCategoryIds: [], // 仅用于表单,不提交
  221. productSpuIds: [] // 仅用于表单,不提交
  222. })
  223. const formRules = reactive({
  224. name: [{ required: true, message: '优惠券名称不能为空', trigger: 'blur' }],
  225. discountType: [{ required: true, message: '优惠券类型不能为空', trigger: 'change' }],
  226. discountPrice: [{ required: true, message: '优惠券面额不能为空', trigger: 'blur' }],
  227. discountPercent: [{ required: true, message: '优惠券折扣不能为空', trigger: 'blur' }],
  228. discountLimitPrice: [{ required: true, message: '最多优惠不能为空', trigger: 'blur' }],
  229. usePrice: [{ required: true, message: '满多少元可以使用不能为空', trigger: 'blur' }],
  230. takeType: [{ required: true, message: '领取方式不能为空', trigger: 'change' }],
  231. totalCount: [{ required: true, message: '发放数量不能为空', trigger: 'blur' }],
  232. takeLimitCount: [{ required: true, message: '每人限领个数不能为空', trigger: 'blur' }],
  233. validityType: [{ required: true, message: '有效期类型不能为空', trigger: 'change' }],
  234. validTimes: [{ required: true, message: '固定日期不能为空', trigger: 'change' }],
  235. fixedStartTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
  236. fixedEndTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
  237. productScope: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }],
  238. productSpuIds: [{ required: true, message: '商品不能为空', trigger: 'blur' }],
  239. productCategoryIds: [{ required: true, message: '分类不能为空', trigger: 'blur' }]
  240. })
  241. const formRef = ref() // 表单 Ref
  242. const productSpus = ref<ProductSpuApi.Spu[]>([]) // 商品列表
  243. /** 打开弹窗 */
  244. const open = async (type: string, id?: number) => {
  245. dialogVisible.value = true
  246. dialogTitle.value = t('action.' + type)
  247. formType.value = type
  248. resetForm()
  249. // 修改时,设置数据
  250. if (id) {
  251. formLoading.value = true
  252. try {
  253. const data = await CouponTemplateApi.getCouponTemplate(id)
  254. formData.value = {
  255. ...data,
  256. discountPrice: data.discountPrice !== undefined ? data.discountPrice / 100.0 : undefined,
  257. discountPercent:
  258. data.discountPercent !== undefined ? data.discountPercent / 10.0 : undefined,
  259. discountLimitPrice:
  260. data.discountLimitPrice !== undefined ? data.discountLimitPrice / 100.0 : undefined,
  261. usePrice: data.usePrice !== undefined ? data.usePrice / 100.0 : undefined,
  262. validTimes: [data.validStartTime, data.validEndTime]
  263. }
  264. // 获得商品范围
  265. await getProductScope()
  266. } finally {
  267. formLoading.value = false
  268. }
  269. }
  270. }
  271. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  272. /** 提交表单 */
  273. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  274. const submitForm = async () => {
  275. // 校验表单
  276. if (!formRef) return
  277. const valid = await formRef.value.validate()
  278. if (!valid) return
  279. // 提交请求
  280. formLoading.value = true
  281. try {
  282. const data = {
  283. ...formData.value,
  284. discountPrice:
  285. formData.value.discountPrice !== undefined ? formData.value.discountPrice * 100 : undefined,
  286. discountPercent:
  287. formData.value.discountPercent !== undefined
  288. ? formData.value.discountPercent * 10
  289. : undefined,
  290. discountLimitPrice:
  291. formData.value.discountLimitPrice !== undefined
  292. ? formData.value.discountLimitPrice * 100
  293. : undefined,
  294. usePrice: formData.value.usePrice !== undefined ? formData.value.usePrice * 100 : undefined,
  295. validStartTime:
  296. formData.value.validTimes && formData.value.validTimes.length === 2
  297. ? formData.value.validTimes[0]
  298. : undefined,
  299. validEndTime:
  300. formData.value.validTimes && formData.value.validTimes.length === 2
  301. ? formData.value.validTimes[1]
  302. : undefined
  303. } as unknown as CouponTemplateApi.CouponTemplateVO
  304. // 设置商品范围
  305. setProductScopeValues(data)
  306. if (formType.value === 'create') {
  307. await CouponTemplateApi.createCouponTemplate(data)
  308. message.success(t('common.createSuccess'))
  309. } else {
  310. await CouponTemplateApi.updateCouponTemplate(data)
  311. message.success(t('common.updateSuccess'))
  312. }
  313. dialogVisible.value = false
  314. // 发送操作成功的事件
  315. emit('success')
  316. } finally {
  317. formLoading.value = false
  318. }
  319. }
  320. /** 重置表单 */
  321. const resetForm = () => {
  322. formData.value = {
  323. id: undefined,
  324. name: undefined,
  325. discountType: PromotionDiscountTypeEnum.PRICE.type,
  326. discountPrice: undefined,
  327. discountPercent: undefined,
  328. discountLimitPrice: undefined,
  329. usePrice: undefined,
  330. takeType: 1,
  331. totalCount: undefined,
  332. takeLimitCount: undefined,
  333. validityType: CouponTemplateValidityTypeEnum.DATE.type,
  334. validTimes: [],
  335. validStartTime: undefined,
  336. validEndTime: undefined,
  337. fixedStartTerm: undefined,
  338. fixedEndTerm: undefined,
  339. productScope: PromotionProductScopeEnum.ALL.scope,
  340. productScopeValues: [],
  341. productSpuIds: [],
  342. productCategoryIds: []
  343. }
  344. formRef.value?.resetFields()
  345. productSpus.value = []
  346. }
  347. /** 获得商品范围 */
  348. const getProductScope = async () => {
  349. switch (formData.value.productScope) {
  350. case PromotionProductScopeEnum.SPU.scope:
  351. // 设置商品编号
  352. formData.value.productSpuIds = formData.value.productScopeValues
  353. // 获得商品列表
  354. productSpus.value = await ProductSpuApi.getSpuDetailList(formData.value.productScopeValues)
  355. break
  356. case PromotionProductScopeEnum.CATEGORY.scope:
  357. await nextTick(() => {
  358. let productCategoryIds = formData.value.productScopeValues
  359. if (Array.isArray(productCategoryIds) && productCategoryIds.length > 0) {
  360. // 单选时使用数组不能反显
  361. productCategoryIds = productCategoryIds[0]
  362. }
  363. // 设置品类编号
  364. formData.value.productCategoryIds = productCategoryIds
  365. })
  366. break
  367. default:
  368. break
  369. }
  370. }
  371. /** 设置商品范围 */
  372. function setProductScopeValues(data: CouponTemplateApi.CouponTemplateVO) {
  373. switch (formData.value.productScope) {
  374. case PromotionProductScopeEnum.SPU.scope:
  375. data.productScopeValues = formData.value.productSpuIds
  376. break
  377. case PromotionProductScopeEnum.CATEGORY.scope:
  378. data.productScopeValues = Array.isArray(formData.value.productCategoryIds)
  379. ? formData.value.productCategoryIds
  380. : [formData.value.productCategoryIds]
  381. break
  382. default:
  383. break
  384. }
  385. }
  386. /** 活动商品的按钮 */
  387. const spuTableSelectRef = ref()
  388. const openSpuTableSelect = () => {
  389. spuTableSelectRef.value.open(productSpus.value)
  390. }
  391. /** 选择商品后触发 */
  392. const handleSpuSelected = (spus: ProductSpuApi.Spu[]) => {
  393. productSpus.value = spus
  394. formData.value.productSpuIds = spus.map((spu) => spu.id) as []
  395. }
  396. /** 选择商品后触发 */
  397. const handleRemoveSpu = (index: number) => {
  398. productSpus.value.splice(index, 1)
  399. formData.value.productSpuIds.splice(index, 1)
  400. }
  401. </script>
  402. <style scoped lang="scss">
  403. .select-box {
  404. display: flex;
  405. align-items: center;
  406. justify-content: center;
  407. border: 1px dashed var(--el-border-color-darker);
  408. border-radius: 8px;
  409. width: 60px;
  410. height: 60px;
  411. }
  412. .spu-pic {
  413. position: relative;
  414. }
  415. .del-icon {
  416. position: absolute;
  417. z-index: 1;
  418. width: 20px !important;
  419. height: 20px !important;
  420. right: -10px;
  421. top: -10px;
  422. }
  423. </style>