diff --git a/apps/miniprogram/src/pages/appointment/create/index.scss b/apps/miniprogram/src/pages/appointment/create/index.scss index 10c819a..9ed98c3 100644 --- a/apps/miniprogram/src/pages/appointment/create/index.scss +++ b/apps/miniprogram/src/pages/appointment/create/index.scss @@ -6,95 +6,85 @@ padding-bottom: 140px; } -/* 步骤指示器 */ -.step-bar { - display: flex; - justify-content: space-around; - padding: 32px 24px 0; -} - -.step-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - opacity: 0.4; - transition: opacity 0.3s; -} - -.step-item.step-active { - opacity: 1; -} - -.step-item.step-done { - opacity: 0.8; -} - -.step-dot { - width: 56px; - height: 56px; - border-radius: 50%; - background: $bd; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.3s; -} - -.step-active .step-dot { - background: $pri; -} - -.step-done .step-dot { - background: $acc; -} - -.step-num { - font-size: 28px; - color: white; - font-weight: bold; -} - -.step-check { - font-size: 28px; - color: white; - font-weight: bold; -} - -.step-label { - font-size: 22px; - color: $tx2; -} - -.step-active .step-label { - color: $pri; - font-weight: 500; -} - -/* 步骤连接线 */ -.step-line-wrapper { - padding: 12px 80px 0; -} - -.step-line { - height: 4px; - background: $bd; - border-radius: 2px; - overflow: hidden; -} - -.step-line-fill { - height: 100%; - background: linear-gradient(90deg, $acc, $pri); - border-radius: 2px; - transition: width 0.3s ease; -} - /* 步骤内容 */ .step-content { padding: 32px 24px; } +/* 科室宫格 */ +.dept-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.dept-card { + background: $card; + border-radius: $r; + padding: 24px 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + border: 2px solid transparent; + transition: border-color 0.2s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.dept-card.dept-selected { + border-color: $pri; + background: $pri-surface; +} + +.dept-icon { + font-size: 40px; +} + +.dept-label { + font-size: 26px; + color: $tx; +} + +/* 时段卡片 */ +.slot-section { + margin-top: 24px; +} + +.slot-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.slot-card { + background: $card; + border-radius: $r-sm; + padding: 16px 20px; + border: 2px solid transparent; + transition: all 0.2s; + + &.slot-few { border-color: $wrn; } + &.slot-full { opacity: 0.5; background: $bd-l; } + &.slot-selected { border-color: $pri; background: $pri-surface; } +} + +.slot-time { + font-size: 28px; + font-weight: bold; + color: $tx; + display: block; +} + +.slot-count { + font-size: 22px; + color: $tx3; + display: block; + margin-top: 4px; +} + +.slot-few .slot-count { color: $wrn; } +.slot-full .slot-count { color: $dan; } + .step-title { font-size: 32px; font-weight: bold; diff --git a/apps/miniprogram/src/pages/appointment/create/index.tsx b/apps/miniprogram/src/pages/appointment/create/index.tsx index 917c05e..664c73e 100644 --- a/apps/miniprogram/src/pages/appointment/create/index.tsx +++ b/apps/miniprogram/src/pages/appointment/create/index.tsx @@ -1,11 +1,20 @@ -import React, { useState, useCallback } from 'react'; -import { View, Text, Picker, Input } from '@tarojs/components'; +import React, { useState, useCallback, useMemo } from 'react'; +import { View, Text, Input } from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { listDoctors, createAppointment } from '../../../services/appointment'; +import { listDoctors, createAppointment, calendarView } from '../../../services/appointment'; import { useAuthStore } from '../../../stores/auth'; +import StepIndicator from '../../../components/StepIndicator'; +import WeekCalendar from '../../../components/WeekCalendar'; import './index.scss'; -const DEPARTMENTS = ['内科', '外科', '妇科', '儿科', '体检中心']; +const DEPARTMENTS = [ + { label: '内科', icon: '🫀' }, + { label: '外科', icon: '🔪' }, + { label: '妇科', icon: '👩‍⚕️' }, + { label: '儿科', icon: '👶' }, + { label: '体检中心', icon: '🏥' }, + { label: '中医科', icon: '🌿' }, +]; interface DoctorItem { id: string; @@ -15,27 +24,33 @@ interface DoctorItem { specialty?: string; } +interface TimeSlot { + time_slot: string; + available_count: number; +} + export default function AppointmentCreate() { const [currentStep, setCurrentStep] = useState(0); const [department, setDepartment] = useState(''); - const [deptPickerIndex, setDeptPickerIndex] = useState(0); 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 [schedules, setSchedules] = useState([]); + const [timeSlots, setTimeSlots] = useState([]); const currentPatient = useAuthStore((s) => s.currentPatient); - // Step 1: 选择科室后加载医生列表 - const onDepartmentChange = useCallback(async (e: { detail: { value: number } }) => { - const idx = e.detail.value; - const dept = DEPARTMENTS[idx]; - setDeptPickerIndex(idx); + 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 || []); @@ -44,44 +59,47 @@ export default function AppointmentCreate() { } }, []); - // Step 2: 选择医生 - const onSelectDoctor = useCallback((doctor: DoctorItem) => { + 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); + } }, []); - // Step 3: 日期变更 - const onDateChange = useCallback((e: { detail: { value: string } }) => { - setAppointmentDate(e.detail.value); - }, []); + const onSelectDate = useCallback((date: string) => { + setAppointmentDate(date); + setTimeSlot(''); + // 从排班数据中提取时段 + const daySlots = schedules + .filter((s: any) => (s.date || s.appointment_date) === date) + .map((s: any) => ({ + time_slot: s.time_slot || `${s.start_time || ''}-${s.end_time || ''}`, + available_count: s.available_count ?? (s.max_patients ?? 10), + })); + setTimeSlots(daySlots); + }, [schedules]); - // Step 3: 时段变更 - const onTimeSlotChange = useCallback((e: { detail: { value: string } }) => { - setTimeSlot(e.detail.value); - }, []); + const getSlotStyle = (available: number) => { + if (available === 0) return 'slot-full'; + if (available <= 3) return 'slot-few'; + return 'slot-available'; + }; - // Step 3: 备注变更 - const onReasonChange = useCallback((e: { detail: { value: string } }) => { - setReason(e.detail.value); - }, []); - - // 提交预约 const handleSubmit = useCallback(async () => { - if (!selectedDoctor) { - Taro.showToast({ title: '请选择医生', icon: 'none' }); - return; - } - if (!appointmentDate) { - Taro.showToast({ title: '请选择日期', icon: 'none' }); - return; - } - if (!timeSlot.trim()) { - Taro.showToast({ title: '请输入时段', icon: 'none' }); - return; - } - if (!currentPatient) { - Taro.showToast({ title: '请先选择就诊人', icon: 'none' }); - return; - } + 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 { @@ -89,13 +107,11 @@ export default function AppointmentCreate() { patient_id: currentPatient.id, doctor_id: selectedDoctor.id, appointment_date: appointmentDate, - time_slot: timeSlot.trim(), + time_slot: timeSlot, reason: reason.trim() || undefined, }); Taro.showToast({ title: '预约成功', icon: 'success' }); - setTimeout(() => { - Taro.navigateBack(); - }, 1500); + setTimeout(() => Taro.navigateBack(), 1500); } catch { Taro.showToast({ title: '预约失败', icon: 'none' }); } finally { @@ -103,76 +119,53 @@ export default function AppointmentCreate() { } }, [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; - } + 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 goPrev = () => { - setCurrentStep(Math.max(currentStep - 1, 0)); + const handleStepChange = (idx: number) => { + if (idx < currentStep) { + if (idx <= 1) setSelectedDoctor(null); + if (idx <= 2) { setAppointmentDate(''); setTimeSlot(''); } + setCurrentStep(idx); + } }; - const stepLabels = ['选择科室', '选择医生', '选择日期与时段']; - return ( - {/* 步骤指示器 */} - - {stepLabels.map((label, idx) => ( - - - {idx < currentStep ? ( - - ) : ( - {idx + 1} - )} - - {label} - - ))} - + - {/* 步骤连接线 */} - - - - - - - {/* Step 1: 选择科室 */} + {/* Step 1: 科室宫格 */} {currentStep === 0 && ( 请选择就诊科室 - - - - {department || '点击选择科室'} - - - - + + {DEPARTMENTS.map((dept) => ( + onSelectDept(dept.label)} + > + {dept.icon} + {dept.label} + + ))} + )} - {/* Step 2: 选择医生 */} + {/* Step 2: 医生列表 */} {currentStep === 1 && ( {department} - 请选择医生 {doctors.length === 0 ? ( - - 暂无可选医生 - + 暂无可选医生 ) : ( {doctors.map((doc) => ( @@ -181,17 +174,13 @@ export default function AppointmentCreate() { key={doc.id} onClick={() => onSelectDoctor(doc)} > - - {doc.name.charAt(0)} - + {doc.name.charAt(0)} {doc.name} {doc.title || '医生'} {doc.specialty && {doc.specialty}} - {selectedDoctor?.id === doc.id && ( - - )} + {selectedDoctor?.id === doc.id && } ))} @@ -199,48 +188,42 @@ export default function AppointmentCreate() { )} - {/* Step 3: 选择日期和时段 */} + {/* Step 3: 日历 + 时段 */} {currentStep === 2 && ( 选择就诊时间 - 医生 - - {selectedDoctor?.name} - {department} - + {selectedDoctor?.name} - {department} - - 就诊日期 - - - - {appointmentDate || '点击选择日期'} - - + + + {appointmentDate && timeSlots.length > 0 && ( + + 选择时段 + + {timeSlots.map((slot) => ( + 0 ? () => setTimeSlot(slot.time_slot) : undefined} + > + {slot.time_slot} + {slot.available_count > 0 ? `剩余 ${slot.available_count} 位` : '已满'} + + ))} - - - - - 就诊时段 - - + + )} 备注(选填) - + setReason(e.detail.value)} /> )} @@ -248,7 +231,7 @@ export default function AppointmentCreate() { {/* 底部操作栏 */} {currentStep > 0 && ( - + handleStepChange(currentStep - 1)}> 上一步 )}