Prechádzať zdrojové kódy

feat(patient-family): 重构家属首页界面与功能

- 重新设计用户信息展示区域,支持头像和昵称显示
- 新增四大核心功能模块:家人管理、健康监控、联系医生、疑问解答
- 添加今日提醒卡片,展示用药提醒和异常数据统计
- 新增家人动态列表,显示家人的最新活动记录
- 集成扫码功能,支持通过扫描二维码进行操作
- 完善用户信息获取逻辑,增加token验证和自动跳转机制
- 优化页面样式布局,采用渐变背景和圆角卡片设计
- 添加底部导航栏组件,提升页面间切换体验
mcbaiyun 1 mesiac pred
rodič
commit
98dd8e8de2
1 zmenil súbory, kde vykonal 547 pridanie a 11 odobranie
  1. 547 11
      src/pages/patient-family/index/index.vue

+ 547 - 11
src/pages/patient-family/index/index.vue

@@ -1,39 +1,575 @@
 <template>
-  <CustomNav title="家属首页" leftType="back" />
-  <view class="page">
+  <CustomNav title="家属首页" leftType="scan" @scan="handleScan" :opacity="0" />
+  <view class="page-container">
     <view class="content">
-      <text class="title">患者家属</text>
-      <text class="desc">这里是患者家属的管理页面</text>
+      <view class="user-info">
+        <view class="avatar-section">
+          <view class="avatar">
+            <view class="avatar-frame">
+              <image class="avatar-img" :src="avatarSrc" mode="aspectFill" />
+            </view>
+          </view>
+          <view class="user-details">
+            <text class="username">{{ user.nickname || '未登录' }}</text>
+            <text class="user-role">家属</text>
+          </view>
+          <view class="qr-button" @click="onQrClick">
+            <image src="/static/icons/remixicon/qr-code-line.svg" class="qr-icon" />
+          </view>
+        </view>
+      </view>
+
+      <view class="function-container">
+        <view class="function-row">
+          <view class="function-item blue" @click="onItemClick('家人管理')">
+            <view class="item-content">
+              <view class="title-row">
+                <view class="item-line"></view>
+                <text class="item-title">家人管理</text>
+              </view>
+              <text class="item-desc">管理被监护的家人</text>
+            </view>
+          </view>
+          <view class="function-item green" @click="onItemClick('健康监控')">
+            <view class="item-content">
+              <view class="title-row">
+                <view class="item-line"></view>
+                <text class="item-title">健康监控</text>
+              </view>
+              <text class="item-desc">查看家人健康数据</text>
+            </view>
+          </view>
+        </view>
+        <view class="function-row">
+          <view class="function-item orange" @click="onItemClick('联系医生')">
+            <view class="item-content">
+              <view class="title-row">
+                <view class="item-line"></view>
+                <text class="item-title">联系医生</text>
+              </view>
+              <text class="item-desc">与医生沟通交流</text>
+            </view>
+          </view>
+          <view class="function-item purple" @click="onItemClick('疑问解答')">
+            <view class="item-content">
+              <view class="title-row">
+                <view class="item-line"></view>
+                <text class="item-title">疑问解答</text>
+              </view>
+              <text class="item-desc">咨询健康相关问题</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="today-reminder-card">
+        <view class="card-header">
+          <text class="card-title">今日提醒</text>
+        </view>
+        <view class="card-content">
+          <view class="reminder-item" @click="onItemClick('健康监控')">
+            <view class="reminder-icon">
+              <image src="/static/icons/remixicon/time-line.svg" class="icon" />
+            </view>
+            <view class="reminder-text">
+              <text class="reminder-number">{{ todayReminders.medicationCount }}</text>
+              <text class="reminder-label">用药提醒</text>
+            </view>
+          </view>
+          <view class="reminder-item" @click="onItemClick('健康监控')">
+            <view class="reminder-icon">
+              <image src="/static/icons/remixicon/alert-line.svg" class="icon" />
+            </view>
+            <view class="reminder-text">
+              <text class="reminder-number">{{ todayReminders.abnormalCount }}</text>
+              <text class="reminder-label">异常数据</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="family-activity-card">
+        <view class="card-header">
+          <text class="card-title">家人动态</text>
+        </view>
+        <view class="activity-card-content">
+          <view class="activity-item" v-for="(activity, index) in familyActivities" :key="index">
+            <view class="activity-avatar">
+              <image :src="activity.familyAvatar" class="avatar-img" mode="aspectFill" />
+            </view>
+            <view class="activity-text">
+              <text class="activity-desc">{{ activity.desc }}</text>
+              <text class="activity-time">{{ activity.time }}</text>
+            </view>
+          </view>
+          <view v-if="familyActivities.length === 0" class="no-activity">
+            <text>暂无家人动态</text>
+          </view>
+        </view>
+      </view>
+
     </view>
   </view>
