702 lines
23 KiB
Vue
702 lines
23 KiB
Vue
<template>
|
||
<div class="doctor-roster p-4">
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-lg font-medium">医生排班管理</span>
|
||
<div>
|
||
<el-button type="success" @click="showBatchDialog">批量排班</el-button>
|
||
<el-button @click="prevWeek">上一周</el-button>
|
||
<el-button @click="nextWeek">下一周</el-button>
|
||
<el-button type="primary" @click="goToday">本周</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="mb-4">
|
||
<el-form inline>
|
||
<el-form-item label="医生">
|
||
<el-input v-model="searchName" placeholder="请输入医生姓名" clearable style="width: 200px" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="loadData">查询</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<el-alert type="info" :closable="false" class="mb-4" show-icon>
|
||
每条排班为一段连续可挂号时间;同一天可添加多段(如白班 + 夜班)。休息时段不添加出诊段即可。号源间隔默认 15 分钟;号源数填 0 表示按时段长度自动满号。
|
||
</el-alert>
|
||
|
||
<div class="text-center mb-4 text-gray-600">
|
||
{{ weekText }}
|
||
</div>
|
||
|
||
<el-table :data="filteredTableData" border v-loading="loading">
|
||
<el-table-column prop="doctorName" label="医生" width="120" fixed />
|
||
<el-table-column v-for="day in weekDays" :key="day.date" :label="day.label" min-width="168">
|
||
<template #default="{ row }">
|
||
<div
|
||
class="day-cell rounded border border-dashed border-gray-200 p-1 min-h-[56px] cursor-pointer hover:bg-gray-50"
|
||
@click="openDayDialog(row, day.date)"
|
||
>
|
||
<template v-if="getDaySegments(row, day.date).length === 0">
|
||
<div class="text-xs text-gray-400 text-center py-2">点击管理</div>
|
||
</template>
|
||
<div v-else class="space-y-1">
|
||
<div
|
||
v-for="seg in getDaySegments(row, day.date)"
|
||
:key="seg.id"
|
||
class="text-xs px-1 py-0.5 rounded leading-tight"
|
||
:class="segmentChipClass(seg.status)"
|
||
>
|
||
{{ formatSegmentLine(seg) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<!-- 某日排班列表 -->
|
||
<el-dialog v-model="dayDialogVisible" :title="dayDialogTitle" width="720px" destroy-on-close>
|
||
<el-table :data="dayDialogSegments" border size="small" max-height="360">
|
||
<el-table-column label="时间" width="150">
|
||
<template #default="{ row }">{{ row.start_time }} - {{ row.end_time }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="班次" width="80">
|
||
<template #default="{ row }">{{ shiftLabel(row.shift_type) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="88">
|
||
<template #default="{ row }">{{ statusText(row.status) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="间隔" width="72">
|
||
<template #default="{ row }">{{ row.slot_minutes || 15 }} 分</template>
|
||
</el-table-column>
|
||
<el-table-column label="号源" width="72">
|
||
<template #default="{ row }">{{ row.status === 1 ? row.quota || '不限' : '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="openSegmentForm(row)">编辑</el-button>
|
||
<el-button type="danger" link @click="removeSegment(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button @click="dayDialogVisible = false">关闭</el-button>
|
||
<el-button type="primary" @click="openSegmentForm()">添加时段</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 单段排班表单 -->
|
||
<el-dialog v-model="segmentDialogVisible" :title="segmentForm.id ? '编辑时段' : '添加时段'" width="520px" destroy-on-close>
|
||
<el-form :model="segmentForm" label-width="110px">
|
||
<el-form-item label="接诊时间" required>
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<el-time-select
|
||
v-model="segmentForm.start_time"
|
||
start="00:00"
|
||
step="00:5"
|
||
end="23:45"
|
||
placeholder="开始"
|
||
style="width: 120px"
|
||
/>
|
||
<span>至</span>
|
||
<el-time-select
|
||
v-model="segmentForm.end_time"
|
||
start="00:00"
|
||
step="00:5"
|
||
end="23:45"
|
||
placeholder="结束"
|
||
style="width: 120px"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="班次标签">
|
||
<el-select v-model="segmentForm.shift_type" placeholder="选填" clearable style="width: 200px">
|
||
<el-option label="白班" value="day" />
|
||
<el-option label="夜班" value="night" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="号源间隔(分)">
|
||
<el-input-number v-model="segmentForm.slot_minutes" :min="5" :max="120" :step="5" />
|
||
<span class="ml-2 text-gray-500 text-sm">默认 15</span>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-radio-group v-model="segmentForm.status">
|
||
<el-radio :label="1">出诊</el-radio>
|
||
<el-radio :label="2">停诊</el-radio>
|
||
<el-radio :label="3">休息</el-radio>
|
||
<el-radio :label="4">请假</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="号源上限" v-if="segmentForm.status === 1">
|
||
<el-input-number v-model="segmentForm.quota" :min="0" :max="200" />
|
||
<div class="text-gray-500 text-xs mt-1">0 表示不限制(由时段÷间隔计算可约数)</div>
|
||
</el-form-item>
|
||
<el-form-item label="最大接诊" v-if="segmentForm.status === 1">
|
||
<el-input-number v-model="segmentForm.max_patients" :min="0" :max="500" />
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="segmentForm.remark" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="segmentDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="segmentSaving" @click="saveSegment">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 批量排班 -->
|
||
<el-dialog v-model="batchDialogVisible" title="批量排班" width="780px" destroy-on-close>
|
||
<el-form :model="batchForm" label-width="100px">
|
||
<el-form-item label="选择医生" required>
|
||
<el-select v-model="batchForm.doctorIds" multiple placeholder="请选择医生" class="w-full" filterable>
|
||
<el-option v-for="doctor in tableData" :key="doctor.doctorId" :label="doctor.doctorName" :value="doctor.doctorId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="日期范围" required>
|
||
<el-date-picker
|
||
v-model="batchForm.dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="星期" required>
|
||
<el-checkbox-group v-model="batchForm.weekdays">
|
||
<el-checkbox :label="1">周一</el-checkbox>
|
||
<el-checkbox :label="2">周二</el-checkbox>
|
||
<el-checkbox :label="3">周三</el-checkbox>
|
||
<el-checkbox :label="4">周四</el-checkbox>
|
||
<el-checkbox :label="5">周五</el-checkbox>
|
||
<el-checkbox :label="6">周六</el-checkbox>
|
||
<el-checkbox :label="0">周日</el-checkbox>
|
||
</el-checkbox-group>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="时段模板">
|
||
<div class="w-full space-y-2">
|
||
<div v-for="(seg, idx) in batchForm.segments" :key="idx" class="flex flex-wrap items-center gap-2 p-2 bg-gray-50 rounded">
|
||
<el-time-select v-model="seg.start_time" start="00:00" step="00:05" end="23:45" placeholder="开始" style="width: 110px" />
|
||
<span>—</span>
|
||
<el-time-select v-model="seg.end_time" start="00:00" step="00:05" end="23:45" placeholder="结束" style="width: 110px" />
|
||
<el-select v-model="seg.shift_type" placeholder="班次" clearable style="width: 100px">
|
||
<el-option label="白班" value="day" />
|
||
<el-option label="夜班" value="night" />
|
||
</el-select>
|
||
<span class="text-sm text-gray-500">间隔</span>
|
||
<el-input-number v-model="seg.slot_minutes" :min="5" :max="120" :step="5" style="width: 100px" />
|
||
<span class="text-sm text-gray-500">号源0=不限</span>
|
||
<el-input-number v-model="seg.quota" :min="0" :max="200" style="width: 100px" />
|
||
<el-button type="danger" link @click="batchRemoveSegment(idx)" :disabled="batchForm.segments.length <= 1">删除</el-button>
|
||
</div>
|
||
<el-button type="primary" link @click="batchAddSegment">+ 增加一段</el-button>
|
||
<el-button type="success" link @click="batchAddNightTemplate">+ 夜班 18:00–22:00</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="排班状态" required>
|
||
<el-radio-group v-model="batchForm.status">
|
||
<el-radio :label="1">出诊</el-radio>
|
||
<el-radio :label="2">停诊</el-radio>
|
||
<el-radio :label="3">休息</el-radio>
|
||
<el-radio :label="4">请假</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="备注">
|
||
<el-input v-model="batchForm.remark" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="batchDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleBatchSave" :loading="batchSaving">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="doctorRoster">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import dayjs from 'dayjs'
|
||
import isoWeek from 'dayjs/plugin/isoWeek'
|
||
import feedback from '@/utils/feedback'
|
||
import { doctorLists, rosterLists, rosterSave, rosterDelete, rosterBatchSave } from '@/api/doctor'
|
||
import { ElMessageBox } from 'element-plus'
|
||
|
||
dayjs.extend(isoWeek)
|
||
|
||
interface RosterSegment {
|
||
id: number
|
||
status: number
|
||
quota: number
|
||
max_patients?: number
|
||
start_time: string
|
||
end_time: string
|
||
shift_type?: string | null
|
||
slot_minutes?: number
|
||
remark?: string
|
||
period?: string
|
||
}
|
||
|
||
interface DoctorRow {
|
||
doctorId: number
|
||
doctorName: string
|
||
/** date -> segments */
|
||
rostersByDate: Record<string, RosterSegment[]>
|
||
}
|
||
|
||
const searchName = ref('')
|
||
const currentWeekStart = ref(dayjs().startOf('isoWeek'))
|
||
const loading = ref(false)
|
||
|
||
const dayDialogVisible = ref(false)
|
||
const dayDialogDoctor = ref<DoctorRow | null>(null)
|
||
const dayDialogDate = ref('')
|
||
const dayDialogSegments = ref<RosterSegment[]>([])
|
||
|
||
const segmentDialogVisible = ref(false)
|
||
const segmentSaving = ref(false)
|
||
const segmentForm = ref({
|
||
id: null as number | null,
|
||
doctorId: 0,
|
||
date: '',
|
||
start_time: '09:00',
|
||
end_time: '12:00',
|
||
shift_type: '' as string,
|
||
slot_minutes: 15,
|
||
status: 1,
|
||
quota: 0,
|
||
max_patients: 0,
|
||
remark: ''
|
||
})
|
||
|
||
const batchDialogVisible = ref(false)
|
||
const batchSaving = ref(false)
|
||
|
||
interface BatchSegTpl {
|
||
start_time: string
|
||
end_time: string
|
||
shift_type: string
|
||
slot_minutes: number
|
||
quota: number
|
||
}
|
||
|
||
const defaultBatchSegments = (): BatchSegTpl[] => [
|
||
{ start_time: '09:00', end_time: '12:00', shift_type: 'day', slot_minutes: 15, quota: 0 },
|
||
{ start_time: '14:00', end_time: '18:00', shift_type: 'day', slot_minutes: 15, quota: 0 }
|
||
]
|
||
|
||
const batchForm = ref({
|
||
doctorIds: [] as number[],
|
||
dateRange: [] as string[],
|
||
weekdays: [1, 2, 3, 4, 5] as number[],
|
||
status: 1,
|
||
remark: '',
|
||
segments: defaultBatchSegments()
|
||
})
|
||
|
||
const tableData = ref<DoctorRow[]>([])
|
||
|
||
const filteredTableData = computed(() => {
|
||
const q = searchName.value.trim()
|
||
if (!q) return tableData.value
|
||
return tableData.value.filter((r) => r.doctorName.includes(q))
|
||
})
|
||
|
||
const dayDialogTitle = computed(() => {
|
||
if (!dayDialogDoctor.value || !dayDialogDate.value) return '当日排班'
|
||
return `${dayDialogDoctor.value.doctorName} · ${dayDialogDate.value}`
|
||
})
|
||
|
||
const weekDays = computed(() => {
|
||
const days = []
|
||
const labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||
for (let i = 0; i < 7; i++) {
|
||
const date = currentWeekStart.value.add(i, 'day')
|
||
days.push({
|
||
date: date.format('YYYY-MM-DD'),
|
||
label: `${labels[i]} ${date.format('MM-DD')}`
|
||
})
|
||
}
|
||
return days
|
||
})
|
||
|
||
const weekText = computed(() => {
|
||
const start = currentWeekStart.value.format('YYYY-MM-DD')
|
||
const end = currentWeekStart.value.add(6, 'day').format('YYYY-MM-DD')
|
||
return `${start} 至 ${end}`
|
||
})
|
||
|
||
function statusText(s: number) {
|
||
const m: Record<number, string> = { 1: '出诊', 2: '停诊', 3: '休息', 4: '请假' }
|
||
return m[s] ?? String(s)
|
||
}
|
||
|
||
function shiftLabel(t?: string | null) {
|
||
if (t === 'day') return '白班'
|
||
if (t === 'night') return '夜班'
|
||
return '—'
|
||
}
|
||
|
||
function segmentChipClass(status: number) {
|
||
if (status === 1) return 'bg-emerald-50 text-emerald-800 border border-emerald-100'
|
||
if (status === 2) return 'bg-amber-50 text-amber-900 border border-amber-100'
|
||
if (status === 3) return 'bg-slate-100 text-slate-600 border border-slate-200'
|
||
return 'bg-gray-100 text-gray-700 border border-gray-200'
|
||
}
|
||
|
||
function formatSegmentLine(seg: RosterSegment) {
|
||
const t = `${seg.start_time}-${seg.end_time}`
|
||
const sh = shiftLabel(seg.shift_type)
|
||
const shPart = sh !== '—' ? ` ${sh}` : ''
|
||
if (seg.status === 1) {
|
||
const q = seg.quota > 0 ? ` 号源${seg.quota}` : ''
|
||
return `${t}${shPart} 出诊${q}`
|
||
}
|
||
return `${t}${shPart} ${statusText(seg.status)}`
|
||
}
|
||
|
||
function getDaySegments(row: DoctorRow, date: string): RosterSegment[] {
|
||
return row.rostersByDate[date] || []
|
||
}
|
||
|
||
function refreshDayDialogList() {
|
||
if (!dayDialogDoctor.value || !dayDialogDate.value) return
|
||
const row = tableData.value.find((r) => r.doctorId === dayDialogDoctor.value!.doctorId)
|
||
if (!row) return
|
||
dayDialogSegments.value = [...(row.rostersByDate[dayDialogDate.value] || [])].sort((a, b) =>
|
||
a.start_time.localeCompare(b.start_time)
|
||
)
|
||
}
|
||
|
||
function openDayDialog(row: DoctorRow, date: string) {
|
||
dayDialogDoctor.value = row
|
||
dayDialogDate.value = date
|
||
refreshDayDialogList()
|
||
dayDialogVisible.value = true
|
||
}
|
||
|
||
function openSegmentForm(existing?: RosterSegment) {
|
||
if (!dayDialogDoctor.value || !dayDialogDate.value) {
|
||
feedback.msgWarning('请先选择日期格子')
|
||
return
|
||
}
|
||
if (existing) {
|
||
segmentForm.value = {
|
||
id: existing.id,
|
||
doctorId: dayDialogDoctor.value.doctorId,
|
||
date: dayDialogDate.value,
|
||
start_time: existing.start_time || '09:00',
|
||
end_time: existing.end_time || '12:00',
|
||
shift_type: existing.shift_type || '',
|
||
slot_minutes: existing.slot_minutes || 15,
|
||
status: existing.status,
|
||
quota: existing.quota ?? 0,
|
||
max_patients: existing.max_patients ?? 0,
|
||
remark: existing.remark || ''
|
||
}
|
||
} else {
|
||
segmentForm.value = {
|
||
id: null,
|
||
doctorId: dayDialogDoctor.value.doctorId,
|
||
date: dayDialogDate.value,
|
||
start_time: '09:00',
|
||
end_time: '12:00',
|
||
shift_type: 'day',
|
||
slot_minutes: 15,
|
||
status: 1,
|
||
quota: 0,
|
||
max_patients: 0,
|
||
remark: ''
|
||
}
|
||
}
|
||
segmentDialogVisible.value = true
|
||
}
|
||
|
||
async function saveSegment() {
|
||
const f = segmentForm.value
|
||
if (!f.start_time || !f.end_time) {
|
||
feedback.msgWarning('请填写接诊开始与结束时间')
|
||
return
|
||
}
|
||
if (f.status === 1 && f.start_time >= f.end_time) {
|
||
feedback.msgWarning('出诊时结束时间须晚于开始时间')
|
||
return
|
||
}
|
||
if (f.start_time >= f.end_time) {
|
||
feedback.msgWarning('结束时间须晚于开始时间')
|
||
return
|
||
}
|
||
|
||
segmentSaving.value = true
|
||
try {
|
||
const payload: Record<string, unknown> = {
|
||
doctor_id: f.doctorId,
|
||
date: f.date,
|
||
period: 'segment',
|
||
start_time: f.start_time,
|
||
end_time: f.end_time,
|
||
slot_minutes: f.slot_minutes || 15,
|
||
status: f.status,
|
||
quota: f.status === 1 ? f.quota : 0,
|
||
max_patients: f.status === 1 ? f.max_patients : 0,
|
||
remark: f.remark || ''
|
||
}
|
||
if (f.shift_type) {
|
||
payload.shift_type = f.shift_type
|
||
}
|
||
if (f.id) {
|
||
payload.id = f.id
|
||
}
|
||
|
||
await rosterSave(payload)
|
||
feedback.msgSuccess('保存成功')
|
||
segmentDialogVisible.value = false
|
||
await loadRosterData()
|
||
refreshDayDialogList()
|
||
} catch {
|
||
feedback.msgError('保存失败')
|
||
} finally {
|
||
segmentSaving.value = false
|
||
}
|
||
}
|
||
|
||
async function removeSegment(row: RosterSegment) {
|
||
try {
|
||
await ElMessageBox.confirm('确定删除该时段?', '提示', { type: 'warning' })
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
await rosterDelete({ id: row.id })
|
||
feedback.msgSuccess('已删除')
|
||
await loadRosterData()
|
||
refreshDayDialogList()
|
||
} catch {
|
||
feedback.msgError('删除失败')
|
||
}
|
||
}
|
||
|
||
const loadDoctors = async () => {
|
||
try {
|
||
const res = await doctorLists({
|
||
page_no: 1,
|
||
page_size: 1000
|
||
})
|
||
tableData.value = (res?.lists || []).map((doctor: any) => ({
|
||
doctorId: doctor.id,
|
||
doctorName: doctor.name || doctor.account,
|
||
rostersByDate: {} as Record<string, RosterSegment[]>
|
||
}))
|
||
await loadRosterData()
|
||
} catch (error) {
|
||
console.error('加载医生列表失败:', error)
|
||
feedback.msgError('加载医生列表失败')
|
||
}
|
||
}
|
||
|
||
const loadRosterData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
start_date: currentWeekStart.value.format('YYYY-MM-DD'),
|
||
end_date: currentWeekStart.value.add(6, 'day').format('YYYY-MM-DD')
|
||
}
|
||
const res = await rosterLists(params)
|
||
const rosters = res?.lists || []
|
||
|
||
tableData.value.forEach((doctor) => {
|
||
doctor.rostersByDate = {}
|
||
rosters.forEach((roster: any) => {
|
||
if (roster.doctor_id !== doctor.doctorId) return
|
||
const d = roster.date
|
||
if (!doctor.rostersByDate[d]) doctor.rostersByDate[d] = []
|
||
doctor.rostersByDate[d].push({
|
||
id: roster.id,
|
||
status: roster.status,
|
||
quota: roster.quota || 0,
|
||
max_patients: roster.max_patients,
|
||
start_time: roster.start_time || legacyStart(roster.period),
|
||
end_time: roster.end_time || legacyEnd(roster.period),
|
||
shift_type: roster.shift_type,
|
||
slot_minutes: roster.slot_minutes ?? 15,
|
||
remark: roster.remark,
|
||
period: roster.period
|
||
})
|
||
})
|
||
})
|
||
refreshDayDialogList()
|
||
} catch (error) {
|
||
console.error('加载排班数据失败:', error)
|
||
feedback.msgError('加载排班数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function legacyStart(period: string) {
|
||
if (period === 'morning') return '09:00'
|
||
if (period === 'afternoon') return '14:00'
|
||
if (period === 'night') return '18:00'
|
||
return '09:00'
|
||
}
|
||
|
||
function legacyEnd(period: string) {
|
||
if (period === 'morning') return '12:00'
|
||
if (period === 'afternoon') return '18:00'
|
||
if (period === 'night') return '22:00'
|
||
return '12:00'
|
||
}
|
||
|
||
const loadData = () => {
|
||
loadRosterData()
|
||
}
|
||
|
||
const prevWeek = () => {
|
||
currentWeekStart.value = currentWeekStart.value.subtract(1, 'week')
|
||
loadRosterData()
|
||
}
|
||
|
||
const nextWeek = () => {
|
||
currentWeekStart.value = currentWeekStart.value.add(1, 'week')
|
||
loadRosterData()
|
||
}
|
||
|
||
const goToday = () => {
|
||
currentWeekStart.value = dayjs().startOf('isoWeek')
|
||
loadRosterData()
|
||
}
|
||
|
||
const showBatchDialog = () => {
|
||
batchForm.value = {
|
||
doctorIds: [],
|
||
dateRange: [],
|
||
weekdays: [1, 2, 3, 4, 5],
|
||
status: 1,
|
||
remark: '',
|
||
segments: defaultBatchSegments()
|
||
}
|
||
batchDialogVisible.value = true
|
||
}
|
||
|
||
const batchAddSegment = () => {
|
||
batchForm.value.segments.push({
|
||
start_time: '09:00',
|
||
end_time: '12:00',
|
||
shift_type: 'day',
|
||
slot_minutes: 15,
|
||
quota: 0
|
||
})
|
||
}
|
||
|
||
const batchAddNightTemplate = () => {
|
||
batchForm.value.segments.push({
|
||
start_time: '18:00',
|
||
end_time: '22:00',
|
||
shift_type: 'night',
|
||
slot_minutes: 15,
|
||
quota: 0
|
||
})
|
||
}
|
||
|
||
const batchRemoveSegment = (idx: number) => {
|
||
if (batchForm.value.segments.length <= 1) return
|
||
batchForm.value.segments.splice(idx, 1)
|
||
}
|
||
|
||
const handleBatchSave = async () => {
|
||
if (batchForm.value.doctorIds.length === 0) {
|
||
feedback.msgWarning('请选择医生')
|
||
return
|
||
}
|
||
if (!batchForm.value.dateRange || batchForm.value.dateRange.length !== 2) {
|
||
feedback.msgWarning('请选择日期范围')
|
||
return
|
||
}
|
||
if (batchForm.value.weekdays.length === 0) {
|
||
feedback.msgWarning('请选择星期')
|
||
return
|
||
}
|
||
for (const seg of batchForm.value.segments) {
|
||
if (!seg.start_time || !seg.end_time) {
|
||
feedback.msgWarning('请完善时段模板的开始与结束时间')
|
||
return
|
||
}
|
||
if (seg.start_time >= seg.end_time) {
|
||
feedback.msgWarning('时段模板中结束时间须晚于开始时间')
|
||
return
|
||
}
|
||
}
|
||
|
||
batchSaving.value = true
|
||
try {
|
||
const rosters: any[] = []
|
||
const startDate = dayjs(batchForm.value.dateRange[0])
|
||
const endDate = dayjs(batchForm.value.dateRange[1])
|
||
let currentDate = startDate
|
||
while (currentDate.isBefore(endDate) || currentDate.isSame(endDate, 'day')) {
|
||
const weekday = currentDate.day()
|
||
if (batchForm.value.weekdays.includes(weekday)) {
|
||
batchForm.value.doctorIds.forEach((doctorId) => {
|
||
batchForm.value.segments.forEach((seg) => {
|
||
const row: any = {
|
||
doctor_id: doctorId,
|
||
date: currentDate.format('YYYY-MM-DD'),
|
||
period: 'segment',
|
||
start_time: seg.start_time,
|
||
end_time: seg.end_time,
|
||
slot_minutes: seg.slot_minutes || 15,
|
||
status: batchForm.value.status,
|
||
quota: batchForm.value.status === 1 ? seg.quota : 0,
|
||
max_patients: 0,
|
||
remark: batchForm.value.remark
|
||
}
|
||
if (seg.shift_type) {
|
||
row.shift_type = seg.shift_type
|
||
}
|
||
rosters.push(row)
|
||
})
|
||
})
|
||
}
|
||
currentDate = currentDate.add(1, 'day')
|
||
}
|
||
|
||
if (rosters.length === 0) {
|
||
feedback.msgWarning('没有符合条件的排班数据')
|
||
return
|
||
}
|
||
|
||
await rosterBatchSave({ rosters })
|
||
feedback.msgSuccess(`批量保存成功!共处理 ${rosters.length} 条记录`)
|
||
batchDialogVisible.value = false
|
||
await loadRosterData()
|
||
} catch (error) {
|
||
console.error('批量保存失败:', error)
|
||
feedback.msgError('批量保存失败')
|
||
} finally {
|
||
batchSaving.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadDoctors()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.doctor-roster {
|
||
min-height: 100vh;
|
||
}
|
||
</style>
|