赛事页面调整
This commit is contained in:
parent
d0a527dd9e
commit
ed6cb2b19e
@ -6,14 +6,16 @@ import { http } from '../../utils/request';
|
||||
/**
|
||||
* 分页查询参赛记录
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.AppUserId] - 普通用户ID
|
||||
* @param {number} [params.MerchantId] - 商户ID
|
||||
* @param {number} [params.AppUserId] - 普通用户ID(AppUser表的Id)
|
||||
* @param {number} [params.SysUserId] - 系统用户ID(SysUser表的Id,用于单位用户)
|
||||
* @param {number} [params.MerchantId] - 商户ID(已弃用,使用SysUserId)
|
||||
* @param {string} [params.EventName] - 赛事名称 (模糊查询)
|
||||
* @param {number} [params.RegistrationStatus] - 报名状态 (0:待支付, 1:已支付, 2:已取消, 3:已退款, 4:已完成)
|
||||
* @param {number} [params.PaymentStatus] - 支付状态 (0:未支付/待审核, 1:已支付/审核通过, 2:支付失败/审核拒绝, 3:已退款, 4:已取消)
|
||||
* @param {string} [params.StartTime] - 开始时间
|
||||
* @param {string} [params.EndTime] - 结束时间
|
||||
* @param {number} [params.PageNo] - 当前页码
|
||||
* @param {number} [params.PageSize] - 每页条数
|
||||
* @param {number} [params.refresh] - 刷新标记(用于强制刷新数据)
|
||||
* @returns {Promise<{
|
||||
* pageNo: number,
|
||||
* pageSize: number,
|
||||
|
||||
@ -69,24 +69,44 @@
|
||||
</ProTable>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="赛事编排" name="third">
|
||||
<el-descriptions :title="item.projectName" v-for="(item, index) in eventlist" :key="index">
|
||||
<el-descriptions-item label="排序:">
|
||||
<el-input-number v-model="item.sortIndex" :min="1" :step="1" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="设置场地数量:">
|
||||
<el-input-number v-model="item.venueCount" :min="0" :step="1" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型:">{{ item.projectType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="时长:">{{ item.totalDurationMinutes }}:{{ item.durationSeconds
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item :label="`组别${idx + 1}:`" v-for="(group, idx) in item.groups">
|
||||
<el-tag>{{ group.groupName }}</el-tag>
|
||||
<span style="margin: 0 70px;">参与人数: {{ group.participantCount }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div style="text-align: right; margin-top: 20px;">
|
||||
<el-button type="primary" @click="onSave"> 保存修改 </el-button>
|
||||
<el-button type="primary" @click="getArr"> 导出编排 </el-button>
|
||||
<div class="arrangement-container">
|
||||
<div class="sort-hint-top">
|
||||
<el-icon><Rank /></el-icon>
|
||||
<span>拖动项目卡片可调整顺序,按最终顺序导出</span>
|
||||
</div>
|
||||
<div ref="arrangementContainerRef" class="arrangement-cards-wrapper">
|
||||
<div class="arrangement-card" v-for="(item, index) in eventlist" :key="index">
|
||||
<div class="card-left">
|
||||
<div class="project-name">{{ item.projectName }}</div>
|
||||
<div class="project-type">类型: {{ item.projectType }}</div>
|
||||
<div class="project-duration">时长: {{ item.totalDurationMinutes }}分{{ item.durationSeconds }}秒</div>
|
||||
</div>
|
||||
|
||||
<div class="card-middle">
|
||||
<div class="group-tag" v-for="(group, idx) in item.groups" :key="idx">
|
||||
<span class="group-name">{{ group.groupName }}</span>
|
||||
<span class="group-count">{{ group.participantCount }}{{ getParticipantUnit(item.projectType) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-right">
|
||||
<div class="venue-box">
|
||||
<div class="box-label">设置场地数量</div>
|
||||
<el-input-number v-model="item.venueCount" :min="0" :step="1" size="large" />
|
||||
</div>
|
||||
<div class="duration-box">
|
||||
<div class="box-label">项目所需时长</div>
|
||||
<div class="duration-value">{{ item.totalDurationMinutes }}'</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-bar">
|
||||
<el-button type="default" @click="previewArrangement">预览</el-button>
|
||||
<el-button type="primary" @click="onSave">保存修改</el-button>
|
||||
<el-button type="primary" @click="getArr">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="成绩与证书" name="fourth">
|
||||
@ -153,15 +173,66 @@
|
||||
@change="onScoreFileChange"
|
||||
/>
|
||||
|
||||
<!-- 赛事编排预览对话框 -->
|
||||
<el-dialog
|
||||
v-model="previewDialogVisible"
|
||||
title="赛事编排预览"
|
||||
width="90%"
|
||||
style="min-height: 70vh;"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="preview-container">
|
||||
<div class="preview-title">{{ previewEventName }}</div>
|
||||
<div class="preview-table-wrapper">
|
||||
<table class="preview-table" border="1" cellpadding="6" cellspacing="0">
|
||||
<thead>
|
||||
<tr class="header-row">
|
||||
<th class="time-col">时间</th>
|
||||
<th class="project-col">参赛项目</th>
|
||||
<th class="group-col">参赛组别</th>
|
||||
<th class="team-col">参赛队伍</th>
|
||||
<th class="athlete-col">运动员姓名</th>
|
||||
<th class="session-col">场次</th>
|
||||
<th class="venue-col">场地</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(item, index) in previewData" :key="index">
|
||||
<tr :class="index % 2 === 0 ? 'data-row-even' : 'data-row-odd'">
|
||||
<td
|
||||
v-if="!isTimeMerged(index)"
|
||||
:rowspan="getTimeRowspan(index)"
|
||||
class="time-cell"
|
||||
>
|
||||
{{ formatTime(item.timeRange) }}
|
||||
</td>
|
||||
<td class="project-cell">{{ item.projectName }}</td>
|
||||
<td class="group-cell">{{ item.groupName }}</td>
|
||||
<td class="team-cell">{{ item.teamName || '-' }}</td>
|
||||
<td class="athlete-cell">{{ item.athleteName }}</td>
|
||||
<td class="session-cell">{{ item.sessionIndex }}</td>
|
||||
<td class="venue-cell">{{ item.venueIndex }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="previewDialogVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="getArr">导出Excel</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, markRaw } from 'vue';
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ArrowLeft, ArrowRight, Check, CircleCheckFilled, Refresh } from '@element-plus/icons-vue';
|
||||
import { eventPost, eventPut, eventDetailGet } from '/@/api/match/event';
|
||||
import { eventarrangementListGet, eventarrangementPut, eventarrangementExportPost } from '/@/api/match/eventarrangement';
|
||||
import { Check, View, Download, Rank } 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 Step1BasicInfo from './Steps/Step1BasicInfo.vue';
|
||||
@ -170,12 +241,20 @@ import Step3ProjectConfig from './Steps/Step3ProjectConfig.vue';
|
||||
import Step4FeeConfig from './Steps/Step4FeeConfig.vue';
|
||||
import ProTable from '/@/components/ProTable/index.vue';
|
||||
import PreviewImage from '/@/components/PreviewImage/index.vue';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
|
||||
const isShowDialog = ref(false);
|
||||
const activeName = ref('first')
|
||||
const eventlist = ref([]);
|
||||
const importingScore = ref(false);
|
||||
const arrangementContainerRef = ref(null);
|
||||
let sortableInstance = null;
|
||||
|
||||
// 预览相关
|
||||
const previewDialogVisible = ref(false);
|
||||
const previewData = ref([]);
|
||||
const previewEventName = ref('赛事编排预览');
|
||||
const scoreFileInputRef = ref(null);
|
||||
const tableuu = ref();
|
||||
const tableCoach = ref();
|
||||
@ -230,9 +309,11 @@ const formData = ref({
|
||||
const openDialog = async (id) => {
|
||||
isShowDialog.value = true;
|
||||
const res = await eventDetailGet(id);
|
||||
initParam.value.eventId = id;
|
||||
initParam.value.id = id;
|
||||
const d = res?.data;
|
||||
if (d) {
|
||||
previewEventName.value = d.eventName || '赛事编排预览';
|
||||
formData.value.basicInfo = {
|
||||
...formData.value.basicInfo,
|
||||
id: d.id,
|
||||
@ -263,22 +344,48 @@ const openDialog = async (id) => {
|
||||
|
||||
const res2 = await eventarrangementListGet(id);
|
||||
eventlist.value = res2.data
|
||||
|
||||
await nextTick();
|
||||
initSortable();
|
||||
};
|
||||
|
||||
const initSortable = () => {
|
||||
if (!arrangementContainerRef.value) return;
|
||||
|
||||
if (sortableInstance) {
|
||||
sortableInstance.destroy();
|
||||
}
|
||||
|
||||
sortableInstance = new Sortable(arrangementContainerRef.value, {
|
||||
animation: 150,
|
||||
ghostClass: 'sort-ghost',
|
||||
dragClass: 'sort-drag',
|
||||
handle: '.arrangement-card',
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) return;
|
||||
|
||||
const item = eventlist.value.splice(oldIndex, 1)[0];
|
||||
eventlist.value.splice(newIndex, 0, item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const initParam = ref({
|
||||
refresh: false,
|
||||
id: '',
|
||||
eventId: 0,
|
||||
id: 0,
|
||||
})
|
||||
|
||||
const initParamCoach = computed(() => ({
|
||||
refresh: initParam.value.refresh,
|
||||
id: initParam.value.id,
|
||||
eventId: initParam.value.eventId,
|
||||
personType: 'Coach'
|
||||
}))
|
||||
|
||||
const initParamLeader = computed(() => ({
|
||||
refresh: initParam.value.refresh,
|
||||
id: initParam.value.id,
|
||||
eventId: initParam.value.eventId,
|
||||
personType: 'Leader'
|
||||
}))
|
||||
|
||||
@ -378,6 +485,52 @@ const selection = (e) => {
|
||||
selectListIds.value.push(element.id);
|
||||
});
|
||||
}
|
||||
//预览赛事编排
|
||||
const previewArrangement = async () => {
|
||||
try {
|
||||
const res = await eventarrangementPreviewPost(initParam.value.id);
|
||||
if (res.succeeded) {
|
||||
previewData.value = res.data || [];
|
||||
previewDialogVisible.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('预览编排失败:', error);
|
||||
ElMessage.error('预览编排失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 时间格式化
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '';
|
||||
// 将 "30''" 格式转换为 "30''"
|
||||
return timeStr.replace("''", "''");
|
||||
}
|
||||
|
||||
// 判断是否需要合并时间列
|
||||
const isTimeMerged = (index) => {
|
||||
if (index === 0) return false;
|
||||
const current = previewData.value[index];
|
||||
const prev = previewData.value[index - 1];
|
||||
return current?.projectName === prev?.projectName && current?.timeRange === prev?.timeRange;
|
||||
}
|
||||
|
||||
// 获取时间列的合并行数
|
||||
const getTimeRowspan = (index) => {
|
||||
const current = previewData.value[index];
|
||||
if (!current) return 1;
|
||||
|
||||
let count = 1;
|
||||
for (let i = index + 1; i < previewData.value.length; i++) {
|
||||
const next = previewData.value[i];
|
||||
if (next?.projectName === current.projectName && next?.timeRange === current.timeRange) {
|
||||
count++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
//更新赛事编排
|
||||
const onSave = async () => {
|
||||
const res = await eventarrangementPut({
|
||||
@ -577,7 +730,7 @@ const downloadTemplate = async () => {
|
||||
const exportCertificate = async () => {
|
||||
try {
|
||||
const { succeeded, data, errors } = await eventscoreExportcertificatePost({
|
||||
eventId: initParam.value.id
|
||||
eventId: initParam.value.eventId
|
||||
});
|
||||
if (!succeeded) {
|
||||
ElMessage.error(errors || '导出证书失败');
|
||||
@ -604,8 +757,291 @@ const exportCertificate = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取参与人数的单位显示
|
||||
const getParticipantUnit = (projectType) => {
|
||||
return projectType === '团队' ? '队' : '人';
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.arrangement-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.sort-hint-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #ecf5ff;
|
||||
border: 1px solid #b3d8ff;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sort-hint-top .el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.arrangement-cards-wrapper {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.arrangement-card {
|
||||
display: flex;
|
||||
border: 2px solid #409eff;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
cursor: move;
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.arrangement-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sort-ghost {
|
||||
opacity: 0.4;
|
||||
background: #c6e2ff !important;
|
||||
}
|
||||
|
||||
.sort-drag {
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
.card-left {
|
||||
flex: 0 0 180px;
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-left .project-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-left .project-type,
|
||||
.card-left .project-duration {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-middle {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-content: flex-start;
|
||||
border-right: 2px solid #409eff;
|
||||
}
|
||||
|
||||
.group-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid #409eff;
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.group-tag .group-name {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.group-tag .group-count {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.card-right {
|
||||
flex: 0 0 280px;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.venue-box,
|
||||
.duration-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.venue-box {
|
||||
border-right: 2px solid #409eff;
|
||||
}
|
||||
|
||||
.box-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.venue-box .el-input-number {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.venue-box :deep(.el-input__wrapper) {
|
||||
justify-content: center;
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.venue-box :deep(.el-input__inner) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.duration-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.action-bar .el-button {
|
||||
min-width: 100px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.arrangement-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-left {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-middle {
|
||||
border-right: none;
|
||||
border-bottom: 2px solid #409eff;
|
||||
}
|
||||
|
||||
.card-right {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.venue-box {
|
||||
border-right: 2px solid #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 赛事编排预览样式 */
|
||||
.preview-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #ffd9b3;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.preview-table-wrapper {
|
||||
overflow-x: auto;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
.preview-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.preview-table .header-row {
|
||||
background: #ccffcc;
|
||||
}
|
||||
|
||||
.preview-table th {
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.preview-table td {
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.data-row-even {
|
||||
background: #f0fff0;
|
||||
}
|
||||
|
||||
.data-row-odd {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.time-col, .time-cell {
|
||||
width: 10%;
|
||||
background: #e8f4f8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-col, .project-cell {
|
||||
width: 16%;
|
||||
}
|
||||
|
||||
.group-col, .group-cell {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.team-col, .team-cell {
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
.athlete-col, .athlete-cell {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.session-col, .session-cell {
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
.venue-col, .venue-cell {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -37,8 +37,8 @@
|
||||
</ProTable>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<el-dialog title="参赛记录" v-model="isShowDialog" width="80%">
|
||||
<ProTable ref="table" :request-api="eventrecordPageGet" :columns="columnsUser" :pagination="true"
|
||||
<el-dialog title="参赛记录" v-model="isShowDialog" width="80%" @close="onDialogClose">
|
||||
<ProTable v-if="isShowDialog" ref="table" :request-api="eventrecordPageGet" :columns="columnsUser" :pagination="true"
|
||||
:tool-button="false" :initParams="initParamUser">
|
||||
<template #index="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
@ -89,7 +89,7 @@ const initParam = ref({
|
||||
});
|
||||
const selectedListIds = ref([]);
|
||||
const initParamUser = ref({
|
||||
appUserId: '',
|
||||
appUserId: null,
|
||||
});
|
||||
const statusList = ['info', 'warning', 'success', 'danger'];
|
||||
|
||||
@ -214,11 +214,28 @@ const columnsUser = ref([
|
||||
]);
|
||||
|
||||
const isShowDialog = ref(false);
|
||||
const table = ref();
|
||||
|
||||
const onOpenMenu = (row) => {
|
||||
console.log(row);
|
||||
|
||||
// 参数验证
|
||||
if (!row || !row.id) {
|
||||
ElMessage.warning('用户信息无效,无法查看参赛记录');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof row.id !== 'number' || row.id <= 0) {
|
||||
ElMessage.warning('用户ID无效,请刷新页面后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置用户ID参数
|
||||
initParamUser.value.appUserId = row.id;
|
||||
isShowDialog.value = true;
|
||||
|
||||
// 下次打开对话框时强制刷新数据
|
||||
initParamUser.value.refresh = Date.now();
|
||||
};
|
||||
|
||||
const onBatchAudit = async () => {
|
||||
@ -250,6 +267,15 @@ const lookBook = async (row) => {
|
||||
};
|
||||
|
||||
|
||||
// 对话框关闭时清理
|
||||
const onDialogClose = () => {
|
||||
// 清理参数,避免下次查询时使用旧参数
|
||||
initParamUser.value = {
|
||||
appUserId: null,
|
||||
refresh: Date.now()
|
||||
};
|
||||
};
|
||||
|
||||
// 取消
|
||||
const onCancel = () => {
|
||||
isShowDialog.value = false;
|
||||
|
||||
@ -33,8 +33,8 @@
|
||||
</ProTable>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<el-dialog title="参赛记录" v-model="isShowDialog" width="80%">
|
||||
<ProTable ref="table" :request-api="eventrecordPageGet" :columns="columnsUser" :pagination="true"
|
||||
<el-dialog title="参赛记录" v-model="isShowDialog" width="80%" @close="onDialogClose">
|
||||
<ProTable v-if="isShowDialog" ref="table" :request-api="eventrecordPageGet" :columns="columnsUser" :pagination="true"
|
||||
:tool-button="false" :initParams="initParamUser">
|
||||
<template #index="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
@ -105,7 +105,7 @@ const initParam = ref({
|
||||
});
|
||||
const selectedListIds = ref([]);
|
||||
const initParamUser = ref({
|
||||
appUserId: '',
|
||||
sysUserId: null,
|
||||
});
|
||||
const statusList = ['info', 'warning', 'success', 'danger'];
|
||||
|
||||
@ -219,11 +219,28 @@ const columnsUser = ref([
|
||||
]);
|
||||
|
||||
const isShowDialog = ref(false);
|
||||
const table = ref();
|
||||
|
||||
const onOpenMenu = (row) => {
|
||||
console.log(row);
|
||||
initParamUser.value.appUserId = row.userId;
|
||||
|
||||
// 参数验证
|
||||
if (!row || !row.userId) {
|
||||
ElMessage.warning('用户信息无效,无法查看参赛记录');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof row.userId !== 'number' || row.userId <= 0) {
|
||||
ElMessage.warning('用户ID无效,请刷新页面后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置用户ID参数 - 单位用户使用 sysUserId
|
||||
initParamUser.value.sysUserId = row.userId;
|
||||
isShowDialog.value = true;
|
||||
|
||||
// 下次打开对话框时强制刷新数据
|
||||
initParamUser.value.refresh = Date.now();
|
||||
};
|
||||
|
||||
const onCancelB = () => {
|
||||
@ -275,6 +292,15 @@ const onSubmitAudit = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 对话框关闭时清理
|
||||
const onDialogClose = () => {
|
||||
// 清理参数,避免下次查询时使用旧参数
|
||||
initParamUser.value = {
|
||||
sysUserId: null,
|
||||
refresh: Date.now()
|
||||
};
|
||||
};
|
||||
|
||||
// 取消
|
||||
const onCancel = () => {
|
||||
isShowDialog.value = false;
|
||||
|
||||
@ -174,7 +174,63 @@
|
||||
</span>
|
||||
<el-icon class="assign-card-close" @click.stop="toggleGroupSelection(group.id)"><Close /></el-icon>
|
||||
</div>
|
||||
<div class="assign-card-members">
|
||||
|
||||
<!-- 团队数量信息和添加团队按钮 -->
|
||||
<div class="team-info-section" v-if="isTeamProject(group) && getGroupMaxTeams(group.id) > 1">
|
||||
<div class="team-count-info">
|
||||
已添加 {{ getGroupTeams(group.id).length }} / {{ getGroupMaxTeams(group.id) }} 队
|
||||
</div>
|
||||
<el-button
|
||||
v-if="getGroupTeams(group.id).length < getGroupMaxTeams(group.id)"
|
||||
type="primary"
|
||||
size="small"
|
||||
text
|
||||
@click.stop="addTeam(group.id)"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加团队
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 团队列表 -->
|
||||
<div v-if="isTeamProject(group) && getGroupMaxTeams(group.id) > 1" class="team-section">
|
||||
<div
|
||||
v-for="team in getGroupTeams(group.id)"
|
||||
:key="team.teamIndex"
|
||||
class="team-card"
|
||||
:class="{ 'team-card-active': activeTeamKey === `${group.id}_${team.teamIndex}` }"
|
||||
@click.stop="setActiveTeam(group.id, team.teamIndex)"
|
||||
>
|
||||
<div class="team-card-header">
|
||||
<span class="team-name">团队{{ getTeamNumber(team.teamIndex) }}</span>
|
||||
<el-button
|
||||
v-if="getGroupTeams(group.id).length > 1"
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
@click.stop="removeTeam(group.id, team.teamIndex)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="team-members">
|
||||
<el-tag
|
||||
v-for="m in team.members"
|
||||
:key="m.userId ?? m.id"
|
||||
closable
|
||||
:type="getMemberValidationResult(m, group).isValid ? '' : 'danger'"
|
||||
@close.stop="removeMemberFromTeam(group.id, team.teamIndex, m.userId ?? m.id)"
|
||||
style="margin: 3px;"
|
||||
>
|
||||
{{ m.name }}
|
||||
</el-tag>
|
||||
<span v-if="!team.members.length" class="team-placeholder">选择此团队后添加队员</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 单团队模式(向后兼容) -->
|
||||
<div v-else class="assign-card-members">
|
||||
<el-tag
|
||||
v-for="m in (groupMemberMap[group.id] ?? [])"
|
||||
:key="m.userId ?? m.id"
|
||||
@ -187,9 +243,10 @@
|
||||
</el-tag>
|
||||
<span v-if="!(groupMemberMap[group.id]?.length)" class="assign-card-placeholder">点击此卡片后,从右侧选择队员</span>
|
||||
</div>
|
||||
|
||||
<!-- 队员验证错误提示 -->
|
||||
<div v-if="hasValidationErrorsForGroup(group)" class="validation-error-section">
|
||||
<div class="validation-error-item" v-for="m in (groupMemberMap[group.id] ?? [])" :key="m.userId ?? m.id">
|
||||
<div class="validation-error-item" v-for="m in (isTeamProject(group) && getGroupMaxTeams(group.id) > 1 ? getGroupTeams(group.id).flatMap(t => t.members) : (groupMemberMap[group.id] ?? []))" :key="m.userId ?? m.id">
|
||||
<el-alert
|
||||
v-if="!getMemberValidationResult(m, group).isValid"
|
||||
:title="getMemberValidationResult(m, group).message"
|
||||
@ -232,7 +289,7 @@
|
||||
:key="m.userId ?? m.id"
|
||||
class="team-member-tag"
|
||||
:class="{ 'team-member-tag-assigned': isMemberAssignedToActive(m.userId ?? m.id) }"
|
||||
@click="assignMemberToActiveGroup(m)"
|
||||
@click="assignMemberToActiveTeam(m)"
|
||||
>
|
||||
{{ m.name }}
|
||||
</div>
|
||||
@ -292,9 +349,37 @@
|
||||
<div v-else-if="activeStep === 2">
|
||||
<el-skeleton :rows="10" animated v-if="previewLoading" />
|
||||
<template v-else>
|
||||
<!-- 支付二维码显示区域 -->
|
||||
<div v-if="paymentData" class="payment-qrcode-section">
|
||||
<div class="qrcode-title">请使用微信扫码支付</div>
|
||||
<!-- 银行转账信息显示区域 -->
|
||||
<div v-if="paymentData && paymentMethod === 3" class="bank-transfer-section">
|
||||
<div class="bank-transfer-title">银行卡转账</div>
|
||||
<div class="bank-transfer-content">
|
||||
<div class="info-item">
|
||||
<text class="info-label">收款商户:</text>
|
||||
<text class="info-value">上海跃动文化科技有限公司</text>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<text class="info-label">收款账号:</text>
|
||||
<text class="info-value">456498465198456261</text>
|
||||
<el-button type="primary" size="small" @click="copyAccount">复制</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment-amount">
|
||||
<span class="amount-label">转账金额:</span>
|
||||
<span class="amount-value">¥ {{ finalPaymentAmount }}</span>
|
||||
</div>
|
||||
<div class="payment-tip">
|
||||
<el-alert title="提示" type="info" :closable="false">
|
||||
请使用银行APP或网银进行转账,转账完成后系统会自动审核。
|
||||
</el-alert>
|
||||
</div>
|
||||
<div class="payment-actions">
|
||||
<el-button @click="cancelPayment">取消支付</el-button>
|
||||
<el-button type="primary" @click="confirmBankTransfer">确认已转账</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 支付二维码显示区域(微信/支付宝) -->
|
||||
<div v-else-if="paymentData" class="payment-qrcode-section">
|
||||
<div class="qrcode-title">请使用{{ paymentMethod === 2 ? '支付宝' : '微信'}}扫码支付</div>
|
||||
<div class="qrcode-wrapper">
|
||||
<div v-if="paymentData.qrcodeUrl" class="qrcode-container">
|
||||
<div ref="qrcodeRef"></div>
|
||||
@ -421,7 +506,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, nextTick, onUnmounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Close, Picture, Loading, CircleCheck, ArrowRight } from '@element-plus/icons-vue';
|
||||
import { Close, Picture, Loading, CircleCheck, ArrowRight, Plus, Delete } from '@element-plus/icons-vue';
|
||||
import ProTable from '/@/components/ProTable/index.vue';
|
||||
import { formatDate } from '/@/utils/formatTime';
|
||||
import {
|
||||
@ -758,6 +843,7 @@ const allGroups = computed(() => {
|
||||
projectCompletionCount: p.completionCount ?? 0,
|
||||
parentProjectTypeName: p.parentProjectTypeName,
|
||||
projectTypeName: p.projectTypeName,
|
||||
projectType: p.projectType,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -860,6 +946,14 @@ const activeAssignGroupId = ref(null); // 当前正在分配队员的组别ID
|
||||
const groupMemberMap = ref({}); // { groupId: [member, ...] }
|
||||
const usedTeamId = ref(null); // 本次报名已使用的团队ID
|
||||
|
||||
// ==================== 多团队管理相关 ====================
|
||||
// 每个组别的团队列表: { groupId: [{ teamIndex, members: [...] }, ...] }
|
||||
const groupTeamsMap = ref({});
|
||||
// 当前正在编辑的团队(组别ID + 团队索引)
|
||||
const activeTeamKey = ref(null); // 格式: "groupId_teamIndex"
|
||||
// 组别限报团队数量: { groupId: maxTeams }
|
||||
const groupMaxTeamsMap = ref({});
|
||||
|
||||
const previewLoading = ref(false);
|
||||
const previewData = ref(null);
|
||||
const paymentMethod = ref(1);
|
||||
@ -913,6 +1007,10 @@ const clearRegisterState = () => {
|
||||
previewData.value = null;
|
||||
paymentMethod.value = 1;
|
||||
draftRegistrationId.value = null;
|
||||
// 多团队管理相关
|
||||
groupTeamsMap.value = {};
|
||||
activeTeamKey.value = null;
|
||||
groupMaxTeamsMap.value = {};
|
||||
// 支付相关
|
||||
paymentData.value = null;
|
||||
polling.value = false;
|
||||
@ -938,6 +1036,10 @@ const resetStep2AndBeyond = () => {
|
||||
activeAssignGroupId.value = null;
|
||||
groupMemberMap.value = {};
|
||||
usedTeamId.value = null;
|
||||
// 多团队管理相关
|
||||
groupTeamsMap.value = {};
|
||||
activeTeamKey.value = null;
|
||||
groupMaxTeamsMap.value = {};
|
||||
previewData.value = null;
|
||||
previewLoading.value = false;
|
||||
draftRegistrationId.value = null;
|
||||
@ -1178,6 +1280,235 @@ const removeMemberFromGroup = (groupId, memberId) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 多团队管理函数 ====================
|
||||
|
||||
// 获取阿拉伯数字表示
|
||||
const getTeamNumber = (num) => {
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
// 判断项目是否为团队类型
|
||||
const isTeamProject = (group) => {
|
||||
// 优先使用 projectType 判断
|
||||
if (group?.projectType) {
|
||||
// 项目类型枚举值:
|
||||
// 1003: 个人速度
|
||||
// 1004: 个人花样
|
||||
// 1005: 团队速度
|
||||
// 1006: 团队花样
|
||||
return group.projectType === 1005 || group.projectType === 1006;
|
||||
}
|
||||
|
||||
// 备用判断:使用 projectTypeName 或 parentProjectTypeName
|
||||
if (group?.projectTypeName || group?.parentProjectTypeName) {
|
||||
const typeName = group.projectTypeName || group.parentProjectTypeName;
|
||||
return typeName && typeName.includes('团队');
|
||||
}
|
||||
|
||||
// 最后的备用判断:使用 projectTeamSize
|
||||
if (group?.projectTeamSize) {
|
||||
return group.projectTeamSize > 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// 初始化组别团队数据
|
||||
const initGroupTeams = (groupId) => {
|
||||
if (!groupTeamsMap.value[groupId]) {
|
||||
groupTeamsMap.value[groupId] = [
|
||||
{
|
||||
teamIndex: 1,
|
||||
members: []
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取某个组别的团队列表
|
||||
const getGroupTeams = (groupId) => {
|
||||
initGroupTeams(groupId);
|
||||
return groupTeamsMap.value[groupId];
|
||||
};
|
||||
|
||||
// 获取某个组别的限报团队数量
|
||||
const getGroupMaxTeams = (groupId) => {
|
||||
return groupMaxTeamsMap.value[groupId] ?? 1;
|
||||
};
|
||||
|
||||
// 添加团队
|
||||
const addTeam = (groupId) => {
|
||||
initGroupTeams(groupId);
|
||||
const teams = groupTeamsMap.value[groupId];
|
||||
const maxTeams = getGroupMaxTeams(groupId);
|
||||
|
||||
if (teams.length >= maxTeams) {
|
||||
ElMessage.warning(`已达到该项目组别限报${maxTeams}队的上限`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newTeamIndex = teams.length + 1;
|
||||
teams.push({
|
||||
teamIndex: newTeamIndex,
|
||||
members: []
|
||||
});
|
||||
|
||||
ElMessage.success(`已添加团队${getTeamNumber(newTeamIndex)}`);
|
||||
};
|
||||
|
||||
// 删除团队
|
||||
const removeTeam = async (groupId, teamIndex) => {
|
||||
initGroupTeams(groupId);
|
||||
const teams = groupTeamsMap.value[groupId];
|
||||
|
||||
if (teams.length <= 1) {
|
||||
ElMessage.warning('至少需要保留一个团队');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除团队${getTeamNumber(teamIndex)}吗?该团队的队员也会被移除。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
const teamIdx = teams.findIndex(t => t.teamIndex === teamIndex);
|
||||
if (teamIdx > -1) {
|
||||
teams.splice(teamIdx, 1);
|
||||
|
||||
// 重新编号团队索引
|
||||
teams.forEach((team, idx) => {
|
||||
team.teamIndex = idx + 1;
|
||||
});
|
||||
|
||||
ElMessage.success('团队删除成功');
|
||||
}
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前激活的团队(如果启用了多团队模式)
|
||||
const getActiveTeamMembers = (groupId) => {
|
||||
// 向后兼容:如果没有启用多团队,使用原来的 groupMemberMap
|
||||
if (!groupTeamsMap.value[groupId] || groupTeamsMap.value[groupId].length === 0) {
|
||||
return groupMemberMap.value[groupId] ?? [];
|
||||
}
|
||||
|
||||
// 多团队模式:找到当前激活的团队
|
||||
if (!activeTeamKey.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [activeGroupId, activeTeamIdx] = activeTeamKey.value.split('_').map(Number);
|
||||
if (activeGroupId !== groupId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const team = groupTeamsMap.value[groupId].find(t => t.teamIndex === activeTeamIdx);
|
||||
return team?.members ?? [];
|
||||
};
|
||||
|
||||
// 设置当前激活的团队
|
||||
const setActiveTeam = (groupId, teamIndex) => {
|
||||
activeTeamKey.value = `${groupId}_${teamIndex}`;
|
||||
};
|
||||
|
||||
// 将成员分配到当前激活的团队
|
||||
const assignMemberToActiveTeam = async (member) => {
|
||||
const gId = activeAssignGroupId.value;
|
||||
if (!gId) {
|
||||
ElMessage.warning('请先点击左侧的组别卡片');
|
||||
return;
|
||||
}
|
||||
|
||||
initGroupTeams(gId);
|
||||
|
||||
// 获取当前组别信息
|
||||
const group = selectedGroupItems.value.find(g => g.id === gId);
|
||||
// 检查是否为团队项目且启用了多团队模式
|
||||
const isMultiTeamMode = group && isTeamProject(group) &&
|
||||
(groupTeamsMap.value[gId].length > 1 || getGroupMaxTeams(gId) > 1);
|
||||
|
||||
if (!isMultiTeamMode) {
|
||||
// 单团队模式,使用原来的逻辑
|
||||
await assignMemberToActiveGroup(member);
|
||||
return;
|
||||
}
|
||||
|
||||
// 多团队模式
|
||||
if (!activeTeamKey.value) {
|
||||
ElMessage.warning('请先选择要分配队员的团队');
|
||||
return;
|
||||
}
|
||||
|
||||
const [activeGroupId, activeTeamIdx] = activeTeamKey.value.split('_').map(Number);
|
||||
if (activeGroupId !== gId) {
|
||||
ElMessage.warning('请先选择对应组别的团队');
|
||||
return;
|
||||
}
|
||||
|
||||
const team = groupTeamsMap.value[gId].find(t => t.teamIndex === activeTeamIdx);
|
||||
if (!team) {
|
||||
return;
|
||||
}
|
||||
|
||||
const memberId = member.userId ?? member.id;
|
||||
// 已存在则不重复添加
|
||||
if (team.members.some((m) => (m.userId ?? m.id) === memberId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查队员是否来自已选择的团队
|
||||
if (usedTeamId.value && member.teamId !== usedTeamId.value) {
|
||||
ElMessage.warning('本次报名只能选择同一个团队的队员');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前组别信息进行验证(group变量已在函数开头声明)
|
||||
if (group) {
|
||||
const validation = await validateMemberForGroup(member, group);
|
||||
if (!validation.isValid) {
|
||||
ElMessage.warning(validation.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置已使用的团队
|
||||
if (!usedTeamId.value) {
|
||||
usedTeamId.value = member.teamId;
|
||||
}
|
||||
|
||||
team.members.push({ ...member, teamId: member.teamId });
|
||||
// 实时检查报名限制
|
||||
await checkRegistrationLimit();
|
||||
};
|
||||
|
||||
// 从团队中移除成员
|
||||
const removeMemberFromTeam = (groupId, teamIndex, memberId) => {
|
||||
initGroupTeams(groupId);
|
||||
const team = groupTeamsMap.value[groupId].find(t => t.teamIndex === teamIndex);
|
||||
if (!team) return;
|
||||
|
||||
team.members = team.members.filter((m) => (m.userId ?? m.id) !== memberId);
|
||||
|
||||
// 检查是否还有队员
|
||||
let hasMembers = false;
|
||||
groupTeamsMap.value[groupId].forEach(t => {
|
||||
if (t.members.length > 0) {
|
||||
hasMembers = true;
|
||||
}
|
||||
});
|
||||
if (!hasMembers) {
|
||||
usedTeamId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 验证优惠券
|
||||
const validateCoupon = async () => {
|
||||
if (!couponCode.value?.trim()) {
|
||||
@ -1262,13 +1593,17 @@ const fetchGroupTeamLimit = async () => {
|
||||
const { succeeded, data } = await usereventregistrationGroupTeamLimitGet(currentEventId.value);
|
||||
if (succeeded && data) {
|
||||
const map = {};
|
||||
const maxTeamsMap = {};
|
||||
data.forEach(item => {
|
||||
map[item.groupId] = {
|
||||
maxTeams: item.maxTeams,
|
||||
currentTeams: item.currentTeams
|
||||
};
|
||||
// 同时设置限报团队数量
|
||||
maxTeamsMap[item.groupId] = item.maxTeams ?? 1;
|
||||
});
|
||||
groupTeamLimitMap.value = map;
|
||||
groupMaxTeamsMap.value = maxTeamsMap;
|
||||
}
|
||||
} catch {
|
||||
console.error('加载队伍限制配置失败');
|
||||
@ -1311,6 +1646,16 @@ const handleStep1Next = async () => {
|
||||
await fetchTeamList();
|
||||
// 加载队伍限制配置
|
||||
await fetchGroupTeamLimit();
|
||||
|
||||
// 初始化每个组别的团队数据
|
||||
await nextTick();
|
||||
selectedGroupItems.value.forEach(group => {
|
||||
initGroupTeams(group.id);
|
||||
// 默认激活第一个团队
|
||||
if (group.id === activeAssignGroupId.value) {
|
||||
setActiveTeam(group.id, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleStep2Next = async () => {
|
||||
@ -1326,8 +1671,30 @@ const handleStep2Next = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查每个组别是否至少分配了一个队员
|
||||
const emptyGroups = selectedGroupItems.value.filter((g) => !(groupMemberMap.value[g.id]?.length));
|
||||
// 检查每个组别是否至少分配了一个队员(考虑多团队模式)
|
||||
const emptyGroups = [];
|
||||
for (const g of selectedGroupItems.value) {
|
||||
const gId = g.id;
|
||||
let hasMembers = false;
|
||||
|
||||
// 检查是否为团队项目且启用了多团队模式
|
||||
if (isTeamProject(g) && groupTeamsMap.value[gId] && groupTeamsMap.value[gId].length > 0) {
|
||||
for (const team of groupTeamsMap.value[gId]) {
|
||||
if (team.members.length > 0) {
|
||||
hasMembers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 单团队模式
|
||||
hasMembers = !!(groupMemberMap.value[gId]?.length);
|
||||
}
|
||||
|
||||
if (!hasMembers) {
|
||||
emptyGroups.push(g);
|
||||
}
|
||||
}
|
||||
|
||||
if (emptyGroups.length > 0) {
|
||||
ElMessage.warning(`请为所有组别分配队员,"${emptyGroups[0].groupName ?? emptyGroups[0].name}"尚未分配`);
|
||||
return;
|
||||
@ -1340,14 +1707,56 @@ const handleStep2Next = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造按组别分配的数据
|
||||
const groupMembers = selectedGroupIds.value.map((gId) => ({
|
||||
groupId: gId,
|
||||
memberIds: (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id),
|
||||
}));
|
||||
|
||||
// 收集所有不重复的 memberIds
|
||||
const allMemberIds = [...new Set(groupMembers.flatMap((g) => g.memberIds))];
|
||||
// 构造按组别分配的数据(支持多团队)
|
||||
const groupMembers = [];
|
||||
const allTeams = []; // 用于收集所有团队数据
|
||||
const allMemberIdsSet = new Set();
|
||||
|
||||
for (const gId of selectedGroupIds.value) {
|
||||
// 找到当前组别信息
|
||||
const group = selectedGroupItems.value.find(g => g.id === gId);
|
||||
// 检查是否为团队项目且启用了多团队模式
|
||||
const isMultiTeamMode = group && isTeamProject(group) && groupTeamsMap.value[gId] &&
|
||||
(groupTeamsMap.value[gId].length > 1 || getGroupMaxTeams(gId) > 1);
|
||||
|
||||
if (isMultiTeamMode) {
|
||||
// 多团队模式
|
||||
const teams = groupTeamsMap.value[gId];
|
||||
const groupTeamsData = [];
|
||||
|
||||
for (const team of teams) {
|
||||
const memberIds = team.members.map(m => m.userId ?? m.id);
|
||||
memberIds.forEach(id => allMemberIdsSet.add(id));
|
||||
|
||||
groupTeamsData.push({
|
||||
teamIndex: team.teamIndex,
|
||||
memberIds: memberIds
|
||||
});
|
||||
}
|
||||
|
||||
groupMembers.push({
|
||||
groupId: gId,
|
||||
memberIds: groupTeamsData.flatMap(t => t.memberIds), // 保持向后兼容
|
||||
teams: groupTeamsData
|
||||
});
|
||||
|
||||
allTeams.push(...groupTeamsData.map(t => ({
|
||||
groupId: gId,
|
||||
...t
|
||||
})));
|
||||
} else {
|
||||
// 单团队模式(保持向后兼容)
|
||||
const memberIds = (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id);
|
||||
memberIds.forEach(id => allMemberIdsSet.add(id));
|
||||
|
||||
groupMembers.push({
|
||||
groupId: gId,
|
||||
memberIds: memberIds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allMemberIds = [...allMemberIdsSet];
|
||||
|
||||
submitDraftLoading.value = true;
|
||||
try {
|
||||
@ -1484,6 +1893,47 @@ const refreshPayment = async () => {
|
||||
await handleCreateOrderAndPay();
|
||||
};
|
||||
|
||||
// 复制银行账号
|
||||
const copyAccount = () => {
|
||||
navigator.clipboard.writeText('456498465198456261').then(() => {
|
||||
ElMessage.success('账号已复制');
|
||||
}).catch(() => {
|
||||
// 如果clipboard API不可用,使用备用方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = '456498465198456261';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
ElMessage.success('账号已复制');
|
||||
} catch (err) {
|
||||
ElMessage.error('复制失败,请手动复制');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
};
|
||||
|
||||
// 确认银行转账
|
||||
const confirmBankTransfer = () => {
|
||||
ElMessageBox.confirm(
|
||||
'确认已完成转账吗?转账完成后订单将进入待审核状态。',
|
||||
'确认转账',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
ElMessage.success('订单已提交,等待审核');
|
||||
initParam.value.refresh = !initParam.value.refresh;
|
||||
setTimeout(() => {
|
||||
isShowRegisterDialog.value = false;
|
||||
}, 1500);
|
||||
}).catch(() => {
|
||||
// 用户取消
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateOrderAndPay = async () => {
|
||||
if (!draftRegistrationId.value) return;
|
||||
createOrderLoading.value = true;
|
||||
@ -1508,7 +1958,13 @@ const handleCreateOrderAndPay = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 付费项目,显示二维码
|
||||
// 如果是银行转账,直接显示银行信息,不需要二维码和轮询
|
||||
if (paymentMethod.value === 3) {
|
||||
paymentData.value = data;
|
||||
return;
|
||||
}
|
||||
|
||||
// 微信/支付宝支付,显示二维码
|
||||
paymentData.value = data;
|
||||
await generateQRCode(data.qrcodeUrl);
|
||||
|
||||
@ -1740,6 +2196,74 @@ const handleCreateOrderAndPay = async () => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- 团队管理样式 ---- */
|
||||
.team-info-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 14px;
|
||||
background: #f0f7ff;
|
||||
border-bottom: 1px solid #e6f4ff;
|
||||
}
|
||||
|
||||
.team-count-info {
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.team-section {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.team-card {
|
||||
border: 1px solid #e6f4ff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: #56b4f9;
|
||||
}
|
||||
|
||||
&-active {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.team-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: #ecf5ff;
|
||||
border-bottom: 1px solid #d9ecff;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.team-members {
|
||||
padding: 8px 10px;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.team-placeholder {
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.team-members-panel {
|
||||
flex: 1;
|
||||
border: 1px solid #d0e4f7;
|
||||
@ -1870,6 +2394,55 @@ const handleCreateOrderAndPay = async () => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 银行转账信息显示区域样式 */
|
||||
.bank-transfer-section {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.bank-transfer-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.bank-transfer-content {
|
||||
background: #f5f7fa;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.bank-transfer-content .info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bank-transfer-content .info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bank-transfer-content .info-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bank-transfer-content .info-value {
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.payment-tip {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.free-project-hint {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user