| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- <template>
- <div class="activities admin-page">
- <el-card>
- <div slot="header" class="admin-page-header">
- <span class="admin-page-title">活动管理</span>
- <el-button type="success" size="mini" icon="el-icon-plus" @click="$router.push('/activity-edit')">创建活动</el-button>
- </div>
- <div class="filter-bar" style="margin-bottom:12px;gap:12px;">
- <el-input
- v-model="keyword"
- placeholder="搜索活动标题/地点"
- prefix-icon="el-icon-search"
- clearable
- class="header-search"
- @keyup.enter.native="handleSearch"
- @clear="handleSearch"
- />
- <el-select
- v-model="filter.status"
- placeholder="活动状态"
- clearable
- @change="loadActivities"
- style="width:130px;"
- >
- <el-option label="全部状态" value="" />
- <el-option label="草稿" value="draft" />
- <el-option label="已发布" value="published" />
- <el-option label="已结束" value="ended" />
- </el-select>
- <el-select
- v-model="filter.dimensionCode"
- placeholder="所属维度"
- clearable
- @change="loadActivities"
- style="width:120px;"
- >
- <el-option label="全部维度" value="" />
- <el-option label="身" value="body" />
- <el-option label="心" value="mind" />
- <el-option label="智" value="wisdom" />
- <el-option label="行" value="action" />
- <el-option label="富" value="wealth" />
- </el-select>
- <el-select
- v-model="filter.auditStatus"
- placeholder="审核状态"
- clearable
- @change="loadActivities"
- style="width:130px;"
- >
- <el-option label="全部审核状态" value="" />
- <el-option label="待审核" value="pending" />
- <el-option label="已通过" value="approved" />
- <el-option label="已拒绝" value="rejected" />
- </el-select>
- <el-button type="primary" size="mini" @click="handleSearch">查询</el-button>
- <el-button size="mini" @click="resetSearch">重置</el-button>
- </div>
- <el-table :data="activities" v-loading="loading" border stripe style="min-width:900px;" :max-height="tableHeight">
- <el-table-column prop="id" label="ID" width="80" />
- <el-table-column label="封面" width="80">
- <template slot-scope="{ row }">
- <img v-if="row.coverImage" :src="row.coverImage" style="width:40px;height:40px;object-fit:cover;border-radius:4px;" />
- <span v-else style="color:#999">无</span>
- </template>
- </el-table-column>
- <el-table-column prop="title" label="活动标题" min-width="160" show-overflow-tooltip />
- <el-table-column prop="dimensionCode" label="维度" width="100">
- <template slot-scope="{ row }">{{ dimensionLabel(row.dimensionCode) }}</template>
- </el-table-column>
- <el-table-column label="类型" width="90">
- <template slot-scope="{ row }">{{ activityTypeLabel(row.activityType) }}</template>
- </el-table-column>
- <el-table-column prop="location" label="地点" width="120" show-overflow-tooltip />
- <el-table-column label="费用(元)" width="90">
- <template slot-scope="{ row }">{{ row.price ? (row.price / 100).toFixed(2) : '免费' }}</template>
- </el-table-column>
- <el-table-column label="会员价(元)" width="90">
- <template slot-scope="{ row }">{{ row.memberPrice && row.memberPrice > 0 ? (row.memberPrice / 100).toFixed(2) : '-' }}</template>
- </el-table-column>
- <el-table-column label="人数" width="90">
- <template slot-scope="{ row }">
- <el-button type="text" size="mini" @click="showRegistrations(row)" style="padding:0;">
- {{ row.currentParticipants || 0 }}/{{ row.maxParticipants || '不限' }}
- </el-button>
- </template>
- </el-table-column>
- <el-table-column label="状态" width="100">
- <template slot-scope="{ row }">
- <el-tag :type="statusType(row.status)" size="mini">
- {{ statusLabel(row.status) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="startTime" label="开始时间" width="170" />
- <el-table-column prop="endTime" label="结束时间" width="170" />
- <el-table-column label="操作" width="200" fixed="right">
- <template slot-scope="{ row }">
- <el-button size="mini" type="primary" @click="$router.push('/activity-edit?id=' + row.id)">编辑</el-button>
- <el-dropdown trigger="hover" @command="(cmd) => handleMoreCmd(row, cmd)">
- <el-button size="mini">
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item v-if="row.status !== 'draft'" command="view" icon="el-icon-view">查看</el-dropdown-item>
- <el-dropdown-item v-if="row.status === 'draft'" command="publish" icon="el-icon-upload2">发布</el-dropdown-item>
- <el-dropdown-item v-if="row.status === 'published'" command="end" icon="el-icon-circle-close">结束</el-dropdown-item>
- <el-dropdown-item command="registrations" icon="el-icon-document">报名列表</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </template>
- </el-table-column>
- </el-table>
- <el-pagination
- v-if="total > 0"
- @size-change="handleSizeChange"
- @current-change="handlePageChange"
- :current-page="filter.page"
- :page-sizes="[10, 20, 50]"
- :page-size="filter.size"
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- class="pagination-wrap"
- />
- </el-card>
- <!-- 报名列表对话框(保留) -->
- <el-dialog
- :title="'报名列表 - ' + (currentActivity ? currentActivity.title : '')"
- :visible.sync="regDialogVisible"
- width="800px"
- @closed="regDialogVisible = false"
- >
- <el-table :data="registrations" v-loading="regLoading" border stripe :max-height="300">
- <el-table-column prop="id" label="ID" width="60" />
- <el-table-column prop="userId" label="用户ID" width="70" />
- <el-table-column prop="childId" label="孩子ID" width="70" />
- <el-table-column label="状态" width="90">
- <template slot-scope="{ row }">
- <el-tag :type="row.status === 'approved' ? 'success' : row.status === 'rejected' ? 'danger' : 'warning'">
- {{ row.status === 'approved' ? '已通过' : row.status === 'rejected' ? '已拒绝' : '待审核' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="registeredAt" label="报名时间" width="160">
- <template slot-scope="{ row }">
- {{ formatTime(row.registeredAt || row.createdAt) }}
- </template>
- </el-table-column>
- <el-table-column label="操作" width="200" fixed="right">
- <template slot-scope="{ row }">
- <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleRegApprove(row)">通过</el-button>
- <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="handleRegReject(row)">拒绝</el-button>
- <span v-else style="color: #999;">已处理</span>
- </template>
- </el-table-column>
- </el-table>
- <p v-if="!registrations.length && !regLoading" style="text-align:center;color:#999;">暂无报名记录</p>
- </el-dialog>
- </div>
- </template>
- <script>
- import { getActivityList, publishActivity, endActivity, getRegistrationList, approveRegistration, rejectRegistration } from '@/api/activity'
- const STATUS_MAP = {
- draft: { label: '草稿', type: 'info' },
- published: { label: '已发布', type: 'success' },
- ended: { label: '已结束', type: 'danger' }
- }
- const ACTIVITY_TYPE_MAP = {
- offline: '线下活动',
- online: '线上活动',
- campaign: '主题活动'
- }
- const DIMENSION_MAP = {
- body: '身',
- mind: '智',
- action: '行',
- wealth: '富',
- heart: '心'
- }
- export default {
- name: 'Activities',
- computed: {
- tableHeight() {
- return window.innerHeight - 300
- }
- },
- data() {
- return {
- loading: false,
- activities: [],
- total: 0,
- keyword: '',
- filter: {
- status: '',
- dimensionCode: '',
- auditStatus: '',
- page: 1,
- size: 20
- },
- // 报名列表对话框
- regDialogVisible: false,
- regLoading: false,
- currentActivity: null,
- registrations: []
- }
- },
- created() {
- this.loadActivities()
- },
- methods: {
- statusType(status) {
- return (STATUS_MAP[status] && STATUS_MAP[status].type) || 'info'
- },
- statusLabel(status) {
- return (STATUS_MAP[status] && STATUS_MAP[status].label) || status
- },
- activityTypeLabel(type) {
- return ACTIVITY_TYPE_MAP[type] || type || '—'
- },
- dimensionLabel(code) {
- return DIMENSION_MAP[code] || code || '—'
- },
- async loadActivities() {
- this.loading = true
- try {
- const params = {
- page: this.filter.page,
- size: this.filter.size
- }
- if (this.filter.status) {
- params.status = this.filter.status
- }
- if (this.filter.dimensionCode) {
- params.dimensionCode = this.filter.dimensionCode
- }
- if (this.filter.auditStatus) {
- params.auditStatus = this.filter.auditStatus
- }
- if (this.keyword) {
- params.keyword = this.keyword
- }
- const res = await getActivityList(params)
- this.activities = (res.data && res.data.records) || []
- this.total = (res.data && res.data.total) || 0
- } catch (e) {
- this.$message && this.$message.error && this.$message.error('加载活动列表失败')
- } finally {
- this.loading = false
- }
- },
- handleSizeChange(size) {
- this.filter.size = size
- this.filter.page = 1
- this.loadActivities()
- },
- handlePageChange(page) {
- this.filter.page = page
- this.loadActivities()
- },
- async handleMoreCmd(row, cmd) {
- switch (cmd) {
- case 'view':
- this.$router.push('/activity-edit?id=' + row.id + '&readonly=1')
- break
- case 'publish':
- try {
- await this.$confirm('确认发布该活动?', '提示')
- await publishActivity(row.id)
- this.$message.success('发布成功')
- this.loadActivities()
- } catch (e) {
- if (e !== 'cancel') {
- this.$message.error('发布失败')
- }
- }
- break
- case 'end':
- try {
- await this.$confirm('确认结束该活动?该操作不可逆', '提示')
- await endActivity(row.id)
- this.$message.success('活动已结束')
- this.loadActivities()
- } catch (e) {
- if (e !== 'cancel') {
- this.$message.error('操作失败')
- }
- }
- break
- case 'registrations':
- this.showRegistrations(row)
- break
- }
- },
- handleSearch() {
- this.filter.page = 1
- this.loadActivities()
- },
- async handlePublish(row) {
- try {
- await this.$confirm('确认发布该活动?', '提示')
- await publishActivity(row.id)
- this.$message.success('发布成功')
- this.loadActivities()
- } catch (e) {
- if (e !== 'cancel') {
- this.$message.error('发布失败')
- }
- }
- },
- async handleEnd(row) {
- try {
- await this.$confirm('确认结束该活动?该操作不可逆', '提示')
- await endActivity(row.id)
- this.$message.success('活动已结束')
- this.loadActivities()
- } catch (e) {
- if (e !== 'cancel') {
- this.$message.error('操作失败')
- }
- }
- },
- // ===== 报名列表 =====
- showRegistrations(row) {
- this.currentActivity = row
- this.regDialogVisible = true
- this.loadRegistrations(row.id)
- },
- async loadRegistrations(activityId) {
- this.regLoading = true
- try {
- const res = await getRegistrationList({ activityId })
- if (res.code === 200) {
- this.registrations = res.data || []
- }
- } catch (e) {
- this.$message.error('加载报名列表失败')
- } finally {
- this.regLoading = false
- }
- },
- formatTime(t) {
- if (!t) return '-'
- return t.replace ? t.replace('T', ' ').substring(0, 19) : t
- },
- async handleRegApprove(row) {
- try {
- const res = await approveRegistration({ registrationId: row.id })
- if (res.code === 200) {
- this.$message.success('已通过')
- this.loadRegistrations(this.currentActivity.id)
- } else {
- this.$message.error(res.message || '操作失败')
- }
- } catch (e) {
- this.$message.error('操作失败')
- }
- },
- async handleRegReject(row) {
- try {
- const res = await rejectRegistration({ registrationId: row.id })
- if (res.code === 200) {
- this.$message.success('已拒绝')
- this.loadRegistrations(this.currentActivity.id)
- } else {
- this.$message.error(res.message || '操作失败')
- }
- } catch (e) {
- this.$message.error('操作失败')
- }
- }
- }
- }
- </script>
- <style scoped>
- .activities {
- padding: 20px;
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
- .activities > .el-card {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
- .activities > .el-card >>> .el-card__body {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- overflow-y: auto;
- }
- .activities >>> .el-table {
- flex: 1;
- }
- .activities >>> .el-table__body-wrapper {
- overflow-y: auto;
- }
- .admin-page-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 12px;
- }
- .admin-page-title {
- font-size: 18px;
- font-weight: bold;
- }
- .admin-page-actions {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
- }
- .header-search {
- width: 220px;
- }
- </style>
|