赛事证书发放

This commit is contained in:
马梓彭 2026-04-14 15:13:40 +08:00
parent 06cbee57a7
commit 0b8390e64c
2 changed files with 391 additions and 34 deletions

View File

@ -89,11 +89,11 @@ export const eventscoreIssuecertificatePost = (data) => {
/**
* 查看证书
* @param {number} id - 报名人员ID (EventRegistrationPerson的Id) (路径参数)
* @param {number} id - 报名人员ID (EventRegistrationPerson的Id)
* @returns {Promise<Object>} 返回证书详情
*/
export const eventscoreGetCertificateDetailGet = (id) => {
return http.get(`/EventScore/GetCertificateDetail/${id}`);
return http.post('/eventscore/getcertificatedetail', { id });
};
/**
@ -139,4 +139,22 @@ export const eventscoreDeleteDelete = (data) => {
*/
export const eventscoreSetstatusPatch = (data) => {
return http.patch('/eventscore/setstatus', data);
};
/**
* 查看证书 URL
* @param {Object} params - 查询参数
* @param {number} params.id - 报名人员ID
*/
export const eventscoreViewcertificateGet = (params) => {
return http.get('/eventscore/viewcertificate', { params });
};
/**
* 发放积分
* @param {Object} data - 发放积分参数
* @param {number} data.eventId - 赛事ID
*/
export const eventscoreIssuepointsPost = (data) => {
return http.post('/eventscore/issuePoints', data);
};

View File

@ -128,35 +128,61 @@
<el-radio-button value="教练">教练</el-radio-button>
<el-radio-button value="领队">领队</el-radio-button>
</el-radio-group>
<ProTable v-if="user == '运动员'" ref="tableuu" :request-api="eventscorePageathletePost"
:columns="columnsScore" :pagination="true" :tool-button="false" :initParam="initParam" toolButton
preserve-expanded-content @selectionChange="selection">
<template #tools>
<el-button v-auth="'event:add'" type="primary" :loading="importingScore" @click="inScore"> 导入成绩 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="downloadTemplate"> 下载模板 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="allSure"> 发放证书 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="exportCertificate"> 导出证书 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #action="scope">
<el-button size="small" text type="primary" @click="onLookBook(scope.row.id)"> 查看证书
</el-button>
</template>
</ProTable>
<div v-if="user == '运动员'" class="score-certificate-container">
<div class="project-group-panel">
<div class="panel-header">项目-组别列表</div>
<el-tree
ref="projectGroupTreeRef"
:data="projectGroupTreeData"
:props="treeProps"
node-key="nodeKey"
highlight-current
:expand-on-click-node="false"
@node-click="handleTreeNodeClick"
default-expand-all
>
<template #default="{ node, data }">
<span class="tree-node-label">
{{ node.label }}
</span>
</template>
</el-tree>
</div>
<div class="athlete-list-panel">
<ProTable ref="tableuu" :request-api="eventscorePageathletePost"
:columns="columnsScore" :pagination="true" :tool-button="false" :initParam="scoreQueryParam" toolButton
preserve-expanded-content @selectionChange="selection">
<template #tools>
<el-button v-auth="'event:add'" type="primary" :loading="importingScore" @click="inScore"> 导入成绩 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="downloadTemplate"> 下载模板 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="openIssueCertificateDialog"> 发放证书 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="exportCertificate"> 导出证书 </el-button>
<el-button v-auth="'event:add'" type="success" :loading="issuingPoints" @click="handleIssuePoints"> 发放积分 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #action="scope">
<el-button size="small" text type="primary" @click="viewCertificate(scope.row.id)"> 查看证书
</el-button>
</template>
</ProTable>
</div>
</div>
<ProTable v-if="user == '教练'" ref="tableCoach" :request-api="eventscorePagecoachleaderPost"
:columns="columnsCoach" :pagination="true" :tool-button="false" :initParam="initParamCoach" toolButton
preserve-expanded-content @selectionChange="selection">
<template #tools>
<el-button v-auth="'event:add'" type="primary" @click="allSure"> 发放证书 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="openIssueCertificateDialog"> 发放证书 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #action="scope">
<el-button size="small" text type="primary" @click="onLookBook(scope.row.id)"> 查看证书
<el-button size="small" text type="primary" @click="viewCertificate(scope.row.id)"> 查看证书
</el-button>
</template>
</ProTable>
@ -165,16 +191,34 @@
:columns="columnsLeader" :pagination="true" :tool-button="false" :initParam="initParamLeader" toolButton
preserve-expanded-content @selectionChange="selection">
<template #tools>
<el-button v-auth="'event:add'" type="primary" @click="allSure"> 发放证书 </el-button>
<el-button v-auth="'event:add'" type="primary" @click="openIssueCertificateDialog"> 发放证书 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #action="scope">
<el-button size="small" text type="primary" @click="onLookBook(scope.row.id)"> 查看证书
<el-button size="small" text type="primary" @click="viewCertificate(scope.row.id)"> 查看证书
</el-button>
</template>
</ProTable>
<!-- 发放证书对话框 -->
<el-dialog v-model="certificateDialogVisible" title="发放证书" width="500px">
<el-form :model="certificateForm" label-width="120px">
<el-form-item label="选择证书模板">
<el-select v-model="selectedTemplateId" placeholder="请选择证书模板" style="width: 100%;">
<el-option label="默认模板" :value="undefined" />
</el-select>
</el-form-item>
<el-form-item label="已选人数">
<span>{{ selectListIds.value?.length || 0 }} 人</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="certificateDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="issuingCertificate" @click="issueCertificate">确定发放</el-button>
</template>
</el-dialog>
</el-tab-pane>
</el-tabs>
@ -247,7 +291,7 @@ import { Check, View, Download, Rank, Warning } from '@element-plus/icons-vue';
import { eventDetailGet } from '/@/api/match/event';
import { eventarrangementListGet, eventarrangementPut, eventarrangementExportPost, eventarrangementPreviewPost } from '/@/api/match/eventarrangement';
import { eventregistrationPagePost, eventregistrationBatchauditPost, eventregistrationMaterialurlsPost } from '/@/api/match/eventregistration';
import { eventscorePageathletePost, eventscorePagecoachleaderPost,eventscoreImportscorePost, eventscoreDownloadtemplateGet, eventscoreExportcertificatePost } from '/@/api/match/eventscore';
import { eventscorePageathletePost, eventscorePagecoachleaderPost,eventscoreImportscorePost, eventscoreDownloadtemplateGet, eventscoreExportcertificatePost, eventscoreIssuecertificatePost, eventscoreGetCertificateDetailGet, eventscoreIssuepointsPost } from '/@/api/match/eventscore';
import Step1BasicInfo from './Steps/Step1BasicInfo.vue';
import Step2Units from './Steps/Step2Units.vue';
import Step3ProjectConfig from './Steps/Step3ProjectConfig.vue';
@ -261,6 +305,7 @@ const isShowDialog = ref(false);
const activeName = ref('first')
const eventlist = ref([]);
const importingScore = ref(false);
const issuingPoints = ref(false);
const arrangementContainerRef = ref(null);
let sortableInstance = null;
@ -272,6 +317,12 @@ const scoreFileInputRef = ref(null);
const tableuu = ref();
const tableCoach = ref();
const tableLeader = ref();
// 证书发放相关
const certificateDialogVisible = ref(false);
const selectedTemplateId = ref();
const issuingCertificate = ref(false);
const certificateForm = ref({});
// 表单数据(响应式对象)
const formData = ref({
// 步骤1:基本信息
@ -353,6 +404,9 @@ const openDialog = async (id) => {
};
formData.value.feeConfig = { ...formData.value.feeConfig, ...(d.feeConfig ?? {}) };
console.log('formData.value', formData.value);
projectGroupTreeData.value = buildProjectGroupTree(d.projects)
resetSelectedGroup()
}
const res2 = await eventarrangementListGet(id);
@ -541,6 +595,13 @@ const selection = (e) => {
//预览赛事编排
const previewArrangement = async () => {
try {
// 验证场地数
const validation = validateVenueCount();
if (!validation.valid) {
ElMessage.warning(validation.message);
return;
}
const res = await eventarrangementPreviewPost(initParam.value.id);
if (res.succeeded) {
previewData.value = res.data || [];
@ -610,6 +671,13 @@ const downloadByBlob = (blob, filename = '赛事编排.xlsx') => {
const getArr = async () => {
try {
// 验证场地数
const validation = validateVenueCount();
if (!validation.valid) {
ElMessage.warning(validation.message);
return;
}
const { succeeded, data, errors } = await eventarrangementExportPost(initParam.value.id);
if (!succeeded) {
ElMessage.error(errors || '导出失败');
@ -659,7 +727,8 @@ const columnsScore = ref([
{ prop: 'index', label: '序号', align: 'center', type: 'selection' },
{ prop: 'name', label: '姓名', width: '120px', },
{ prop: 'score', label: '成绩', width: '120px', },
{ prop: 'rank', label: '名次', },
{ prop: 'unitName', label: '单位', width: '180px', },
{ prop: 'rankText', label: '名次', width: '120px', },
{
prop: 'gender',
label: '性别',
@ -671,9 +740,67 @@ const columnsScore = ref([
{ label: '女', value: 1 },
],
},
{ prop: 'unitName', label: '单位', },
{ prop: 'action', label: '操作', align: 'center', width: 140, fixed: 'right' }
])
const projectGroupTreeRef = ref()
const projectGroupTreeData = ref([])
const treeProps = {
children: 'children',
label: 'label'
}
const selectedProjectGroup = ref(null)
const scoreQueryParam = computed(() => ({
refresh: initParam.value.refresh,
eventId: initParam.value.eventId,
projectId: selectedProjectGroup.value?.type === 'project' ? selectedProjectGroup.value?.id : undefined,
groupName: selectedProjectGroup.value?.type === 'group' ? selectedProjectGroup.value?.groupName : undefined
}))
const buildProjectGroupTree = (projects) => {
const treeData = []
if (!projects || !Array.isArray(projects)) return treeData
projects.forEach(project => {
const projectNode = {
nodeKey: `project-${project.id}`,
id: project.id,
type: 'project',
label: project.name,
children: []
}
if (project.groups && Array.isArray(project.groups)) {
project.groups.forEach((group, idx) => {
projectNode.children.push({
nodeKey: `group-${project.id}-${idx}`,
id: project.id,
type: 'group',
groupName: group.groupName,
label: group.groupName
})
})
}
treeData.push(projectNode)
})
return treeData
}
const handleTreeNodeClick = (data) => {
selectedProjectGroup.value = data
initParam.value.refresh = !initParam.value.refresh
}
const resetSelectedGroup = () => {
selectedProjectGroup.value = null
if (projectGroupTreeRef.value) {
projectGroupTreeRef.value.setCurrentKey(null)
}
initParam.value.refresh = !initParam.value.refresh
}
const columnsCoach = ref([
{ prop: 'index', label: '序号', align: 'center', type: 'selection' },
{ prop: 'name', label: '教练姓名', width: '120px', search: { el: 'input' }, },
@ -689,8 +816,8 @@ const columnsCoach = ref([
{ label: '女', value: 1 },
],
},
{ prop: 'idCard', label: '身份证号', },
{ prop: 'phone', label: '联系电话', search: { el: 'input' }, },
{ prop: 'idCardNumber', label: '身份证号', },
{ prop: 'mobile', label: '联系电话', search: { el: 'input' }, },
{ prop: 'action', label: '操作', align: 'center', width: 140, fixed: 'right' }
])
const columnsLeader = ref([
@ -708,8 +835,8 @@ const columnsLeader = ref([
{ label: '女', value: 1 },
],
},
{ prop: 'idCard', label: '身份证号', },
{ prop: 'phone', label: '联系电话', search: { el: 'input' }, },
{ prop: 'idCardNumber', label: '身份证号', },
{ prop: 'mobile', label: '联系电话', search: { el: 'input' }, },
{ prop: 'action', label: '操作', align: 'center', width: 140, fixed: 'right' }
])
@ -810,6 +937,99 @@ const exportCertificate = async () => {
}
};
// 打开发放证书对话框
const openIssueCertificateDialog = () => {
if (!selectListIds.value || selectListIds.value.length === 0) {
ElMessage.warning('请先选择要发放证书的人员');
return;
}
selectedTemplateId.value = undefined;
certificateDialogVisible.value = true;
};
// 发放证书
const issueCertificate = async () => {
if (!selectListIds.value || selectListIds.value.length === 0) {
ElMessage.warning('请先选择要发放证书的人员');
return;
}
issuingCertificate.value = true;
try {
const res = await eventscoreIssuecertificatePost({
personIds: selectListIds.value,
eventId: initParam.value.eventId,
templateId: selectedTemplateId.value
});
if (res?.succeeded) {
ElMessage.success(`证书发放成功!成功 ${res.data?.successCount || 0} 条,失败 ${res.data?.failCount || 0} 条`);
certificateDialogVisible.value = false;
refreshScoreTable();
} else {
ElMessage.error(res?.errors || '证书发放失败');
}
} catch (error) {
console.error('发放证书失败:', error);
ElMessage.error('发放证书失败');
} finally {
issuingCertificate.value = false;
}
};
// 发放积分
const handleIssuePoints = async () => {
try {
ElMessageBox.confirm('确定要发放积分吗?系统将根据赛事积分规则为符合条件的用户发放积分。', '发放积分确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
issuingPoints.value = true;
try {
const res = await eventscoreIssuepointsPost({
eventId: initParam.value.eventId
});
if (res?.succeeded) {
ElMessage.success(res.data?.Message || '积分发放成功');
refreshScoreTable();
} else {
ElMessage.error(res?.errors || '积分发放失败');
}
} catch (error) {
console.error('发放积分失败:', error);
ElMessage.error(error?.message || '积分发放失败');
} finally {
issuingPoints.value = false;
}
}).catch(() => {
// 用户取消
});
} catch (error) {
console.error('发放积分确认失败:', error);
}
};
// 查看证书
const viewCertificate = async (id) => {
try {
const { succeeded, data, errors } = await eventscoreGetCertificateDetailGet(id);
if (succeeded) {
if (data?.url) {
window.open(data.url, '_blank');
} else if (data?.Url) {
window.open(data.Url, '_blank');
} else {
ElMessage.warning('该人员暂无证书');
}
} else {
ElMessage.error(errors || '查看证书失败');
}
} catch (error) {
console.error('查看证书失败:', error);
ElMessage.error('查看证书失败');
}
};
// 获取组别单位显示
const getGroupUnit = (projectType) => {
return projectType && projectType.includes('团队') ? '组' : '人';
@ -907,16 +1127,34 @@ const calculateProjectDuration = (item) => {
return { totalMinutes, totalSeconds, error };
};
// 监听 eventlist 变化,实时计算总时长
// 验证场地数是否有效
const validateVenueCount = () => {
if (!eventlist.value || eventlist.value.length === 0) {
return { valid: true };
}
const invalidProjects = [];
eventlist.value.forEach((item) => {
if (item.venueCount === 0 || !item.venueCount) {
invalidProjects.push(item.projectName);
}
});
if (invalidProjects.length > 0) {
const message = invalidProjects.map(name => `${name}: 场地数不能为0`).join('\n');
return { valid: false, message };
}
return { valid: true };
};
// 监听 eventlist 变化,实时计算总时长(不显示错误提示)
watch(
eventlist,
(newList) => {
if (newList && newList.length > 0) {
newList.forEach((item) => {
const result = calculateProjectDuration(item);
if (result.error) {
ElMessage.warning(`${item.projectName}: ${result.error}`);
}
item.totalDurationMinutes = result.totalMinutes;
});
}
@ -1240,4 +1478,105 @@ defineExpose({
width: 8%;
}
/* 成绩与证书页面样式 */
.score-certificate-container {
display: flex;
gap: 16px;
height: 600px;
}
.project-group-panel {
flex: 0 0 280px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #ffffff;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 12px 16px;
background: #409eff;
color: #ffffff;
font-size: 14px;
font-weight: 600;
border-radius: 4px 4px 0 0;
}
.project-group-panel :deep(.el-tree) {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.project-group-panel :deep(.el-tree-node__content) {
height: 36px;
padding-left: 16px !important;
}
.project-group-panel :deep(.el-tree-node__content:hover) {
background: #f5f7fa;
}
.project-group-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #ecf5ff;
color: #409eff;
font-weight: 500;
}
.tree-node-label {
font-size: 13px;
}
.athlete-list-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.athlete-list-panel :deep(.el-card__body) {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
}
@media (max-width: 1200px) {
.score-certificate-container {
flex-direction: column;
height: auto;
min-height: 600px;
}
.project-group-panel {
flex: none;
height: 250px;
}
.athlete-list-panel {
flex: 1;
min-height: 350px;
}
}
@media (max-width: 768px) {
.score-certificate-container {
gap: 12px;
}
.project-group-panel {
flex: none;
height: 200px;
}
.panel-header {
padding: 10px 12px;
font-size: 13px;
}
}
</style>