CouponManagement.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <template>
  2. <div class="coupon-management admin-page">
  3. <el-card>
  4. <div slot="header" class="admin-page-header">
  5. <span class="admin-page-title">优惠券管理</span>
  6. <div class="admin-page-actions">
  7. <el-input v-model="keyword" placeholder="搜索优惠券..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
  8. <el-button type="primary" size="small" @click="handleCreate">新建优惠券</el-button>
  9. <el-button type="success" size="small" @click="handleBatchIssue">批量发放</el-button>
  10. </div>
  11. </div>
  12. <div class="table-scroll-wrap-sm">
  13. <div style="overflow:auto;max-height:calc(100vh - 300px);">
  14. <el-table style="max-height:calc(100vh - 300px);" :data="list" v-loading="loading" border stripe>
  15. <el-table-column prop="id" label="ID" width="70" />
  16. <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
  17. <el-table-column label="类型" width="90">
  18. <template slot-scope="{ row }">
  19. <el-tag size="mini">{{ row.type }}</el-tag>
  20. </template>
  21. </el-table-column>
  22. <el-table-column label="面值(元)" width="90">
  23. <template slot-scope="{ row }">{{ (row.value / 100).toFixed(2) }}</template>
  24. </el-table-column>
  25. <el-table-column label="使用门槛(元)" width="110">
  26. <template slot-scope="{ row }">{{ row.minSpend ? (row.minSpend / 100).toFixed(2) : '无' }}</template>
  27. </el-table-column>
  28. <el-table-column prop="applicableTo" label="适用对象" width="110">
  29. <template slot-scope="{ row }">{{ applicableLabel(row.applicableTo) }}</template>
  30. </el-table-column>
  31. <el-table-column label="有效期" width="180">
  32. <template slot-scope="{ row }">{{ row.validFrom }} ~ {{ row.validUntil }}</template>
  33. </el-table-column>
  34. <el-table-column prop="totalCount" label="总量" width="70" />
  35. <el-table-column prop="usedCount" label="已用" width="70" />
  36. <el-table-column label="状态" width="80">
  37. <template slot-scope="{ row }">
  38. <el-tag :type="row.status === 'active' ? 'success' : 'info'" size="mini">
  39. {{ row.status === 'active' ? '启用' : '停用' }}
  40. </el-tag>
  41. </template>
  42. </el-table-column>
  43. <el-table-column label="操作" width="180" fixed="right">
  44. <template slot-scope="{ row }">
  45. <el-button size="mini" type="primary" @click="handleEdit(row)">编辑</el-button>
  46. <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
  47. <el-button size="mini">
  48. 更多<i class="el-icon-arrow-down el-icon--right"></i>
  49. </el-button>
  50. <el-dropdown-menu slot="dropdown">
  51. <el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
  52. </el-dropdown-menu>
  53. </el-dropdown>
  54. </template>
  55. </el-table-column>
  56. </el-table></div>
  57. </div>
  58. <el-pagination
  59. @current-change="onPageChange"
  60. @size-change="onPageChange"
  61. :current-page="page"
  62. :page-size="size"
  63. :total="total"
  64. layout="total, prev, pager, next"
  65. class="pagination-wrap"
  66. />
  67. </el-card>
  68. <el-dialog :visible.sync="formDialogVisible" :title="isEdit ? '编辑优惠券' : '新建优惠券'" width="550px">
  69. <el-form :model="form" label-width="120px" ref="couponForm">
  70. <el-form-item label="优惠券名称" required>
  71. <el-input v-model="form.name" placeholder="请输入优惠券名称" />
  72. </el-form-item>
  73. <el-form-item label="类型" required>
  74. <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
  75. <el-option label="FIXED" value="FIXED" />
  76. </el-select>
  77. </el-form-item>
  78. <el-form-item label="面值(元)" required>
  79. <el-input-number v-model="form.valueYuan" :min="0.01" :step="0.5" :precision="2" style="width: 200px" />
  80. </el-form-item>
  81. <el-form-item label="使用门槛(元)">
  82. <el-input-number v-model="form.minSpendYuan" :min="0" :step="10" :precision="2" style="width: 200px" />
  83. </el-form-item>
  84. <el-form-item label="适用对象">
  85. <el-select v-model="form.applicableTo" placeholder="请选择适用对象" style="width: 100%">
  86. <el-option label="全部商品" value="ALL" />
  87. <el-option label="会员服务" value="MEMBERSHIP" />
  88. <el-option label="特定商品" value="PRODUCT" />
  89. <el-option label="测评服务" value="ASSESSMENT" />
  90. </el-select>
  91. </el-form-item>
  92. <el-form-item label="有效期起" required>
  93. <el-date-picker v-model="form.validFrom" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" style="width: 100%" />
  94. </el-form-item>
  95. <el-form-item label="有效期止" required>
  96. <el-date-picker v-model="form.validUntil" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd" style="width: 100%" />
  97. </el-form-item>
  98. <el-form-item label="发放总量" required>
  99. <el-input-number v-model="form.totalCount" :min="1" :step="100" style="width: 200px" />
  100. </el-form-item>
  101. </el-form>
  102. <div slot="footer">
  103. <el-button @click="formDialogVisible = false">取消</el-button>
  104. <el-button type="primary" @click="handleSubmit" :loading="submitting">保存</el-button>
  105. </div>
  106. </el-dialog>
  107. <el-dialog title="批量发放优惠券" :visible.sync="batchDialogVisible" width="500px">
  108. <el-form label-width="120px">
  109. <el-form-item label="选择优惠券" required>
  110. <el-select v-model="batchCouponId" placeholder="请选择优惠券" style="width: 100%">
  111. <el-option v-for="c in couponOptions" :key="c.id" :label="c.name" :value="c.id" />
  112. </el-select>
  113. </el-form-item>
  114. <el-form-item label="用户ID列表" required>
  115. <el-input v-model="batchUserIds" type="textarea" :rows="6" placeholder="每行一个用户ID&#10;例如:&#10;1001&#10;1002&#10;1003" />
  116. </el-form-item>
  117. </el-form>
  118. <div slot="footer">
  119. <el-button @click="batchDialogVisible = false">取消</el-button>
  120. <el-button type="primary" @click="confirmBatchIssue" :loading="batchSubmitting">确认发放</el-button>
  121. </div>
  122. </el-dialog>
  123. </div>
  124. </template>
  125. <script>
  126. import { getCouponList, createCoupon, updateCoupon, deleteCoupon, batchIssueCoupons } from '@/api/coupon'
  127. export default {
  128. name: 'CouponManagement',
  129. data() {
  130. return {
  131. list: [],
  132. loading: false,
  133. page: 1,
  134. size: 20,
  135. total: 0,
  136. formDialogVisible: false,
  137. isEdit: false,
  138. submitting: false,
  139. form: this.getEmptyForm(),
  140. batchDialogVisible: false,
  141. batchCouponId: '',
  142. batchUserIds: '',
  143. batchSubmitting: false,
  144. couponOptions: [],
  145. keyword: ''
  146. }
  147. },
  148. created() {
  149. this.loadData()
  150. },
  151. methods: {
  152. getEmptyForm() {
  153. return {
  154. id: null,
  155. name: '',
  156. type: 'FIXED',
  157. valueYuan: null,
  158. minSpendYuan: null,
  159. applicableTo: 'ALL',
  160. validFrom: '',
  161. validUntil: '',
  162. totalCount: null
  163. }
  164. },
  165. applicableLabel(val) {
  166. const map = { ALL: '全部商品', MEMBERSHIP: '会员服务', PRODUCT: '特定商品', ASSESSMENT: '测评服务' }
  167. return map[val] || val
  168. },
  169. async loadData() {
  170. this.loading = true
  171. try {
  172. const res = await getCouponList({ page: this.page, size: this.size, keyword: this.keyword || undefined })
  173. if (res.data) {
  174. this.list = res.data.records || res.data.list || res.data
  175. this.total = res.data.total || this.list.length
  176. }
  177. } catch (e) {
  178. console.error(e)
  179. } finally {
  180. this.loading = false
  181. }
  182. },
  183. handleSearch() {
  184. this.page = 1
  185. this.loadData()
  186. },
  187. onPageChange(p) {
  188. if (p) this.page = p
  189. this.loadData()
  190. },
  191. handleCreate() {
  192. this.isEdit = false
  193. this.form = this.getEmptyForm()
  194. this.formDialogVisible = true
  195. },
  196. handleEdit(row) {
  197. this.isEdit = true
  198. this.form = {
  199. id: row.id,
  200. name: row.name,
  201. type: row.type,
  202. valueYuan: row.value / 100,
  203. minSpendYuan: row.minSpend ? row.minSpend / 100 : null,
  204. applicableTo: row.applicableTo,
  205. validFrom: row.validFrom,
  206. validUntil: row.validUntil,
  207. totalCount: row.totalCount
  208. }
  209. this.formDialogVisible = true
  210. },
  211. async handleDelete(row) {
  212. try {
  213. await this.$confirm('确定删除该优惠券吗?', '提示', { type: 'warning' })
  214. await deleteCoupon(row.id)
  215. this.$message.success('删除成功')
  216. this.loadData()
  217. } catch (e) {
  218. if (e !== 'cancel') this.$message.error('删除失败')
  219. }
  220. },
  221. async handleSubmit() {
  222. if (!this.form.name || !this.form.valueYuan || !this.form.totalCount || !this.form.validFrom || !this.form.validUntil) {
  223. this.$message.warning('请填写完整信息')
  224. return
  225. }
  226. this.submitting = true
  227. try {
  228. const payload = {
  229. name: this.form.name,
  230. type: this.form.type,
  231. value: Math.round(this.form.valueYuan * 100),
  232. minSpend: this.form.minSpendYuan ? Math.round(this.form.minSpendYuan * 100) : 0,
  233. applicableTo: this.form.applicableTo,
  234. validFrom: this.form.validFrom,
  235. validUntil: this.form.validUntil,
  236. totalCount: this.form.totalCount
  237. }
  238. if (this.isEdit) payload.id = this.form.id
  239. if (this.isEdit) {
  240. await updateCoupon(payload)
  241. } else {
  242. await createCoupon(payload)
  243. }
  244. this.$message.success('保存成功')
  245. this.formDialogVisible = false
  246. this.loadData()
  247. } catch (e) {
  248. this.$message.error('保存失败')
  249. } finally {
  250. this.submitting = false
  251. }
  252. },
  253. async handleBatchIssue() {
  254. this.batchCouponId = ''
  255. this.batchUserIds = ''
  256. this.batchDialogVisible = true
  257. try {
  258. const res = await getCouponList({ page: 1, size: 999 })
  259. this.couponOptions = res.data.records || res.data.list || res.data || []
  260. } catch (e) {
  261. console.error(e)
  262. }
  263. },
  264. async confirmBatchIssue() {
  265. if (!this.batchCouponId) {
  266. this.$message.warning('请选择优惠券')
  267. return
  268. }
  269. const ids = this.batchUserIds.split('\n').map(s => s.trim()).filter(Boolean)
  270. if (ids.length === 0) {
  271. this.$message.warning('请输入至少一个用户ID')
  272. return
  273. }
  274. this.batchSubmitting = true
  275. try {
  276. await batchIssueCoupons({ couponId: this.batchCouponId, userIds: ids })
  277. this.$message.success(`成功向 ${ids.length} 个用户发放优惠券`)
  278. this.batchDialogVisible = false
  279. this.loadData()
  280. } catch (e) {
  281. this.$message.error('发放失败')
  282. } finally {
  283. this.batchSubmitting = false
  284. }
  285. },
  286. handleActionCmd(row, cmd) {
  287. switch (cmd) {
  288. case 'delete':
  289. this.handleDelete(row)
  290. break
  291. }
  292. }
  293. }
  294. }
  295. </script>
  296. <style scoped>
  297. .coupon-management { padding: 20px; }
  298. </style>