运动员数据上传

This commit is contained in:
马梓彭 2026-10-28 15:06:36 +08:00
parent e7c414e509
commit 5a6ddc9e0c
2 changed files with 296 additions and 0 deletions

View File

@ -0,0 +1,16 @@
import { http } from '../../utils/request';
/** 测试当前服务端配置与市民运动会平台的连接。 */
export const citizenSportsConnectionTestPost = () => {
return http.post('/citizen-sports-sync/connection-test', {});
};
/** 预览待同步的报名数据,不会向外部平台写入。 */
export const citizenSportsPreviewPost = (data) => {
return http.post('/citizen-sports-sync/preview', data);
};
/** 将预检通过的报名数据同步到市民运动会平台。 */
export const citizenSportsExecutePost = (data) => {
return http.post('/citizen-sports-sync/execute', data);
};

View File

@ -0,0 +1,280 @@
<template>
<div class="citizen-sports-sync layout-padding">
<el-alert
title="此页面仅供超级管理员使用。请先在服务端配置平台地址和密钥,预览确认无误后再上传。"
type="warning"
:closable="false"
show-icon
/>
<el-card class="mt16" shadow="never">
<template #header>
<div class="card-header">
<span>同步条件</span>
<el-button :loading="connectionTesting" @click="testConnection">测试平台连接</el-button>
</div>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px" class="sync-form">
<el-form-item label="悦动赛事" prop="eventId">
<el-select
v-model="form.eventId"
filterable
remote
reserve-keyword
:remote-method="searchEvents"
:loading="eventLoading"
placeholder="输入赛事名称搜索"
style="width: 100%"
@change="selectEvent"
>
<el-option v-for="item in eventOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="平台赛事名称" prop="platformEventName">
<el-input v-model="form.platformEventName" maxlength="100" placeholder="必须与一网通办申报名称完全一致" />
</el-form-item>
<el-form-item label="系列赛场次名称">
<el-input v-model="form.seriesName" maxlength="100" placeholder="没有系列赛时留空" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="previewLoading" @click="preview">预览报名数据</el-button>
<el-button
type="danger"
:disabled="!previewResult?.canUpload"
:loading="uploading"
@click="execute"
>
确认上传
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-if="previewResult" class="mt16" shadow="never">
<template #header>数据预览</template>
<el-descriptions :column="4" border>
<el-descriptions-item label="悦动赛事">{{ previewResult.eventName }}</el-descriptions-item>
<el-descriptions-item label="报名人数">{{ previewResult.registrationCount }}</el-descriptions-item>
<el-descriptions-item label="待上传人数">{{ previewResult.uploadRecordCount }}</el-descriptions-item>
<el-descriptions-item label="组别数">{{ previewResult.typeCount }}</el-descriptions-item>
</el-descriptions>
<el-table :data="previewResult.types || []" border class="mt16">
<el-table-column prop="typeName" label="平台组别" min-width="200" />
<el-table-column prop="playerCount" label="人数" width="120" />
</el-table>
<div class="detail-title">待上传人员明细</div>
<el-table :data="previewResult.players || []" border height="520">
<el-table-column type="index" label="序号" width="70" fixed="left" />
<el-table-column prop="typeName" label="平台组别" min-width="160" fixed="left" />
<el-table-column prop="playerName" label="姓名" width="110" fixed="left" />
<el-table-column prop="playerId" label="用户ID" min-width="180" />
<el-table-column prop="cardType" label="证件类型" width="100" />
<el-table-column prop="cardNumber" label="证件号码" min-width="190" />
<el-table-column label="性别" width="80">
<template #default="scope">{{ formatSex(scope.row.playerSex) }}</template>
</el-table-column>
<el-table-column prop="playerPhone" label="手机号" width="140" />
<el-table-column prop="playerBirth" label="出生日期" width="120" />
<el-table-column prop="groupName" label="队伍名称" min-width="160" />
<el-table-column prop="deleteStatus" label="删除状态" width="100" />
</el-table>
<el-alert
v-if="previewResult.canUpload"
class="mt16"
title="预检通过,可以上传"
type="success"
:closable="false"
show-icon
/>
<template v-else>
<el-alert class="mt16" title="预检未通过,请先修正以下报名资料" type="error" :closable="false" show-icon />
<el-table :data="previewResult.errors || []" border class="mt16">
<el-table-column prop="registrationPersonId" label="报名人员ID" width="190" />
<el-table-column prop="message" label="问题" min-width="320" />
</el-table>
</template>
</el-card>
<el-card v-if="executeResult" class="mt16" shadow="never">
<template #header>上传结果</template>
<el-descriptions :column="4" border>
<el-descriptions-item label="平台赛事编码">{{ executeResult.platformEventCode }}</el-descriptions-item>
<el-descriptions-item label="请求人数">{{ executeResult.requestedCount }}</el-descriptions-item>
<el-descriptions-item label="成功 / 失败">{{ executeResult.successCount }} / {{ executeResult.failureCount }}</el-descriptions-item>
<el-descriptions-item label="未尝试">{{ executeResult.unattemptedCount }}</el-descriptions-item>
</el-descriptions>
<el-alert
class="mt16"
:title="executeResult.aborted ? '上传中途终止,请根据批次结果核查,勿直接重复上传' : '上传完成'"
:type="executeResult.aborted ? 'error' : 'success'"
:closable="false"
show-icon
/>
<el-table :data="executeResult.batches || []" border class="mt16">
<el-table-column prop="typeName" label="组别" min-width="180" />
<el-table-column prop="requestedCount" label="请求" width="90" />
<el-table-column prop="successCount" label="成功" width="90" />
<el-table-column prop="failureCount" label="失败" width="90" />
<el-table-column label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.success ? 'success' : 'danger'">{{ scope.row.success ? '成功' : '失败' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="平台说明" min-width="260" />
</el-table>
</el-card>
</div>
</template>
<script setup name="citizenSportsSync">
import { onMounted, reactive, ref, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { eventPagePost } from '/@/api/match/event';
import {
citizenSportsConnectionTestPost,
citizenSportsExecutePost,
citizenSportsPreviewPost,
} from '/@/api/match/citizenSportsSync';
const formRef = ref();
const form = reactive({
eventId: null,
platformEventName: '',
seriesName: '',
});
const rules = {
eventId: [{ required: true, message: '请选择悦动赛事', trigger: 'change' }],
platformEventName: [{ required: true, message: '请输入平台赛事名称', trigger: 'blur' }],
};
const eventOptions = ref([]);
const eventLoading = ref(false);
const connectionTesting = ref(false);
const previewLoading = ref(false);
const uploading = ref(false);
const previewResult = ref(null);
const executeResult = ref(null);
const formatSex = (value) => ({ 1: '男', 2: '女' })[value] || value || '-';
const requestInput = () => ({
eventId: form.eventId,
platformEventName: form.platformEventName.trim(),
seriesName: form.seriesName.trim() || null,
});
async function searchEvents(keyword = '') {
eventLoading.value = true;
try {
const response = await eventPagePost({ pageNo: 1, pageSize: 40, eventName: keyword || undefined });
eventOptions.value = (response?.data?.rows || []).map((item) => ({
value: Number(item.id ?? item.eventId),
label: item.eventName ?? item.EventName ?? `赛事 #${item.id ?? item.eventId}`,
}));
} finally {
eventLoading.value = false;
}
}
function selectEvent(eventId) {
const selected = eventOptions.value.find((item) => item.value === eventId);
if (selected) form.platformEventName = selected.label;
}
async function testConnection() {
connectionTesting.value = true;
try {
const response = await citizenSportsConnectionTestPost();
if (!response?.succeeded) return;
if (response.data?.success) ElMessage.success(response.data.message || '平台连接正常');
else ElMessage.error(response.data?.message || '平台连接失败');
} finally {
connectionTesting.value = false;
}
}
async function preview() {
if (!(await formRef.value?.validate().catch(() => false))) return;
previewLoading.value = true;
executeResult.value = null;
try {
const response = await citizenSportsPreviewPost(requestInput());
if (!response?.succeeded) return;
previewResult.value = response.data;
if (response.data?.canUpload) ElMessage.success('预检通过,请核对人数和组别后再确认上传');
} finally {
previewLoading.value = false;
}
}
async function execute() {
if (!previewResult.value?.canUpload) return;
const confirmed = await ElMessageBox.confirm(
`确认将 ${previewResult.value.uploadRecordCount} 人上传到“${form.platformEventName.trim()}”吗?`,
'确认上传',
{
type: 'warning',
confirmButtonText: '确认上传',
cancelButtonText: '取消',
}
)
.then(() => true)
.catch(() => false);
if (!confirmed) return;
uploading.value = true;
try {
const response = await citizenSportsExecutePost({ ...requestInput(), confirmUpload: true });
if (!response?.succeeded) return;
executeResult.value = response.data;
if (!response.data?.aborted) ElMessage.success('报名数据上传完成');
} finally {
uploading.value = false;
}
}
watch(
() => [form.eventId, form.platformEventName, form.seriesName],
() => {
previewResult.value = null;
executeResult.value = null;
}
);
onMounted(() => searchEvents());
</script>
<style scoped lang="scss">
.citizen-sports-sync {
display: block;
overflow-y: auto;
}
.mt16 {
margin-top: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.sync-form {
max-width: 760px;
}
.detail-title {
margin: 20px 0 12px;
font-weight: 600;
}
</style>