|
|
<script setup lang="ts">
|
|
|
import { computed, inject, reactive, ref, watch, type Ref } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import PortalPagination from '../../components/portal/PortalPagination.vue'
|
|
|
import {
|
|
|
exportAdminApplications,
|
|
|
getAdminApplicationExportMeta,
|
|
|
listAdminApplications,
|
|
|
} from '../../api/admin/applications'
|
|
|
import { batchReviewResult } from '../../api/admin/manageProjects'
|
|
|
import { listTracks } from '../../api/admin/competitions'
|
|
|
import { listPublicSourceChannels } from '../../api/admin/publicSourceChannels'
|
|
|
import { listSignupChannels } from '../../api/admin/signupChannels'
|
|
|
import type {
|
|
|
AdminApplicationExportMode,
|
|
|
AdminApplicationListParams,
|
|
|
AdminApplicationRow,
|
|
|
CompetitionTrackRow,
|
|
|
PublicSourceChannelRow,
|
|
|
SignupChannelRow,
|
|
|
} from '../../api/admin/types'
|
|
|
|
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
|
const competitionId = inject<Ref<number | null>>('manageCompetitionId', ref(null))
|
|
|
const competitionSlug = inject<Ref<string>>('manageCompetitionSlug', ref(''))
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const rows = ref<AdminApplicationRow[]>([])
|
|
|
const pager = reactive({ page: 1, perPage: 15, total: 0 })
|
|
|
|
|
|
function parsePositiveInt(v: unknown, fallback: number): number {
|
|
|
const n = typeof v === 'string' ? Number(v) : typeof v === 'number' ? v : NaN
|
|
|
return Number.isInteger(n) && n > 0 ? n : fallback
|
|
|
}
|
|
|
|
|
|
function listReturnQuery(): Record<string, string> {
|
|
|
const q: Record<string, string> = {}
|
|
|
if (pager.page > 1) q.page = String(pager.page)
|
|
|
if (pager.perPage !== 15) q.per_page = String(pager.perPage)
|
|
|
return q
|
|
|
}
|
|
|
|
|
|
function applyListQueryFromRoute(): void {
|
|
|
pager.page = parsePositiveInt(route.query.page, 1)
|
|
|
pager.perPage = parsePositiveInt(route.query.per_page, 15)
|
|
|
}
|
|
|
|
|
|
function syncListQueryToRoute(): void {
|
|
|
const next = listReturnQuery()
|
|
|
const curPage = typeof route.query.page === 'string' ? route.query.page : undefined
|
|
|
const curPer = typeof route.query.per_page === 'string' ? route.query.per_page : undefined
|
|
|
const nextPage = next.page
|
|
|
const nextPer = next.per_page
|
|
|
if (curPage === nextPage && curPer === nextPer) return
|
|
|
void router.replace({ query: next })
|
|
|
}
|
|
|
const tracks = ref<CompetitionTrackRow[]>([])
|
|
|
const signupChannels = ref<SignupChannelRow[]>([])
|
|
|
const publicSources = ref<PublicSourceChannelRow[]>([])
|
|
|
|
|
|
const filters = reactive({
|
|
|
keyword: '',
|
|
|
track: '',
|
|
|
review_result: '' as '' | 'passed' | 'rejected' | 'pending',
|
|
|
signup_channel_id: '' as number | '',
|
|
|
public_source_channel_id: '' as number | '',
|
|
|
})
|
|
|
|
|
|
const sortDir = ref<'asc' | 'desc'>('desc')
|
|
|
const selectionMode = ref<'audit' | 'export'>('audit')
|
|
|
const auditSelectedIds = ref<number[]>([])
|
|
|
const exportSelectedIds = ref<number[]>([])
|
|
|
const exportOpen = ref(false)
|
|
|
const exportMode = ref<AdminApplicationExportMode>('xlsx')
|
|
|
const exportBusy = ref(false)
|
|
|
const exportMetaTotal = ref(0)
|
|
|
|
|
|
const batchPassOpen = ref(false)
|
|
|
const batchFailOpen = ref(false)
|
|
|
const batchNote = ref('')
|
|
|
const batchBusy = ref(false)
|
|
|
|
|
|
function buildListParams(page: number): AdminApplicationListParams {
|
|
|
const params: AdminApplicationListParams = {
|
|
|
page,
|
|
|
per_page: pager.perPage,
|
|
|
status: 'submitted',
|
|
|
sort_by: 'team_avg',
|
|
|
sort_dir: sortDir.value,
|
|
|
}
|
|
|
const kw = filters.keyword.trim()
|
|
|
if (kw) params.keyword = kw
|
|
|
if (filters.track) params.track = filters.track
|
|
|
if (filters.review_result === 'passed' || filters.review_result === 'rejected' || filters.review_result === 'pending') {
|
|
|
params.review_result = filters.review_result
|
|
|
}
|
|
|
if (filters.signup_channel_id !== '') params.signup_channel_id = Number(filters.signup_channel_id)
|
|
|
if (filters.public_source_channel_id !== '') {
|
|
|
params.public_source_channel_id = Number(filters.public_source_channel_id)
|
|
|
}
|
|
|
return params
|
|
|
}
|
|
|
|
|
|
function signupChannelLabel(row: AdminApplicationRow): string {
|
|
|
return row.signup_channel?.channel_name || row.signup_channel_code || '—'
|
|
|
}
|
|
|
|
|
|
function publicSourceLabel(row: AdminApplicationRow): string {
|
|
|
if (row.public_source_channel) {
|
|
|
return `${row.public_source_channel.source_name}(${row.public_source_channel.source_code})`
|
|
|
}
|
|
|
return row.public_source_code || '—'
|
|
|
}
|
|
|
|
|
|
function submitterName(row: AdminApplicationRow): string {
|
|
|
return row.submitter_name?.trim() || row.submitter_mobile?.trim() || '—'
|
|
|
}
|
|
|
|
|
|
function formatDateTime(s: string | null | undefined): string {
|
|
|
if (!s) return '—'
|
|
|
const d = new Date(s.includes(' ') ? s.replace(/-/g, '/') : s)
|
|
|
if (Number.isNaN(d.getTime())) return s
|
|
|
const p = (n: number) => String(n).padStart(2, '0')
|
|
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
|
|
}
|
|
|
|
|
|
function formatScore(v: number | null | undefined): string {
|
|
|
if (v == null || Number.isNaN(v)) return '-'
|
|
|
return v.toFixed(2)
|
|
|
}
|
|
|
|
|
|
function totalCell(row: AdminApplicationRow): string {
|
|
|
if (!row.review_fully_completed) return '待评审'
|
|
|
return row.team_sum != null ? row.team_sum.toFixed(2) : '-'
|
|
|
}
|
|
|
|
|
|
function avgCell(row: AdminApplicationRow): string {
|
|
|
if (!row.review_fully_completed) return '-'
|
|
|
return formatScore(row.team_avg)
|
|
|
}
|
|
|
|
|
|
function rowSelectable(row: AdminApplicationRow): boolean {
|
|
|
if (selectionMode.value === 'export') return true
|
|
|
return row.review_fully_completed
|
|
|
}
|
|
|
|
|
|
const activeSelectedIds = computed(() =>
|
|
|
selectionMode.value === 'audit' ? auditSelectedIds.value : exportSelectedIds.value,
|
|
|
)
|
|
|
|
|
|
function setActiveSelectedIds(ids: number[]) {
|
|
|
if (selectionMode.value === 'audit') auditSelectedIds.value = ids
|
|
|
else exportSelectedIds.value = ids
|
|
|
}
|
|
|
|
|
|
function switchSelectionMode(mode: 'audit' | 'export') {
|
|
|
selectionMode.value = mode
|
|
|
}
|
|
|
|
|
|
const selectionModeHint = computed(() =>
|
|
|
selectionMode.value === 'audit'
|
|
|
? '当前勾选用于批量通过/未通过'
|
|
|
: '当前勾选用于「导出表格 + 所选附件」,与审核勾选互不影响',
|
|
|
)
|
|
|
|
|
|
function isRowSelected(id: number): boolean {
|
|
|
return activeSelectedIds.value.includes(id)
|
|
|
}
|
|
|
|
|
|
const allVisibleSelected = computed(() => {
|
|
|
const selectable = rows.value.filter(rowSelectable)
|
|
|
const ids = activeSelectedIds.value
|
|
|
return selectable.length > 0 && selectable.every((r) => ids.includes(r.id))
|
|
|
})
|
|
|
|
|
|
function toggleSelectAll(ev: Event) {
|
|
|
const checked = (ev.target as HTMLInputElement).checked
|
|
|
const selectableIds = rows.value.filter(rowSelectable).map((r) => r.id)
|
|
|
const ids = activeSelectedIds.value
|
|
|
if (checked) {
|
|
|
setActiveSelectedIds(Array.from(new Set([...ids, ...selectableIds])))
|
|
|
} else {
|
|
|
setActiveSelectedIds(ids.filter((id) => !selectableIds.includes(id)))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function toggleRow(id: number, row: AdminApplicationRow, ev: Event) {
|
|
|
if (!rowSelectable(row)) {
|
|
|
;(ev.target as HTMLInputElement).checked = false
|
|
|
return
|
|
|
}
|
|
|
const checked = (ev.target as HTMLInputElement).checked
|
|
|
const ids = [...activeSelectedIds.value]
|
|
|
if (checked) {
|
|
|
if (!ids.includes(id)) ids.push(id)
|
|
|
} else {
|
|
|
const idx = ids.indexOf(id)
|
|
|
if (idx >= 0) ids.splice(idx, 1)
|
|
|
}
|
|
|
setActiveSelectedIds(ids)
|
|
|
}
|
|
|
function toggleSortAvg() {
|
|
|
sortDir.value = sortDir.value === 'desc' ? 'asc' : 'desc'
|
|
|
void refresh(1)
|
|
|
}
|
|
|
|
|
|
function onPageChange(page: number) {
|
|
|
void refresh(page)
|
|
|
}
|
|
|
|
|
|
function onPerPageChange(size: number) {
|
|
|
pager.perPage = size
|
|
|
void refresh(1)
|
|
|
}
|
|
|
|
|
|
async function loadFilters() {
|
|
|
const cid = competitionId.value
|
|
|
if (!cid) return
|
|
|
const [t, ch, ps] = await Promise.all([
|
|
|
listTracks(cid),
|
|
|
listSignupChannels(cid),
|
|
|
listPublicSourceChannels(),
|
|
|
])
|
|
|
tracks.value = t
|
|
|
signupChannels.value = ch
|
|
|
publicSources.value = ps
|
|
|
}
|
|
|
|
|
|
async function refresh(page = pager.page) {
|
|
|
const cid = competitionId.value
|
|
|
if (!cid) return
|
|
|
loading.value = true
|
|
|
pager.page = page
|
|
|
syncListQueryToRoute()
|
|
|
try {
|
|
|
const res = await listAdminApplications(cid, buildListParams(page))
|
|
|
rows.value = res.data
|
|
|
pager.total = res.meta.total
|
|
|
if (res.meta.current_page && res.meta.current_page !== pager.page) {
|
|
|
pager.page = res.meta.current_page
|
|
|
syncListQueryToRoute()
|
|
|
}
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function resetFilters() {
|
|
|
filters.keyword = ''
|
|
|
filters.track = ''
|
|
|
filters.review_result = ''
|
|
|
filters.signup_channel_id = ''
|
|
|
filters.public_source_channel_id = ''
|
|
|
auditSelectedIds.value = []
|
|
|
exportSelectedIds.value = []
|
|
|
void refresh(1)
|
|
|
}
|
|
|
|
|
|
async function openExport(mode: AdminApplicationExportMode) {
|
|
|
const cid = competitionId.value
|
|
|
if (!cid) return
|
|
|
if (mode === 'selected' && exportSelectedIds.value.length === 0) {
|
|
|
ElMessage.warning('请切换到「导出选择」并勾选要导出附件的项目')
|
|
|
return
|
|
|
}
|
|
|
exportMode.value = mode
|
|
|
exportOpen.value = true
|
|
|
try {
|
|
|
const meta = await getAdminApplicationExportMeta(cid, {
|
|
|
mode,
|
|
|
...buildListParams(pager.page),
|
|
|
application_ids: mode === 'selected' ? exportSelectedIds.value : undefined,
|
|
|
})
|
|
|
exportMetaTotal.value = meta.total
|
|
|
} catch {
|
|
|
exportMetaTotal.value = pager.total
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function startExport() {
|
|
|
const cid = competitionId.value
|
|
|
if (!cid) return
|
|
|
exportBusy.value = true
|
|
|
try {
|
|
|
await exportAdminApplications(cid, {
|
|
|
mode: exportMode.value,
|
|
|
...buildListParams(pager.page),
|
|
|
application_ids: exportMode.value === 'selected' ? exportSelectedIds.value : undefined,
|
|
|
})
|
|
|
exportOpen.value = false
|
|
|
} finally {
|
|
|
exportBusy.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function confirmBatch(result: 'passed' | 'rejected') {
|
|
|
const cid = competitionId.value
|
|
|
if (!cid || auditSelectedIds.value.length === 0) return
|
|
|
batchBusy.value = true
|
|
|
try {
|
|
|
await batchReviewResult(cid, {
|
|
|
application_ids: auditSelectedIds.value,
|
|
|
result,
|
|
|
note: batchNote.value.trim() || undefined,
|
|
|
})
|
|
|
batchPassOpen.value = false
|
|
|
batchFailOpen.value = false
|
|
|
batchNote.value = ''
|
|
|
auditSelectedIds.value = []
|
|
|
await refresh(pager.page)
|
|
|
} finally {
|
|
|
batchBusy.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function openDetail(row: AdminApplicationRow) {
|
|
|
void router.push({
|
|
|
name: 'manage-project-detail',
|
|
|
params: { slug: competitionSlug.value, id: row.id },
|
|
|
query: listReturnQuery(),
|
|
|
})
|
|
|
}
|
|
|
|
|
|
watch(competitionId, async (cid) => {
|
|
|
if (!cid) return
|
|
|
await loadFilters()
|
|
|
applyListQueryFromRoute()
|
|
|
await refresh(pager.page)
|
|
|
}, { immediate: true })
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="container-fluid p-3 p-md-4">
|
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
|
|
<h5 class="mb-0 section-title">项目管理</h5>
|
|
|
</div>
|
|
|
|
|
|
<div class="filter-panel mb-3">
|
|
|
<div class="row g-2">
|
|
|
<div class="col-md-3">
|
|
|
<input
|
|
|
v-model="filters.keyword"
|
|
|
type="search"
|
|
|
class="form-control"
|
|
|
placeholder="搜索项目、姓名、学校、手机号、推荐方"
|
|
|
@keydown.enter="refresh(1)"
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="col-md-2">
|
|
|
<select v-model="filters.track" class="form-select">
|
|
|
<option value="">全部赛道</option>
|
|
|
<option v-for="t in tracks" :key="t.track_code" :value="t.track_code">{{ t.title }}</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="col-md-2">
|
|
|
<select v-model="filters.review_result" class="form-select">
|
|
|
<option value="">全部状态</option>
|
|
|
<option value="passed">通过</option>
|
|
|
<option value="rejected">未通过</option>
|
|
|
<option value="pending">待评定</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="col-md-2">
|
|
|
<select v-model="filters.signup_channel_id" class="form-select">
|
|
|
<option value="">全部报名渠道</option>
|
|
|
<option v-for="ch in signupChannels" :key="ch.id" :value="ch.id">{{ ch.channel_name }}</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="col-md-2">
|
|
|
<select v-model="filters.public_source_channel_id" class="form-select">
|
|
|
<option value="">全部用户渠道</option>
|
|
|
<option v-for="ps in publicSources" :key="ps.id" :value="ps.id">{{ ps.source_name }}</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="col-12 col-md-auto d-flex gap-2">
|
|
|
<button type="button" class="btn btn-primary px-3" @click="refresh(1)">查询</button>
|
|
|
<button type="button" class="btn btn-light border px-3" @click="resetFilters">重置</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="selection-toolbar mt-2">
|
|
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
|
|
<span class="small text-secondary">勾选用途:</span>
|
|
|
<div class="btn-group btn-group-sm portal-selection-mode-group" role="group">
|
|
|
<button
|
|
|
type="button"
|
|
|
class="btn"
|
|
|
:class="selectionMode === 'audit' ? 'btn-primary' : 'btn-outline-primary'"
|
|
|
@click="switchSelectionMode('audit')"
|
|
|
>
|
|
|
审核选择
|
|
|
<span v-if="auditSelectedIds.length" class="badge bg-light text-dark ms-1">{{ auditSelectedIds.length }}</span>
|
|
|
</button>
|
|
|
<button
|
|
|
type="button"
|
|
|
class="btn"
|
|
|
:class="selectionMode === 'export' ? 'btn-primary' : 'btn-outline-primary'"
|
|
|
@click="switchSelectionMode('export')"
|
|
|
>
|
|
|
导出选择
|
|
|
<span v-if="exportSelectedIds.length" class="badge bg-light text-dark ms-1">{{ exportSelectedIds.length }}</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
<template v-if="selectionMode === 'audit'">
|
|
|
<button
|
|
|
type="button"
|
|
|
class="btn btn-sm btn-manage-pass"
|
|
|
:disabled="auditSelectedIds.length === 0"
|
|
|
@click="batchPassOpen = true; batchNote = '同意参赛。'"
|
|
|
>
|
|
|
通过
|
|
|
</button>
|
|
|
<button
|
|
|
type="button"
|
|
|
class="btn btn-sm btn-manage-reject"
|
|
|
:disabled="auditSelectedIds.length === 0"
|
|
|
@click="batchFailOpen = true; batchNote = ''"
|
|
|
>
|
|
|
未通过
|
|
|
</button>
|
|
|
</template>
|
|
|
<div v-else class="dropdown">
|
|
|
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
|
导出
|
|
|
</button>
|
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
|
<li><button class="dropdown-item" type="button" @click="openExport('xlsx')">仅导出表格</button></li>
|
|
|
<li>
|
|
|
<button
|
|
|
class="dropdown-item"
|
|
|
type="button"
|
|
|
:disabled="exportSelectedIds.length === 0"
|
|
|
@click="openExport('selected')"
|
|
|
>
|
|
|
导出表格 + 所选附件
|
|
|
</button>
|
|
|
</li>
|
|
|
<li><button class="dropdown-item" type="button" @click="openExport('all')">导出表格 + 全部附件</button></li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
</div>
|
|
|
<p class="small text-secondary mb-0 mt-2">{{ selectionModeHint }}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="portal-table-scroll table-panel">
|
|
|
<table class="table table-hover align-middle mb-0 admin-list-table portal-data-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th class="text-center portal-sticky-left" style="width: 2.5rem">
|
|
|
<input
|
|
|
:key="`select-all-${selectionMode}-${pager.page}`"
|
|
|
class="form-check-input"
|
|
|
type="checkbox"
|
|
|
:checked="allVisibleSelected"
|
|
|
aria-label="全选本页"
|
|
|
@change="toggleSelectAll"
|
|
|
/>
|
|
|
</th>
|
|
|
<th style="width: 3rem">序号</th>
|
|
|
<th>项目编号</th>
|
|
|
<th>项目名称</th>
|
|
|
<th>负责人</th>
|
|
|
<th>学校</th>
|
|
|
<th>组别</th>
|
|
|
<th>赛道</th>
|
|
|
<th>报名渠道</th>
|
|
|
<th>推荐方</th>
|
|
|
<th>手机号</th>
|
|
|
<th>提交人</th>
|
|
|
<th>提交人手机号</th>
|
|
|
<th>用户渠道</th>
|
|
|
<th>附件</th>
|
|
|
<th>提交时间</th>
|
|
|
<th class="portal-sticky-right-5">总分</th>
|
|
|
<th class="portal-sticky-right-4">进程</th>
|
|
|
<th class="portal-sticky-right-3">
|
|
|
<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" @click="toggleSortAvg">
|
|
|
平均分 {{ sortDir === 'desc' ? '↓' : '↑' }}
|
|
|
</button>
|
|
|
</th>
|
|
|
<th class="portal-sticky-right-2">状态</th>
|
|
|
<th class="portal-sticky-right-1">操作</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-if="loading">
|
|
|
<td colspan="21" class="text-center py-4 text-secondary">加载中…</td>
|
|
|
</tr>
|
|
|
<tr v-else-if="rows.length === 0">
|
|
|
<td colspan="21" class="text-center py-4 text-secondary">暂无符合条件的数据</td>
|
|
|
</tr>
|
|
|
<tr v-for="(row, idx) in rows" v-else :key="row.id">
|
|
|
<td class="text-center portal-sticky-left">
|
|
|
<input
|
|
|
:key="`${selectionMode}-${pager.page}-${row.id}`"
|
|
|
class="form-check-input"
|
|
|
type="checkbox"
|
|
|
:checked="isRowSelected(row.id)"
|
|
|
:disabled="!rowSelectable(row)"
|
|
|
@change="toggleRow(row.id, row, $event)"
|
|
|
/>
|
|
|
</td>
|
|
|
<td class="text-secondary">{{ (pager.page - 1) * pager.perPage + idx + 1 }}</td>
|
|
|
<td>{{ row.project_code }}</td>
|
|
|
<td class="cell-wrap">{{ row.project_name }}</td>
|
|
|
<td>{{ row.player_name }}</td>
|
|
|
<td class="cell-wrap">{{ row.school || '—' }}</td>
|
|
|
<td>{{ row.entry_group || '—' }}</td>
|
|
|
<td class="cell-wrap">{{ row.track_title }}</td>
|
|
|
<td class="cell-wrap">{{ signupChannelLabel(row) }}</td>
|
|
|
<td class="cell-wrap">{{ row.recommend || '—' }}</td>
|
|
|
<td>{{ row.contact_mobile || '—' }}</td>
|
|
|
<td>{{ submitterName(row) }}</td>
|
|
|
<td>{{ row.submitter_mobile || '—' }}</td>
|
|
|
<td class="cell-wrap">{{ publicSourceLabel(row) }}</td>
|
|
|
<td>{{ row.files_count }}</td>
|
|
|
<td>{{ formatDateTime(row.submitted_at) }}</td>
|
|
|
<td class="portal-sticky-right-5" :class="{ 'admin-total-pending-text': !row.review_fully_completed }">{{ totalCell(row) }}</td>
|
|
|
<td class="portal-sticky-right-4">{{ row.review_progress }}</td>
|
|
|
<td class="portal-sticky-right-3">{{ avgCell(row) }}</td>
|
|
|
<td class="portal-sticky-right-2">
|
|
|
<span
|
|
|
v-if="row.review_result === 'passed'"
|
|
|
class="admin-manage-status-tag admin-manage-status-pass"
|
|
|
>通过</span>
|
|
|
<span
|
|
|
v-else-if="row.review_result === 'rejected'"
|
|
|
class="admin-manage-status-tag admin-manage-status-fail"
|
|
|
>未通过</span>
|
|
|
<span v-else>-</span>
|
|
|
</td>
|
|
|
<td class="portal-sticky-right-1">
|
|
|
<button type="button" class="btn btn-sm btn-outline-primary" @click="openDetail(row)">查看</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<PortalPagination
|
|
|
:page="pager.page"
|
|
|
:per-page="pager.perPage"
|
|
|
:total="pager.total"
|
|
|
@update:page="onPageChange"
|
|
|
@update:per-page="onPerPageChange"
|
|
|
/>
|
|
|
|
|
|
<div v-if="exportOpen" class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,.35)">
|
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
|
<div class="modal-content">
|
|
|
<div class="modal-header">
|
|
|
<h5 class="modal-title">导出报名材料</h5>
|
|
|
<button type="button" class="btn-close" @click="exportOpen = false" />
|
|
|
</div>
|
|
|
<div class="modal-body">
|
|
|
<p class="mb-0">共 {{ exportMetaTotal || pager.total }} 份数据,模式:{{ exportMode }}</p>
|
|
|
</div>
|
|
|
<div class="modal-footer">
|
|
|
<button type="button" class="btn btn-light" @click="exportOpen = false">取消</button>
|
|
|
<button type="button" class="btn btn-primary" :disabled="exportBusy" @click="startExport">
|
|
|
{{ exportBusy ? '导出中…' : '开始导出' }}
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div v-if="batchPassOpen" class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,.35)">
|
|
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
|
|
<div class="modal-content">
|
|
|
<div class="modal-header"><h5 class="modal-title">审核意见</h5></div>
|
|
|
<div class="modal-body">
|
|
|
<textarea v-model="batchNote" class="form-control" rows="6" />
|
|
|
</div>
|
|
|
<div class="modal-footer">
|
|
|
<button type="button" class="btn btn-secondary" @click="batchPassOpen = false">取消</button>
|
|
|
<button type="button" class="btn btn-success" :disabled="batchBusy" @click="confirmBatch('passed')">
|
|
|
确认通过
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div v-if="batchFailOpen" class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,.35)">
|
|
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
|
|
<div class="modal-content">
|
|
|
<div class="modal-header"><h5 class="modal-title">审核意见</h5></div>
|
|
|
<div class="modal-body">
|
|
|
<textarea v-model="batchNote" class="form-control" rows="8" />
|
|
|
</div>
|
|
|
<div class="modal-footer">
|
|
|
<button type="button" class="btn btn-secondary" @click="batchFailOpen = false">取消</button>
|
|
|
<button type="button" class="btn btn-warning text-dark" :disabled="batchBusy" @click="confirmBatch('rejected')">
|
|
|
确认未通过
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|