You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
538 lines
14 KiB
538 lines
14 KiB
<template>
|
|
<el-dialog
|
|
v-model="showDialog"
|
|
:title="formData.id ? t('updateCourseSchedule') : t('addCourseSchedule')"
|
|
width="50%"
|
|
class="diy-dialog-wrap"
|
|
:destroy-on-close="true"
|
|
>
|
|
<el-form
|
|
:model="formData"
|
|
label-width="120px"
|
|
ref="formRef"
|
|
:rules="formRules"
|
|
class="page-form"
|
|
v-loading="loading"
|
|
>
|
|
<el-form-item :label="t('campusId')" prop="campus_id">
|
|
<el-select
|
|
v-model="formData.campus_id"
|
|
clearable
|
|
:placeholder="t('campusIdPlaceholder')"
|
|
class="input-width"
|
|
>
|
|
<el-option
|
|
v-for="item in campusList"
|
|
:key="item.id"
|
|
:label="item.campus_name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="t('venueId')" prop="venue_id">
|
|
<el-select
|
|
v-model="formData.venue_id"
|
|
clearable
|
|
:placeholder="t('venueIdPlaceholder')"
|
|
class="input-width"
|
|
>
|
|
<el-option
|
|
v-for="item in venueList"
|
|
:key="item.id"
|
|
:label="item.venue_name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="t('courseDate')" prop="course_date">
|
|
<el-date-picker
|
|
v-model="formData.course_date"
|
|
type="date"
|
|
clearable
|
|
:placeholder="t('courseDatePlaceholder')"
|
|
format="YYYY-MM-DD"
|
|
value-format="YYYY-MM-DD"
|
|
class="input-width"
|
|
style="width: 100%"
|
|
:disabled-date="disabledDate"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="t('timeSlot')" prop="time_slot">
|
|
<el-select
|
|
v-model="formData.time_slot"
|
|
clearable
|
|
:placeholder="t('timeSlotPlaceholder')"
|
|
class="input-width"
|
|
style="width: 100%"
|
|
allow-create
|
|
filterable
|
|
>
|
|
<el-option
|
|
v-for="(item, index) in timeSlotOptions"
|
|
:key="index"
|
|
:label="item"
|
|
:value="item"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="t('courseId')" prop="course_id">
|
|
<el-select
|
|
v-model="formData.course_id"
|
|
clearable
|
|
:placeholder="t('courseIdPlaceholder')"
|
|
class="input-width"
|
|
>
|
|
<el-option
|
|
v-for="item in courseList"
|
|
:key="item.id"
|
|
:label="item.course_name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="t('coachId')" prop="coach_id">
|
|
<el-select
|
|
v-model="formData.coach_id"
|
|
clearable
|
|
:placeholder="t('coachIdPlaceholder')"
|
|
class="input-width"
|
|
>
|
|
<el-option
|
|
v-for="item in coachList"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="t('autoSchedule')" prop="auto_schedule">
|
|
<el-radio-group v-model="formData.auto_schedule">
|
|
<el-radio :label="1">{{ t('yes') }}</el-radio>
|
|
<el-radio :label="0">{{ t('no') }}</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<span class="dialog-footer">
|
|
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
|
<el-button
|
|
type="primary"
|
|
:loading="loading"
|
|
@click="confirm(formRef)"
|
|
>{{ t('confirm') }}</el-button
|
|
>
|
|
</span>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
|
import { useDictionary } from '@/app/api/dict'
|
|
import { t } from '@/lang'
|
|
import type { FormInstance } from 'element-plus'
|
|
import {
|
|
addCourseSchedule,
|
|
editCourseSchedule,
|
|
getCourseScheduleInfo,
|
|
} from '@/app/api/course_schedule'
|
|
import { getWithCampusList, getAllVenueList } from '@/app/api/venue'
|
|
import { getVenueInfo } from '@/app/api/venue'
|
|
import { generateTimeSlots } from '@/utils/timeslots'
|
|
import { getAllCourseList } from '@/app/api/course'
|
|
import { getWithCoachList } from '@/app/api/customer_resources'
|
|
|
|
let showDialog = ref(false)
|
|
const loading = ref(false)
|
|
|
|
/**
|
|
* 表单数据
|
|
*/
|
|
const initialFormData = {
|
|
id: '',
|
|
campus_id: '',
|
|
venue_id: '',
|
|
course_date: '',
|
|
time_slot: '',
|
|
course_id: '',
|
|
coach_id: '',
|
|
participants: '',
|
|
student_ids: '',
|
|
available_capacity: '',
|
|
status: '',
|
|
auto_schedule: 1, // 默认自动排课
|
|
}
|
|
const formData: Record<string, any> = reactive({ ...initialFormData })
|
|
|
|
const formRef = ref<FormInstance>()
|
|
|
|
// 表单验证规则
|
|
const formRules = computed(() => {
|
|
return {
|
|
campus_id: [
|
|
{ required: true, message: t('campusIdPlaceholder'), trigger: 'blur' },
|
|
],
|
|
venue_id: [
|
|
{ required: true, message: t('venueIdPlaceholder'), trigger: 'blur' },
|
|
],
|
|
course_date: [
|
|
{ required: true, message: t('courseDatePlaceholder'), trigger: 'blur' },
|
|
],
|
|
time_slot: [
|
|
{ required: true, message: t('timeSlotPlaceholder'), trigger: 'blur' },
|
|
],
|
|
course_id: [
|
|
{ required: true, message: t('courseIdPlaceholder'), trigger: 'blur' },
|
|
],
|
|
coach_id: [
|
|
{ required: true, message: t('coachIdPlaceholder'), trigger: 'blur' },
|
|
],
|
|
auto_schedule: [
|
|
{ required: true, message: t('autoSchedulePlaceholder'), trigger: 'change' },
|
|
]
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['complete'])
|
|
|
|
/**
|
|
* 确认
|
|
* @param formEl
|
|
*/
|
|
const confirm = async (formEl: FormInstance | undefined) => {
|
|
if (loading.value || !formEl) return
|
|
let save = formData.id ? editCourseSchedule : addCourseSchedule
|
|
|
|
await formEl.validate(async (valid) => {
|
|
if (valid) {
|
|
loading.value = true
|
|
|
|
let data = formData
|
|
|
|
save(data)
|
|
.then((res) => {
|
|
loading.value = false
|
|
showDialog.value = false
|
|
emit('complete')
|
|
})
|
|
.catch((err) => {
|
|
loading.value = false
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// 获取字典数据
|
|
|
|
// 校区列表
|
|
const campusList = ref<any[]>([])
|
|
// 场地列表
|
|
const venueList = ref<any[]>([])
|
|
// 时间段选项
|
|
const timeSlotOptions = ref<string[]>([])
|
|
// 课程列表
|
|
const courseList = ref<any[]>([])
|
|
// 教练列表
|
|
const coachList = ref<any[]>([])
|
|
|
|
// 用于标记是否正在初始化加载数据
|
|
const isInitializing = ref(false);
|
|
|
|
// 获取校区列表
|
|
const loadCampusList = () => {
|
|
getWithCampusList({})
|
|
.then((res) => {
|
|
campusList.value = res.data || []
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
|
|
// 获取场地列表
|
|
const loadVenueList = (campus_id?: string | number) => {
|
|
getAllVenueList({ campus_id })
|
|
.then((res) => {
|
|
venueList.value = res.data || []
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
// 获取课程列表
|
|
const loadCourseList = () => {
|
|
getAllCourseList({})
|
|
.then((res) => {
|
|
courseList.value = res.data || []
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
// 获取教练列表
|
|
const loadCoachList = (campus_id?: string | number) => {
|
|
getWithCoachList({ campus_id })
|
|
.then((res) => {
|
|
coachList.value = res.data || []
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
// 监听校区变化
|
|
watch(
|
|
() => formData.campus_id,
|
|
(newValue, oldValue) => {
|
|
// 如果正在初始化数据,不触发联动
|
|
if (isInitializing.value) return;
|
|
|
|
// 只有当校区真正变化时才清空场地和教练
|
|
if (newValue !== oldValue) {
|
|
formData.venue_id = ''
|
|
formData.coach_id = ''
|
|
timeSlotOptions.value = []
|
|
|
|
if (newValue) {
|
|
getAllVenueList({ campus_id: newValue })
|
|
.then((res) => {
|
|
venueList.value = res.data || []
|
|
})
|
|
.catch(() => {})
|
|
|
|
getWithCoachList({ campus_id: newValue })
|
|
.then((res) => {
|
|
coachList.value = res.data || []
|
|
})
|
|
.catch(() => {})
|
|
} else {
|
|
venueList.value = []
|
|
coachList.value = []
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
// 监听场地变化,加载时间段
|
|
watch(
|
|
() => formData.venue_id,
|
|
async (newValue, oldValue) => {
|
|
// 如果正在初始化数据,不触发联动
|
|
if (isInitializing.value) return;
|
|
|
|
// 只有当场地真正变化时才清空时间段
|
|
if (newValue !== oldValue) {
|
|
formData.time_slot = ''
|
|
timeSlotOptions.value = []
|
|
|
|
if (newValue) {
|
|
try {
|
|
const res = await getVenueInfo(newValue)
|
|
if (res.data) {
|
|
timeSlotOptions.value = generateTimeSlots(res.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取场地详情失败:', error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
const setFormData = async (row: any = null) => {
|
|
Object.assign(formData, initialFormData)
|
|
loading.value = true
|
|
isInitializing.value = true
|
|
|
|
try {
|
|
// 1. 先获取课程详情数据(如果是编辑模式)
|
|
let detailData = null;
|
|
if (row) {
|
|
const response = await getCourseScheduleInfo(row.id);
|
|
detailData = response.data;
|
|
}
|
|
|
|
// 2. 加载校区列表
|
|
await new Promise<void>((resolve) => {
|
|
getWithCampusList({})
|
|
.then((res) => {
|
|
campusList.value = res.data || [];
|
|
resolve();
|
|
})
|
|
.catch(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// 3. 加载课程列表
|
|
await new Promise<void>((resolve) => {
|
|
getAllCourseList({})
|
|
.then((res) => {
|
|
courseList.value = res.data || [];
|
|
resolve();
|
|
})
|
|
.catch(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// 4. 如果是编辑模式且有数据
|
|
if (detailData) {
|
|
// 先保存时间段值,以便后面设置
|
|
const timeSlotValue = detailData.time_slot;
|
|
|
|
// 4.1 先设置校区,这样能触发对应的场地和教练加载
|
|
if (detailData.campus_id !== undefined) {
|
|
formData.campus_id = detailData.campus_id;
|
|
|
|
// 4.2 直接加载该校区下的场地和教练
|
|
await Promise.all([
|
|
new Promise<void>((resolve) => {
|
|
getAllVenueList({ campus_id: detailData.campus_id })
|
|
.then((res) => {
|
|
venueList.value = res.data || [];
|
|
resolve();
|
|
})
|
|
.catch(() => {
|
|
resolve();
|
|
});
|
|
}),
|
|
new Promise<void>((resolve) => {
|
|
getWithCoachList({ campus_id: detailData.campus_id })
|
|
.then((res) => {
|
|
coachList.value = res.data || [];
|
|
resolve();
|
|
})
|
|
.catch(() => {
|
|
resolve();
|
|
});
|
|
})
|
|
]);
|
|
}
|
|
|
|
// 4.3 如果有场地ID,加载时间段选项
|
|
if (detailData.venue_id) {
|
|
// 先设置场地ID,确保时间段选项与此场地关联
|
|
formData.venue_id = detailData.venue_id;
|
|
|
|
try {
|
|
const res = await getVenueInfo(detailData.venue_id);
|
|
if (res.data) {
|
|
// 生成时间段选项
|
|
timeSlotOptions.value = generateTimeSlots(res.data);
|
|
|
|
// 验证保存的时间段是否在选项中
|
|
const isValidTimeSlot = timeSlotOptions.value.includes(timeSlotValue);
|
|
|
|
// 如果时间段有效,直接设置;否则选择第一个可用选项或清空
|
|
if (isValidTimeSlot) {
|
|
formData.time_slot = timeSlotValue;
|
|
} else if (timeSlotOptions.value.length > 0) {
|
|
formData.time_slot = timeSlotOptions.value[0];
|
|
console.warn('原时间段不可用,已选择第一个可用时间段');
|
|
} else {
|
|
formData.time_slot = '';
|
|
console.warn('没有可用的时间段选项');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取场地详情失败:', error);
|
|
}
|
|
}
|
|
|
|
// 4.4 设置其他表单数据
|
|
Object.keys(formData).forEach((key) => {
|
|
// 跳过已设置的字段
|
|
if (key === 'campus_id' || key === 'venue_id' || key === 'time_slot') {
|
|
return;
|
|
}
|
|
|
|
if (detailData[key] !== undefined) {
|
|
formData[key] = detailData[key];
|
|
} else if (key === 'auto_schedule' && detailData[key] === undefined) {
|
|
// 如果后端没有返回自动排课字段,使用默认值1
|
|
formData.auto_schedule = 1;
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('加载数据失败:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
isInitializing.value = false;
|
|
}
|
|
}
|
|
|
|
// 验证手机号格式
|
|
const mobileVerify = (rule: any, value: any, callback: any) => {
|
|
if (value && !/^1[3-9]\d{9}$/.test(value)) {
|
|
callback(new Error(t('generateMobile')))
|
|
} else {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
// 验证身份证号
|
|
const idCardVerify = (rule: any, value: any, callback: any) => {
|
|
if (
|
|
value &&
|
|
!/^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
|
|
value
|
|
)
|
|
) {
|
|
callback(new Error(t('generateIdCard')))
|
|
} else {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
// 验证邮箱号
|
|
const emailVerify = (rule: any, value: any, callback: any) => {
|
|
if (value && !/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(value)) {
|
|
callback(new Error(t('generateEmail')))
|
|
} else {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
// 验证请输入整数
|
|
const numberVerify = (rule: any, value: any, callback: any) => {
|
|
if (!Number.isInteger(value)) {
|
|
callback(new Error(t('generateNumber')))
|
|
} else {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
// 禁用非当天日期
|
|
const disabledDate = (time: Date) => {
|
|
const today = new Date()
|
|
today.setHours(0, 0, 0, 0)
|
|
const currentDate = new Date(time)
|
|
currentDate.setHours(0, 0, 0, 0)
|
|
|
|
// 只允许选择今天及以后
|
|
return today.getTime() > currentDate.getTime()
|
|
}
|
|
|
|
defineExpose({
|
|
showDialog,
|
|
setFormData,
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.input-width {
|
|
width: 100%;
|
|
}
|
|
|
|
:deep(.el-date-editor.el-input),
|
|
:deep(.el-date-editor.el-input__wrapper),
|
|
:deep(.el-select),
|
|
:deep(.el-select__wrapper) {
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
<style lang="scss">
|
|
.diy-dialog-wrap .el-form-item__label {
|
|
height: auto !important;
|
|
}
|
|
</style>
|
|
|