coupon-detail.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. <template>
  2. <view class="page">
  3. <NavBar :title="isCreate ? '新建优惠券' : '优惠券详情'" :showBack="true" />
  4. <scroll-view class="detail-scroll" scroll-y>
  5. <view class="section">
  6. <text class="section-title">基本信息</text>
  7. <view class="form-item">
  8. <text class="form-label">优惠券名称</text>
  9. <input class="form-input" v-model="form.name" placeholder="请输入名称" />
  10. </view>
  11. <view class="form-item">
  12. <text class="form-label">优惠类型</text>
  13. <picker :range="typeOptions" range-key="label" :value="typeIndex" @change="onTypeChange" :disabled="!isCreate">
  14. <view class="form-picker">{{ typeOptions[typeIndex]?.label || '请选择' }}</view>
  15. </picker>
  16. </view>
  17. <view class="form-item" v-if="form.type === 1">
  18. <text class="form-label">满减金额</text>
  19. <input class="form-input" v-model="form.amount" type="digit" placeholder="抵扣金额" />
  20. </view>
  21. <view class="form-item" v-if="form.type === 2">
  22. <text class="form-label">折扣比例(%)</text>
  23. <input class="form-input" v-model="form.discount" type="number" placeholder="如 80 表示8折" />
  24. </view>
  25. <view class="form-item">
  26. <text class="form-label">最低消费(元)</text>
  27. <input class="form-input" v-model="form.minAmount" type="digit" placeholder="使用门槛" />
  28. </view>
  29. <view class="form-item">
  30. <text class="form-label">开始日期</text>
  31. <picker mode="date" :value="form.startTime" @change="onStartDateChange">
  32. <view class="form-picker">{{ form.startTime || '请选择' }}</view>
  33. </picker>
  34. </view>
  35. <view class="form-item">
  36. <text class="form-label">结束日期</text>
  37. <picker mode="date" :value="form.endTime" @change="onEndDateChange">
  38. <view class="form-picker">{{ form.endTime || '请选择' }}</view>
  39. </picker>
  40. </view>
  41. <view class="form-item">
  42. <text class="form-label">发行总量</text>
  43. <input class="form-input" v-model="form.totalQuantity" type="number" placeholder="发行数量" />
  44. </view>
  45. </view>
  46. <view class="section" v-if="!isCreate && stats">
  47. <text class="section-title">统计数据</text>
  48. <view class="stats-grid">
  49. <view class="stat-item"><text class="stat-value">{{ stats.receivedCount || 0 }}</text><text class="stat-label">已领取</text></view>
  50. <view class="stat-item"><text class="stat-value">{{ stats.usedCount || 0 }}</text><text class="stat-label">已使用</text></view>
  51. <view class="stat-item"><text class="stat-value">{{ formatMoney(stats.totalDiscount || 0) }}</text><text class="stat-label">总优惠</text></view>
  52. </view>
  53. <view class="distribute-btn" @click="handleDistribute" v-if="form.status === 1"><text>发放优惠券</text></view>
  54. </view>
  55. </scroll-view>
  56. <view class="bottom-actions">
  57. <view class="save-btn" @click="isCreate ? handleCreate() : handleSave()"><text>{{ isCreate ? '创建优惠券' : '保存修改' }}</text></view>
  58. </view>
  59. </view>
  60. </template>
  61. <script setup lang="ts">
  62. import { ref } from 'vue'
  63. import { onLoad } from '@dcloudio/uni-app';
  64. import NavBar from '@/components/NavBar.vue';
  65. import { getCouponById, createCoupon, updateCouponStatus, distributeCoupon, getCouponStatistics } from '@/api/coupon';
  66. import { CouponTypeText } from '@/utils/constants';
  67. import { formatMoney as formatMoneyUtil, showToast, showConfirm } from '@/utils/common';
  68. const typeOptions = [{ label: CouponTypeText[1], value: 1 }, { label: CouponTypeText[2], value: 2 }];
  69. const isCreate = ref(true);
  70. const typeIndex = ref(0);
  71. const form = ref<any>({ type: 1, startTime: '', endTime: '' });
  72. const stats = ref<any>(null);
  73. const formatMoney = formatMoneyUtil;
  74. onLoad((options?: Record<string, string>) => {
  75. const id = options?.id ? Number(options.id) : 0;
  76. if (id > 0) { isCreate.value = false; loadDetail(id); }
  77. });
  78. const loadDetail = async (id: number) => {
  79. try {
  80. const data = await getCouponById(id);
  81. form.value = data;
  82. typeIndex.value = typeOptions.findIndex(t => t.value === data.type);
  83. if (typeIndex.value < 0) typeIndex.value = 0;
  84. loadStats(id);
  85. } catch { showToast('加载失败'); uni.navigateBack(); }
  86. };
  87. const loadStats = async (id: number) => {
  88. try { stats.value = await getCouponStatistics(id); } catch { /* ignore */ }
  89. };
  90. const onTypeChange = (e: any) => { typeIndex.value = e.detail.value; form.value.type = typeOptions[typeIndex.value].value; };
  91. const onStartDateChange = (e: any) => { form.value.startTime = e.detail.value; };
  92. const onEndDateChange = (e: any) => { form.value.endTime = e.detail.value; };
  93. const handleCreate = async () => {
  94. try { await createCoupon(form.value); showToast('创建成功', 'success'); setTimeout(() => uni.navigateBack(), 1500); } catch { showToast('创建失败'); }
  95. };
  96. const handleSave = async () => {
  97. try { await updateCouponStatus(form.value.id, form.value.status); showToast('保存成功', 'success'); } catch { showToast('保存失败'); }
  98. };
  99. const handleDistribute = async () => {
  100. if (!(await showConfirm('确认向全部用户发放此优惠券?'))) return;
  101. try { await distributeCoupon({ templateId: form.value.id, targetType: 1 }); showToast('发放成功', 'success'); } catch { showToast('发放失败'); }
  102. };
  103. </script>
  104. <style lang="scss" scoped>
  105. .page { height: 100vh; background: $bg-color-page; display: flex; flex-direction: column; padding-bottom: env(safe-area-inset-bottom); }
  106. .detail-scroll { flex: 1; height: 0; }
  107. .section { background: $bg-color-card; margin-bottom: $spacing-2; padding: $spacing-3 $spacing-4; }
  108. .section-title { display: block; font-size: 28rpx; font-weight: 600; color: $text-color-primary; margin-bottom: $spacing-3; padding-bottom: $spacing-2; border-bottom: 1rpx solid $border-color-light; }
  109. .form-item { display: flex; align-items: center; justify-content: space-between; padding: $spacing-2 0; border-bottom: 1rpx solid $border-color-light; }
  110. .form-item:last-child { border-bottom: none; }
  111. .form-label { font-size: 28rpx; color: $text-color-secondary; flex-shrink: 0; width: 180rpx; }
  112. .form-input { flex: 1; text-align: right; font-size: 28rpx; color: $text-color-primary; }
  113. .form-picker { flex: 1; text-align: right; font-size: 28rpx; color: $text-color-muted; }
  114. .stats-grid { display: flex; }
  115. .stat-item { flex: 1; display: flex; flex-direction: column; align-items: center; padding: $spacing-2; }
  116. .stat-value { font-size: $font-size-xl; font-weight: 700; color: $text-color-primary; margin-bottom: $spacing-1; }
  117. .stat-label { font-size: 22rpx; color: $text-color-muted; }
  118. .distribute-btn { margin-top: $spacing-4; background: $primary-color; padding: 24rpx 0; border-radius: $radius-base; display: flex; align-items: center; justify-content: center; }
  119. .distribute-btn text { font-size: 28rpx; font-weight: 600; color: $text-color-primary; }
  120. .bottom-actions { padding: $spacing-3 $spacing-4; padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); background: $bg-color-card; border-top: 1rpx solid $border-color-light; flex-shrink: 0; }
  121. .save-btn { width: 100%; background: $primary-color; padding: 28rpx 0; border-radius: $radius-xl; display: flex; align-items: center; justify-content: center; }
  122. .save-btn text { font-size: $font-size-lg; font-weight: 600; color: $text-color-primary; }
  123. </style>