智慧教务系统
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

<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>