From f0adf77a4194ce55d945a92583968e6b4d2e4d51 Mon Sep 17 00:00:00 2001 From: lik Date: Wed, 7 Oct 2026 14:55:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E9=87=8D=E6=9E=84=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 62 +++- pages/attendant/attendantdetail.js | 31 ++ pages/attendant/attendantdetail.wxml | 6 +- pages/attendant/attendantlist.js | 36 +- pages/attendant/attendantlist.wxml | 5 + pages/escort/guide.js | 4 +- pages/escort/itemdetail.js | 112 +++--- pages/escort/itemdetail.wxml | 8 +- pages/escort/itemdetail.wxss | 16 + pages/escort/recorddetail.js | 344 +++++++++++++----- pages/escort/recorddetail.wxml | 472 ++++++++++++++----------- pages/escort/recorddetail.wxss | 509 +++++++++++++++++++-------- pages/escort/recordlist.js | 63 ++-- pages/escort/recordlist.wxml | 49 ++- pages/escort/recordlist.wxss | 72 ++-- pages/health/profileedit.wxml | 4 +- pages/health/profilelist.wxml | 10 +- pages/health/profilelist.wxss | 37 +- pages/mine/addressedit.js | 4 +- pages/mine/mine.js | 56 +-- pages/mine/profileedit.js | 7 +- utils/api.js | 11 +- utils/request.js | 51 ++- 23 files changed, 1284 insertions(+), 685 deletions(-) diff --git a/app.js b/app.js index 39b6efb..16cf4d5 100644 --- a/app.js +++ b/app.js @@ -1,8 +1,13 @@ const AIChatSocket = require('./utils/chatmsg.js') const API = require('./utils/api.js') +const request = require('./utils/request.js') App({ onLaunch() { + // request 层遇到 code 401(token 已失效)时回调这里:静默重登后自动重试原请求。 + // 登录逻辑只在 app 里有一份,request 层不碰 + request.setUnauthorizedHandler(() => this.silentSignin()) + let appId = wx.getStorageSync('attendant-appid') if (!appId) { appId = 'app_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9) @@ -63,39 +68,64 @@ App({ this.checkUpdate(); }, - onShow(options) { - const request = require('./utils/request.js') - // 用户主动退出过就不再静默重登:wxsignin 只凭 code 换到 openid 就能重新 - // 签发 token(api/user/handler/users.js 的 wxSignin),不拦这一下, - // 「我的」页的退出登录会被切后台/重进抵消。 - // 用户在「我的」页重新点登录并授权成功时会清掉这个标记 + onShow() { + this.silentSignin() + this.initChatSocket() + }, + + // 静默重登:app.onShow 与 request 层的 401 兜底共用这一个入口 + // (wxsignin 只凭 code 换到 openid 就能签发 token,见 api/user/handler/users.js)。 + // + // 单飞:并发只发一次——每次 wxsignin 都会重签 token 覆盖上一次, + // 两个请求重叠时 DB 里存的是后保存的那个、而 globalData 存的是后返回的那个, + // 一旦错位本机就拿着作废的 token,之后所有接口都 401。 + // + // 用户主动退出过(has_logout)就不再静默重登,否则「我的」页的退出登录 + // 会被切后台/重进抵消;用户在「我的」页重新授权成功时会清掉这个标记 + silentSignin() { if (wx.getStorageSync('has_logout')) { - this.initChatSocket() - return + return Promise.resolve(false) } - wx.login({ - success: res => { - if (res.code) { - request.post('https://api.huashengtec.com/user/wxsignin', { + if (this.signinPromise) { + return this.signinPromise + } + + this.signinPromise = new Promise((resolve) => { + wx.login({ + success: res => { + if (!res.code) { + console.log('登录失败!' + res.errMsg) + resolve(false) + return + } + API.user.wxSignin({ code: res.code }) .then((data) => { if (data.code == 0) { this.globalData.user = data.data.user + resolve(true) } else { console.log('error:', data) + resolve(false) } }) .catch((err) => { console.log('请求失败', err) + resolve(false) }) - } else { - console.log('登录失败!' + res.errMsg) + }, + fail: (err) => { + console.log('登录失败!' + err.errMsg) + resolve(false) } - } + }) + }).then((ok) => { + this.signinPromise = null + return ok }) - this.initChatSocket() + return this.signinPromise }, initChatSocket() { diff --git a/pages/attendant/attendantdetail.js b/pages/attendant/attendantdetail.js index e4d893f..4f4476b 100644 --- a/pages/attendant/attendantdetail.js +++ b/pages/attendant/attendantdetail.js @@ -1,5 +1,34 @@ const API = require('../../utils/api.js') +// 由 YYYY-MM-DD 生日换算周岁。 +// 后端只下发到「日」、不含时分秒——公示场景要的是"多大",不是哪一天生的。 +// 生日缺失 / 非法 / 算出不合常理(负数或 >120),一律返回 0,模板按 0 隐藏该项 +function calcAge(birthday) { + if (!birthday) { + return 0 + } + // 把 '-' 换成 '/' 再解析:小程序 iOS 对 'YYYY-MM-DD HH:mm:ss' 这类格式 + // 会得到 Invalid Date,'/' 分隔是各端都稳的写法 + const birth = new Date(String(birthday).replace(/-/g, '/')) + if (Number.isNaN(birth.getTime())) { + return 0 + } + + const today = new Date() + let age = today.getFullYear() - birth.getFullYear() + // 今年生日还没到,减一岁 + const monthDiff = today.getMonth() - birth.getMonth() + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { + age -= 1 + } + + // 上限 120 是给运营手抖填错年份兜底,否则页面上会出现"年龄 226 岁" + if (!Number.isFinite(age) || age < 0 || age > 120) { + return 0 + } + return age +} + // 陪诊师详情:数据来自后端公开接口 /health/attendant/:id // 与列表页是同一份白名单数据(不含手机号 / 身份证号 / 证书编号原文), // 差别在于详情页按 GB/T 47801—2026 4.2.1.5 把公示项补全: @@ -55,6 +84,8 @@ Page({ initial: name.slice(0, 1), // 后端 sex 为空串表示未登记,模板据此隐藏该行 sexText: item.sex === 'male' ? '男' : item.sex === 'female' ? '女' : '', + // 周岁由本页现算,未登记生日时为 0,模板据 0 隐藏该行 + age: calcAge(item.birthday), workYears: item.workYears || 0, orderCount: item.orderCount || 0, qualificationVerified: !!item.qualificationVerified, diff --git a/pages/attendant/attendantdetail.wxml b/pages/attendant/attendantdetail.wxml index f944522..b01d532 100644 --- a/pages/attendant/attendantdetail.wxml +++ b/pages/attendant/attendantdetail.wxml @@ -58,7 +58,7 @@ - + 服务信息 @@ -68,6 +68,10 @@ 性别 {{ attendant.sexText }} + + 年龄 + {{ attendant.age }} 岁 + 服务区域 {{ areaText }} diff --git a/pages/attendant/attendantlist.js b/pages/attendant/attendantlist.js index e53265a..ba50834 100644 --- a/pages/attendant/attendantlist.js +++ b/pages/attendant/attendantlist.js @@ -1,8 +1,38 @@ const API = require('../../utils/api.js') // 陪诊师公示列表:数据来自后端公开接口 /health/attendant -// 该接口已做脱敏(不含手机号 / 身份证号 / 证书编号原文), -// 前端不再做二次裁剪,只把后端字段整形为展示所需结构 +// 该接口已做脱敏(不含手机号 / 身份证号 / 证书编号原文,生日只下发到日), +// 前端不做二次裁剪,只把后端字段整形为展示所需结构(周岁由生日现算) + +// 由 YYYY-MM-DD 生日换算周岁。 +// 后端只下发到「日」、不含时分秒——公示场景要的是"多大",不是哪一天生的。 +// 生日缺失 / 非法 / 算出不合常理(负数或 >120),一律返回 0,模板按 0 隐藏该项 +function calcAge(birthday) { + if (!birthday) { + return 0 + } + // 把 '-' 换成 '/' 再解析:小程序 iOS 对 'YYYY-MM-DD HH:mm:ss' 这类格式 + // 会得到 Invalid Date,'/' 分隔是各端都稳的写法 + const birth = new Date(String(birthday).replace(/-/g, '/')) + if (Number.isNaN(birth.getTime())) { + return 0 + } + + const today = new Date() + let age = today.getFullYear() - birth.getFullYear() + // 今年生日还没到,减一岁 + const monthDiff = today.getMonth() - birth.getMonth() + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { + age -= 1 + } + + // 上限 120 是给运营手抖填错年份兜底,否则页面上会出现"年龄 226 岁" + if (!Number.isFinite(age) || age < 0 || age > 120) { + return 0 + } + return age +} + Page({ data: { loading: true, @@ -36,6 +66,8 @@ Page({ // 头像兜底用的首字:姓名已脱敏为「姓 + 陪诊师」,取第一个字即可 initial: (item.name || '').slice(0, 1), workYears: item.workYears || 0, + // 周岁由本页现算,未登记生日时为 0,模板据 0 隐藏该段 + age: calcAge(item.birthday), orderCount: item.orderCount || 0, qualificationVerified: !!item.qualificationVerified, // 擅长场景最多展示 3 个,避免长尾把卡片撑高;完整列表在详情页 diff --git a/pages/attendant/attendantlist.wxml b/pages/attendant/attendantlist.wxml index 3d4af0c..bbb9815 100644 --- a/pages/attendant/attendantlist.wxml +++ b/pages/attendant/attendantlist.wxml @@ -48,6 +48,11 @@ 从业 {{ item.workYears }} 年 + + + + {{ item.age }} 岁 + 已服务 {{ item.orderCount }} 次 diff --git a/pages/escort/guide.js b/pages/escort/guide.js index e264ffd..7214b80 100644 --- a/pages/escort/guide.js +++ b/pages/escort/guide.js @@ -48,7 +48,7 @@ Page({ // 原「需要空腹的项目,就诊当天请勿进食进水」是替医院下医嘱; // 要求来源交回医院,与 id 4「请提前了解…要求」同口径 '部分检查要求空腹(禁食禁水),请以医院的检查前通知为准', - '提前与陪诊师确认医院、院区与集合地点' + '提前与陪诊师确认医院、院区、集合地点与时间' ], // 温馨提示:通用提醒,不涉及具体退款或收费规则 tips: [ @@ -59,7 +59,7 @@ Page({ // 8.2 不应签署具有法律效力的医疗文书 '具有法律效力的医疗文书(手术、麻醉、特殊检查知情同意书等)需您本人或家属签署,陪诊师不代签', // 全站统一时限,与服务页 notices、协议第 2 节一致(原「请尽早联系客服调整」没给时限,等于缺公示) - '如需改期或取消,请至少提前4小时联系客服' + '如需改期或取消,请至少提前1天联系客服' ] }, diff --git a/pages/escort/itemdetail.js b/pages/escort/itemdetail.js index 3a788ee..6d02519 100644 --- a/pages/escort/itemdetail.js +++ b/pages/escort/itemdetail.js @@ -16,9 +16,11 @@ Page({ totalPrice: '0.00', showOrderPopup: false, profiles: [], // 已保存的就诊人 - selectedProfileId: '', // 当前选中的就诊人档案 ID,为空表示手动填写 - patientName: '', - patientPhone: '', + selectedProfileId: '', // 必填:订单必须关联就诊人档案 + patientName: '', // 只读展示,来自所选档案 + patientPhone: '', // 只读展示,来自所选档案 + // 选中档案的全部信息,下单时整体写入订单 patient 快照 + selectedPatient: null, // 默认北京:取值必须与 provinces[0] 完全一致('北京市' 而非 '北京'), // 否则 picker 的 value=0 与显示文案会对不上 province: '北京市', @@ -99,11 +101,12 @@ Page({ return } - const user = app.globalData.user + // 订单必须关联健康档案,就诊人不再手输:打开弹窗时清空,等用户选择就诊人后带入 this.setData({ - patientName: user.profile?.name || '', - patientPhone: user.profile?.mobile || '', + patientName: '', + patientPhone: '', selectedProfileId: '', + selectedPatient: null, showOrderPopup: true }) @@ -120,11 +123,21 @@ Page({ if (res.code !== 0) { return } - const list = ((res.data && res.data.list) || []).map((item) => ({ - _id: item._id, - name: item.profile?.name || '', - mobile: item.profile?.mobile || '' - })) + // 档案是 profile + health 两层嵌套,这里拍平成选人后可直接带入的字段 + const list = ((res.data && res.data.list) || []).map((item) => { + const profile = item.profile || {} + const health = item.health || {} + return { + _id: item._id, + name: profile.name || '', + mobile: profile.mobile || '', + sex: profile.sex || '', + birth: profile.birth || '', + weight: health.weight || 0, + height: health.height || 0, + idnumber: profile.idnumber || '' + } + }) this.setData({ profiles: list }) @@ -134,7 +147,7 @@ Page({ }) }, - // 选择就诊人:自动带入姓名与联系电话 + // 选择就诊人:姓名/电话回填展示,其余字段存入 selectedPatient 供下单使用 onSelectProfile(e) { const id = e.currentTarget.dataset.id const profile = this.data.profiles.find((item) => item._id === id) @@ -144,8 +157,16 @@ Page({ this.setData({ selectedProfileId: id, + // patientName / patientPhone 只用于弹窗内只读展示 patientName: profile.name, - patientPhone: profile.mobile + patientPhone: profile.mobile, + // 订单 patient 快照的唯一来源:所选档案的名单字段(体征与证件号不进快照) + selectedPatient: { + name: profile.name, + mobile: profile.mobile, + sex: profile.sex, + birth: profile.birth + } }) }, @@ -162,21 +183,6 @@ Page({ }) }, - // 手动修改姓名/电话后,不再与所选档案对应,清空选择避免存档信息不一致 - onPatientNameChange(e) { - this.setData({ - patientName: e.detail.value, - selectedProfileId: '' - }) - }, - - onPatientPhoneChange(e) { - this.setData({ - patientPhone: e.detail.value, - selectedProfileId: '' - }) - }, - onProvinceChange(e) { const index = e.detail.value const province = this.data.provinces[index] @@ -228,8 +234,8 @@ Page({ submitOrder() { const { service, - patientName, - patientPhone, + selectedProfileId, + selectedPatient, province, hospital, department, @@ -239,16 +245,10 @@ Page({ remark } = this.data - if (!patientName.trim()) { + // 订单必须关联就诊人:没有档案就没有 patient 快照,不允许下单 + if (!selectedProfileId || !selectedPatient) { wx.showToast({ - title: '请输入就诊人姓名', - icon: 'none' - }) - return - } - if (!patientPhone.trim()) { - wx.showToast({ - title: '请输入联系电话', + title: '请选择就诊人', icon: 'none' }) return @@ -283,9 +283,9 @@ Page({ } const phoneRegex = /^1[3-9]\d{9}$/ - if (!phoneRegex.test(patientPhone.trim())) { + if (!phoneRegex.test(selectedPatient.mobile.trim())) { wx.showToast({ - title: '请输入正确的手机号', + title: '就诊人手机号有误,请到就诊人档案修改', icon: 'none' }) return @@ -297,12 +297,22 @@ Page({ if (gender == '男') sex = 'male' else if (gender == '女') sex = 'female' + // 就诊人快照只取名单字段(schema 口径:name/mobile/sex/birth); + // sex 是 male/female 枚举,空值不写该字段 + const patient = { + name: selectedPatient.name.trim(), + mobile: selectedPatient.mobile.trim(), + birth: selectedPatient.birth || '' + } + if (selectedPatient.sex) { + patient.sex = selectedPatient.sex + } + const orderData = { userId: user._id, - patient: { - name: patientName.trim(), - mobile: patientPhone.trim(), - }, + // 订单与就诊人档案的关联,必填(patient 只是下单当刻的快照) + healthProfileId: selectedProfileId, + patient: patient, escort: { serviceId: service.id, serviceName: service.title, @@ -325,11 +335,6 @@ Page({ } } - // 选择了已保存的就诊人时,一并记录档案 ID - if (this.data.selectedProfileId) { - orderData.healthProfileId = this.data.selectedProfileId - } - wx.showLoading({ title: '提交中...' }) @@ -346,7 +351,8 @@ Page({ showOrderPopup: false }) this.resetForm() - wx.navigateTo({url: '/pages/escort/recordlist?tab=pending'}) + // 下单成功回到记录页,直接落到「进行中的」:新单是待确认,属未完成档 + wx.navigateTo({url: '/pages/escort/recordlist?tab=ongoing'}) } else { wx.showToast({ title: data.msg || '下单失败', @@ -369,6 +375,7 @@ Page({ selectedProfileId: '', patientName: '', patientPhone: '', + selectedPatient: null, // 重置后仍回到默认北京,与 data 初值保持一致 province: '北京市', provinceIndex: 0, @@ -376,7 +383,8 @@ Page({ department: '', appointmentDate: '', appointmentTime: '', - gender: '', + // 重置后回到默认「不限」,与 data 初值(gender: '不限' / genderIndex: 0)保持一致 + gender: '不限', genderIndex: 0, remark: '' }) diff --git a/pages/escort/itemdetail.wxml b/pages/escort/itemdetail.wxml index 4afe154..6299e3c 100644 --- a/pages/escort/itemdetail.wxml +++ b/pages/escort/itemdetail.wxml @@ -91,7 +91,7 @@ - + 选择就诊人 @@ -112,16 +112,16 @@ - + 就诊人姓名 - + {{patientName || '请先选择就诊人'}} 联系电话 - + {{patientPhone || '请先选择就诊人'}} diff --git a/pages/escort/itemdetail.wxss b/pages/escort/itemdetail.wxss index 1040569..a719eb4 100644 --- a/pages/escort/itemdetail.wxss +++ b/pages/escort/itemdetail.wxss @@ -462,6 +462,22 @@ page { font-weight: 400; } +/* 只读值(就诊人姓名/电话):与 .order-input 同形,仅去掉输入框语义 */ +.order-value { + flex: 1; + min-width: 0; + margin-left: 24rpx; + text-align: right; + font-size: 28rpx; + font-weight: 700; + color: var(--ink); +} + +.order-value.placeholder { + color: var(--muted); + font-weight: 400; +} + /* picker 需要占满剩余宽度,值才能靠右对齐 */ .order-picker { flex: 1; diff --git a/pages/escort/recorddetail.js b/pages/escort/recorddetail.js index 66e0c0b..472d91f 100644 --- a/pages/escort/recorddetail.js +++ b/pages/escort/recorddetail.js @@ -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` }) diff --git a/pages/escort/recorddetail.wxml b/pages/escort/recorddetail.wxml index 08b3e2c..91b062f 100644 --- a/pages/escort/recorddetail.wxml +++ b/pages/escort/recorddetail.wxml @@ -1,8 +1,10 @@ - + + - + @@ -10,193 +12,107 @@ class="status-chip status-chip-lg" style="background: {{record.statusBg}}; color: {{record.statusColor}}; border-color: {{record.statusBorder}};" >{{record.statusText}} + {{record.statusDesc}} - {{record.statusDesc}} - + + + 就诊人信息 - + 修改 - + - - - 姓名 - {{record.patientName || '--'}} - - - 性别 - {{record.patientSexText || '--'}} - - - 年龄 - {{record.patientAge ? record.patientAge + '岁' : '--'}} - - - 体重 - {{record.patientWeight ? record.patientWeight + 'kg' : '--'}} - - - 身高 - {{record.patientHeight ? record.patientHeight + 'cm' : '--'}} - - - 联系电话 - {{record.patientMobile || '--'}} - - - 身份证号 - {{record.patientIdnumber}} - - + + + + + + {{record.patientInitial}} + + + + {{record.patientName || '--'}} - - - 姓名 - - - - 性别 - - 男 - 女 + + + + + {{item}} + + + + 身份证号 {{record.patientIdnumber}} - - 出生年月 - - {{editPatient.birth || '请选择'}} - + + + + + {{record.patientMobile || '--'}} - - 体重(kg) - - - - 身高(cm) - - - - 联系电话 - - - - 身份证号 - - - - 取消 - 保存 - - + - + 医院预约信息 + + 修改 + + - - 就诊省份 - {{record.hospitalProvince || '--'}} - - - 就诊医院 - {{record.hospitalName || '--'}} - - - 医院地址 - {{record.hospitalAddress}} - - - 就诊科室 - {{record.hospitalDepartment || '--'}} - - - 就诊医生 - {{record.hospitalDoctor}} - - - 病历号 - {{record.medicalRecordNo}} - - - 预约日期 - {{record.scheduleDate || '--'}} - - - 预约时间 - {{record.scheduleStartTime || '--'}} - - - 结束时间 - {{record.scheduleEndTime}} - - - 陪诊时长 - {{record.scheduleDuration}}分钟 + + + + + {{record.hospitalName || '就诊医院待补充'}} + {{record.hospitalProvince}} + + + + + + {{item}} + + + + {{record.hospitalAddress}} + + + + {{record.scheduleDateText || '预约日期待定'}} + {{record.scheduleTimeText}} + - - - - - 陪诊服务 - - - 服务类型 - {{record.escortServiceName || '陪诊服务'}} - - - 性别要求 - {{record.sexRequirementText}} - - - 陪诊员 - {{record.attendantName || '待分配'}} - - - 陪诊员电话 - {{record.attendantMobile}} - - - - + 陪诊需求与症状 - + 修改 - + - -