|
@@ -30,6 +30,51 @@
|
|
|
class="mb-16"
|
|
class="mb-16"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
|
|
+ <!-- 报名名册(T03:仅需报名的活动展示,详情页/编辑页均可查看) -->
|
|
|
|
|
+ <el-card v-if="showRosterCard" class="roster-card" shadow="never">
|
|
|
|
|
+ <div slot="header" class="roster-card-header">
|
|
|
|
|
+ <span>报名名册</span>
|
|
|
|
|
+ <div class="roster-card-actions">
|
|
|
|
|
+ <el-button size="mini" :loading="rosterLoading" @click="loadRoster">刷新</el-button>
|
|
|
|
|
+ <el-button size="mini" type="primary" :disabled="!rosterRegistrants.length" @click="exportRosterCsv">导出 CSV</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-loading="rosterLoading" class="roster-body">
|
|
|
|
|
+ <el-row :gutter="12" class="roster-stats">
|
|
|
|
|
+ <el-col :span="8">
|
|
|
|
|
+ <div class="stat-box">
|
|
|
|
|
+ <div class="stat-num">{{ roster.approvedCount != null ? roster.approvedCount : '-' }}</div>
|
|
|
|
|
+ <div class="stat-label">已通过报名</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="8">
|
|
|
|
|
+ <div class="stat-box">
|
|
|
|
|
+ <div class="stat-num">{{ roster.pendingCount != null ? roster.pendingCount : '-' }}</div>
|
|
|
|
|
+ <div class="stat-label">待审核</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="8">
|
|
|
|
|
+ <div class="stat-box">
|
|
|
|
|
+ <div class="stat-num" :class="{ 'stat-danger': roster.remaining != null && roster.remaining <= 0 }">
|
|
|
|
|
+ {{ roster.remaining != null ? roster.remaining : '不限' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-label">剩余名额</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ <div class="roster-list">
|
|
|
|
|
+ <div class="roster-list-title">最近报名人({{ rosterRegistrants.length }})</div>
|
|
|
|
|
+ <div v-if="rosterRegistrants.length" class="roster-list-body">
|
|
|
|
|
+ <div v-for="(r, idx) in rosterRegistrants" :key="'roster-' + idx" class="roster-item">
|
|
|
|
|
+ <span class="roster-name">{{ r.name }}</span>
|
|
|
|
|
+ <span class="roster-time">{{ formatTime(r.registeredAt) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="roster-empty">还没有人报名,快来抢占第一个名额吧</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
<div class="activity-edit-form-wrap">
|
|
<div class="activity-edit-form-wrap">
|
|
|
<el-form label-width="120px" v-loading="loading" :model="form" :rules="formRules" ref="form">
|
|
<el-form label-width="120px" v-loading="loading" :model="form" :rules="formRules" ref="form">
|
|
|
<el-form-item label="活动标题" prop="title">
|
|
<el-form-item label="活动标题" prop="title">
|
|
@@ -173,7 +218,7 @@
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
|
|
import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
|
|
|
-import { getActivityDetail, createActivity, updateActivity, publishActivity, submitActivityReview, reDraftActivity } from '@/api/activity'
|
|
|
|
|
|
|
+import { getActivityDetail, createActivity, updateActivity, publishActivity, submitActivityReview, reDraftActivity, getRegistrationRoster } from '@/api/activity'
|
|
|
import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
|
|
import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
|
|
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
|
import '@wangeditor/editor/dist/css/style.css'
|
|
import '@wangeditor/editor/dist/css/style.css'
|
|
@@ -233,10 +278,18 @@ export default {
|
|
|
},
|
|
},
|
|
|
editorConfig: null,
|
|
editorConfig: null,
|
|
|
toolbarConfig: {},
|
|
toolbarConfig: {},
|
|
|
- editorInstance: null
|
|
|
|
|
|
|
+ editorInstance: null,
|
|
|
|
|
+ // T03 报名名册
|
|
|
|
|
+ roster: { approvedCount: 0, pendingCount: 0, remaining: null },
|
|
|
|
|
+ rosterRegistrants: [],
|
|
|
|
|
+ rosterLoading: false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
|
|
|
+ showRosterCard() {
|
|
|
|
|
+ // 仅需报名的活动,在详情页(readonly)或编辑已发布活动页显示报名名册
|
|
|
|
|
+ return this.readonly && this.form.requireRegistration === 1
|
|
|
|
|
+ },
|
|
|
statusLabel() {
|
|
statusLabel() {
|
|
|
const map = { draft: '当前状态:草稿箱', pending: '当前状态:待审核(等待管理员审核)', rejected: '当前状态:已驳回', published: '当前状态:发布中', withdrawn: '当前状态:已撤回' }
|
|
const map = { draft: '当前状态:草稿箱', pending: '当前状态:待审核(等待管理员审核)', rejected: '当前状态:已驳回', published: '当前状态:发布中', withdrawn: '当前状态:已撤回' }
|
|
|
return map[this.activityStatus] || ''
|
|
return map[this.activityStatus] || ''
|
|
@@ -333,7 +386,8 @@ export default {
|
|
|
visibility: d.visibility || 'public',
|
|
visibility: d.visibility || 'public',
|
|
|
visibleScope: d.visibleScope || '',
|
|
visibleScope: d.visibleScope || '',
|
|
|
visibleTo: d.visibleTo || '',
|
|
visibleTo: d.visibleTo || '',
|
|
|
- checkinPoints: d.checkinPoints || 0
|
|
|
|
|
|
|
+ checkinPoints: d.checkinPoints || 0,
|
|
|
|
|
+ requireRegistration: d.requireRegistration != null ? d.requireRegistration : 0
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
const dwRes = await getDimensionWeights('activity', d.id)
|
|
const dwRes = await getDimensionWeights('activity', d.id)
|
|
@@ -342,6 +396,10 @@ export default {
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
}
|
|
}
|
|
|
|
|
+ // T03:需报名的活动自动拉取名册(详情页/编辑页都展示)
|
|
|
|
|
+ if (this.form.requireRegistration === 1) {
|
|
|
|
|
+ this.loadRoster()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
this.$message.error('加载活动详情失败')
|
|
this.$message.error('加载活动详情失败')
|
|
@@ -349,6 +407,43 @@ export default {
|
|
|
this.loading = false
|
|
this.loading = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
+ // ===== T03 报名名册 =====
|
|
|
|
|
+ formatTime(t) {
|
|
|
|
|
+ return t ? String(t).replace('T', ' ').substring(0, 19) : '-'
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadRoster() {
|
|
|
|
|
+ if (!this.$route.query.id) return
|
|
|
|
|
+ this.rosterLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getRegistrationRoster(this.$route.query.id)
|
|
|
|
|
+ if (res && res.data) {
|
|
|
|
|
+ this.roster = res.data
|
|
|
|
|
+ this.rosterRegistrants = res.data.registrants || []
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.$message.error('加载报名名册失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.rosterLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ exportRosterCsv() {
|
|
|
|
|
+ if (!this.rosterRegistrants.length) return
|
|
|
|
|
+ var rows = ['姓名,报名时间']
|
|
|
|
|
+ this.rosterRegistrants.forEach(function(r) {
|
|
|
|
|
+ var name = String(r.name || '').replace(/,/g, ',')
|
|
|
|
|
+ var time = r.registeredAt ? String(r.registeredAt).replace('T', ' ').substring(0, 19) : ''
|
|
|
|
|
+ rows.push(name + ',' + time)
|
|
|
|
|
+ })
|
|
|
|
|
+ // 汇总行(Excel 友好:UTF-8 BOM)
|
|
|
|
|
+ var csv = '\uFEFF' + rows.join('\r\n') + '\r\n'
|
|
|
|
|
+ var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
|
|
|
+ var link = document.createElement('a')
|
|
|
|
|
+ link.href = URL.createObjectURL(blob)
|
|
|
|
|
+ link.download = '报名名册-' + (this.form.title || '活动') + '.csv'
|
|
|
|
|
+ link.click()
|
|
|
|
|
+ URL.revokeObjectURL(link.href)
|
|
|
|
|
+ this.$message.success('已导出 ' + this.rosterRegistrants.length + ' 条报名记录')
|
|
|
|
|
+ },
|
|
|
beforeUpload(file) {
|
|
beforeUpload(file) {
|
|
|
const isImage = file.type === 'image/jpeg' || file.type === 'image/png' ||
|
|
const isImage = file.type === 'image/jpeg' || file.type === 'image/png' ||
|
|
|
file.type === 'image/gif' || file.type === 'image/webp'
|
|
file.type === 'image/gif' || file.type === 'image/webp'
|
|
@@ -638,4 +733,84 @@ activityId
|
|
|
padding-right: 8px;
|
|
padding-right: 8px;
|
|
|
margin-right: -8px;
|
|
margin-right: -8px;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== T03 报名名册 ===== */
|
|
|
|
|
+.roster-card {
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-card /deep/ .el-card__header {
|
|
|
|
|
+ padding: 10px 20px;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-card-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--adm-text-88);
|
|
|
|
|
+}
|
|
|
|
|
+.roster-card-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-stats {
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-box {
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ background: var(--adm-bg-header);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-num {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: var(--adm-text-88);
|
|
|
|
|
+}
|
|
|
|
|
+.stat-danger {
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--adm-text-45);
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-list {
|
|
|
|
|
+ border-top: 1px solid var(--adm-border-split);
|
|
|
|
|
+ padding-top: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-list-title {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--adm-text-strong);
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-list-body {
|
|
|
|
|
+ max-height: 200px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 6px 0;
|
|
|
|
|
+ border-bottom: 1px solid var(--adm-border-split);
|
|
|
|
|
+}
|
|
|
|
|
+.roster-item:last-child {
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+}
|
|
|
|
|
+.roster-name {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: var(--adm-text-strong);
|
|
|
|
|
+}
|
|
|
|
|
+.roster-time {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--adm-text-45);
|
|
|
|
|
+}
|
|
|
|
|
+.roster-empty {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: var(--adm-text-45);
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 20px 0;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|