diff --git a/src/api/match/eventscore.js b/src/api/match/eventscore.js index 4a815e8..c1bc5b0 100644 --- a/src/api/match/eventscore.js +++ b/src/api/match/eventscore.js @@ -89,11 +89,11 @@ export const eventscoreIssuecertificatePost = (data) => { /** * 查看证书 - * @param {number} id - 报名人员ID (EventRegistrationPerson的Id) (路径参数) + * @param {number} id - 报名人员ID (EventRegistrationPerson的Id) * @returns {Promise} 返回证书详情 */ 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); }; \ No newline at end of file diff --git a/src/views/match/manage/work.vue b/src/views/match/manage/work.vue index 88c92b0..2318a2a 100644 --- a/src/views/match/manage/work.vue +++ b/src/views/match/manage/work.vue @@ -128,35 +128,61 @@ 教练 领队 - - - - - + +
+
+
项目-组别列表
+ + + +
+ +
+ + + + + +
+
@@ -165,16 +191,34 @@ :columns="columnsLeader" :pagination="true" :tool-button="false" :initParam="initParamLeader" toolButton preserve-expanded-content @selectionChange="selection"> + + + + + + + + + + + {{ selectListIds.value?.length || 0 }} 人 + + + + @@ -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; + } +} +