基本重构完成

This commit is contained in:
lik
2026-10-07 14:55:06 +08:00
parent f655c906f5
commit f0adf77a41
23 changed files with 1284 additions and 685 deletions
+253 -91
View File
@@ -3,18 +3,22 @@ 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: '订单已确认,陪诊员将准时为您服务' },
// 待确认是唯一「等用户自己决定」的阶段(底栏此时一定有「取消订单」),
// 说明文字因此带上可执行动作,靠文字而不是靠再叠一层色块来提请注意
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 描边 + 透明底)
// 支付状态胶囊:未支付 / 部分支付复用主色浅档提示待处理;已退款走中性档(--muted 字 + --sep 描边 + 透明底);
// 已支付不显示胶囊(支付完成是常态,不需要状态强调),paid 档仅为映射兜底保留。
// hint 是未付清时的出口指引:全站没有线上支付入口,只能引导走客服;
// 部分支付要说清是「补齐」而不是「完成支付」,否则用户会以为要再付一次全款
const PAYMENT_STATUS_MAP = {
unpaid: { text: '未支付', color: '#B8520A', bg: '#FFF4E8', border: '#FFE0C2' },
partial: { text: '部分支付', color: '#B8520A', bg: '#FFF4E8', border: '#FFE0C2' },
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' }
}
@@ -30,22 +34,26 @@ const SEX_REQUIREMENT_MAP = {
female: '女'
}
const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
Page({
data: {
recordId: '',
record: {},
loading: true,
editing: false,
// 编辑全部收进底部弹窗:页面只读,表单只活在弹窗里,两个弹窗各自一个开关
showNotePopup: false,
editPatientNote: '',
editingPatient: false,
editPatient: {},
// 医院预约信息编辑弹窗:省份列表与下单页(itemdetail.js)保持同一份口径
showHospitalPopup: false,
provinces: ['北京市', '天津市', '河北省', '山西省', '内蒙古自治区', '辽宁省', '吉林省', '黑龙江省', '上海市', '江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省', '河南省', '湖北省', '湖南省', '广东省', '广西壮族自治区', '海南省', '重庆市', '四川省', '贵州省', '云南省', '西藏自治区', '陕西省', '甘肃省', '青海省', '宁夏回族自治区', '新疆维吾尔自治区', '台湾省', '香港特别行政区', '澳门特别行政区'],
editHospital: {},
showCancelPopup: false,
cancelReason: '',
cancelReasons: [
'临时有事,无法前往',
'病情好转,无需陪诊',
'医院停诊/改期',
'陪诊员无法到达',
'陪诊师无法到达',
'其他原因'
]
},
@@ -58,9 +66,9 @@ Page({
return
}
this.setData({ recordId: id })
this.loadRecord(id)
},
// onShow 在首次渲染时紧跟 onLoad 触发,加载只放在这里,避免进页面发两次请求
onShow() {
if (this.data.recordId) {
this.loadRecord(this.data.recordId)
@@ -75,13 +83,17 @@ Page({
}
},
// 渲染到位判据是 record._id(WXML 用它守内容),本方法只负责取数
loadRecord(id) {
this.setData({ loading: true })
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' })
}
@@ -89,60 +101,178 @@ Page({
.catch(() => {
wx.showToast({ title: '网络请求失败', icon: 'none' })
})
.finally(() => {
this.setData({ loading: false })
},
// 用档案最新信息覆盖订单里的 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 statusInfo = STATUS_MAP[item.status] || STATUS_MAP.pending
const paymentInfo = PAYMENT_STATUS_MAP[item.payment?.status] || PAYMENT_STATUS_MAP.unpaid
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,
patientName: item.patient?.name || '',
patientSexText: SEX_MAP[item.patient?.sex] || '',
patientBirth: item.patient?.birth || '',
patientAge: this.calcAge(item.patient?.birth),
patientWeight: item.patient?.weight || 0,
patientHeight: item.patient?.height || 0,
patientMobile: item.patient?.mobile || '',
patientIdnumber: item.patient?.idnumber || '',
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 || '',
scheduleDuration: item.schedule?.duration || 0,
scheduleDateText: scheduleMeta.dateText,
scheduleTimeText: scheduleMeta.timeText,
escortServiceName: item.escort?.serviceName || '',
attendantName: item.attendant?.name || '',
attendantMobile: item.attendant?.mobile || '',
sexRequirementText: SEX_REQUIREMENT_MAP[item.escort?.sexRequirement] || '',
totalFee: (item.payment?.totalFee || 0).toFixed(2),
paidFee: (item.payment?.paidFee || 0).toFixed(2),
paymentStatus: item.payment?.status || 'unpaid',
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: item.status,
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
@@ -179,19 +309,11 @@ Page({
wx.makePhoneCall({ phoneNumber: mobile })
},
onCallService() {
wx.makePhoneCall({ phoneNumber: '18618162956' })
},
onCallAttendant() {
const mobile = this.data.record.attendantMobile
if (!mobile || mobile === '--') return
wx.makePhoneCall({ phoneNumber: mobile })
},
// ===== 陪诊需求编辑(底部弹窗) =====
onEditPatientNote() {
this.setData({
editing: true,
showNotePopup: true,
editPatientNote: this.data.record.patientNote || ''
})
},
@@ -201,7 +323,7 @@ Page({
},
onCancelEdit() {
this.setData({ editing: false })
this.setData({ showNotePopup: false })
},
onSavePatientNote() {
@@ -215,7 +337,7 @@ Page({
if (res.code === 0) {
wx.showToast({ title: '保存成功', icon: 'success' })
this.setData({
editing: false,
showNotePopup: false,
'record.patientNote': note
})
} else {
@@ -228,74 +350,108 @@ Page({
})
},
onEditPatientInfo() {
const record = this.data.record
const now = new Date()
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
// ===== 医院预约信息编辑(底部弹窗) =====
// 打开弹窗:用当前展示值填充表单(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({
today,
editingPatient: true,
editPatient: {
name: record.patientName || '',
sex: record.patientSexText === '男' ? 'male' : record.patientSexText === '女' ? 'female' : '',
birth: record.patientBirth || '',
weight: record.patientWeight ? String(record.patientWeight) : '',
height: record.patientHeight ? String(record.patientHeight) : '',
mobile: record.patientMobile || '',
idnumber: record.patientIdnumber || ''
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 || ''
}
})
},
onPatientBirthChange(e) {
this.setData({ 'editPatient.birth': e.detail.value })
},
onPatientInfoInput(e) {
// 文本字段统一走 data-field 分发,省掉每字段一个 handler
onHospitalFieldInput(e) {
const field = e.currentTarget.dataset.field
const value = e.detail.value
this.setData({ [`editHospital.${field}`]: e.detail.value })
},
onProvinceChange(e) {
const index = Number(e.detail.value)
this.setData({
[`editPatient.${field}`]: value
'editHospital.provinceIndex': index,
'editHospital.province': this.data.provinces[index]
})
},
onPatientSexChange(e) {
const value = e.currentTarget.dataset.value
this.setData({
'editPatient.sex': value
})
onScheduleDateChange(e) {
this.setData({ 'editHospital.scheduleDate': e.detail.value })
},
onCancelEditPatientInfo() {
this.setData({ editingPatient: false })
onStartTimeChange(e) {
this.setData({ 'editHospital.startTime': e.detail.value })
},
onSavePatientInfo() {
const patient = this.data.editPatient
if (!patient.name.trim()) {
wx.showToast({ title: '请输入姓名', icon: 'none' })
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 (!patient.mobile.trim()) {
wx.showToast({ title: '请输入联系电话', icon: 'none' })
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, {
'patient.name': patient.name.trim(),
'patient.sex': patient.sex,
'patient.birth': patient.birth || '',
'patient.weight': parseFloat(patient.weight) || 0,
'patient.height': parseFloat(patient.height) || 0,
'patient.mobile': patient.mobile.trim(),
'patient.idnumber': patient.idnumber.trim()
})
API.escort.updateRecord(this.data.recordId, update)
.then((res) => {
wx.hideLoading()
if (res.code === 0) {
wx.showToast({ title: '保存成功', icon: 'success' })
this.setData({ editingPatient: false })
this.setData({ showHospitalPopup: false })
// 重新拉取,保证 formatRecord 出的展示字段(日历牌、时长)与库内一致
this.loadRecord(this.data.recordId)
} else {
wx.showToast({ title: res.msg || '保存失败', icon: 'none' })
@@ -307,6 +463,17 @@ Page({
})
},
// 就诊人信息统一在健康档案里维护,这里只负责跳转
onEditProfile() {
const profileId = this.data.record.healthProfileId
if (!profileId) {
return
}
wx.navigateTo({
url: `/pages/health/profileedit?id=${profileId}`
})
},
onCancelOrder() {
this.setData({
showCancelPopup: true,
@@ -349,10 +516,6 @@ Page({
})
},
onPayOrder() {
wx.showToast({ title: '支付功能开发中', icon: 'none' })
},
onConfirmComplete() {
wx.showModal({
title: '确认完成',
@@ -412,7 +575,6 @@ Page({
},
onRebook() {
const serviceId = this.data.record.escortServiceName
wx.navigateTo({
url: `/pages/escort/itemlist`
})