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

530 lines
13 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">
<span><span style="color: red;">*</span> 项目配置</span>
2026-03-26 19:46:08 +08:00
<el-button 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">
<el-input v-model="group.groupName" class="group-row__name" placeholder="组别名称" />
<el-date-picker 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]; }" />
<el-select v-model="group.gender" class="group-row__gender" placeholder="性别">
<el-option label="全部" :value="0" />
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
</el-select>
</div>
<div 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>
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">
<span class="required-star">*</span>
项目限报配置
</div>
<div class="project-limit-section__desc">为每个项目设置单独的限报数(必填)</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
: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" />
</el-form-item>
</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">*</span>
个人项目限报数
</div>
<el-form-item
prop="registrationLimit.individualProjectLimit"
:rules="[{ required: true, message: '请填写个人项目限报数', trigger: 'change' }]"
style="margin: 0"
>
<el-input-number v-model="localFormData.registrationLimit.individualProjectLimit" :min="0" :step="1" />
2026-03-18 18:03:12 +08:00
</el-form-item>
2026-03-26 19:46:08 +08:00
</div>
<div class="limit-summary__card">
<div class="limit-summary__label">
<span class="required-star">*</span>
团队项目限报数
</div>
<el-form-item
prop="registrationLimit.teamProjectLimit"
:rules="[{ required: true, message: '请填写团队项目限报数', trigger: 'change' }]"
style="margin: 0"
>
<el-input-number v-model="localFormData.registrationLimit.teamProjectLimit" :min="0" :step="1" />
2026-03-18 18:03:12 +08:00
</el-form-item>
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 #projectTypeId="scope">
{{ scope.row.projectTypeName }}
2026-03-18 18:03:12 +08:00
</template>
2026-03-23 06:14:31 +08:00
<template #completionCount="scope">
{{ scope.row.projectTypeId == '1003' || scope.row.projectTypeId == '1004' ? 1 :
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>
2026-03-18 18:03:12 +08:00
</div>
2026-02-12 21:38:29 +08:00
</template>
<script setup>
2026-03-25 18:25:26 +08:00
import { ref, watch, nextTick } 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-03-18 18:03:12 +08:00
const props = defineProps({ modelValue: { type: Object, required: true } });
const emit = defineEmits(['update:modelValue']);
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-03-19 01:20:23 +08:00
const addProject = () => {
2026-03-23 06:14:31 +08:00
timeProjects.value = [];
isShowDialog.value = true;
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 columns = ref([
{
prop: 'index',
label: '序号',
align: 'center',
type: 'selection'
},
{
prop: 'name',
label: '项目名称',
align: 'center',
search: {
el: 'input',
},
},
{
prop: 'projectTypeId',
label: '项目类型',
search: {
el: 'select',
},
enum: [
{ label: '全部', value: 0 },
{ label: '团队速度', value: 1001 },
{ label: '团队花样', value: 1002 },
{ label: '个人速度', value: 1003 },
{ label: '个人花样', value: 1004 },
],
},
{
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
const rules = {
'registrationLimit.individualProjectLimit': [{ required: true, message: '请填写个人项目限报数', trigger: 'change' }],
'registrationLimit.teamProjectLimit': [{ required: true, message: '请填写团队项目限报数', trigger: 'change' }],
};
2026-02-12 21:38:29 +08:00
const validate = async () => {
2026-03-18 18:03:12 +08:00
if (!formRef.value) return false;
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>