600 lines
16 KiB
Vue
600 lines
16 KiB
Vue
<template>
|
||
<div class="step-config">
|
||
<el-form ref="formRef" :model="localFormData" :rules="rules" label-position="top">
|
||
<el-card shadow="never" style="margin-bottom: 40px">
|
||
<template #header>
|
||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||
<span><span style="color: red;" v-if="!readonly">*</span> 项目配置</span>
|
||
<el-button v-if="!readonly" type="primary" plain @click="addProject">选择项目</el-button>
|
||
</div>
|
||
</template>
|
||
<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">
|
||
<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>
|
||
|
||
<el-date-picker value-format="YYYY-MM-DD" v-if="!readonly" class="group-row__range" type="daterange" range-separator="~"
|
||
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">{{ formatGroupDate(group) }}</span>
|
||
|
||
<el-select v-if="!readonly" v-model="group.gender" class="group-row__gender" placeholder="性别" :disabled="readonly">
|
||
<el-option label="全部" :value="3" />
|
||
<el-option label="男" :value="0" />
|
||
<el-option label="女" :value="1" />
|
||
</el-select>
|
||
<span v-else class="group-row__gender">{{ group.gender === 0 ? '男' : group.gender === 1 ? '女' : '全部' }}</span>
|
||
</div>
|
||
|
||
<div v-if="!readonly" class="group-row__actions">
|
||
<el-popconfirm title="确认删除该组别吗?" @confirm="removeGroup(index, gIndex)">
|
||
<template #reference>
|
||
<el-button text type="danger" icon="ele-Delete">删除</el-button>
|
||
</template>
|
||
</el-popconfirm>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!(item.groups ?? []).length" class="group-empty">暂无组别</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card shadow="never">
|
||
<template #header>限报配置</template>
|
||
<div v-if="localFormData.projects?.length" class="project-limit-section">
|
||
<div class="project-limit-section__header">
|
||
<div class="project-limit-section__title">
|
||
<span class="required-star" v-if="!readonly">*</span>
|
||
项目限报配置
|
||
</div>
|
||
<div class="project-limit-section__desc" v-if="!readonly">为每个项目设置单独的限报数(必填)</div>
|
||
</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 }">
|
||
<el-form-item v-if="!readonly"
|
||
:prop="`projects.${$index}.limitCount`"
|
||
:rules="[{ required: true, message: '请填写项目限报数', trigger: 'change' }]"
|
||
style="margin: 0; justify-content: center;"
|
||
>
|
||
<el-input-number v-model="row.limitCount" :min="0" :step="1" :precision="0" :controls="true" class="project-limit-input" :disabled="readonly" />
|
||
</el-form-item>
|
||
<span v-else>{{ row.limitCount }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="limit-summary">
|
||
<div class="limit-summary__card">
|
||
<div class="limit-summary__label">
|
||
<span class="required-star" v-if="!readonly">*</span>
|
||
个人项目限报数
|
||
</div>
|
||
<el-form-item v-if="!readonly"
|
||
prop="registrationLimit.individualProjectLimit"
|
||
:rules="[{ required: true, message: '请填写个人项目限报数', trigger: 'change' }]"
|
||
style="margin: 0"
|
||
>
|
||
<el-input-number v-model="localFormData.registrationLimit.individualProjectLimit" :min="0" :step="1" :disabled="readonly" />
|
||
</el-form-item>
|
||
<span v-else>{{ localFormData.registrationLimit.individualProjectLimit }}</span>
|
||
</div>
|
||
<div class="limit-summary__card" v-if="!isOnlyIndividual">
|
||
<div class="limit-summary__label">
|
||
<span class="required-star" v-if="!readonly">*</span>
|
||
团队项目限报数
|
||
</div>
|
||
<el-form-item v-if="!readonly"
|
||
prop="registrationLimit.teamProjectLimit"
|
||
:rules="isOnlyIndividual ? [] : [{ required: true, message: '请填写团队项目限报数', trigger: 'change' }]"
|
||
style="margin: 0"
|
||
>
|
||
<el-input-number v-model="localFormData.registrationLimit.teamProjectLimit" :min="0" :step="1" :disabled="readonly" />
|
||
</el-form-item>
|
||
<span v-else>{{ localFormData.registrationLimit.teamProjectLimit }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-form>
|
||
|
||
<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">
|
||
{{ scope.row.projectTypeName }}
|
||
</template>
|
||
<template #completionCount="scope">
|
||
{{ scope.row.completionCount }}
|
||
</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>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
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 },
|
||
readonly: { type: Boolean, default: false }
|
||
});
|
||
const emit = defineEmits(['update:modelValue']);
|
||
|
||
// 判断是否只允许个人报名
|
||
const isOnlyIndividual = computed(() => {
|
||
return props.modelValue?.registrationType === 0;
|
||
});
|
||
|
||
const formRef = ref(null);
|
||
const localFormData = ref({
|
||
projects: [],
|
||
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,以便确定后保留勾选
|
||
timeProjects.value = [...(localFormData.value.projects ?? [])];
|
||
};
|
||
const timeProjects = ref([])
|
||
const selectionChange = (list) => {
|
||
console.log(list)
|
||
timeProjects.value = list
|
||
}
|
||
|
||
const onSubmit = async () => {
|
||
// 一次性拉取详情,避免 forEach(async) 导致多次触发更新/竞态
|
||
localFormData.value.projects = [];
|
||
const selected = timeProjects.value ?? [];
|
||
if (!selected.length) {
|
||
isShowDialog.value = false;
|
||
return;
|
||
}
|
||
|
||
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;
|
||
isShowDialog.value = false;
|
||
};
|
||
|
||
|
||
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(); }
|
||
});
|
||
});
|
||
|
||
const columns = ref([
|
||
{
|
||
prop: 'index',
|
||
label: '序号',
|
||
align: 'center',
|
||
type: 'selection'
|
||
},
|
||
{
|
||
prop: 'name',
|
||
label: '项目名称',
|
||
align: 'center',
|
||
search: {
|
||
el: 'input',
|
||
},
|
||
},
|
||
{
|
||
prop: 'projectType',
|
||
label: '项目类型',
|
||
search: {
|
||
el: 'select',
|
||
},
|
||
enum: [
|
||
{ label: '全部', value: '' },
|
||
{ label: '个人速度', value: 1003 },
|
||
{ label: '个人花样', value: 1004 },
|
||
{ label: '团队速度', value: 1005 },
|
||
{ label: '团队花样', value: 1006 },
|
||
],
|
||
},
|
||
{
|
||
prop: 'completionCount',
|
||
label: '完成人数',
|
||
},
|
||
{ prop: 'fee', label: '费用(元)' },
|
||
]);
|
||
|
||
|
||
const removeProject = (idx) => localFormData.value.projects.splice(idx, 1);
|
||
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('删除组别成功');
|
||
};
|
||
|
||
const isSyncingFromParent = ref(false);
|
||
|
||
const normalizeFromParent = async (v) => {
|
||
isSyncingFromParent.value = true;
|
||
localFormData.value.registrationLimit = v?.registrationLimit ?? { individualProjectLimit: 1, teamProjectLimit: 1 };
|
||
localFormData.value.projects = (v?.projects ?? []).map((p) => ({
|
||
...p,
|
||
limitCount: p?.limitCount ?? 0,
|
||
}));
|
||
await nextTick();
|
||
isSyncingFromParent.value = false;
|
||
// 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('请选择项目!');
|
||
};
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
async (v) => await normalizeFromParent(v),
|
||
{ deep: true, immediate: true }
|
||
);
|
||
|
||
watch(
|
||
localFormData,
|
||
(v) => {
|
||
// 防止父子双向同步时形成递归更新
|
||
if (isSyncingFromParent.value) return;
|
||
// 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 });
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
/* addProject and removeProject are declared above to avoid initialization order issues */
|
||
|
||
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;
|
||
});
|
||
|
||
const validate = async () => {
|
||
if (!formRef.value) return false;
|
||
|
||
// 1. 验证必须选择至少一个项目
|
||
if (!localFormData.value.projects || localFormData.value.projects.length === 0) {
|
||
ElMessage.warning('请至少选择一个项目');
|
||
return false;
|
||
}
|
||
|
||
// 2. 验证表单字段(限报数等)
|
||
return await formRef.value.validate();
|
||
};
|
||
|
||
defineExpose({ validate });
|
||
</script>
|
||
|
||
<style scoped>
|
||
.step-config {
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.step-config .el-form-item {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.project-limit-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.project-limit-section__header {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.project-limit-section__title {
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.project-limit-section__desc {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.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 {
|
||
width: 160px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.input-hint {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.switch-hint {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.type-info-card {
|
||
margin-top: 20px;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.type-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.type-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
padding: 10px 15px;
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
border: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.type-name {
|
||
font-weight: 600;
|
||
color: #409eff;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.type-desc {
|
||
color: #606266;
|
||
font-size: 14px;
|
||
}
|
||
</style>
|