diff --git a/app.js b/app.js index 9222de5..6c11e23 100644 --- a/app.js +++ b/app.js @@ -28,22 +28,51 @@ App({ }, onShow(options) { - wx.login({ - success: (res) => { - if (res.code) { + // 每次回前台重新登录刷新用户信息;进行中的页面请求会复用同一次登录 + this.signin(); + }, + + /** 执行登录,返回可复用的 Promise(避免页面请求早于登录完成的竞态) */ + signin() { + if (this._loginPromise) return this._loginPromise; + + this._loginPromise = new Promise((resolve, reject) => { + wx.login({ + success: (res) => { + if (!res.code) { + this._loginPromise = null; + return reject(new Error('wx.login 未返回 code')); + } API.user.wxSignin({ code: res.code }) .then((data) => { if (data.code == 0) { - this.globalData.user = data.data.user - this.eventBus.emit('user-login', data.data.user) + this.globalData.user = data.data.user; + this.eventBus.emit('user-login', data.data.user); + resolve(data.data.user); } else { - console.log('登录失败!') + this._loginPromise = null; + reject(new Error(data.msg || '登录失败')); } }) - } else { + .catch((err) => { + this._loginPromise = null; + reject(err); + }); + }, + fail: (err) => { + this._loginPromise = null; + reject(err); } - } - }) + }); + }); + + return this._loginPromise; + }, + + /** 保证已登录:已有用户信息立即返回,否则等待登录完成 */ + ensureLogin() { + if (this.globalData.user) return Promise.resolve(this.globalData.user); + return this.signin(); }, /** 全局事件总线 */ diff --git a/app.json b/app.json index fb712ff..0bf51e3 100644 --- a/app.json +++ b/app.json @@ -5,11 +5,16 @@ "pages/set/index", "pages/order/index", "pages/customer/index", - "pages/order/orderDetail" + "pages/order/orderDetail", + "pages/order/orderEdit", + "pages/healthprofile/index", + "pages/healthprofile/profileInfo", + "pages/me/index" ], "usingComponents": { "t-toast": "tdesign-miniprogram/toast/toast" }, + "lazyCodeLoading": "requiredComponents", "subpackages": [], "window": { "backgroundTextStyle": "light", @@ -30,21 +35,9 @@ "iconPath": "images/home.png", "selectedIconPath": "images/home-blue.png" }, - { - "pagePath": "pages/order/index", - "text": "订单", - "iconPath": "images/order.png", - "selectedIconPath": "images/order-blue.png" - }, - { - "pagePath": "pages/customer/index", - "text": "客户", - "iconPath": "images/customer.png", - "selectedIconPath": "images/customer-blue.png" - }, { "pagePath": "pages/ai/index", - "text": "消息", + "text": "AI", "iconPath": "images/chat.png", "selectedIconPath": "images/chat-blue.png" }, diff --git a/components/order-stats/index.js b/components/order-stats/index.js new file mode 100644 index 0000000..db34e64 --- /dev/null +++ b/components/order-stats/index.js @@ -0,0 +1,70 @@ +// components/order-stats/index.js +// 首页订单统计卡片:全部完成 / 本月完成 / 未完成 +const API = require('../../utils/api.js') + +Component({ + data: { + allCount: 0, + allFee: 0, + monthCount: 0, + monthFee: 0, + uncompletedCount: 0, + uncompletedFee: 0 + }, + + lifetimes: { + attached() { + this.load() + } + }, + + methods: { + /** + * 拉取订单记录并统计三个口径(数量与费用) + * 全部完成:status === 'completed' + * 本月完成:completed 且预约日期在本月 + * 未完成:status !== 'completed' + * 费用取 payment.totalFee 合计 + */ + load() { + API.escort.getAllRecords({ page: 1, pageSize: 200 }) + .then(res => { + if (res.code !== 0) return + const records = res.data.records || [] + const now = new Date() + const year = now.getFullYear() + const month = now.getMonth() + const feeOf = (item) => Number(item.payment && item.payment.totalFee) || 0 + + let allCount = 0 + let allFee = 0 + let monthCount = 0 + let monthFee = 0 + let uncompletedCount = 0 + let uncompletedFee = 0 + + records.forEach(item => { + if (item.status === 'completed') { + allCount++ + allFee += feeOf(item) + const d = item.schedule && item.schedule.date + ? new Date(item.schedule.date) + : null + if (d && !isNaN(d.getTime()) && d.getFullYear() === year && d.getMonth() === month) { + monthCount++ + monthFee += feeOf(item) + } + } else { + uncompletedCount++ + uncompletedFee += feeOf(item) + } + }) + + this.setData({ allCount, allFee, monthCount, monthFee, uncompletedCount, uncompletedFee }) + }) + .catch(err => { + console.error('获取订单统计失败', err) + }) + } + } +}) \ No newline at end of file diff --git a/components/order-stats/index.json b/components/order-stats/index.json new file mode 100644 index 0000000..32640e0 --- /dev/null +++ b/components/order-stats/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/components/order-stats/index.wxml b/components/order-stats/index.wxml new file mode 100644 index 0000000..07ce069 --- /dev/null +++ b/components/order-stats/index.wxml @@ -0,0 +1,24 @@ + + + 订单统计 + + + + {{allCount || 0}} + 全部完成 + ¥{{allFee || 0}} + + + + {{monthCount || 0}} + 本月完成 + ¥{{monthFee || 0}} + + + + {{uncompletedCount || 0}} + 未完成 + ¥{{uncompletedFee || 0}} + + + \ No newline at end of file diff --git a/components/order-stats/index.wxss b/components/order-stats/index.wxss new file mode 100644 index 0000000..56fefdd --- /dev/null +++ b/components/order-stats/index.wxss @@ -0,0 +1,109 @@ +/* components/order-stats/index.wxss */ + +.os-section { + margin-top: 30rpx; +} + +.os-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.os-title { + font-size: 30rpx; + font-weight: 500; + color: #1a1a1a; + margin-left: 8rpx; + letter-spacing: 1rpx; +} + +.os-card { + background: linear-gradient(135deg, #ffffff 0%, #f3fbf8 100%); + border-radius: 24rpx; + box-shadow: 0 4rpx 20rpx rgba(23, 195, 165, 0.10); + padding: 36rpx 0 32rpx; + display: flex; + align-items: stretch; + position: relative; + overflow: hidden; +} + +/* 左上角装饰色块,呼应首页头部渐变 */ +.os-card::before { + content: ''; + position: absolute; + top: -40rpx; + left: -40rpx; + width: 160rpx; + height: 160rpx; + border-radius: 50%; + background: linear-gradient(135deg, rgba(45, 211, 111, 0.10), rgba(23, 195, 165, 0.10)); +} + +.os-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 1; +} + +.os-num { + font-size: 48rpx; + font-weight: 700; + line-height: 1; + font-family: 'DIN Alternate', -apple-system, sans-serif; +} + +.os-num.green { + color: #2dd36f; +} + +.os-num.blue { + color: #409eff; +} + +.os-num.orange { + color: #ff9f43; +} + +.os-label { + margin-top: 12rpx; + font-size: 24rpx; + color: #888888; +} + +.os-fee { + margin-top: 10rpx; + font-size: 22rpx; + font-weight: 600; + color: #1a1a1a; + padding: 4rpx 16rpx; + border-radius: 20rpx; + background: rgba(26, 26, 46, 0.05); +} + +.os-item:nth-child(1) .os-fee { + color: #1ea55c; + background: rgba(45, 211, 111, 0.12); +} + +.os-item:nth-child(3) .os-fee { + color: #3d8fe0; + background: rgba(64, 158, 255, 0.12); +} + +.os-item:nth-child(5) .os-fee { + color: #e08a1e; + background: rgba(255, 159, 67, 0.14); +} + +.os-divider { + width: 1rpx; + margin: 8rpx 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #e8e8e8 30%, #e8e8e8 70%, rgba(0, 0, 0, 0) 100%); + flex-shrink: 0; +} \ No newline at end of file diff --git a/components/patient-info/index.js b/components/patient-info/index.js new file mode 100644 index 0000000..5866aa7 --- /dev/null +++ b/components/patient-info/index.js @@ -0,0 +1,80 @@ +// components/patient-info/index.js +// 患者/健康档案两行信息项组件 +// 第一行:姓名 + 性别(左) · 年龄(右) +// 第二行:电话(左,可选拨打) · 所在地(右) +// +// 两种用法: +// 1. 直接传展示字段(name/sexLabel/ageText/mobile/locationText),适用于列表批量渲染 +// 2. 只传 profile-id,组件内部加载档案:优先读全局缓存(utils/store.js),未命中再请求 +// 加载完成后触发 loaded 事件(携带档案原始文档),展示字段以加载结果为准 +const store = require('../../utils/store.js') +const { calcAge } = require('../../utils/format.js') + +Component({ + properties: { + name: { type: String, value: '' }, + sexLabel: { type: String, value: '' }, + ageText: { type: String, value: '' }, + mobile: { type: String, value: '' }, + locationText: { type: String, value: '' }, + // 是否显示"拨打"并触发 call 事件 + dial: { type: Boolean, value: false }, + // 健康档案ID:传入后组件内部加载,展示字段以加载结果为准 + profileId: { + type: String, + value: '', + observer(newVal) { + if (newVal) this.loadProfile(newVal) + } + }, + // 是否读取缓存:true 缓存优先(默认),false 直接请求最新数据(请求成功后仍回填缓存) + useCache: { type: Boolean, value: true } + }, + + methods: { + onCallTap() { + if (!this.data.mobile) return + this.triggerEvent('call', { mobile: this.data.mobile }) + }, + + /** + * 按档案ID加载:缓存优先,未命中再请求 + */ + loadProfile(id) { + const cached = store.getProfile(id) + if (cached) { + this.applyProfile(cached) + return + } + const API = require('../../utils/api.js') + API.healthProfile.getProfileById(id) + .then(res => { + if (res.code !== 0 || !res.data || !res.data.profile) return + const profile = res.data.profile + store.cacheProfile(profile) + this.applyProfile(profile) + }) + .catch(err => { + console.error('获取健康档案失败', err) + }) + }, + + /** + * 将档案文档映射为展示字段,并抛出 loaded 事件 + */ + applyProfile(profile) { + const p = profile.profile || {} + const loc = profile.location || {} + const age = calcAge(p.birth) + const sexLabel = p.sex === 'male' ? '男' : (p.sex === 'female' ? '女' : '') + this.setData({ + name: p.name || '', + sexLabel, + ageText: age ? `${age}岁` : '', + mobile: p.mobile || '', + locationText: [loc.province, loc.city].filter(Boolean).join(' ') + }) + this.triggerEvent('loaded', { profile }) + } + } +}) diff --git a/components/patient-info/index.json b/components/patient-info/index.json new file mode 100644 index 0000000..467ce29 --- /dev/null +++ b/components/patient-info/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/components/patient-info/index.wxml b/components/patient-info/index.wxml new file mode 100644 index 0000000..346ccd3 --- /dev/null +++ b/components/patient-info/index.wxml @@ -0,0 +1,21 @@ + + + {{name[0]}} + + + + + {{name}} + {{sexLabel}} + + {{ageText}} + + + + {{mobile || '暂无电话'}} + 拨打 + + {{locationText}} + + + diff --git a/components/patient-info/index.wxss b/components/patient-info/index.wxss new file mode 100644 index 0000000..e273c2d --- /dev/null +++ b/components/patient-info/index.wxss @@ -0,0 +1,93 @@ +.pi-item { + display: flex; + align-items: center; +} + +.pi-avatar { + width: 72rpx; + height: 72rpx; + border-radius: 50%; + background-color: #e6f9f3; + color: #1abc9c; + font-size: 30rpx; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.pi-main { + flex: 1; + min-width: 0; + margin-left: 20rpx; +} + +.pi-line { + display: flex; + align-items: center; + justify-content: space-between; +} + +.pi-line + .pi-line { + margin-top: 6rpx; +} + +.pi-name-row { + display: flex; + align-items: center; + gap: 12rpx; + min-width: 0; +} + +.pi-name { + font-size: 30rpx; + font-weight: 600; + color: #1a1a1a; +} + +.pi-sex { + font-size: 20rpx; + padding: 2rpx 12rpx; + border-radius: 8rpx; + font-weight: 500; + flex-shrink: 0; +} + +.pi-sex.male { + background: rgba(26, 188, 156, 0.1); + color: #1abc9c; +} + +.pi-sex.female { + background: rgba(234, 102, 150, 0.1); + color: #ea6696; +} + +.pi-mobile-wrap { + display: flex; + align-items: center; + min-width: 0; +} + +.pi-mobile { + font-size: 24rpx; + color: #888888; +} + +.pi-call { + font-size: 24rpx; + color: #1abc9c; + font-weight: 500; + margin-left: 8rpx; +} + +.pi-extra { + font-size: 24rpx; + color: #888888; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-left: 16rpx; + flex-shrink: 0; +} diff --git a/pages/ai/index.wxss b/pages/ai/index.wxss index cb170d5..0dc800f 100644 --- a/pages/ai/index.wxss +++ b/pages/ai/index.wxss @@ -60,10 +60,10 @@ page { .quick-item { background-color: #ffffff; - border-radius: 16rpx; + border-radius: 24rpx; padding: 24rpx 32rpx; font-size: 28rpx; - color: #4c6ef5; + color: #1abc9c; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); text-align: center; border: 1rpx solid #e5e7eb; @@ -72,7 +72,7 @@ page { .quick-item:active { transform: scale(0.98); - background-color: rgba(76, 110, 245, 0.05); + background-color: rgba(26, 188, 156, 0.05); } .message-item { @@ -118,8 +118,8 @@ page { } .user-avatar { - background: linear-gradient(135deg, #4c6ef5, #748ffc); - box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25); + background: linear-gradient(135deg, #2dd36f, #17c3a5); + box-shadow: 0 4rpx 12rpx rgba(23, 195, 165, 0.25); } .ai-avatar { @@ -157,10 +157,10 @@ page { } .user-bubble { - background: linear-gradient(135deg, #4c6ef5, #748ffc); + background: linear-gradient(135deg, #2dd36f, #17c3a5); color: #fff; border-bottom-right-radius: 4rpx; - box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.2); + box-shadow: 0 4rpx 12rpx rgba(23, 195, 165, 0.2); margin-right: 4rpx; } @@ -315,8 +315,8 @@ page { .chat-toolbar-action:active { transform: scale(0.96); - background-color: rgba(76, 110, 245, 0.08); - border-color: #4c6ef5; + background-color: rgba(26, 188, 156, 0.08); + border-color: #1abc9c; } .chat-toolbar-action text { @@ -351,8 +351,8 @@ page { } .send-btn.active { - background: linear-gradient(135deg, #4c6ef5, #748ffc); - box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25); + background: linear-gradient(135deg, #2dd36f, #17c3a5); + box-shadow: 0 4rpx 12rpx rgba(23, 195, 165, 0.25); } .send-text { diff --git a/pages/customer/index.less b/pages/customer/index.less index 21fd4b1..b6a40d6 100644 --- a/pages/customer/index.less +++ b/pages/customer/index.less @@ -4,15 +4,15 @@ @bg-primary: #f5f6fa; @bg-secondary: #ffffff; @bg-card: #ffffff; -@accent-primary: #4c6ef5; -@accent-secondary: #6b7aff; -@accent-gradient-start: #4c6ef5; -@accent-gradient-end: #748ffc; +@accent-primary: #1abc9c; +@accent-secondary: #17c3a5; +@accent-gradient-start: #2dd36f; +@accent-gradient-end: #17c3a5; @text-primary: #1a1a2e; @text-secondary: #6b7280; @text-muted: #9ca3af; @border-color: #e5e7eb; -@male-color: #4c6ef5; +@male-color: #1abc9c; @female-color: #ff6b6b; @divider-color: #f3f4f6; @@ -77,7 +77,7 @@ page { font-weight: 500; border-radius: 32rpx; white-space: nowrap; - box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.25); + box-shadow: 0 4rpx 16rpx rgba(23, 195, 165, 0.25); &:active { opacity: 0.9; @@ -215,10 +215,10 @@ page { align-items: center; justify-content: center; background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end); - box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25); + box-shadow: 0 4rpx 12rpx rgba(23, 195, 165, 0.25); &.male { - background: linear-gradient(135deg, #4c6ef5, #748ffc); + background: linear-gradient(135deg, #2dd36f, #17c3a5); } &.female { @@ -263,7 +263,7 @@ page { font-weight: 500; &.male { - background-color: rgba(76, 110, 245, 0.1); + background-color: rgba(26, 188, 156, 0.1); color: @male-color; } @@ -301,16 +301,16 @@ page { width: 72rpx; height: 72rpx; border-radius: 50%; - background-color: rgba(76, 110, 245, 0.08); + background-color: rgba(26, 188, 156, 0.08); transition: all 0.2s ease; &:active { - background-color: rgba(76, 110, 245, 0.15); + background-color: rgba(26, 188, 156, 0.15); transform: scale(0.95); } &.call { - background-color: rgba(76, 110, 245, 0.08); + background-color: rgba(26, 188, 156, 0.08); } } diff --git a/pages/customer/index.wxml b/pages/customer/index.wxml index 73c51a6..35527bb 100644 --- a/pages/customer/index.wxml +++ b/pages/customer/index.wxml @@ -100,7 +100,7 @@ - + diff --git a/pages/healthprofile/index.js b/pages/healthprofile/index.js new file mode 100644 index 0000000..1d6b37a --- /dev/null +++ b/pages/healthprofile/index.js @@ -0,0 +1,81 @@ +// pages/healthprofile/index.js +const API = require('../../utils/api.js') +const { calcAge } = require('../../utils/format.js') + +Page({ + data: { + totalCount: 0, + profileList: [], + quickMenus: [ + { icon: 'view-list', title: '全部档案' }, + { icon: 'add', title: '新建档案' }, + { icon: 'search', title: '患者查询' }, + { icon: 'time', title: '就诊记录' } + ] + }, + + onLoad() { + this.loadData() + }, + + onShow() { + this.loadData() + }, + + /** + * 加载档案总数与最近更新的档案列表(按修改时间排序,最多20条) + */ + loadData() { + API.healthProfile.getProfiles({ page: 1, pageSize: 20, sortBy: 'updatetime' }) + .then(res => { + if (res.code !== 0) { + wx.showToast({ title: res.msg || '获取档案失败', icon: 'none' }) + return + } + const data = res.data || {} + const list = (data.list || []).map(item => { + const age = calcAge(item.profile.birth) + const loc = item.location || {} + return { + id: item._id, + name: item.profile.name || '', + mobile: item.profile.mobile || '', + sexLabel: item.profile.sex === 'male' ? '男' : (item.profile.sex === 'female' ? '女' : ''), + ageText: age ? `${age}岁` : '', + locationText: [loc.province, loc.city].filter(Boolean).join(' ') + } + }) + this.setData({ totalCount: data.total || 0, profileList: list }) + }) + .catch(err => { + console.error('获取健康档案失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + }) + }, + + onMenuTap(e) { + const { index } = e.currentTarget.dataset + const item = this.data.quickMenus[index] + if (item.title === '新建档案') { + wx.navigateTo({ url: '/pages/healthprofile/profileInfo' }) + return + } + if (item.title === '全部档案') { + // 列表已展示在本页,暂不跳转 + wx.showToast({ title: '档案列表见下方', icon: 'none' }) + return + } + wx.showToast({ + title: `${item.title} 开发中`, + icon: 'none' + }) + }, + + /** + * 点击档案 → 编辑/删除 + */ + onProfileTap(e) { + const { id } = e.currentTarget.dataset + wx.navigateTo({ url: `/pages/healthprofile/profileInfo?id=${id}` }) + } +}) diff --git a/pages/healthprofile/index.json b/pages/healthprofile/index.json new file mode 100644 index 0000000..dc6f9dd --- /dev/null +++ b/pages/healthprofile/index.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "健康档案", + "usingComponents": { + "t-icon": "tdesign-miniprogram/icon/icon", + "patient-info": "../../components/patient-info/index" + } +} diff --git a/pages/healthprofile/index.less b/pages/healthprofile/index.less new file mode 100644 index 0000000..46a7706 --- /dev/null +++ b/pages/healthprofile/index.less @@ -0,0 +1,135 @@ +/* pages/healthprofile/index.less */ +page { + background-color: #f5f6fa; + color: #1a1a2e; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.profile-page { + padding: 24rpx; + padding-bottom: 48rpx; +} + +.card { + background-color: #ffffff; + border-radius: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); +} + +/* 头部统计卡 */ +.header-card { + background: linear-gradient(135deg, #2dd36f, #17c3a5); + border-radius: 24rpx; + padding: 32rpx; + color: #ffffff; + box-shadow: 0 8rpx 24rpx rgba(23, 195, 165, 0.25); +} + +.header-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-title { + font-size: 32rpx; + font-weight: 600; +} + +.stat-center { + margin-top: 36rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-label { + font-size: 24rpx; + opacity: 0.9; +} + +.stat-value { + display: flex; + align-items: baseline; + margin-top: 8rpx; + font-weight: 600; +} + +.stat-num { + font-size: 64rpx; + font-weight: 700; + line-height: 1.1; +} + +.stat-unit { + font-size: 26rpx; + margin-left: 8rpx; +} + +/* 常用功能 */ +.section { + margin-top: 32rpx; +} + +.section-title { + display: block; + font-size: 32rpx; + font-weight: 700; + color: #1a1a1a; + margin-left: 8rpx; +} + +.quick-grid { + margin-top: 16rpx; + padding: 24rpx 0 0; + display: flex; + flex-wrap: wrap; +} + +.quick-item { + width: 25%; + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 28rpx; +} + +.quick-item:active { + opacity: 0.7; +} + +.quick-icon { + width: 80rpx; + height: 80rpx; + background-color: #e6f9f3; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12rpx; +} + +.quick-label { + font-size: 24rpx; + color: #333333; +} + +/* 健康档案列表 */ +.profile-list { + margin-top: 16rpx; + padding: 0 24rpx; + + patient-info { + display: block; + padding: 24rpx 0; + border-bottom: 1rpx solid #f5f5f5; + + &:last-child { + border-bottom: none; + } + + &:active { + opacity: 0.7; + } + } +} diff --git a/pages/healthprofile/index.wxml b/pages/healthprofile/index.wxml new file mode 100644 index 0000000..d8e8160 --- /dev/null +++ b/pages/healthprofile/index.wxml @@ -0,0 +1,47 @@ + + + + + + 健康档案 + + + 档案总数 + + {{totalCount}} + + + + + + + + 常用功能 + + + + + + {{item.title}} + + + + + + + 健康档案 + + + + + diff --git a/pages/healthprofile/profileInfo.js b/pages/healthprofile/profileInfo.js new file mode 100644 index 0000000..cf078a3 --- /dev/null +++ b/pages/healthprofile/profileInfo.js @@ -0,0 +1,219 @@ +// pages/healthprofile/profileInfo.js +const API = require('../../utils/api.js') +const store = require('../../utils/store.js') + +const SEX_MAP = [ + { label: '男', value: 'male' }, + { label: '女', value: 'female' } +] +const BLOOD_TYPES = ['A', 'B', 'O', 'AB'] + +Page({ + data: { + isEdit: false, + profileId: '', + isSubmitting: false, + form: { + name: '', + mobile: '', + sexLabel: '', + birth: '', + idnumber: '', + address: '', + height: '', + weight: '', + bloodType: '', + remark: '' + }, + region: [], + regionLabel: '', + sexLabels: SEX_MAP.map(item => item.label), + bloodTypes: BLOOD_TYPES + }, + + onLoad(options) { + const now = new Date() + const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` + this.setData({ today }) + if (options && options.id) { + this.setData({ isEdit: true, profileId: options.id }) + wx.setNavigationBarTitle({ title: '档案详情' }) + this.loadProfile(options.id) + } else { + wx.setNavigationBarTitle({ title: '新建档案' }) + } + }, + + /** + * 加载档案详情 + */ + loadProfile(id) { + API.healthProfile.getProfileById(id) + .then(res => { + if (res.code !== 0 || !res.data || !res.data.profile) { + wx.showToast({ title: res.msg || '获取档案失败', icon: 'none' }) + return + } + const p = res.data.profile + const sexItem = SEX_MAP.find(item => item.value === p.profile.sex) + const loc = p.location || {} + this.setData({ + form: { + name: p.profile.name || '', + mobile: p.profile.mobile || '', + sex: p.profile.sex || '', + sexLabel: sexItem ? sexItem.label : '', + birth: p.profile.birth || '', + idnumber: p.profile.idnumber || '', + address: loc.address || '', + height: p.health.height || '', + weight: p.health.weight || '', + bloodType: p.health.bloodType || '', + remark: p.health.remark || '' + }, + region: [loc.province, loc.city, loc.district].filter(Boolean), + regionLabel: [loc.province, loc.city, loc.district].filter(Boolean).join(' ') + }) + }) + .catch(() => { + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + }) + }, + + /** + * 文本输入统一处理 + */ + onFieldChange(e) { + const { field } = e.currentTarget.dataset + this.setData({ [`form.${field}`]: e.detail.value }) + }, + + onSexChange(e) { + const item = SEX_MAP[e.detail.value] + if (item) { + this.setData({ 'form.sex': item.value, 'form.sexLabel': item.label }) + } + }, + + onBloodChange(e) { + this.setData({ 'form.bloodType': this.data.bloodTypes[e.detail.value] }) + }, + + onBirthChange(e) { + this.setData({ 'form.birth': e.detail.value }) + }, + + onRegionChange(e) { + const value = e.detail.value || [] + this.setData({ region: value, regionLabel: value.join(' ') }) + }, + + /** + * 校验并提交 + */ + onSave() { + if (this.data.isSubmitting) return + + const { form, isEdit, profileId } = this.data + if (!form.name.trim()) { + wx.showToast({ title: '请输入患者姓名', icon: 'none' }) + return + } + if (!form.mobile.trim()) { + wx.showToast({ title: '请输入患者电话', icon: 'none' }) + return + } + + const region = this.data.region + const payload = { + profile: { + name: form.name.trim(), + mobile: form.mobile.trim(), + sex: form.sex || '', + birth: form.birth || '', + idnumber: form.idnumber.trim() + }, + location: { + province: region[0] || '', + city: region[1] || '', + district: region[2] || '', + address: form.address.trim() + }, + health: { + height: parseFloat(form.height) || 0, + weight: parseFloat(form.weight) || 0, + bloodType: form.bloodType, + remark: form.remark.trim() + } + } + + this.setData({ isSubmitting: true }) + + const request = isEdit + ? API.healthProfile.updateProfile(profileId, payload) + : API.healthProfile.createProfile(payload) + + request.then(res => { + if (res.code !== 0) { + wx.showToast({ title: res.msg || '保存失败', icon: 'none' }) + return + } + wx.showToast({ + title: isEdit ? '保存成功' : '创建成功', + icon: 'success' + }) + setTimeout(() => wx.navigateBack(), 800) + }) + .catch(() => { + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + }) + .finally(() => { + this.setData({ isSubmitting: false }) + }) + }, + + /** + * 跳转陪诊预约(携带档案患者信息) + */ + onBooking() { + const { profileId, form } = this.data + const params = [ + `id=${profileId}`, + `name=${encodeURIComponent(form.name)}`, + `mobile=${form.mobile}`, + `sex=${form.sex || ''}`, + `birth=${form.birth || ''}` + ].join('&') + wx.navigateTo({ url: `/pages/order/orderEdit?${params}` }) + }, + + /** + * 删除档案(仅编辑模式) + */ + onDelete() { + const { profileId } = this.data + wx.showModal({ + title: '删除确认', + content: '确定要删除该健康档案吗?删除后不可恢复。', + confirmColor: '#ff4d4f', + success: res => { + if (!res.confirm) return + API.healthProfile.deleteProfile(profileId) + .then(res2 => { + if (res2.code !== 0) { + wx.showToast({ title: res2.msg || '删除失败', icon: 'none' }) + return + } + // 清除缓存并通知列表页刷新 + store.removeProfile(profileId) + store.notifyProfileChange('remove', profileId) + wx.showToast({ title: '删除成功', icon: 'success' }) + setTimeout(() => wx.navigateBack(), 800) + }) + .catch(() => { + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + }) + } + }) + } +}) diff --git a/pages/healthprofile/profileInfo.json b/pages/healthprofile/profileInfo.json new file mode 100644 index 0000000..3a31d0c --- /dev/null +++ b/pages/healthprofile/profileInfo.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "新建档案", + "usingComponents": {} +} diff --git a/pages/healthprofile/profileInfo.less b/pages/healthprofile/profileInfo.less new file mode 100644 index 0000000..4b1c076 --- /dev/null +++ b/pages/healthprofile/profileInfo.less @@ -0,0 +1,124 @@ +/* pages/healthprofile/profileInfo.less */ +page { + background-color: #f5f6fa; + color: #1a1a2e; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.info-page { + padding: 24rpx; + padding-bottom: 48rpx; +} + +.card { + background-color: #ffffff; + border-radius: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); +} + +.section { + margin-top: 32rpx; +} + +.section-title { + display: block; + font-size: 32rpx; + font-weight: 700; + color: #1a1a1a; + margin-left: 8rpx; +} + +.form-card { + margin-top: 16rpx; + padding: 0 24rpx; +} + +.form-item { + display: flex; + align-items: center; + padding: 26rpx 0; +} + +.form-label { + width: 170rpx; + font-size: 28rpx; + color: #333333; + flex-shrink: 0; +} + +.form-input, +.form-textarea, +.picker-value { + flex: 1; + font-size: 28rpx; + color: #1a1a1a; +} + +.form-textarea { + min-height: 100rpx; + line-height: 1.5; +} + +.placeholder { + color: #b8b8c4; +} + +.form-divider { + height: 1rpx; + background-color: #f0f0f0; +} + +/* 操作按钮 */ +.btn-group { + display: flex; + gap: 24rpx; + margin-top: 48rpx; +} + +.btn { + flex: 1; + border-radius: 44rpx; + padding: 24rpx 0; + text-align: center; + + text { + font-size: 30rpx; + font-weight: 500; + color: #ffffff; + } + + &:active { + opacity: 0.85; + } +} + +.btn-primary { + background: linear-gradient(135deg, #2dd36f, #17c3a5); + box-shadow: 0 8rpx 24rpx rgba(23, 195, 165, 0.25); + + &.disabled { + opacity: 0.6; + } +} + +.btn-danger { + background-color: #ff4d4f; +} + +.btn-outline { + background-color: #ffffff; + border: 2rpx solid #1abc9c; + + text { + color: #1abc9c; + } +} + +.booking-btn { + margin-top: 48rpx; +} + +.btn-group + .btn-group, +.booking-btn + .btn-group { + margin-top: 24rpx; +} diff --git a/pages/healthprofile/profileInfo.wxml b/pages/healthprofile/profileInfo.wxml new file mode 100644 index 0000000..2c0a2be --- /dev/null +++ b/pages/healthprofile/profileInfo.wxml @@ -0,0 +1,98 @@ + + + + + 患者信息 + + + 姓名 + + + + + 电话 + + + + + 性别 + + {{form.sexLabel || '请选择性别'}} + + + + + 出生年月 + + {{form.birth || '请选择出生日期'}} + + + + + 身份证号 + + + + + + + + 所在地 + + + 省市区 + + {{regionLabel || '请选择省市区(选填)'}} + + + + + 详细地址 + + + + + + + + 健康信息 + + + 身高(cm) + + + + + 体重(kg) + + + + + 血型 + + {{form.bloodType || '请选择血型'}} + + + + + 备注 +