YD_Event.Web/src/views/match/manage/Steps/Step3ProjectConfig.vue

600 lines
16 KiB
Vue
Raw Normal View History

2026-02-12 21:38:29 +08:00
<template>
2026-03-18 18:03:12 +08:00
<div class="step-config">
<el-form ref="formRef" :model="localFormData" :rules="rules" label-position="top">
2026-03-23 06:14:31 +08:00
<el-card shadow="never" style="margin-bottom: 40px">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
2026-04-01 10:14:45 +08:00
<span><span style="color: red;" v-if="!readonly">*</span> 项目配置</span>
<el-button v-if="!readonly" type="primary" plain @click="addProject">选择项目</el-button>
2026-03-23 06:14:31 +08:00
</div>
</template>
2026-03-26 19:46:08 +08:00
<div class="project-card" v-for="(item, index) in localFormData.projects"
:key="item.projectId ?? item.id ?? index">
<div class="project-card__meta">
<div class="project-card__meta-name">{{ item.name }}</div>
<div class="project-card__meta-line">类型:{{ item.projectTypeName }}</div>
<div class="project-card__meta-line">时长: {{ item.durationMinutes ?? 0 }}分 {{ item.durationSeconds ?? 0 }}秒</div>
<div class="project-card__meta-line meta-muted">费用:{{ item.fee }} 元</div>
</div>
<div class="project-card__groups">
<div v-for="(group, gIndex) in (item.groups ?? [])"
:key="group.projectGroupId ?? group.id ?? gIndex" class="group-row">
<div class="group-row__fields">
2026-04-01 10:14:45 +08:00
<el-input v-if="!readonly" v-model="group.groupName" class="group-row__name" placeholder="组别名称" :disabled="readonly" />
<span v-else class="group-row__name">{{ group.groupName }}</span>
2026-03-26 19:46:08 +08:00
2026-04-14 10:26:25 +08:00
<el-date-picker value-format="YYYY-MM-DD" v-if="!readonly" class="group-row__range" type="daterange" range-separator="~"
2026-03-26 19:46:08 +08:00
start-placeholder="开始日期" end-placeholder="结束日期"
:model-value="[group.birthDateStart, group.birthDateEnd]"
2026-04-01 10:14:45 +08:00
@update:modelValue="(v) => { group.birthDateStart = v?.[0]; group.birthDateEnd = v?.[1]; }" :disabled="readonly" />
2026-04-13 02:07:55 +08:00
<span v-else class="group-row__range">{{ formatGroupDate(group) }}</span>
2026-03-26 19:46:08 +08:00
2026-04-01 10:14:45 +08:00
<el-select v-if="!readonly" v-model="group.gender" class="group-row__gender" placeholder="性别" :disabled="readonly">
<el-option label="全部" :value="3" />
2026-03-30 02:42:26 +08:00
<el-option label="男" :value="0" />
<el-option label="女" :value="1" />
2026-03-26 19:46:08 +08:00
</el-select>
2026-04-01 10:14:45 +08:00
<span v-else class="group-row__gender">{{ group.gender === 0 ? '男' : group.gender === 1 ? '女' : '全部' }}</span>
2026-03-26 19:46:08 +08:00
</div>
2026-04-01 10:14:45 +08:00
<div v-if="!readonly" class="group-row__actions">
2026-03-26 19:46:08 +08:00
<el-popconfirm title="确认删除该组别吗?" @confirm="removeGroup(index, gIndex)">
<template #reference>
<el-button text type="danger" icon="ele-Delete">删除</el-button>
</template>
</el-popconfirm>
2026-03-25 18:25:26 +08:00
</div>
2026-03-26 19:46:08 +08:00
</div>
2026-03-23 06:14:31 +08:00
2026-03-26 19:46:08 +08:00
<div v-if="!(item.groups ?? []).length" class="group-empty">暂无组别</div>
</div>
</div>
2026-03-23 06:14:31 +08:00
</el-card>
<el-card shadow="never">
2026-03-18 18:03:12 +08:00
<template #header>限报配置</template>
2026-03-26 19:46:08 +08:00
<div v-if="localFormData.projects?.length" class="project-limit-section">
<div class="project-limit-section__header">
<div class="project-limit-section__title">
2026-04-01 10:14:45 +08:00
<span class="required-star" v-if="!readonly">*</span>
2026-03-26 19:46:08 +08:00
项目限报配置
</div>
2026-04-01 10:14:45 +08:00
<div class="project-limit-section__desc" v-if="!readonly">为每个项目设置单独的限报数(必填)</div>
2026-03-26 19:46:08 +08:00
</div>
<div class="project-limit-table">
<el-table :data="localFormData.projects" stripe style="width: 100%">
<el-table-column label="项目" min-width="220">
<template #default="{ row, $index }">
<div class="project-limit-name">
<div class="project-limit-name__idx">#{{ $index + 1 }}</div>
<div class="project-limit-name__text">{{ row.name }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="限报数" width="220" align="center">
<template #default="{ row, $index }">
2026-04-01 10:14:45 +08:00
<el-form-item v-if="!readonly"
2026-03-26 19:46:08 +08:00
:prop="`projects.${$index}.limitCount`"
:rules="[{ required: true, message: '请填写项目限报数', trigger: 'change' }]"
style="margin: 0; justify-content: center;"
>
2026-04-01 10:14:45 +08:00
<el-input-number v-model="row.limitCount" :min="0" :step="1" :precision="0" :controls="true" class="project-limit-input" :disabled="readonly" />
2026-03-26 19:46:08 +08:00
</el-form-item>
2026-04-01 10:14:45 +08:00
<span v-else>{{ row.limitCount }}</span>
2026-03-26 19:46:08 +08:00
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="limit-summary">
<div class="limit-summary__card">
<div class="limit-summary__label">
2026-04-01 10:14:45 +08:00
<span class="required-star" v-if="!readonly">*</span>
2026-03-26 19:46:08 +08:00
个人项目限报数
</div>
2026-04-01 10:14:45 +08:00
<el-form-item v-if="!readonly"
2026-03-26 19:46:08 +08:00
prop="registrationLimit.individualProjectLimit"
:rules="[{ required: true, message: '请填写个人项目限报数', trigger: 'change' }]"
style="margin: 0"
>
2026-04-01 10:14:45 +08:00
<el-input-number v-model="localFormData.registrationLimit.individualProjectLimit" :min="0" :step="1" :disabled="readonly" />
2026-03-18 18:03:12 +08:00
</el-form-item>
2026-04-01 10:14:45 +08:00
<span v-else>{{ localFormData.registrationLimit.individualProjectLimit }}</span>
2026-03-26 19:46:08 +08:00
</div>
2026-04-05 21:01:02 +08:00
<div class="limit-summary__card" v-if="!isOnlyIndividual">
2026-03-26 19:46:08 +08:00
<div class="limit-summary__label">
2026-04-01 10:14:45 +08:00
<span class="required-star" v-if="!readonly">*</span>
2026-03-26 19:46:08 +08:00
团队项目限报数
</div>
2026-04-01 10:14:45 +08:00
<el-form-item v-if="!readonly"
2026-03-26 19:46:08 +08:00
prop="registrationLimit.teamProjectLimit"
2026-04-05 21:01:02 +08:00
:rules="isOnlyIndividual ? [] : [{ required: true, message: '请填写团队项目限报数', trigger: 'change' }]"
2026-03-26 19:46:08 +08:00
style="margin: 0"
>
2026-04-01 10:14:45 +08:00
<el-input-number v-model="localFormData.registrationLimit.teamProjectLimit" :min="0" :step="1" :disabled="readonly" />
2026-03-18 18:03:12 +08:00
</el-form-item>
2026-04-01 10:14:45 +08:00
<span v-else>{{ localFormData.registrationLimit.teamProjectLimit }}</span>
2026-03-26 19:46:08 +08:00
</div>
</div>
2026-03-18 18:03:12 +08:00
</el-card>
2026-03-23 06:14:31 +08:00
</el-form>
2026-03-18 18:03:12 +08:00
2026-03-23 06:14:31 +08:00
<el-dialog title="选择项目" v-model="isShowDialog">
<ProTable ref="tableRef" row-key="id" @selectionChange="selectionChange" :request-api="projectPageGet"
:columns="columns" :pagination="true" :tool-button="false">
<!-- <template #index="scope">
{{ scope.$index + 1 }}
</template> -->
<template #projectType="scope">
2026-03-23 06:14:31 +08:00
{{ scope.row.projectTypeName }}
2026-03-18 18:03:12 +08:00
</template>
2026-03-23 06:14:31 +08:00
<template #completionCount="scope">
2026-04-07 23:54:51 +08:00
{{ scope.row.completionCount }}
2026-03-23 06:14:31 +08:00
</template>
</ProTable>
<template #footer>
<span class="dialog-footer">
<el-button @click="isShowDialog = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</span>
</template>
</el-dialog>
2026-03-18 18:03:12 +08:00
</div>
2026-02-12 21:38:29 +08:00
</template>
<script setup>
2026-04-05 21:01:02 +08:00
import { ref, watch, nextTick, computed } from 'vue';
2026-03-23 06:14:31 +08:00
import ProTable from '/@/components/ProTable/index.vue';
import { projectPageGet, projectDetailGet } from '/@/api/match/project';
import { ElMessage } from 'element-plus';
2026-04-13 02:07:55 +08:00
import { formatDate } from '/@/utils/formatTime';
2026-03-18 18:03:12 +08:00
2026-04-01 10:14:45 +08:00
const props = defineProps({
modelValue: { type: Object, required: true },
readonly: { type: Boolean, default: false }
});
2026-03-18 18:03:12 +08:00
const emit = defineEmits(['update:modelValue']);
2026-04-05 21:01:02 +08:00
// 判断是否只允许个人报名
const isOnlyIndividual = computed(() => {
return props.modelValue?.registrationType === 0;
});
2026-03-18 18:03:12 +08:00
const formRef = ref(null);
const localFormData = ref({
projects: [],
registrationLimit: { individualProjectLimit: 1, teamProjectLimit: 1 },
});
2026-03-23 06:14:31 +08:00
const genderList = ref(['全部', '男', '女'])
2026-04-13 02:07:55 +08:00
// 格式化组别的年龄段(出生日期范围)
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);
};
2026-03-19 01:20:23 +08:00
const addProject = () => {
2026-03-23 06:14:31 +08:00
isShowDialog.value = true;
// 打开弹窗时,用当前已选项目初始化 timeProjects,以便确定后保留勾选
timeProjects.value = [...(localFormData.value.projects ?? [])];
2026-03-19 01:20:23 +08:00
};
2026-03-23 06:14:31 +08:00
const timeProjects = ref([])
const selectionChange = (list) => {
console.log(list)
timeProjects.value = list
}
2026-03-25 18:25:26 +08:00
const onSubmit = async () => {
// 一次性拉取详情,避免 forEach(async) 导致多次触发更新/竞态
localFormData.value.projects = [];
const selected = timeProjects.value ?? [];
if (!selected.length) {
isShowDialog.value = false;
return;
2026-03-23 06:14:31 +08:00
}
2026-03-25 18:25:26 +08:00
const projects = await Promise.all(
selected.map(async (e) => {
const res = await projectDetailGet({ id: e.id });
return {
...(res.data ?? {}),
limitCount: res.data?.limitCount ?? 0,
};
})
);
localFormData.value.projects = projects;
2026-03-23 06:14:31 +08:00
isShowDialog.value = false;
2026-03-25 18:25:26 +08:00
};
2026-03-23 06:14:31 +08:00
const isShowDialog = ref(false);
const tableRef = ref(null);
// 弹窗打开后,每当 ProTable 加载完新数据,将已选项目回显为勾选状态
watch(isShowDialog, (visible) => {
if (!visible) return;
// 用 watchEffect 追踪 tableData 变化,数据每次刷新后重新回显
const stopWatch = watch(
() => tableRef.value?.tableData,
(tableData) => {
if (!tableData?.length) return;
nextTick(() => {
const elTable = tableRef.value?.element;
if (!elTable) return;
const alreadySelectedIds = new Set(
(localFormData.value.projects ?? []).map((p) => p.projectId ?? p.id)
);
tableData.forEach((row) => {
elTable.toggleRowSelection(row, alreadySelectedIds.has(row.id));
});
});
},
{ deep: true, immediate: true }
);
// 弹窗关闭时停止监听
const stopDialog = watch(isShowDialog, (v) => {
if (!v) { stopWatch(); stopDialog(); }
});
});
2026-03-23 06:14:31 +08:00
const columns = ref([
{
prop: 'index',
label: '序号',
align: 'center',
type: 'selection'
},
{
prop: 'name',
label: '项目名称',
align: 'center',
search: {
el: 'input',
},
},
{
prop: 'projectType',
2026-03-23 06:14:31 +08:00
label: '项目类型',
search: {
el: 'select',
},
enum: [
{ label: '全部', value: '' },
2026-03-23 06:14:31 +08:00
{ label: '个人速度', value: 1003 },
{ label: '个人花样', value: 1004 },
{ label: '团队速度', value: 1005 },
{ label: '团队花样', value: 1006 },
2026-03-23 06:14:31 +08:00
],
},
{
prop: 'completionCount',
label: '完成人数',
},
{ prop: 'fee', label: '费用(元)' },
]);
2026-03-19 01:20:23 +08:00
const removeProject = (idx) => localFormData.value.projects.splice(idx, 1);
2026-03-26 19:46:08 +08:00
const removeGroup = (projectIndex, groupIndex) => {
const p = localFormData.value.projects?.[projectIndex];
if (!p) return;
if (!Array.isArray(p.groups)) p.groups = [];
p.groups.splice(groupIndex, 1);
ElMessage.success('删除组别成功');
};
2026-03-19 01:20:23 +08:00
2026-03-25 18:25:26 +08:00
const isSyncingFromParent = ref(false);
const normalizeFromParent = async (v) => {
isSyncingFromParent.value = true;
2026-03-18 18:03:12 +08:00
localFormData.value.registrationLimit = v?.registrationLimit ?? { individualProjectLimit: 1, teamProjectLimit: 1 };
2026-03-25 18:25:26 +08:00
localFormData.value.projects = (v?.projects ?? []).map((p) => ({
...p,
limitCount: p?.limitCount ?? 0,
}));
await nextTick();
isSyncingFromParent.value = false;
2026-03-23 06:14:31 +08:00
// localFormData.value.projects = (v?.projects ?? []).map((p) => ({
// ...p,
// // 用文本编辑组别,提交时再解析成 groups[]
// groupsText: Array.isArray(p.groups)
// ? p.groups.map((g) => `${g.groupName}|${g.birthDateStart ?? ''}|${g.birthDateEnd ?? ''}|${g.gender ?? ''}`).join('\n')
// : '',
// }));
// if (!localFormData.value.projects.length) ElMessage.error('请选择项目!');
2026-03-18 18:03:12 +08:00
};
watch(
() => props.modelValue,
2026-03-25 18:25:26 +08:00
async (v) => await normalizeFromParent(v),
2026-03-18 18:03:12 +08:00
{ deep: true, immediate: true }
);
watch(
localFormData,
(v) => {
2026-03-25 18:25:26 +08:00
// 防止父子双向同步时形成递归更新
if (isSyncingFromParent.value) return;
2026-03-23 06:14:31 +08:00
// const projects = v.projects.map((p) => ({
// id: p.id,
// name: p.name,
// projectTypeId: p.projectTypeId,
// projectTypeName: p.projectTypeName,
// completionCount: p.completionCount,
// durationMinutes: p.durationMinutes,
// durationSeconds: p.durationSeconds,
// fee: p.fee,
// limitCount: p.limitCount,
// groups: p.groups,
// }));
emit('update:modelValue', { ...props.modelValue, registrationLimit: v.registrationLimit, projects: v.projects });
2026-03-18 18:03:12 +08:00
},
{ deep: true }
);
2026-03-19 01:20:23 +08:00
/* addProject and removeProject are declared above to avoid initialization order issues */
2026-03-18 18:03:12 +08:00
2026-04-05 21:01:02 +08:00
const rules = computed(() => {
const baseRules = {
'registrationLimit.individualProjectLimit': [{ required: true, message: '请填写个人项目限报数', trigger: 'change' }],
};
// 只有在不是仅允许个人报名时,才添加团队项目限报数的验证
if (!isOnlyIndividual.value) {
baseRules['registrationLimit.teamProjectLimit'] = [{ required: true, message: '请填写团队项目限报数', trigger: 'change' }];
}
return baseRules;
});
2026-02-12 21:38:29 +08:00
const validate = async () => {
2026-03-18 18:03:12 +08:00
if (!formRef.value) return false;
2026-03-30 21:28:49 +08:00
// 1. 验证必须选择至少一个项目
if (!localFormData.value.projects || localFormData.value.projects.length === 0) {
ElMessage.warning('请至少选择一个项目');
return false;
}
// 2. 验证表单字段(限报数等)
2026-03-18 18:03:12 +08:00
return await formRef.value.validate();
};
2026-02-12 21:38:29 +08:00
2026-03-18 18:03:12 +08:00
defineExpose({ validate });
2026-02-12 21:38:29 +08:00
</script>
<style scoped>
.step-config {
2026-03-23 06:14:31 +08:00
padding: 0 20px;
2026-02-12 21:38:29 +08:00
}
.step-config .el-form-item {
2026-03-23 06:14:31 +08:00
margin-bottom: 24px;
2026-02-12 21:38:29 +08:00
}
2026-03-26 19:46:08 +08:00
.project-limit-section {
margin-bottom: 16px;
}
.project-limit-section__header {
2026-03-25 18:25:26 +08:00
display: flex;
2026-03-26 19:46:08 +08:00
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
2026-03-25 18:25:26 +08:00
}
2026-03-26 19:46:08 +08:00
.project-limit-section__title {
font-weight: 600;
color: var(--el-text-color-primary);
}
.project-limit-section__desc {
2026-03-25 18:25:26 +08:00
color: var(--el-text-color-secondary);
font-size: 12px;
white-space: nowrap;
}
2026-03-26 19:46:08 +08:00
.required-star {
color: var(--el-color-danger);
margin-right: 4px;
}
.project-limit-table {
border: 1px solid var(--el-border-color);
border-radius: 12px;
overflow: hidden;
background: #fff;
}
.project-limit-name {
display: flex;
align-items: center;
gap: 10px;
}
.project-limit-name__idx {
font-size: 12px;
color: var(--el-text-color-secondary);
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
padding: 2px 8px;
border-radius: 999px;
}
.project-limit-name__text {
font-weight: 600;
color: var(--el-text-color-primary);
}
.project-limit-input {
2026-03-25 18:25:26 +08:00
width: 160px;
}
2026-03-26 19:46:08 +08:00
.limit-summary {
margin-top: 14px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.limit-summary__card {
border: 1px solid var(--el-border-color);
border-radius: 12px;
background: var(--el-fill-color-light);
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.limit-summary__label {
font-weight: 600;
color: var(--el-text-color-primary);
}
.project-card {
display: flex;
border: 1px solid var(--el-border-color);
border-radius: 14px;
overflow: hidden;
background: #fff;
margin-bottom: 16px;
}
.project-card__meta {
width: 210px;
padding: 16px 14px;
background: linear-gradient(180deg, #4f8ffb 0%, #2f73f5 100%);
color: #fff;
display: flex;
flex-direction: column;
gap: 8px;
}
.project-card__meta-name {
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.project-card__meta-line {
font-size: 13px;
opacity: 0.95;
}
.meta-muted {
opacity: 0.85;
}
.project-card__groups {
flex: 1;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 12px;
background: #fff;
}
.group-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
background: var(--el-fill-color-light);
}
.group-row__fields {
display: grid;
grid-template-columns: 140px minmax(280px, 1fr) 120px;
gap: 10px;
align-items: center;
flex: 1;
}
.group-row__name,
.group-row__range,
.group-row__gender {
width: 100%;
}
.group-row__actions {
opacity: 0;
transition: opacity 0.15s ease;
flex: none;
}
.group-row:hover .group-row__actions {
opacity: 1;
}
.group-empty {
color: var(--el-text-color-secondary);
font-size: 13px;
padding: 6px 2px;
}
2026-02-12 21:38:29 +08:00
.input-hint {
2026-03-23 06:14:31 +08:00
font-size: 12px;
color: #909399;
margin-top: 5px;
2026-02-12 21:38:29 +08:00
}
.switch-hint {
2026-03-23 06:14:31 +08:00
font-size: 12px;
color: #909399;
margin-top: 5px;
2026-02-12 21:38:29 +08:00
}
.type-info-card {
2026-03-23 06:14:31 +08:00
margin-top: 20px;
background: #f5f7fa;
2026-02-12 21:38:29 +08:00
}
.card-header {
2026-03-23 06:14:31 +08:00
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
2026-02-12 21:38:29 +08:00
}
.type-list {
2026-03-23 06:14:31 +08:00
display: flex;
flex-direction: column;
gap: 12px;
2026-02-12 21:38:29 +08:00
}
.type-item {
2026-03-23 06:14:31 +08:00
display: flex;
align-items: center;
gap: 15px;
padding: 10px 15px;
background: #fff;
border-radius: 6px;
border: 1px solid #e4e7ed;
2026-02-12 21:38:29 +08:00
}
.type-name {
2026-03-23 06:14:31 +08:00
font-weight: 600;
color: #409eff;
min-width: 120px;
2026-02-12 21:38:29 +08:00
}
.type-desc {
2026-03-23 06:14:31 +08:00
color: #606266;
font-size: 14px;
2026-02-12 21:38:29 +08:00
}
</style>