PromotionManagement.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <template>
  2. <div class="promotion-management admin-page">
  3. <el-card>
  4. <el-tabs v-model="activeTab" @tab-click="handleTabChange">
  5. <el-tab-pane label="邀请里程碑" name="milestone">
  6. <div style="margin-bottom: 16px">
  7. <el-input v-model="milestoneKeyword" placeholder="搜索里程碑..." prefix-icon="el-icon-search" style="width:200px" clearable @keyup.enter.native="handleMilestoneSearch" @clear="handleMilestoneSearch" />
  8. </div>
  9. <el-table :data="filteredMilestones" v-loading="milestoneLoading" border stripe :max-height="tableHeight">
  10. <el-table-column prop="milestone" label="里程碑" min-width="180" show-overflow-tooltip />
  11. <el-table-column prop="rewardPoints" label="奖励积分" width="120" />
  12. <el-table-column prop="rewardEnergy" label="奖励能量" width="120" />
  13. <el-table-column label="操作" width="140" fixed="right">
  14. <template slot-scope="{ row }">
  15. <el-button size="mini" @click="handleEditMilestone(row)" :disabled="row.claimed">
  16. {{ row.claimed ? '已达成' : '编辑' }}
  17. </el-button>
  18. </template>
  19. </el-table-column>
  20. </el-table>
  21. </el-tab-pane>
  22. <el-tab-pane label="排行榜" name="leaderboard">
  23. <div style="margin-bottom: 16px; display: flex; gap: 16px; align-items: center; flex-wrap: wrap">
  24. <el-date-picker
  25. v-model="weekStart"
  26. type="date"
  27. placeholder="选择周起始日"
  28. value-format="yyyy-MM-dd"
  29. @change="handleWeekChange"
  30. style="width: 180px"
  31. />
  32. <el-input
  33. v-model="searchKeyword"
  34. placeholder="搜索用户昵称"
  35. style="width: 200px"
  36. clearable
  37. @keyup.enter.native="handleSearch"
  38. />
  39. <el-button type="primary" @click="handleSearch">搜索</el-button>
  40. <el-button @click="resetLeaderboard">重置</el-button>
  41. </div>
  42. <el-table :data="leaderboardList" v-loading="leaderboardLoading" border stripe :max-height="tableHeight">
  43. <el-table-column label="排名" width="80" type="index" :index="leaderboardIndex" />
  44. <el-table-column label="头像" width="80">
  45. <template slot-scope="{ row }">
  46. <el-avatar :src="row.avatar" size="small" />
  47. </template>
  48. </el-table-column>
  49. <el-table-column prop="nickname" label="昵称" min-width="140" show-overflow-tooltip />
  50. <el-table-column prop="referralCount" label="邀请数" width="100" />
  51. </el-table>
  52. <el-pagination
  53. v-if="leaderboardTotal > leaderboardSize"
  54. @current-change="onLeaderboardPageChange"
  55. :current-page="leaderboardPage"
  56. :page-size="leaderboardSize"
  57. :total="leaderboardTotal"
  58. layout="total, prev, pager, next"
  59. style="margin-top: 20px; text-align: right"
  60. />
  61. </el-tab-pane>
  62. </el-tabs>
  63. </el-card>
  64. <el-dialog title="编辑里程碑" :visible.sync="milestoneDialogVisible" width="450px">
  65. <el-form :model="milestoneForm" label-width="120px">
  66. <el-form-item label="里程碑" required>
  67. <el-input v-model="milestoneForm.milestone" disabled />
  68. </el-form-item>
  69. <el-form-item label="奖励积分" required>
  70. <el-input-number v-model="milestoneForm.rewardPoints" :min="0" style="width: 200px" />
  71. </el-form-item>
  72. <el-form-item label="奖励能量" required>
  73. <el-input-number v-model="milestoneForm.rewardEnergy" :min="0" style="width: 200px" />
  74. </el-form-item>
  75. </el-form>
  76. <div slot="footer">
  77. <el-button @click="milestoneDialogVisible = false">取消</el-button>
  78. <el-button type="primary" @click="handleMilestoneSubmit" :loading="milestoneSubmitting">保存</el-button>
  79. </div>
  80. </el-dialog>
  81. </div>
  82. </template>
  83. <script>
  84. import { getMilestoneConfig, updateMilestoneConfig, getLeaderboardTop, getLeaderboardByUser } from '@/api/promotion'
  85. export default {
  86. name: 'PromotionManagement',
  87. data() {
  88. return {
  89. activeTab: 'milestone',
  90. milestoneList: [],
  91. milestoneKeyword: '',
  92. milestoneLoading: false,
  93. milestoneDialogVisible: false,
  94. milestoneSubmitting: false,
  95. milestoneForm: {
  96. milestone: '',
  97. rewardPoints: 0,
  98. rewardEnergy: 0
  99. },
  100. leaderboardList: [],
  101. leaderboardLoading: false,
  102. weekStart: this.getCurrentWeekStart(),
  103. searchKeyword: '',
  104. leaderboardPage: 1,
  105. leaderboardSize: 20,
  106. leaderboardTotal: 0,
  107. searchMode: false
  108. }
  109. },
  110. created() {
  111. this.loadMilestones()
  112. },
  113. computed: {
  114. tableHeight() {
  115. return window.innerHeight - 300
  116. },
  117. filteredMilestones() {
  118. if (!this.milestoneKeyword) return this.milestoneList
  119. var kw = this.milestoneKeyword.toLowerCase()
  120. return this.milestoneList.filter(function(item) {
  121. return item.milestone && item.milestone.toLowerCase().indexOf(kw) !== -1
  122. })
  123. }
  124. },
  125. methods: {
  126. getCurrentWeekStart() {
  127. const now = new Date()
  128. const day = now.getDay()
  129. const diff = now.getDate() - day + (day === 0 ? -6 : 1)
  130. const monday = new Date(now.setDate(diff))
  131. return monday.toISOString().split('T')[0]
  132. },
  133. leaderboardIndex(index) {
  134. return (this.leaderboardPage - 1) * this.leaderboardSize + index + 1
  135. },
  136. handleMilestoneSearch() {},
  137. handleTabChange() {
  138. if (this.activeTab === 'leaderboard') {
  139. this.loadLeaderboard()
  140. } else {
  141. this.loadMilestones()
  142. }
  143. },
  144. async loadMilestones() {
  145. this.milestoneLoading = true
  146. try {
  147. const res = await getMilestoneConfig()
  148. if (res.data) {
  149. this.milestoneList = res.data.records || res.data.list || res.data
  150. }
  151. } catch (e) {
  152. console.error(e)
  153. this.$message.error('加载里程碑配置失败')
  154. } finally {
  155. this.milestoneLoading = false
  156. }
  157. },
  158. handleEditMilestone(row) {
  159. if (row.claimed) return
  160. this.milestoneForm = {
  161. milestone: row.milestone,
  162. rewardPoints: row.rewardPoints,
  163. rewardEnergy: row.rewardEnergy
  164. }
  165. this.milestoneDialogVisible = true
  166. },
  167. async handleMilestoneSubmit() {
  168. this.milestoneSubmitting = true
  169. try {
  170. await updateMilestoneConfig({
  171. milestone: this.milestoneForm.milestone,
  172. rewardPoints: this.milestoneForm.rewardPoints,
  173. rewardEnergy: this.milestoneForm.rewardEnergy
  174. })
  175. this.$message.success('保存成功')
  176. this.milestoneDialogVisible = false
  177. this.loadMilestones()
  178. } catch (e) {
  179. this.$message.error('保存失败')
  180. } finally {
  181. this.milestoneSubmitting = false
  182. }
  183. },
  184. async loadLeaderboard() {
  185. this.leaderboardLoading = true
  186. this.searchMode = false
  187. try {
  188. const params = { page: this.leaderboardPage, size: this.leaderboardSize }
  189. if (this.weekStart) {
  190. params.weekStart = this.weekStart
  191. }
  192. const res = await getLeaderboardTop(params)
  193. if (res.data) {
  194. this.leaderboardList = res.data.records || res.data.list || res.data
  195. this.leaderboardTotal = res.data.total || this.leaderboardList.length
  196. }
  197. } catch (e) {
  198. console.error(e)
  199. this.$message.error('加载排行榜失败')
  200. } finally {
  201. this.leaderboardLoading = false
  202. }
  203. },
  204. handleWeekChange() {
  205. if (this.activeTab === 'leaderboard') {
  206. this.leaderboardPage = 1
  207. this.loadLeaderboard()
  208. }
  209. },
  210. async handleSearch() {
  211. if (!this.searchKeyword.trim()) {
  212. this.resetLeaderboard()
  213. return
  214. }
  215. this.leaderboardLoading = true
  216. this.searchMode = true
  217. try {
  218. const res = await getLeaderboardByUser({ keyword: this.searchKeyword.trim() })
  219. if (res.data) {
  220. this.leaderboardList = res.data.records || res.data.list || res.data
  221. this.leaderboardTotal = this.leaderboardList.length
  222. }
  223. } catch (e) {
  224. console.error(e)
  225. this.$message.error('搜索失败')
  226. } finally {
  227. this.leaderboardLoading = false
  228. }
  229. },
  230. resetLeaderboard() {
  231. this.searchKeyword = ''
  232. this.leaderboardPage = 1
  233. this.loadLeaderboard()
  234. },
  235. onLeaderboardPageChange(p) {
  236. this.leaderboardPage = p
  237. this.loadLeaderboard()
  238. }
  239. }
  240. }
  241. </script>
  242. <style scoped>
  243. .promotion-management {
  244. padding: 20px;
  245. }
  246. </style>