Activities.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <template>
  2. <div class="activities admin-page">
  3. <el-card>
  4. <div slot="header" class="admin-page-header">
  5. <span class="admin-page-title">活动管理</span>
  6. <div>
  7. <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
  8. <el-select
  9. v-model="filter.status"
  10. placeholder="状态筛选"
  11. size="mini"
  12. clearable
  13. @change="loadActivities"
  14. >
  15. <el-option label="全部状态" value="" />
  16. <el-option label="草稿" value="draft" />
  17. <el-option label="已发布" value="published" />
  18. <el-option label="已结束" value="ended" />
  19. </el-select>
  20. <el-button type="primary" size="mini" @click="$router.push('/activity-edit')">创建活动</el-button>
  21. </div>
  22. </div>
  23. <div class="table-scroll-wrap-lg">
  24. <div style="overflow:auto;max-height:calc(100vh - 300px);">
  25. <el-table style="max-height:calc(100vh - 300px);" :data="activities" v-loading="loading" border stripe>
  26. <el-table-column prop="id" label="ID" width="80" />
  27. <el-table-column label="封面" width="80">
  28. <template slot-scope="{ row }">
  29. <img v-if="row.coverImage" :src="row.coverImage" style="width:40px;height:40px;object-fit:cover;border-radius:4px;" />
  30. <span v-else style="color:#999">无</span>
  31. </template>
  32. </el-table-column>
  33. <el-table-column prop="title" label="活动标题" min-width="160" show-overflow-tooltip />
  34. <el-table-column prop="dimensionCode" label="维度" width="100">
  35. <template slot-scope="{ row }">{{ dimensionLabel(row.dimensionCode) }}</template>
  36. </el-table-column>
  37. <el-table-column label="类型" width="90">
  38. <template slot-scope="{ row }">{{ activityTypeLabel(row.activityType) }}</template>
  39. </el-table-column>
  40. <el-table-column prop="location" label="地点" width="120" show-overflow-tooltip />
  41. <el-table-column label="费用(元)" width="90">
  42. <template slot-scope="{ row }">{{ row.price ? (row.price / 100).toFixed(2) : '免费' }}</template>
  43. </el-table-column>
  44. <el-table-column label="人数" width="90">
  45. <template slot-scope="{ row }">
  46. <el-button type="text" size="mini" @click="showRegistrations(row)" style="padding:0;">
  47. {{ row.currentParticipants || 0 }}/{{ row.maxParticipants || '不限' }}
  48. </el-button>
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="状态" width="100">
  52. <template slot-scope="{ row }">
  53. <el-tag :type="statusType(row.status)" size="mini">
  54. {{ statusLabel(row.status) }}
  55. </el-tag>
  56. </template>
  57. </el-table-column>
  58. <el-table-column prop="startTime" label="开始时间" width="170" />
  59. <el-table-column prop="endTime" label="结束时间" width="170" />
  60. <el-table-column label="操作" width="200" fixed="right">
  61. <template slot-scope="{ row }">
  62. <el-button size="mini" type="primary" @click="$router.push('/activity-edit?id=' + row.id)">编辑</el-button>
  63. <el-dropdown trigger="hover" @command="(cmd) => handleMoreCmd(row, cmd)">
  64. <el-button size="mini">
  65. 更多<i class="el-icon-arrow-down el-icon--right"></i>
  66. </el-button>
  67. <el-dropdown-menu slot="dropdown">
  68. <el-dropdown-item v-if="row.status !== 'draft'" command="view" icon="el-icon-view">查看</el-dropdown-item>
  69. <el-dropdown-item v-if="row.status === 'draft'" command="publish" icon="el-icon-upload2">发布</el-dropdown-item>
  70. <el-dropdown-item v-if="row.status === 'published'" command="end" icon="el-icon-circle-close">结束</el-dropdown-item>
  71. <el-dropdown-item command="registrations" icon="el-icon-document">报名列表</el-dropdown-item>
  72. </el-dropdown-menu>
  73. </el-dropdown>
  74. </template>
  75. </el-table-column>
  76. </el-table></div>
  77. </div>
  78. <el-pagination
  79. v-if="total > 0"
  80. @size-change="handleSizeChange"
  81. @current-change="handlePageChange"
  82. :current-page="filter.page"
  83. :page-sizes="[10, 20, 50]"
  84. :page-size="filter.size"
  85. layout="total, sizes, prev, pager, next, jumper"
  86. :total="total"
  87. class="pagination-wrap"
  88. />
  89. </el-card>
  90. <!-- 报名列表对话框(保留) -->
  91. <el-dialog
  92. :title="'报名列表 - ' + (currentActivity ? currentActivity.title : '')"
  93. :visible.sync="regDialogVisible"
  94. width="800px"
  95. @closed="regDialogVisible = false"
  96. >
  97. <el-table :data="registrations" v-loading="regLoading" border stripe>
  98. <el-table-column prop="id" label="ID" width="60" />
  99. <el-table-column prop="userId" label="用户ID" width="70" />
  100. <el-table-column prop="childId" label="孩子ID" width="70" />
  101. <el-table-column label="状态" width="90">
  102. <template slot-scope="{ row }">
  103. <el-tag :type="row.status === 'approved' ? 'success' : row.status === 'rejected' ? 'danger' : 'warning'">
  104. {{ row.status === 'approved' ? '已通过' : row.status === 'rejected' ? '已拒绝' : '待审核' }}
  105. </el-tag>
  106. </template>
  107. </el-table-column>
  108. <el-table-column prop="registeredAt" label="报名时间" width="160">
  109. <template slot-scope="{ row }">
  110. {{ formatTime(row.registeredAt || row.createdAt) }}
  111. </template>
  112. </el-table-column>
  113. <el-table-column label="操作" width="200" fixed="right">
  114. <template slot-scope="{ row }">
  115. <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleRegApprove(row)">通过</el-button>
  116. <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="handleRegReject(row)">拒绝</el-button>
  117. <span v-else style="color: #999;">已处理</span>
  118. </template>
  119. </el-table-column>
  120. </el-table>
  121. <p v-if="!registrations.length && !regLoading" style="text-align:center;color:#999;">暂无报名记录</p>
  122. </el-dialog>
  123. </div>
  124. </template>
  125. <script>
  126. import { getActivityList, publishActivity, endActivity, getRegistrationList, approveRegistration, rejectRegistration } from '@/api/activity'
  127. const STATUS_MAP = {
  128. draft: { label: '草稿', type: 'info' },
  129. published: { label: '已发布', type: 'success' },
  130. ended: { label: '已结束', type: 'danger' }
  131. }
  132. const ACTIVITY_TYPE_MAP = {
  133. offline: '线下活动',
  134. online: '线上活动',
  135. campaign: '主题活动'
  136. }
  137. const DIMENSION_MAP = {
  138. body: '身',
  139. mind: '智',
  140. action: '行',
  141. wealth: '富',
  142. heart: '心'
  143. }
  144. export default {
  145. name: 'Activities',
  146. data() {
  147. return {
  148. loading: false,
  149. activities: [],
  150. total: 0,
  151. keyword: '',
  152. filter: {
  153. status: '',
  154. page: 1,
  155. size: 10
  156. },
  157. // 报名列表对话框
  158. regDialogVisible: false,
  159. regLoading: false,
  160. currentActivity: null,
  161. registrations: []
  162. }
  163. },
  164. created() {
  165. this.loadActivities()
  166. },
  167. methods: {
  168. statusType(status) {
  169. return (STATUS_MAP[status] && STATUS_MAP[status].type) || 'info'
  170. },
  171. statusLabel(status) {
  172. return (STATUS_MAP[status] && STATUS_MAP[status].label) || status
  173. },
  174. activityTypeLabel(type) {
  175. return ACTIVITY_TYPE_MAP[type] || type || '—'
  176. },
  177. dimensionLabel(code) {
  178. return DIMENSION_MAP[code] || code || '—'
  179. },
  180. async loadActivities() {
  181. this.loading = true
  182. try {
  183. const params = {
  184. page: this.filter.page,
  185. size: this.filter.size
  186. }
  187. if (this.filter.status) {
  188. params.status = this.filter.status
  189. }
  190. if (this.keyword) {
  191. params.keyword = this.keyword
  192. }
  193. const res = await getActivityList(params)
  194. this.activities = (res.data && res.data.records) || []
  195. this.total = (res.data && res.data.total) || 0
  196. } catch (e) {
  197. this.$message && this.$message.error && this.$message.error('加载活动列表失败')
  198. } finally {
  199. this.loading = false
  200. }
  201. },
  202. handleSizeChange(size) {
  203. this.filter.size = size
  204. this.filter.page = 1
  205. this.loadActivities()
  206. },
  207. handlePageChange(page) {
  208. this.filter.page = page
  209. this.loadActivities()
  210. },
  211. async handleMoreCmd(row, cmd) {
  212. switch (cmd) {
  213. case 'view':
  214. this.$router.push('/activity-edit?id=' + row.id + '&readonly=1')
  215. break
  216. case 'publish':
  217. try {
  218. await this.$confirm('确认发布该活动?', '提示')
  219. await publishActivity(row.id)
  220. this.$message.success('发布成功')
  221. this.loadActivities()
  222. } catch (e) {
  223. if (e !== 'cancel') {
  224. this.$message.error('发布失败')
  225. }
  226. }
  227. break
  228. case 'end':
  229. try {
  230. await this.$confirm('确认结束该活动?该操作不可逆', '提示')
  231. await endActivity(row.id)
  232. this.$message.success('活动已结束')
  233. this.loadActivities()
  234. } catch (e) {
  235. if (e !== 'cancel') {
  236. this.$message.error('操作失败')
  237. }
  238. }
  239. break
  240. case 'registrations':
  241. this.showRegistrations(row)
  242. break
  243. }
  244. },
  245. handleSearch() {
  246. this.filter.page = 1
  247. this.loadActivities()
  248. },
  249. async handlePublish(row) {
  250. try {
  251. await this.$confirm('确认发布该活动?', '提示')
  252. await publishActivity(row.id)
  253. this.$message.success('发布成功')
  254. this.loadActivities()
  255. } catch (e) {
  256. if (e !== 'cancel') {
  257. this.$message.error('发布失败')
  258. }
  259. }
  260. },
  261. async handleEnd(row) {
  262. try {
  263. await this.$confirm('确认结束该活动?该操作不可逆', '提示')
  264. await endActivity(row.id)
  265. this.$message.success('活动已结束')
  266. this.loadActivities()
  267. } catch (e) {
  268. if (e !== 'cancel') {
  269. this.$message.error('操作失败')
  270. }
  271. }
  272. },
  273. // ===== 报名列表 =====
  274. showRegistrations(row) {
  275. this.currentActivity = row
  276. this.regDialogVisible = true
  277. this.loadRegistrations(row.id)
  278. },
  279. async loadRegistrations(activityId) {
  280. this.regLoading = true
  281. try {
  282. const res = await getRegistrationList({ activityId })
  283. if (res.code === 200) {
  284. this.registrations = res.data || []
  285. }
  286. } catch (e) {
  287. this.$message.error('加载报名列表失败')
  288. } finally {
  289. this.regLoading = false
  290. }
  291. },
  292. formatTime(t) {
  293. if (!t) return '-'
  294. return t.replace ? t.replace('T', ' ').substring(0, 19) : t
  295. },
  296. async handleRegApprove(row) {
  297. try {
  298. const res = await approveRegistration({ registrationId: row.id })
  299. if (res.code === 200) {
  300. this.$message.success('已通过')
  301. this.loadRegistrations(this.currentActivity.id)
  302. } else {
  303. this.$message.error(res.message || '操作失败')
  304. }
  305. } catch (e) {
  306. this.$message.error('操作失败')
  307. }
  308. },
  309. async handleRegReject(row) {
  310. try {
  311. const res = await rejectRegistration({ registrationId: row.id })
  312. if (res.code === 200) {
  313. this.$message.success('已拒绝')
  314. this.loadRegistrations(this.currentActivity.id)
  315. } else {
  316. this.$message.error(res.message || '操作失败')
  317. }
  318. } catch (e) {
  319. this.$message.error('操作失败')
  320. }
  321. }
  322. }
  323. }
  324. </script>
  325. <style scoped>
  326. .activities { padding: 20px; flex: 1; display: flex; flex-direction: column; min-height: 0; }
  327. .activities > .el-card { flex: 1; display: flex; flex-direction: column; min-height: 0; }
  328. .activities > .el-card >>> .el-card__body { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; }
  329. .activities >>> .el-table { flex: 1; }
  330. .activities >>> .el-table__body-wrapper { overflow-y: auto; }
  331. </style>