|
|
@@ -0,0 +1,489 @@
|
|
|
+<template>
|
|
|
+ <div class="activity-checkin-manage admin-page">
|
|
|
+ <PageHeader title="活动签到审核">
|
|
|
+ <div class="filters">
|
|
|
+ <el-select v-model="activityId" placeholder="选择活动" filterable clearable style="width: 220px;" @change="loadCheckinList">
|
|
|
+ <el-option v-for="a in activityList" :key="a.id" :label="a.title" :value="a.id" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="checkInTypeFilter" placeholder="签到类型" clearable style="width: 130px;" @change="loadCheckinList">
|
|
|
+ <el-option label="全部类型" value="" />
|
|
|
+ <el-option label="扫码签到" value="qr" />
|
|
|
+ <el-option label="手动补录" value="admin" />
|
|
|
+ <el-option label="GPS签到" value="gps" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="reviewStatusFilter" placeholder="审核状态" clearable style="width: 130px;" @change="loadCheckinList">
|
|
|
+ <el-option label="全部状态" value="" />
|
|
|
+ <el-option label="待审核" value="pending" />
|
|
|
+ <el-option label="已通过" value="approved" />
|
|
|
+ <el-option label="已拒绝" value="rejected" />
|
|
|
+ </el-select>
|
|
|
+ <el-input v-model="keyword" placeholder="搜索姓名/手机号/用户ID" prefix-icon="el-icon-search" style="width: 220px;" clearable @keyup.enter.native="loadCheckinList" @clear="loadCheckinList" />
|
|
|
+ <el-button type="primary" size="mini" icon="el-icon-download" :disabled="!activityId || exporting" @click="handleExport">导出名单</el-button>
|
|
|
+ <el-button type="success" size="mini" icon="el-icon-plus" :disabled="!activityId" @click="showAddDialog">手动补录</el-button>
|
|
|
+ </div>
|
|
|
+ </PageHeader>
|
|
|
+
|
|
|
+ <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
|
|
+ <el-tab-pane label="已签到记录" name="checked" />
|
|
|
+ <el-tab-pane label="未签到人员" name="unchecked" />
|
|
|
+ </el-tabs>
|
|
|
+
|
|
|
+ <div class="adm-table-wrap">
|
|
|
+ <el-table v-show="activeTab === 'checked'" height="100%" :data="filteredRecords" v-loading="loading" border stripe>
|
|
|
+ <el-table-column prop="id" label="ID" width="70">
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="签到人" min-width="130">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <div class="user-cell">
|
|
|
+ <span class="user-name">{{ row.userName || ('用户' + row.userId) }}</span>
|
|
|
+ <span class="user-meta">ID: {{ row.userId }}</span>
|
|
|
+ <span v-if="row.userPhone" class="user-meta">{{ row.userPhone }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="checkInAt" label="签到时间" width="160">
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('checkInAt', $event)">签到时间<span v-if="sortIndex('checkInAt') !== null" class="sort-index" :class="sortClass('checkInAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('checkInAt') }}</span>{{ sortIcon('checkInAt') }}</span>
|
|
|
+ </template>
|
|
|
+ <template slot-scope="{ row }">{{ fmtLocalTime(row.checkInAt) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="签到类型" width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag size="mini" :type="checkInTypeTagType(row.checkInType)" effect="plain">{{ checkInTypeLabel(row.checkInType) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="审核状态" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag size="mini" :type="reviewStatusTagType(row.reviewStatus)">{{ reviewStatusLabel(row.reviewStatus) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="补录操作人" width="130">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span v-if="row.checkInBy">{{ row.checkInByName || ('操作人' + row.checkInBy) }}</span>
|
|
|
+ <span v-else class="text-muted">—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="registrationId" label="关联报名ID" width="110">
|
|
|
+ <template slot-scope="{ row }">{{ row.registrationId || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createdAt" label="创建时间" width="160">
|
|
|
+ <template slot-scope="{ row }">{{ fmtLocalTime(row.createdAt) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="150" fixed="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button v-if="row.reviewStatus !== 'approved'" type="success" size="mini" @click="handleReview(row, 'approved')">通过</el-button>
|
|
|
+ <el-button v-if="row.reviewStatus !== 'rejected'" type="danger" size="mini" @click="handleReview(row, 'rejected')">拒绝</el-button>
|
|
|
+ <span v-if="row.reviewStatus === 'approved'" class="text-muted">已通过</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <el-table v-show="activeTab === 'unchecked'" height="100%" :data="uncheckedList" v-loading="loading" border stripe>
|
|
|
+ <el-table-column prop="userId" label="用户ID" width="90" />
|
|
|
+ <el-table-column label="报名人" min-width="160">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <div class="user-cell">
|
|
|
+ <span class="user-name">{{ row.registrantName || placeholderName(row) }}</span>
|
|
|
+ <span v-if="row.registrantPhone" class="user-meta">{{ row.registrantPhone }}</span>
|
|
|
+ <span v-else-if="isSelfRegistration(row)" class="user-meta">本人报名</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="status" label="报名状态" width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag size="mini" :type="regStatusTagType(row.status)">{{ regStatusLabel(row.status) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="registeredAt" label="报名时间" width="170">
|
|
|
+ <template slot-scope="{ row }">{{ fmtLocalTime(row.registeredAt) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="120" fixed="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button type="primary" size="mini" @click="showAddDialog(row)">补录签到</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pagination-wrap">
|
|
|
+ <el-pagination
|
|
|
+ v-show="activeTab === 'checked'"
|
|
|
+ background
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="total"
|
|
|
+ :current-page="page"
|
|
|
+ :page-size="size"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-dialog title="手动补录签到" :visible.sync="addDialogVisible" width="460px">
|
|
|
+ <el-form label-width="90px">
|
|
|
+ <el-form-item label="活动">
|
|
|
+ <div class="readonly-field">{{ currentActivityTitle || ('活动 #' + activityId) }}</div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="签到人" required>
|
|
|
+ <el-select
|
|
|
+ v-model="addForm.userId"
|
|
|
+ filterable
|
|
|
+ remote
|
|
|
+ clearable
|
|
|
+ reserve-keyword
|
|
|
+ placeholder="输入姓名/手机号/用户ID搜索"
|
|
|
+ :remote-method="searchUsers"
|
|
|
+ :loading="searchLoading"
|
|
|
+ style="width: 100%;"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="u in userOptions"
|
|
|
+ :key="u.id"
|
|
|
+ :label="userLabel(u)"
|
|
|
+ :value="u.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="签到时间">
|
|
|
+ <div class="readonly-field">提交时由系统记录(当前操作时间)</div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div class="dialog-tip">补录后记录 check_in_type=admin、check_in_by=当前操作人,审核状态默认为「已通过」。</div>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="addDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitting" @click="handleAdd">确认补录</el-button>
|
|
|
+ </span>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getActivityList, getCheckinList, exportCheckins, addCheckin, reviewCheckin, getRegistrationList } from '@/api/activity'
|
|
|
+import { searchUsers } from '@/api/admin'
|
|
|
+import SortMixin from '@/mixins/SortMixin'
|
|
|
+
|
|
|
+const CHECKIN_TYPE_MAP = {
|
|
|
+ qr: { label: '扫码签到', type: 'success' },
|
|
|
+ admin: { label: '手动补录', type: 'warning' },
|
|
|
+ gps: { label: 'GPS签到', type: 'primary' }
|
|
|
+}
|
|
|
+
|
|
|
+const REVIEW_STATUS_MAP = {
|
|
|
+ pending: { label: '待审核', type: 'warning' },
|
|
|
+ approved: { label: '已通过', type: 'success' },
|
|
|
+ rejected: { label: '已拒绝', type: 'danger' }
|
|
|
+}
|
|
|
+
|
|
|
+const REG_STATUS_MAP = {
|
|
|
+ pending: { label: '待审核', type: 'warning' },
|
|
|
+ approved: { label: '已通过', type: 'success' },
|
|
|
+ rejected: { label: '已拒绝', type: 'danger' },
|
|
|
+ cancelled: { label: '已取消', type: 'info' }
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'ActivityCheckinManage',
|
|
|
+ mixins: [SortMixin],
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ sortableColumns: { id: 'ID', checkInAt: '签到时间' },
|
|
|
+ activityList: [],
|
|
|
+ activityId: null,
|
|
|
+ currentActivityTitle: '',
|
|
|
+ checkInTypeFilter: '',
|
|
|
+ reviewStatusFilter: '',
|
|
|
+ keyword: '',
|
|
|
+ activeTab: 'checked',
|
|
|
+ loading: false,
|
|
|
+ exporting: false,
|
|
|
+ records: [],
|
|
|
+ total: 0,
|
|
|
+ page: 1,
|
|
|
+ size: 10,
|
|
|
+ registrationList: [],
|
|
|
+ addDialogVisible: false,
|
|
|
+ submitting: false,
|
|
|
+ addForm: { userId: null },
|
|
|
+ userOptions: [],
|
|
|
+ searchLoading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ filteredRecords() {
|
|
|
+ var kw = this.keyword.trim().toLowerCase()
|
|
|
+ var list = this.records
|
|
|
+ if (kw) {
|
|
|
+ list = this.records.filter(function (row) {
|
|
|
+ return (row.userName && String(row.userName).toLowerCase().indexOf(kw) !== -1) ||
|
|
|
+ (row.userPhone && String(row.userPhone).indexOf(kw) !== -1) ||
|
|
|
+ (row.userId !== null && row.userId !== undefined && String(row.userId).indexOf(kw) !== -1)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 表头排序(sortState 由 SortMixin 维护,客户端生效)
|
|
|
+ if (this.sortState && this.sortState.length > 0) {
|
|
|
+ var copy = list.slice()
|
|
|
+ var sortState = this.sortState
|
|
|
+ copy.sort(function (a, b) {
|
|
|
+ for (var i = 0; i < sortState.length; i++) {
|
|
|
+ var s = sortState[i]
|
|
|
+ var av = a[s.column]
|
|
|
+ var bv = b[s.column]
|
|
|
+ if (av === null || av === undefined) av = ''
|
|
|
+ if (bv === null || bv === undefined) bv = ''
|
|
|
+ if (av < bv) return s.order === 'asc' ? -1 : 1
|
|
|
+ if (av > bv) return s.order === 'asc' ? 1 : -1
|
|
|
+ }
|
|
|
+ return 0
|
|
|
+ })
|
|
|
+ list = copy
|
|
|
+ }
|
|
|
+ return list
|
|
|
+ },
|
|
|
+ // 已报名但未签到的名单:以 activity_registrations 为基数减去同一 userId 的签到记录
|
|
|
+ uncheckedList() {
|
|
|
+ const checkedUserIds = new Set(this.records.map(function (r) { return r.userId }))
|
|
|
+ return this.registrationList.filter(function (reg) {
|
|
|
+ if (reg.status === 'rejected' || reg.status === 'cancelled') return false
|
|
|
+ return !checkedUserIds.has(reg.userId)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.loadActivities()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ checkInTypeTagType(type) {
|
|
|
+ return (CHECKIN_TYPE_MAP[type] && CHECKIN_TYPE_MAP[type].type) || 'info'
|
|
|
+ },
|
|
|
+ checkInTypeLabel(type) {
|
|
|
+ return (CHECKIN_TYPE_MAP[type] && CHECKIN_TYPE_MAP[type].label) || type || '—'
|
|
|
+ },
|
|
|
+ reviewStatusTagType(status) {
|
|
|
+ return (REVIEW_STATUS_MAP[status] && REVIEW_STATUS_MAP[status].type) || 'info'
|
|
|
+ },
|
|
|
+ reviewStatusLabel(status) {
|
|
|
+ return (REVIEW_STATUS_MAP[status] && REVIEW_STATUS_MAP[status].label) || status || '—'
|
|
|
+ },
|
|
|
+ regStatusTagType(status) {
|
|
|
+ return (REG_STATUS_MAP[status] && REG_STATUS_MAP[status].type) || 'info'
|
|
|
+ },
|
|
|
+ regStatusLabel(status) {
|
|
|
+ return (REG_STATUS_MAP[status] && REG_STATUS_MAP[status].label) || status || '—'
|
|
|
+ },
|
|
|
+ isSelfRegistration(row) {
|
|
|
+ return row.userId !== null && row.userId !== undefined
|
|
|
+ },
|
|
|
+ placeholderName(row) {
|
|
|
+ const fallback = row.userId != null ? '用户' + row.userId : (row.memberId != null ? '成员' + row.memberId : '—')
|
|
|
+ return fallback
|
|
|
+ },
|
|
|
+ userLabel(u) {
|
|
|
+ var name = u.nickname || u.realName || ('用户' + u.id)
|
|
|
+ return u.phone ? name + '(' + u.phone + ')' : name
|
|
|
+ },
|
|
|
+ // 后端把 java.util.Date 序列化为 UTC 偏移串(如 2026-10-25T01:00:00.000+00:00),
|
|
|
+ // 统一转 Asia/Shanghai 本地墙钟时间展示
|
|
|
+ fmtLocalTime(val) {
|
|
|
+ if (val === null || val === undefined || val === '') return '—'
|
|
|
+ var d = new Date(val)
|
|
|
+ if (isNaN(d.getTime())) return String(val)
|
|
|
+ var bj = new Date(d.getTime() + d.getTimezoneOffset() * 60000 + 8 * 3600000)
|
|
|
+ var pad = function (n) { return n < 10 ? '0' + n : '' + n }
|
|
|
+ return bj.getFullYear() + '-' + pad(bj.getMonth() + 1) + '-' + pad(bj.getDate()) + ' ' +
|
|
|
+ pad(bj.getHours()) + ':' + pad(bj.getMinutes()) + ':' + pad(bj.getSeconds())
|
|
|
+ },
|
|
|
+ async loadActivities() {
|
|
|
+ try {
|
|
|
+ const res = await getActivityList({ page: 1, size: 200 })
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.activityList = (res.data && res.data.records) || []
|
|
|
+ if (this.activityList.length === 1) {
|
|
|
+ this.activityId = this.activityList[0].id
|
|
|
+ this.currentActivityTitle = this.activityList[0].title
|
|
|
+ this.loadCheckinList()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) { console.error(e) }
|
|
|
+ },
|
|
|
+ async loadCheckinList() {
|
|
|
+ if (!this.activityId) {
|
|
|
+ this.records = []
|
|
|
+ this.total = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ let selected = null
|
|
|
+ for (let i = 0; i < this.activityList.length; i++) {
|
|
|
+ if (this.activityList[i].id === this.activityId) selected = this.activityList[i]
|
|
|
+ }
|
|
|
+ this.currentActivityTitle = selected ? selected.title : ''
|
|
|
+ const params = { activityId: this.activityId, page: this.page, size: this.size }
|
|
|
+ if (this.checkInTypeFilter) params.checkInType = this.checkInTypeFilter
|
|
|
+ if (this.reviewStatusFilter) params.reviewStatus = this.reviewStatusFilter
|
|
|
+ if (this.sortState.length > 0) this.applySortToParams(params)
|
|
|
+ const res = await getCheckinList(params)
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.records = (res.data && res.data.records) || []
|
|
|
+ this.total = (res.data && res.data.total) || 0
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleTabClick(tab) {
|
|
|
+ if (tab.name === 'unchecked') {
|
|
|
+ await this.loadRegistrations()
|
|
|
+ } else {
|
|
|
+ this.loadCheckinList()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadRegistrations() {
|
|
|
+ if (!this.activityId) return
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const res = await getRegistrationList({ activityId: this.activityId })
|
|
|
+ if (res.code === 200) this.registrationList = res.data || []
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handlePageChange(p) {
|
|
|
+ this.page = p
|
|
|
+ this.loadCheckinList()
|
|
|
+ },
|
|
|
+ handleSizeChange(s) {
|
|
|
+ this.size = s
|
|
|
+ this.page = 1
|
|
|
+ this.loadCheckinList()
|
|
|
+ },
|
|
|
+ async handleReview(row, reviewStatus) {
|
|
|
+ try {
|
|
|
+ const res = await reviewCheckin({ id: row.id, reviewStatus: reviewStatus })
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.$message.success(reviewStatus === 'approved' ? '已通过' : '已拒绝')
|
|
|
+ this.loadCheckinList()
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.message || '操作失败')
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('操作失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ showAddDialog(row) {
|
|
|
+ this.addForm = {
|
|
|
+ userId: row ? row.userId : null
|
|
|
+ }
|
|
|
+ this.userOptions = []
|
|
|
+ this.addDialogVisible = true
|
|
|
+ },
|
|
|
+ async searchUsers(query) {
|
|
|
+ this.searchLoading = true
|
|
|
+ try {
|
|
|
+ const res = await searchUsers({ keyword: query })
|
|
|
+ if (res.code === 200) this.userOptions = res.data || []
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ } finally {
|
|
|
+ this.searchLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleAdd() {
|
|
|
+ if (!this.activityId || !this.addForm.userId) {
|
|
|
+ this.$message.warning('请选择签到人')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.submitting = true
|
|
|
+ try {
|
|
|
+ const res = await addCheckin({
|
|
|
+ activityId: this.activityId,
|
|
|
+ userId: this.addForm.userId
|
|
|
+ })
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.$message.success('补录成功,操作人与时间已记录')
|
|
|
+ this.addDialogVisible = false
|
|
|
+ this.loadCheckinList()
|
|
|
+ if (this.registrationList.length) this.loadRegistrations()
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.message || '补录失败')
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('补录失败')
|
|
|
+ } finally {
|
|
|
+ this.submitting = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleExport() {
|
|
|
+ if (!this.activityId) {
|
|
|
+ this.$message.warning('请先选择活动')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.$confirm('将按当前筛选条件导出签到名单(CSV),是否继续?', '导出确认', {
|
|
|
+ confirmButtonText: '导出',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'info'
|
|
|
+ }).then(async () => {
|
|
|
+ this.exporting = true
|
|
|
+ try {
|
|
|
+ const params = { activityId: this.activityId }
|
|
|
+ if (this.checkInTypeFilter) params.checkInType = this.checkInTypeFilter
|
|
|
+ if (this.reviewStatusFilter) params.reviewStatus = this.reviewStatusFilter
|
|
|
+ const res = await exportCheckins(params)
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.downloadCsv(res.data || [])
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.message || '导出失败')
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('导出失败')
|
|
|
+ } finally {
|
|
|
+ this.exporting = false
|
|
|
+ }
|
|
|
+ }).catch(function () {})
|
|
|
+ },
|
|
|
+ downloadCsv(rows) {
|
|
|
+ var headers = ['签到ID', '活动ID', '活动标题', '签到人ID', '签到人姓名', '手机号', '关联报名ID', '签到时间', '签到类型', '审核状态', '补录操作人', '创建时间']
|
|
|
+ var lines = [headers.join(',')]
|
|
|
+ rows.forEach(function (r) {
|
|
|
+ var cells = [r.id, r.activityId, r.activityTitle, r.userId, r.userName, r.userPhone, r.registrationId, r.checkInAt, r.checkInType, r.reviewStatus, r.checkInByName, r.createdAt]
|
|
|
+ lines.push(cells.map(function (c) { return c === null || c === undefined ? '' : String(c) }).join(','))
|
|
|
+ })
|
|
|
+ var activityName = this.currentActivityTitle ? this.currentActivityTitle.replace(/[\\/:*?"<>|]/g, '') : ('activity_' + this.activityId)
|
|
|
+ var fileName = '活动签到名单_' + activityName + '_' + this.today() + '.csv'
|
|
|
+ // 带 BOM,避免 Excel 打开中文乱码
|
|
|
+ var blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8;' })
|
|
|
+ var url = URL.createObjectURL(blob)
|
|
|
+ var link = document.createElement('a')
|
|
|
+ link.href = url
|
|
|
+ link.download = fileName
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ this.$message.success('已导出 ' + rows.length + ' 条记录')
|
|
|
+ },
|
|
|
+ today() {
|
|
|
+ var d = new Date()
|
|
|
+ var pad = function (n) { return n < 10 ? '0' + n : '' + n }
|
|
|
+ return d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate())
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.filters { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
|
+.user-cell { display: flex; flex-direction: column; line-height: 1.4; }
|
|
|
+.user-name { font-weight: 500; }
|
|
|
+.user-meta { font-size: 12px; color: var(--adm-text-45); }
|
|
|
+.text-muted { color: var(--adm-text-45); }
|
|
|
+.readonly-field { color: var(--adm-text-65); }
|
|
|
+.dialog-tip { margin: 0 0 6px 90px; font-size: 12px; color: var(--adm-text-45); }
|
|
|
+</style>
|