This commit is contained in:
lik
2026-09-03 11:35:21 +08:00
parent f8f7afceb8
commit b4c8bf0add
45 changed files with 2989 additions and 426 deletions
+38 -9
View File
@@ -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();
},
/** 全局事件总线 */
+7 -14
View File
@@ -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"
},
+70
View File
@@ -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)
})
}
}
})
+3
View File
@@ -0,0 +1,3 @@
{
"component": true
}
+24
View File
@@ -0,0 +1,24 @@
<view class="os-section">
<view class="os-header">
<text class="os-title">订单统计</text>
</view>
<view class="os-card">
<view class="os-item">
<text class="os-num green">{{allCount || 0}}</text>
<text class="os-label">全部完成</text>
<text class="os-fee">¥{{allFee || 0}}</text>
</view>
<view class="os-divider"></view>
<view class="os-item">
<text class="os-num blue">{{monthCount || 0}}</text>
<text class="os-label">本月完成</text>
<text class="os-fee">¥{{monthFee || 0}}</text>
</view>
<view class="os-divider"></view>
<view class="os-item">
<text class="os-num orange">{{uncompletedCount || 0}}</text>
<text class="os-label">未完成</text>
<text class="os-fee">¥{{uncompletedFee || 0}}</text>
</view>
</view>
</view>
+109
View File
@@ -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;
}
+80
View File
@@ -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 })
}
}
})
+3
View File
@@ -0,0 +1,3 @@
{
"component": true
}
+21
View File
@@ -0,0 +1,21 @@
<view class="pi-item">
<view class="pi-avatar">
<text>{{name[0]}}</text>
</view>
<view class="pi-main">
<view class="pi-line">
<view class="pi-name-row">
<text class="pi-name">{{name}}</text>
<text wx:if="{{sexLabel}}" class="pi-sex {{sexLabel === '男' ? 'male' : 'female'}}">{{sexLabel}}</text>
</view>
<text wx:if="{{ageText}}" class="pi-extra">{{ageText}}</text>
</view>
<view class="pi-line">
<view class="pi-mobile-wrap">
<text class="pi-mobile">{{mobile || '暂无电话'}}</text>
<text wx:if="{{dial && mobile}}" class="pi-call" catchtap="onCallTap">拨打</text>
</view>
<text wx:if="{{locationText}}" class="pi-extra">{{locationText}}</text>
</view>
</view>
</view>
+93
View File
@@ -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;
}
+11 -11
View File
@@ -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 {
+12 -12
View File
@@ -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);
}
}
+1 -1
View File
@@ -100,7 +100,7 @@
<!-- 操作按钮 -->
<view class="customer-action">
<view class="action-btn call" data-phone="{{item.mobile}}" catchtap="onCallPhone">
<t-icon name="call" size="32rpx" color="#4c6ef5" />
<t-icon name="call" size="32rpx" color="#1abc9c" />
</view>
</view>
</view>
+81
View File
@@ -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}` })
}
})
+7
View File
@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "健康档案",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"patient-info": "../../components/patient-info/index"
}
}
+135
View File
@@ -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;
}
}
}
+47
View File
@@ -0,0 +1,47 @@
<!--pages/healthprofile/index.wxml-->
<view class="profile-page">
<!-- 头部统计卡 -->
<view class="header-card">
<view class="header-row">
<text class="header-title">健康档案</text>
</view>
<view class="stat-center">
<text class="stat-label">档案总数</text>
<view class="stat-value">
<text class="stat-num">{{totalCount}}</text>
<text class="stat-unit">份</text>
</view>
</view>
</view>
<!-- 常用功能 -->
<view class="section">
<text class="section-title">常用功能</text>
<view class="quick-grid card">
<view class="quick-item" wx:for="{{quickMenus}}" wx:key="title" data-index="{{index}}" bindtap="onMenuTap">
<view class="quick-icon">
<t-icon name="{{item.icon}}" size="40rpx" color="#1abc9c" />
</view>
<text class="quick-label">{{item.title}}</text>
</view>
</view>
</view>
<!-- 健康档案列表 -->
<view class="section" wx:if="{{profileList.length > 0}}">
<text class="section-title">健康档案</text>
<view class="profile-list card">
<patient-info
wx:for="{{profileList}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onProfileTap"
name="{{item.name}}"
sex-label="{{item.sexLabel}}"
age-text="{{item.ageText}}"
mobile="{{item.mobile}}"
location-text="{{item.locationText}}"
/>
</view>
</view>
</view>
+219
View File
@@ -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' })
})
}
})
}
})
+4
View File
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "新建档案",
"usingComponents": {}
}
+124
View File
@@ -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;
}
+98
View File
@@ -0,0 +1,98 @@
<!--pages/healthprofile/profileInfo.wxml-->
<view class="info-page">
<!-- 患者信息 -->
<view class="section">
<text class="section-title">患者信息</text>
<view class="form-card card">
<view class="form-item">
<text class="form-label">姓名</text>
<input class="form-input" placeholder="请输入患者姓名" value="{{form.name}}" bindinput="onFieldChange" data-field="name" />
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">电话</text>
<input class="form-input" type="number" placeholder="请输入患者电话" value="{{form.mobile}}" bindinput="onFieldChange" data-field="mobile" />
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">性别</text>
<picker mode="selector" range="{{sexLabels}}" bindchange="onSexChange">
<view class="picker-value {{form.sex ? '' : 'placeholder'}}">{{form.sexLabel || '请选择性别'}}</view>
</picker>
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">出生年月</text>
<picker mode="date" value="{{form.birth}}" end="{{today}}" bindchange="onBirthChange">
<view class="picker-value {{form.birth ? '' : 'placeholder'}}">{{form.birth || '请选择出生日期'}}</view>
</picker>
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">身份证号</text>
<input class="form-input" placeholder="请输入身份证号(选填)" value="{{form.idnumber}}" bindinput="onFieldChange" data-field="idnumber" />
</view>
</view>
</view>
<!-- 所在地 -->
<view class="section">
<text class="section-title">所在地</text>
<view class="form-card card">
<view class="form-item">
<text class="form-label">省市区</text>
<picker mode="region" value="{{region}}" bindchange="onRegionChange">
<view class="picker-value {{regionLabel ? '' : 'placeholder'}}">{{regionLabel || '请选择省市区(选填)'}}</view>
</picker>
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">详细地址</text>
<input class="form-input" placeholder="街道、门牌号等(选填)" value="{{form.address}}" bindinput="onFieldChange" data-field="address" />
</view>
</view>
</view>
<!-- 健康信息 -->
<view class="section">
<text class="section-title">健康信息</text>
<view class="form-card card">
<view class="form-item">
<text class="form-label">身高(cm)</text>
<input class="form-input" type="digit" placeholder="选填" value="{{form.height}}" bindinput="onFieldChange" data-field="height" />
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">体重(kg)</text>
<input class="form-input" type="digit" placeholder="选填" value="{{form.weight}}" bindinput="onFieldChange" data-field="weight" />
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">血型</text>
<picker mode="selector" range="{{bloodTypes}}" value="{{form.bloodIndex}}" bindchange="onBloodChange">
<view class="picker-value {{form.bloodType ? '' : 'placeholder'}}">{{form.bloodType || '请选择血型'}}</view>
</picker>
</view>
<view class="form-divider"></view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea class="form-textarea" placeholder="病史、过敏史等(选填)" value="{{form.remark}}" bindinput="onFieldChange" data-field="remark" auto-height maxlength="200" />
</view>
</view>
</view>
<!-- 陪诊预约(仅编辑已有档案时显示) -->
<view wx:if="{{isEdit}}" class="btn btn-outline booking-btn" bindtap="onBooking">
<text>陪诊预约</text>
</view>
<!-- 操作按钮 -->
<view class="btn-group">
<view class="btn btn-primary {{isSubmitting ? 'disabled' : ''}}" bindtap="onSave">
<text>{{isEdit ? '保存修改' : '创建档案'}}</text>
</view>
<view wx:if="{{isEdit}}" class="btn btn-danger" bindtap="onDelete">
<text>删除档案</text>
</view>
</view>
</view>
+102 -28
View File
@@ -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}`
})
}
})
+4 -1
View File
@@ -1,4 +1,7 @@
{
"navigationBarTitleText": "暖橙陪诊",
"usingComponents": {}
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"order-stats": "../../components/order-stats/index"
}
}
+66 -70
View File
@@ -1,97 +1,93 @@
<!--pages/home/index.wxml-->
<view class="page">
<!-- 顶部问候 -->
<view class="header">
<view class="greeting">
<text class="greeting-text">您好,管理员</text>
<text class="greeting-sub">今天是 {{today}}</text>
</view>
</view>
<!-- 数据统计 -->
<view class="stats-container">
<view class="stats-grid">
<view class="stat-card">
<view class="stat-value">{{todayCount}}</view>
<view class="stat-label">新增用户</view>
</view>
<view class="stat-card">
<view class="stat-value">{{todayCount}}</view>
<view class="stat-label">新增预约</view>
</view>
<view class="stat-card">
<view class="stat-value">{{pendingCount}}</view>
<view class="stat-label">待处理</view>
</view>
<view class="stat-card">
<view class="stat-value">{{completedCount}}</view>
<view class="stat-label">已完成</view>
</view>
</view>
</view>
<!-- 功能入口 -->
<view class="section">
<view class="section-header">
<text class="section-title">快捷入口</text>
</view>
<view class="menu-grid">
<view class="menu-card" wx:for="{{menuList}}" wx:key="index" bindtap="navigateTo" data-url="{{item.url}}">
<view class="menu-icon-wrap">
<text class="menu-icon-text">{{item.name[0]}}</text>
<!-- 头部信息卡 -->
<view class="header-card">
<view class="profile-row">
<view class="profile-left">
<view class="avatar">
<image wx:if="{{profile.avatar}}" class="avatar-img" src="{{profile.avatar}}" mode="aspectFill" />
<text wx:else class="avatar-text">{{profile.name[0]}}</text>
</view>
<view class="profile-info">
<view class="name-row">
<text class="name">{{profile.name}}</text>
<view class="level-tag">
<text class="level-text">{{profile.level}}</text>
</view>
</view>
</view>
</view>
<view class="online-switch" bindtap="toggleOnline">
<text class="switch-text">{{online ? '接单中' : '休息中'}}</text>
<view class="switch-track {{online ? 'on' : 'off'}}">
<view class="switch-thumb"></view>
</view>
</view>
</view>
<view class="stats-row">
<view class="stat">
<text class="stat-label">今日收益</text>
<view class="stat-value">
<text class="stat-unit">¥</text>
<text class="stat-num">{{todayIncome}}</text>
</view>
</view>
<view class="stat stat-right">
<text class="stat-label">今日接单</text>
<view class="stat-value">
<text class="stat-num">{{todayOrderCount}}</text>
<text class="stat-unit">单</text>
</view>
<text class="menu-name">{{item.name}}</text>
</view>
</view>
</view>
<!-- 今日订单 -->
<!-- 快捷入口 -->
<view class="section">
<text class="section-title">快捷入口</text>
<view class="quick-grid card">
<view class="quick-item" wx:for="{{quickEntries}}" wx:key="name" bindtap="onEntryTap" data-index="{{index}}">
<view class="quick-icon" style="background-color: {{item.color}}1a;">
<t-icon name="{{item.icon}}" size="40rpx" color="{{item.color}}" />
</view>
<text class="quick-label">{{item.name}}</text>
</view>
</view>
</view>
<!-- 订单统计 -->
<order-stats />
<!-- 近7日订单 -->
<view class="section">
<view class="section-header">
<text class="section-title">日订单</text>
<text class="section-title">近7日订单</text>
<view class="view-all" bindtap="viewAllOrders">
<text>全部</text>
<text class="arrow">→</text>
</view>
</view>
<view class="order-list" wx:if="{{todayOrders.length > 0}}">
<view class="order-card" wx:for="{{todayOrders}}" wx:key="_id">
<view class="order-top">
<text class="order-no">#{{item._id}}</text>
<view class="order-list" wx:if="{{recentOrders.length > 0}}">
<view class="order-card" wx:for="{{recentOrders}}" wx:key="_id" data-id="{{item._id}}" bindtap="onOrderTap">
<view class="order-line1">
<text class="order-patient">{{item.patient.name}}</text>
<text class="order-service">{{item.escort.serviceName}}</text>
<view class="status-tag status-{{item.status}}">
<text>{{statusMap[item.status] || item.status}}</text>
</view>
</view>
<view class="order-body">
<view class="order-row">
<text class="row-label">患者</text>
<text class="row-value">{{item.patient.name}}</text>
</view>
<view class="order-row">
<text class="row-label">医院</text>
<text class="row-value">{{item.hospital.name}} · {{item.hospital.department}}</text>
</view>
<view class="order-row">
<text class="row-label">时间</text>
<text class="row-value">{{item.schedule.date}}</text>
</view>
<view class="order-row">
<text class="row-label">服务</text>
<text class="row-value">{{item.escort.serviceName}}</text>
</view>
</view>
<view class="order-footer">
<text class="fee-label">服务费用</text>
<text class="fee-value">¥{{item.payment.totalFee}}</text>
<view class="order-line2">
<text class="order-hospital">{{item.hospital.name}} · {{item.hospital.department}}</text>
<text class="order-time">{{item.schedule.date}}</text>
</view>
</view>
</view>
<view class="empty-state" wx:else>
<text class="empty-text">暂无日订单</text>
<text class="empty-text">暂无近7日订单</text>
</view>
</view>
</view>
+264 -158
View File
@@ -1,120 +1,226 @@
/* pages/home/index.wxss */
page {
background: linear-gradient(180deg, #e8f8f2 0%, #f5f6fa 360rpx);
color: #1a1a2e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.page {
min-height: 100vh;
background: #f7f8fc;
padding-bottom: 40rpx;
box-sizing: border-box;
padding: 24rpx 24rpx 64rpx;
}
/* === 顶部问候 === */
.header {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
padding: 60rpx 40rpx 80rpx;
/* === 头部信息卡 === */
.header-card {
position: relative;
overflow: hidden;
}
.header::after {
content: '';
position: absolute;
top: -60rpx;
right: -40rpx;
width: 280rpx;
height: 280rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.03);
}
.header::before {
content: '';
position: absolute;
bottom: -100rpx;
right: 80rpx;
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.02);
}
.greeting {
position: relative;
z-index: 1;
}
.greeting-text {
display: block;
font-size: 44rpx;
font-weight: 700;
background: linear-gradient(135deg, #2dd36f, #17c3a5);
border-radius: 28rpx;
padding: 36rpx 32rpx;
color: #ffffff;
letter-spacing: 2rpx;
margin-bottom: 12rpx;
box-shadow: 0 12rpx 32rpx rgba(23, 195, 165, 0.28);
}
.greeting-sub {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
font-weight: 300;
letter-spacing: 1rpx;
/* 装饰光斑,增加层次 */
.header-card::before,
.header-card::after {
content: '';
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
pointer-events: none;
}
/* === 数据统计 === */
.stats-container {
margin: -40rpx 30rpx 0;
.header-card::before {
width: 260rpx;
height: 260rpx;
top: -120rpx;
right: -60rpx;
}
.header-card::after {
width: 160rpx;
height: 160rpx;
bottom: -80rpx;
left: -40rpx;
background: rgba(255, 255, 255, 0.08);
}
.profile-row {
position: relative;
z-index: 2;
}
.stats-grid {
display: flex;
gap: 16rpx;
justify-content: space-between;
align-items: center;
}
.stat-card {
flex: 1;
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx 16rpx;
text-align: center;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
transition: transform 0.2s;
.profile-left {
display: flex;
align-items: center;
}
.stat-value {
display: block;
font-size: 48rpx;
.avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.25);
border: 2rpx solid rgba(255, 255, 255, 0.4);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-text {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
}
.profile-info {
margin-left: 20rpx;
}
.name-row {
display: flex;
align-items: center;
}
.name {
font-size: 36rpx;
font-weight: 700;
color: #1a1a2e;
line-height: 1.2;
margin-bottom: 8rpx;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
}
.level-tag {
margin-left: 12rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 16rpx;
padding: 4rpx 14rpx;
}
.level-text {
font-size: 20rpx;
color: #ffffff;
}
.online-switch {
display: flex;
align-items: center;
background-color: rgba(255, 255, 255, 0.25);
border-radius: 28rpx;
padding: 6rpx 6rpx 6rpx 18rpx;
}
.switch-text {
font-size: 24rpx;
margin-right: 10rpx;
}
.switch-track {
width: 56rpx;
height: 32rpx;
border-radius: 16rpx;
background-color: rgba(0, 0, 0, 0.15);
position: relative;
transition: background-color 0.2s ease;
}
.switch-track.on {
background-color: #ffffff;
}
.switch-thumb {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background-color: #ffffff;
position: absolute;
top: 2rpx;
left: 2rpx;
transition: left 0.2s ease, background-color 0.2s ease;
}
.switch-track.on .switch-thumb {
left: 26rpx;
background-color: #17c3a5;
}
.stats-row {
display: flex;
justify-content: space-between;
margin-top: 36rpx;
}
.stat {
display: flex;
flex-direction: column;
}
.stat-right {
align-items: flex-end;
}
.stat-label {
display: block;
font-size: 22rpx;
color: #a0a3bd;
font-weight: 400;
letter-spacing: 1rpx;
font-size: 24rpx;
opacity: 0.9;
}
.stat-value {
display: flex;
align-items: baseline;
margin-top: 8rpx;
font-weight: 600;
}
.stat-unit {
font-size: 26rpx;
margin-right: 4rpx;
}
.stat-num {
font-size: 44rpx;
font-weight: 700;
line-height: 1;
}
/* === 通用 Section === */
.section {
margin: 32rpx 30rpx 0;
margin-top: 44rpx;
}
.section-title {
position: relative;
font-size: 30rpx;
font-weight: 600;
color: #1a1a1a;
margin-left: 20rpx;
letter-spacing: 1rpx;
}
.section-title::before {
content: '';
position: absolute;
left: -20rpx;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(180deg, #2dd36f, #17c3a5);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #1a1a2e;
letter-spacing: 1rpx;
margin-bottom: 16rpx;
}
.view-all {
@@ -130,44 +236,50 @@
color: #a0a3bd;
}
/* === 功能菜单 === */
.menu-grid {
display: flex;
gap: 16rpx;
/* === 快捷功能入口 === */
.card {
background-color: #ffffff;
border-radius: 24rpx;
border: 1rpx solid rgba(31, 61, 56, 0.06);
box-shadow: 0 6rpx 20rpx rgba(31, 61, 56, 0.07);
}
.menu-card {
flex: 1;
background: #ffffff;
border-radius: 20rpx;
padding: 32rpx 0;
.quick-grid {
margin-top: 16rpx;
padding: 32rpx 8rpx 8rpx;
display: flex;
flex-wrap: wrap;
}
.quick-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
margin-bottom: 32rpx;
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
.menu-icon-wrap {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.quick-item:active {
opacity: 0.85;
transform: scale(0.92);
}
.quick-icon {
width: 88rpx;
height: 88rpx;
background-color: #e6f9f3;
border-radius: 26rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14rpx;
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8), 0 4rpx 10rpx rgba(31, 61, 56, 0.06);
}
.menu-icon-text {
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
}
.menu-name {
.quick-label {
font-size: 24rpx;
color: #5a5d7a;
font-weight: 500;
color: #3a3f4d;
}
/* === 订单列表 === */
@@ -179,24 +291,38 @@
.order-card {
background: #ffffff;
border-radius: 20rpx;
border-radius: 24rpx;
border: 1rpx solid rgba(31, 61, 56, 0.06);
padding: 28rpx 32rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
box-shadow: 0 6rpx 20rpx rgba(31, 61, 56, 0.07);
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1), box-shadow 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
.order-top {
.order-card:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(31, 61, 56, 0.08);
}
.order-line1 {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f1f5;
}
.order-no {
font-size: 24rpx;
color: #a0a3bd;
.order-patient {
font-size: 30rpx;
font-weight: 600;
color: #1a1a1a;
margin-right: 16rpx;
}
.order-service {
font-size: 26rpx;
font-weight: 500;
font-family: 'DIN Alternate', 'Helvetica Neue', monospace;
color: #1abc9c;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
@@ -204,6 +330,7 @@
padding: 6rpx 20rpx;
border-radius: 30rpx;
font-weight: 500;
flex-shrink: 0;
}
.status-pending {
@@ -231,58 +358,37 @@
color: #b0b0b0;
}
.order-body {
padding: 20rpx 0;
.order-line2 {
display: flex;
flex-direction: column;
gap: 14rpx;
}
.order-row {
display: flex;
align-items: baseline;
}
.row-label {
font-size: 24rpx;
color: #a0a3bd;
width: 72rpx;
flex-shrink: 0;
}
.row-value {
font-size: 26rpx;
color: #3a3d5c;
flex: 1;
}
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 1rpx solid #f0f1f5;
justify-content: space-between;
margin-top: 12rpx;
}
.fee-label {
.order-hospital {
font-size: 24rpx;
color: #888888;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 16rpx;
}
.order-time {
font-size: 24rpx;
color: #a0a3bd;
}
.fee-value {
font-size: 34rpx;
font-weight: 700;
color: #1a1a2e;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
flex-shrink: 0;
}
/* === 空状态 === */
.empty-state {
background: #ffffff;
border-radius: 20rpx;
border-radius: 24rpx;
border: 1rpx solid rgba(31, 61, 56, 0.06);
padding: 80rpx 0;
text-align: center;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
box-shadow: 0 6rpx 20rpx rgba(31, 61, 56, 0.07);
}
.empty-text {
+77
View File
@@ -0,0 +1,77 @@
// pages/me/index.js
// 性别映射
const SEX_MAP = {
male: '男',
female: '女'
}
Page({
data: {
user: {}
},
onShow() {
this.loadUser()
},
/**
* 加载当前登录用户信息
*/
loadUser() {
const app = getApp()
app.ensureLogin()
.then(user => {
this.setUser(user)
})
.catch(() => {
wx.showToast({ title: '获取用户信息失败', icon: 'none' })
})
},
/**
* 处理并展示用户信息
*/
setUser(user) {
if (!user) return
const profile = user.profile || {}
const location = user.location || {}
const meta = user.meta || {}
const name = profile.name || '微信用户'
const locationText = [location.province, location.city, location.district]
.filter(Boolean).join(' ')
this.setData({
user: {
avatar: profile.avatar || '',
avatarText: name[0],
name,
mobile: profile.mobile || '',
sexLabel: SEX_MAP[profile.sex] || '未知',
birthText: this.formatDate(profile.birth),
email: profile.email || '',
idnumber: profile.idnumber || '',
locationText,
createText: this.formatDate(meta.createtime)
}
})
},
/**
* 格式化日期为 yyyy-MM-dd
*/
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
if (isNaN(date.getTime())) return ''
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
},
onShareAppMessage() {
return {
title: '个人中心',
path: '/pages/me/index'
}
}
})
+5
View File
@@ -0,0 +1,5 @@
{
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}
+106
View File
@@ -0,0 +1,106 @@
/* pages/me/index.less */
page {
background-color: #f5f6fa;
color: #1a1a2e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.page {
min-height: 100vh;
background: #f5f6fa;
box-sizing: border-box;
padding: 24rpx;
}
/* === 用户信息卡 === */
.header-card {
background: linear-gradient(135deg, #2dd36f, #17c3a5);
border-radius: 24rpx;
padding: 40rpx 32rpx;
display: flex;
align-items: center;
color: #ffffff;
box-shadow: 0 8rpx 24rpx rgba(23, 195, 165, 0.25);
}
.avatar {
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.25);
border: 2rpx solid rgba(255, 255, 255, 0.4);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-text {
font-size: 44rpx;
font-weight: 600;
color: #ffffff;
}
.user-info {
margin-left: 28rpx;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 36rpx;
font-weight: 600;
}
.user-mobile {
margin-top: 10rpx;
font-size: 26rpx;
opacity: 0.9;
}
/* === 详细信息卡 === */
.info-card {
margin-top: 24rpx;
background: #ffffff;
border-radius: 24rpx;
padding: 8rpx 28rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 0;
& + .info-item {
border-top: 1rpx solid #f0f1f5;
}
}
.info-left {
display: flex;
align-items: center;
}
.info-label {
margin-left: 16rpx;
font-size: 28rpx;
color: #6b7280;
}
.info-value {
font-size: 28rpx;
color: #1a1a2e;
max-width: 400rpx;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+60
View File
@@ -0,0 +1,60 @@
<!--pages/me/index.wxml-->
<view class="page">
<!-- 用户信息卡 -->
<view class="header-card">
<view class="avatar">
<image wx:if="{{user.avatar}}" class="avatar-img" src="{{user.avatar}}" mode="aspectFill" />
<text wx:else class="avatar-text">{{user.avatarText}}</text>
</view>
<view class="user-info">
<text class="user-name">{{user.name}}</text>
<text class="user-mobile">{{user.mobile || '暂未绑定手机号'}}</text>
</view>
</view>
<!-- 详细信息 -->
<view class="info-card">
<view class="info-item">
<view class="info-left">
<t-icon name="user" size="36rpx" color="#1abc9c" />
<text class="info-label">性别</text>
</view>
<text class="info-value">{{user.sexLabel}}</text>
</view>
<view class="info-item">
<view class="info-left">
<t-icon name="calendar" size="36rpx" color="#1abc9c" />
<text class="info-label">出生日期</text>
</view>
<text class="info-value">{{user.birthText || '未填写'}}</text>
</view>
<view class="info-item">
<view class="info-left">
<t-icon name="mail" size="36rpx" color="#1abc9c" />
<text class="info-label">邮箱</text>
</view>
<text class="info-value">{{user.email || '未填写'}}</text>
</view>
<view class="info-item">
<view class="info-left">
<t-icon name="id-card" size="36rpx" color="#1abc9c" />
<text class="info-label">身份证号</text>
</view>
<text class="info-value">{{user.idnumber || '未填写'}}</text>
</view>
<view class="info-item">
<view class="info-left">
<t-icon name="location" size="36rpx" color="#1abc9c" />
<text class="info-label">所在地区</text>
</view>
<text class="info-value">{{user.locationText || '未填写'}}</text>
</view>
<view class="info-item">
<view class="info-left">
<t-icon name="time" size="36rpx" color="#1abc9c" />
<text class="info-label">注册时间</text>
</view>
<text class="info-value">{{user.createText || '未知'}}</text>
</view>
</view>
</view>
+38 -15
View File
@@ -1,10 +1,11 @@
// pages/order/index.js
const API = require('../../utils/api.js')
const { calcAge } = require('../../utils/format.js')
// 状态映射配置
const STATUS_MAP = {
pending: { label: '待确认', text: '待确认', color: '#f59f00' },
confirmed: { label: '已确认', text: '已确认', color: '#4c6ef5' },
confirmed: { label: '已确认', text: '已确认', color: '#1abc9c' },
in_progress: { label: '进行中', text: '进行中', color: '#20c997' },
completed: { label: '已完成', text: '已完成', color: '#51cf66' },
cancelled: { label: '已取消', text: '已取消', color: '#ff6b6b' }
@@ -37,7 +38,7 @@ Page({
},
// 状态筛选
statusFilters: STATUS_FILTERS,
currentStatus: '',
currentStatus: 'pending',
// 订单列表
orderList: [],
// 分页
@@ -149,15 +150,19 @@ Page({
* 处理订单数据
*/
processOrders(orders) {
return orders.map(order => ({
...order,
statusText: STATUS_MAP[order.status]?.text || order.status,
patientFirstChar: (order.patient?.name || '?')[0],
schedule: {
...order.schedule,
dateText: this.formatDate(order.schedule?.date)
}
}));
return orders.map(order => {
const age = calcAge(order.patient?.birth)
return {
...order,
statusText: STATUS_MAP[order.status]?.text || order.status,
patientFirstChar: (order.patient?.name || '?')[0],
patientAgeText: age ? `${age}` : '',
schedule: {
...order.schedule,
dateText: this.formatDate(order.schedule?.date)
}
};
});
},
/**
@@ -176,10 +181,10 @@ Page({
status: 'pending,confirmed,in_progress,completed,cancelled'
};
API.escort.getMyRecords(params)
API.escort.getAllRecords(params)
.then(res => {
if (res.code !== 0) {
wx.showToast({ title: res.message || '获取订单失败', icon: 'none' });
wx.showToast({ title: res.msg || '获取订单失败', icon: 'none' });
this.setData({ isLoading: false, isLoadingMore: false, isRefreshing: false });
return;
}
@@ -228,6 +233,24 @@ Page({
});
},
/**
* 跳转新增预约(页面内选择健康档案)
*/
onAddOrder() {
wx.navigateTo({
url: '/pages/order/orderEdit'
});
},
/**
* 空状态刷新
*/
onReload() {
this.setData({ page: 1, hasMore: true }, () => {
this.loadOrderList();
});
},
/**
* 状态筛选切换
*/
@@ -284,7 +307,7 @@ Page({
wx.showModal({
title: actionConfig.title,
content: actionConfig.content,
confirmColor: '#4c6ef5',
confirmColor: '#1abc9c',
success: (res) => {
if (res.confirm) {
this.updateOrderStatus(id, actionConfig.nextStatus);
@@ -302,7 +325,7 @@ Page({
API.escort.updateStatus(id, { status: newStatus })
.then(res => {
if (res.code !== 0) {
wx.showToast({ title: res.message || '操作失败', icon: 'none' });
wx.showToast({ title: res.msg || '操作失败', icon: 'none' });
wx.hideLoading();
return;
}
+1 -5
View File
@@ -4,12 +4,8 @@
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"usingComponents": {
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-divider": "tdesign-miniprogram/divider/divider"
"t-icon": "tdesign-miniprogram/icon/icon"
}
}
+27 -9
View File
@@ -1,13 +1,13 @@
/* pages/order/index.less */
@bg: #f7f8fc;
@bg: #f5f6fa;
@card: #ffffff;
@dark: #1a1a2e;
@text: #3a3d5c;
@muted: #a0a3bd;
@border: #f0f1f5;
@accent: #667eea;
@accent-end: #764ba2;
@accent: #1abc9c;
@accent-end: #17c3a5;
@pending: #e6a23c;
@confirmed: #409eff;
@@ -17,6 +17,8 @@
page {
background-color: @bg;
color: @dark;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.page {
@@ -28,10 +30,14 @@ page {
/* === 顶部 === */
.header {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
background: linear-gradient(135deg, #2dd36f 0%, #17c3a5 100%);
box-shadow: 0 8rpx 24rpx rgba(23, 195, 165, 0.25);
padding: 60rpx 40rpx 70rpx;
position: relative;
overflow: hidden;
display: flex;
align-items: flex-start;
justify-content: space-between;
&::after {
content: '';
@@ -78,6 +84,18 @@ page {
letter-spacing: 1rpx;
}
.header-add {
position: relative;
z-index: 1;
flex-shrink: 0;
padding: 16rpx 28rpx;
font-size: 26rpx;
color: #fff;
background: rgba(255, 255, 255, 0.18);
border: 1rpx solid rgba(255, 255, 255, 0.4);
border-radius: 32rpx;
}
/* === 筛选栏 === */
.filter-bar {
background: @card;
@@ -110,7 +128,7 @@ page {
background: linear-gradient(135deg, @accent, @accent-end);
color: #fff;
border-color: transparent;
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
box-shadow: 0 4rpx 16rpx rgba(23, 195, 165, 0.3);
.filter-count {
background: rgba(255, 255, 255, 0.25);
@@ -181,8 +199,8 @@ page {
.order-card {
margin-bottom: 20rpx;
background: @card;
border-radius: 20rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
border-radius: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
&:active {
@@ -289,7 +307,7 @@ page {
font-weight: 500;
&.male {
background: rgba(102, 126, 234, 0.1);
background: rgba(26, 188, 156, 0.1);
color: @accent;
}
@@ -382,7 +400,7 @@ page {
.btn-solid {
background: linear-gradient(135deg, @accent, @accent-end);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.25);
box-shadow: 0 4rpx 16rpx rgba(23, 195, 165, 0.25);
}
.btn-ghost {
+3 -2
View File
@@ -6,6 +6,7 @@
<text class="header-title">订单管理</text>
<text class="header-sub">共 {{stats.total}} 个订单</text>
</view>
<view class="header-add" bindtap="onAddOrder">新增预约</view>
</view>
<!-- 状态筛选 -->
@@ -45,7 +46,7 @@
<view class="empty-box" wx:elif="{{!isLoading && orderList.length === 0}}">
<t-empty icon="file" description="暂无订单" t-class-description="empty-text">
<view slot="action">
<view class="empty-btn" bindtap="loadOrderList">刷新</view>
<view class="empty-btn" bindtap="onReload">刷新</view>
</view>
</t-empty>
</view>
@@ -76,7 +77,7 @@
<view class="patient-name-row">
<text class="patient-name">{{item.patient.name || '未知患者'}}</text>
<text class="patient-sex {{item.patient.sex}}">{{item.patient.sex === 'male' ? '男' : item.patient.sex === 'female' ? '女' : ''}}</text>
<text class="patient-age" wx:if="{{item.patient.age}}">{{item.patient.age}}</text>
<text class="patient-age" wx:if="{{item.patientAgeText}}">{{item.patientAgeText}}</text>
</view>
<text class="patient-phone">{{item.patient.mobile || '暂无电话'}}</text>
</view>
+19 -13
View File
@@ -55,7 +55,6 @@ Page({
// 格式化后的显示字段
statusText: '',
statusDesc: '',
patientFirstChar: '',
attendantFirstChar: '',
scheduleDateText: '',
scheduleStartTimeText: '',
@@ -63,7 +62,6 @@ Page({
createtimeText: '',
updatetimeText: '',
paymentStatusText: '',
patientSexText: '',
attendantSexText: '',
sexRequirementText: '',
// 操作按钮配置
@@ -98,11 +96,11 @@ Page({
wx.hideLoading()
if (res.code !== 0) {
wx.showToast({ title: res.message || '加载失败', icon: 'none' })
wx.showToast({ title: res.msg || '加载失败', icon: 'none' })
return
}
const order = res.data || {}
const order = res.data?.record || {}
this.applyOrderData(order)
} catch (err) {
wx.hideLoading()
@@ -113,7 +111,6 @@ Page({
applyOrderData(order) {
const status = order.status || 'pending'
const patient = order.patient || {}
const attendant = order.attendant || {}
const schedule = order.schedule || {}
const payment = order.payment || {}
@@ -123,9 +120,10 @@ Page({
this.setData({
loaded: true,
order: order,
// 有关联档案时交由 patient-info 按 ID 加载(不读缓存,以档案最新数据为准)
healthProfileId: order.healthProfileId || '',
statusText: STATUS_MAP[status] || status,
statusDesc: STATUS_DESC_MAP[status] || '',
patientFirstChar: (patient.name || '?')[0],
attendantFirstChar: (attendant.name || '?')[0],
scheduleDateText: formatDate(schedule.date),
scheduleStartTimeText: schedule.startTime || '',
@@ -133,10 +131,10 @@ Page({
createtimeText: formatDateTime(meta.createtime),
updatetimeText: formatDateTime(meta.updatetime),
paymentStatusText: PAYMENT_STATUS_MAP[payment.status] || payment.status || '未支付',
patientSexText: patient.sex === 'male' ? '男' : patient.sex === 'female' ? '女' : '',
attendantSexText: attendant.sex === 'male' ? '男' : attendant.sex === 'female' ? '女' : '',
sexRequirementText: escort.sexRequirement === 'male' ? '要求男陪诊' : escort.sexRequirement === 'female' ? '要求女陪诊' : '不限',
// 操作按钮
// 操作按钮(待确认/已确认状态下可编辑订单信息)
showEditBtn: status === 'pending' || status === 'confirmed',
showConfirmBtn: status === 'pending',
showCancelBtn: status === 'pending',
showStartBtn: status === 'confirmed',
@@ -146,7 +144,8 @@ Page({
// 拨打电话
callPhone(e) {
const phone = e.currentTarget.dataset.phone
// 兼容 patient-info 组件的 call 事件与 data-phone 用法
const phone = (e.detail && e.detail.mobile) || e.currentTarget.dataset.phone
if (!phone) {
wx.showToast({ title: '暂无电话', icon: 'none' })
return
@@ -154,12 +153,19 @@ Page({
wx.makePhoneCall({ phoneNumber: phone })
},
// 编辑订单
onEdit() {
wx.navigateTo({
url: `/pages/order/orderEdit?orderId=${this.data.orderId}`
})
},
// 确认订单
onConfirm() {
wx.showModal({
title: '确认订单',
content: '确认接受此订单?',
confirmColor: '#4c6ef5',
confirmColor: '#1abc9c',
success: (res) => {
if (res.confirm) {
this.updateStatus('confirmed')
@@ -187,7 +193,7 @@ Page({
wx.showModal({
title: '开始服务',
content: '确认开始陪诊服务?',
confirmColor: '#4c6ef5',
confirmColor: '#1abc9c',
success: (res) => {
if (res.confirm) {
this.updateStatus('in_progress')
@@ -201,7 +207,7 @@ Page({
wx.showModal({
title: '完成服务',
content: '确认陪诊服务已完成?',
confirmColor: '#4c6ef5',
confirmColor: '#1abc9c',
success: (res) => {
if (res.confirm) {
this.updateStatus('completed')
@@ -217,7 +223,7 @@ Page({
wx.hideLoading()
if (res.code !== 0) {
wx.showToast({ title: res.message || '操作失败', icon: 'none' })
wx.showToast({ title: res.msg || '操作失败', icon: 'none' })
return
}
+3 -1
View File
@@ -2,5 +2,7 @@
"navigationBarTitleText": "订单详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {}
"usingComponents": {
"patient-info": "../../components/patient-info/index"
}
}
+9 -17
View File
@@ -18,22 +18,13 @@
<view class="info-section">
<text class="section-title">患者信息</text>
<view class="info-card">
<view class="patient-header">
<view class="patient-avatar">
<text class="avatar-text">{{patientFirstChar}}</text>
</view>
<view class="patient-info">
<view class="patient-name-row">
<text class="patient-name">{{order.patient.name || '未知患者'}}</text>
<text class="patient-sex {{order.patient.sex}}" wx:if="{{patientSexText}}">{{patientSexText}}</text>
<text class="patient-age" wx:if="{{order.patient.age}}">{{order.patient.age}}岁</text>
</view>
<view class="patient-phone" data-phone="{{order.patient.mobile}}" bindtap="callPhone">
<text>{{order.patient.mobile || '暂无电话'}}</text>
<text class="call-icon" wx:if="{{order.patient.mobile}}"> 拨打</text>
</view>
</view>
</view>
<!-- 订单必须关联健康档案:按档案ID加载(不读缓存,直接请求最新) -->
<patient-info
profile-id="{{healthProfileId}}"
use-cache="{{false}}"
dial="{{true}}"
bind:call="callPhone"
/>
<view class="divider"></view>
<view class="info-row" wx:if="{{order.patient.weight}}">
<text class="info-label">体重</text>
@@ -192,7 +183,8 @@
</view>
<!-- 底部操作 -->
<view class="bottom-actions" wx:if="{{showConfirmBtn || showCancelBtn || showStartBtn || showCompleteBtn}}">
<view class="bottom-actions" wx:if="{{showEditBtn || showConfirmBtn || showCancelBtn || showStartBtn || showCompleteBtn}}">
<view class="action-btn btn-ghost" wx:if="{{showEditBtn}}" bindtap="onEdit">编辑订单</view>
<view class="action-btn btn-ghost" wx:if="{{showCancelBtn}}" bindtap="onCancel">取消订单</view>
<view class="action-btn btn-solid" wx:if="{{showConfirmBtn}}" bindtap="onConfirm">确认订单</view>
<view class="action-btn btn-solid" wx:if="{{showStartBtn}}" bindtap="onStart">开始服务</view>
+24 -52
View File
@@ -1,12 +1,14 @@
/* pages/order/orderDetail.wxss */
page {
background-color: #f7f8fc;
background-color: #f5f6fa;
color: #1a1a2e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.detail-page {
min-height: 100vh;
background: #f7f8fc;
background: #f5f6fa;
padding-bottom: 120rpx;
}
@@ -87,18 +89,19 @@ page {
}
.section-title {
font-size: 30rpx;
font-size: 32rpx;
font-weight: 700;
color: #1a1a2e;
color: #1a1a1a;
margin-bottom: 16rpx;
margin-left: 8rpx;
letter-spacing: 1rpx;
}
.info-card {
background: #ffffff;
border-radius: 20rpx;
border-radius: 24rpx;
padding: 28rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.info-row {
@@ -125,45 +128,8 @@ page {
}
/* === 患者信息 === */
.patient-header {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 16rpx;
}
.patient-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-text {
font-size: 32rpx;
font-weight: 700;
color: #fff;
}
.patient-info {
flex: 1;
}
.patient-name-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 6rpx;
}
.patient-name {
font-size: 32rpx;
font-weight: 600;
color: #1a1a2e;
.info-card patient-info {
display: block;
}
.patient-sex {
@@ -174,8 +140,8 @@ page {
}
.patient-sex.male {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
background: rgba(26, 188, 156, 0.1);
color: #1abc9c;
}
.patient-sex.female {
@@ -194,7 +160,7 @@ page {
}
.call-icon {
color: #667eea;
color: #1abc9c;
font-weight: 500;
}
@@ -215,13 +181,19 @@ page {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
background: linear-gradient(135deg, #2dd36f, #17c3a5);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-text {
font-size: 32rpx;
font-weight: 700;
color: #fff;
}
.attendant-info {
flex: 1;
display: flex;
@@ -297,13 +269,13 @@ page {
}
.btn-solid {
background: linear-gradient(135deg, #667eea, #764ba2);
background: linear-gradient(135deg, #2dd36f, #17c3a5);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
box-shadow: 0 4rpx 16rpx rgba(23, 195, 165, 0.3);
}
.btn-ghost {
background: #f7f8fc;
background: #f5f6fa;
color: #a0a3bd;
border: 1rpx solid #f0f1f5;
}
+390
View File
@@ -0,0 +1,390 @@
// pages/order/orderEdit.js
const API = require('../../utils/api.js')
const { calcAge } = require('../../utils/format.js')
Page({
/**
* 页面的初始数据
*/
data: {
isEdit: false,
form: {
hospital: { name: '', department: '', doctor: '' },
schedule: { date: '' },
escort: { serviceName: '' },
payment: { totalFee: '' },
notes: { patientNote: '' }
},
profileSummary: null,
// 新增模式下尚未选择档案时为 true,展示选择占位
needPickProfile: false,
// 档案选择弹层
showProfilePicker: false,
profileList: [],
filteredProfiles: [],
profileKeyword: '',
today: '',
submitting: false
},
/**
* 生命周期函数--监听页面加载
* 新增:?id=健康档案ID(患者信息取自档案),不带 id 则进入档案选择
* 编辑:?orderId=订单ID(回填订单数据)
*/
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.orderId) {
// 编辑模式
this.orderId = options.orderId
this.setData({ isEdit: true })
wx.setNavigationBarTitle({ title: '编辑订单' })
this.loadOrder(options.orderId)
return
}
wx.setNavigationBarTitle({ title: '新增预约' })
if (!options || !options.id) {
// 未携带档案ID:弹出档案选择
this.setData({ needPickProfile: true })
this.openProfilePicker()
return
}
this.profileId = options.id
// 组件根据档案ID自行加载(缓存优先)
this.setData({ profileId: options.id })
},
/**
* 空方法:阻止弹层内容区域点击冒泡关闭
*/
noop() {},
/**
* 档案选择弹层:打开并加载档案列表
*/
openProfilePicker() {
this.setData({ showProfilePicker: true, profileKeyword: '' })
if (this.data.profileList.length === 0) {
this.loadProfiles()
} else {
this.filterProfiles()
}
},
closeProfilePicker() {
this.setData({ showProfilePicker: false })
},
loadProfiles() {
API.healthProfile.getProfiles({ page: 1, pageSize: 100, sortBy: 'updatetime' })
.then(res => {
if (res.code !== 0) {
return wx.showToast({ title: res.msg || '获取档案失败', icon: 'none' })
}
const list = ((res.data && res.data.list) || []).map(item => {
const p = item.profile || {}
const age = calcAge(p.birth)
return {
id: item._id,
name: p.name || '',
mobile: p.mobile || '',
sexLabel: p.sex === 'male' ? '男' : (p.sex === 'female' ? '女' : ''),
ageText: age ? `${age}` : ''
}
})
this.setData({ profileList: list })
this.filterProfiles()
})
.catch(err => {
console.error('获取健康档案失败', err)
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
})
},
onProfileSearch(e) {
this.setData({ profileKeyword: e.detail.value })
this.filterProfiles()
},
filterProfiles() {
const kw = (this.data.profileKeyword || '').trim()
const list = this.data.profileList
const filtered = kw
? list.filter(item => item.name.includes(kw) || (item.mobile || '').includes(kw))
: list
this.setData({ filteredProfiles: filtered })
},
/**
* 选中档案:加载档案信息并进入表单
*/
onProfileSelect(e) {
const id = e.currentTarget.dataset.id
if (!id) return
this.profileId = id
// 组件根据档案ID自行加载(缓存优先)
this.setData({ profileId: id, needPickProfile: false, showProfilePicker: false })
},
/**
* 编辑模式:加载订单并回填表单
*/
loadOrder(id) {
wx.showLoading({ title: '加载中...' })
API.escort.getRecordById(id)
.then(res => {
wx.hideLoading()
if (res.code !== 0 || !res.data || !res.data.record) {
wx.showToast({ title: res.msg || '订单加载失败', icon: 'none' })
setTimeout(() => wx.navigateBack(), 800)
return
}
const order = res.data.record
this.profileId = order.healthProfileId || null
// 患者信息取自订单快照(作为无关联档案时的回退)
const p = order.patient || {}
this.patientInfo = {
name: p.name || '',
mobile: p.mobile || '',
sex: p.sex || '',
birth: p.birth || '',
idnumber: p.idnumber || ''
}
const sexLabel = p.sex === 'male' ? '男' : (p.sex === 'female' ? '女' : '')
const age = calcAge(p.birth)
this.setData({
// 有关联档案时交由 patient-info 按 ID 加载(不读缓存,以档案最新数据为准)
profileId: order.healthProfileId || '',
profileSummary: {
name: p.name || '',
mobile: p.mobile || '',
sexLabel,
ageText: age ? `${age}` : '',
bloodType: (order.health && order.health.bloodType) || '',
height: (order.health && order.health.height) || '',
weight: (order.health && order.health.weight) || '',
remark: (order.health && order.health.remark) || ''
},
form: {
hospital: {
name: (order.hospital && order.hospital.name) || '',
department: (order.hospital && order.hospital.department) || '',
doctor: (order.hospital && order.hospital.doctor) || ''
},
schedule: { date: this.formatDate(order.schedule && order.schedule.date) },
escort: { serviceName: (order.escort && order.escort.serviceName) || '' },
payment: {
totalFee: order.payment && order.payment.totalFee ? String(order.payment.totalFee) : '',
paid: !!(order.payment && order.payment.status === 'paid')
},
notes: { patientNote: (order.notes && order.notes.patientNote) || '' }
}
})
})
.catch(err => {
console.error('获取订单失败', err)
wx.hideLoading()
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
})
},
/**
* 格式化日期为 YYYY-MM-DD(兼容时间戳/字符串)
*/
formatDate(date) {
if (!date) return ''
const d = new Date(date)
if (isNaN(d.getTime())) return String(date)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
},
/**
* patient-info 组件按档案ID加载完成:保存患者原始信息(供提交)及档案附加信息(供标签展示)
*/
onProfileLoaded(e) {
const profile = e.detail.profile || {}
const p = profile.profile || {}
this.patientInfo = {
name: p.name || '',
mobile: p.mobile || '',
sex: p.sex || '',
birth: p.birth || '',
idnumber: p.idnumber || ''
}
this.setData({
profileSummary: {
bloodType: (profile.health && profile.health.bloodType) || '',
height: (profile.health && profile.health.height) || '',
weight: (profile.health && profile.health.weight) || '',
remark: (profile.health && profile.health.remark) || ''
}
})
},
/**
* 表单输入处理
*/
onInput(e) {
const key = e.currentTarget.dataset.key
const value = e.detail.value
this.setData({ [`form.${key}`]: value })
},
/**
* 就诊日期选择
*/
onDateChange(e) {
this.setData({ 'form.schedule.date': e.detail.value })
},
/**
* 是否已支付切换
*/
onPaidChange(e) {
this.setData({ 'form.payment.paid': e.detail.value })
},
/**
* 提交订单(新增或保存修改)
*/
onSubmit() {
if (this.data.submitting) return
const { form } = this.data
if (!this.profileId) return wx.showToast({ title: '请先选择健康档案', icon: 'none' })
// 患者信息取自档案/订单
const patient = this.patientInfo
if (!patient || !patient.name) return wx.showToast({ title: '健康档案信息未加载', icon: 'none' })
if (!form.hospital.name) return wx.showToast({ title: '请输入医院名称', icon: 'none' })
if (!form.schedule.date) return wx.showToast({ title: '请选择就诊日期', icon: 'none' })
const app = getApp()
const userId = app.globalData.user?._id
if (!userId) return wx.showToast({ title: '未获取到用户信息,请重新登录', icon: 'none' })
if (this.data.isEdit) {
this.updateOrder(form)
} else {
this.createOrder(form, userId)
}
},
/**
* 新增订单
*/
createOrder(form, userId) {
const payload = {
userId,
healthProfileId: this.profileId || null,
patient: this.patientInfo,
hospital: {
name: form.hospital.name,
department: form.hospital.department,
doctor: form.hospital.doctor
},
schedule: {
date: form.schedule.date,
duration: 60
},
escort: {
serviceId: -1,
serviceName: form.escort.serviceName
},
payment: {
totalFee: form.payment.totalFee ? Number(form.payment.totalFee) : 0,
paidFee: 0,
status: 'unpaid'
},
notes: {
patientNote: form.notes.patientNote
}
}
this.setData({ submitting: true })
wx.showLoading({ title: '提交中...' })
API.escort.createRecord(payload)
.then(res => {
wx.hideLoading()
if (res.code !== 0) {
this.setData({ submitting: false })
return wx.showToast({ title: res.msg || '创建失败', icon: 'none' })
}
wx.showToast({ title: '创建成功', icon: 'success' })
setTimeout(() => {
wx.navigateBack()
}, 600)
})
.catch(err => {
console.error('创建订单失败', err)
this.setData({ submitting: false })
wx.hideLoading()
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
})
},
/**
* 编辑模式:保存修改
*/
updateOrder(form) {
const payload = {
'hospital.name': form.hospital.name,
'hospital.department': form.hospital.department,
'hospital.doctor': form.hospital.doctor,
'schedule.date': form.schedule.date,
'escort.serviceName': form.escort.serviceName,
'payment.totalFee': form.payment.totalFee ? Number(form.payment.totalFee) : 0,
'payment.paidFee': form.payment.paid ? (form.payment.totalFee ? Number(form.payment.totalFee) : 0) : 0,
'payment.status': form.payment.paid ? 'paid' : 'unpaid',
'notes.patientNote': form.notes.patientNote
}
this.setData({ submitting: true })
wx.showLoading({ title: '保存中...' })
API.escort.updateRecord(this.orderId, payload)
.then(res => {
wx.hideLoading()
if (res.code !== 0) {
this.setData({ submitting: false })
return wx.showToast({ title: res.msg || '保存失败', icon: 'none' })
}
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {
wx.navigateBack()
}, 600)
})
.catch(err => {
console.error('更新订单失败', err)
this.setData({ submitting: false })
wx.hideLoading()
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
})
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
+9
View File
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "新增预约",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"usingComponents": {
"patient-info": "/components/patient-info/index"
}
}
+316
View File
@@ -0,0 +1,316 @@
/* pages/order/orderEdit.wxss */
page {
background-color: #f5f6fa;
color: #1a1a2e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.page {
min-height: 100vh;
background: #f5f6fa;
padding: 24rpx;
box-sizing: border-box;
}
.form {
padding-bottom: 40rpx;
}
/* 健康档案简要信息 */
.profile-brief {
background: #ffffff;
border-radius: 24rpx;
padding: 28rpx 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.brief-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 20rpx;
}
.brief-tag {
font-size: 22rpx;
background-color: rgba(26, 188, 156, 0.1);
color: #1abc9c;
border-radius: 16rpx;
padding: 6rpx 16rpx;
}
.brief-remark {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
color: #888888;
line-height: 1.5;
}
/* 未选择档案占位 */
.profile-empty {
display: flex;
align-items: center;
justify-content: space-between;
}
.profile-empty-main {
display: flex;
flex-direction: column;
}
.profile-empty-title {
font-size: 30rpx;
font-weight: 600;
color: #1a1a1a;
}
.profile-empty-sub {
margin-top: 6rpx;
font-size: 24rpx;
color: #888888;
}
.profile-empty-btn {
padding: 14rpx 32rpx;
border-radius: 40rpx;
background-color: rgba(26, 188, 156, 0.1);
color: #1abc9c;
font-size: 26rpx;
font-weight: 600;
}
/* 档案选择弹层 */
.picker-mask {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 999;
display: flex;
align-items: flex-end;
}
.picker-sheet {
width: 100%;
max-height: 75vh;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
}
.picker-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx 0;
}
.picker-title {
font-size: 32rpx;
font-weight: 700;
color: #1a1a1a;
}
.picker-close {
font-size: 32rpx;
color: #999;
padding: 8rpx;
}
.picker-search {
padding: 24rpx 32rpx;
}
.picker-search-input {
width: 100%;
height: 72rpx;
background: #f5f6fa;
border-radius: 36rpx;
padding: 0 32rpx;
box-sizing: border-box;
font-size: 28rpx;
color: #1a1a2e;
}
.picker-list {
flex: 1;
min-height: 0;
max-height: 55vh;
padding: 0 32rpx 32rpx;
box-sizing: border-box;
}
.picker-item {
display: flex;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f2;
}
.picker-item:last-child {
border-bottom: none;
}
.picker-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #e6f9f3;
color: #17c3a5;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
flex-shrink: 0;
}
.picker-item-main {
flex: 1;
margin-left: 20rpx;
min-width: 0;
}
.picker-item-name-row {
display: flex;
align-items: center;
}
.picker-item-name {
font-size: 30rpx;
font-weight: 600;
color: #1a1a2e;
}
.picker-item-sub {
margin-left: 12rpx;
font-size: 24rpx;
color: #999;
}
.picker-item-mobile {
display: block;
margin-top: 4rpx;
font-size: 24rpx;
color: #999;
}
.picker-arrow {
color: #c0c4cc;
font-size: 32rpx;
}
.picker-empty {
padding: 60rpx 0;
text-align: center;
font-size: 26rpx;
color: #999;
}
.section {
background: #ffffff;
border-radius: 24rpx;
padding: 0 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #1a1a1a;
padding: 28rpx 0 12rpx;
}
.field {
display: flex;
align-items: center;
min-height: 96rpx;
border-bottom: 1rpx solid #f0f0f2;
}
.field:last-child {
border-bottom: none;
}
.field-label {
width: 160rpx;
flex-shrink: 0;
font-size: 28rpx;
color: #666;
}
.field-input {
flex: 1;
font-size: 28rpx;
color: #1a1a2e;
}
.field-picker {
flex: 1;
}
.field-switch {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.switch-label {
margin-right: 16rpx;
font-size: 26rpx;
&.paid {
color: #17c3a5;
}
&.unpaid {
color: #9ca3af;
}
}
.field-value {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
color: #1a1a2e;
}
.field-value.placeholder {
color: #c0c4cc;
}
.arrow {
color: #c0c4cc;
font-size: 32rpx;
}
.field-textarea {
align-items: flex-start;
padding: 24rpx 0;
}
.field-area {
flex: 1;
min-height: 120rpx;
font-size: 28rpx;
color: #1a1a2e;
}
.submit-btn {
margin-top: 16rpx;
height: 96rpx;
line-height: 96rpx;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
background: linear-gradient(135deg, #2dd36f, #17c3a5);
border-radius: 48rpx;
box-shadow: 0 4rpx 16rpx rgba(23, 195, 165, 0.25);
}
+135
View File
@@ -0,0 +1,135 @@
<!--pages/order/orderEdit.wxml-->
<view class="page">
<!-- 未选择档案时的占位提示 -->
<view class="profile-brief profile-empty" wx:if="{{needPickProfile}}" bindtap="openProfilePicker">
<view class="profile-empty-main">
<text class="profile-empty-title">尚未选择健康档案</text>
<text class="profile-empty-sub">患者信息将取自所选档案</text>
</view>
<view class="profile-empty-btn">选择档案</view>
</view>
<!-- 健康档案简要信息 -->
<view class="profile-brief" wx:if="{{profileId || profileSummary}}">
<!-- 有关联档案:统一按档案ID加载(不读缓存,直接请求最新),患者信息以档案为准 -->
<patient-info
wx:if="{{profileId}}"
profile-id="{{profileId}}"
use-cache="{{false}}"
bindloaded="onProfileLoaded"
/>
<!-- 无关联档案(如订单未关联档案):使用订单快照 -->
<patient-info
wx:else
name="{{profileSummary.name}}"
sex-label="{{profileSummary.sexLabel}}"
age-text="{{profileSummary.ageText}}"
mobile="{{profileSummary.mobile}}"
location-text="{{profileSummary.locationText}}"
/>
<view class="brief-tags" wx:if="{{profileSummary.bloodType || profileSummary.height || profileSummary.weight}}">
<text wx:if="{{profileSummary.bloodType}}" class="brief-tag">血型 {{profileSummary.bloodType}}</text>
<text wx:if="{{profileSummary.height}}" class="brief-tag">身高 {{profileSummary.height}}cm</text>
<text wx:if="{{profileSummary.weight}}" class="brief-tag">体重 {{profileSummary.weight}}kg</text>
</view>
<text wx:if="{{profileSummary.remark}}" class="brief-remark">备注:{{profileSummary.remark}}</text>
</view>
<view class="form">
<!-- 就诊信息 -->
<view class="section">
<view class="section-title">就诊信息</view>
<view class="field">
<text class="field-label">医院</text>
<input class="field-input" placeholder="请输入医院名称" value="{{form.hospital.name}}" data-key="hospital.name" bindinput="onInput" />
</view>
<view class="field">
<text class="field-label">科室</text>
<input class="field-input" placeholder="请输入科室" value="{{form.hospital.department}}" data-key="hospital.department" bindinput="onInput" />
</view>
<view class="field">
<text class="field-label">医生姓名</text>
<input class="field-input" placeholder="请输入医生姓名(选填)" value="{{form.hospital.doctor}}" data-key="hospital.doctor" bindinput="onInput" />
</view>
<view class="field">
<text class="field-label">就诊日期</text>
<picker class="field-picker" mode="date" value="{{form.schedule.date}}" start="{{today}}" bindchange="onDateChange">
<view class="field-value {{form.schedule.date ? '' : 'placeholder'}}">
{{form.schedule.date || '请选择就诊日期'}}
<text class="arrow"></text>
</view>
</picker>
</view>
</view>
<!-- 服务信息 -->
<view class="section">
<view class="section-title">服务信息</view>
<view class="field">
<text class="field-label">服务名称</text>
<input class="field-input" placeholder="请输入服务名称" value="{{form.escort.serviceName}}" data-key="escort.serviceName" bindinput="onInput" />
</view>
<view class="field">
<text class="field-label">费用(元)</text>
<input class="field-input" type="digit" placeholder="请输入服务费用" value="{{form.payment.totalFee}}" data-key="payment.totalFee" bindinput="onInput" />
</view>
<view class="field">
<text class="field-label">是否已支付</text>
<view class="field-switch">
<text class="switch-label {{form.payment.paid ? 'paid' : 'unpaid'}}">{{form.payment.paid ? '已支付' : '未支付'}}</text>
<switch checked="{{form.payment.paid}}" color="#17c3a5" bindchange="onPaidChange" />
</view>
</view>
<view class="field field-textarea">
<text class="field-label">备注</text>
<textarea class="field-area" placeholder="请输入备注信息(选填)" value="{{form.notes.patientNote}}" data-key="notes.patientNote" bindinput="onInput" />
</view>
</view>
<!-- 提交 -->
<view class="submit-btn" bindtap="onSubmit">{{isEdit ? '保存修改' : '提交订单'}}</view>
</view>
<!-- 健康档案选择弹层 -->
<view class="picker-mask" wx:if="{{showProfilePicker}}" bindtap="closeProfilePicker">
<view class="picker-sheet" catchtap="noop">
<view class="picker-head">
<text class="picker-title">选择健康档案</text>
<text class="picker-close" bindtap="closeProfilePicker">✕</text>
</view>
<view class="picker-search">
<input class="picker-search-input" placeholder="搜索姓名 / 手机号" value="{{profileKeyword}}" bindinput="onProfileSearch" />
</view>
<scroll-view scroll-y class="picker-list">
<view
class="picker-item"
wx:for="{{filteredProfiles}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onProfileSelect"
>
<view class="picker-avatar">
<text>{{item.name[0]}}</text>
</view>
<view class="picker-item-main">
<view class="picker-item-name-row">
<text class="picker-item-name">{{item.name}}</text>
<text class="picker-item-sub">{{item.sexLabel}}{{item.sexLabel ? ' · ' : ''}}{{item.ageText}}</text>
</view>
<text class="picker-item-mobile">{{item.mobile || '暂无电话'}}</text>
</view>
<text class="picker-arrow"></text>
</view>
<view class="picker-empty" wx:if="{{filteredProfiles.length === 0}}">未找到匹配的档案</view>
</scroll-view>
</view>
</view>
</view>
+16 -1
View File
@@ -3,7 +3,7 @@ const request = require('./request.js')
const API = {
user: {
wxGetPhoneNumber: (data) => request.post('/user/wxgetphonenumber', data),
wxSignin: (data) => request.post('/user/wxsignin', data),
wxSignin: (data) => request.post('/user/wxsignin', data, { skipAuthWait: true }),
signout: (data) => request.post('/user/signout', data),
update: (data) => request.post('/user/update', data),
userInfo: (data) => request.post('/user/userInfo', data),
@@ -11,6 +11,9 @@ const API = {
},
escort: {
// 管理端:按条件查询全部记录
getAllRecords: (params) => request.get('/health/escort-record', params),
// 当前登录用户的记录
getMyRecords: (params) => request.get('/health/escort-record/my', params),
getAttendantRecords: (params) => request.get('/health/escort-record/attendant', params),
getRecordById: (id) => request.get(`/health/escort-record/${id}`),
@@ -20,9 +23,21 @@ const API = {
deleteRecord: (id) => request.delete(`/health/escort-record/${id}`),
},
healthProfile: {
getProfiles: (params) => request.get('/health/health-profile', params),
getProfileById: (id) => request.get(`/health/health-profile/${id}`),
createProfile: (data) => request.post('/health/health-profile', data),
updateProfile: (id, data) => request.put(`/health/health-profile/${id}`, data),
deleteProfile: (id) => request.delete(`/health/health-profile/${id}`),
},
resource: {
getServices: (params) => request.get('/health/service', params),
getAgreement: (params) => request.get('/health/agreement', params),
getHospitalInfo: (params) => request.get('/health/hospital-info', params),
getHospitalRanking: (params) => request.get('/health/hospital-ranking', params),
getDepartmentRankings: (params) => request.get('/health/department-rankings', params),
getAiQuickQuestions: (params) => request.get('/health/ai-quick-questions', params),
},
ai: {
+13
View File
@@ -0,0 +1,13 @@
// 根据出生年月(YYYY-MM-DD)计算年龄
function calcAge(birth) {
if (!birth) return 0
const d = new Date(birth)
if (isNaN(d.getTime())) return 0
const now = new Date()
let age = now.getFullYear() - d.getFullYear()
const m = now.getMonth() - d.getMonth()
if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age--
return age > 0 ? age : 0
}
module.exports = { calcAge }
+19 -5
View File
@@ -1,17 +1,31 @@
class Request {
constructor(baseURL = 'https://api.huashengtec.com') {
//constructor(baseURL = 'http://127.0.0.1:9010') {
//constructor(baseURL = 'http://127.0.0.1:9004') {
this.baseURL = baseURL
}
request(options) {
return new Promise((resolve, reject) => {
const { url, method = 'GET', data = {}, header = {}, ...rest } = options
async request(options) {
const { url, method = 'GET', data = {}, header = {}, skipAuthWait = false, ...rest } = options
// 等待登录完成后再携带 token,避免页面请求早于 app.js 登录的竞态
// skipAuthWait: 登录请求自身使用,否则会等待自己导致死锁
let token = ''
try {
const app = getApp()
const token = app?.globalData?.user?.security?.token || ''
if (skipAuthWait) {
token = app?.globalData?.user?.security?.token || ''
} else if (app && app.ensureLogin) {
const user = await app.ensureLogin()
token = user?.security?.token || ''
} else {
token = app?.globalData?.user?.security?.token || ''
}
} catch (e) {
// 登录失败仍继续请求,由服务端返回未授权错误
}
return new Promise((resolve, reject) => {
data.appId = 'wxapp-escort-admin'
wx.request({
+94
View File
@@ -0,0 +1,94 @@
// utils/store.js
// 轻量全局缓存(内存级,冷启动自然为空)
// - cacheProfile / getProfile / removeProfile:健康档案缓存(id → 原始文档)
// - notifyProfileChange:档案变更通知,列表页订阅后刷新
// 注意:拉取列表回填缓存时务必用 cacheProfile(静默),
// 只有真实的增删改才走 notifyProfileChange,避免"回填→通知→再拉取"死循环
const cache = {}
const listeners = {}
/**
* 订阅事件,返回取消订阅函数
*/
function on(event, cb) {
if (!listeners[event]) listeners[event] = []
listeners[event].push(cb)
return () => off(event, cb)
}
/**
* 取消订阅
*/
function off(event, cb) {
const list = listeners[event]
if (!list) return
const idx = list.indexOf(cb)
if (idx > -1) list.splice(idx, 1)
}
function emit(event, data) {
;(listeners[event] || []).slice().forEach(cb => {
try {
cb(data)
} catch (err) {
console.error(`[store] ${event} 订阅回调异常`, err)
}
})
}
// ---- 通用键值 ----
function get(key) {
return cache[key]
}
function set(key, value) {
cache[key] = value
}
function remove(key) {
delete cache[key]
}
// ---- 健康档案缓存 ----
/**
* 静默回填缓存(不触发变更通知)
*/
function cacheProfile(profile) {
if (profile && profile._id) {
cache[`profile:${profile._id}`] = profile
}
}
function getProfile(id) {
return id ? cache[`profile:${id}`] : null
}
/**
* 删除缓存
*/
function removeProfile(id) {
if (id) delete cache[`profile:${id}`]
}
/**
* 档案变更通知(增删改成功后调用)
* @param {string} action - 'create' | 'update' | 'remove'
* @param {string} id - 档案ID
*/
function notifyProfileChange(action, id) {
emit('profile-change', { action, id })
}
module.exports = {
on,
off,
get,
set,
remove,
cacheProfile,
getProfile,
removeProfile,
notifyProfileChange
}