916 lines
24 KiB
Vue
916 lines
24 KiB
Vue
<template>
|
|
<el-drawer
|
|
v-model="visible"
|
|
title="预约问诊"
|
|
direction="rtl"
|
|
size="60%"
|
|
:close-on-click-modal="false"
|
|
:z-index="2000"
|
|
>
|
|
<div v-loading="loading">
|
|
<el-alert
|
|
v-if="todayHasBlockingAppointment"
|
|
:type="isSelectingToday ? 'warning' : 'info'"
|
|
:closable="false"
|
|
show-icon
|
|
class="today-block-alert"
|
|
:title="todayBlockingAlertText"
|
|
/>
|
|
<el-form :model="form" label-width="100px" class="appointment-form">
|
|
<!-- 上次就诊 -->
|
|
<el-form-item label="上次就诊:">
|
|
<span class="text-gray-500">{{ lastVisit || '无就诊记录' }}</span>
|
|
</el-form-item>
|
|
|
|
<!-- 预约方式 -->
|
|
<el-form-item label="预约方式:">
|
|
<el-radio-group v-model="form.appointmentMethod" @change="handleMethodChange">
|
|
<el-radio value="time">选择时段预约有专医生</el-radio>
|
|
<!-- <el-radio value="doctor">选择医生预约有专时段</el-radio> -->
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<!-- 预约类型 -->
|
|
<el-form-item label="预约类型:">
|
|
<el-radio-group v-model="form.appointmentType">
|
|
<el-radio value="video">视频问诊</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<!-- 选择患者 -->
|
|
<el-form-item label="选择患者:">
|
|
<el-radio-group v-model="form.patientId">
|
|
<el-radio :value="patientInfo.id">{{ patientInfo.name }}</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="渠道来源:" required>
|
|
<el-select
|
|
v-model="form.channel_source"
|
|
placeholder="请选择渠道来源"
|
|
class="channel-source-select"
|
|
@change="onChannelSourceChange"
|
|
>
|
|
<el-option
|
|
v-for="item in channelOptions"
|
|
:key="String(item.value)"
|
|
:label="item.name"
|
|
:value="item.value"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item
|
|
v-if="needsChannelSourceDetail"
|
|
label="自媒体补充:"
|
|
required
|
|
>
|
|
<el-input
|
|
v-model="form.channel_source_detail"
|
|
clearable
|
|
maxlength="128"
|
|
show-word-limit
|
|
placeholder="请输入自媒体相关补充内容"
|
|
class="channel-source-select"
|
|
/>
|
|
</el-form-item>
|
|
<!-- 预约医生 -->
|
|
<el-form-item label="预约医生:">
|
|
<div class="doctor-list">
|
|
<el-radio-group v-model="selectedDoctorId" @change="handleDoctorChange">
|
|
<el-radio
|
|
v-for="doctor in doctorList"
|
|
:key="doctor.id"
|
|
:value="doctor.id"
|
|
class="doctor-radio"
|
|
>
|
|
{{ doctor.name }}
|
|
<!-- <el-tag
|
|
v-if="getDoctorAvailability(doctor.id)"
|
|
:type="getDoctorAvailability(doctor.id) > 0 ? 'success' : 'info'"
|
|
size="small"
|
|
class="ml-2"
|
|
>
|
|
{{ getDoctorAvailability(doctor.id) > 0 ? getDoctorAvailability(doctor.id) : '无' }}
|
|
</el-tag> -->
|
|
</el-radio>
|
|
</el-radio-group>
|
|
</div>
|
|
</el-form-item>
|
|
|
|
<!-- 预约时间 -->
|
|
<el-form-item label="预约时间:">
|
|
<!-- 未选择医生提示 -->
|
|
<el-empty
|
|
v-if="!selectedDoctorId"
|
|
description="请先选择医生"
|
|
:image-size="80"
|
|
/>
|
|
|
|
<!-- 医生无排班提示 -->
|
|
<el-empty
|
|
v-else-if="selectedDoctorId && doctorRosterDates.length === 0"
|
|
description="该医生暂无排班"
|
|
:image-size="80"
|
|
/>
|
|
|
|
<!-- 有排班时显示日期和时间段 -->
|
|
<div v-else class="appointment-time-container">
|
|
<!-- 日期选择 -->
|
|
<div class="date-selector">
|
|
<el-button
|
|
v-for="dateOption in dateOptions"
|
|
:key="dateOption.date"
|
|
:type="form.date === dateOption.date ? 'primary' : ''"
|
|
@click="selectDate(dateOption.date)"
|
|
class="date-button"
|
|
>
|
|
{{ dateOption.label }}
|
|
</el-button>
|
|
</div>
|
|
|
|
<!-- 时间段区域 -->
|
|
<div v-if="form.date" class="time-slots-container">
|
|
<!-- 时间段标题和刷新按钮 -->
|
|
<div class="time-slots-header">
|
|
<span class="header-title">可预约时段</span>
|
|
<el-button
|
|
text
|
|
type="primary"
|
|
size="small"
|
|
:loading="refreshing"
|
|
@click="handleRefreshSlots"
|
|
>
|
|
<template #icon>
|
|
<Refresh />
|
|
</template>
|
|
刷新
|
|
</el-button>
|
|
</div>
|
|
|
|
<!-- 时间段网格 -->
|
|
<div class="time-slots-grid">
|
|
<div
|
|
v-for="slot in filteredTimeSlots"
|
|
:key="slot.time"
|
|
class="time-slot-item"
|
|
:class="{
|
|
'available': slot.available,
|
|
'unavailable': !slot.available,
|
|
'selected': form.appointmentTime === slot.time
|
|
}"
|
|
@click="selectTimeSlot(slot)"
|
|
>
|
|
<div class="slot-time">{{ slot.time }}</div>
|
|
<div class="slot-status" :class="{ 'status-available': slot.available }">
|
|
{{ slot.available ? '可约' : '已约' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-form-item>
|
|
|
|
<!-- 备注 -->
|
|
<el-form-item label="备注:">
|
|
<el-input
|
|
v-model="form.remark"
|
|
type="textarea"
|
|
:rows="2"
|
|
placeholder="请输入备注信息"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="drawer-footer">
|
|
<el-button @click="visible = false">取消</el-button>
|
|
<el-button
|
|
type="primary"
|
|
@click="handleConfirm"
|
|
:loading="submitting"
|
|
:disabled="!canSubmit"
|
|
>
|
|
确定
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
</el-drawer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, nextTick } from 'vue'
|
|
import { Refresh } from '@element-plus/icons-vue'
|
|
import dayjs from 'dayjs'
|
|
import isoWeek from 'dayjs/plugin/isoWeek'
|
|
import { getDoctors } from '@/api/tcm'
|
|
import { getDictData } from '@/api/app'
|
|
import { getAvailableSlots, createAppointment, rosterLists, appointmentLists } from '@/api/doctor'
|
|
import feedback from '@/utils/feedback'
|
|
|
|
dayjs.extend(isoWeek)
|
|
|
|
interface TimeSlot {
|
|
time: string
|
|
available: boolean
|
|
quota: number
|
|
}
|
|
|
|
interface DateOption {
|
|
date: string
|
|
label: string
|
|
}
|
|
|
|
const visible = ref(false)
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const refreshing = ref(false)
|
|
const doctorList = ref<any[]>([])
|
|
const timeSlots = ref<TimeSlot[]>([])
|
|
const lastVisit = ref('')
|
|
const selectedDoctorId = ref(0)
|
|
const doctorRosterDates = ref<string[]>([]) // 医生有排班的日期列表
|
|
const channelOptions = ref<{ name: string; value: string }[]>([])
|
|
/** 今日是否已有「已预约(1)/已过号(4)」挂号;仅在选择「今天」再预约时禁止提交,其他日期可正常挂 */
|
|
const todayHasBlockingAppointment = ref(false)
|
|
|
|
const patientInfo = reactive({
|
|
id: 0,
|
|
name: '',
|
|
gender: '',
|
|
age: 0
|
|
})
|
|
|
|
const form = reactive({
|
|
appointmentMethod: 'time' as 'time' | 'doctor',
|
|
appointmentType: 'video',
|
|
patientId: 0,
|
|
date: '',
|
|
appointmentTime: '',
|
|
remark: '',
|
|
channel_source: '' as string,
|
|
channel_source_detail: '' as string
|
|
})
|
|
|
|
/** 仅这些渠道字典 name 需填「自媒体补充」(与后台字典名称完全一致) */
|
|
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
|
|
'自媒体4H',
|
|
'自媒体3Q',
|
|
'自媒体3H',
|
|
'自媒体2H',
|
|
'自媒体2Q'
|
|
])
|
|
|
|
function channelNameRequiresSelfMediaDetail(name: string) {
|
|
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
|
|
}
|
|
|
|
/** 当前选中渠道字典名称(用于判断是否需填「自媒体补充」) */
|
|
const selectedChannelDictName = computed(() => {
|
|
const v = form.channel_source
|
|
if (v === '' || v == null) return ''
|
|
const row = channelOptions.value.find((item: any) => String(item.value) === String(v))
|
|
return row ? String(row.name ?? '').trim() : ''
|
|
})
|
|
|
|
/** 仅白名单内字典项需填写补充说明 */
|
|
const needsChannelSourceDetail = computed(() =>
|
|
channelNameRequiresSelfMediaDetail(selectedChannelDictName.value)
|
|
)
|
|
|
|
const onChannelSourceChange = (val: string) => {
|
|
const row = channelOptions.value.find((item: any) => String(item.value) === String(val))
|
|
const name = row ? String(row.name ?? '').trim() : ''
|
|
if (!channelNameRequiresSelfMediaDetail(name)) {
|
|
form.channel_source_detail = ''
|
|
}
|
|
}
|
|
|
|
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
|
|
const dateOptions = computed<DateOption[]>(() => {
|
|
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
|
|
return []
|
|
}
|
|
|
|
const options: DateOption[] = []
|
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
|
const today = dayjs().startOf('day')
|
|
|
|
// 只显示有排班的日期,且日期大于等于今天
|
|
for (const date of doctorRosterDates.value) {
|
|
const dateObj = dayjs(date)
|
|
|
|
// 过滤掉今天之前的日期
|
|
if (dateObj.isBefore(today)) {
|
|
continue
|
|
}
|
|
|
|
const label = `${dateObj.format('MM月DD日')} (${weekDays[dateObj.day()]})`
|
|
options.push({
|
|
date: date,
|
|
label
|
|
})
|
|
}
|
|
|
|
return options
|
|
})
|
|
|
|
// 过滤时间段:如果是今天,只显示当前时间之后的时间段
|
|
const filteredTimeSlots = computed(() => {
|
|
if (!form.date || timeSlots.value.length === 0) {
|
|
return []
|
|
}
|
|
|
|
const today = dayjs().format('YYYY-MM-DD')
|
|
const isToday = form.date === today
|
|
|
|
if (!isToday) {
|
|
// 不是今天,显示所有时间段
|
|
return timeSlots.value
|
|
}
|
|
|
|
// 是今天,只显示当前时间之后的时间段
|
|
const now = dayjs()
|
|
return timeSlots.value.map(slot => {
|
|
const slotDateTime = dayjs(`${form.date} ${slot.time}`)
|
|
const isPast = slotDateTime.isBefore(now) || slotDateTime.isSame(now, 'minute')
|
|
|
|
// 如果时间已过,标记为不可用
|
|
if (isPast) {
|
|
return {
|
|
...slot,
|
|
available: false
|
|
}
|
|
}
|
|
return slot
|
|
})
|
|
})
|
|
|
|
const todayYmd = () => dayjs().format('YYYY-MM-DD')
|
|
|
|
/** 当前选中的预约日是否为今天(与今日冲突检测联动) */
|
|
const isSelectingToday = computed(() => form.date === todayYmd())
|
|
|
|
/** 今日已有占用号时:选今天给警告文案,选他日给提示文案 */
|
|
const todayBlockingAlertText = computed(() => {
|
|
if (!todayHasBlockingAppointment.value) return ''
|
|
return isSelectingToday.value
|
|
? '该患者今日已有挂号(已预约或已过号),不能重复预约今天,请改选其他日期。'
|
|
: '该患者今日已有挂号记录;可为患者预约其他日期的时段。'
|
|
})
|
|
|
|
/** 仅当「选今天 + 今日已有占用号」时禁止提交 */
|
|
const blockedByTodayDuplicate = computed(
|
|
() => todayHasBlockingAppointment.value && isSelectingToday.value
|
|
)
|
|
|
|
// 是否可以提交
|
|
const canSubmit = computed(() => {
|
|
return (
|
|
!blockedByTodayDuplicate.value &&
|
|
selectedDoctorId.value &&
|
|
form.date &&
|
|
form.appointmentTime &&
|
|
!!form.channel_source &&
|
|
(!needsChannelSourceDetail.value || form.channel_source_detail.trim() !== '')
|
|
)
|
|
})
|
|
|
|
const emit = defineEmits(['success'])
|
|
|
|
// 打开弹窗
|
|
const open = async (patient: any) => {
|
|
visible.value = true
|
|
patientInfo.id = patient.patient_id || patient.id
|
|
patientInfo.name = patient.patient_name || patient.name
|
|
patientInfo.gender = patient.gender_desc || patient.gender
|
|
patientInfo.age = patient.age || 0
|
|
|
|
// 重置表单
|
|
form.appointmentMethod = 'time'
|
|
form.appointmentType = 'video'
|
|
form.patientId = patientInfo.id
|
|
selectedDoctorId.value = 0
|
|
form.date = ''
|
|
form.appointmentTime = ''
|
|
form.remark = ''
|
|
form.channel_source = ''
|
|
form.channel_source_detail = ''
|
|
timeSlots.value = []
|
|
doctorRosterDates.value = []
|
|
lastVisit.value = ''
|
|
todayHasBlockingAppointment.value = false
|
|
|
|
await loadChannelOptions()
|
|
// 加载医生列表
|
|
await loadDoctors()
|
|
|
|
// 查询该患者的最近一次就诊记录
|
|
await loadLastVisit()
|
|
await loadTodayAppointmentBlock()
|
|
}
|
|
|
|
/** 今日是否已有状态为已预约(1)或已过号(4)的挂号(仅禁止再挂「今天」,不禁止挂其他天) */
|
|
const loadTodayAppointmentBlock = async () => {
|
|
todayHasBlockingAppointment.value = false
|
|
const pid = patientInfo.id
|
|
if (!pid) {
|
|
return
|
|
}
|
|
const today = dayjs().format('YYYY-MM-DD')
|
|
try {
|
|
const res = await appointmentLists({
|
|
patient_id: pid,
|
|
start_date: today,
|
|
end_date: today,
|
|
page_no: 1,
|
|
page_size: 50
|
|
})
|
|
const rows = res?.lists || []
|
|
const blocking = rows.find((r: any) => {
|
|
const s = Number(r.status)
|
|
return s === 1 || s === 4
|
|
})
|
|
todayHasBlockingAppointment.value = !!blocking
|
|
} catch (e) {
|
|
console.error('检查今日挂号状态失败:', e)
|
|
}
|
|
}
|
|
|
|
// 加载患者最近一次就诊记录(从预约表查询已完成的预约)
|
|
const loadLastVisit = async () => {
|
|
try {
|
|
const res = await appointmentLists({
|
|
patient_id: patientInfo.id,
|
|
status: 3, // 只查询已完成的预约
|
|
page_no: 1,
|
|
page_size: 1
|
|
})
|
|
|
|
if (res?.lists && res.lists.length > 0) {
|
|
const lastRecord = res.lists[0]
|
|
// 格式化显示:日期 + 时间
|
|
if (lastRecord.appointment_date && lastRecord.appointment_time) {
|
|
lastVisit.value = `${lastRecord.appointment_date} ${lastRecord.appointment_time}`
|
|
} else if (lastRecord.appointment_date) {
|
|
lastVisit.value = lastRecord.appointment_date
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('加载就诊记录失败:', error)
|
|
}
|
|
}
|
|
|
|
const loadChannelOptions = async () => {
|
|
try {
|
|
const data = await getDictData({ type: 'channels' })
|
|
const rows = (data?.channels || []).filter((row: any) => row.status !== 0)
|
|
rows.sort((a: any, b: any) => {
|
|
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
|
|
if (ds !== 0) return ds
|
|
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
|
|
})
|
|
// 统一为字符串,避免字典 value 为数字时 el-select 与表单校验不一致导致看似选了但实际未绑定
|
|
channelOptions.value = rows.map((row: any) => ({
|
|
...row,
|
|
value: row?.value != null && row.value !== '' ? String(row.value) : ''
|
|
}))
|
|
} catch (e) {
|
|
console.error('加载渠道来源失败:', e)
|
|
channelOptions.value = []
|
|
}
|
|
}
|
|
|
|
// 加载医生列表
|
|
const loadDoctors = async () => {
|
|
try {
|
|
loading.value = true
|
|
const res = await getDoctors()
|
|
doctorList.value = res || []
|
|
} catch (error) {
|
|
console.error('加载医生列表失败:', error)
|
|
feedback.msgError('加载医生列表失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 获取医生可用号源数
|
|
const getDoctorAvailability = (doctorId: number) => {
|
|
// 这里应该从后端获取医生的可用号源数
|
|
// 暂时返回模拟数据
|
|
return Math.floor(Math.random() * 10)
|
|
}
|
|
|
|
// 预约方式改变
|
|
const handleMethodChange = () => {
|
|
selectedDoctorId.value = 0
|
|
form.date = ''
|
|
form.appointmentTime = ''
|
|
timeSlots.value = []
|
|
}
|
|
|
|
// 医生改变
|
|
const handleDoctorChange = async () => {
|
|
form.appointmentTime = ''
|
|
form.date = ''
|
|
timeSlots.value = []
|
|
doctorRosterDates.value = []
|
|
|
|
if (selectedDoctorId.value) {
|
|
await loadDoctorRoster()
|
|
}
|
|
}
|
|
|
|
// 加载医生排班信息
|
|
const loadDoctorRoster = async () => {
|
|
if (!selectedDoctorId.value) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
loading.value = true
|
|
|
|
// 获取未来7天的日期范围
|
|
const startDate = dayjs().format('YYYY-MM-DD')
|
|
const endDate = dayjs().add(6, 'day').format('YYYY-MM-DD')
|
|
|
|
// 查询医生在这个日期范围内的排班
|
|
const res = await rosterLists({
|
|
doctor_id: selectedDoctorId.value,
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
status: 1 // 只查询出诊状态
|
|
})
|
|
|
|
// 提取有排班的日期
|
|
if (res?.lists && res.lists.length > 0) {
|
|
doctorRosterDates.value = [...new Set(res.lists.map((item: any) => item.date))] as string[]
|
|
doctorRosterDates.value.sort()
|
|
|
|
// 自动选择日期:优先选择今天,如果今天没有排班则选择第一个有排班的日期
|
|
await nextTick()
|
|
if (doctorRosterDates.value.length > 0) {
|
|
const today = dayjs().format('YYYY-MM-DD')
|
|
let defaultDate = doctorRosterDates.value.includes(today)
|
|
? today
|
|
: doctorRosterDates.value[0]
|
|
// 今日已有占用号时,默认不要落在「今天」,避免一进来确定按钮灰掉
|
|
if (todayHasBlockingAppointment.value && defaultDate === today) {
|
|
const later = doctorRosterDates.value.find((d) => d > today)
|
|
defaultDate = later ?? doctorRosterDates.value.find((d) => d !== today) ?? defaultDate
|
|
}
|
|
selectDate(defaultDate)
|
|
}
|
|
} else {
|
|
doctorRosterDates.value = []
|
|
feedback.msgWarning('该医生暂无排班')
|
|
}
|
|
} catch (error) {
|
|
console.error('加载医生排班失败:', error)
|
|
feedback.msgError('加载医生排班失败')
|
|
doctorRosterDates.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 选择日期
|
|
const selectDate = (date: string) => {
|
|
form.date = date
|
|
form.appointmentTime = ''
|
|
if (selectedDoctorId.value) {
|
|
loadTimeSlots()
|
|
}
|
|
}
|
|
|
|
// 加载时间段
|
|
const loadTimeSlots = async () => {
|
|
if (!selectedDoctorId.value || !form.date) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
loading.value = true
|
|
|
|
// 加载全天时段(9:00-18:00,每15分钟一个)
|
|
const response = await getAvailableSlots({
|
|
doctor_id: selectedDoctorId.value,
|
|
appointment_date: form.date,
|
|
period: 'all' // 获取全天时段
|
|
})
|
|
|
|
console.log('时间段数据:', response)
|
|
|
|
timeSlots.value = (response?.slots || []).map((slot: any) => ({
|
|
time: slot.time,
|
|
available: slot.available,
|
|
quota: slot.available ? 1 : 0 // 每个时间段只能预约1次
|
|
}))
|
|
|
|
console.log('处理后的时间段:', timeSlots.value)
|
|
} catch (error) {
|
|
console.error('加载时间段失败:', error)
|
|
feedback.msgError('加载时间段失败')
|
|
timeSlots.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 选择时间段
|
|
const selectTimeSlot = (slot: TimeSlot) => {
|
|
if (!slot.available) {
|
|
feedback.msgWarning('该时间段不可预约')
|
|
return
|
|
}
|
|
form.appointmentTime = slot.time
|
|
}
|
|
|
|
// 刷新时间段
|
|
const handleRefreshSlots = async () => {
|
|
if (!selectedDoctorId.value || !form.date) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
refreshing.value = true
|
|
const previousSelection = form.appointmentTime
|
|
form.appointmentTime = ''
|
|
|
|
await loadTimeSlots()
|
|
|
|
if (previousSelection) {
|
|
const slot = timeSlots.value.find(s => s.time === previousSelection)
|
|
if (slot && slot.available) {
|
|
form.appointmentTime = previousSelection
|
|
}
|
|
}
|
|
|
|
feedback.msgSuccess('刷新成功')
|
|
} catch (error) {
|
|
console.error('刷新失败:', error)
|
|
feedback.msgError('刷新失败,请重试')
|
|
} finally {
|
|
refreshing.value = false
|
|
}
|
|
}
|
|
|
|
// 确认挂号
|
|
const handleConfirm = async () => {
|
|
if (!selectedDoctorId.value) {
|
|
feedback.msgWarning('请选择医生')
|
|
return
|
|
}
|
|
if (!form.date) {
|
|
feedback.msgWarning('请选择日期')
|
|
return
|
|
}
|
|
if (!form.appointmentTime) {
|
|
feedback.msgWarning('请选择预约时间')
|
|
return
|
|
}
|
|
if (!form.channel_source) {
|
|
feedback.msgWarning('请选择渠道来源')
|
|
return
|
|
}
|
|
if (needsChannelSourceDetail.value && !form.channel_source_detail.trim()) {
|
|
feedback.msgWarning('请填写自媒体渠道补充信息')
|
|
return
|
|
}
|
|
if (blockedByTodayDuplicate.value) {
|
|
feedback.msgWarning(
|
|
'该患者今日已有挂号(已预约或已过号),不能重复预约今天,请改选其他日期。'
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
submitting.value = true
|
|
|
|
// 构建请求参数
|
|
const params = {
|
|
patient_id: patientInfo.id,
|
|
doctor_id: selectedDoctorId.value,
|
|
appointment_date: form.date,
|
|
period: 'all', // 全天时段
|
|
appointment_time: form.appointmentTime,
|
|
appointment_type: form.appointmentType,
|
|
remark: form.remark,
|
|
channel_source: String(form.channel_source ?? ''),
|
|
channel_source_detail: form.channel_source_detail.trim()
|
|
}
|
|
|
|
console.log('提交预约参数:', params)
|
|
|
|
await createAppointment(params)
|
|
|
|
feedback.msgSuccess('预约成功')
|
|
visible.value = false
|
|
emit('success')
|
|
} catch (error) {
|
|
console.error('预约失败:', error)
|
|
feedback.msgError('预约失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
defineExpose({ open })
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.today-block-alert {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.appointment-form {
|
|
:deep(.el-form-item__label) {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.channel-source-select {
|
|
width: 100%;
|
|
max-width: 360px;
|
|
}
|
|
}
|
|
|
|
.doctor-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
|
|
.doctor-radio {
|
|
margin-right: 0;
|
|
|
|
:deep(.el-radio__label) {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
}
|
|
|
|
.appointment-time-container {
|
|
width: 100%;
|
|
}
|
|
|
|
.date-selector {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 20px;
|
|
|
|
.date-button {
|
|
min-width: 130px;
|
|
height: 40px;
|
|
font-size: 14px;
|
|
border-radius: 8px;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
}
|
|
}
|
|
|
|
.time-slots-container {
|
|
background-color: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.time-slots-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
|
|
.header-title {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
}
|
|
}
|
|
|
|
.time-slots-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
|
gap: 10px;
|
|
max-height: 450px;
|
|
overflow-y: auto;
|
|
padding: 2px;
|
|
|
|
&::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
background-color: #dcdfe6;
|
|
border-radius: 3px;
|
|
|
|
&:hover {
|
|
background-color: #c0c4cc;
|
|
}
|
|
}
|
|
|
|
.time-slot-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0px 8px;
|
|
border: 2px solid #e4e7ed;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background-color: #fff;
|
|
min-height: 70px;
|
|
|
|
.slot-time {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.slot-status {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
padding: 0px 8px;
|
|
border-radius: 4px;
|
|
background-color: #f4f4f5;
|
|
|
|
&.status-available {
|
|
color: #67c23a;
|
|
background-color: #f0f9ff;
|
|
}
|
|
}
|
|
|
|
&.available {
|
|
border-color: #e4e7ed;
|
|
|
|
&:hover {
|
|
border-color: #409eff;
|
|
background-color: #ecf5ff;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
|
}
|
|
}
|
|
|
|
&.unavailable {
|
|
background-color: #f5f7fa;
|
|
border-color: #e4e7ed;
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
|
|
.slot-time {
|
|
color: #c0c4cc;
|
|
}
|
|
|
|
.slot-status {
|
|
color: #c0c4cc;
|
|
background-color: #f5f7fa;
|
|
}
|
|
|
|
&:hover {
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
|
|
&.selected {
|
|
border-color: #409eff;
|
|
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
|
.slot-time {
|
|
color: #fff;
|
|
}
|
|
|
|
.slot-status {
|
|
color: #fff;
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
:deep(.el-radio-group) {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
:deep(.el-radio) {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.drawer-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
padding: 12px 0;
|
|
}
|
|
</style>
|