Files
his/openhis-ui-vue3/src/views/appoinmentmanage/deptManage/doctorschedule/index.vue
2026-02-05 16:00:56 +08:00

894 lines
26 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="doctorschedule-container">
<div class="doctorschedule-header">
<h2 class="doctorschedule-title">医生排班</h2>
</div>
<div class="doctorschedule-content">
<!-- 筛选条件 -->
<div class="filter-condition">
<span class="filter-label">卫生机构</span>
<el-select v-model="filterParams.orgName" class="filter-select">
<el-option label="演示医院" value="演示医院"></el-option>
</el-select>
<span class="filter-label">科室名称</span>
<el-select v-model="filterParams.deptName" class="filter-select">
<el-option label="测试内科" value="测试内科"></el-option>
</el-select>
<span class="filter-label">开始日期</span>
<el-date-picker
v-model="filterParams.startDate"
type="date"
placeholder="选择日期"
class="filter-date-picker"
/>
<span class="filter-label">排班类型</span>
<div class="radio-group">
<el-radio v-model="filterParams.appointmentType" label="普通" @change="handleAppointmentTypeChange">普通</el-radio>
<el-radio v-model="filterParams.appointmentType" label="专家" @change="handleAppointmentTypeChange">专家</el-radio>
</div>
</div>
<!-- 排班表格 -->
<div class="schedule-table-container">
<!-- 按日期分组显示排班 -->
<div v-for="(dateGroup, index) in groupedSchedule" :key="index" class="daily-schedule">
<div class="daily-header">
<span class="date-text">{{ dateGroup.date }}</span>
<span class="weekday-text">{{ dateGroup.weekday }}</span>
</div>
<el-table :data="dateGroup.items" border style="width: 100%" class="schedule-table">
<el-table-column prop="timeSlot" label="时段" width="100"></el-table-column>
<el-table-column prop="doctorName" :label="filterParams.appointmentType === '专家' ? '专家' : '医生'" width="150">
<template #default="scope">
<el-select
v-model="scope.row.doctorName"
placeholder="请选"
class="inline-select"
:disabled="!isEditMode"
>
<el-option
v-for="doctor in getDoctorOptions(filterParams.appointmentType)"
:key="doctor.value"
:label="doctor.label"
:value="doctor.value"
></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="room" label="诊室" width="100">
<template #default="scope">
<el-select
v-model="scope.row.room"
placeholder="请选"
class="inline-select"
:disabled="!isEditMode"
>
<el-option label="诊室1" value="诊室1"></el-option>
<el-option label="诊室2" value="诊室2"></el-option>
<el-option label="诊室3" value="诊室3"></el-option>
<el-option label="诊室4" value="诊室4"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="120">
<template #default="scope">
<el-time-picker
v-model="scope.row.startTime"
type="time"
format="HH:mm"
value-format="HH:mm"
placeholder="选择开始时间"
:disabled="!isEditMode"
class="time-picker"
/>
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" width="120">
<template #default="scope">
<el-time-picker
v-model="scope.row.endTime"
type="time"
format="HH:mm"
value-format="HH:mm"
placeholder="选择结束时间"
:disabled="!isEditMode"
class="time-picker"
/>
</template>
</el-table-column>
<el-table-column prop="maxNumber" label="限号数量" width="80">
<template #default="scope">
<el-input
v-model="scope.row.maxNumber"
type="number"
:disabled="!isEditMode"
/>
</template>
</el-table-column>
<el-table-column prop="record" label="号源记录" width="80">
<template #default="scope">
<el-icon
@click="handleViewRecord(scope.row)"
class="record-icon"
title="查看号源记录"
>
<View />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="appointmentItem" label="挂号项目" width="120">
<template #default="scope">
<el-select
v-model="scope.row.appointmentItem"
placeholder="请选"
class="inline-select"
:disabled="!isEditMode"
@change="handleAppointmentItemChange(scope.row)"
>
<el-option label="挂号费 50" value="挂号费 50"></el-option>
<el-option label="一般诊疗费 10" value="一般诊疗费 10"></el-option>
<el-option label="主任医师 27" value="主任医师 27"></el-option>
<el-option label="副主任 15" value="副主任 15"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="registrationFee" label="挂号费(元)" width="100">
<template #default="scope">
<span>{{ scope.row.registrationFee || '0' }}</span>
</template>
</el-table-column>
<el-table-column prop="clinicItem" label="诊查项目" width="150">
<template #default="scope">
<el-select
v-model="scope.row.clinicItem"
placeholder="请选择诊查项目"
class="inline-select"
:disabled="!isEditMode"
@change="handleClinicItemChange(scope.row)"
>
<el-option label="常规诊查" value="常规诊查"></el-option>
<el-option label="专科诊查" value="专科诊查"></el-option>
<el-option label="特殊诊查" value="特殊诊查"></el-option>
<el-option label="专家诊查" value="专家诊查"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="treatmentFee" label="诊疗费(元)" width="100">
<template #default="scope">
<span>{{ scope.row.treatmentFee || '0' }}</span>
</template>
</el-table-column>
<el-table-column prop="online" label="线上" width="60">
<template #default="scope">
<el-checkbox v-model="scope.row.online" :disabled="!isEditMode"></el-checkbox>
</template>
</el-table-column>
<el-table-column prop="stopClinic" label="停诊" width="60">
<template #default="scope">
<el-checkbox v-model="scope.row.stopClinic" :disabled="!isEditMode"></el-checkbox>
</template>
</el-table-column>
<el-table-column prop="stopReason" label="停诊原因" width="150">
<template #default="scope">
<el-input
v-model="scope.row.stopReason"
:placeholder="scope.row.stopClinic ? '请输入停诊原因' : ''"
class="inline-input"
:disabled="!isEditMode || !scope.row.stopClinic"
></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleAddSchedule(scope.row)"
:disabled="!isEditMode"
>
添加
</el-button>
<el-button
type="danger"
size="small"
@click="handleDeleteSchedule(scope.row)"
:disabled="!isEditMode"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="bottom-buttons">
<el-button type="primary" @click="handleSave" :disabled="!isEditMode">确定</el-button>
<el-button @click="handleCancel">取消</el-button>
</div>
</div>
<!-- 号源记录对话框 -->
<el-dialog
v-model="recordDialogVisible"
title="号源记录"
width="30%"
:close-on-click-modal="true"
>
<div class="appointment-records">
<div class="record-item" v-for="record in appointmentRecords" :key="record.index">
<span class="record-time">{{ record.time }}</span>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="recordDialogVisible = false">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="DoctorSchedule">
import {computed, onMounted, ref, watch} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {ElDialog, ElMessage, ElMessageBox} from 'element-plus'
import {View} from '@element-plus/icons-vue'
import {addDoctorSchedule, addDoctorScheduleWithDate, deleteDoctorSchedule} from '../api'
// 路由和导航
const route = useRoute()
const router = useRouter()
// 筛选参数
const filterParams = ref({
orgName: '演示医院',
deptName: '测试内科',
startDate: new Date('2025-12-01'),
appointmentType: '普通'
})
// 医生列表数据
const doctorOptions = ref({
'普通': [
{ label: '张医生', value: '张医生' },
{ label: '李医生', value: '李医生' },
{ label: '王医生', value: '王医生' },
{ label: '赵医生', value: '赵医生' }
],
'专家': [
{ label: '请选择专家', value: '' },
{ label: '王教授', value: '王教授' },
{ label: '李主任', value: '李主任' },
{ label: '赵教授', value: '赵教授' },
{ label: '张主任', value: '张主任' }
]
})
// 根据排班类型获取医生选项
const getDoctorOptions = (appointmentType) => {
return doctorOptions.value[appointmentType] || doctorOptions.value['普通']
}
// 编辑模式控制
const isEditMode = computed(() => {
return route.query.mode === 'edit'
})
// 排班列表数据
const scheduleList = ref([])
// 当前科室信息
const currentDept = ref(null)
// 按日期分组的排班数据
const groupedSchedule = computed(() => {
// 按日期分组
const groups = {}
scheduleList.value.forEach(item => {
if (!groups[item.date]) {
groups[item.date] = {
date: item.date,
weekday: item.weekday,
items: []
}
}
groups[item.date].items.push(item)
})
// 转换为数组
return Object.values(groups)
})
// 生成一周排班数据
const generateWeekSchedule = (startDate) => {
const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const timeSlots = [
{ label: '上午', startTime: '08:00', endTime: '12:00' },
{ label: '下午', startTime: '14:30', endTime: '18:00' }
]
const schedule = []
// 生成一周7天的数据
for (let i = 0; i < 7; i++) {
const currentDate = new Date(startDate)
currentDate.setDate(startDate.getDate() + i)
const dateStr = currentDate.toISOString().split('T')[0]
const weekday = days[currentDate.getDay()]
// 每个时间段生成一条记录
timeSlots.forEach(slot => {
schedule.push({
id: `${dateStr}-${slot.label}`,
date: dateStr,
weekday: weekday,
timeSlot: slot.label,
startTime: slot.startTime,
endTime: slot.endTime,
doctorName: '',
room: '',
maxNumber: '',
appointmentItem: '',
registrationFee: 0,
clinicItem: '',
treatmentFee: 0,
online: true,
stopClinic: false,
stopReason: ''
})
})
}
return schedule
}
// 初始化数据
const initData = () => {
// 从路由参数获取科室ID
const deptId = route.query.deptId
console.log('科室ID:', deptId)
// 设置当前科室信息
if (deptId) {
currentDept.value = { id: deptId }
}
// 生成排班数据
scheduleList.value = generateWeekSchedule(filterParams.value.startDate)
}
// 排班类型变化处理
const handleAppointmentTypeChange = () => {
// 当排班类型改变时,清空所有排班记录的医生选择
scheduleList.value.forEach(item => {
// 如果当前选择的医生不在新类型对应的医生列表中,则清空
const currentDoctors = getDoctorOptions(filterParams.value.appointmentType)
const doctorExists = currentDoctors.some(doctor => doctor.value === item.doctorName)
if (!doctorExists) {
item.doctorName = ''
}
})
}
// 挂号项目变化处理
const handleAppointmentItemChange = (row) => {
if (row.appointmentItem) {
// 从挂号项目中提取费用数字,例如 "挂号费 50" -> 50
const feeMatch = row.appointmentItem.match(/(\d+)/)
if (feeMatch) {
row.registrationFee = parseInt(feeMatch[1])
} else {
row.registrationFee = 0
}
} else {
row.registrationFee = 0
}
}
// 诊查项目变化处理
const handleClinicItemChange = (row) => {
// 诊查项目费用映射表
const clinicFeeMap = {
'常规诊查': 10,
'专科诊查': 20,
'特殊诊查': 30,
'专家诊查': 50
}
if (row.clinicItem && clinicFeeMap[row.clinicItem]) {
row.treatmentFee = clinicFeeMap[row.clinicItem]
} else {
row.treatmentFee = 0
}
}
// 添加排班
const handleAddSchedule = (row) => {
// 创建新的排班记录,基于当前行的日期和时段
const newSchedule = {
id: `new-${Date.now()}-${Math.random()}`,
date: row.date,
weekday: row.weekday,
timeSlot: row.timeSlot,
startTime: row.startTime || '08:00',
endTime: row.endTime || '12:00',
doctorName: '',
room: '',
maxNumber: '',
appointmentItem: '',
registrationFee: '',
clinicItem: '',
treatmentFee: '',
online: true,
stopClinic: false,
stopReason: '',
isNew: true // 标记为新添加的记录
}
// 找到当前行在列表中的位置,在其后插入新记录
const currentIndex = scheduleList.value.findIndex(item => item.id === row.id)
if (currentIndex !== -1) {
scheduleList.value.splice(currentIndex + 1, 0, newSchedule)
ElMessage.success('添加排班成功')
} else {
// 如果找不到,添加到对应日期组的末尾
scheduleList.value.push(newSchedule)
ElMessage.success('添加排班成功')
}
}
// 删除排班
const handleDeleteSchedule = (row) => {
ElMessageBox.confirm('确定要删除这条排班记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 如果是已保存的记录有后端ID调用删除接口
if (row.backendId && !row.isNew) {
deleteDoctorSchedule(row.backendId).then(res => {
if (res.code === 200) {
// 从列表中移除
const index = scheduleList.value.findIndex(item => item.id === row.id)
if (index !== -1) {
scheduleList.value.splice(index, 1)
}
ElMessage.success('删除成功')
} else {
ElMessage.error(res.msg || '删除失败')
}
}).catch(error => {
console.error('删除排班失败:', error)
ElMessage.error('删除失败,请稍后重试')
})
} else {
// 如果是新添加的记录(未保存),直接从列表中移除
const index = scheduleList.value.findIndex(item => item.id === row.id)
if (index !== -1) {
scheduleList.value.splice(index, 1)
ElMessage.success('删除成功')
}
}
}).catch(() => {
// 用户取消删除
})
}
// 号源记录对话框相关
const recordDialogVisible = ref(false)
const currentRow = ref(null)
const appointmentRecords = ref([])
// 计算号源记录
const calculateAppointmentRecords = (row) => {
const { startTime, endTime, maxNumber } = row
// 将时间转换为分钟数
const [startHour, startMinute] = startTime.split(':').map(Number)
const [endHour, endMinute] = endTime.split(':').map(Number)
const startTotalMinutes = startHour * 60 + startMinute
const endTotalMinutes = endHour * 60 + endMinute
// 计算总时长和间隔
const totalDuration = endTotalMinutes - startTotalMinutes
const interval = Math.floor(totalDuration / maxNumber)
// 生成号源记录
const records = []
for (let i = 0; i < maxNumber; i++) {
const minutes = startTotalMinutes + i * interval
const hour = Math.floor(minutes / 60).toString().padStart(2, '0')
const minute = (minutes % 60).toString().padStart(2, '0')
records.push({
index: i + 1,
time: `${hour}:${minute}`
})
}
return records
}
// 查看号源记录
const handleViewRecord = (row) => {
// 验证开始时间、结束时间和限号数量
if (!row.startTime || !row.endTime || !row.maxNumber) {
ElMessageBox.confirm('请先设置开始时间、结束时间和限号数量', '', {
confirmButtonText: '确定',
type: 'warning',
showCancelButton: false
})
return
}
// 计算号源记录
currentRow.value = row
appointmentRecords.value = calculateAppointmentRecords(row)
recordDialogVisible.value = true
}
// 保存排班
const handleSave = async () => {
try {
// 验证必填字段
const invalidSchedules = scheduleList.value.filter(item => {
// 检查是否有必填字段为空
return !item.doctorName || !item.room || !item.startTime || !item.endTime || !item.maxNumber
})
if (invalidSchedules.length > 0) {
ElMessage.warning('请完善所有排班记录的必填信息(医生、诊室、开始时间、结束时间、限号数量)')
return
}
// 转换数据格式以匹配后端实体,并建立映射关系
const schedulesToSave = scheduleList.value.map((item, index) => {
// 解析挂号项目和诊查项目,提取费用
let registrationFee = 0
let diagnosisFee = 0
if (item.appointmentItem) {
const feeMatch = item.appointmentItem.match(/(\d+)/)
if (feeMatch) {
registrationFee = parseInt(feeMatch[1])
}
}
if (item.clinicItem) {
// 这里可以根据诊查项目设置诊疗费暂时设为0或从其他字段获取
diagnosisFee = item.treatmentFee ? parseInt(item.treatmentFee) : 0
}
return {
localIndex: index, // 保存本地索引,用于更新
localId: item.id, // 保存本地ID
scheduleData: {
id: item.backendId || null, // 如果是新记录id为null
weekday: item.weekday,
timePeriod: item.timeSlot,
doctor: item.doctorName,
clinic: item.room,
startTime: item.startTime,
endTime: item.endTime,
limitNumber: parseInt(item.maxNumber) || 0,
registerItem: item.appointmentItem || '',
registerFee: registrationFee,
diagnosisItem: item.clinicItem || '',
diagnosisFee: diagnosisFee,
isOnline: item.online || false,
isStopped: item.stopClinic || false,
stopReason: item.stopClinic ? (item.stopReason || '') : '',
deptId: currentDept.value?.id || route.query.deptId || null,
scheduledDate: item.date // 添加具体日期字段
}
}
})
// 只保存新添加的记录isNew: true或没有backendId的记录
const newSchedulesToSave = schedulesToSave.filter(({ localIndex }) => {
const item = scheduleList.value[localIndex]
return item.isNew || !item.backendId // 只保存新记录或没有backendId的记录
})
if (newSchedulesToSave.length === 0) {
ElMessage.info('没有需要保存的新排班记录')
// 返回上一页
router.back()
return
}
// 批量保存排班
let successCount = 0
let failCount = 0
const errors = []
for (const { localIndex, localId, scheduleData } of newSchedulesToSave) {
try {
// 确保新记录的id为null
scheduleData.id = null
// 使用带日期的API日期信息已包含在scheduleData中
const res = await addDoctorScheduleWithDate(scheduleData)
if (res.code === 200 && res.data) {
successCount++
// 更新本地记录的backendId
const localItem = scheduleList.value[localIndex]
if (localItem) {
// 从响应中获取保存后的ID
if (res.data && typeof res.data === 'object' && res.data.id) {
localItem.backendId = res.data.id
localItem.isNew = false
} else if (res.data && typeof res.data === 'number') {
localItem.backendId = res.data
localItem.isNew = false
} else if (res.data === true) {
// 如果后端返回的是boolean true标记为已保存
localItem.isNew = false
localItem.saved = true
} else {
// 其他情况,标记为已保存
localItem.isNew = false
}
}
} else {
failCount++
const errorMsg = res.msg || '保存失败'
errors.push(errorMsg)
console.error('保存排班失败:', errorMsg, res)
}
} catch (error) {
failCount++
const errorMsg = error.message || '保存异常'
errors.push(errorMsg)
console.error('保存排班异常:', error)
}
}
// 如果有错误,显示详细错误信息
if (errors.length > 0) {
console.error('保存错误详情:', errors)
}
if (failCount === 0) {
ElMessage.success(`排班保存成功,共保存 ${successCount} 条记录`)
// 返回上一页
router.back()
} else {
ElMessage.warning(`部分排班保存失败,成功 ${successCount} 条,失败 ${failCount}`)
if (errors.length > 0) {
console.error('错误详情:', errors.join('; '))
}
}
} catch (error) {
console.error('保存排班失败:', error)
ElMessage.error('保存失败,请稍后重试')
}
}
// 取消操作
const handleCancel = () => {
// 返回上一页
router.back()
}
// 页面加载时初始化数据
onMounted(() => {
initData()
})
// 监听路由参数变化,重新初始化数据
watch(
() => [route.query.deptId, route.query.mode],
() => {
initData()
},
{ deep: true }
)
</script>
<style scoped lang="scss">
.doctorschedule-container {
width: 100%;
height: 100%;
padding: 20px;
background-color: #f5f7fa;
}
.doctorschedule-header {
margin-bottom: 20px;
}
.doctorschedule-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
}
.doctorschedule-content {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.filter-condition {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 16px;
}
.filter-label {
font-weight: 500;
white-space: nowrap;
}
.filter-select {
width: 150px;
}
.filter-date-picker {
width: 200px;
}
.radio-group {
display: flex;
align-items: center;
gap: 10px;
}
.schedule-table-container {
margin-bottom: 20px;
width: 100%;
overflow-x: auto;
}
.daily-schedule {
margin-bottom: 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
width: 100%;
}
.daily-header {
background-color: #f5f7fa;
padding: 10px 15px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
gap: 15px;
font-size: 16px;
font-weight: 600;
}
.date-text {
color: #333;
}
.weekday-text {
color: #606266;
font-weight: normal;
}
.schedule-table {
width: 100% !important;
min-width: 100% !important;
:deep(.el-table__header-wrapper) {
width: 100% !important;
border-top: none;
}
:deep(.el-table__body-wrapper) {
width: 100% !important;
}
:deep(.el-table__header-wrapper th.el-table__cell),
:deep(.el-table__body-wrapper td.el-table__cell) {
text-align: center;
padding: 8px 0;
}
/* 确保表格容器填满 */
:deep(.el-table__inner-wrapper) {
width: 100% !important;
}
/* 确保表格本身填满 */
:deep(.el-table__body) {
width: 100% !important;
}
/* 确保表格列正确分配宽度 */
:deep(.el-table__header) {
width: 100% !important;
}
:deep(.el-table__header tr),
:deep(.el-table__body tr) {
width: 100% !important;
}
/* 确保表格容器的最小宽度与内容匹配 */
:deep(.el-table) {
width: 100% !important;
min-width: 100% !important;
}
}
.inline-select {
width: 100%;
}
.inline-input {
width: 100%;
}
.time-picker {
width: 100%;
}
.record-icon {
font-size: 18px;
color: #409eff;
cursor: pointer;
transition: color 0.2s;
}
.record-icon:hover {
color: #66b1ff;
}
.bottom-buttons {
display: flex;
justify-content: flex-end;
gap: 16px;
}
/* 号源记录对话框样式 */
.appointment-records {
max-height: 300px;
overflow-y: auto;
padding: 10px 0;
}
.record-item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.record-item:last-child {
border-bottom: none;
}
.record-time {
font-size: 16px;
color: #333;
}
.dialog-footer {
text-align: center;
}
/* 隐藏数字输入框的增减按钮 */
:deep(.el-input__inner[type="number"]) {
appearance: textfield;
-moz-appearance: textfield;
}
:deep(.el-input__inner[type="number"])::-webkit-outer-spin-button,
:deep(.el-input__inner[type="number"])::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none;
margin: 0;
}
</style>