Files
wxapp_escort/pages/escort/recorddetail.js
T

592 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
}
}
})