548 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="projectPageGet" :columns="columns" :pagination="true"
:tool-button="false" :initParam="initParam">
<template #tools>
<el-button v-auth="'project:add'" type="primary" icon="ele-Plus" @click="onOpenMenu()"> 新增 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #projectType="scope">
{{ scope.row.projectTypeName }}
</template>
<template #time="scope">
{{ scope.row.durationMinutes }}分{{ scope.row.durationSeconds }}秒
</template>
<template #completionCount="scope">
{{ scope.row.completionCount }}
</template>
<template #action="scope">
<el-button v-auth="'project:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'project:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm>
<el-button v-auth="'project:detail'" size="small" text type="primary"
@click="onViewDetail(scope.row)"> 查看详情 </el-button>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog :title="dialogTitle" v-model="isShowDialog" width="1100px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px"
style="width: 90%; margin-bottom: 30px" v-if="isShowDialog">
<!-- 项目名称 -->
<el-form-item label="项目名称:" prop="name">
<el-input v-model="formData.name" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 项目类型 -->
<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>
<el-option label="团队花样" :value="1006"></el-option>
</el-select>
</el-form-item>
<!-- 完成人数 -->
<el-form-item label="完成人数:" prop="completionCount">
<el-input-number v-model="formData.completionCount" placeholder="请输入完成人数" :min="0" :step="1" style="width: 100%"></el-input-number>
</el-form-item>
<!-- 组别 -->
<div>
<el-form-item style="display: none;" prop="groups">
<el-input v-model="formData.groups.length" style="display: none;"></el-input>
</el-form-item>
<el-form-item :label="'组别' + (index + 1) + ':'" :prop="'groups[' + index + ']'"
v-for="(group, index) in (formData.groups || [])" :key="index" :rules="{
validator: (rule, value, callback) => {
if (!value || !value.groupName) {
callback(new Error('请输入组别名'));
} else if (!value.birthDateStart) {
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 {
callback();
}
},
trigger: 'blur',
}">
<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" value-format="YYYY-MM-DD"></el-date-picker>
<el-date-picker v-model="group.birthDateEnd" type="date" placeholder="出生日期结束"
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>
<el-option label="不限" value="3"></el-option>
</el-select>
<el-button circle @click="removeGroup(index)" style="margin-left: 30px" size="small"
icon="Minus" v-if="index == (formData.groups || []).length - 1 && index != 0"></el-button>
</el-form-item>
<el-button @click="addGroup" icon="Plus" style="margin: 20px 0px 20px 150px; width: 85%"
dashed></el-button>
</div>
<!-- 费用 -->
<el-form-item label="费用:" prop="fee">
<el-input v-model="formData.fee" placeholder="请输入费用" class="fee-input">
<template slot="append">元</template>
</el-input>
</el-form-item>
<!-- 时长 -->
<el-form-item label="时长:" prop="duration" required>
<div class="duration-row">
<el-input v-model="formData.durationMinutes" placeholder="分" type="number"></el-input>
<span class="duration-label">分</span>
<span class="duration-separator">--</span>
<el-input v-model="formData.durationSeconds" placeholder="秒" type="number"></el-input>
<span class="duration-label">秒</span>
</div>
</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>
</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>
<script setup name="project">
import { defineAsyncComponent, ref, reactive } from 'vue';
import { ElMessage, ElForm } from 'element-plus';
import SysMenuApi from '/@/api/SysMenuApi';
import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { projectPageGet, projectAddPost, projectEditPut, projectDeleteDelete, projectDetailGet } from '/@/api/match/project';
// 定义表单引用类型
const formRef = ref();
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: '',
birthDateStart: '',
birthDateEnd: '',
gender: '',
});
};
const removeGroup = (index) => {
formData.value.groups.splice(index, 1);
};
const formData = ref({
name: '',
projectType: '',
completionCount: '',
fee: '',
durationMinutes: '',
durationSeconds: '',
groups: [
{
groupName: '',
birthDateStart: '',
birthDateEnd: '',
gender: '',
},
],
});
const validateDuration = (rule, value, callback) => {
const minutes = Number(formData.value.durationMinutes ?? 0);
const seconds = Number(formData.value.durationSeconds ?? 0);
// 检查是否为有效数字
if (isNaN(minutes) || isNaN(seconds)) {
callback(new Error('时长必须为有效数值'));
return;
}
// 检查是否同时为0或为空
if ((minutes === 0 && seconds === 0) ||
(formData.value.durationMinutes === '' && formData.value.durationSeconds === '')) {
callback(new Error('时长不能为空,请输入有效时长'));
return;
}
// 检查秒数是否在有效范围内
if (seconds < 0 || seconds >= 60) {
callback(new Error('秒数必须在 0-59 之间'));
return;
}
callback();
};
const validateGroups = (rule, value, callback) => {
// 检查是否至少有一个组别
if (!value || value.length === 0) {
callback(new Error('请至少添加一个组别'));
return;
}
callback();
};
const formRules = ref({
name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
projectType: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
completionCount: [{ required: true, message: '请输入完成人数', trigger: 'blur' }],
fee: [{ required: true, message: '请输入费用', trigger: 'blur' }],
duration: [
{ required: true, validator: validateDuration, trigger: ['blur', 'change'] }
],
groups: [
{
required: true,
validator: validateGroups,
trigger: ['blur', 'change']
}
],
});
const columns = ref([
{
prop: 'index',
label: '序号',
align: 'center',
},
{
prop: 'name',
label: '项目名称',
align: 'center',
search: {
el: 'input',
key: 'ProjectName',
},
},
{
prop: 'projectType',
label: '项目类型',
search: {
el: 'select',
key: 'ProjectType',
},
enum: [
{ label: '全部', value: '' },
{ label: '个人速度', value: 1003 },
{ label: '个人花样', value: 1004 },
{ label: '团队速度', value: 1005 },
{ label: '团队花样', value: 1006 },
],
},
{
prop: 'completionCount',
label: '完成人数',
},
{ prop: 'fee', label: '费用(元)' },
{ prop: 'time', label: '时长' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right' },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenMenu = async (row) => {
dialogTitle.value = row ? '编辑' : '新增';
formData.value = {
name: '',
projectType: '',
completionCount: '',
fee: '',
durationMinutes: '',
durationSeconds: '',
groups: [
{
groupName: '',
birthDateStart: '',
birthDateEnd: '',
gender: '',
},
],
};
if (row) {
const { succeeded, data } = await projectDetailGet({ id: row.id });
formData.value = data;
// 确保 groups 数组存在且有效
if (!formData.value.groups || !Array.isArray(formData.value.groups)) {
formData.value.groups = [
{
groupName: '',
birthDateStart: '',
birthDateEnd: '',
gender: '',
},
];
} else {
// 过滤无效的 group 并补充缺失的属性,同时格式化日期
formData.value.groups = formData.value.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() : '',
}));
// 如果过滤后没有组,添加一个空组
if (formData.value.groups.length === 0) {
formData.value.groups = [
{
groupName: '',
birthDateStart: '',
birthDateEnd: '',
gender: '',
},
];
}
}
}
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 });
if (succeeded) {
ElMessage.success('删除成功');
initParam.value.refresh = !initParam.value.refresh; // 刷新表格数据
} else {
ElMessage.error(errors);
}
};
// 表单提交
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid, invalidFields) => {
if (!valid) {
// 验证失败,自动聚焦到第一个错误字段
if (invalidFields && Object.keys(invalidFields).length > 0) {
const firstErrorField = Object.keys(invalidFields)[0];
// 延时一点时间,确保DOM更新后再聚焦
setTimeout(() => {
// 查找并聚焦到第一个错误的表单项
const formItems = document.querySelectorAll('.el-form-item.is-error');
if (formItems && formItems.length > 0) {
const firstFormItem = formItems[0];
const input = firstFormItem.querySelector('input, select, .el-select');
if (input) {
input.focus?.();
input.click?.();
}
}
}, 100);
// 显示具体的错误信息
const errorMessages = [];
Object.values(invalidFields).forEach(field => {
field.forEach(error => {
if (error.message) {
errorMessages.push(error.message);
}
});
});
if (errorMessages.length > 0) {
ElMessage.warning(errorMessages[0]);
}
}
return;
}
try {
let res;
formData.value.completionCount = Number(formData.value.completionCount);
if (formData.value.id) {
res = await projectEditPut(formData.value);
} else {
res = await projectAddPost(formData.value);
}
if (res.succeeded) {
initParam.value.refresh = !initParam.value.refresh; // 刷新表格数据
ElMessage.success(dialogTitle.value + '成功');
isShowDialog.value = false;
} else {
ElMessage.error(res.errors);
}
} catch (error) {
console.error(error);
}
});
};
// 取消
const onCancel = () => {
isShowDialog.value = false;
};
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
/* 详情页面组别区域样式 */
.detail-groups-section {
margin-bottom: 20px;
}
</style>