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}}
+
+
+
+
+
+ {{mobile || '暂无电话'}}
+ 拨打
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ 常用功能
+
+
+
+
+
+ {{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 || '请选择血型'}}
+
+
+
+
+ 备注
+
+
+
+
+
+
+
+ 陪诊预约
+
+
+
+
+
+ {{isEdit ? '保存修改' : '创建档案'}}
+
+
+ 删除档案
+
+
+
diff --git a/pages/home/index.js b/pages/home/index.js
index a74f882..3774840 100644
--- a/pages/home/index.js
+++ b/pages/home/index.js
@@ -4,16 +4,39 @@ const API = require('../../utils/api.js')
Page({
data: {
today: '',
- todayCount: 0,
pendingCount: 0,
completedCount: 0,
- menuList: [
- { icon: '/images/icon_order.png', name: '订单管理', url: '/pages/order/index' },
- { icon: '/images/icon_patient.png', name: '患者管理', url: '/pages/patient/index' },
- { icon: '/images/icon_escort.png', name: '陪诊员管理', url: '/pages/escort/index' },
- { icon: '/images/icon_schedule.png', name: '排班管理', url: '/pages/schedule/index' }
+ recentOrders: [],
+ // 快捷入口:前3个为快捷操作(橙色系),后4个为功能入口(绿色系)
+ quickEntries: [
+ { icon: 'add-circle', name: '增加预约', url: '/pages/order/orderEdit', color: '#ff8f4d' },
+ { icon: 'file-add', name: '增加档案', url: '/pages/healthprofile/profileInfo', color: '#ff8f4d' },
+ { icon: 'lightbulb-circle-filled', name: 'AI办公', url: '/pages/ai/index', isTab: true, color: '#ff8f4d' },
+ { icon: 'bill', name: '预约订单', url: '/pages/order/index', color: '#1abc9c' },
+ { icon: 'heart', name: '健康档案', url: '/pages/healthprofile/index', color: '#1abc9c' },
+ { icon: 'user', name: '注册用户', url: '/pages/customer/index', color: '#1abc9c' },
+ { icon: 'user-circle', name: '个人信息', url: '/pages/me/index', color: '#1abc9c' }
],
- todayOrders: [],
+ online: true,
+ profile: {
+ name: '王姐陪诊师',
+ level: '初级陪诊师',
+ avatar: ''
+ },
+ todayIncome: 168,
+ todayOrderCount: 0,
+ pendingOrder: {
+ count: 1,
+ hospital: '中医院',
+ time: '10:00',
+ price: 88
+ },
+ ongoingOrder: {
+ count: 1,
+ hospital: '康复医院',
+ time: '15:30',
+ price: 128
+ },
statusMap: {
pending: '待确认',
confirmed: '已确认',
@@ -37,56 +60,107 @@ Page({
},
onLoad(options) {
+ this.loadUser()
const now = new Date();
const today = now.toISOString().substring(0, 10);
this.setData({ today });
- this.getTodayOrders();
+ this.getRecentOrders();
this.getStats();
},
onShow() {
- this.getTodayOrders();
+ this.loadUser()
+ this.getRecentOrders();
this.getStats();
},
+ loadUser() {
+ const app = getApp()
+ const user = app.globalData && app.globalData.user
+ if (user) {
+ this.setData({
+ 'profile.name': user.nickname || this.data.profile.name,
+ 'profile.level': user.level || this.data.profile.level,
+ 'profile.avatar': user.avatar || ''
+ })
+ }
+ },
+
+ toggleOnline() {
+ this.setData({
+ online: !this.data.online
+ })
+ },
+
+ onOrderCardTap(e) {
+ const { type } = e.currentTarget.dataset
+ const title = type === 'pending' ? '待接单' : '进行中'
+ wx.showToast({
+ title: `${title} 订单列表`,
+ icon: 'none'
+ })
+ },
+
async getStats() {
const today = new Date().toISOString().substring(0, 10);
const [todayRes, pendingRes, completedRes] = await Promise.all([
- API.escort.getMyRecords({ appointmentDate: today }),
- API.escort.getMyRecords({ status: ['pending', 'confirmed'] }),
- API.escort.getMyRecords({ status: ['completed'] })
+ API.escort.getAllRecords({ appointmentDate: today }),
+ API.escort.getAllRecords({ status: 'pending,confirmed' }),
+ API.escort.getAllRecords({ status: 'completed' })
]);
this.setData({
- todayCount: todayRes.code === 0 ? (todayRes.data.records || []).length : 0,
+ todayOrderCount: todayRes.code === 0 ? (todayRes.data.records || []).length : 0,
pendingCount: pendingRes.code === 0 ? (pendingRes.data.records || []).length : 0,
completedCount: completedRes.code === 0 ? (completedRes.data.records || []).length : 0,
});
},
- async getTodayOrders() {
- const res = await API.escort.getMyRecords({
- appointmentDate: new Date().toISOString().substring(0, 10),
- });
- if (res.code == 0) {
- const records = (res.data.records || []).map(item => {
- if (item.schedule && item.schedule.date) {
- const d = new Date(item.schedule.date);
- item.schedule.date = d.toISOString().substring(0, 10) + ' ' + d.toTimeString().substring(0, 5);
- }
+ /**
+ * 加载近7日订单(含6天前至今及未来订单,按预约时间倒序,最多10条)
+ */
+ async getRecentOrders() {
+ const res = await API.escort.getAllRecords({ page: 1, pageSize: 50 });
+ if (res.code !== 0) return;
+
+ const start = new Date();
+ start.setDate(start.getDate() - 6);
+ start.setHours(0, 0, 0, 0);
+ const pad = (n) => String(n).padStart(2, '0');
+ const records = (res.data.records || [])
+ .filter(item => item.schedule && item.schedule.date && new Date(item.schedule.date) >= start)
+ .slice(0, 10)
+ .map(item => {
+ const d = new Date(item.schedule.date);
+ item.schedule.date = `${d.getMonth() + 1}月${d.getDate()}日 ${pad(d.getHours())}:${pad(d.getMinutes())}`;
return item;
});
- this.setData({ todayOrders: records });
- }
+ this.setData({ recentOrders: records });
},
- navigateTo(e) {
- const url = e.currentTarget.dataset.url
- wx.navigateTo({ url })
+ onEntryTap(e) {
+ const { index } = e.currentTarget.dataset
+ const item = this.data.quickEntries[index]
+ if (item.isTab) {
+ wx.switchTab({ url: item.url })
+ } else {
+ wx.navigateTo({ url: item.url })
+ }
},
viewAllOrders() {
wx.navigateTo({
url: '/pages/order/index'
})
+ },
+
+ /**
+ * 点击近7日订单卡片,进入订单详情
+ */
+ onOrderTap(e) {
+ const id = e.currentTarget.dataset.id
+ if (!id) return
+ wx.navigateTo({
+ url: `/pages/order/orderDetail?id=${id}`
+ })
}
})
diff --git a/pages/home/index.json b/pages/home/index.json
index 63ea1ed..2237554 100644
--- a/pages/home/index.json
+++ b/pages/home/index.json
@@ -1,4 +1,7 @@
{
"navigationBarTitleText": "暖橙陪诊",
- "usingComponents": {}
+ "usingComponents": {
+ "t-icon": "tdesign-miniprogram/icon/icon",
+ "order-stats": "../../components/order-stats/index"
+ }
}
\ No newline at end of file
diff --git a/pages/home/index.wxml b/pages/home/index.wxml
index 71b9edb..388e89b 100644
--- a/pages/home/index.wxml
+++ b/pages/home/index.wxml
@@ -1,97 +1,93 @@
-
-
-
-
-
-
-
- {{todayCount}}
- 新增用户
-
-
- {{todayCount}}
- 新增预约
-
-
- {{pendingCount}}
- 待处理
-
-
- {{completedCount}}
- 已完成
-
-
-
-
-
-
-
-