Просмотр исходного кода

feat(patient): 新增患者端首页及个人信息功能

- 新增患者端首页页面,包含用户信息展示和功能入口
- 新增个人中心页面,支持查看和编辑用户信息
- 新增完善基本信息页面,支持头像、昵称、手机号、年龄、性别和地址等信息录入
- 新增患者建档信息页面,支持吸烟史和饮酒史的选择录入
- 更新 tab-bar 组件,根据用户角色跳转不同首页
- 在 pages.json 中配置新增的患者端页面路径和导航栏标题
- 重命名页面文件,将原 public 首页相关文件调整为 patient 页面结构
mcbaiyun 1 месяц назад
Родитель
Сommit
3500b9b53f

+ 12 - 3
src/components/tab-bar.vue

@@ -25,9 +25,18 @@ const onTabClick = (index: number) => {
 
   switch (index) {
     case 0: // 慢病首页
-      uni.switchTab({
-        url: '/pages/public/index/index'
-      })
+      try {
+        const logged = checkLogin()
+        const role = logged ? getRole() : null
+        if (logged && role === 3) {
+          uni.switchTab({ url: '/pages/patient/index/index' })
+        } else {
+          uni.switchTab({ url: '/pages/public/index/index' })
+        }
+      } catch (err) {
+        console.error('tab click index redirect error', err)
+        uni.switchTab({ url: '/pages/public/index/index' })
+      }
       break
     case 1: // 健康数据
       try {

+ 12 - 3
src/pages.json

@@ -3,7 +3,12 @@
 		{
 			"path": "pages/public/index/index",
 			"style": {
-				"navigationBarTitleText": "慢病APP"
+				"navigationBarTitleText": "首页"
+			}
+		},{
+			"path": "pages/patient/index/index",
+			"style": {
+				"navigationBarTitleText": "首页-患者端"
 			}
 		},
 		{
@@ -20,7 +25,7 @@
 		},
 		{
 			"path": "pages/patient/health/index",
-			"style": { "navigationBarTitleText": "健康数据-患者" }
+			"style": { "navigationBarTitleText": "健康数据-患者" }
 		},
 		{
 			"path": "pages/patient/health/details/height",
@@ -88,7 +93,11 @@
 		"list": [
 			{
 				"pagePath": "pages/public/index/index",
-				"text": "慢病首页"
+				"text": "首页"
+			},
+			{
+				"pagePath": "pages/patient/index/index",
+				"text": "患者端首页"
 			},
 			{
 				"pagePath": "pages/public/health/index",

+ 0 - 0
src/pages/public/index/index.unuse → src/pages/patient/index/index.unuse


+ 445 - 0
src/pages/patient/index/index.vue

@@ -0,0 +1,445 @@
+<template>
+  <CustomNav title="首页" leftType="scan" @scan="handleScan" :opacity="0" />
+  <view class="page-container">
+    <view class="content">
+      <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-age" v-if="user.age">年龄: {{ user.age }}</text>
+            <text class="user-id" v-if="user.openid">ID: {{ user.openid }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="function-container">
+        <view class="function-row">
+          <view class="function-item green">
+            <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 blue">
+            <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">
+            <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">
+            <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="health-news-card">
+        <view class="card-header">
+          <text class="card-title">健康资讯</text>
+        </view>
+        <view class="card-content">
+          <view class="news-item" v-for="(news, index) in newsList" :key="index">
+            <view v-if="news.image" class="news-image-container">
+              <image class="news-image" :src="news.image" mode="aspectFill" />
+            </view>
+            <view v-else class="news-placeholder">
+              <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
+            </view>
+            <view class="news-text">
+              <text class="news-title">{{ news.title }}</text>
+              <text class="news-desc">{{ news.desc }}</text>
+            </view>
+          </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; openid?: string; age?: number }>({})
+
+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 newsList = ref([
+  {
+    title: '健康饮食指南',
+    desc: '了解均衡饮食的重要性,掌握健康饮食的基本原则。',
+    image: '/static/carousel/BHFIIABBCDJII-5kCEkD6zh9.png'
+  },
+  {
+    title: '运动与健康',
+    desc: '定期运动对身体的好处,以及如何制定适合自己的运动计划。'
+  },
+  {
+    title: '心理健康维护',
+    desc: '保持良好的心理状态,应对日常生活中的压力和挑战。'
+  },
+  {
+    title: '运动与健康',
+    desc: '定期运动对身体的好处,以及如何制定适合自己的运动计划。'
+  },
+  {
+    title: '心理健康维护',
+    desc: '保持良好的心理状态,应对日常生活中的压力和挑战。'
+  }
+])
+
+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/public/profile/infos/base-info' })
+      }
+    }
+  } catch (err) {
+    uni.hideLoading()
+    console.error('Fetch user info error:', err)
+  }
+}
+
+// 如果在微信小程序端且未登录,自动跳转到登录页
+onShow(() => {
+  const token = uni.getStorageSync('token')
+  if (!token) {
+    // 使用 uni.reLaunch 替代 navigateTo,确保页面栈被清空
+    uni.reLaunch({ url: '/pages/public/login/index' })
+  } else {
+    fetchUserInfo()
+  }
+})
+
+function handleScan(res: any) {
+  console.log('[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' })
+  }
+}</script>
+
+<style>
+.page-container {
+  min-height: 100vh;
+  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;
+  margin-top: 0rpx;
+}
+
+.avatar-section {
+  display: flex;
+  align-items: center;
+}
+
+.avatar {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  border: 1px solid rgba(128, 128, 128, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 30rpx;
+}
+
+.avatar-frame {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.user-details {
+  flex: 1;
+}
+
+.username {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.user-age {
+  font-size: 28rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.user-id {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.function-container {
+  padding-inline: 20rpx;
+}
+
+.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;
+}
+
+.green .item-line {
+  background-color: #2ed573;
+}
+
+.blue .item-line {
+  background-color: #3742fa;
+}
+
+.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;
+}
+
+.health-news-card {
+  background-color: #fff;
+  border-radius: 20rpx;
+  margin: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.card-header {
+  padding: 20rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.card-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.card-content {
+  padding: 20rpx;
+  display: flex;
+  flex-direction: column;
+}
+
+.news-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20rpx;
+  padding-bottom: 20rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.news-item:last-child {
+  margin-bottom: 0;
+  padding-bottom: 0;
+  border-bottom: none;
+}
+
+.news-image-container {
+  width: 180rpx;
+  height: 130rpx;
+  border-radius: 10rpx;
+  margin-right: 20rpx;
+  overflow: hidden;
+}
+
+.news-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.news-placeholder {
+  width: 180rpx;
+  height: 130rpx;
+  background-color: #f0f0f0;
+  border-radius: 10rpx;
+  margin-right: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.placeholder-icon {
+  width: 40rpx;
+  height: 40rpx;
+  opacity: 0.5;
+}
+
+.news-text {
+  flex: 1;
+}
+
+.news-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.news-desc {
+  font-size: 28rpx;
+  color: #666;
+  line-height: 1.4;
+}
+</style>

+ 281 - 0
src/pages/patient/profile/index.vue

@@ -0,0 +1,281 @@
+<template>
+  <CustomNav title="个人中心" leftType="none" />
+  <view class="profile-container">
+    <view class="header">
+      <view class="avatar-section">
+        <view class="avatar">
+          <view class="avatar-frame">
+            <image class="avatar-img" :src="avatarSrc" mode="aspectFill" />
+          </view>
+        </view>
+        <view class="user-info">
+          <text class="username">{{ user.nickname || '慢病患者' }}</text>
+          <text class="user-id" v-if="user.openid">ID: {{ user.openid }}</text>
+        </view>
+      </view>
+    </view>
+
+
+
+    <view class="menu-list">
+      <view class="menu-item" @click="onMenuClick('base-info')">
+        <text class="menu-text">基本信息设定</text>
+        <text class="menu-arrow"></text>
+      </view>
+            <view class="menu-item" @click="onMenuClick('patient-filing')">
+        <text class="menu-text">建档信息设定</text>
+        <text class="menu-arrow"></text>
+      </view>
+      <view class="menu-item" @click="onMenuClick('settings')">
+        <text class="menu-text">设置</text>
+        <text class="menu-arrow"></text>
+      </view>
+      <view class="menu-item" @click="onMenuClick('about')">
+        <text class="menu-text">关于我们</text>
+        <text class="menu-arrow"></text>
+      </view>
+    </view>
+
+    <view class="logout-section">
+      <button class="logout-btn" @click="onLogout">退出登录</button>
+    </view>
+  </view>
+  <TabBar />
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { computed } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import CustomNav from '@/components/custom-nav.vue'
+import TabBar from '@/components/tab-bar.vue'
+
+const title = ref('个人中心')
+
+const user = ref<{ avatar?: string; nickname?: string; openid?: string }>({})
+
+const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+
+// 计算一个安全的 avatar src:优先使用有效的 user.avatar,否则使用默认头像
+const avatarSrc = computed(() => {
+  const a = user.value?.avatar
+  if (!a) return defaultAvatarUrl
+  try {
+    const s = String(a)
+    // 常见有效前缀:http(s), data:, wxfile://, file://, /static
+    if (/^(https?:\/\/|data:|wxfile:\/\/|file:\/\/|\/static\/)/i.test(s)) {
+      return s
+    }
+    // 有时候小程序临时路径也以 / 开头或以 temp 开头,尽量允许以 ./ 或 / 开头
+    if (/^(\.|\/|temp)/i.test(s)) return s
+  } catch (e) {
+    // fallback
+  }
+  return defaultAvatarUrl
+})
+
+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)
+      // 检查 nickname 和 avatar
+      if (!resp.data.nickname || !resp.data.avatar) {
+        uni.navigateTo({ url: '/pages/public/profile/infos/base-info' })
+      }
+    }
+  } catch (err) {
+    uni.hideLoading()
+    console.error('Fetch user info error:', err)
+  }
+}
+
+// 如果在微信小程序端且未登录,自动跳转到登录页
+onShow(() => {
+  const token = uni.getStorageSync('token')
+  if (!token) {
+    // 使用 uni.reLaunch 替代 navigateTo,确保页面栈被清空
+    uni.reLaunch({ url: '/pages/public/login/index' })
+  } else {
+    fetchUserInfo()
+  }
+})
+
+const onMenuClick = (type: string) => {
+  console.log('Menu clicked:', type)
+  if (type === 'base-info') {
+    // 跳转到修改个人信息页面
+    uni.navigateTo({ url: '/pages/public/profile/infos/base-info' })
+    return
+  }
+  if (type === 'patient-filing') {
+    // 跳转到建档信息设定页面
+    uni.navigateTo({ url: '/pages/public/profile/infos/patient-filing' })
+    return
+  }
+  uni.showToast({
+    title: `${type} 功能开发中`,
+    icon: 'none'
+  })
+}
+
+const onLogout = () => {
+  uni.showModal({
+    title: '提示',
+    content: '确定要退出登录吗?',
+    success: (res) => {
+      if (res.confirm) {
+        uni.removeStorageSync('token')
+        uni.removeStorageSync('role')
+        user.value = {}
+        uni.showToast({ title: '已退出登录', icon: 'none' })
+      }
+    }
+  })
+}
+</script>
+
+<style>
+.profile-container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-top: calc(var(--status-bar-height) + 44px);
+}
+
+.header {
+  background-color: #fff;
+  padding: 40rpx;
+  margin-bottom: 20rpx;
+}
+
+.avatar-section {
+  display: flex;
+  align-items: center;
+}
+
+.avatar {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  /* background-color: #007aff; */
+  border: 1px solid rgba(128, 128, 128, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 30rpx;
+}
+
+.avatar-frame {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  overflow: hidden; /* 裁切超出部分,避免溢出 */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover; /* 在部分平台下生效,保证图片填充同时裁切 */
+}
+
+.avatar-text {
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: bold;
+}
+
+.user-info {
+  flex: 1;
+}
+
+.username {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.user-id {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.menu-list {
+  background-color: #fff;
+  margin-bottom: 20rpx;
+}
+
+.menu-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx 40rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.menu-item:last-child {
+  border-bottom: none;
+}
+
+.menu-text {
+  font-size: 32rpx;
+  color: #333;
+}
+
+.menu-arrow {
+  font-size: 28rpx;
+  color: #ccc;
+}
+
+.logout-section {
+  padding: 40rpx;
+}
+
+.logout-btn {
+  width: 100%;
+  background-color: #ff4757;
+  color: #fff;
+  border-radius: 8rpx;
+  font-size: 32rpx;
+  line-height: 80rpx;
+}
+</style>

+ 516 - 0
src/pages/patient/profile/infos/base-info.vue

@@ -0,0 +1,516 @@
+<template>
+  <CustomNav title="完善基本信息" leftType="home" />
+  <view class="complete-container">
+    <view class="form-section">
+      <view class="avatar-section">
+        <view class="avatar">
+          <button
+            class="avatar-wrapper"
+            :class="{ disabled: isChoosing }"
+            :disabled="isChoosing"
+            open-type="chooseAvatar"
+            @tap="startChooseAvatar"
+            @chooseavatar="onChooseAvatar"
+          >
+            <view class="avatar-frame">
+              <image v-if="form.avatar" class="avatar-img" :src="form.avatar" mode="aspectFill" />
+              <text v-else class="avatar-placeholder">点击选择头像</text>
+            </view>
+          </button>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <text class="label">姓名</text>
+        <input class="input" type="nickname" v-model="form.nickname" placeholder="请输入姓名" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">手机号</text>
+        <input class="input" v-model="form.phone" placeholder="请输入手机号" type="number" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">年龄</text>
+        <input class="input" v-model="form.age" placeholder="请输入年龄" type="number" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">性别</text>
+        <view class="radio-group">
+          <label class="radio-item" @click="form.sex = 1">
+            <radio :checked="form.sex === 1" color="#07C160" />
+            <text>男</text>
+          </label>
+          <label class="radio-item" @click="form.sex = 2">
+            <radio :checked="form.sex === 2" color="#07C160" />
+            <text>女</text>
+          </label>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <text class="label">省/市</text>
+        <view class="location-section">
+          <picker mode="region" :value="region" @change="onRegionChange">
+            <view class="picker">{{ region.join(' ') || '请选择省/市' }}</view>
+          </picker>
+          <button class="get-location-btn" @click="getCurrentLocation" :disabled="gettingLocation">
+            {{ gettingLocation ? '获取中...' : '使用当前定位' }}
+          </button>
+        </view>
+      </view>
+
+
+
+      <view class="submit-section">
+        <button class="submit-btn" @click="onSubmit" :disabled="submitting">{{ submitting ? '提交中...' : '提交' }}</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import CustomNav from '@/components/custom-nav.vue'
+
+const form = ref({
+  avatar: '',
+  nickname: '',
+  phone: '',
+  age: '',
+  sex: 0,
+  address: ''
+})
+
+const region = ref<string[]>([])
+const submitting = ref(false)
+const isChoosing = ref(false)
+const gettingLocation = ref(false)
+
+// 从后端拉取已有用户信息并填充表单
+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('fetchUserInfo response:', response)
+    if (response.statusCode === 401) {
+      // 未授权,跳转登录
+      uni.removeStorageSync('token')
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
+    }
+    const resp = response.data as any
+    if (resp && resp.code === 200 && resp.data) {
+      const d = resp.data
+      // 填充基本字段
+      form.value.avatar = d.avatar || form.value.avatar
+      form.value.nickname = d.nickname || form.value.nickname
+      form.value.phone = d.phone || form.value.phone
+      form.value.age = d.age || form.value.age
+      // sex 可能为 'MALE'/'FEMALE' 或数字/1/2
+      if (d.sex) {
+        if (typeof d.sex === 'string') {
+          const s = d.sex.toUpperCase()
+          form.value.sex = s === 'MALE' ? 1 : s === 'FEMALE' ? 2 : form.value.sex
+        } else if (typeof d.sex === 'number') {
+          form.value.sex = d.sex
+        }
+      }
+      // address 可能为 '重庆市 重庆市' 或包含空格,用空格或中文空格分割
+      if (d.address) {
+        const parts = String(d.address).split(/\s+/).filter(Boolean)
+        region.value = parts
+        form.value.address = parts.join(' ')
+      }
+    }
+  } catch (err) {
+    uni.hideLoading()
+    console.error('fetchUserInfo error:', err)
+  }
+}
+
+// 在页面显示时尝试填充
+onShow(() => {
+  fetchUserInfo()
+})
+
+
+const onChooseAvatar = (e: any) => {
+  console.log('onChooseAvatar called with event:', e)
+  try {
+    const detail = e?.detail
+    console.log('Event detail:', detail)
+    let url = ''
+    if (!detail) {
+      url = typeof e === 'string' ? e : ''
+      console.log('No detail, using event as string:', url)
+    } else if (typeof detail === 'string') {
+      url = detail
+      console.log('Detail is string:', url)
+    } else if (detail.avatarUrl) {
+      url = detail.avatarUrl
+      console.log('Using detail.avatarUrl:', url)
+    } else if (Array.isArray(detail) && detail[0]) {
+      url = detail[0]
+      console.log('Using first element of array:', url)
+    }
+    if (url) {
+      form.value.avatar = url
+      console.log('Avatar URL updated to:', form.value.avatar)
+    }
+    if (detail && detail.nickName && !form.value.nickname) {
+      form.value.nickname = detail.nickName
+      console.log('Nickname updated from detail:', form.value.nickname)
+    }
+  } catch (err) {
+    console.error('Error in onChooseAvatar:', err)
+  } finally {
+    isChoosing.value = false
+    console.log('isChoosing reset to false')
+  }
+}
+
+const startChooseAvatar = () => {
+  console.log('startChooseAvatar called, current isChoosing:', isChoosing.value)
+  if (isChoosing.value) {
+    console.log('Already choosing, ignoring')
+    return
+  }
+  isChoosing.value = true
+  console.log('isChoosing set to true')
+  setTimeout(() => {
+    isChoosing.value = false
+    console.log('Timeout: isChoosing reset to false')
+  }, 3000)
+}
+
+
+
+const onRegionChange = (e: any) => {
+  region.value = e.detail.value
+  form.value.address = region.value.join(' ')
+}
+
+const getCurrentLocation = async () => {
+  if (gettingLocation.value) return
+  gettingLocation.value = true
+  try {
+    // 获取经纬度
+    const locationRes = await uni.getLocation({ type: 'wgs84' })
+    const { latitude, longitude } = locationRes
+    console.log('getLocation success:', locationRes)
+
+    // 调用用户自己的地理编码接口
+    const token = uni.getStorageSync('token')
+    const geocodeRes = await uni.request({
+      url: `https://wx.baiyun.work/geo/nearest?latitude=${latitude}&longitude=${longitude}`,
+      method: 'GET',
+      header: {
+        'Authorization': `Bearer ${token}`
+      }
+    })
+
+    const data = geocodeRes.data as any
+    if (data && data.code === 200 && data.data) {
+      // 解析 data 字段中的 JSON 字符串
+      const addressData = JSON.parse(data.data)
+      if (addressData && addressData.province && addressData.city) {
+        region.value = [addressData.province, addressData.city, addressData.district].filter(Boolean)
+        form.value.address = region.value.join(' ')
+        uni.showToast({ title: '位置获取成功', icon: 'success' })
+      } else {
+        throw new Error('Invalid address data')
+      }
+    } else {
+      throw new Error('Geocode failed')
+    }
+  } catch (err) {
+    console.error('Get location error:', err)
+    uni.showToast({ title: '获取位置失败,请检查定位权限和网络', icon: 'none' })
+  } finally {
+    gettingLocation.value = false
+  }
+}
+
+const onSubmit = async () => {
+  if (submitting.value) return
+
+  // 检查所有必需字段
+  if (!form.value.avatar) {
+    uni.showToast({ title: '请选择头像', icon: 'none' })
+    return
+  }
+  if (!form.value.nickname) {
+    uni.showToast({ title: '请输入姓名', icon: 'none' })
+    return
+  }
+  // 姓名格式校验:2-30 个中文、字母、·或空格
+  const nameRegex = /^[\u4e00-\u9fa5A-Za-z·\s]{2,10}$/
+  if (!nameRegex.test(String(form.value.nickname))) {
+    uni.showToast({ title: '姓名格式不正确,请输入2-10位中文或字母', icon: 'none' })
+    return
+  }
+  if (!form.value.phone) {
+    uni.showToast({ title: '请输入手机号', icon: 'none' })
+    return
+  }
+  // 手机号格式(中国)校验
+  const phoneRegex = /^1[3-9]\d{9}$/
+  if (!phoneRegex.test(String(form.value.phone))) {
+    uni.showToast({ title: '手机号格式不正确', icon: 'none' })
+    return
+  }
+  if (!form.value.age) {
+    uni.showToast({ title: '请输入年龄', icon: 'none' })
+    return
+  }
+  // 年龄为 1-120 的整数
+  const ageNum = Number(form.value.age)
+  if (!Number.isInteger(ageNum) || ageNum < 1 || ageNum > 120) {
+    uni.showToast({ title: '年龄请输入1到120之间的整数', icon: 'none' })
+    return
+  }
+  if (!form.value.sex) {
+    uni.showToast({ title: '请选择性别', icon: 'none' })
+    return
+  }
+  if (!form.value.address) {
+    uni.showToast({ title: '请选择或自动获取省/市', icon: 'none' })
+    return
+  }
+
+  submitting.value = true
+  try {
+    const token = uni.getStorageSync('token')
+    const response = await uni.request({
+      url: 'https://wx.baiyun.work/update_user_info',
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${token}`
+      },
+      data: form.value
+    })
+    console.log('Update response:', response)
+    const resp = response.data as any
+    if (resp && resp.code === 200) {
+      uni.showToast({ title: '信息更新成功', icon: 'success' })
+      setTimeout(() => {
+        uni.navigateBack()
+      }, 1500)
+    } else {
+      throw new Error('Update failed')
+    }
+  } catch (err) {
+    console.error('Update error:', err)
+    uni.showToast({ title: '更新失败', icon: 'error' })
+  } finally {
+    submitting.value = false
+  }
+}
+</script>
+
+<style>
+.complete-container {
+  min-height: 100vh;
+  /* 为固定在顶部的 CustomNav 留出空间(状态栏 + 导航栏 44px) */
+  padding-top: calc(var(--status-bar-height) + 44px + 40rpx);
+  /* 保留侧边与内部间距,使用 border-box 避免计算误差 */
+  padding-right: 40rpx;
+  padding-left: 40rpx;
+  /* 底部安全区:使用项目中声明的 --window-bottom 或 fallback */
+  padding-bottom: calc(var(--window-bottom, 0px) + 40rpx);
+  box-sizing: border-box;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.form-section {
+  background-color: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  width: 100%;
+  max-width: 600rpx;
+  box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
+}
+
+.avatar-section {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 40rpx;
+}
+
+.avatar {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 90rpx;
+  background-color: #eee;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar-wrapper {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 90rpx;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+  border: 2rpx solid #07C160;
+  background: #fff;
+}
+
+.avatar-wrapper.disabled {
+  opacity: 0.5;
+}
+
+.avatar-frame {
+  width: 100%;
+  height: 100%;
+  border-radius: 60rpx;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.avatar-placeholder {
+  color: #999;
+  font-size: 24rpx;
+}
+
+.form-item {
+  margin-bottom: 30rpx;
+}
+
+.label {
+  display: block;
+  font-size: 32rpx;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.input {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 0 20rpx;
+  font-size: 32rpx;
+  box-sizing: border-box;
+  max-width: 100%;
+}
+
+.phone-section {
+  display: flex;
+  align-items: center;
+}
+
+.get-phone-btn {
+  flex: 1;
+  height: 80rpx;
+  background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
+  color: #fff;
+  border-radius: 8rpx;
+  font-size: 32rpx;
+  border: none;
+}
+
+.get-phone-btn:disabled {
+  opacity: 0.5;
+}
+
+.radio-group {
+  display: flex;
+  gap: 40rpx;
+}
+
+.radio-item {
+  display: flex;
+  align-items: center;
+  font-size: 32rpx;
+  color: #333;
+}
+
+picker {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 0 20rpx;
+  display: flex;
+  align-items: center;
+  font-size: 32rpx;
+  color: hsl(0, 0%, 20%);
+  box-sizing: border-box;
+  max-width: 100%;
+}
+
+.form-item, .picker, .input, .get-location-btn, .submit-btn {
+  box-sizing: border-box;
+}
+
+.location-section {
+  display: flex;
+  align-items: center;
+  gap: 20rpx;
+}
+
+.get-location-btn {
+  flex-shrink: 0;
+  height: 80rpx;
+  background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
+  color: #fff;
+  border-radius: 8rpx;
+  font-size: 28rpx;
+  padding: 0 10rpx;
+  border: none;
+}
+
+.get-location-btn:disabled {
+  opacity: 0.5;
+}
+
+.submit-section {
+  margin-top: 60rpx;
+}
+
+.submit-btn {
+  width: 100%;
+  background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
+  color: #fff;
+  border-radius: 12rpx;
+  font-size: 32rpx;
+  line-height: 80rpx;
+  border: none;
+}
+
+.submit-btn:disabled {
+  opacity: 0.5;
+}
+</style>

+ 291 - 0
src/pages/patient/profile/infos/patient-filing.vue

@@ -0,0 +1,291 @@
+<template>
+  <CustomNav title="患者建档信息" leftType="home" />
+  <view class="complete-container">
+    <view class="form-section">
+      <text class="section-title">患者建档信息</text>
+
+      <!-- 吸烟饮酒史 -->
+      <view class="form-item">
+        <text class="label">吸烟史</text>
+        <picker mode="selector" :range="smokingOptions" :value="smokingIndex" range-key="label" @change="onSmokingChange">
+          <view class="picker">{{ smokingSelected ? smokingOptions[smokingIndex]?.label : '请点击选择' }}</view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <text class="label">饮酒史</text>
+        <picker mode="selector" :range="drinkingOptions" :value="drinkingIndex" range-key="label" @change="onDrinkingChange">
+          <view class="picker">{{ drinkingSelected ? drinkingOptions[drinkingIndex]?.label : '请点击选择' }}</view>
+        </picker>
+      </view>
+
+      <!-- 疾病诊断 -->
+      <view class="form-item">
+        <text class="label">疾病诊断</text>
+        <view class="disease-list">
+          <view class="disease-item" v-for="disease in diseases" :key="disease.key">
+            <text class="disease-label">{{ disease.label }}</text>
+            <switch :checked="diseaseHistory[disease.key]" @change="(e: any) => toggleDisease(disease.key, e.detail.value)" />
+          </view>
+        </view>
+      </view>
+
+      <!-- 用药情况 -->
+      <view class="form-item">
+        <text class="label">用药情况</text>
+        <textarea
+          v-model="medicationHistory"
+          placeholder="请输入用药情况"
+          class="input"
+        />
+      </view>
+
+      <!-- 过敏史 -->
+      <view class="form-item">
+        <view class="switch-row">
+          <text class="label">过敏史</text>
+          <switch :checked="hasAllergy" @change="(e: any) => hasAllergy = e.detail.value" />
+        </view>
+        <view v-if="hasAllergy" class="input-container">
+          <textarea
+            v-model="allergyHistory"
+            placeholder="请输入过敏史"
+            class="input"
+          />
+        </view>
+      </view>
+
+      <!-- 家族史 -->
+      <view class="form-item">
+        <view class="switch-row">
+          <text class="label">家族史</text>
+          <switch :checked="hasFamilyHistory" @change="(e: any) => hasFamilyHistory = e.detail.value" />
+        </view>
+        <view v-if="hasFamilyHistory" class="input-container">
+          <textarea
+            v-model="familyHistory"
+            placeholder="请输入家族史"
+            class="input"
+          />
+        </view>
+      </view>
+
+      <!-- 提交按钮 -->
+      <view class="submit-section">
+        <button class="submit-btn" @click="submitForm">提交</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import CustomNav from '@/components/custom-nav.vue'
+
+// 吸烟选项
+const smokingOptions = [
+  { label: '否', value: 'no' },
+  { label: '偶尔(吸烟<1包/周)', value: 'occasional' },
+  { label: '经常', value: 'frequent' }
+]
+
+// 饮酒选项
+const drinkingOptions = [
+  { label: '否', value: 'no' },
+  { label: '偶尔(饮酒<1次/周)', value: 'occasional' },
+  { label: '经常', value: 'frequent' }
+]
+
+// 疾病列表
+const diseases = [
+  { key: 'diabetes', label: '糖尿病' },
+  { key: 'hypertension', label: '高血压' },
+  { key: 'dyslipidemia', label: '血脂异常' },
+  { key: 'coronaryHeartDisease', label: '冠心病' },
+  { key: 'cerebralInfarction', label: '脑梗塞' }
+]
+
+// 响应式数据
+const smokingHistory = ref('')
+const drinkingHistory = ref('')
+const smokingIndex = ref(0)
+const drinkingIndex = ref(0)
+const smokingSelected = ref(false)
+const drinkingSelected = ref(false)
+const diseaseHistory = ref<Record<string, boolean>>({
+  diabetes: false,
+  hypertension: false,
+  dyslipidemia: false,
+  coronaryHeartDisease: false,
+  cerebralInfarction: false
+})
+const medicationHistory = ref('')
+const allergyHistory = ref('')
+const familyHistory = ref('')
+const hasAllergy = ref(false)
+const hasFamilyHistory = ref(false)
+
+// 切换疾病状态
+const toggleDisease = (key: string, value: boolean) => {
+  diseaseHistory.value[key] = value
+}
+
+// picker change事件处理
+const onSmokingChange = (e: any) => {
+  smokingIndex.value = e.detail.value
+  smokingHistory.value = smokingOptions[smokingIndex.value].value
+  smokingSelected.value = true
+}
+
+const onDrinkingChange = (e: any) => {
+  drinkingIndex.value = e.detail.value
+  drinkingHistory.value = drinkingOptions[drinkingIndex.value].value
+  drinkingSelected.value = true
+}
+
+// 提交表单
+const submitForm = () => {
+  const formData = {
+    smokingHistory: smokingHistory.value,
+    drinkingHistory: drinkingHistory.value,
+    diseaseHistory: diseaseHistory.value,
+    medicationHistory: medicationHistory.value,
+    allergyHistory: hasAllergy.value ? allergyHistory.value : '',
+    familyHistory: hasFamilyHistory.value ? familyHistory.value : ''
+  }
+  console.log('提交的表单数据:', formData)
+  // 这里可以添加提交逻辑,比如调用API
+  uni.showToast({
+    title: '提交成功',
+    icon: 'success'
+  })
+}
+</script>
+
+<style>
+.complete-container {
+  min-height: 100vh;
+  /* 为固定在顶部的 CustomNav 留出空间(状态栏 + 导航栏 44px) */
+  padding-top: calc(var(--status-bar-height) + 44px + 40rpx);
+  /* 保留侧边与内部间距,使用 border-box 避免计算误差 */
+  padding-right: 40rpx;
+  padding-left: 40rpx;
+  /* 底部安全区:使用项目中声明的 --window-bottom 或 fallback */
+  padding-bottom: calc(var(--window-bottom, 0px) + 40rpx);
+  box-sizing: border-box;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.form-section {
+  background-color: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  width: 100%;
+  max-width: 600rpx;
+  box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
+}
+
+.section-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 40rpx;
+  display: block;
+  text-align: center;
+}
+
+.form-item {
+  margin-bottom: 30rpx;
+}
+
+.label {
+  display: block;
+  font-size: 32rpx;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.radio-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 40rpx;
+  margin-bottom: 20rpx;
+}
+
+.radio-item {
+  display: flex;
+  align-items: center;
+  font-size: 32rpx;
+  color: #333;
+}
+
+.disease-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.disease-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx;
+  background-color: #f9f9f9;
+  border-radius: 8rpx;
+}
+
+.disease-label {
+  font-size: 32rpx;
+  color: #333;
+}
+
+.input {
+  width: 100%;
+  height: 120rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 20rpx;
+  font-size: 32rpx;
+  box-sizing: border-box;
+  max-width: 100%;
+}
+
+.picker {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 0 20rpx;
+  display: flex;
+  align-items: center;
+  font-size: 32rpx;
+  color: #333;
+  box-sizing: border-box;
+  max-width: 100%;
+}
+
+.switch-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+
+.input-container {
+  margin-top: 10rpx;
+}
+
+.submit-btn {
+  width: 100%;
+  background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
+  color: #fff;
+  border-radius: 12rpx;
+  font-size: 32rpx;
+  line-height: 80rpx;
+  border: none;
+}
+</style>

+ 72 - 366
src/pages/public/index/index.vue

@@ -1,185 +1,59 @@
 <template>
-  <CustomNav title="首页" leftType="scan" @scan="handleScan" :opacity="0" />
-  <view class="page-container">
-    <view class="content">
-      <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-age" v-if="user.age">年龄: {{ user.age }}</text>
-            <text class="user-id" v-if="user.openid">ID: {{ user.openid }}</text>
-          </view>
-        </view>
+  <CustomNav title="首页" leftType="scan" @scan="handleScan" />
+  <view class="content">
+    <!-- 原始页面已移至此文件,保留以备参考 -->
+    <swiper class="banner-swiper" :indicator-dots="true" :autoplay="true" :interval="3000" :circular="true">
+      <swiper-item v-for="(img, idx) in bannerImages" :key="idx">
+        <image :src="img" class="banner-img" mode="aspectFill" />
+      </swiper-item>
+    </swiper>
+    <view class="card-list">
+      <view class="card" v-for="(card, idx) in cards" :key="idx">
+        <view class="card-title">{{ card.title }}</view>
+        <view class="card-desc">{{ card.desc }}</view>
       </view>
-
-      <view class="function-container">
-        <view class="function-row">
-          <view class="function-item green">
-            <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 blue">
-            <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">
-            <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">
-            <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="health-news-card">
-        <view class="card-header">
-          <text class="card-title">健康资讯</text>
-        </view>
-        <view class="card-content">
-          <view class="news-item" v-for="(news, index) in newsList" :key="index">
-            <view v-if="news.image" class="news-image-container">
-              <image class="news-image" :src="news.image" mode="aspectFill" />
-            </view>
-            <view v-else class="news-placeholder">
-              <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
-            </view>
-            <view class="news-text">
-              <text class="news-title">{{ news.title }}</text>
-              <text class="news-desc">{{ news.desc }}</text>
-            </view>
-          </view>
-        </view>
-      </view>
-
     </view>
   </view>
   <TabBar />
 </template>
 
 <script setup lang="ts">
-import { ref, computed } from 'vue'
+import { ref } from 'vue'
 import { onShow } from '@dcloudio/uni-app'
 import CustomNav from '@/components/custom-nav.vue'
 import TabBar from '@/components/tab-bar.vue'
+import { isLoggedIn as checkLogin, getRole } from '@/composables/useAuth'
 
-const user = ref<{ avatar?: string; nickname?: string; openid?: string; age?: number }>({})
+const title = ref('Hello')
 
-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 bannerImages = [
+  '/static/carousel/BHFIIABBCDJII-5kCEkD6zh9.png',
+  '/static/carousel/BHFIIABBDGHEA-wtWLrLS75o.png',
+  '/static/carousel/BHFIIABBHJBAH-yDeckRiiQP.png'
+]
 
-const newsList = ref([
-  {
-    title: '健康饮食指南',
-    desc: '了解均衡饮食的重要性,掌握健康饮食的基本原则。',
-    image: '/static/carousel/BHFIIABBCDJII-5kCEkD6zh9.png'
-  },
-  {
-    title: '运动与健康',
-    desc: '定期运动对身体的好处,以及如何制定适合自己的运动计划。'
-  },
-  {
-    title: '心理健康维护',
-    desc: '保持良好的心理状态,应对日常生活中的压力和挑战。'
-  },
-  {
-    title: '运动与健康',
-    desc: '定期运动对身体的好处,以及如何制定适合自己的运动计划。'
-  },
-  {
-    title: '心理健康维护',
-    desc: '保持良好的心理状态,应对日常生活中的压力和挑战。'
-  }
-])
+const cards = [
+  { title: '健康档案', desc: '管理您的健康信息' },
+  { title: '慢病管理', desc: '查看慢病相关数据' },
+  { title: '健康咨询', desc: '在线咨询医生' },
+  { title: '用药提醒', desc: '设置用药提醒' }
+]
 
-const loadUser = () => {
+onShow(() => {
   try {
-    const u = uni.getStorageSync('user_info')
-    if (u) {
-      user.value = u
+    const logged = checkLogin()
+    if (!logged) {
+      return
     }
-  } 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 (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/public/profile/infos/base-info' })
-      }
+    const r = getRole()
+    if (r === 3) {
+      console.log('[index] redirecting to patient index, role=', r)
+      uni.reLaunch({ url: '/pages/patient/index/index' })
+      return
     }
   } catch (err) {
-    uni.hideLoading()
-    console.error('Fetch user info error:', err)
-  }
-}
-
-onShow(() => {
-  const token = uni.getStorageSync('token')
-  if (token) {
-    fetchUserInfo()
+    console.error('检查登录态时出错:', err)
   }
 })
 
@@ -191,243 +65,75 @@ function handleScan(res: any) {
   } else {
     uni.showToast({ title: '未识别到有效内容', icon: 'none' })
   }
-}</script>
-
-<style>
-.page-container {
-  min-height: 100vh;
-  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%);
 }
+</script>
 
+<style>
 .content {
-  width: 100%;
-}
-
-.user-info {
-  /* background-color: #fff; */
-  padding: 40rpx;
-  margin-top: 0rpx;
-}
-
-.avatar-section {
-  display: flex;
-  align-items: center;
-}
-
-.avatar {
-  width: 120rpx;
-  height: 120rpx;
-  border-radius: 50%;
-  border: 1px solid rgba(128, 128, 128, 0.5);
   display: flex;
+  flex-direction: column;
   align-items: center;
-  justify-content: center;
-  margin-right: 30rpx;
+  justify-content: flex-start;
+  padding-top: calc(var(--status-bar-height) + 44px);
+  height: calc(100vh - var(--status-bar-height) - 44px);
+  background: #f7f8fa;
 }
 
-.avatar-frame {
-  width: 100%;
-  height: 100%;
-  border-radius: 50%;
+.banner-swiper {
+  width: 670rpx;
+  max-width: 100vw;
+  height: 400rpx;
+  margin: 20rpx auto 30rpx auto;
+  border-radius: 16rpx;
   overflow: hidden;
-  display: flex;
-  align-items: center;
-  justify-content: center;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
+  background: #fff;
 }
 
-.avatar-img {
+.banner-img {
   width: 100%;
   height: 100%;
-  object-fit: cover;
-}
-
-.user-details {
-  flex: 1;
-}
-
-.username {
-  font-size: 36rpx;
-  font-weight: bold;
-  color: #333;
   display: block;
-  margin-bottom: 10rpx;
-}
-
-.user-age {
-  font-size: 28rpx;
-  color: #666;
-  display: block;
-  margin-bottom: 10rpx;
-}
-
-.user-id {
-  font-size: 28rpx;
-  color: #666;
-}
-
-.function-container {
-  padding-inline: 20rpx;
-}
-
-.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;
+.card-list {
+  width: 90%;
   display: flex;
   flex-direction: column;
+  gap: 32rpx;
+  padding: 0 40rpx;
+  margin: 0 auto;
 }
 
-.title-row {
+.card {
+  background: #fff;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+  padding: 40rpx 32rpx;
+  box-sizing: border-box;
   display: flex;
-  align-items: center;
-  margin-bottom: 10rpx;
-}
-
-.item-line {
-  width: 8rpx;
-  height: 48rpx;
-  margin-right: 15rpx;
-  border-radius: 10rpx;
-}
-
-.green .item-line {
-  background-color: #2ed573;
-}
-
-.blue .item-line {
-  background-color: #3742fa;
-}
-
-.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;
-}
-
-.health-news-card {
-  background-color: #fff;
-  border-radius: 20rpx;
-  margin: 20rpx;
-  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
-  overflow: hidden;
-}
-
-.card-header {
-  padding: 20rpx;
-  border-bottom: 1rpx solid #eee;
+  flex-direction: column;
 }
 
 .card-title {
   font-size: 36rpx;
   font-weight: bold;
   color: #333;
+  margin-bottom: 16rpx;
 }
 
-.card-content {
-  padding: 20rpx;
-  display: flex;
-  flex-direction: column;
-}
-
-.news-item {
-  display: flex;
-  align-items: center;
-  margin-bottom: 20rpx;
-  padding-bottom: 20rpx;
-  border-bottom: 1rpx solid #eee;
-}
-
-.news-item:last-child {
-  margin-bottom: 0;
-  padding-bottom: 0;
-  border-bottom: none;
-}
-
-.news-image-container {
-  width: 180rpx;
-  height: 130rpx;
-  border-radius: 10rpx;
-  margin-right: 20rpx;
-  overflow: hidden;
-}
-
-.news-image {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
+.card-desc {
+  font-size: 28rpx;
+  color: #888;
 }
 
-.news-placeholder {
-  width: 180rpx;
-  height: 130rpx;
-  background-color: #f0f0f0;
-  border-radius: 10rpx;
-  margin-right: 20rpx;
+.text-area {
   display: flex;
-  align-items: center;
   justify-content: center;
 }
 
-.placeholder-icon {
-  width: 40rpx;
-  height: 40rpx;
-  opacity: 0.5;
-}
-
-.news-text {
-  flex: 1;
-}
-
-.news-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-  margin-bottom: 10rpx;
-}
-
-.news-desc {
-  font-size: 28rpx;
-  color: #666;
-  line-height: 1.4;
+.title {
+  font-size: 36rpx;
+  color: #8f8f94;
 }
 </style>