const API = require('../../utils/api.js') // 状态配色遵循 doc/DESIGN.md §三:四档状态色 + 状态说明文字, // 颜色只做强化,文字才是信息。待确认与已确认同属「等待」阶段,共用待服务档 const STATUS_MAP = { // 待确认是唯一「等用户自己决定」的阶段(底栏此时一定有「取消订单」), // 说明文字因此带上可执行动作,靠文字而不是靠再叠一层色块来提请注意 pending: { text: '待确认', color: '#0F766E', bg: '#ECF7F4', border: '#C7E5DF', desc: '订单已提交,等待陪诊师确认;确认前可取消' }, confirmed: { text: '已确认', color: '#0F766E', bg: '#ECF7F4', border: '#C7E5DF', desc: '订单已确认,陪诊师将准时为您服务' }, in_progress: { text: '进行中', color: '#B8520A', bg: '#FFF4E8', border: '#FFE0C2', desc: '陪诊服务正在进行中' }, completed: { text: '已完成', color: '#1F7A4D', bg: '#EAF6EE', border: '#CDE9D7', desc: '陪诊服务已完成,感谢您的使用' }, cancelled: { text: '已取消', color: '#B4231A', bg: '#FCF0EE', border: '#F2D6D1', desc: '该订单已取消' } } // 支付状态胶囊:未支付 / 部分支付复用主色浅档提示待处理;已退款走中性档(--muted 字 + --sep 描边 + 透明底); // 已支付不显示胶囊(支付完成是常态,不需要状态强调),paid 档仅为映射兜底保留。 // hint 是未付清时的出口指引:全站没有线上支付入口,只能引导走客服; // 部分支付要说清是「补齐」而不是「完成支付」,否则用户会以为要再付一次全款 const PAYMENT_STATUS_MAP = { unpaid: { text: '未支付', color: '#B8520A', bg: '#FFF4E8', border: '#FFE0C2', hint: '请联系客服完成支付' }, partial: { text: '部分支付', color: '#B8520A', bg: '#FFF4E8', border: '#FFE0C2', hint: '请联系客服补齐剩余费用' }, paid: { text: '已支付', color: '#1F7A4D', bg: '#EAF6EE', border: '#CDE9D7' }, refunded: { text: '已退款', color: '#6F655C', bg: 'transparent', border: '#E5DACC' } } const SEX_MAP = { male: '男', female: '女' } const SEX_REQUIREMENT_MAP = { none: '不限', male: '男', female: '女' } const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] Page({ data: { recordId: '', record: {}, // 编辑全部收进底部弹窗:页面只读,表单只活在弹窗里,两个弹窗各自一个开关 showNotePopup: false, editPatientNote: '', // 医院预约信息编辑弹窗:省份列表与下单页(itemdetail.js)保持同一份口径 showHospitalPopup: false, provinces: ['北京市', '天津市', '河北省', '山西省', '内蒙古自治区', '辽宁省', '吉林省', '黑龙江省', '上海市', '江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省', '河南省', '湖北省', '湖南省', '广东省', '广西壮族自治区', '海南省', '重庆市', '四川省', '贵州省', '云南省', '西藏自治区', '陕西省', '甘肃省', '青海省', '宁夏回族自治区', '新疆维吾尔自治区', '台湾省', '香港特别行政区', '澳门特别行政区'], editHospital: {}, showCancelPopup: false, cancelReason: '', cancelReasons: [ '临时有事,无法前往', '病情好转,无需陪诊', '医院停诊/改期', '陪诊师无法到达', '其他原因' ] }, onLoad(options) { const id = options.id if (!id) { wx.showToast({ title: '参数错误', icon: 'none' }) setTimeout(() => wx.navigateBack(), 1500) return } this.setData({ recordId: id }) }, // onShow 在首次渲染时紧跟 onLoad 触发,加载只放在这里,避免进页面发两次请求 onShow() { if (this.data.recordId) { this.loadRecord(this.data.recordId) } }, onPullDownRefresh() { if (this.data.recordId) { this.loadRecord(this.data.recordId).finally(() => { wx.stopPullDownRefresh() }) } }, // 渲染到位判据是 record._id(WXML 用它守内容),本方法只负责取数 loadRecord(id) { return API.escort.getRecordById(id) .then((res) => { if (res.code === 0 && res.data) { const record = this.formatRecord(res.data.record) this.setData({ record }) // 订单关联了健康档案时,就诊人信息以档案最新数据为准 if (record.healthProfileId) { this.loadProfileSnapshot(record.healthProfileId) } } else { wx.showToast({ title: res.msg || '加载失败', icon: 'none' }) } }) .catch(() => { wx.showToast({ title: '网络请求失败', icon: 'none' }) }) }, // 用档案最新信息覆盖订单里的 patient 快照: // 订单必须关联就诊人,档案才是信息源;读取失败(如档案已删除)时保留快照兜底 loadProfileSnapshot(profileId) { return API.healthProfile.getMyProfileById(profileId) .then((res) => { if (res.code !== 0 || !res.data) { return } const detail = res.data.profile || {} const profile = detail.profile || {} const health = detail.health || {} // 与 formatRecord 同一批就诊人字段:档案加载完成后覆盖订单快照, // 摘要行与首字牌因此需要一起重算 const patient = { name: profile.name || '', sexText: SEX_MAP[profile.sex] || '', age: this.calcAge(profile.birth), weight: health.weight || 0, height: health.height || 0, mobile: profile.mobile || '', idnumber: profile.idnumber || '' } this.setData({ 'record.patientName': patient.name, 'record.patientAge': patient.age, 'record.patientWeight': patient.weight, 'record.patientHeight': patient.height, 'record.patientMobile': patient.mobile, 'record.patientIdnumber': patient.idnumber, 'record.patientInitial': patient.name.charAt(0), 'record.patientMeta': this.buildPatientMeta(patient) }) }) .catch((err) => { console.log('获取就诊人档案失败,展示订单快照', err) }) }, formatRecord(item) { const status = item.status const paymentStatus = item.payment?.status || 'unpaid' const statusInfo = STATUS_MAP[status] || STATUS_MAP.pending const paymentInfo = PAYMENT_STATUS_MAP[paymentStatus] || PAYMENT_STATUS_MAP.unpaid // 底栏与「修改」入口的口径在这里算一次,WXML 只读结果,不再重复一串状态判断。 // 删除收窄为「未支付 且 未进入服务阶段」:进行中的订单已产生服务成本,不允许直接删 const canCancel = status === 'pending' || status === 'confirmed' const canDelete = paymentStatus === 'unpaid' && (status === 'pending' || status === 'confirmed' || status === 'cancelled') const canComplete = status === 'completed' const canRebook = status === 'completed' || status === 'cancelled' // 终态单据只读:医院预约与陪诊需求不允许回改(就诊人「修改」改的是档案,不在此列) const canEditOrder = status !== 'completed' && status !== 'cancelled' // 就诊人字段先落到局部变量:摘要行(patientMeta)与首字牌(patientInitial) // 都从同一批值派生,口径不在 WXML 与 JS 两处各写一份 const patient = { name: item.patient?.name || '', sexText: SEX_MAP[item.patient?.sex] || '', age: this.calcAge(item.patient?.birth), weight: item.patient?.weight || 0, height: item.patient?.height || 0, mobile: item.patient?.mobile || '', idnumber: item.patient?.idnumber || '' } // 预约时间派生:日期行(带星期)、时间行(时段 + 陪诊时长) const scheduleMeta = this.buildScheduleMeta(item.schedule || {}) return { _id: item._id, healthProfileId: item.healthProfileId || '', patientName: patient.name, patientAge: patient.age, patientMobile: patient.mobile, patientIdnumber: patient.idnumber, patientInitial: patient.name.charAt(0), patientMeta: this.buildPatientMeta(patient), hospitalProvince: item.hospital?.province || '', hospitalName: item.hospital?.name || '', hospitalAddress: item.hospital?.address || '', hospitalMeta: this.buildHospitalMeta(item.hospital || {}), // 以下三个原始字段不进展示(展示走 hospitalMeta),是编辑弹窗的表单回填源 hospitalDepartment: item.hospital?.department || '', hospitalDoctor: item.hospital?.doctor || '', medicalRecordNo: item.hospital?.medicalRecordNo || '', // scheduleDate 保留 YYYY-MM-DD 原格式:编辑弹窗的 date picker 直接喂它 scheduleDate: item.schedule?.date ? this.formatDate(item.schedule.date) : '', scheduleStartTime: item.schedule?.startTime || '', scheduleEndTime: item.schedule?.endTime || '', scheduleDateText: scheduleMeta.dateText, scheduleTimeText: scheduleMeta.timeText, escortServiceName: item.escort?.serviceName || '', sexRequirementText: SEX_REQUIREMENT_MAP[item.escort?.sexRequirement] || '', totalFee: (item.payment?.totalFee || 0).toFixed(2), paidFee: (item.payment?.paidFee || 0).toFixed(2), paymentStatus, paymentStatusText: paymentInfo.text, paymentStatusColor: paymentInfo.color, paymentStatusBg: paymentInfo.bg, paymentStatusBorder: paymentInfo.border, paymentHint: paymentInfo.hint || '', patientNote: item.notes?.patientNote || '', escortNote: item.notes?.escortNote || '', medicalSummary: item.notes?.medicalSummary || '', status, statusText: statusInfo.text, statusColor: statusInfo.color, statusBg: statusInfo.bg, statusBorder: statusInfo.border, statusDesc: statusInfo.desc, canCancel, canDelete, canComplete, canRebook, canEditOrder, // 底栏只在确实有按钮时出现,避免无动作状态空留一条白栏 showBottomBar: canCancel || canDelete || canComplete || canRebook, createTime: item.meta?.createtime ? this.formatDateTime(item.meta.createtime) : '', updateTime: item.meta?.updatetime ? this.formatDateTime(item.meta.updatetime) : '' } }, // 就诊人摘要行的字段序列:空值整项剔除,WXML 只负责在项之间插竖线, // 不靠一串 && 条件去猜哪条分隔线该出现(避免留下孤立的分隔线) buildPatientMeta(patient) { const list = [] if (patient.sexText) list.push(patient.sexText) if (patient.age) list.push(`${patient.age}岁`) if (patient.height) list.push(`身高 ${patient.height}cm`) if (patient.weight) list.push(`体重 ${patient.weight}kg`) return list }, // 医院元信息行:科室 / 医生 / 病历号,与就诊人摘要同一套插线逻辑; // 省份不进元信息行,由 hospitalProvince 单独提到卡内右上角 buildHospitalMeta(hospital) { const list = [] if (hospital.department) list.push(hospital.department) if (hospital.doctor) list.push(hospital.doctor) if (hospital.medicalRecordNo) list.push(`病历号 ${hospital.medicalRecordNo}`) return list }, // 预约时间派生:日期行(带星期)、时间行(时段 + 陪诊时长); // 日期无效或缺失时返回空串,由 WXML 走兜底文案 buildScheduleMeta(schedule) { const result = { dateText: '', timeText: '' } if (schedule.date) { const d = new Date(schedule.date) if (!isNaN(d.getTime())) { result.dateText = `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${WEEKDAYS[d.getDay()]}` } } const start = schedule.startTime || '' const end = schedule.endTime || '' let timeText = start && end ? `${start} - ${end}` : (start ? `${start} 开始` : '') const duration = schedule.duration || 0 if (timeText && duration > 0) { const hours = Math.floor(duration / 60) const minutes = duration % 60 const durationText = hours > 0 ? (minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时`) : `${minutes}分钟` timeText = `${timeText} · 陪诊${durationText}` } result.timeText = timeText return result }, // 根据出生年月(YYYY-MM-DD)计算年龄 calcAge(birth) { if (!birth) return 0 const d = new Date(birth) if (isNaN(d.getTime())) return 0 const now = new Date() let age = now.getFullYear() - d.getFullYear() const m = now.getMonth() - d.getMonth() if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age-- return age > 0 ? age : 0 }, formatDate(dateStr) { const d = new Date(dateStr) const year = d.getFullYear() const month = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${year}-${month}-${day}` }, formatDateTime(dateStr) { const d = new Date(dateStr) const year = d.getFullYear() const month = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') const hour = String(d.getHours()).padStart(2, '0') const minute = String(d.getMinutes()).padStart(2, '0') return `${year}-${month}-${day} ${hour}:${minute}` }, onCallPatient() { const mobile = this.data.record.patientMobile if (!mobile || mobile === '--') return wx.makePhoneCall({ phoneNumber: mobile }) }, // ===== 陪诊需求编辑(底部弹窗) ===== onEditPatientNote() { this.setData({ showNotePopup: true, editPatientNote: this.data.record.patientNote || '' }) }, onPatientNoteInput(e) { this.setData({ editPatientNote: e.detail.value }) }, onCancelEdit() { this.setData({ showNotePopup: false }) }, onSavePatientNote() { const note = this.data.editPatientNote.trim() wx.showLoading({ title: '保存中...' }) API.escort.updateRecord(this.data.recordId, { 'notes.patientNote': note }) .then((res) => { wx.hideLoading() if (res.code === 0) { wx.showToast({ title: '保存成功', icon: 'success' }) this.setData({ showNotePopup: false, 'record.patientNote': note }) } else { wx.showToast({ title: res.msg || '保存失败', icon: 'none' }) } }) .catch(() => { wx.hideLoading() wx.showToast({ title: '网络请求失败', icon: 'none' }) }) }, // ===== 医院预约信息编辑(底部弹窗) ===== // 打开弹窗:用当前展示值填充表单(scheduleDate 已是 YYYY-MM-DD,可直接喂给 date picker) onEditHospital() { const r = this.data.record const provinceIndex = Math.max(0, this.data.provinces.indexOf(r.hospitalProvince)) this.setData({ showHospitalPopup: true, editHospital: { province: r.hospitalProvince || '', provinceIndex, name: r.hospitalName || '', address: r.hospitalAddress || '', department: r.hospitalDepartment || '', doctor: r.hospitalDoctor || '', medicalRecordNo: r.medicalRecordNo || '', scheduleDate: r.scheduleDate || '', startTime: r.scheduleStartTime || '', endTime: r.scheduleEndTime || '' } }) }, // 文本字段统一走 data-field 分发,省掉每字段一个 handler onHospitalFieldInput(e) { const field = e.currentTarget.dataset.field this.setData({ [`editHospital.${field}`]: e.detail.value }) }, onProvinceChange(e) { const index = Number(e.detail.value) this.setData({ 'editHospital.provinceIndex': index, 'editHospital.province': this.data.provinces[index] }) }, onScheduleDateChange(e) { this.setData({ 'editHospital.scheduleDate': e.detail.value }) }, onStartTimeChange(e) { this.setData({ 'editHospital.startTime': e.detail.value }) }, onEndTimeChange(e) { this.setData({ 'editHospital.endTime': e.detail.value }) }, // 起止时间都在时重算陪诊时长(分钟);结束不晚于开始视为无效,拦在保存前 calcDuration(startTime, endTime) { if (!startTime || !endTime) return null const [sh, sm] = startTime.split(':').map(Number) const [eh, em] = endTime.split(':').map(Number) const diff = eh * 60 + em - (sh * 60 + sm) return diff > 0 ? diff : -1 }, onCancelHospitalEdit() { this.setData({ showHospitalPopup: false }) }, onSaveHospital() { const form = this.data.editHospital if (!form.name || !form.name.trim()) { wx.showToast({ title: '请输入就诊医院', icon: 'none' }) return } if (!form.scheduleDate) { wx.showToast({ title: '请选择预约日期', icon: 'none' }) return } const duration = this.calcDuration(form.startTime, form.endTime) if (duration === -1) { wx.showToast({ title: '结束时间需晚于开始时间', icon: 'none' }) return } const update = { 'hospital.province': form.province || '', 'hospital.name': form.name.trim(), 'hospital.address': (form.address || '').trim(), 'hospital.department': (form.department || '').trim(), 'hospital.doctor': (form.doctor || '').trim(), 'hospital.medicalRecordNo': (form.medicalRecordNo || '').trim(), // schedule.date 后端是 Date 类型,mongoose 会把 YYYY-MM-DD 字符串自动转换 'schedule.date': form.scheduleDate, 'schedule.startTime': form.startTime || '', 'schedule.endTime': form.endTime || '' } if (duration !== null) { update['schedule.duration'] = duration } wx.showLoading({ title: '保存中...' }) API.escort.updateRecord(this.data.recordId, update) .then((res) => { wx.hideLoading() if (res.code === 0) { wx.showToast({ title: '保存成功', icon: 'success' }) this.setData({ showHospitalPopup: false }) // 重新拉取,保证 formatRecord 出的展示字段(日历牌、时长)与库内一致 this.loadRecord(this.data.recordId) } else { wx.showToast({ title: res.msg || '保存失败', icon: 'none' }) } }) .catch(() => { wx.hideLoading() wx.showToast({ title: '网络请求失败', icon: 'none' }) }) }, // 就诊人信息统一在健康档案里维护,这里只负责跳转 onEditProfile() { const profileId = this.data.record.healthProfileId if (!profileId) { return } wx.navigateTo({ url: `/pages/health/profileedit?id=${profileId}` }) }, onCancelOrder() { this.setData({ showCancelPopup: true, cancelReason: '' }) }, onCloseCancelPopup() { this.setData({ showCancelPopup: false }) }, onSelectCancelReason(e) { this.setData({ cancelReason: e.currentTarget.dataset.reason }) }, onConfirmCancel() { if (!this.data.cancelReason) { wx.showToast({ title: '请选择取消原因', icon: 'none' }) return } wx.showLoading({ title: '处理中...' }) API.escort.updateStatus(this.data.recordId, { status: 'cancelled', reason: this.data.cancelReason }) .then((res) => { wx.hideLoading() if (res.code === 0) { wx.showToast({ title: '订单已取消', icon: 'success' }) this.setData({ showCancelPopup: false }) this.loadRecord(this.data.recordId) } else { wx.showToast({ title: res.msg || '操作失败', icon: 'none' }) } }) .catch(() => { wx.hideLoading() wx.showToast({ title: '网络请求失败', icon: 'none' }) }) }, onConfirmComplete() { wx.showModal({ title: '确认完成', content: '确认陪诊服务已完成?', confirmColor: '#B8520A', success: (res) => { if (res.confirm) { wx.showLoading({ title: '处理中...' }) API.escort.updateStatus(this.data.recordId, { status: 'completed' }) .then((res) => { wx.hideLoading() if (res.code === 0) { wx.showToast({ title: '已确认完成', icon: 'success' }) this.loadRecord(this.data.recordId) } else { wx.showToast({ title: res.msg || '操作失败', icon: 'none' }) } }) .catch(() => { wx.hideLoading() wx.showToast({ title: '网络请求失败', icon: 'none' }) }) } } }) }, onDeleteOrder() { wx.showModal({ title: '删除订单', content: '确定要删除该订单吗?删除后不可恢复。', confirmColor: '#B4231A', success: (res) => { if (res.confirm) { wx.showLoading({ title: '删除中...' }) API.escort.deleteRecord(this.data.recordId) .then((res) => { wx.hideLoading() if (res.code === 0) { wx.showToast({ title: '删除成功', icon: 'success' }) setTimeout(() => { wx.navigateBack() }, 1500) } else { wx.showToast({ title: res.msg || '删除失败', icon: 'none' }) } }) .catch(() => { wx.hideLoading() wx.showToast({ title: '网络请求失败', icon: 'none' }) }) } } }) }, onRebook() { wx.navigateTo({ url: `/pages/escort/itemlist` }) }, // 本页含就诊人姓名 / 手机号 / 身份证号与订单号,转发卡片不指向本页,统一落到首页; // 也不提供朋友圈分享——朋友圈只能分享当前页 URL,会把订单页地址带出去 onShareAppMessage() { return { title: '暖橙陪诊 - 专业陪诊服务', path: '/pages/home/index' } } })