基本重构完成

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
+46 -16
View File
@@ -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() {
+31
View File
@@ -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,
+5 -1
View File
@@ -58,7 +58,7 @@
</view>
<!-- 服务信息:有哪项展示哪项,整行不留空标签 -->
<view class="section" wx:if="{{ attendant.sexText || areaText || langText }}">
<view class="section" wx:if="{{ attendant.sexText || attendant.age > 0 || areaText || langText }}">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">服务信息</text>
@@ -68,6 +68,10 @@
<text class="info-label">性别</text>
<text class="info-value">{{ attendant.sexText }}</text>
</view>
<view class="info-row" wx:if="{{ attendant.age > 0 }}">
<text class="info-label">年龄</text>
<text class="info-value">{{ attendant.age }} 岁</text>
</view>
<view class="info-row" wx:if="{{ areaText }}">
<text class="info-label">服务区域</text>
<text class="info-value">{{ areaText }}</text>
+34 -2
View File
@@ -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 个,避免长尾把卡片撑高;完整列表在详情页
+5
View File
@@ -48,6 +48,11 @@
</view>
<view class="staff-meta">
<text class="staff-meta-text">从业 {{ item.workYears }} 年</text>
<!-- 年龄未登记时(后端给 0)整段连同分隔线一起隐藏,不留孤立竖线 -->
<block wx:if="{{ item.age > 0 }}">
<view class="meta-sep"></view>
<text class="staff-meta-text">{{ item.age }} 岁</text>
</block>
<view class="meta-sep"></view>
<text class="staff-meta-text">已服务 {{ item.orderCount }} 次</text>
</view>
+2 -2
View File
@@ -48,7 +48,7 @@ Page({
// 原「需要空腹的项目,就诊当天请勿进食进水」是替医院下医嘱;
// 要求来源交回医院,与 id 4「请提前了解…要求」同口径
'部分检查要求空腹(禁食禁水),请以医院的检查前通知为准',
'提前与陪诊师确认医院、院区与集合地点'
'提前与陪诊师确认医院、院区、集合地点与时间'
],
// 温馨提示:通用提醒,不涉及具体退款或收费规则
tips: [
@@ -59,7 +59,7 @@ Page({
// 8.2 不应签署具有法律效力的医疗文书
'具有法律效力的医疗文书(手术、麻醉、特殊检查知情同意书等)需您本人或家属签署,陪诊师不代签',
// 全站统一时限,与服务页 notices、协议第 2 节一致(原「请尽早联系客服调整」没给时限,等于缺公示)
'如需改期或取消,请至少提前4小时联系客服'
'如需改期或取消,请至少提前1天联系客服'
]
},
+60 -52
View File
@@ -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: ''
})
+4 -4
View File
@@ -91,7 +91,7 @@
</view>
</view>
<!-- 选择就诊人:从已保存的就诊人一键带入姓名与电话 -->
<!-- 选择就诊人:订单必须关联健康档案,就诊人信息全部由此带入 -->
<view class="order-row column">
<text class="order-label">选择就诊人</text>
<scroll-view class="patient-scroll" scroll-x wx:if="{{profiles.length > 0}}">
@@ -112,16 +112,16 @@
</view>
</view>
<!-- 就诊人姓名 -->
<!-- 就诊人姓名/电话:只读,与所选档案严格一致(要改就去「编辑就诊人」) -->
<view class="order-row">
<text class="order-label">就诊人姓名</text>
<input class="order-input" value="{{patientName}}" placeholder="请输入就诊人姓名" placeholder-class="input-placeholder" bindinput="onPatientNameChange" />
<text class="order-value {{patientName ? '' : 'placeholder'}}">{{patientName || '请先选择就诊人'}}</text>
</view>
<!-- 就诊人电话 -->
<view class="order-row">
<text class="order-label">联系电话</text>
<input class="order-input" value="{{patientPhone}}" placeholder="请输入联系电话" placeholder-class="input-placeholder" type="number" bindinput="onPatientPhoneChange" />
<text class="order-value {{patientPhone ? '' : 'placeholder'}}">{{patientPhone || '请先选择就诊人'}}</text>
</view>
<!-- 就诊省份 -->
+16
View File
@@ -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;
+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`
})
+267 -203
View File
@@ -1,8 +1,10 @@
<view class="page-bg">
<view class="container">
<!-- 数据到位前不渲染内容:record 初始是 {},直接铺开会先闪一帧
「就诊医院待补充 / ¥0.00 / 暂无备注信息」的假数据,用 _id 作到位判据 -->
<view class="container" wx:if="{{record._id}}">
<!-- 状态头:短横色标 + 状态胶囊 + 状态说明。
状态色只包住胶囊文字本身(DESIGN.md §三),去掉旧版渐变横幅与大色块 -->
<!-- 状态头:短横色标 + 状态胶囊 + 状态说明,是本页的「单据抬头」,不走卡片。
状态色只包住胶囊文字本身(DESIGN.md §三),不做整块主色面板 -->
<view class="page-header">
<view class="header-accent"></view>
<view class="status-row">
@@ -10,193 +12,107 @@
class="status-chip status-chip-lg"
style="background: {{record.statusBg}}; color: {{record.statusColor}}; border-color: {{record.statusBorder}};"
>{{record.statusText}}</view>
<text class="header-subtitle">{{record.statusDesc}}</text>
</view>
<text class="header-subtitle">{{record.statusDesc}}</text>
</view>
<!-- 就诊人信息 -->
<!-- 全页信息架构:抬头之下五张同构白卡(就诊人 / 医院预约 / 需求与症状 / 服务与费用 / 订单),
标题在纸面上、内容在白卡里——同一层级只用一种载体。
页面本身纯展示(不铺「标签左/值右」的表单行),所有修改都收进底部弹窗:
卡片回答「是什么」,弹窗回答「改成什么」,两种语境不混在同一版面里 -->
<!-- 就诊人信息:订单关联健康档案,就诊人信息以档案为准,
因此「修改」直接去档案页,不在订单侧另开一套编辑(同一意图只保留一个入口) -->
<view class="section">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">就诊人信息</text>
<view class="section-link" wx:if="{{!editingPatient}}" bindtap="onEditPatientInfo">
<view class="section-link" wx:if="{{record.healthProfileId}}" bindtap="onEditProfile">
<text class="section-link-text">修改</text>
<t-icon name="edit" size="26rpx" color="#9B4508" />
<t-icon name="edit" size="26rpx" color="#6F655C" />
</view>
</view>
<block wx:if="{{!editingPatient}}">
<view class="info-row">
<text class="info-label">姓名</text>
<text class="info-value">{{record.patientName || '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">性别</text>
<text class="info-value">{{record.patientSexText || '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">年龄</text>
<text class="info-value">{{record.patientAge ? record.patientAge + '岁' : '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">体重</text>
<text class="info-value">{{record.patientWeight ? record.patientWeight + 'kg' : '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">身高</text>
<text class="info-value">{{record.patientHeight ? record.patientHeight + 'cm' : '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">联系电话</text>
<text class="info-value info-link" bindtap="onCallPatient">{{record.patientMobile || '--'}}</text>
</view>
<view class="info-row" wx:if="{{record.patientIdnumber}}">
<text class="info-label">身份证号</text>
<text class="info-value">{{record.patientIdnumber}}</text>
</view>
</block>
<!-- 身份牌:首字牌定位「是谁」,姓名、体征元信息、证件号收进首字牌右侧纵列;
发丝线之下只留一个动作(拨打) -->
<view class="card patient-card">
<view class="patient-head">
<!-- 首字牌沿用「浅橙衬底承载标识」:有姓名取姓氏首字,无则退到 user 图标 -->
<view class="patient-mark">
<text wx:if="{{record.patientInitial}}" class="patient-mark-text">{{record.patientInitial}}</text>
<t-icon wx:else name="user" size="44rpx" color="#9B4508" />
</view>
<view class="patient-info">
<text class="patient-name">{{record.patientName || '--'}}</text>
<block wx:else>
<view class="edit-row">
<text class="edit-label">姓名</text>
<input class="edit-input" value="{{editPatient.name}}" placeholder="请输入姓名" placeholder-class="input-placeholder" data-field="name" bindinput="onPatientInfoInput" />
</view>
<view class="edit-row">
<text class="edit-label">性别</text>
<view class="edit-radio-group">
<view class="edit-radio {{editPatient.sex === 'male' ? 'active' : ''}}" data-value="male" bindtap="onPatientSexChange">男</view>
<view class="edit-radio {{editPatient.sex === 'female' ? 'active' : ''}}" data-value="female" bindtap="onPatientSexChange">女</view>
<!-- 体征行:空值已在 JS 里整项剔除,这里只按序插竖线 -->
<view class="meta-list patient-meta-row" wx:if="{{record.patientMeta.length > 0}}">
<block wx:for="{{record.patientMeta}}" wx:key="*this">
<view class="meta-sep" wx:if="{{index > 0}}"></view>
<text class="meta-item">{{item}}</text>
</block>
</view>
<text class="patient-id" wx:if="{{record.patientIdnumber}}">身份证号 {{record.patientIdnumber}}</text>
</view>
</view>
<view class="edit-row">
<text class="edit-label">出生年月</text>
<picker class="edit-picker" mode="date" value="{{editPatient.birth}}" end="{{today}}" bindchange="onPatientBirthChange">
<view class="edit-input {{editPatient.birth ? '' : 'placeholder'}}">{{editPatient.birth || '请选择'}}</view>
</picker>
<!-- 发丝线之上是「是谁」,之下是唯一动作:整行可点拨打 -->
<view class="patient-contact" bindtap="onCallPatient">
<t-icon name="call" size="30rpx" color="#9B4508" />
<text class="patient-contact-text">{{record.patientMobile || '--'}}</text>
</view>
<view class="edit-row">
<text class="edit-label">体重(kg)</text>
<input class="edit-input" type="digit" value="{{editPatient.weight}}" placeholder="请输入" placeholder-class="input-placeholder" data-field="weight" bindinput="onPatientInfoInput" />
</view>
<view class="edit-row">
<text class="edit-label">身高(cm)</text>
<input class="edit-input" type="digit" value="{{editPatient.height}}" placeholder="请输入" placeholder-class="input-placeholder" data-field="height" bindinput="onPatientInfoInput" />
</view>
<view class="edit-row">
<text class="edit-label">联系电话</text>
<input class="edit-input" type="number" value="{{editPatient.mobile}}" placeholder="请输入" placeholder-class="input-placeholder" data-field="mobile" bindinput="onPatientInfoInput" />
</view>
<view class="edit-row">
<text class="edit-label">身份证号</text>
<input class="edit-input" value="{{editPatient.idnumber}}" placeholder="请输入" placeholder-class="input-placeholder" data-field="idnumber" bindinput="onPatientInfoInput" />
</view>
<view class="edit-actions">
<view class="btn btn-ghost" bindtap="onCancelEditPatientInfo">取消</view>
<view class="btn btn-primary" bindtap="onSavePatientInfo">保存</view>
</view>
</block>
</view>
</view>
<!-- 医院预约信息 -->
<!-- 医院预约信息:医院名为标题,省份提到标题行右上角,科室/医生/病历号收进元信息行;
预约时间收在卡末发丝线之下——日期左、时段与时长右,一行排开。修改走底部弹窗 -->
<view class="section">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">医院预约信息</text>
<view class="section-link" wx:if="{{record.canEditOrder}}" bindtap="onEditHospital">
<text class="section-link-text">修改</text>
<t-icon name="edit" size="26rpx" color="#6F655C" />
</view>
</view>
<view class="info-row">
<text class="info-label">就诊省份</text>
<text class="info-value">{{record.hospitalProvince || '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">就诊医院</text>
<text class="info-value">{{record.hospitalName || '--'}}</text>
</view>
<view class="info-row" wx:if="{{record.hospitalAddress}}">
<text class="info-label">医院地址</text>
<text class="info-value">{{record.hospitalAddress}}</text>
</view>
<view class="info-row">
<text class="info-label">就诊科室</text>
<text class="info-value">{{record.hospitalDepartment || '--'}}</text>
</view>
<view class="info-row" wx:if="{{record.hospitalDoctor}}">
<text class="info-label">就诊医生</text>
<text class="info-value">{{record.hospitalDoctor}}</text>
</view>
<view class="info-row" wx:if="{{record.medicalRecordNo}}">
<text class="info-label">病历号</text>
<text class="info-value">{{record.medicalRecordNo}}</text>
</view>
<view class="info-row">
<text class="info-label">预约日期</text>
<text class="info-value">{{record.scheduleDate || '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">预约时间</text>
<text class="info-value">{{record.scheduleStartTime || '--'}}</text>
</view>
<view class="info-row" wx:if="{{record.scheduleEndTime}}">
<text class="info-label">结束时间</text>
<text class="info-value">{{record.scheduleEndTime}}</text>
</view>
<view class="info-row" wx:if="{{record.scheduleDuration}}">
<text class="info-label">陪诊时长</text>
<text class="info-value">{{record.scheduleDuration}}分钟</text>
<view class="card hospital-card">
<!-- 省份提到右上角,与医院名同一行:名称是主信息,省份降为右侧小字标签 -->
<view class="hospital-head">
<text class="hospital-name {{!record.hospitalName ? 'is-empty' : ''}}">{{record.hospitalName || '就诊医院待补充'}}</text>
<text class="hospital-province" wx:if="{{record.hospitalProvince}}">{{record.hospitalProvince}}</text>
</view>
<view class="meta-list hospital-meta" wx:if="{{record.hospitalMeta.length > 0}}">
<block wx:for="{{record.hospitalMeta}}" wx:key="*this">
<view class="meta-sep" wx:if="{{index > 0}}"></view>
<text class="meta-item">{{item}}</text>
</block>
</view>
<text class="hospital-address" wx:if="{{record.hospitalAddress}}">{{record.hospitalAddress}}</text>
<!-- 预约时间条:日期(带星期)在左、时段与陪诊时长在右,一行排开;空值有兜底文案 -->
<view class="schedule-row">
<text class="schedule-date {{!record.scheduleDateText ? 'is-empty' : ''}}">{{record.scheduleDateText || '预约日期待定'}}</text>
<text class="schedule-time" wx:if="{{record.scheduleTimeText}}">{{record.scheduleTimeText}}</text>
</view>
</view>
</view>
<!-- 陪诊服务 -->
<view class="section">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">陪诊服务</text>
</view>
<view class="info-row">
<text class="info-label">服务类型</text>
<text class="info-value">{{record.escortServiceName || '陪诊服务'}}</text>
</view>
<view class="info-row" wx:if="{{record.sexRequirementText}}">
<text class="info-label">性别要求</text>
<text class="info-value">{{record.sexRequirementText}}</text>
</view>
<view class="info-row">
<text class="info-label">陪诊员</text>
<text class="info-value">{{record.attendantName || '待分配'}}</text>
</view>
<view class="info-row" wx:if="{{record.attendantMobile}}">
<text class="info-label">陪诊员电话</text>
<text class="info-value info-link" bindtap="onCallAttendant">{{record.attendantMobile}}</text>
</view>
</view>
<!-- 陪诊需求与症状 -->
<!-- 陪诊需求与症状:纯文本块展示,修改走底部弹窗(textarea 在弹窗里) -->
<view class="section">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">陪诊需求与症状</text>
<view class="section-link" wx:if="{{!editing}}" bindtap="onEditPatientNote">
<view class="section-link" wx:if="{{record.canEditOrder}}" bindtap="onEditPatientNote">
<text class="section-link-text">修改</text>
<t-icon name="edit" size="26rpx" color="#9B4508" />
<t-icon name="edit" size="26rpx" color="#6F655C" />
</view>
</view>
<block wx:if="{{editing}}">
<textarea
class="note-textarea"
value="{{editPatientNote}}"
placeholder="请输入陪诊需求或症状描述"
maxlength="500"
placeholder-class="input-placeholder"
bindinput="onPatientNoteInput"
focus="{{editing}}"
/>
<view class="edit-actions">
<view class="btn btn-ghost" bindtap="onCancelEdit">取消</view>
<view class="btn btn-primary" bindtap="onSavePatientNote">保存</view>
</view>
</block>
<block wx:else>
<view class="card">
<view class="note-block" wx:if="{{record.patientNote}}">
<text class="note-text">{{record.patientNote}}</text>
</view>
@@ -211,88 +127,236 @@
<view class="note-empty" wx:if="{{!record.patientNote && !record.escortNote && !record.medicalSummary}}">
<text class="note-empty-text">暂无备注信息</text>
</view>
</block>
</view>
</view>
<!-- 费用信息 -->
<!-- 服务与费用:服务名称为主、支付状态胶囊收尾同行(仅待处理时显示,已付清不出现);
费用行用字号差做层级(金额是全卡唯一的大字号焦点),不铺清单行;
未付清时在费用行下补一句客服指引(全站没有线上支付入口,本页给不出可点的动作)。
位置收在末尾:前面各块回答「服务什么」(人 → 去哪 → 要什么),
费用与订单号同属「账与单」,连排在最后 -->
<view class="section">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">费用信息</text>
<text class="section-title">服务与费用</text>
</view>
<view class="info-row">
<text class="info-label">服务费用</text>
<text class="info-value fee-value">¥{{record.totalFee}}</text>
</view>
<view class="info-row">
<text class="info-label">已支付</text>
<text class="info-value">¥{{record.paidFee}}</text>
</view>
<view class="info-row">
<text class="info-label">支付状态</text>
<view
class="status-chip"
style="background: {{record.paymentStatusBg}}; color: {{record.paymentStatusColor}}; border-color: {{record.paymentStatusBorder}};"
>{{record.paymentStatusText}}</view>
<view class="card service-card">
<view class="service-head">
<text class="service-name">{{record.escortServiceName || '陪诊服务'}}</text>
<!-- 支付完成(paid)是常态不显示胶囊;只有待处理(未支付/部分支付/已退款)才提示 -->
<view
class="status-chip"
wx:if="{{record.paymentStatus !== 'paid'}}"
style="background: {{record.paymentStatusBg}}; color: {{record.paymentStatusColor}}; border-color: {{record.paymentStatusBorder}};"
>{{record.paymentStatusText}}</view>
</view>
<view class="meta-list service-meta" wx:if="{{record.sexRequirementText}}">
<text class="meta-item">陪诊师性别要求 {{record.sexRequirementText}}</text>
</view>
<view class="fee-row">
<view class="fee-main">
<text class="fee-label">服务费用</text>
<text class="fee-amount">¥{{record.totalFee}}</text>
</view>
<text class="fee-paid">已支付 ¥{{record.paidFee}}</text>
</view>
<!-- 未付清时的出口指引:全站没有线上支付入口,只能引到客服;
收在费用行之下——「已支付 ¥0.00」之后正是用户想问「怎么付」的位置 -->
<text class="pay-hint" wx:if="{{record.paymentHint}}">{{record.paymentHint}}</text>
</view>
</view>
<!-- 订单信息 -->
<!-- 订单信息:只留编号与时间,订单状态在抬头已出现一次,不重复 -->
<view class="section">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">订单信息</text>
</view>
<view class="info-row">
<text class="info-label">订单编号</text>
<text class="info-value order-id">{{record._id || '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">订单状态</text>
<view
class="status-chip"
style="background: {{record.statusBg}}; color: {{record.statusColor}}; border-color: {{record.statusBorder}};"
>{{record.statusText}}</view>
</view>
<view class="info-row">
<text class="info-label">创建时间</text>
<text class="info-value">{{record.createTime || '--'}}</text>
</view>
<view class="info-row">
<text class="info-label">更新时间</text>
<text class="info-value">{{record.updateTime || '--'}}</text>
<view class="card order-card">
<text class="order-no">订单编号 {{record._id || '--'}}</text>
<view class="meta-list order-meta">
<text class="meta-item">创建于 {{record.createTime || '--'}}</text>
<view class="meta-sep"></view>
<text class="meta-item">更新于 {{record.updateTime || '--'}}</text>
</view>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
<!-- 底部占位:只为垫开固定底栏,底栏不出现时不留这段空 -->
<view class="bottom-placeholder" wx:if="{{record.showBottomBar}}"></view>
</view>
<!-- 底部操作栏:同类实心块只保留一处——
再次预约为「已完成」时让位给「确认完成」,为「已取消」时自己成为主操作(§2.1) -->
<view class="bottom-bar">
再次预约为「已完成」时让位给「确认完成」,为「已取消」时自己成为主操作(§2.1)。
四种动作都为假时整条栏不出现:进行中且已支付这类状态没有动作可给,不留空白栏 -->
<view class="bottom-bar" wx:if="{{record.showBottomBar}}">
<view
class="action-btn action-ghost"
wx:if="{{record.status === 'pending' || record.status === 'confirmed'}}"
wx:if="{{record.canCancel}}"
bindtap="onCancelOrder"
>取消订单</view>
<view
class="action-btn action-ghost"
wx:if="{{record.paymentStatus === 'unpaid' && record.status !== 'completed'}}"
class="action-btn action-ghost action-danger"
wx:if="{{record.canDelete}}"
bindtap="onDeleteOrder"
>删除订单</view>
<view
class="action-btn action-primary"
wx:if="{{record.status === 'completed'}}"
wx:if="{{record.canComplete}}"
bindtap="onConfirmComplete"
>确认完成</view>
<view
class="action-btn {{record.status === 'cancelled' ? 'action-primary' : 'action-ghost'}}"
wx:if="{{record.status === 'completed' || record.status === 'cancelled'}}"
wx:if="{{record.canRebook}}"
bindtap="onRebook"
>再次预约</view>
</view>
<!-- 医院预约信息编辑弹窗:页面内嵌表单全部收进这里。
弹窗比取消弹窗高(popup-tall),表单行可滚动 -->
<view class="popup-mask {{showHospitalPopup ? 'show' : ''}}" bindtap="onCancelHospitalEdit"></view>
<view class="popup-content popup-tall {{showHospitalPopup ? 'show' : ''}}">
<view class="popup-header">
<text class="popup-title">修改医院预约信息</text>
<view class="popup-close" bindtap="onCancelHospitalEdit">
<t-icon name="close" size="40rpx" color="#6F655C" />
</view>
</view>
<view class="popup-body">
<view class="form-row">
<text class="form-label">就诊省份</text>
<picker
class="form-picker"
mode="selector"
range="{{provinces}}"
value="{{editHospital.provinceIndex}}"
bindchange="onProvinceChange"
>
<view class="form-picker-value">{{editHospital.province || '请选择省份'}}</view>
</picker>
</view>
<view class="form-row">
<text class="form-label">就诊医院</text>
<input
class="form-input"
value="{{editHospital.name}}"
placeholder="请输入医院名称"
placeholder-class="input-placeholder"
bindinput="onHospitalFieldInput"
data-field="name"
/>
</view>
<view class="form-row">
<text class="form-label">医院地址</text>
<input
class="form-input"
value="{{editHospital.address}}"
placeholder="请输入医院地址"
placeholder-class="input-placeholder"
bindinput="onHospitalFieldInput"
data-field="address"
/>
</view>
<view class="form-row">
<text class="form-label">就诊科室</text>
<input
class="form-input"
value="{{editHospital.department}}"
placeholder="请输入就诊科室"
placeholder-class="input-placeholder"
bindinput="onHospitalFieldInput"
data-field="department"
/>
</view>
<view class="form-row">
<text class="form-label">就诊医生</text>
<input
class="form-input"
value="{{editHospital.doctor}}"
placeholder="请输入就诊医生"
placeholder-class="input-placeholder"
bindinput="onHospitalFieldInput"
data-field="doctor"
/>
</view>
<view class="form-row">
<text class="form-label">病历号</text>
<input
class="form-input"
value="{{editHospital.medicalRecordNo}}"
placeholder="请输入病历号"
placeholder-class="input-placeholder"
bindinput="onHospitalFieldInput"
data-field="medicalRecordNo"
/>
</view>
<view class="form-row">
<text class="form-label">预约日期</text>
<picker
class="form-picker"
mode="date"
value="{{editHospital.scheduleDate}}"
bindchange="onScheduleDateChange"
>
<view class="form-picker-value">{{editHospital.scheduleDate || '请选择日期'}}</view>
</picker>
</view>
<view class="form-row">
<text class="form-label">开始时间</text>
<picker
class="form-picker"
mode="time"
value="{{editHospital.startTime}}"
bindchange="onStartTimeChange"
>
<view class="form-picker-value">{{editHospital.startTime || '请选择时间'}}</view>
</picker>
</view>
<view class="form-row">
<text class="form-label">结束时间</text>
<picker
class="form-picker"
mode="time"
value="{{editHospital.endTime}}"
bindchange="onEndTimeChange"
>
<view class="form-picker-value">{{editHospital.endTime || '请选择时间'}}</view>
</picker>
</view>
</view>
<view class="popup-footer">
<view class="popup-btn popup-btn-ghost" bindtap="onCancelHospitalEdit">取消</view>
<view class="popup-btn popup-btn-primary" bindtap="onSaveHospital">保存</view>
</view>
</view>
<!-- 陪诊需求编辑弹窗 -->
<view class="popup-mask {{showNotePopup ? 'show' : ''}}" bindtap="onCancelEdit"></view>
<view class="popup-content {{showNotePopup ? 'show' : ''}}">
<view class="popup-header">
<text class="popup-title">修改陪诊需求</text>
<view class="popup-close" bindtap="onCancelEdit">
<t-icon name="close" size="40rpx" color="#6F655C" />
</view>
</view>
<view class="popup-body">
<textarea
class="note-textarea"
value="{{editPatientNote}}"
placeholder="请输入陪诊需求或症状描述"
maxlength="500"
placeholder-class="input-placeholder"
bindinput="onPatientNoteInput"
focus="{{showNotePopup}}"
/>
</view>
<view class="popup-footer">
<view class="popup-btn popup-btn-ghost" bindtap="onCancelEdit">取消</view>
<view class="popup-btn popup-btn-primary" bindtap="onSavePatientNote">保存</view>
</view>
</view>
<!-- 取消订单弹窗 -->
<view class="popup-mask {{showCancelPopup ? 'show' : ''}}" bindtap="onCloseCancelPopup"></view>
<view class="popup-content {{showCancelPopup ? 'show' : ''}}">
@@ -303,7 +367,7 @@
</view>
</view>
<view class="popup-body">
<text class="cancel-tip">确定要取消该陪诊订单吗?取消后陪诊员将收到通知。</text>
<text class="cancel-tip">确定要取消该陪诊订单吗?取消后陪诊师将收到通知。</text>
<view class="cancel-reason-list">
<view
class="cancel-reason-item {{cancelReason === item ? 'active' : ''}}"
+356 -151
View File
@@ -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);
+28 -35
View File
@@ -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)
},
+21 -28
View File
@@ -1,32 +1,24 @@
<view class="page-bg">
<!-- 状态筛选:吸顶横滑胶囊。
选中态 = 浅橙底 + 2rpx 实心橙描边 + 暖橙字(全页唯一「选中」语言);
未选中 = 纸面安静胶囊(透明底 + 发丝边 + --muted 字)。
胶囊必须带文字标签,颜色只做强化(DESIGN.md §四.8) -->
<!-- 状态筛选:吸顶两段式分段控件。
只有两档,横滑胶囊没有意义(两根胶囊横着摆会显得松散、且左右都不占满),
改为等宽两段:一个白底胶囊容器(发丝边 + 紧阴影)内分两格,
选中格 = 浅橙底 + 暖橙字(全页唯一「选中」语言),未选中格 = --muted 字。
选中格不再单独描边——容器已有发丝边,内层再描一圈会出现双描边;
两段必须带文字标签,颜色只做强化(DESIGN.md §四.8) -->
<view class="tabs-bar">
<!-- scroll-into-view 让左右滑动切换后,选中胶囊自动滚入可视区 -->
<scroll-view
class="tabs-scroll"
scroll-x
scroll-with-animation
scroll-into-view="{{tabIntoView}}"
show-scrollbar="{{false}}"
>
<view class="tabs">
<view
id="tab-{{index}}"
class="tab {{statusFilter === item.value ? 'active' : ''}}"
wx:for="{{statusTabs}}"
wx:key="value"
data-index="{{index}}"
bindtap="onTabChange"
>
<text class="tab-label">{{item.label}}</text>
<text class="tab-count" wx:if="{{item.count > 0}}">{{item.count}}</text>
</view>
<view class="seg">
<view
class="seg-item {{statusFilter === item.value ? 'active' : ''}}"
wx:for="{{statusTabs}}"
wx:key="label"
data-index="{{index}}"
bindtap="onTabChange"
>
<text class="seg-label">{{item.label}}</text>
<text class="seg-count" wx:if="{{item.count > 0}}">{{item.count}}</text>
</view>
</scroll-view>
</view>
</view>
<!-- 记录区承载左右滑动手势:横向拖动时内容跟手平移做反馈,纵向滑动仍交给页面滚动 -->
@@ -96,11 +88,12 @@
</view>
</view>
<!-- 空状态 -->
<!-- 空状态:两档口径不同,文案跟着当前档走,
避免「进行中的」为空时误报成「您还没有预约陪诊服务」 -->
<view class="empty-state" wx:if="{{!loading && records.length === 0}}">
<t-icon name="file-unknown" size="120rpx" color="#A79C92" />
<text class="empty-text">暂无陪诊记录</text>
<text class="empty-subtext">您还没有预约陪诊服务</text>
<text class="empty-text">{{statusFilter ? '暂无进行中的预约' : '暂无陪诊记录'}}</text>
<text class="empty-subtext">{{statusFilter ? '已结束的预约可在「全部预约」里查看' : '您还没有预约陪诊服务'}}</text>
</view>
<!-- 加载中 -->
+40 -32
View File
@@ -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);
}
+2 -2
View File
@@ -47,13 +47,13 @@
无法与标签同排,故留在本块末尾单独一排——标签在上、文本域在下、字数右下 -->
<view class="form-card">
<view class="form-row">
<text class="form-label">身高</text>
<text class="form-label">身高(cm)</text>
<input class="form-input" value="{{form.height}}" type="digit" placeholder="选填,单位 cm" placeholder-class="form-placeholder"
maxlength="5" data-field="height" bindinput="onFieldInput" />
</view>
<view class="form-row">
<text class="form-label">体重</text>
<text class="form-label">体重(kg)</text>
<input class="form-input" value="{{form.weight}}" type="digit" placeholder="选填,单位 kg" placeholder-class="form-placeholder"
maxlength="5" data-field="weight" bindinput="onFieldInput" />
</view>
+5 -5
View File
@@ -7,12 +7,13 @@
<text class="tip-text">下单时可直接选择已保存的就诊人,无需重复填写</text>
</view>
<!-- 就诊人列表:通栏白块(白面 + 上下 2rpx 发丝线 + 中性阴影,圆角 0),
与「邮寄地址」列表同源。每条主体可点进编辑(右侧 chevron 是点击暗示),
不做无暗示的整卡可点;删除动作单独一行、走 --muted -->
<!-- 就诊人列表:一卡一人(白面 + 2rpx 发丝边 + 28rpx 圆角 + 中性紧阴影),
与预约记录卡同一配方,卡间留出 24rpx 纸面。
每条主体可点进编辑,点击暗示由按下态承担(hover-class="profile-main-hover");
删除动作另起一行、与主体隔一条发丝线,走 --muted -->
<view class="profile-list" wx:if="{{profiles.length > 0}}">
<view
class="profile-card {{index === profiles.length - 1 ? 'profile-card-last' : ''}}"
class="profile-card"
wx:for="{{profiles}}"
wx:key="_id"
>
@@ -39,7 +40,6 @@
<text class="info-value">{{item.remark}}</text>
</view>
</view>
<t-icon name="chevron-right" class="profile-arrow" size="32rpx" />
</view>
<view class="profile-actions">
+16 -21
View File
@@ -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;
+3 -1
View File
@@ -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' })
+30 -26
View File
@@ -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)
})
}
})
}
})
+5 -2
View File
@@ -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()
+7 -4
View File
@@ -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: {
+45 -4
View File
@@ -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