+  <TabBar />
 </template>
 
 <script setup lang="ts">
+import { ref, computed } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
 import CustomNav from '@/components/custom-nav.vue'
+import TabBar from '@/components/tab-bar.vue'
+
+const user = ref<{ avatar?: string; nickname?: string }>({})
+
+const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+
+const avatarSrc = computed(() => {
+  const a = user.value?.avatar
+  if (!a) return defaultAvatarUrl
+  try {
+    const s = String(a)
+    if (/^(https?:\/\/|data:|wxfile:\/\/|file:\/\/|\/static\/)/i.test(s)) {
+      return s
+    }
+    if (/^(\.|\/|temp)/i.test(s)) return s
+  } catch (e) {
+    // fallback
+  }
+  return defaultAvatarUrl
+})
+
+const todayReminders = ref({
+  medicationCount: 0,
+  abnormalCount: 0
+})
+
+const familyActivities = ref([
+  {
+    desc: '家人张三完成了今日用药',
+    time: '10分钟前',
+    familyAvatar: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+  },
+  {
+    desc: '家人李四血糖数据已更新',
+    time: '1小时前',
+    familyAvatar: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+  },
+  {
+    desc: '家人王五预约了下周复诊',
+    time: '2小时前',
+    familyAvatar: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+  }
+])
+
+const loadUser = () => {
+  try {
+    const u = uni.getStorageSync('user_info')
+    if (u) {
+      user.value = u
+    }
+  } catch (e) {
+    // ignore
+  }
+}
+
+const fetchUserInfo = async () => {
+  try {
+    const token = uni.getStorageSync('token')
+    if (!token) return
+    uni.showLoading({ title: '加载中...' })
+    const response = await uni.request({
+      url: 'https://wx.baiyun.work/user_info',
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${token}`
+      },
+      data: {}
+    })
+    uni.hideLoading()
+    console.log('User info response:', response)
+    const resp = response.data as any
+    if (response.statusCode === 401) {
+      // Token 无效,清除并跳转登录
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('role')
+      user.value = {}
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
+    }
+    if (resp && resp.code === 200 && resp.data) {
+      user.value = resp.data
+      uni.setStorageSync('user_info', resp.data)
+      if (!resp.data.nickname || !resp.data.avatar) {
+        uni.navigateTo({ url: '/pages/patient-family/profile/index' })
+      }
+    }
+  } catch (err) {
+    uni.hideLoading()
+    console.error('Fetch user info error:', err)
+  }
+}
+
+const fetchTodayReminders = async () => {
+  try {
+    const token = uni.getStorageSync('token')
+    if (!token) return
+    const response = await uni.request({
+      url: 'https://wx.baiyun.work/family/today_reminders',
+      method: 'GET',
+      header: {
+        'Authorization': `Bearer ${token}`
+      }
+    })
+    const resp = response.data as any
+    if (resp && resp.code === 200 && resp.data) {
+      todayReminders.value = resp.data
+    }
+  } catch (err) {
+    console.error('Fetch today reminders error:', err)
+  }
+}
+
+const fetchFamilyActivities = async () => {
+  try {
+    const token = uni.getStorageSync('token')
+    if (!token) return
+    const response = await uni.request({
+      url: 'https://wx.baiyun.work/family/activities',
+      method: 'GET',
+      header: {
+        'Authorization': `Bearer ${token}`
+      }
+    })
+    const resp = response.data as any
+    if (resp && resp.code === 200 && resp.data) {
+      familyActivities.value = resp.data
+    }
+  } catch (err) {
+    console.error('Fetch family activities error:', err)
+  }
+}
+
+// 如果在微信小程序端且未登录,自动跳转到登录页
+onShow(() => {
+  const token = uni.getStorageSync('token')
+  if (!token) {
+    // 使用 uni.reLaunch 替代 navigateTo,确保页面栈被清空
+    uni.reLaunch({ url: '/pages/public/login/index' })
+  } else {
+    fetchUserInfo()
+    fetchTodayReminders()
+    fetchFamilyActivities()
+  }
+})
+
+function handleScan(res: any) {
+  console.log('[family-index] scan result', res)
+  const resultText = res?.result || ''
+  if (resultText) {
+    uni.showToast({ title: String(resultText), icon: 'none', duration: 2000 })
+  } else {
+    uni.showToast({ title: '未识别到有效内容', icon: 'none' })
+  }
+}
+
+function onItemClick(type: string) {
+  if (type === '家人管理') {
+    uni.navigateTo({ url: '/pages/patient-family/family/index' })
+  } else if (type === '健康监控') {
+    uni.navigateTo({ url: '/pages/patient-family/health/index' })
+  } else if (type === '联系医生') {
+    uni.navigateTo({ url: '/pages/patient-family/doctor/index' })
+  } else if (type === '疑问解答') {
+    uni.navigateTo({ url: '/pages/patient-family/qa/index' })
+  } else {
+    uni.showToast({ title: '功能正在开发中', icon: 'none' })
+  }
+}
+
+function onQrClick() {
+  uni.showToast({ title: '二维码功能开发中', icon: 'none' })
+}
 </script>
 
-<style scoped>
-.page {
+<style>
+.page-container {
   min-height: 100vh;
-  background: #f5f6f8;
   padding-top: calc(var(--status-bar-height) + 44px);
+  padding-bottom: 100rpx;
+  box-sizing: border-box;
+  justify-content: center;
+  align-items: center;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
 }
 
 .content {
+  width: 100%;
+}
+
+.user-info {
+  /* background-color: #fff; */
   padding: 40rpx;
-  text-align: center;
+  margin-top: 0rpx;
 }
 
-.title {
-  font-size: 48rpx;
+.avatar-section {
+  display: flex;
+  align-items: center;
+}
+
+.avatar {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  margin-right: 30rpx;
+  position: relative;
+}
+
+.avatar-frame {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  overflow: hidden;
+  border: 4rpx solid #fff;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
+}
+
+.avatar-img {
+  width: 100%;
+  height: 100%;
+}
+
+.user-details {
+  flex: 1;
+}
+
+.username {
+  font-size: 36rpx;
+  font-weight: bold;
   color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.user-role {
+  font-size: 28rpx;
+  color: #666;
+  display: block;
+}
+
+.qr-button {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  background-color: #007aff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3);
+}
+
+.qr-icon {
+  width: 40rpx;
+  height: 40rpx;
+}
+
+.function-container {
+  padding-inline: 20rpx;
+  margin-top: 40rpx;
+}
+
+.function-row {
+  display: flex;
+  justify-content: space-between;
   margin-bottom: 20rpx;
+}
+
+.function-row:last-child {
+  margin-bottom: 0;
+}
+
+.function-item {
+  flex: 1;
+  height: 160rpx;
+  background-color: #fff;
+  border-radius: 20rpx;
+  margin: 0 10rpx;
+  position: relative;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.item-content {
+  position: absolute;
+  top: 20rpx;
+  left: 20rpx;
+  display: flex;
+  flex-direction: column;
+}
+
+.title-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+
+.item-line {
+  width: 8rpx;
+  height: 48rpx;
+  margin-right: 15rpx;
+  border-radius: 10rpx;
+}
+
+.blue .item-line {
+  background-color: #3742fa;
+}
+
+.green .item-line {
+  background-color: #2ed573;
+}
+
+.orange .item-line {
+  background-color: #ffa502;
+}
+
+.purple .item-line {
+  background-color: #9c88ff;
+}
+
+.item-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.item-desc {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.today-reminder-card {
+  margin: 40rpx 20rpx 0;
+  background-color: #fff;
+  border-radius: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.card-header {
+  padding: 30rpx 40rpx 20rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.card-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.card-content {
+  padding: 20rpx 40rpx 30rpx;
+  display: flex;
+  justify-content: space-around;
+}
+
+.reminder-item {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  padding: 20rpx;
+  border-radius: 12rpx;
+  background-color: #f8f9fa;
+  margin: 0 10rpx;
+}
+
+.reminder-icon {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 50%;
+  background-color: #007aff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+}
+
+.icon {
+  width: 30rpx;
+  height: 30rpx;
+}
+
+.reminder-text {
+  flex: 1;
+}
+
+.reminder-number {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #007aff;
   display: block;
 }
 
-.desc {
+.reminder-label {
   font-size: 28rpx;
   color: #666;
   display: block;
 }
+
+.family-activity-card {
+  margin: 40rpx 20rpx 0;
+  background-color: #fff;
+  border-radius: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.activity-card-content {
+  padding: 20rpx 40rpx 30rpx;
+}
+
+.activity-item {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.activity-item:last-child {
+  border-bottom: none;
+}
+
+.activity-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  overflow: hidden;
+  margin-right: 20rpx;
+  border: 2rpx solid #f0f0f0;
+}
+
+.activity-text {
+  flex: 1;
+}
+
+.activity-desc {
+  font-size: 30rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 8rpx;
+  line-height: 1.4;
+}
+
+.activity-time {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+}
+
+.no-activity {
+  text-align: center;
+  padding: 60rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
 </style>