diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java
index 68e313a7..eaf514ef 100644
--- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java
+++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java
@@ -163,6 +163,30 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
return R.fail("限号数量必须大于0");
}
+ // 检查结束时间必须大于开始时间
+ if (doctorSchedule.getStartTime() != null && doctorSchedule.getEndTime() != null) {
+ if (!doctorSchedule.getStartTime().isBefore(doctorSchedule.getEndTime())) {
+ return R.fail("结束时间必须大于开始时间");
+ }
+ }
+
+ // 检查时间重叠(同一医生、同一天时间段有重叠)
+ if (doctorSchedule.getDoctorId() != null && scheduledDate != null
+ && doctorSchedule.getStartTime() != null && doctorSchedule.getEndTime() != null) {
+ LocalDate scheduleDate = LocalDate.parse(scheduledDate);
+ boolean hasOverlap = checkTimeOverlap(
+ doctorSchedule.getDoctorId(),
+ scheduleDate,
+ doctorSchedule.getStartTime(),
+ doctorSchedule.getEndTime()
+ );
+ if (hasOverlap) {
+ return R.fail("该医生在 " + scheduledDate + " 的 "
+ + doctorSchedule.getStartTime() + "-" + doctorSchedule.getEndTime()
+ + " 时间段与已有排班重叠,不能重复添加");
+ }
+ }
+
// 创建新对象,排除id字段(数据库id列是GENERATED ALWAYS,由数据库自动生成)
DoctorSchedule newSchedule = new DoctorSchedule();
newSchedule.setWeekday(doctorSchedule.getWeekday());
@@ -381,6 +405,42 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
return slots;
}
+ /**
+ * 检查同一医生、同一天、同时段是否已存在排班
+ *
+ * @param doctorId 医生ID
+ * @param scheduleDate 出诊日期
+ * @param timePeriod 时段(上午/下午)
+ * @return true表示存在重复排班,false表示不存在
+ */
+ // private boolean checkDuplicateSchedule(Long doctorId, LocalDate scheduleDate, String timePeriod) {
+ // return schedulePoolService.lambdaQuery()
+ // .eq(SchedulePool::getDoctorId, doctorId)
+ // .eq(SchedulePool::getScheduleDate, scheduleDate)
+ // .eq(SchedulePool::getShift, timePeriod)
+ // .exists();
+ // }
+
+ /**
+ * 检查同一医生、同一天是否有时间重叠的排班
+ * 重叠判断:startA < endB AND startB < endA(即时间段有交集)
+ *
+ * @param doctorId 医生ID
+ * @param scheduleDate 出诊日期
+ * @param startTime 开始时间
+ * @param endTime 结束时间
+ * @return true表示存在时间重叠,false表示不重叠
+ */
+ private boolean checkTimeOverlap(Long doctorId, LocalDate scheduleDate,
+ LocalTime startTime, LocalTime endTime) {
+ return schedulePoolService.lambdaQuery()
+ .eq(SchedulePool::getDoctorId, doctorId)
+ .eq(SchedulePool::getScheduleDate, scheduleDate)
+ .lt(SchedulePool::getStartTime, endTime) // 已有开始时间 < 新结束时间
+ .gt(SchedulePool::getEndTime, startTime) // 已有结束时间 > 新开始时间
+ .exists();
+ }
+
/**
* 根据星期几计算具体日期(下周的对应星期)
*/
diff --git a/openhis-ui-vue3/src/views/appoinmentmanage/deptManage/index.vue b/openhis-ui-vue3/src/views/appoinmentmanage/deptManage/index.vue
index 468275e4..535e10a0 100644
--- a/openhis-ui-vue3/src/views/appoinmentmanage/deptManage/index.vue
+++ b/openhis-ui-vue3/src/views/appoinmentmanage/deptManage/index.vue
@@ -166,7 +166,19 @@
{{ dateGroup.weekday }}
-
+
+
+ handleTimeSlotChange(val, scope.row)"
+ >
+
+
+
+
+
- handleStartTimeChange(val, scope.row)"
/>
@@ -929,17 +942,24 @@ const handleAppointmentSettingCancel = () => {
// 路由和导航
const router = useRouter()
+// 根据开始时间自动判断时段(上午/下午)
+const getTimeSlot = (startTime) => {
+ if (!startTime) return '上午';
+ const hour = parseInt(startTime.split(':')[0]);
+ return hour < 12 ? '上午' : '下午';
+};
+
// 生成一周排班数据
const generateWeekSchedule = (startDate, workTimeConfig) => {
const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const timeSlots = [
{
- label: '上午',
+ label: getTimeSlot(workTimeConfig?.morningStart || '08:00'),
startTime: workTimeConfig?.morningStart || '08:00',
endTime: workTimeConfig?.morningEnd || '12:00'
},
{
- label: '下午',
+ label: getTimeSlot(workTimeConfig?.afternoonStart || '14:30'),
startTime: workTimeConfig?.afternoonStart || '14:30',
endTime: workTimeConfig?.afternoonEnd || '18:00'
}
@@ -1380,16 +1400,17 @@ const isLastDraftRowInSlot = (row) => {
const handleAddSchedule = (row) => {
// 创建新的排班记录,基于当前行的日期和时段
+ const startTime = row.startTime || '08:00';
const newSchedule = {
id: `new-${Date.now()}-${Math.random()}`,
date: row.date,
weekday: row.weekday,
- timeSlot: row.timeSlot,
- startTime: row.startTime || '08:00',
+ timeSlot: getTimeSlot(startTime),
+ startTime: startTime,
endTime: row.endTime || '12:00',
doctorName: '',
room: '',
- maxNumber: '',
+ maxNumber: row.maxNumber ?? '',
appointmentItem: '',
registrationFee: 0,
clinicItem: '',
@@ -1412,6 +1433,29 @@ const handleAddSchedule = (row) => {
}
}
+// 时段变化时,更新默认的开始结束时间
+const handleTimeSlotChange = (val, row) => {
+ const config = workTimeConfig.value;
+ if (val === '上午') {
+ if (!row.startTime || row.startTime >= '12:00') {
+ row.startTime = config?.morningStart || '08:00';
+ row.endTime = config?.morningEnd || '12:00';
+ }
+ } else if (val === '下午') {
+ if (!row.startTime || row.startTime < '12:00') {
+ row.startTime = config?.afternoonStart || '14:30';
+ row.endTime = config?.afternoonEnd || '18:00';
+ }
+ }
+};
+
+// 开始时间变化时,自动更新时段
+const handleStartTimeChange = (val, row) => {
+ if (val) {
+ row.timeSlot = getTimeSlot(val);
+ }
+};
+
// 删除排班
const handleDeleteSchedule = (row) => {
if (isLastDraftRowInSlot(row)) {
@@ -1585,6 +1629,19 @@ const handleSave = async () => {
return !!resolveDoctorName(item) || !!item.doctorId;
});
+ // 验证结束时间必须大于开始时间
+ const invalidTimeSchedules = filledSchedules.filter(item => {
+ if (item.startTime && item.endTime) {
+ return item.startTime >= item.endTime;
+ }
+ return false;
+ });
+
+ if (invalidTimeSchedules.length > 0) {
+ ElMessage.warning('结束时间必须大于开始时间,请检查排班记录');
+ return;
+ }
+
const incompleteSchedules = filledSchedules.filter(item => {
const isDoctorValid = !!resolveDoctorName(item) || !!item.doctorId;
const isRoomValid = item.room && item.room.trim() !== '';
diff --git a/openhis-ui-vue3/src/views/maintainSystem/Inspection/index.vue b/openhis-ui-vue3/src/views/maintainSystem/Inspection/index.vue
index 21b02482..490ddc3c 100644
--- a/openhis-ui-vue3/src/views/maintainSystem/Inspection/index.vue
+++ b/openhis-ui-vue3/src/views/maintainSystem/Inspection/index.vue
@@ -595,17 +595,19 @@
{{ $index + 1 }}
-
+
- {{ row.name }}
+ {{ row.name || '-' }}
@@ -668,9 +670,30 @@
-
-
- {{ row.unit || '-' }}
+
+
+
+
+
+
+
+
+
+ {{ row.unit || '-' }}
+
@@ -1530,7 +1553,7 @@ const queryDiagnosisItems = (queryString, cb) => {
} else if (response.data && response.data.rows) {
data = response.data.rows;
} else {
- console.error('API返回数据格式不符合预期:', response.data);
+
return cb([]);
}
@@ -1557,16 +1580,14 @@ const queryDiagnosisItems = (queryString, cb) => {
cb(results);
}).catch(error => {
- console.error('查询诊疗目录失败:', error);
ElMessage.error('查询诊疗目录失败,请稍后重试');
cb([]);
});
};
-// 添加新的套餐项目
let addingItem = false;
const addPackageItem = () => {
- if (addingItem) return; // 防止重复调用
+ if (addingItem) return;
addingItem = true;
const newItem = {
@@ -1583,14 +1604,24 @@ const addPackageItem = () => {
totalAmount: 0.00,
origin: ''
};
+
packageItems.value.push(newItem);
+ editingRowId.value = packageItems.value.length - 1; // 进入编辑模式
- // 延迟重置标志位,确保不会影响其他操作
- setTimeout(() => {
+ // 【关键】等待 DOM 更新后,聚焦到新行的输入框
+ nextTick(() => {
+ const newIndex = packageItems.value.length - 1;
+ const inputEl = itemNameRefs.value[newIndex];
+ if (inputEl && inputEl.$el) {
+ // Element Plus 的 el-input 组件,实际 input 在 $el 内部
+ const realInput = inputEl.$el.querySelector('input');
+ if (realInput) {
+ realInput.focus();
+ }
+ }
addingItem = false;
- }, 100);
+ });
};
-
// 删除套餐项目
const deletePackageItem = (index) => {
ElMessageBox.confirm('确定要删除该项目吗?', '提示', {
@@ -1599,6 +1630,7 @@ const deletePackageItem = (index) => {
type: 'warning'
}).then(() => {
packageItems.value.splice(index, 1);
+ itemNameRefs.value.splice(index, 1); // 同步移除 ref
calculateAmounts();
ElMessage.success('删除成功');
}).catch(() => {
@@ -1636,48 +1668,69 @@ const updateItemAmount = (item) => {
const updateItemTotalAmount = (item) => {
item.totalAmount = (item.amount || 0.00) + (item.serviceFee || 0.00);
};
-
// 处理编辑项目
const handleEditItem = (index) => {
- if (editingRowId.value !== null && editingRowId.value !== index) {
- ElMessage.warning('请先保存或取消当前正在编辑的行');
+ const item = packageItems.value[index];
+ if (!item.name) {
+ ElMessage.warning('请输入项目名称');
+ if (itemNameRefs.value[index]) {
+ itemNameRefs.value[index].focus();
+ }
return;
}
+
if (editingRowId.value === index) {
- // 保存编辑 - 验证并计算金额
- const item = packageItems.value[index];
- if (!item.name || item.name.trim() === '') {
- ElMessage.error('请输入项目名称');
+ // 验证数量、单价等...
+ if (!item.quantity || item.quantity <= 0) {
+ ElMessage.warning('请输入正确的数量');
return;
}
- if (!item.unit || item.unit.trim() === '') {
- ElMessage.error('请输入单位');
- return;
- }
- if (item.quantity <= 0) {
- ElMessage.error('数量必须大于0');
- return;
- }
- if (item.unitPrice <= 0) {
- ElMessage.error('单价必须大于0');
+ if (!item.unitPrice || item.unitPrice < 0) {
+ ElMessage.warning('请输入正确的单价');
return;
}
- // 重新计算金额
+ // 计算单项金额
updateItemAmount(item);
+ // 【重要】保存成功后,清除之前备份的原始数据
+ if (item._originalData) {
+ delete item._originalData;
+ }
+
editingRowId.value = null;
ElMessage.success('保存成功');
} else {
- // 进入编辑模式
+ // 进入编辑模式:深拷贝当前行数据作为备份,以便取消时恢复
+ item._originalData = JSON.parse(JSON.stringify(item));
editingRowId.value = index;
}
};
-
// 取消编辑项目
const cancelEditItem = (index) => {
+ const row = packageItems.value[index];
+
+ // 判断依据:有 _originalData 备份说明是从编辑模式进入的(无论是否有ID),还原数据
+ // 没有备份说明是刚新增、第一次进入编辑、从未保存过,才删除
+ if (row._originalData) {
+ // 编辑已有数据 → 用备份数据还原,严禁清空字段
+ Object.assign(row, row._originalData);
+ delete row._originalData;
+ ElMessage.info('已取消编辑');
+ } else {
+ // 全新新增行(无备份)→ 直接删除,并清理 ref 引用
+ packageItems.value.splice(index, 1);
+ if (itemNameRefs.value[index]) {
+ delete itemNameRefs.value[index];
+ }
+ ElMessage.info('已取消新增');
+ }
+
+ // 退出编辑模式
editingRowId.value = null;
- ElMessage.info('已取消编辑');
+
+ // 重新计算总金额
+ calculateAmounts();
};
// 计算单个项目的服务费(基于折扣后的金额)
@@ -1741,6 +1794,14 @@ const calculateAmounts = () => {
}
};
+const itemNameRefs = ref([]); // 存储每行项目名称输入框的 DOM 引用
+
+const setItemNameRef = (el, index) => {
+ if (el) {
+ itemNameRefs.value[index] = el;
+ }
+};
+
// 检验类型相关方法
// 处理表格点击事件,用于自动删除空的编辑行
@@ -2337,7 +2398,7 @@ const savePackageData = async (basicInfo, detailData) => {
throw new Error('保存成功但未返回套餐ID。请检查后端接口是否正确返回了packageId或id字段');
}
- // 2. 分别保存每个明细数据到明细表
+ // 2. 分别保存每个明细数据到明细表,并将后端返回的 id 回填到 packageItems
for (let i = 0; i < detailData.length; i++) {
const detailItem = {
...detailData[i],
@@ -2350,6 +2411,12 @@ const savePackageData = async (basicInfo, detailData) => {
loading.close();
throw new Error(`保存第 ${i + 1} 个明细项失败: ${detailResponse.msg || '未知错误'}`);
}
+
+ // 回填后端生成的明细 id,防止取消编辑时被误判为新增行
+ // 后端返回字段名是 detailId(见 InspectionPackageDetail.java)
+ if (detailResponse.data && (detailResponse.data.detailId || detailResponse.data.id)) {
+ packageItems.value[i].id = detailResponse.data.detailId || detailResponse.data.id;
+ }
}
// 关闭加载提示
@@ -2474,8 +2541,9 @@ const loadInspectionPackage = async (packageId) => {
bloodVolume.value = basicData.bloodVolume || '';
remarks.value = basicData.remarks || '';
- // 填充明细数据
+ // 填充明细数据(必须映射 id,否则取消编辑时会被误判为新增行删除)
packageItems.value = detailData.map(item => ({
+ id: item.detailId || item.id,
name: item.itemName || item.name,
dosage: item.dosage || '',
route: item.route || '',
@@ -2582,6 +2650,20 @@ watch(generateServiceFee, (newVal) => {
watch(packageItems, (newVal) => {
calculateAmounts();
}, { deep: true });
+// 样本类型数据
+
+// 【新增】定义通用单位列表
+const unitOptions = ref([
+ { value: '片', label: '片' },
+ { value: '粒', label: '粒' },
+ { value: 'ml', label: 'ml' },
+ { value: 'g', label: 'g' },
+ { value: 'mg', label: 'mg' },
+ { value: '支', label: '支' },
+ { value: '盒', label: '盒' },
+ { value: '次', label: '次' },
+ { value: '天', label: '天' }
+]);