Explorar el Código

chore: auto bump version and changelog [skip ci]

iwt hace 2 semanas
padre
commit
2115236710

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-936416908336efa78a85566e1261ad4dd0efb648
+b1309f8683c0e74e2dd82448a0c98bdb0447beb2

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1774",
+  "version": "1.0.1775",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1774",
+      "version": "1.0.1775",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1775",
+  "version": "1.0.1776",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 16 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,22 @@
 
 ---
 
+## v1.0.1776 (2026-10-23)
+
+### 新功能
+- T12 管理端活动签到审核页
+
+### 其他
+- - 新增 ActivityCheckinVO(含 userName/userPhone/checkInByName/activityTitle 冗余展示字段,字段对齐 activity_checkins)
+- - ActivityCheckinService 新增 pageByActivity(支持 reviewStatus/checkInType 筛选+分页 total)与 exportByActivity(全量导出)
+- - AdminActivityCheckinController:list 支持筛选并返回 total;新增 /export 端点
+- 前端(cfc-web)
+- - 新增 ActivityCheckinManage.vue 管理页:活动筛选、已签到/未签到 Tab、通过/拒绝审核、手动补录(操作人+时间留痕)、CSV 导出
+- - api/activity.js 增加 getCheckinList/exportCheckins/addCheckin/reviewCheckin
+- - router/Layout.vue 增加路由与菜单项
+- 
+
+
 ## v1.0.1775 (2026-10-23)
 
 ### 测试

+ 17 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1775
+> 当前版本: v1.0.1776
 
 ## 历史版本
 
@@ -8,6 +8,22 @@
 
 ---
 
+## v1.0.1776 (2026-10-23)
+
+### 新功能
+- T12 管理端活动签到审核页
+
+### 其他
+- - 新增 ActivityCheckinVO(含 userName/userPhone/checkInByName/activityTitle 冗余展示字段,字段对齐 activity_checkins)
+- - ActivityCheckinService 新增 pageByActivity(支持 reviewStatus/checkInType 筛选+分页 total)与 exportByActivity(全量导出)
+- - AdminActivityCheckinController:list 支持筛选并返回 total;新增 /export 端点
+- 前端(cfc-web)
+- - 新增 ActivityCheckinManage.vue 管理页:活动筛选、已签到/未签到 Tab、通过/拒绝审核、手动补录(操作人+时间留痕)、CSV 导出
+- - api/activity.js 增加 getCheckinList/exportCheckins/addCheckin/reviewCheckin
+- - router/Layout.vue 增加路由与菜单项
+- 
+
+
 ## v1.0.1775 (2026-10-23)
 
 ### 测试

+ 110 - 1
cfc-web/src/views/admin/Activities.vue

@@ -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>

+ 1 - 1
cfc-web/src/views/admin/ActivityEdit.vue

@@ -783,7 +783,7 @@ activityId
 .roster-list-title {
   font-size: 13px;
   font-weight: 600;
-  color: var(--adm-text-strong);
+  color: var(--adm-text-88);
   margin-bottom: 8px;
 }
 .roster-list-body {