import React, { useState, useCallback, useMemo } from 'react'; import { View, Text, Input } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { listDoctors, createAppointment, calendarView } from '../../../services/appointment'; import { useAuthStore } from '../../../stores/auth'; import { TEMPLATE_IDS } from '@/services/wechat-templates'; import { trackEvent } from '@/services/analytics'; import StepIndicator from '../../../components/StepIndicator'; import WeekCalendar from '../../../components/WeekCalendar'; import PageShell from '@/components/ui/PageShell'; import ContentCard from '@/components/ui/ContentCard'; import { useElderClass } from '../../../hooks/useElderClass'; import { useSafeTimeout } from '@/hooks/useSafeTimeout'; import './index.scss'; const DEPARTMENTS = [ { label: '内科', initial: '内' }, { label: '外科', initial: '外' }, { label: '妇科', initial: '妇' }, { label: '儿科', initial: '儿' }, { label: '体检中心', initial: '检' }, { label: '中医科', initial: '中' }, ]; interface DoctorItem { id: string; name: string; title?: string; department?: string; specialty?: string; } interface TimeSlot { start_time: string; end_time: string; label: string; available_count: number; } export default function AppointmentCreate() { const [currentStep, setCurrentStep] = useState(0); const [department, setDepartment] = useState(''); const [doctors, setDoctors] = useState([]); const [selectedDoctor, setSelectedDoctor] = useState(null); const [appointmentDate, setAppointmentDate] = useState(''); const [timeSlot, setTimeSlot] = useState(''); const [reason, setReason] = useState(''); const [loading, setLoading] = useState(false); const { safeSetTimeout } = useSafeTimeout(); const [schedules, setSchedules] = useState([]); const [timeSlots, setTimeSlots] = useState([]); const modeClass = useElderClass(); const currentPatient = useAuthStore((s) => s.currentPatient); const scheduledDates = useMemo(() => { if (!schedules) return new Set(); return new Set(schedules.map((s: any) => s.date || s.appointment_date)); }, [schedules]); const onSelectDept = useCallback(async (dept: string) => { setDepartment(dept); setSelectedDoctor(null); try { const res = await listDoctors(dept); setDoctors(res.data || []); } catch { Taro.showToast({ title: '加载医生失败', icon: 'none' }); } }, []); const onSelectDoctor = useCallback(async (doctor: DoctorItem) => { setSelectedDoctor(doctor); setLoading(true); try { const today = new Date(); const endDate = new Date(today); endDate.setDate(today.getDate() + 30); const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const res = await calendarView(fmt(today), fmt(endDate), doctor.id); setSchedules(res || []); } catch { // 日历加载失败不阻塞 } finally { setLoading(false); } }, []); const onSelectDate = useCallback((date: string) => { setAppointmentDate(date); setTimeSlot(''); const daySlots = schedules .filter((s: any) => (s.date || s.appointment_date) === date) .map((s: any) => ({ start_time: s.start_time || '', end_time: s.end_time || '', label: `${s.start_time || ''}-${s.end_time || ''}`, available_count: s.available_count ?? (s.max_appointments - (s.current_appointments || 0)), })); setTimeSlots(daySlots); }, [schedules]); const getSlotStyle = (available: number) => { if (available === 0) return 'slot-full'; if (available <= 3) return 'slot-few'; return 'slot-available'; }; const handleSubmit = useCallback(async () => { if (!selectedDoctor) { Taro.showToast({ title: '请选择医生', icon: 'none' }); return; } if (!appointmentDate) { Taro.showToast({ title: '请选择日期', icon: 'none' }); return; } if (!timeSlot) { Taro.showToast({ title: '请选择时段', icon: 'none' }); return; } if (!currentPatient) { Taro.showToast({ title: '请先选择就诊人', icon: 'none' }); return; } setLoading(true); try { const selectedSlot = timeSlots.find((s) => s.label === timeSlot); await createAppointment({ patient_id: currentPatient.id, doctor_id: selectedDoctor.id, appointment_date: appointmentDate, start_time: selectedSlot?.start_time || timeSlot, end_time: selectedSlot?.end_time || timeSlot, notes: reason.trim() || undefined, }); Taro.showToast({ title: '预约成功', icon: 'success' }); trackEvent('appointment_create', { doctor_id: selectedDoctor.id, date: appointmentDate }); const tmplId = TEMPLATE_IDS.APPOINTMENT_REMINDER; if (tmplId) { try { await (Taro.requestSubscribeMessage as any)({ tmplIds: [tmplId] }); } catch { /* 用户拒绝 */ } } safeSetTimeout(() => Taro.navigateBack(), 1500); } catch (err: any) { const msg = err?.message || '预约失败'; Taro.showToast({ title: msg.length > 20 ? msg.slice(0, 20) : msg, icon: 'none' }); } finally { setLoading(false); } }, [selectedDoctor, appointmentDate, timeSlot, reason, currentPatient]); const goNext = () => { if (currentStep === 0 && !department) { Taro.showToast({ title: '请先选择科室', icon: 'none' }); return; } if (currentStep === 1 && !selectedDoctor) { Taro.showToast({ title: '请选择医生', icon: 'none' }); return; } setCurrentStep(Math.min(currentStep + 1, 2)); }; const handleStepChange = (idx: number) => { if (idx < currentStep) { if (idx <= 1) setSelectedDoctor(null); if (idx <= 2) { setAppointmentDate(''); setTimeSlot(''); } setCurrentStep(idx); } }; return ( {/* Step 1: 科室宫格 */} {currentStep === 0 && ( 请选择就诊科室 {DEPARTMENTS.map((dept) => ( onSelectDept(dept.label)} > {dept.initial} {dept.label} ))} )} {/* Step 2: 医生列表 */} {currentStep === 1 && ( {department} - 请选择医生 {doctors.length === 0 ? ( 暂无可选医生 ) : ( {doctors.map((doc) => ( onSelectDoctor(doc)} > {doc.name.charAt(0)} {doc.name} {doc.title || '医生'} {doc.specialty && {doc.specialty}} {selectedDoctor?.id === doc.id && ( )} ))} )} )} {/* Step 3: 日历 + 时段 */} {currentStep === 2 && ( 选择就诊时间 主治医生 {selectedDoctor?.name} {department} {appointmentDate && timeSlots.length > 0 && ( 选择时段 {timeSlots.map((slot) => ( 0 ? () => setTimeSlot(slot.label) : undefined} > {slot.label} {slot.available_count > 0 ? `剩余 ${slot.available_count} 位` : '已满'} ))} )} 备注(选填) setReason(e.detail.value)} /> )} {/* 底部操作栏 */} {currentStep > 0 && ( handleStepChange(currentStep - 1)}> 上一步 )} {currentStep < 2 ? ( 下一步 ) : ( {loading ? '提交中...' : '确认预约'} )} ); }