|
|
@@ -136,6 +136,7 @@
|
|
|
<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-item v-if="row.requireRegistration === 1" command="roster" icon="el-icon-collection">报名名册</el-dropdown-item>
|
|
|
</el-dropdown-menu>
|
|
|
</el-dropdown>
|
|
|
</template>
|
|
|
@@ -157,6 +158,52 @@
|
|
|
/>
|
|
|
</el-card>
|
|
|
|
|
|
+ <!-- 报名名册对话框(T03) -->
|
|
|
+ <el-dialog
|
|
|
+ :title="'报名名册 - ' + (currentActivity ? currentActivity.title : '')"
|
|
|
+ :visible.sync="rosterDialogVisible"
|
|
|
+ width="600px"
|
|
|
+ >
|
|
|
+ <div v-loading="rosterLoading" class="roster-body">
|
|
|
+ <el-row :gutter="12" class="roster-stats mb-12">
|
|
|
+ <el-col :span="8">
|
|
|
+ <div class="stat-box">
|
|
|
+ <div class="stat-num">{{ rosterData.approvedCount != null ? rosterData.approvedCount : '-' }}</div>
|
|
|
+ <div class="stat-label">已通过报名</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <div class="stat-box">
|
|
|
+ <div class="stat-num">{{ rosterData.pendingCount != null ? rosterData.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': rosterData.remaining != null && rosterData.remaining <= 0 }">
|
|
|
+ {{ rosterData.remaining != null ? rosterData.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>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="rosterDialogVisible = false">关闭</el-button>
|
|
|
+ <el-button type="primary" :disabled="!rosterRegistrants.length" @click="exportRosterCsv">导出 CSV</el-button>
|
|
|
+ </span>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
<!-- 报名列表对话框(保留) -->
|
|
|
<el-dialog
|
|
|
:title="'报名列表 - ' + (currentActivity ? currentActivity.title : '')"
|
|
|
@@ -194,7 +241,7 @@
|
|
|
<script>
|
|
|
import SortMixin from '@/mixins/SortMixin'
|
|
|
import ListStateMixin from '@/mixins/ListStateMixin'
|
|
|
-import { getActivityList, publishActivity, endActivity, getRegistrationList, approveRegistration, rejectRegistration } from '@/api/activity'
|
|
|
+import { getActivityList, publishActivity, endActivity, getRegistrationList, approveRegistration, rejectRegistration, getRegistrationRoster } from '@/api/activity'
|
|
|
|
|
|
const STATUS_MAP = {
|
|
|
draft: { label: '草稿', type: 'info' },
|
|
|
@@ -240,6 +287,11 @@ export default {
|
|
|
regLoading: false,
|
|
|
currentActivity: null,
|
|
|
registrations: [],
|
|
|
+ // 报名名册对话框(T03)
|
|
|
+ rosterDialogVisible: false,
|
|
|
+ rosterLoading: false,
|
|
|
+ rosterData: { approvedCount: 0, pendingCount: 0, remaining: null },
|
|
|
+ rosterRegistrants: [],
|
|
|
sortableColumns: { id: 'ID', title: '活动标题', dimensionCode: '维度', location: '地点', startTime: '开始时间', endTime: '结束时间', createdAt: '创建时间' }
|
|
|
}
|
|
|
},
|
|
|
@@ -340,6 +392,9 @@ export default {
|
|
|
case 'registrations':
|
|
|
this.showRegistrations(row)
|
|
|
break
|
|
|
+ case 'roster':
|
|
|
+ this.showRoster(row)
|
|
|
+ break
|
|
|
}
|
|
|
},
|
|
|
handleSearch() {
|
|
|
@@ -416,6 +471,43 @@ export default {
|
|
|
} catch (e) {
|
|
|
this.$message.error('操作失败')
|
|
|
}
|
|
|
+ },
|
|
|
+ // ===== 报名名册(T03) =====
|
|
|
+ showRoster(row) {
|
|
|
+ this.currentActivity = row
|
|
|
+ this.rosterDialogVisible = true
|
|
|
+ this.loadRoster(row.id)
|
|
|
+ },
|
|
|
+ async loadRoster(activityId) {
|
|
|
+ this.rosterLoading = true
|
|
|
+ try {
|
|
|
+ const res = await getRegistrationRoster(activityId)
|
|
|
+ if (res && res.data) {
|
|
|
+ this.rosterData = 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)
|
|
|
+ })
|
|
|
+ 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.currentActivity ? this.currentActivity.title : '活动') + '.csv'
|
|
|
+ link.click()
|
|
|
+ URL.revokeObjectURL(link.href)
|
|
|
+ this.$message.success('已导出 ' + this.rosterRegistrants.length + ' 条报名记录')
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -461,4 +553,21 @@ export default {
|
|
|
.btn-no-pad { padding: 0; }
|
|
|
.text-muted-inline { color: var(--adm-text-45); }
|
|
|
.empty-hint { text-align: center; color: var(--adm-text-45); }
|
|
|
+
|
|
|
+/* T03 报名名册 */
|
|
|
+.roster-body { min-height: 80px; }
|
|
|
+.mb-12 { 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-stats { margin-bottom: 12px; }
|
|
|
+.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-88); margin-bottom: 8px; }
|
|
|
+.roster-list-body { max-height: 220px; 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-88); }
|
|
|
+.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>
|