This commit is contained in:
ltx 2026-04-14 01:40:11 +08:00
commit 03e5670af4
20 changed files with 1663 additions and 326 deletions

View File

@ -9,7 +9,7 @@ import { http } from '../../utils/request';
* @param {number} [data.pageNo] - 当前页码
* @param {number} [data.pageSize] - 每页条数
* @param {string} [data.name] - 优惠券名称 (模糊查询)
* @param {number} [data.type] - 优惠券类型 (1:满减券, 2:折扣券, 3:代金券)
* @param {number} [data.type] - 优惠券类型 (0:折扣券, 1:定额券)
* @returns {Promise<{
* pageNo: number,
* pageSize: number,
@ -41,7 +41,7 @@ export const couponPage = (data) => {
* 添加优惠券
* @param {Object} data - 优惠券信息
* @param {string} data.name - 优惠券名称
* @param {number} data.type - 优惠券类型 (1:满减券, 2:折扣券, 3:代金券)
* @param {number} data.type - 优惠券类型 (0:折扣券, 1:定额券)
* @param {number} data.value - 优惠券面值
* @param {string} data.startTime - 开始时间 (格式:2026-03-08T18:23:19.664Z)
* @param {string} data.endTime - 结束时间 (格式:2026-03-08T18:23:19.664Z)
@ -57,7 +57,7 @@ export const couponAdd = (data) => {
* @param {Object} data - 优惠券信息
* @param {number} data.id - 优惠券ID
* @param {string} data.name - 优惠券名称
* @param {number} data.type - 优惠券类型 (1:满减券, 2:折扣券, 3:代金券)
* @param {number} data.type - 优惠券类型 (0:折扣券, 1:定额券)
* @param {number} data.value - 优惠券面值
* @param {string} data.startTime - 开始时间 (格式:2026-03-08T18:23:19.665Z)
* @param {string} data.endTime - 结束时间 (格式:2026-03-08T18:23:19.665Z)

View File

@ -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,

View File

@ -8,7 +8,7 @@ import { http } from '../../utils/request';
* @param {Object} params - 查询参数
* @param {number} [params.EventId] - 赛事ID
* @param {string} [params.ProjectName] - 项目名称 (模糊查询)
* @param {number} [params.ProjectTypeId] - 项目类型ID
* @param {number} [params.ProjectType] - 项目类型
* @param {number} [params.PageNo] - 当前页码
* @param {number} [params.PageSize] - 页码容量
*/
@ -21,7 +21,7 @@ export const projectPageGet = (params) => {
* @param {Object} data - 项目信息
* @param {number} data.eventId - 赛事ID
* @param {string} data.name - 项目名称
* @param {number} data.projectTypeId - 项目类型ID
* @param {number} data.projectType - 项目类型
* @param {number} data.completionCount - 完成人数
* @param {number} data.fee - 费用
* @param {number} data.durationMinutes - 时长(分钟)
@ -44,7 +44,7 @@ export const projectAddPost = (data) => {
* @param {number} data.id - 项目ID
* @param {number} data.eventId - 赛事ID
* @param {string} data.name - 项目名称
* @param {number} data.projectTypeId - 项目类型ID
* @param {number} data.projectType - 项目类型
* @param {number} data.completionCount - 完成次数/达标次数
* @param {number} data.fee - 费用
* @param {number} data.durationMinutes - 时长(分钟)

View File

@ -93,11 +93,40 @@ export const useTable = (
for (let key in state.searchParam) {
// * 某些情况下参数为 false/0 也应该携带参数
if (state.searchParam[key] || state.searchParam[key] === false || state.searchParam[key] === 0) {
nowSearchParam[key] = state.searchParam[key];
// 处理日期范围参数:如果是数组,拆分为 StartTime 和 EndTime
if (Array.isArray(state.searchParam[key]) && state.searchParam[key].length === 2) {
let startTime = state.searchParam[key][0];
let endTime = state.searchParam[key][1];
// 如果是 Date 对象,格式化为本地日期字符串,避免时区转换问题
if (startTime instanceof Date) {
startTime = formatLocalDate(startTime);
}
if (endTime instanceof Date) {
endTime = formatLocalDate(endTime);
}
nowSearchParam['StartTime'] = startTime;
nowSearchParam['EndTime'] = endTime;
} else {
nowSearchParam[key] = state.searchParam[key];
}
}
}
Object.assign(state.totalParam, nowSearchParam, isPageable ? pageParam.value : {});
};
/**
* @description 格式化日期为本地日期字符串,避免时区转换
* @param date Date 对象
* @returns YYYY-MM-DD 格式的字符串
* */
const formatLocalDate = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
/**
* @description 更新分页信息

View File

@ -79,10 +79,11 @@ service.interceptors.response.use(
ElMessage.error('无权访问');
return response;
}
// 处理规范化结果错误
// 处理规范化结果错误
if (res.statusCode !== 200) {
var message = JSON.stringify(res.errors);
ElMessage.error(message);
return Promise.reject(new Error(res.errors || 'Request failed'));
}
return response;
},

View File

@ -13,10 +13,10 @@
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #type="scope">
{{ scope.row.type == 1 ? '折扣券' : '代金券' }}
{{ scope.row.type == 0 ? '折扣券' : '定额券' }}
</template>
<template #value="scope">
{{ scope.row.type == 1 ? scope.row.value + '%' : scope.row.value + '元' }}
{{ scope.row.type == 0 ? scope.row.value + '%' : scope.row.value + '元' }}
</template>
<template #startTime="scope">
{{ formatDate(scope.row.startTime) }} - {{ formatDate(scope.row.endTime) }}
@ -49,20 +49,20 @@
<!-- 优惠券类型 -->
<el-form-item label="优惠券类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择优惠券类型">
<el-option label="折扣" :value="1"></el-option>
<el-option label="定价" :value="2"></el-option>
<el-option label="折扣" :value="0"></el-option>
<el-option label="定额" :value="1"></el-option>
</el-select>
</el-form-item>
<!-- 填写折扣 -->
<el-form-item label="填写折扣" prop="value" v-if="formData.type == '1'">
<el-form-item label="填写折扣" prop="value" v-if="formData.type === 0">
<el-input v-model="formData.value" placeholder="请输入折扣百分比">
<template #append>%</template>
</el-input>
</el-form-item>
<!-- 填写优惠金额 -->
<el-form-item label="填写优惠金额" prop="value" v-if="formData.type == '2'">
<el-form-item label="填写优惠金额" prop="value" v-if="formData.type === 1">
<el-input v-model="formData.value" placeholder="请输入优惠金额" class="fee-input">
<template #append>元</template>
</el-input>
@ -70,9 +70,9 @@
<!-- 选择有效期 -->
<el-form-item label="选择有效期" prop="startTime">
<el-date-picker type="daterange" v-model="formData.time" start-placeholder="开始日期"
end-placeholder="结束日期" style="width: 100%;" value-format="YYYY-MM-DD" @change="(val) => {
console.log('选择的日期范围:', val);
<el-date-picker type="datetimerange" v-model="formData.time" start-placeholder="开始日期时间"
end-placeholder="结束日期时间" style="width: 100%;" value-format="YYYY-MM-DD HH:mm" @change="(val) => {
console.log('选择的日期时间范围:', val);
formData.startTime = val[0];
formData.endTime = val[1];
}"></el-date-picker>
@ -120,14 +120,39 @@ import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { couponPage, couponAdd, couponEdit, couponDelete, couponEventlistGet, couponGeneratecodePost } from '/@/api/match/coupon';
// 格式化日期,只显示年月日
// 格式化日期,显示完整时间(YYYY-MM-DD HH:MM)
const formatDate = (dateStr) => {
if (!dateStr) return '';
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
// 如果已经是日期时间格式,直接返回(或调整格式)
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (e) {
return dateStr;
}
};
// 格式化日期为选择器用的格式
const formatDateTimePicker = (dateStr) => {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (e) {
return dateStr;
}
// 截取前10个字符(YYYY-MM-DD)
return dateStr.slice(0, 10);
};
// 定义表单引用类型
@ -188,7 +213,7 @@ const columns = ref([
el: 'input',
},
},
{ prop: 'type', label: '优惠券类型', search: { el: 'select', }, enum: [{ label: '全部', value: '' }, { label: '折扣', value: 1 }, { label: '定价', value: 2 },] },
{ prop: 'type', label: '优惠券类型', search: { el: 'select', }, enum: [{ label: '全部', value: '' }, { label: '折扣', value: 0 }, { label: '定额', value: 1 },] },
{ prop: 'value', label: '详情', },
{ prop: 'startTime', label: '有效期' },
{ prop: 'eventNames', label: '可用赛事' },
@ -209,9 +234,9 @@ const onOpenMenu = async (row) => {
if (row) {
formData.value = {
...row,
startTime: formatDate(row.startTime),
endTime: formatDate(row.endTime),
time: [formatDate(row.startTime), formatDate(row.endTime)],
startTime: formatDateTimePicker(row.startTime),
endTime: formatDateTimePicker(row.endTime),
time: [formatDateTimePicker(row.startTime), formatDateTimePicker(row.endTime)],
};
} else {
formData.value = {

View File

@ -28,7 +28,7 @@
start-placeholder="开始日期" end-placeholder="结束日期"
:model-value="[group.birthDateStart, group.birthDateEnd]"
@update:modelValue="(v) => { group.birthDateStart = v?.[0]; group.birthDateEnd = v?.[1]; }" :disabled="readonly" />
<span v-else class="group-row__range">{{ group.birthDateStart }} ~ {{ group.birthDateEnd }}</span>
<span v-else class="group-row__range">{{ formatGroupDate(group) }}</span>
<el-select v-if="!readonly" v-model="group.gender" class="group-row__gender" placeholder="性别" :disabled="readonly">
<el-option label="全部" :value="3" />
@ -128,7 +128,7 @@
<!-- <template #index="scope">
{{ scope.$index + 1 }}
</template> -->
<template #projectTypeId="scope">
<template #projectType="scope">
{{ scope.row.projectTypeName }}
</template>
<template #completionCount="scope">
@ -150,6 +150,7 @@ import { ref, watch, nextTick, computed } from 'vue';
import ProTable from '/@/components/ProTable/index.vue';
import { projectPageGet, projectDetailGet } from '/@/api/match/project';
import { ElMessage } from 'element-plus';
import { formatDate } from '/@/utils/formatTime';
const props = defineProps({
modelValue: { type: Object, required: true },
@ -168,6 +169,20 @@ const localFormData = ref({
registrationLimit: { individualProjectLimit: 1, teamProjectLimit: 1 },
});
const genderList = ref(['全部', '男', '女'])
// 格式化组别的年龄段(出生日期范围)
const formatGroupDate = (item) => {
const start = item?.birthDateStart;
const end = item?.birthDateEnd;
if (!start && !end) return '';
const fmt = (v) => {
if (!v) return '';
try { return formatDate(new Date(v), 'YYYY-mm-dd'); } catch { return String(v); }
};
if (start && end) return `${fmt(start)} ~ ${fmt(end)}`;
return fmt(start || end);
};
const addProject = () => {
isShowDialog.value = true;
// 打开弹窗时,用当前已选项目初始化 timeProjects,以便确定后保留勾选
@ -248,17 +263,17 @@ const columns = ref([
},
},
{
prop: 'projectTypeId',
prop: 'projectType',
label: '项目类型',
search: {
el: 'select',
},
enum: [
{ label: '全部', value: 0 },
{ label: '团队速度', value: 1001 },
{ label: '团队花样', value: 1002 },
{ label: '全部', value: '' },
{ label: '个人速度', value: 1003 },
{ label: '个人花样', value: 1004 },
{ label: '团队速度', value: 1005 },
{ label: '团队花样', value: 1006 },
],
},
{

View File

@ -201,9 +201,22 @@ const columns = reactive([
{ prop: 'action', label: '操作', align: 'center', width: 200, fixed: 'right', isShow: auths(['event:edit', 'event:delete']) },
]);
const exportableColumns = computed(() =>
columns.filter((item) => item?.prop && item?.label && !['action', 'index'].includes(item.prop))
);
const exportableColumns = computed(() => [
{ prop: 'eventName', label: '赛事名称' },
{ prop: 'eventType', label: '赛事类型' },
{ prop: 'chargeMode', label: '收费模式' },
{ prop: 'merchantName', label: '收款商户' },
{ prop: 'registrationType', label: '报名方式' },
{ prop: 'eventStatus', label: '赛事状态' },
{ prop: 'registrationTime', label: '报名起止时间' },
{ prop: 'materialUploadEndTime', label: '上传素材截至时间' },
{ prop: 'matchTime', label: '比赛起止时间' },
{ prop: 'detailAddress', label: '赛事地址' },
{ prop: 'hostUnit', label: '主办单位' },
{ prop: 'organizerUnit', label: '承办单位' },
{ prop: 'coOrganizerUnit', label: '协办单位' },
{ prop: 'sponsorUnit', label: '赞助单位' }
]);
// 定义变量内容
const menuDialogRef = ref();

View File

@ -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'
}))
@ -298,9 +405,16 @@ const columns = reactive([
el: 'select',
},
enum: [
{ label: '个人报名', value: 0 },
{ label: '团队报名', value: 1 },
{ label: '个人报名', value: 1 },
{ label: '团队报名', value: 2 },
],
customRender: ({ text }) => {
const modeMap = {
1: '个人报名',
2: '团队报名'
};
return modeMap[text] || text;
}
},
{
width: '120px',
@ -319,6 +433,9 @@ const columns = reactive([
{ label: '是', value: true },
{ label: '否', value: false },
],
customRender: ({ text }) => {
return text ? '已上传' : '未上传';
}
},
{
width: '120px',
@ -328,10 +445,18 @@ const columns = reactive([
el: 'select',
},
enum: [
{ label: '线上', value: 0 },
{ label: '线下', value: 1 },
{ label: '线上', value: 2 },
{ label: '全部', value: '' },
],
customRender: ({ record }) => {
const methodMap = {
0: '未选择',
1: '线下',
2: '线上'
};
return methodMap[record.paymentMethod] || '未知';
}
},
{
width: '120px', prop: 'paymentStatus', label: '支付状态',
@ -344,10 +469,32 @@ const columns = reactive([
{ label: '待审核', value: 2 },
{ label: '全部', value: '' },
],
customRender: ({ text }) => {
const statusMap = {
0: '未支付',
1: '已支付',
2: '待审核',
3: '支付失败',
4: '已退款'
};
return statusMap[text] || '未知';
}
},
{ width: '150px', prop: 'teamLeaderInfo', label: '领队信息',
customRender: ({ text }) => {
return text || '-';
}
},
{ width: '150px', prop: 'coachInfo', label: '教练信息',
customRender: ({ text }) => {
return text || '-';
}
},
{ width: '120px', prop: 'orderNo', label: '订单信息',
customRender: ({ text }) => {
return text || '-';
}
},
{ width: '120px', prop: 'teamLeaderInfo', label: '领队信息', },
{ width: '120px', prop: 'coachInfo', label: '教练信息', },
{ width: '120px', prop: 'orderNo', label: '订单信息', },
{ width: '120px', prop: 'paymentProofUrl', label: '付款凭证', },
{
width: '120px',
@ -378,6 +525,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 +770,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 +797,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>

View File

@ -22,6 +22,7 @@
<el-button size="small" v-auth="'integral:config:delete'" text type="danger" icon="ele-Delete"> 删除 </el-button>
</template>
</el-popconfirm>
<el-button size="small" v-auth="'integral:config:detail'" text type="primary" icon="ele-View" @click="onOpenDialog(scope.row, true)"> 查看详情 </el-button>
</template>
</ProTable>
@ -29,37 +30,34 @@
<el-dialog :title="dialogTitle" v-model="isShowDialog" width="900px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" v-if="isShowDialog">
<el-form-item label="规则名称" prop="ruleName">
<el-input v-model="formData.ruleName" placeholder="请输入规则名称" />
<el-input v-model="formData.ruleName" placeholder="请输入规则名称" :disabled="isDetailMode" />
</el-form-item>
<el-form-item label="参与积分" prop="participationPoints">
<el-input-number v-model="formData.participationPoints" :min="0" :step="1" style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可不填" />
<el-input-number v-model="formData.participationPoints" :min="0" :step="1" style="width: 100%" :disabled="isDetailMode" />
</el-form-item>
<el-form-item label="名次积分">
<div style="width: 100%">
<div style="display: flex; justify-content: flex-end; margin-bottom: 10px">
<div style="display: flex; justify-content: flex-end; margin-bottom: 10px" v-if="!isDetailMode">
<el-button type="primary" plain icon="ele-Plus" @click="onAddDetailRow">新增区间</el-button>
</div>
<el-table :data="formData.details" border style="width: 100%" >
<el-table-column label="起始排名" width="180">
<template #default="scope">
<el-input-number v-model="scope.row.startRank" :min="1" :step="1" />
<el-input-number v-model="scope.row.startRank" :min="1" :step="1" :disabled="isDetailMode" />
</template>
</el-table-column>
<el-table-column label="结束排名" width="180">
<template #default="scope">
<el-input-number v-model="scope.row.endRank" :min="1" :step="1" />
<el-input-number v-model="scope.row.endRank" :min="1" :step="1" :disabled="isDetailMode" />
</template>
</el-table-column>
<el-table-column label="对应积分" width="180">
<template #default="scope">
<el-input-number v-model="scope.row.points" :min="0" :step="1" />
<el-input-number v-model="scope.row.points" :min="0" :step="1" :disabled="isDetailMode" />
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center">
<el-table-column label="操作" width="140" align="center" v-if="!isDetailMode">
<template #default="scope">
<el-button text type="danger" icon="ele-Delete" @click="onRemoveDetailRow(scope.$index)">
删除
@ -69,11 +67,15 @@
</el-table>
</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可不填" :disabled="isDetailMode" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
<el-button type="primary" @click="onSubmit" v-if="!isDetailMode">确定</el-button>
</span>
</template>
</el-dialog>
@ -105,6 +107,7 @@ const buildEmptyForm = () => ({
});
const formData = ref(buildEmptyForm());
const isDetailMode = ref(false);
const formRules = ref({
ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
@ -125,14 +128,15 @@ const columns = ref([
// search: { el: 'select' },
// },
{ prop: 'createdTime', label: '创建时间', align: 'center' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 220 },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 320 },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenDialog = async (row?: any) => {
dialogTitle.value = row ? '编辑' : '新增';
const onOpenDialog = async (row?: any, isDetail = false) => {
isDetailMode.value = isDetail;
dialogTitle.value = isDetail ? '查看详情' : row ? '编辑' : '新增';
if (row?.id) {
const res: any = await pointsruleDetailGet({ id: row.id });
formData.value = {
@ -150,7 +154,7 @@ const onOpenDialog = async (row?: any) => {
} else {
formData.value = buildEmptyForm();
}
if (!formData.value.details?.length) onAddDetailRow();
if (!formData.value.details?.length && !isDetail) onAddDetailRow();
isShowDialog.value = true;
};
@ -162,6 +166,30 @@ const onRemoveDetailRow = (index: number) => {
formData.value.details.splice(index, 1);
};
// 验证名次范围是否重叠
const validateRankOverlap = () => {
const details = formData.value.details;
if (details.length <= 1) return true;
for (let i = 0; i < details.length; i++) {
const item1 = details[i];
if (!item1.startRank || !item1.endRank) continue;
for (let j = i + 1; j < details.length; j++) {
const item2 = details[j];
if (!item2.startRank || !item2.endRank) continue;
// 检查是否重叠
const isOverlap = !(item1.endRank < item2.startRank || item2.endRank < item1.startRank);
if (isOverlap) {
ElMessage.error(`名次范围 ${item1.startRank}-${item1.endRank} 与 ${item2.startRank}-${item2.endRank} 存在重叠,请调整!`);
return false;
}
}
}
return true;
};
const onDelete = async (id: number) => {
await pointsruleDeleteDelete({ id });
ElMessage.success('删除成功');
@ -185,6 +213,10 @@ const onSubmit = () => {
ElMessage.warning('请至少配置一个排名区间');
return;
}
// 验证名次范围是否重叠
if (!validateRankOverlap()) {
return;
}
const payload = {
id: formData.value.id,
ruleName: formData.value.ruleName,
@ -211,6 +243,7 @@ const onSubmit = () => {
// 取消
const onCancel = () => {
isShowDialog.value = false;
isDetailMode.value = false;
};
</script>
<style scoped lang="scss">

View File

@ -9,7 +9,7 @@
{{ scope.$index + 1 }}
</template>
<template #projectTypeId="scope">
<template #projectType="scope">
{{ scope.row.projectTypeName }}
</template>
@ -30,9 +30,8 @@
</el-button>
</template>
</el-popconfirm>
<el-button v-auth="'project:detail'" size="small" text type="primary" @click="onOpenMenu(scope.row)">
查看详情 </el-button>
<el-button v-auth="'project:detail'" size="small" text type="primary"
@click="onViewDetail(scope.row)"> 查看详情 </el-button>
</template>
</ProTable>
@ -46,8 +45,8 @@
</el-form-item>
<!-- 项目类型 -->
<el-form-item label="项目类型:" prop="projectTypeId">
<el-select v-model="formData.projectTypeId" placeholder="请选择项目类型" style="width: 100%">
<el-form-item label="项目类型:" prop="projectType">
<el-select v-model="formData.projectType" placeholder="请选择项目类型" style="width: 100%">
<el-option label="个人速度" :value="1003"></el-option>
<el-option label="个人花样" :value="1004"></el-option>
<el-option label="团队速度" :value="1005"></el-option>
@ -74,6 +73,8 @@
callback(new Error('请选择出生日期开始'));
} else if (!value.birthDateEnd) {
callback(new Error('请选择出生日期结束'));
} else if (new Date(value.birthDateStart) > new Date(value.birthDateEnd)) {
callback(new Error('出生日期结束必须大于或等于出生日期开始'));
} else if (!value.gender) {
callback(new Error('请选择性别'));
} else {
@ -85,9 +86,9 @@
<el-input v-model="group.groupName" placeholder="输入组别名"
style="width: 100px; margin-right: 18px"></el-input>
<el-date-picker v-model="group.birthDateStart" type="date" placeholder="出生日期开始"
style="margin-right: 18px"></el-date-picker>
style="margin-right: 18px" value-format="YYYY-MM-DD"></el-date-picker>
<el-date-picker v-model="group.birthDateEnd" type="date" placeholder="出生日期结束"
style="margin-right: 18px"></el-date-picker>
style="margin-right: 18px" value-format="YYYY-MM-DD"></el-date-picker>
<el-select v-model="group.gender" placeholder="性别" style="width: 100px">
<el-option label="男" value="0"></el-option>
<el-option label="女" value="1"></el-option>
@ -125,6 +126,76 @@
</span>
</template>
</el-dialog>
<!-- 详情弹窗 - 复用编辑页面布局 -->
<el-dialog title="项目详情" v-model="isShowDetailDialog" width="1100px">
<el-form :model="detailData" label-width="150px"
style="width: 90%; margin-bottom: 30px" v-if="isShowDetailDialog">
<!-- 项目名称 -->
<el-form-item label="项目名称:">
<el-input v-model="detailData.name" disabled></el-input>
</el-form-item>
<!-- 项目类型 -->
<el-form-item label="项目类型:">
<el-select v-model="detailData.projectType" disabled style="width: 100%">
<el-option label="个人速度" :value="1003"></el-option>
<el-option label="个人花样" :value="1004"></el-option>
<el-option label="团队速度" :value="1005"></el-option>
<el-option label="团队花样" :value="1006"></el-option>
</el-select>
</el-form-item>
<!-- 完成人数 -->
<el-form-item label="完成人数:">
<el-input-number v-model="detailData.completionCount" disabled :min="0" :step="1" style="width: 100%"></el-input-number>
</el-form-item>
<!-- 组别 -->
<div class="detail-groups-section">
<el-form-item :label="'组别' + (index + 1) + ':'"
v-for="(group, index) in (detailData.groups || [])" :key="index">
<el-input v-model="group.groupName" disabled
style="width: 100px; margin-right: 18px"></el-input>
<el-date-picker v-model="group.birthDateStart" type="date" disabled
style="margin-right: 18px" value-format="YYYY-MM-DD"></el-date-picker>
<el-date-picker v-model="group.birthDateEnd" type="date" disabled
style="margin-right: 18px" value-format="YYYY-MM-DD"></el-date-picker>
<el-select v-model="group.gender" disabled style="width: 100px">
<el-option label="男" value="0"></el-option>
<el-option label="女" value="1"></el-option>
<el-option label="不限" value="3"></el-option>
</el-select>
</el-form-item>
</div>
<!-- 视觉分隔线 -->
<div class="detail-section-divider"></div>
<!-- 费用 -->
<el-form-item label="费用:">
<el-input v-model="detailData.fee" disabled class="fee-input">
<template slot="append">元</template>
</el-input>
</el-form-item>
<!-- 时长 -->
<el-form-item label="时长:">
<div class="duration-row">
<el-input v-model="detailData.durationMinutes" disabled type="number"></el-input>
<span class="duration-label">分</span>
<span class="duration-separator">--</span>
<el-input v-model="detailData.durationSeconds" disabled type="number"></el-input>
<span class="duration-label">秒</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="isShowDetailDialog = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@ -142,6 +213,20 @@ const initParam = ref({
refresh: true,
});
// 详情弹窗相关
const isShowDetailDialog = ref(false);
const detailData = ref(null);
// 日期格式化函数
const formatDate = (date) => {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const addGroup = () => {
formData.value.groups.push({
groupName: '',
@ -157,7 +242,7 @@ const removeGroup = (index) => {
const formData = ref({
name: '',
projectTypeId: '',
projectType: '',
completionCount: '',
fee: '',
durationMinutes: '',
@ -209,7 +294,7 @@ const validateGroups = (rule, value, callback) => {
const formRules = ref({
name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
projectTypeId: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
projectType: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
completionCount: [{ required: true, message: '请输入完成人数', trigger: 'blur' }],
fee: [{ required: true, message: '请输入费用', trigger: 'blur' }],
duration: [
@ -240,11 +325,11 @@ const columns = ref([
},
},
{
prop: 'projectTypeId',
prop: 'projectType',
label: '项目类型',
search: {
el: 'select',
key: 'ProjectTypeId',
key: 'ProjectType',
},
enum: [
{ label: '全部', value: '' },
@ -262,7 +347,7 @@ const columns = ref([
{ prop: 'time', label: '时长' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', isShow: auths(['project:delete', 'project:edit', 'project:add']) },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right' },
]);
const isShowDialog = ref(false);
@ -272,7 +357,7 @@ const onOpenMenu = async (row) => {
dialogTitle.value = row ? '编辑' : '新增';
formData.value = {
name: '',
projectTypeId: '',
projectType: '',
completionCount: '',
fee: '',
durationMinutes: '',
@ -300,13 +385,13 @@ const onOpenMenu = async (row) => {
},
];
} else {
// 过滤无效的 group 并补充缺失的属性
// 过滤无效的 group 并补充缺失的属性,同时格式化日期
formData.value.groups = formData.value.groups
.filter(group => group)
.map(group => ({
groupName: group.groupName || '',
birthDateStart: group.birthDateStart || '',
birthDateEnd: group.birthDateEnd || '',
birthDateStart: formatDate(group.birthDateStart),
birthDateEnd: formatDate(group.birthDateEnd),
gender: group.gender !== undefined && group.gender !== null ? group.gender.toString() : '',
}));
// 如果过滤后没有组,添加一个空组
@ -325,6 +410,31 @@ const onOpenMenu = async (row) => {
isShowDialog.value = true;
};
// 查看详情
const onViewDetail = async (row) => {
const { succeeded, data } = await projectDetailGet({ id: row.id });
if (succeeded) {
// 确保 groups 数组存在且有效,并格式化日期
let groupsData = [];
if (data.groups && Array.isArray(data.groups)) {
groupsData = data.groups
.filter(group => group)
.map(group => ({
groupName: group.groupName || '',
birthDateStart: formatDate(group.birthDateStart),
birthDateEnd: formatDate(group.birthDateEnd),
gender: group.gender !== undefined && group.gender !== null ? group.gender.toString() : '',
}));
}
detailData.value = {
...data,
groups: groupsData
};
isShowDetailDialog.value = true;
}
};
//删除菜单
const onDeleteMenu = async (id) => {
const { succeeded, errors } = await projectDeleteDelete({ id });
@ -429,4 +539,9 @@ const onCancel = () => {
color: #909399;
font-size: 14px;
}
/* 详情页面组别区域样式 */
.detail-groups-section {
margin-bottom: 20px;
}
</style>

View File

@ -169,7 +169,12 @@ const formRules = ref({
{ required: true, message: '请上传营业执照', trigger: 'blur' }
],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' }
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号格式',
trigger: 'blur'
}
],
});

View File

@ -1,12 +1,10 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="appuserPageGet" @selection-change="handleSelectionChange" :columns="columns" :pagination="true"
:tool-button="false" :initParams="initParam" row-key="id">
<ProTable ref="tableRef" :request-api="appuserPageGet" @selection-change="handleSelectionChange" :columns="columns" :pagination="true" :tool-button="false" :initParams="initParam" row-key="id">
<template #tools>
<el-popconfirm title="确认批量审核吗?" @confirm="onBatchAudit">
<template #reference>
<el-button v-auth="'user:general:audit'" size="small" type="primary"> 批量审核
</el-button>
<el-button v-auth="'user:general:audit'" size="small" type="primary"> 批量审核 </el-button>
</template>
</el-popconfirm>
</template>
@ -14,44 +12,28 @@
{{ scope.$index + 1 }}
</template>
<template #auditStatus="scope">
<el-tag :type="statusList[scope.row.auditStatus]">
{{ statusNameList[scope.row.auditStatus] }}
<el-tag :type="getAuditStatusType(scope.row.auditStatus)">
{{ getAuditStatusText(scope.row.auditStatus) }}
</el-tag>
</template>
<template #ss="scope">
</template>
<template #idCardImageUrl="scope">
<PreviewImage
v-if="scope.row.idCardImageUrl"
:src="scope.row.idCardImageUrl"
width="100px"
height="100px"
fit="cover"
radius="4px"
:show-zoom-icon="true"
/>
<PreviewImage v-if="scope.row.idCardImageUrl" :src="scope.row.idCardImageUrl" width="100px" height="100px" fit="cover" radius="4px" :show-zoom-icon="true" />
<span v-else>-</span>
</template>
<template #action="scope">
<el-button v-auth="'user:general:record'" size="small" text type="primary"
@click="onOpenMenu(scope.row)"> 参赛记录 </el-button>
<el-button v-auth="'user:general:record'" size="small" text type="primary" @click="onOpenMenu(scope.row)"> 参赛记录 </el-button>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="参赛记录" v-model="isShowDialog" width="80%">
<ProTable ref="table" :request-api="eventrecordPageGet" :columns="columnsUser" :pagination="true"
:tool-button="false" :initParams="initParamUser">
<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" :initParam="initParamUser">
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #startTime="scope">
{{ scope.row.startTime }} -- {{ scope.row.endTime }}
</template>
<template #startTime="scope"> {{ scope.row.startTime }} -- {{ scope.row.endTime }} </template>
<template #action="scope">
<el-button v-auth="'user:general:record'" size="small" text type="primary"
@click="lookBook(scope.row)"> 查看证书 </el-button>
<el-button v-auth="'user:general:record'" size="small" text type="primary" @click="lookBook(scope.row)"> 查看证书 </el-button>
</template>
</ProTable>
<template #footer>
@ -62,11 +44,9 @@
</template>
</el-dialog>
<!-- 弹窗 -->
<!-- 弹窗 -->
<el-dialog title="查看证书" v-model="isShowBook" width="80%">
<div>
证书
</div>
<div>证书</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancelB">取消</el-button>
@ -83,8 +63,8 @@ import { ElMessage, ElForm } from 'element-plus';
import SysMenuApi from '/@/api/SysMenuApi';
import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { appuserPageGet , appuserAuditPost} from '/@/api/match/appuser';
import { eventrecordPageGet , eventrecordCertificatesGet} from '/@/api/match/eventRecord';
import { appuserPageGet, appuserAuditPost } from '/@/api/match/appuser';
import { eventrecordPageGet, eventrecordCertificatesGet } from '/@/api/match/eventRecord';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const initParam = ref({
@ -92,10 +72,44 @@ const initParam = ref({
});
const selectedListIds = ref([]);
const initParamUser = ref({
appUserId: '',
appUserId: null,
});
const statusList = ['info', 'warning', 'success', 'danger'];
const getAuditStatusText = (status) => {
switch (status) {
case 0:
return '待提交';
case 1:
return '待审核';
case 2:
return '审核通过';
case 3:
return '审核拒绝';
case 4:
return '已取消';
default:
return status;
}
};
const getAuditStatusType = (status) => {
switch (status) {
case 0:
return 'info';
case 1:
return 'warning';
case 2:
return 'success';
case 3:
return 'danger';
case 4:
return 'info';
default:
return 'info';
}
};
const columns = ref([
{ prop: 'ss', type: 'selection' },
{ prop: 'index', label: '序号', align: 'center' },
@ -113,7 +127,7 @@ const columns = ref([
],
},
{ prop: 'mobile', label: '联系电话', search: { el: 'input' } },
{ prop: 'idCardImageUrl', label: '身份证' },
{ prop: 'idCardImageUrl', label: '身份证', width: '130px' },
{
prop: 'auditStatus',
label: '审核状态',
@ -130,7 +144,7 @@ const columns = ref([
},
{
prop: 'createdTime',
label: '创建时间',
label: '生成时间',
search: {
el: 'date-picker',
props: { type: 'daterange' },
@ -145,7 +159,7 @@ const handleSelectionChange = (val) => {
selectedListIds.value = val.map((item) => item.id);
};
const statusNameList = ['未提交','未审核','已通过','未通过'];
const statusNameList = ['未提交', '未审核', '已通过', '未通过'];
const columnsUser = ref([
{ prop: 'index', label: '序号', align: 'center' },
@ -179,20 +193,37 @@ const columnsUser = ref([
isShow: false,
},
{ prop: 'action', label: '操作', align: 'center', fixed: 'right'},
{ prop: 'action', label: '操作', align: 'center', fixed: 'right' },
]);
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 () => {
console.log(selectedListIds.value);
const { succeeded, data } = await appuserAuditPost({ ids: selectedListIds.value , status: 2 });
const { succeeded, data } = await appuserAuditPost({ ids: selectedListIds.value, status: 2 });
if (succeeded) {
ElMessage.success('操作成功');
// initParam.value.refresh = !initParam.value.refresh;
@ -218,6 +249,14 @@ const lookBook = async (row) => {
}
};
// 对话框关闭时清理
const onDialogClose = () => {
// 清理参数,避免下次查询时使用旧参数
initParamUser.value = {
appUserId: null,
refresh: Date.now(),
};
};
// 取消
const onCancel = () => {

View File

@ -1,50 +1,44 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="merchantcertificationPageGet" @selection-change="handleSelectionChange" :columns="columns" :pagination="true"
:tool-button="false" :initParams="initParam" row-key="id">
<ProTable
ref="tableRef"
:request-api="merchantcertificationPageGet"
@selection-change="handleSelectionChange"
:columns="columns"
:pagination="true"
:tool-button="false"
:initParams="initParam"
row-key="id"
>
<template #tools>
<el-button v-auth="'user:general:audit'" size="small" type="primary" @click="showAuditDialog"> 批量审核
</el-button>
</template>
<el-button v-auth="'user:general:audit'" size="small" type="primary" @click="showAuditDialog"> 批量审核 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #auditStatus="scope">
<el-tag :type="getAuditStatusType(scope.row.auditStatus)">
{{ getAuditStatusText(scope.row.auditStatus) }}
</el-tag>
</template>
<el-tag :type="getAuditStatusType(scope.row.auditStatus)">
{{ getAuditStatusText(scope.row.auditStatus) }}
</el-tag>
</template>
<template #businessLicenseUrl="scope">
<PreviewImage
v-if="scope.row.businessLicenseUrl"
:src="scope.row.businessLicenseUrl"
width="100px"
height="100px"
fit="cover"
radius="4px"
:show-zoom-icon="true"
/>
<PreviewImage v-if="scope.row.businessLicenseUrl" :src="scope.row.businessLicenseUrl" width="100px" height="100px" fit="cover" radius="4px" :show-zoom-icon="true" />
<span v-else>-</span>
</template>
<template #action="scope">
<el-button v-auth="'user:general:record'" size="small" text type="primary"
@click="onOpenMenu(scope.row)"> 参赛记录 </el-button>
<el-button v-auth="'user:general:record'" size="small" text type="primary" @click="onOpenMenu(scope.row)"> 参赛记录 </el-button>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="参赛记录" v-model="isShowDialog" width="80%">
<ProTable ref="table" :request-api="eventrecordPageGet" :columns="columnsUser" :pagination="true"
:tool-button="false" :initParams="initParamUser">
<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" :initParam="initParamUser">
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #startTime="scope">
{{ scope.row.startTime }} -- {{ scope.row.endTime }}
</template>
<template #startTime="scope"> {{ scope.row.startTime }} -- {{ scope.row.endTime }} </template>
<template #action="scope">
<el-button v-auth="'user:general:record'" size="small" text type="primary"
@click="lookBook(scope.row)"> 查看证书 </el-button>
<el-button v-auth="'user:general:record'" size="small" text type="primary" @click="lookBook(scope.row)"> 查看证书 </el-button>
</template>
</ProTable>
<template #footer>
@ -55,11 +49,9 @@
</template>
</el-dialog>
<!-- 弹窗 -->
<!-- 弹窗 -->
<el-dialog title="查看证书" v-model="isShowBook" width="80%">
<div>
证书
</div>
<div>证书</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancelB">取消</el-button>
@ -96,8 +88,8 @@ import { defineAsyncComponent, ref, reactive } from 'vue';
import { ElMessage, ElForm } from 'element-plus';
import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { eventrecordPageGet , eventrecordCertificatesGet} from '/@/api/match/eventRecord';
import { merchantcertificationPageGet, merchantcertificationAuditPost} from '/@/api/match/merchantcertification';
import { eventrecordPageGet, eventrecordCertificatesGet } from '/@/api/match/eventRecord';
import { merchantcertificationPageGet, merchantcertificationAuditPost } from '/@/api/match/merchantcertification';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const initParam = ref({
@ -105,7 +97,7 @@ const initParam = ref({
});
const selectedListIds = ref([]);
const initParamUser = ref({
appUserId: '',
sysUserId: null,
});
const statusList = ['info', 'warning', 'success', 'danger'];
@ -147,11 +139,10 @@ const columns = ref([
{ prop: 'ss', type: 'selection' },
{ prop: 'index', label: '序号', align: 'center' },
{ prop: 'merchantName', label: '单位名称', search: { el: 'input' } },
{ prop: 'creditCode', label: '统一社会信用代码', search: { el: 'input' } },
{ prop: 'address', label: '地址' },
{ prop: 'contactPhone', label: '联系电话', search: { el: 'input' } },
{prop: 'businessLicenseUrl', label: '营业执照', width: '130px',},
{ prop: 'businessLicenseUrl', label: '营业执照', width: '130px' },
{
prop: 'auditStatus',
label: '审核状态',
@ -159,17 +150,17 @@ const columns = ref([
el: 'select',
},
enum: [
{ label: '全部', value: '' },
{ label: '审核拒绝', value: 3 },
{ label: '审核通过', value: 2 },
{ label: '待审核', value: 1 },
{ label: '待提交', value: 0 },
{ label: '已取消', value: 4 },
],
{ label: '全部', value: '' },
{ label: '审核拒绝', value: 3 },
{ label: '审核通过', value: 2 },
{ label: '待审核', value: 1 },
{ label: '待提交', value: 0 },
{ label: '已取消', value: 4 },
],
},
{
prop: 'createdTime',
label: '创建时间',
label: '生成时间',
search: {
el: 'date-picker',
props: { type: 'daterange' },
@ -216,15 +207,32 @@ const columnsUser = ref([
isShow: false,
},
{ prop: 'action', label: '操作', align: 'center', fixed: 'right'},
{ prop: 'action', label: '操作', align: 'center', fixed: 'right' },
]);
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 = () => {
@ -246,7 +254,7 @@ const lookBook = async (row) => {
const isShowAuditDialog = ref(false);
const auditForm = reactive({
status: 2, // 默认审核通过
remark: ''
remark: '',
});
const showAuditDialog = () => {
@ -261,10 +269,10 @@ const showAuditDialog = () => {
const onSubmitAudit = async () => {
console.log(selectedListIds.value);
const { succeeded, data } = await merchantcertificationAuditPost({
ids: selectedListIds.value,
const { succeeded, data } = await merchantcertificationAuditPost({
ids: selectedListIds.value,
status: auditForm.status,
remark: auditForm.remark
remark: auditForm.remark,
});
if (succeeded) {
ElMessage.success('操作成功');
@ -276,6 +284,15 @@ const onSubmitAudit = async () => {
}
};
// 对话框关闭时清理
const onDialogClose = () => {
// 清理参数,避免下次查询时使用旧参数
initParamUser.value = {
sysUserId: null,
refresh: Date.now(),
};
};
// 取消
const onCancel = () => {
isShowDialog.value = false;

View File

@ -1,7 +1,7 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="sysuserPageGet" :columns="columns" :pagination="true"
:tool-button="false" :initParam="initParam">
:tool-button="true" :initParam="initParam">
<template #tools>
<el-button v-auth="'permission:account:add'" type="primary" icon="ele-Plus" @click="onOpenMenu()"> 新增
</el-button>
@ -141,6 +141,9 @@ const columns = ref([
prop: 'account',
label: '账号',
align: 'center',
search: {
el: 'input',
},
},
{
prop: 'roleNames',
@ -157,7 +160,7 @@ const columns = ref([
enum: [],
},
},
{ prop: 'createdTime', label: '生成时间', search: { el: 'date-picker', props: { type: 'date' } } },
{ prop: 'createdTime', label: '生成时间', search: { el: 'date-picker', props: { type: 'daterange' } }, minWidth: 160 },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', isShow: auths(['coupon:delete', 'coupon:edit']) },
]);
@ -226,10 +229,11 @@ const onSubmit = () => {
ElMessage.success(dialogTitle.value + '成功');
isShowDialog.value = false;
} else {
ElMessage.error(res.errors);
ElMessage.error(res.errors || dialogTitle.value + '失败');
}
} catch (error) {
console.error(error);
console.error('提交失败:', error);
// 错误已经在拦截器中显示,这里不需要再显示
}
});
};
@ -240,10 +244,19 @@ const onCancel = () => {
};
onMounted(async () => {
const { data } = await sysroleSelectGet();
console.log('获取的角色数据:', data);
columns.value[4].search.enum = data;
eventList.value = data;
try {
const result = await sysroleSelectGet();
console.log('获取的角色数据:', result);
if (result.succeeded && result.data) {
columns.value[4].search.enum = result.data;
eventList.value = result.data;
} else {
ElMessage.error(result.errors || '获取角色数据失败');
}
} catch (error) {
console.error('获取角色数据时发生错误:', error);
ElMessage.error('获取角色数据时发生错误,请稍后重试');
}
});
</script>

View File

@ -21,11 +21,6 @@
<el-input v-model="state.ruleForm.code" maxlength="32" placeholder="请输入角色标识" clearable></el-input>
</el-form-item>
</el-col> -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="排序" prop="sort">
<el-input-number v-model="state.ruleForm.sort" controls-position="right" placeholder="请输入排序" class="w100" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="角色状态" prop="status">
<el-switch
@ -43,7 +38,7 @@
<el-input v-model="state.ruleForm.remark" type="textarea" placeholder="请输入角色描述" maxlength="200"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20" v-if="state.ruleForm.id != 0">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="菜单权限" prop="menus">
<el-tree
ref="treeRef"
@ -86,25 +81,23 @@ const treeRef = ref();
const rules = reactive({
name: [{ required: true, message: '请输入角色名称' }],
code: [{ required: true, message: '请输入角色标识' }],
sort: [{ required: true, message: '请输入排序' }],
// menus: [
// {
// validator: (rule, value, callback) => {
// if ((value ?? []).length === 0) {
// callback(new Error('请为角色分配权限'));
// return;
// }
// callback();
// },
// },
// ],
menus: [
{
validator: (rule, value, callback) => {
if ((value ?? []).length === 0) {
callback(new Error('请为角色分配权限'));
return;
}
callback();
},
},
],
});
//表单状态
const state = reactive({
ruleForm: {
status: 0,
sort: 100,
code: 10000,
},
menuData: [],
@ -116,6 +109,22 @@ const state = reactive({
},
});
// 重置表单和权限选择
const resetForm = () => {
state.ruleForm = {
id: 0,
name: '',
code: 10000,
status: 0,
remark: '',
menus: [],
};
nextTick(() => {
roleDialogFormRef.value?.resetFields();
treeRef.value?.setCheckedKeys([]);
});
};
// 打开弹窗
const openDialog = async (row) => {
console.log(row);
@ -123,21 +132,21 @@ const openDialog = async (row) => {
state.dialog.loading = true;
const { data: menus } = await sysmenuTreeselectGet();
state.menuData = menus ?? [];
if (row != null) {
state.ruleForm = { ...row };
const { data } = await sysroleGetrolemenuGet({ id: row.id });
state.ruleForm.menus = data ?? [];
state.dialog.title = '修改角色';
state.dialog.submitTxt = '修 改';
treeRef.value?.setCheckedKeys(data ?? []);
nextTick(() => {
treeRef.value?.setCheckedKeys(data ?? []);
});
} else {
state.ruleForm.id = 0;
// 新增角色时完全重置表单和权限
resetForm();
state.dialog.title = '新增角色';
state.dialog.submitTxt = '新 增';
// 重置表单
nextTick(() => {
roleDialogFormRef.value?.resetFields();
});
}
state.dialog.loading = false;
};
@ -151,26 +160,22 @@ const onCancel = () => {
};
// 提交
const onSubmit = async () => {
if (state.ruleForm.id != 0) {
state.ruleForm.menus = treeRef.value.getCheckedKeys();
}
state.ruleForm.menus = treeRef.value?.getCheckedKeys() ?? [];
roleDialogFormRef.value?.validate(async (v) => {
if (v) {
const { succeeded } = state.ruleForm.id === 0 ? await sysroleAddPost(state.ruleForm) : await sysroleEditPut(state.ruleForm);
const { succeeded, data } = state.ruleForm.id === 0
? await sysroleAddPost(state.ruleForm)
: await sysroleEditPut(state.ruleForm);
if (succeeded) {
if (state.ruleForm.id != 0) {
await sysroleGrantpermissionPost({
id: state.ruleForm.id,
menuIds: state.ruleForm.menus,
});
ElMessage.success('修改成功');
closeDialog();
emit('refresh');
} else {
ElMessage.success('新增成功');
closeDialog();
emit('refresh');
}
// 无论是新增还是修改,都需要授予菜单权限
const roleId = state.ruleForm.id === 0 ? data : state.ruleForm.id;
await sysroleGrantpermissionPost({
id: roleId,
menuIds: state.ruleForm.menus,
});
ElMessage.success(state.ruleForm.id === 0 ? '新增成功' : '修改成功');
closeDialog();
emit('refresh');
}
}
});

View File

@ -35,29 +35,24 @@ const columns = reactive([
{
type: 'index',
label: '序号',
width: 60,
},
{
prop: 'name',
label: '角色名称',
search: { el: 'input' },
width: 200,
},
// {
// prop: 'code',
// label: '角色标识',
// },
{
prop: 'sort',
label: '排序',
},
{
prop: 'status',
label: '状态',
},
{
prop: 'createdTime',
label: '创建时间',
prop: 'remark',
label: '角色描述',
showOverflowTooltip: true,
},
{
prop: 'action',

View File

@ -79,7 +79,7 @@ import { appuserLeaderPageGet, appuserLeaderSavePost, appuserMemberDeletePost }
const formRef = ref();
const initParam = ref({
refresh: true,
personType: 3
PersonType: 3
});
const getDefaultFormData = () => ({
@ -109,7 +109,12 @@ const formRules = ref({
{ required: true, message: '请选择性别', trigger: 'change' }
],
mobile: [
{ required: true, message: '请输入联系电话', trigger: 'blur' }
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入有效的手机号码',
trigger: ['blur', 'change']
}
],
});

View File

@ -91,13 +91,13 @@ import { ElMessage, ElForm } from 'element-plus';
import SysMenuApi from '/@/api/SysMenuApi';
import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { appuserCoachPageGet ,appuserLeaderSavePost , appuserMemberDeletePost} from '/@/api/match/appuser';
import { appuserCoachPageGet, appuserCoachSavePost, appuserMemberDeletePost } from '/@/api/match/appuser';
// 定义表单引用类型
const formRef = ref();
const initParam = ref({
refresh: true,
personType:2
PersonType: 2
});
const formData = ref({
name: '',
@ -119,7 +119,12 @@ const formRules = ref({
{ required: true, message: '请选择性别', trigger: 'change' }
],
mobile: [
{ required: true, message: '请输入联系电话', trigger: 'blur' }
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入有效的手机号码',
trigger: ['blur', 'change']
}
],
// idCardImageUrl: [
// { required: true, message: '请上传身份证', trigger: 'change' }
@ -142,8 +147,8 @@ const columns = ref([
{ label: '女', value: 1 },
],
},
{ prop: 'age', label: '年龄' , search: { el: 'input' }},
{ prop: 'mobile', label: '联系电话' , search: { el: 'input' }},
{ prop: 'age', label: '年龄' },
{ prop: 'mobile', label: '联系电话', search: { el: 'input' } },
// { prop: 'idCardImageUrl', label: '身份证' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right' },
@ -201,9 +206,9 @@ const onSubmit = () => {
try {
let res;
if (formData.value.id) {
res = await appuserLeaderSavePost(formData.value);
res = await appuserCoachSavePost(formData.value);
} else {
res = await appuserLeaderSavePost(formData.value);
res = await appuserCoachSavePost(formData.value);
}
if (res.succeeded) {

View File

@ -103,7 +103,7 @@
<div class="project-card" v-for="project in projects" :key="project.id">
<div class="project-card-left">
<div class="project-card-name">名称:{{ project.name }}</div>
<div class="project-card-type">类型:{{ project.parentProjectTypeName ?? project.projectTypeName ?? '' }}{{ project.parentProjectTypeName || project.projectTypeName ? '-' + project.name : '' }}</div>
<div class="project-card-type">类型:{{ project.parentProjectTypeName ?? project.projectTypeName ?? '' }}</div>
<div class="project-card-count">完成人数:{{ project.completionCount ?? 0 }}</div>
</div>
<div class="project-card-right">
@ -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>
@ -261,14 +318,7 @@
style="margin-bottom: 12px;"
/>
<!-- 项目完成人数提示 -->
<el-alert
v-if="hasProjectCompletionCountError()"
title="项目完成人数错误"
type="error"
:closable="false"
style="margin-bottom: 12px;"
/>
<!-- 队伍数量限制提示 -->
<el-alert
@ -288,7 +338,7 @@
type="primary"
@click="handleStep2Next"
:loading="submitDraftLoading"
:disabled="hasAnyValidationErrors() || hasProjectCompletionCountError() || hasAnyRegistrationLimit()"
:disabled="hasAnyValidationErrors() || hasAnyRegistrationLimit()"
>
下一步
</el-button>
@ -299,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>
@ -428,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 {
@ -442,7 +520,6 @@ import {
usereventregistrationMemberValidatePost,
usereventregistrationRegistrationLimitCheckPost,
usereventregistrationGroupTeamLimitGet,
usereventregistrationProjectCompletionCheckPost,
usereventregistrationOrderStatusGet,
} from '/@/api/match/usereventregistration';
import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser';
@ -766,6 +843,7 @@ const allGroups = computed(() => {
projectCompletionCount: p.completionCount ?? 0,
parentProjectTypeName: p.parentProjectTypeName,
projectTypeName: p.projectTypeName,
projectType: p.projectType,
});
});
});
@ -807,8 +885,19 @@ const isFreePreview = computed(() => {
const toggleGroupSelection = (id) => {
const idx = selectedGroupIds.value.indexOf(id);
if (idx >= 0) {
// 取消选中
selectedGroupIds.value.splice(idx, 1);
} else {
// 选中新组别:先找出这个组别属于哪个项目,然后取消该项目的其他组别
const clickedGroup = allGroups.value.find(g => g.id === id);
if (clickedGroup && clickedGroup.projectId) {
// 移除同一项目下的其他组别
selectedGroupIds.value = selectedGroupIds.value.filter(selectedId => {
const selectedGroup = allGroups.value.find(g => g.id === selectedId);
return selectedGroup?.projectId !== clickedGroup.projectId;
});
}
// 添加新选中的组别
selectedGroupIds.value.push(id);
}
};
@ -845,7 +934,6 @@ const selectedMemberIds = ref([]);
const memberValidationMap = ref({}); // { groupId_memberId: { isValid: boolean, ageValid: boolean, genderValid: boolean, message: string } }
const groupTeamLimitMap = ref({}); // { groupId: { maxTeams: number, currentTeams: number } }
const memberRegistrationLimitMap = ref({}); // { memberId: { isLimited: boolean, message: string } }
const projectCompletionCheckResult = ref(null); // 项目完成人数检查结果
const registrationLimitCheckResult = ref(null); // 报名限制检查结果
// Step2: 团队相关
@ -858,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);
@ -906,12 +1002,15 @@ const clearRegisterState = () => {
memberValidationMap.value = {};
groupTeamLimitMap.value = {};
memberRegistrationLimitMap.value = {};
projectCompletionCheckResult.value = null;
registrationLimitCheckResult.value = null;
previewLoading.value = false;
previewData.value = null;
paymentMethod.value = 1;
draftRegistrationId.value = null;
// 多团队管理相关
groupTeamsMap.value = {};
activeTeamKey.value = null;
groupMaxTeamsMap.value = {};
// 支付相关
paymentData.value = null;
polling.value = false;
@ -937,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;
@ -1102,10 +1205,7 @@ const hasAnyRegistrationLimit = () => {
return registrationLimitCheckResult.value?.hasLimit ?? false;
};
// 检查是否有项目完成人数错误
const hasProjectCompletionCountError = () => {
return projectCompletionCheckResult.value?.hasError ?? false;
};
// 判断组别是否已达到队伍数量限制
const isGroupAtTeamLimit = (groupId) => {
@ -1159,8 +1259,7 @@ const assignMemberToActiveGroup = async (member) => {
}
groupMemberMap.value[gId].push({ ...member, teamId: member.teamId });
// 实时检查项目完成人数和报名限制
await checkProjectCompletion();
// 实时检查报名限制
await checkRegistrationLimit();
};
@ -1181,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()) {
@ -1265,38 +1593,24 @@ 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('加载队伍限制配置失败');
}
};
// 检查项目完成人数
const checkProjectCompletion = async () => {
if (!currentEventId.value) return;
const groupMembers = selectedGroupIds.value.map((gId) => ({
groupId: gId,
memberIds: (groupMemberMap.value[gId] ?? []).map((m) => m.userId ?? m.id),
}));
try {
const { succeeded, data } = await usereventregistrationProjectCompletionCheckPost({
eventId: currentEventId.value,
groupMembers
});
if (succeeded) {
projectCompletionCheckResult.value = data;
}
} catch {
console.error('检查项目完成人数失败');
}
};
// 检查报名限制
const checkRegistrationLimit = async () => {
@ -1332,22 +1646,24 @@ 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 () => {
if (!currentEventId.value) return;
// 先检查项目完成人数和报名限制
await checkProjectCompletion();
// 先检查报名限制
await checkRegistrationLimit();
// 检查是否有项目完成人数错误
if (hasProjectCompletionCountError()) {
const errors = projectCompletionCheckResult.value?.errors ?? [];
ElMessage.error(errors[0]?.errorMessage ?? '项目完成人数有错误,请检查');
return;
}
// 检查是否有报名限制
if (hasAnyRegistrationLimit()) {
const details = registrationLimitCheckResult.value?.limitDetails ?? [];
@ -1355,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;
@ -1369,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 {
@ -1513,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;
@ -1537,7 +1958,13 @@ const handleCreateOrderAndPay = async () => {
return;
}
// 付费项目,显示二维码
// 如果是银行转账,直接显示银行信息,不需要二维码和轮询
if (paymentMethod.value === 3) {
paymentData.value = data;
return;
}
// 微信/支付宝支付,显示二维码
paymentData.value = data;
await generateQRCode(data.qrcodeUrl);
@ -1769,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;
@ -1899,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;