const API = require('../../utils/api.js') Page({ data: { userInfo: { // 留空表示"没有真头像",模板据此渲染暖橙实心底 + 白色 user 线标的默认头像 // (与首页 .brand-mark 同源)。默认头像不走位图:位图着色读不到 CSS 变量, // 改主色时须整图重上色(见 DESIGN.md §7.3),用 t-icon + 令牌则自动跟随 avatar: '', name: '用户', // 身份行副行,loadUserInfo 时由手机号脱敏填入 account: '', phone: '', sex: '', birth: '', email: '', idnumber: '', ssn: '', isLoggedIn: false }, // 菜单分组:按"用途"而非"是不是陪诊"分,三组才立得住。 // 图标与首页同目的地同图形(同一含义只对应一个表达,从色值延伸到图标): // 预约记录 calendar-1、就诊人 user-checked、联系我们 call 均与首页一致。 // requiresLogin 的组在未登录时整组隐藏——这些入口都依赖用户身份 menuGroups: [{ groupName: '我的服务', requiresLogin: true, items: [{ // 陪诊产品的「我的」页核心是订单,故排全页第一项 name: '预约记录', icon: 'calendar-1', bindtap: 'onTapEscortRecord' }, { name: '就诊人', icon: 'user-checked', bindtap: 'onTapHealth' }, { name: '邮寄地址', icon: 'location', bindtap: 'onTapAddress' }] }, { groupName: '帮助与支持', requiresLogin: false, items: [{ name: '联系我们', icon: 'call', bindtap: 'onTapContact' }, { // 口径统一为「服务指南」(首页区块标题行、列表页说明卡都用此名), // 不另起「陪诊指南」这一第三叫法;与列表页「服务指南 │ 用户协议」同一配对 name: '服务指南', icon: 'book-open', bindtap: 'onTapGuide' }, { name: '用户协议', icon: 'secured', bindtap: 'onTapAgreement' }] }, { groupName: '其他', requiresLogin: false, items: [{ name: '关于我们', icon: 'info-circle', bindtap: 'onTapAboutUs' }] }] }, onLoad() { this.loadUserInfo() }, onShow() { this.loadUserInfo() // 同步自定义 tabBar 选中态(我的为第 2 项) if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ selected: 2 }) } }, loadUserInfo() { const app = getApp() if (app.globalData.user && app.globalData.user.profile) { const profile = app.globalData.user.profile this.setData({ 'userInfo.isLoggedIn': true, 'userInfo.name': profile.name || '用户', // 头像补读后端返回值;取不到留空,由模板渲染暖橙默认头像。 // 注:profileedit 页里新选的图目前不会落库——utils/api.js 没有文件上传接口, // 微信返回的是本地临时路径,存进库里也会失效,所以这里只修"显示"这一半 'userInfo.avatar': profile.avatar || '', 'userInfo.phone': profile.mobile || '', // 身份行副行(微信那行是微信号):本应用没有账号体系, // 等价标识是手机号,脱敏成 138****1234 后再上屏 'userInfo.account': this.maskMobile(profile.mobile || ''), 'userInfo.sex': profile.sex || '', 'userInfo.birth': profile.birth ? this.formatDate(profile.birth) : '', 'userInfo.email': profile.email || '', 'userInfo.idnumber': profile.idnumber || '', 'userInfo.ssn': profile.ssn || '' }) } else { // 未登录(含刚退出登录):身份行收敛回默认态。 // 少了这个分支,globalData.user 被清空后页面仍停在"已登录"的样子—— // 「我的服务」组和退出块都不会消失 this.setData({ 'userInfo.isLoggedIn': false, 'userInfo.avatar': '', 'userInfo.name': '用户', 'userInfo.account': '', 'userInfo.phone': '' }) } }, // 手机号脱敏:11 位取前三后四;位数不符时原样返回,不制造错位星号 maskMobile(mobile) { const digits = String(mobile).replace(/\D/g, '') if (digits.length !== 11) return mobile || '' return `${digits.slice(0, 3)}****${digits.slice(7)}` }, formatDate(date) { if (!date) return '' const d = new Date(date) const year = d.getFullYear() const month = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${year}-${month}-${day}` }, // 顶部身份行整行可点,但未登录时不该跳进"填完才被拦"的资料页 // (个人信息与邮寄地址改真页面后系统返回键归页面栈管, // 不再像原来的浮层那样被系统返回键直接退出小程序) onTapProfile() { if (!this.data.userInfo.isLoggedIn) return wx.navigateTo({ url: '/pages/mine/profileedit' }) }, onTapAddress() { wx.navigateTo({ url: '/pages/mine/addressedit' }) }, onTapHealth() { wx.navigateTo({ url: '/pages/health/profilelist' }) }, onTapEscortRecord() { wx.navigateTo({ url: '/pages/escort/recordlist' }) }, // 帮助与支持:联系我们(页面已存在,与首页「常用功能」同目的地) onTapContact() { wx.navigateTo({ url: '/pages/mine/contact' }) }, // 帮助与支持:服务指南(与首页区块标题行、列表页说明卡同一页面) onTapGuide() { wx.navigateTo({ url: '/pages/escort/guide' }) }, // 帮助与支持:用户协议(与陪诊服务列表页的「用户协议」同一页面) onTapAgreement() { wx.navigateTo({ url: '/pages/escort/agreement' }) }, onTapAboutUs() { wx.navigateTo({ url: '/pages/mine/about' }) }, // 页脚三个 B 端入口:都进「合作与入驻」页,用 section 定位到各自的卡片 onFooterTap(e) { const section = e.currentTarget.dataset.section || '' wx.navigateTo({ url: `/pages/mine/business?section=${section}` }) }, // 退出登录:二次确认 → 调后端登出 → 清全局与本地缓存 onLogout() { wx.showModal({ title: '退出登录', content: '退出后需要重新授权手机号,才能查看预约记录与就诊人', confirmText: '退出', // confirmColor 是属性值,读不到 CSS 变量,取值须与令牌同步(DESIGN.md §5.1) confirmColor: '#B8520A', success: (res) => { if (!res.confirm) return wx.showLoading({ title: '退出中...' }) // 必须先调接口:token 由 request.js 从 globalData.user 里取, // 先清全局对象就带不上 token 了 API.user.signout({}) .then(() => this.doLogout()) // 接口失败也要把本地退干净,不把用户卡在"已登录"的假象里 .catch(() => this.doLogout()) .finally(() => wx.hideLoading()) } }) }, doLogout() { const app = getApp() app.globalData.user = null wx.removeStorageSync('user_phonenumber') // 落一个"已主动退出"标记:app.js 的 onShow 每次都会静默重登 // (wxsignin 只凭 code 换 openid 即可重新签发 token),不拦这一下, // 退出就形同虚设——切后台再回来又变成登录态 wx.setStorageSync('has_logout', true) this.loadUserInfo() wx.showToast({ title: '已退出登录', icon: 'success' }) }, handleLogin(e) { // 微信手机号 code 是一次性凭证(5 分钟内有效、只能用一次),只能在服务端消费一次。 // 所以不能先调 /user/wxgetphonenumber 把它用掉、再在 wxSignin 里复用—— // 那样第二次向微信换号必然报 invalid code。 // wxSignin 内部已做可信的服务端换号(api/user/handler/users.js),直接交给它即可。 const phoneCode = e.detail.code ? e.detail.code : '' 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 } 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) }) } }) } })