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 @@
-
+
+
-
+
-
-
+
+
+
就诊人信息
-
+
修改
-
+
-
-
- 姓名
- {{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}}
-
-
-
-
+
陪诊需求与症状
-
+
修改
-
+
-
-
-
- 取消
- 保存
-
-
-
-
+
{{record.patientNote}}
@@ -211,88 +127,236 @@
暂无备注信息
-
+
-
+
- 费用信息
+ 服务与费用
-
- 服务费用
- ¥{{record.totalFee}}
-
-
- 已支付
- ¥{{record.paidFee}}
-
-
- 支付状态
- {{record.paymentStatusText}}
+
+
+ {{record.escortServiceName || '陪诊服务'}}
+
+ {{record.paymentStatusText}}
+
+
+ 陪诊师性别要求 {{record.sexRequirementText}}
+
+
+
+ 服务费用
+ ¥{{record.totalFee}}
+
+ 已支付 ¥{{record.paidFee}}
+
+
+
+ {{record.paymentHint}}
-
+
订单信息
-
- 订单编号
- {{record._id || '--'}}
-
-
- 订单状态
- {{record.statusText}}
-
-
- 创建时间
- {{record.createTime || '--'}}
-
-
- 更新时间
- {{record.updateTime || '--'}}
+
+ 订单编号 {{record._id || '--'}}
+
+ 创建于 {{record.createTime || '--'}}
+
+ 更新于 {{record.updateTime || '--'}}
+
-
-
+
+
-
+ 再次预约为「已完成」时让位给「确认完成」,为「已取消」时自己成为主操作(§2.1)。
+ 四种动作都为假时整条栏不出现:进行中且已支付这类状态没有动作可给,不留空白栏 -->
+
取消订单
删除订单
确认完成
再次预约
+
+
+
+
+
+
+
+
- 确定要取消该陪诊订单吗?取消后陪诊员将收到通知。
+ 确定要取消该陪诊订单吗?取消后陪诊师将收到通知。
确认取消
-
\ No newline at end of file
+
diff --git a/pages/escort/recorddetail.wxss b/pages/escort/recorddetail.wxss
index d8f61b2..bbe534c 100644
--- a/pages/escort/recorddetail.wxss
+++ b/pages/escort/recorddetail.wxss
@@ -1,12 +1,17 @@
/* pages/escort/recorddetail.wxss
- 设计方向:本页是一张陪诊订单的详情,按「状态 → 就诊人 → 医院 → 服务 → 需求 → 费用 → 订单」的顺序铺开。
- - 配色并入全站暖橙体系(doc/DESIGN.md):令牌来自 app.wxss,本页不再留副本;
+ 设计方向:本页是一张陪诊订单的详情,按「状态抬头 → 就诊人 → 医院预约 → 需求与症状 → 服务与费用 → 订单」铺开。
+ - 配色并入全站暖橙体系(doc/DESIGN.md):令牌来自 app.wxss,本页不留副本;
状态色只用 §三 四档,旧版渐变横幅与金色体系全部移除
- - 头部:短横色标 + 状态胶囊 + 状态说明(内页规范,不做整块主色面板)
- - 分区:发丝线切分,标题走统一的 8×34rpx 单竖条 .section-bar
- - 表单:标签 --muted + 值 --ink 700;选中项走浅橙底 + 实心橙描边
- - 按钮:底栏主操作实心胶囊,其余为空心胶囊;同类实心块全页只保留一处(§2.1)
- - 字号阶梯:34 分节标题 / 28 正文 · 表单 / 26 标签 / 24 元信息,最小 24rpx(12px) */
+ - 抬头:短横色标 + 状态胶囊 + 状态说明,是唯一的非卡片区(内页规范,不做整块主色面板);
+ 下方用 2rpx --line 通栏发丝线收口,靠字号两档差(32 胶囊 / 26 说明)与留白立住,不套第二套容器
+ - 分区:标题在纸面上(8×32rpx 单竖条),内容统一收进同一种白卡
+ (§2.3:2rpx --line + 中性阴影 --shadow-card;圆角本页取 24rpx 为例外,见下方 .card 注释)
+ - 展示去表单化:不铺「标签左/值右」清单行,层级由结构给出——
+ 就诊人用首字牌、医院预约用预约时间条、费用用字号差;元信息一律竖线分隔行
+ - 编辑全部收进底部弹窗:页面只读,表单只活在弹窗里
+ - 按钮:底栏与弹窗的主操作实心胶囊、其余空心胶囊,同类实心块全页只保留一处(§2.1)
+ - 字号阶梯:40 首字牌 / 32 分节标题 · 卡片主名 · 金额 · 状态胶囊 / 28 服务名 · 备注正文 · 预约日期
+ / 26 次强 · 「修改」入口 · 预约时段 · 订单号 / 24 元信息 · 已支付 · 支付指引,最小 24rpx(12px) */
page {
background-color: var(--paper);
@@ -23,9 +28,15 @@ page {
padding: 0 32rpx 40rpx;
}
-/* ===== 头部状态 ===== */
+/* ===== 抬头状态 ===== */
.page-header {
- padding-top: 32rpx;
+ /* 抬头自成一区,靠空间 + 一条线立住,不引入第二套容器语言:
+ 上方留白 40rpx(比卡间距 48rpx 略紧,让抬头贴住顶栏而非浮在中间);
+ 左右负 margin 抵消 .container 的 32rpx 内距,使下方发丝线通栏顶到屏幕两侧,
+ 同时用 padding 把色标与胶囊重新拉回卡片同一条左对齐线 */
+ margin: 0 -32rpx;
+ padding: 40rpx 32rpx 28rpx;
+ border-bottom: 2rpx solid var(--line);
}
.header-accent {
@@ -37,6 +48,8 @@ page {
.status-row {
margin-top: 22rpx;
+ display: flex;
+ align-items: center;
}
/* 状态胶囊:底色 + 描边 + 字色三件套,颜色只做强化,文字才是信息(§三) */
@@ -55,42 +68,46 @@ page {
.status-chip-lg {
padding: 8rpx 22rpx;
- font-size: 28rpx;
+ /* 28rpx 抬到 32rpx:与分节标题、卡片主名同档。
+ 同字号下胶囊靠状态底色 + 2rpx 描边 + 内距压过裸字标题,
+ 层级才回到「单据抬头 > 卡片标题」,不再是从前抬头比它统领的内容还小 */
+ font-size: 32rpx;
}
.header-subtitle {
- display: block;
- margin-top: 14rpx;
- font-size: 28rpx;
+ margin-left: 20rpx;
+ /* 28rpx 降到 26rpx:说明退到「次强」档,与 32rpx 的胶囊拉开一档差,
+ 抬头内部才有焦点;此前两者同为 28rpx,只靠字重区分,谁都不是主角 */
+ font-size: 26rpx;
color: var(--muted);
line-height: 1.6;
letter-spacing: 1rpx;
}
-/* ===== 分区:发丝线 + 单竖条标题 ===== */
+/* ===== 分区:纸面留白分隔,标题在纸面上、内容在白卡里 ===== */
.section {
- margin-top: 44rpx;
- padding-top: 30rpx;
- border-top: 2rpx solid var(--line);
+ margin-top: 48rpx;
}
.section-head {
display: flex;
align-items: center;
- margin-bottom: 6rpx;
}
.section-bar {
flex-shrink: 0;
width: 8rpx;
- height: 34rpx;
+ /* 色标高度跟随标题字号,缩字号时同步收,保持「短横 + 标题」的等高一档 */
+ height: 32rpx;
border-radius: 4rpx;
background: var(--brand-strong);
}
+/* 34rpx 降一档到 32rpx:与 attendantdetail / agreement / about 的分区标题同一档,
+ 不新增字号档位(DESIGN.md 字号阶梯) */
.section-title {
margin-left: 16rpx;
- font-size: 34rpx;
+ font-size: 32rpx;
font-weight: 700;
color: var(--ink);
letter-spacing: 1rpx;
@@ -108,166 +125,294 @@ page {
opacity: 0.6;
}
+/* 「修改」降为中性字:全页三处入口若都用暖橙,会和所在行的标题抢注意力,
+ 一个分组标题下不该有两个同权重焦点。入口靠位置(标题行右端)和热区成立,
+ 不靠颜色——颜色留给底栏主按钮 */
.section-link-text {
margin-right: 4rpx;
- font-size: 24rpx;
- color: var(--brand-ink);
-}
-
-/* ===== 信息行(只读) ===== */
-.info-row {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- padding: 18rpx 0;
- border-bottom: 2rpx solid var(--line);
-}
-
-.info-row:last-child {
- border-bottom: none;
-}
-
-.info-label {
- flex-shrink: 0;
+ /* 26rpx 与图标同档:入口降为中性的同时留一点体量,
+ 避免和卡片正文(26rpx)拉开层级后又退到看不清 */
font-size: 26rpx;
color: var(--muted);
}
-.info-value {
+/* ===== 白卡:全页唯一的容器语言(§2.3 主规则) =====
+ 2rpx --line 发丝边、中性阴影 --shadow-card;
+ 卡内首尾留白由卡片自己的 padding 给。
+ 本页例外(已登记):圆角取 24rpx 而非 §2.3 的 28rpx——
+ 与卡内首字牌(24rpx)同档,圆角家族内部不倒挂;
+ 注意全站其余九页的白卡仍是 28rpx,从订单列表切进本页时卡片会略方一点 */
+.card {
+ box-sizing: border-box;
+ margin-top: 16rpx;
+ padding: 20rpx 24rpx;
+ background: var(--white);
+ border: 2rpx solid var(--line);
+ border-radius: 24rpx;
+ box-shadow: var(--shadow-card);
+}
+
+/* ===== 通用元信息行:竖线分隔,空值已在 JS 整项剔除 ===== */
+.meta-list {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.meta-item {
+ font-size: 24rpx;
+ color: var(--muted);
+ letter-spacing: 1rpx;
+ line-height: 1.6;
+}
+
+/* 元信息竖线:2rpx×20rpx,取 --sep(比 --line 深一档,白卡与纸面上都看得见) */
+.meta-sep {
+ flex-shrink: 0;
+ width: 2rpx;
+ height: 20rpx;
+ margin: 0 14rpx;
+ background: var(--sep);
+}
+
+/* ===== 就诊人卡:首字牌 + 元信息行 + 发丝线下的拨打动作 ===== */
+.patient-head {
+ display: flex;
+ align-items: center;
+}
+
+/* 首字牌:浅橙衬底 + --brand-line 描边,与陪诊师列表的兜底头像同源 */
+.patient-mark {
+ box-sizing: border-box;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 88rpx;
+ height: 88rpx;
+ border: 2rpx solid var(--brand-line);
+ border-radius: 24rpx;
+ background: var(--brand-soft);
+}
+
+.patient-mark-text {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: var(--brand-ink);
+ letter-spacing: 1rpx;
+}
+
+/* 首字牌右侧纵列:姓名 / 体征元信息 / 证件号自上而下 */
+.patient-info {
flex: 1;
min-width: 0;
- margin-left: 24rpx;
- text-align: right;
- font-size: 28rpx;
- color: var(--ink-2);
+ margin-left: 20rpx;
+}
+
+/* 姓名 32rpx/700:低于区块标题 34rpx 一档,不与标题争层级 */
+.patient-name {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: var(--ink);
+ letter-spacing: 1rpx;
+}
+
+.patient-meta-row {
+ margin-top: 10rpx;
+}
+
+/* 身份证 18 位,折行而不是撑破卡片 */
+.patient-id {
+ display: block;
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ color: var(--muted);
+ letter-spacing: 1rpx;
word-break: break-all;
}
-.info-link {
+/* 发丝线之上是「是谁」,之下是「怎么联系」;整行即热区 */
+.patient-contact {
+ display: flex;
+ align-items: center;
+ margin-top: 22rpx;
+ padding-top: 20rpx;
+ border-top: 2rpx solid var(--line);
+}
+
+.patient-contact-text {
+ margin-left: 10rpx;
+ font-size: 28rpx;
+ font-weight: 700;
color: var(--brand-ink);
- font-weight: 700;
+ letter-spacing: 1rpx;
}
-.fee-value {
+.patient-contact:active {
+ opacity: 0.6;
+}
+
+/* ===== 医院预约卡:日历牌是全卡视觉锚点 ===== */
+.hospital-head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+}
+
+.hospital-name {
+ flex: 1;
+ min-width: 0;
+ font-size: 32rpx;
+ font-weight: 700;
+ color: var(--ink);
+ letter-spacing: 1rpx;
+ line-height: 1.4;
+}
+
+.hospital-name.is-empty {
+ font-weight: 400;
+ color: var(--muted);
+}
+
+.hospital-province {
+ margin-left: 20rpx;
+ font-size: 24rpx;
+ color: var(--muted);
+ letter-spacing: 1rpx;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+.hospital-meta {
+ margin-top: 12rpx;
+}
+
+.hospital-address {
+ display: block;
+ margin-top: 10rpx;
+ font-size: 24rpx;
+ color: var(--muted);
+ letter-spacing: 1rpx;
+ line-height: 1.5;
+}
+
+/* 预约时间条:发丝线之上是「去哪看」,之下是「什么时候看」——日期左、时段右 */
+.schedule-row {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-top: 22rpx;
+ padding-top: 20rpx;
+ border-top: 2rpx solid var(--line);
+}
+
+.schedule-date {
+ flex: 1;
+ min-width: 0;
+ font-size: 28rpx;
+ font-weight: 700;
+ color: var(--ink);
+ letter-spacing: 1rpx;
+}
+
+.schedule-date.is-empty {
+ font-weight: 400;
+ color: var(--muted);
+}
+
+.schedule-time {
+ margin-left: 20rpx;
+ font-size: 26rpx;
+ font-weight: 700;
+ color: var(--brand-ink);
+ letter-spacing: 1rpx;
+ white-space: nowrap;
+}
+
+/* ===== 服务与费用卡:金额是全卡唯一的大字号焦点 ===== */
+.service-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.service-name {
+ font-size: 28rpx;
+ font-weight: 700;
+ color: var(--ink);
+ letter-spacing: 1rpx;
+}
+
+.service-meta {
+ margin-top: 10rpx;
+}
+
+.fee-row {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-top: 18rpx;
+ padding-top: 18rpx;
+ border-top: 2rpx solid var(--line);
+}
+
+.fee-main {
+ display: flex;
+ align-items: baseline;
+}
+
+.fee-label {
+ font-size: 26rpx;
+ color: var(--muted);
+}
+
+.fee-amount {
+ margin-left: 16rpx;
+ /* 34rpx 降一档到 32rpx,与分区标题同一档,不新增字号档位;
+ 仍明显高于标签(26rpx)与「已支付」(24rpx),保住金额在卡内的大字号焦点地位 */
+ font-size: 32rpx;
+ font-weight: 700;
color: var(--brand-strong);
- font-weight: 700;
+ letter-spacing: 1rpx;
}
-.order-id {
+.fee-paid {
font-size: 24rpx;
color: var(--muted);
letter-spacing: 1rpx;
}
-/* ===== 表单行(编辑态) ===== */
-.edit-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20rpx 0;
- border-bottom: 2rpx solid var(--line);
-}
-
-.edit-label {
- flex-shrink: 0;
- font-size: 28rpx;
+/* 未付清时的出口指引:与元信息同一档(24rpx / --muted),只做说明不做按钮——
+ 付款动作发生在客服侧,本页给不出可点的东西,硬做描边按钮会是空动作 */
+.pay-hint {
+ display: block;
+ margin-top: 12rpx;
+ font-size: 24rpx;
color: var(--muted);
+ letter-spacing: 1rpx;
+ line-height: 1.6;
}
-.edit-picker {
- flex: 1;
- min-width: 0;
- margin-left: 24rpx;
-}
-
-/* 选择器内层已由 .edit-picker 让出间距,内层值不再重复缩进 */
-.edit-picker .edit-input {
- margin-left: 0;
-}
-
-.edit-input {
- flex: 1;
- min-width: 0;
- margin-left: 24rpx;
- font-size: 28rpx;
- font-weight: 700;
- color: var(--ink);
- text-align: right;
-}
-
-.edit-input.placeholder {
- font-weight: 400;
- color: var(--muted);
-}
-
-.input-placeholder {
- color: var(--muted);
-}
-
-.edit-radio-group {
- display: flex;
- gap: 16rpx;
-}
-
-/* 选中项:浅橙底 + 实心橙描边(表单选中语言) */
-.edit-radio {
- padding: 8rpx 28rpx;
- border: 2rpx solid var(--line);
- border-radius: 999rpx;
+/* ===== 订单卡:编号为主,时间收进元信息行 ===== */
+.order-no {
+ display: block;
font-size: 26rpx;
- color: var(--muted);
- background: transparent;
+ color: var(--ink-2);
+ letter-spacing: 1rpx;
+ word-break: break-all;
}
-.edit-radio.active {
- background: var(--brand-soft);
- border-color: var(--brand-strong);
- color: var(--brand-ink);
- font-weight: 700;
+.order-meta {
+ margin-top: 10rpx;
}
-/* ===== 按键:空心胶囊 / 实心胶囊 ===== */
-.edit-actions {
- display: flex;
- justify-content: flex-end;
- gap: 16rpx;
- margin-top: 28rpx;
-}
-
-.btn {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 16rpx 44rpx;
- border-radius: 999rpx;
- font-size: 28rpx;
- line-height: 1.4;
-}
-
-.btn:active {
- opacity: 0.7;
-}
-
-.btn-ghost {
- border: 3rpx solid var(--sep);
- color: var(--muted);
- background: transparent;
-}
-
-.btn-primary {
- background: var(--brand-strong);
- color: var(--white);
- font-weight: 700;
- box-shadow: var(--shadow-cta);
-}
-
-/* ===== 备注 ===== */
+/* ===== 备注(只读文本块) ===== */
.note-block {
margin-top: 18rpx;
}
.note-block:first-child {
- margin-top: 6rpx;
+ margin-top: 4rpx;
}
.note-label {
@@ -286,8 +431,7 @@ page {
}
.note-empty {
- margin-top: 6rpx;
- padding: 24rpx 0;
+ padding: 6rpx 0;
}
.note-empty-text {
@@ -295,11 +439,52 @@ page {
color: var(--muted);
}
+/* ===== 表单行:只活在编辑弹窗里 =====
+ 行结构仍是「标签左、值右、发丝线分隔」,但它是弹窗内的输入语境,不是页面展示语言 */
+.form-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 72rpx;
+ padding: 12rpx 0;
+ border-bottom: 2rpx solid var(--line);
+}
+
+.form-row:last-child {
+ border-bottom: none;
+}
+
+.form-label {
+ flex-shrink: 0;
+ font-size: 26rpx;
+ color: var(--muted);
+}
+
+.form-input {
+ flex: 1;
+ min-width: 0;
+ margin-left: 24rpx;
+ font-size: 28rpx;
+ color: var(--ink);
+ text-align: right;
+}
+
+.form-picker {
+ flex: 1;
+ min-width: 0;
+ margin-left: 24rpx;
+}
+
+.form-picker-value {
+ font-size: 28rpx;
+ color: var(--ink);
+ text-align: right;
+}
+
.note-textarea {
box-sizing: border-box;
width: 100%;
min-height: 200rpx;
- margin-top: 6rpx;
padding: 24rpx;
border: 2rpx solid var(--line);
border-radius: 16rpx;
@@ -309,6 +494,10 @@ page {
line-height: 1.7;
}
+.input-placeholder {
+ color: var(--muted);
+}
+
/* ===== 底部占位与操作栏 ===== */
.bottom-placeholder {
height: 160rpx;
@@ -322,7 +511,6 @@ page {
z-index: 100;
display: flex;
align-items: center;
- justify-content: flex-end;
gap: 16rpx;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
@@ -331,7 +519,10 @@ page {
box-shadow: var(--shadow-bar);
}
+/* 按钮等宽铺满:与同页弹窗脚(.popup-btn 的 flex:1)同一套排布,
+ 热区也更大;单个按钮时即整条通栏主操作 */
.action-btn {
+ flex: 1;
display: flex;
align-items: center;
justify-content: center;
@@ -351,6 +542,12 @@ page {
background: transparent;
}
+/* 破坏性动作:只换字色为状态色「已取消」档(#B4231A 已在色板内,不新增色相),
+ 与并排的取消订单区分开——删除不可恢复,确认弹窗也已走同色 */
+.action-danger {
+ color: #B4231A;
+}
+
.action-primary {
background: var(--brand-strong);
color: var(--white);
@@ -358,7 +555,7 @@ page {
box-shadow: var(--shadow-cta);
}
-/* ===== 取消订单弹窗 ===== */
+/* ===== 底部弹窗(编辑与取消共用一套骨架) ===== */
.popup-mask {
position: fixed;
left: 0;
@@ -392,6 +589,11 @@ page {
transition: transform 0.3s ease-out;
}
+/* 医院预约编辑有 9 行表单,弹窗加高 */
+.popup-content.popup-tall {
+ max-height: 86vh;
+}
+
.popup-content.show {
transform: translateY(0);
}
@@ -400,6 +602,7 @@ page {
display: flex;
align-items: center;
justify-content: space-between;
+ flex-shrink: 0;
padding: 28rpx 32rpx;
border-bottom: 2rpx solid var(--line);
}
@@ -422,9 +625,10 @@ page {
.popup-body {
flex: 1;
overflow-y: auto;
- padding: 28rpx 32rpx;
+ padding: 20rpx 32rpx 28rpx;
}
+/* ===== 取消订单弹窗内的原因单选 ===== */
.cancel-tip {
font-size: 28rpx;
color: var(--muted);
@@ -481,6 +685,7 @@ page {
.popup-footer {
display: flex;
gap: 16rpx;
+ flex-shrink: 0;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 2rpx solid var(--line);
@@ -510,4 +715,4 @@ page {
background: var(--brand-strong);
color: var(--white);
font-weight: 700;
-}
\ No newline at end of file
+}
diff --git a/pages/escort/recordlist.js b/pages/escort/recordlist.js
index 2ebda0a..ea00c14 100644
--- a/pages/escort/recordlist.js
+++ b/pages/escort/recordlist.js
@@ -10,6 +10,11 @@ const STATUS_MAP = {
cancelled: { text: '已取消', color: '#B4231A', bg: '#FCF0EE', border: '#F2D6D1' }
}
+// 「进行中的」= 未完成全部:待确认 / 已确认 / 进行中 都还没结束,
+// 只是处在预约的不同阶段;只有已完成、已取消才算结束。
+// 全局筛选只分「全部预约 / 进行中的」两档,卡片上的状态胶囊仍按 STATUS_MAP 逐档显示
+const ONGOING_STATUSES = ['pending', 'confirmed', 'in_progress']
+
const TYPE_MAP = {
outpatient: '门诊陪诊',
inpatient: '住院陪诊',
@@ -41,24 +46,23 @@ Page({
hasMore: true,
page: 1,
pageSize: 10,
- statusFilter: '',
- // 供筛选条 scroll-into-view 使用,滑动切换后把选中胶囊滚入可视区
- tabIntoView: '',
+ // 默认落在左格「进行中的」:用户进记录页首先要看的是手上还没结束的单子
+ statusFilter: 'ongoing',
// 跟手位移(px)与拖动标记:拖动中内容随手左右移动,松手后弹回
dragX: 0,
dragging: false,
+ // 两段等宽:左 = 进行中的,右 = 全部预约;顺序即 index 顺序,滑动切换按 index 递增递减
statusTabs: [
- { value: '', label: '全部', count: 0 },
- { value: 'pending', label: '待确认', count: 0 },
- { value: 'confirmed', label: '已确认', count: 0 },
- { value: 'in_progress', label: '进行中', count: 0 },
- { value: 'completed', label: '已完成', count: 0 }
+ { value: 'ongoing', label: '进行中的', count: 0 },
+ { value: '', label: '全部预约', count: 0 }
]
},
onLoad(options) {
- const tab = options.tab || ''
- this.setData({ statusFilter: tab })
+ // tab 来自 URL,只接受当前两档内的值,避免旧参数(如 tab=pending)把列表筛空
+ const tab = options.tab || this.data.statusFilter
+ const valid = this.data.statusTabs.some(item => item.value === tab)
+ this.setData({ statusFilter: valid ? tab : this.data.statusFilter })
this.loadRecords(true)
},
@@ -96,20 +100,14 @@ Page({
pageSize: this.data.pageSize
}
- if (this.data.statusFilter) {
- //params.status = this.data.statusFilter
- }
-
return API.escort.getMyRecords(params)
.then((res) => {
if (res.code === 0) {
let list = res.data.records || []
let formatted = list.map(item => this.formatRecord(item))
+ // 计数按当前页全量算,再按筛选档裁剪展示,保证两档的角标口径一致
const statusTabs = this.computeStatusCounts(formatted, this.data.statusTabs)
- // 根据this.data.formatted
- if (this.data.statusFilter) {
- formatted = formatted.filter(item => item.status === this.data.statusFilter)
- }
+ formatted = formatted.filter(item => this.matchStatusFilter(item, this.data.statusFilter))
this.setData({
records: reset ? formatted : this.data.records.concat(formatted),
page,
@@ -128,19 +126,17 @@ Page({
})
},
+ // 两档筛选的唯一判定处:'' = 全部预约,'ongoing' = 未完成的三种状态
+ matchStatusFilter(record, filter) {
+ if (!filter) return true
+ if (filter === 'ongoing') return ONGOING_STATUSES.indexOf(record.status) > -1
+ return record.status === filter
+ },
+
computeStatusCounts(records, tabs) {
- tabs.forEach(tab => {
- tab.count = 0
- })
- tabs[0].count = records.length
- records.forEach(item => {
- tabs.forEach(tab => {
- if (tab.value === item.status) {
- tab.count++
- }
- })
- })
- return tabs
+ return tabs.map(tab => Object.assign({}, tab, {
+ count: records.filter(item => this.matchStatusFilter(item, tab.value)).length
+ }))
},
formatRecord(item) {
@@ -187,14 +183,11 @@ Page({
this.switchStatusTab(Number(e.currentTarget.dataset.index))
},
- // 统一的状态页切换入口:点胶囊与左右滑动都走这里
+ // 统一的状态页切换入口:点分段与左右滑动都走这里
switchStatusTab(index) {
const tab = this.data.statusTabs[index]
if (!tab || tab.value === this.data.statusFilter) return
- this.setData({
- statusFilter: tab.value,
- tabIntoView: `tab-${index}`
- })
+ this.setData({ statusFilter: tab.value })
this.loadRecords(true)
},
diff --git a/pages/escort/recordlist.wxml b/pages/escort/recordlist.wxml
index e9d4c5a..e4ec4a5 100644
--- a/pages/escort/recordlist.wxml
+++ b/pages/escort/recordlist.wxml
@@ -1,32 +1,24 @@
-
+
-
-
-
-
- {{item.label}}
- {{item.count}}
-
+
+
+ {{item.label}}
+ {{item.count}}
-
+
@@ -96,11 +88,12 @@
-
+
- 暂无陪诊记录
- 您还没有预约陪诊服务
+ {{statusFilter ? '暂无进行中的预约' : '暂无陪诊记录'}}
+ {{statusFilter ? '已结束的预约可在「全部预约」里查看' : '您还没有预约陪诊服务'}}
diff --git a/pages/escort/recordlist.wxss b/pages/escort/recordlist.wxss
index 37728b2..40e6637 100644
--- a/pages/escort/recordlist.wxss
+++ b/pages/escort/recordlist.wxss
@@ -2,13 +2,14 @@
设计方向:本页是「一单一卡」的预约记录表,扫读优先。
- 配色并入全站暖橙体系(doc/DESIGN.md):令牌来自 app.wxss,本页不再留副本;
状态色只用 §三 四档,旧的金色体系与全部渐变已移除
- - 顶部筛选做成吸顶横滑胶囊:选中态 = 浅橙底 + 2rpx 实心橙描边 + 暖橙字,
- 未选中 = 纸面安静胶囊;胶囊带文字标签,颜色只做强化(§四.8)
+ - 顶部筛选做成吸顶两段式分段控件(只有「进行中的 / 全部预约」两档):
+ 白底胶囊容器(发丝边 + 紧阴影)内两格等宽,选中格 = 浅橙底 + 暖橙字,
+ 未选中格 = --muted 字;两格带文字标签,颜色只做强化(§四.8)
- 卡面走「白底 + 发丝边 + 紧阴影」(中性 --shadow-card,28rpx 圆角),
与首页、清单页同一套白面语言
- 卡内四段:类型 + 状态 → 时间 → 就诊信息 → 费用 + 支付状态;
长文本(需求、陪诊记录)交给详情页,保证每张卡一眼看完
- - 形状锁定:卡 28rpx、状态胶囊 10rpx、筛选胶囊 999rpx
+ - 形状锁定:卡 28rpx、状态胶囊 10rpx、分段控件 999rpx
- 左右滑动切换状态页:记录区横向拖动时跟手平移(有阻尼与上限),
松手弹回;拖动中关过渡保即时响应,松手后由过渡做回弹
- 字号阶梯:费用 34 / 类型 30 / 就诊信息 28 / 时间 · 胶囊 24(rpx),
@@ -33,59 +34,66 @@ page {
transition: none;
}
-/* ===== 顶部筛选:吸顶横滑 ===== */
+/* ===== 顶部筛选:吸顶两段式分段控件 ===== */
.tabs-bar {
position: sticky;
top: 0;
z-index: 10;
- padding: 20rpx 0 12rpx;
+ padding: 20rpx 32rpx 16rpx;
background: var(--paper);
}
-.tabs-scroll {
- white-space: nowrap;
-}
-
-.tabs {
- display: inline-flex;
- align-items: center;
- gap: 16rpx;
- padding: 0 32rpx;
-}
-
-.tab {
- display: inline-flex;
- align-items: baseline;
- padding: 12rpx 26rpx;
+/* 容器走白面语言:白底 + 发丝边 + 紧阴影,与下方记录卡同源,
+ 让筛选条从纸面上拎起来,不再是一排浮在纸面上的空心胶囊 */
+.seg {
+ display: flex;
+ padding: 6rpx;
border: 2rpx solid var(--line);
border-radius: 999rpx;
- background: transparent;
- transition: background-color 0.15s ease;
+ background: var(--white);
+ box-shadow: var(--shadow-card);
}
-.tab:active {
+/* 两格等宽:容器内距各 32rpx 后可用宽 622rpx,一格 305rpx,
+ 四字标签(28rpx)+ 角标横向只占约 150rpx,居中后左右留白充足 */
+.seg-item {
+ flex: 1;
+ display: flex;
+ align-items: baseline;
+ justify-content: center;
+ padding: 14rpx 0;
+ border-radius: 999rpx;
+ /* 只过渡颜色:选中格靠底色块平移切换,不做位移动画(两格等宽无需滑块) */
+ transition: background-color 0.18s ease;
+}
+
+.seg-item:active {
opacity: 0.75;
}
-.tab-label {
- font-size: 26rpx;
+.seg-label {
+ font-size: 28rpx;
color: var(--muted);
}
-.tab-count {
- margin-left: 6rpx;
+.seg-count {
+ margin-left: 8rpx;
font-size: 24rpx;
color: var(--muted);
}
-/* 选中:浅橙底 + 实心橙描边 + 暖橙字(浅橙底 5.96:1,过 AA) */
-.tab.active {
+/* 选中:浅橙底 + 暖橙字(浅橙底 5.96:1,过 AA)。
+ 内层不描边——容器已有发丝边,再描一圈会成双描边 */
+.seg-item.active {
background: var(--brand-soft);
- border-color: var(--brand-strong);
}
-.tab.active .tab-label,
-.tab.active .tab-count {
+.seg-item.active .seg-label {
+ color: var(--brand-ink);
+ font-weight: 700;
+}
+
+.seg-item.active .seg-count {
color: var(--brand-ink);
}
diff --git a/pages/health/profileedit.wxml b/pages/health/profileedit.wxml
index 74c66bf..8b83d22 100644
--- a/pages/health/profileedit.wxml
+++ b/pages/health/profileedit.wxml
@@ -47,13 +47,13 @@
无法与标签同排,故留在本块末尾单独一排——标签在上、文本域在下、字数右下 -->
- 身高
+ 身高(cm)
- 体重
+ 体重(kg)
diff --git a/pages/health/profilelist.wxml b/pages/health/profilelist.wxml
index 3365cb8..d2ade3a 100644
--- a/pages/health/profilelist.wxml
+++ b/pages/health/profilelist.wxml
@@ -7,12 +7,13 @@
下单时可直接选择已保存的就诊人,无需重复填写
-
+
@@ -39,7 +40,6 @@
{{item.remark}}
-
diff --git a/pages/health/profilelist.wxss b/pages/health/profilelist.wxss
index 9756484..d86a9a5 100644
--- a/pages/health/profilelist.wxss
+++ b/pages/health/profilelist.wxss
@@ -37,29 +37,30 @@ page {
line-height: 1.5;
}
-/* ===== 通栏白块 =====
- 白面 + 上下各 2rpx 发丝线 + 中性阴影,圆角 0;
- 负向 margin 抵消容器 32rpx 内距后左右顶到屏幕两侧,
- 卡内再用 32rpx 内距回到同一左基线,卡间发丝线因此通到底。
- 圆角必须为 0,否则满宽块的圆角会切在屏幕边上 */
+/* ===== 就诊人卡片:配方与预约记录卡完全同源 =====
+ --white + 2rpx --line 发丝边 + 28rpx 圆角 + --shadow-card,内距也对齐 28/28/24。
+ 原先走的是通栏白块(负 margin 顶到屏幕两侧、圆角 0、卡间靠发丝线分隔),
+ 改圆角卡片后负 margin 与上下两条发丝线一并去掉,卡片缩回容器 32rpx 内距里 */
.profile-list {
- margin: 0 -32rpx;
- background: var(--white);
- border-top: 2rpx solid var(--line);
- border-bottom: 2rpx solid var(--line);
- box-shadow: var(--shadow-card);
+ display: flex;
+ flex-direction: column;
}
.profile-card {
- padding: 28rpx 32rpx;
- border-bottom: 2rpx solid var(--line);
+ padding: 28rpx 28rpx 24rpx;
+ border-radius: 28rpx;
+ background: var(--white);
+ border: 2rpx solid var(--line);
+ box-shadow: var(--shadow-card);
}
-.profile-card-last {
- border-bottom: none;
+/* 卡间距用相邻兄弟 margin,不用 flex 的 gap:
+ gap 在部分微信基础库 / 老设备上不生效,一旦失效几张卡会挨在一起连成一条白条 */
+.profile-card + .profile-card {
+ margin-top: 24rpx;
}
-/* 主体整块可点进编辑,右侧 chevron 即点击暗示 */
+/* 主体整块可点进编辑。chevron 已去掉,点击暗示由按下态(.profile-main-hover)承担 */
.profile-main {
display: flex;
align-items: center;
@@ -127,12 +128,6 @@ page {
word-break: break-all;
}
-.profile-arrow {
- flex-shrink: 0;
- margin-left: 16rpx;
- color: var(--muted);
-}
-
/* 卡片操作行:与主体隔一条发丝线,删除靠右排(与「邮寄地址」卡同一位置) */
.profile-actions {
display: flex;
diff --git a/pages/mine/addressedit.js b/pages/mine/addressedit.js
index 0aa294a..ee4eae6 100644
--- a/pages/mine/addressedit.js
+++ b/pages/mine/addressedit.js
@@ -198,7 +198,9 @@ Page({
})
.then((data) => {
if (data.code === 0) {
- app.globalData.user = data.data.user
+ // 与 profileedit 同一处坑:/user/update 的响应不含 security.token,
+ // 整替会抹掉本机 token,security 必须保留本地那份
+ app.globalData.user = { ...user, ...data.data.user, security: user.security || {} }
wx.showToast({ title: '保存成功', icon: 'success' })
} else {
wx.showToast({ title: data.msg || '保存失败', icon: 'none' })
diff --git a/pages/mine/mine.js b/pages/mine/mine.js
index c787491..6ce4198 100644
--- a/pages/mine/mine.js
+++ b/pages/mine/mine.js
@@ -230,33 +230,37 @@ Page({
},
handleLogin(e) {
+ // 微信手机号 code 是一次性凭证(5 分钟内有效、只能用一次),只能在服务端消费一次。
+ // 所以不能先调 /user/wxgetphonenumber 把它用掉、再在 wxSignin 里复用——
+ // 那样第二次向微信换号必然报 invalid code。
+ // wxSignin 内部已做可信的服务端换号(api/user/handler/users.js),直接交给它即可。
const phoneCode = e.detail.code ? e.detail.code : ''
- API.user.wxGetPhoneNumber({ code: phoneCode })
- .then((data) => {
- const phoneNumber = data.data.phoneNumber
- wx.setStorageSync('user_phonenumber', phoneNumber)
- wx.login({
- success: (res) => {
- if (res.code) {
- API.user.wxSignin({ phoneNumber, phoneCode, code: res.code })
- .then((data) => {
- if (data.code == 0) {
- const app = getApp()
- app.globalData.user = data.data.user
- // 用户重新授权登录,解除对静默重登的拦截
- wx.removeStorageSync('has_logout')
- this.loadUserInfo()
- }
- })
- } else {
- console.log('登录失败!' + res.errMsg)
+ wx.login({
+ success: (res) => {
+ if (!res.code) {
+ console.log('登录失败!' + res.errMsg)
+ return
+ }
+ API.user.wxSignin({ phoneCode, code: res.code })
+ .then((data) => {
+ if (data.code !== 0) {
+ // 后端失败(如 code 过期、该手机号已注册等)此前被静默吞掉,
+ // 用户点了授权却毫无反馈,这里补一个提示
+ wx.showToast({ title: data.msg || '登录失败', icon: 'none' })
+ return
}
- }
- })
- })
- .catch((err) => {
- wx.showToast({ title: '网络请求失败', icon: 'none' })
- console.log('请求失败', err)
- })
+ const app = getApp()
+ app.globalData.user = data.data.user
+ wx.setStorageSync('user_phonenumber', data.data.user?.profile?.mobile || '')
+ // 用户重新授权登录,解除对静默重登的拦截
+ wx.removeStorageSync('has_logout')
+ this.loadUserInfo()
+ })
+ .catch((err) => {
+ wx.showToast({ title: '网络请求失败', icon: 'none' })
+ console.log('请求失败', err)
+ })
+ }
+ })
}
})
\ No newline at end of file
diff --git a/pages/mine/profileedit.js b/pages/mine/profileedit.js
index bdb8b83..f64c394 100644
--- a/pages/mine/profileedit.js
+++ b/pages/mine/profileedit.js
@@ -130,8 +130,11 @@ Page({
API.user.update(updateData)
.then((data) => {
if (data.code === 0) {
- // 回写全局对象:「我的」页 onShow 依赖它刷新头像行与资料
- app.globalData.user = data.data.user
+ // 回写全局对象:「我的」页 onShow 依赖它刷新头像行与资料。
+ // 不能整替——/user/update 的响应是脱敏用户(security.token 已被删掉),
+ // 整替会把本机 token 抹掉,之后所有接口都变成未登录。security 保留本地那份
+ const prev = app.globalData.user || {}
+ app.globalData.user = { ...prev, ...data.data.user, security: prev.security || {} }
wx.showToast({ title: '保存成功', icon: 'success' })
// 返回上一页(「我的」),由它的 onShow 重新读取全局对象
wx.navigateBack()
diff --git a/utils/api.js b/utils/api.js
index 7114bcd..07d4664 100644
--- a/utils/api.js
+++ b/utils/api.js
@@ -1,11 +1,14 @@
const request = require('./request.js')
+// user 服务与 health 服务是两个独立进程(本地 :9010 / :9004,线上才由 nginx 同域分流),
+// 所以 user 段必须用单独实例,否则本地调试时 user 接口会打到 health 端口上走不通
+const requestUser = new request.Request(request.USER_BASE_URL)
const API = {
user: {
- wxGetPhoneNumber: (data) => request.post('/user/wxgetphonenumber', data),
- wxSignin: (data) => request.post('/user/wxsignin', data),
- signout: (data) => request.post('/user/signout', data),
- update: (data) => request.post('/user/update', data),
+ wxGetPhoneNumber: (data) => requestUser.post('/user/wxgetphonenumber', data),
+ wxSignin: (data) => requestUser.post('/user/wxsignin', data),
+ signout: (data) => requestUser.post('/user/signout', data),
+ update: (data) => requestUser.post('/user/update', data),
},
escort: {
diff --git a/utils/request.js b/utils/request.js
index 91a81d0..38cdfba 100644
--- a/utils/request.js
+++ b/utils/request.js
@@ -1,14 +1,50 @@
+// 本地调试开关:置 true 时直连本机进程(health :9004、user :9010)
+// 线上由 nginx 按路径分流(/user → 9010、/health → 9004),两个服务共用同一域名
+const DEBUG = false
+
+const BASE_URL = DEBUG ? 'http://127.0.0.1:9004' : 'https://api.huashengtec.com'
+const USER_BASE_URL = DEBUG ? 'http://127.0.0.1:9010' : 'https://api.huashengtec.com'
+
class Request {
-
- constructor(baseURL = 'https://api.huashengtec.com') {
- //constructor(baseURL = 'http://127.0.0.1:9004') {
+
+ constructor(baseURL = BASE_URL) {
this.baseURL = baseURL
+ // 401 兜底回调,由 app.js 注入(静默重登)。
+ // request 层不直接写登录逻辑:utils/api.js → request.js 是单向依赖,
+ // 反过来引 api.js 会成环
+ this.onUnauthorized = null
+ }
+
+ setUnauthorizedHandler(handler) {
+ this.onUnauthorized = handler
}
request(options) {
- return new Promise((resolve, reject) => {
- const { url, method = 'GET', data = {}, header = {}, ...rest } = options
+ return this.send(options).then((body) => this.recoverUnauthorized(body, options))
+ }
+ // token 失效时后端返回 HTTP 200 + code 401(api/health/utils/responseUtil.js),
+ // 这里静默重登一次再重试:token 会被别处登录轮换,小程序长时间停在前台
+ // 也不会再触发 app.onShow 的重登,本机 token 因此会变陈旧
+ recoverUnauthorized(body, options) {
+ if (!body || body.code !== 401 || options._retried || !this.onUnauthorized) {
+ return body
+ }
+
+ // 重登失败不当成网络错误抛出去:回一句可执行的提示,
+ // 调用方现有的「res.msg 直接弹 toast」就能显示出来(「登录已失效」是死胡同)
+ const notLoggedIn = { code: 401, msg: '请先登录', data: null }
+
+ return Promise.resolve()
+ .then(() => this.onUnauthorized())
+ .then((ok) => (ok ? this.request({ ...options, _retried: true }) : notLoggedIn))
+ .catch(() => notLoggedIn)
+ }
+
+ send(options) {
+ const { url, method = 'GET', data = {}, header = {}, _retried, ...rest } = options
+
+ return new Promise((resolve, reject) => {
const app = getApp()
const token = app?.globalData?.user?.security?.token || ''
@@ -61,4 +97,9 @@ class Request {
const request = new Request()
+// 默认导出 health 实例(保持既有 require('./request.js') 用法不变);
+// 类与 user 侧基址一并挂出,供 api.js 派生 user 服务专用实例
+request.Request = Request
+request.USER_BASE_URL = USER_BASE_URL
+
module.exports = request
\ No newline at end of file