Activities.vue 14 KB

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