Kaynağa Gözat

feat(pages): 重构页面路径结构并添加患者端健康页

- 调整所有页面路径,将原有页面移至 /pages/public 或 /pages/patient 目录下
- 新增患者端健康数据页面,区分公共健康页与患者健康页
- 更新 tabBar 配置与导航逻辑,适配新的页面路径
- 添加 useAuth 组合式函数,用于判断登录状态与角色并实现患者自动跳转
- 修改登录成功后跳转逻辑,指向新的个人中心页面
- 修复健康提醒页面的来源识别逻辑,适配新路径结构
- 更新文档中涉及页面路径的说明与示例
mcbaiyun 1 ay önce
ebeveyn
işleme
8d860cc237

+ 4 - 4
docs/page-entry-source-detection.md

@@ -14,8 +14,8 @@
 ### 小程序启动和页面跳转机制
 
 当用户通过推送消息等外部链接进入小程序时,流程通常是:
-1. 小程序首先启动到默认首页(如 `pages/index/index`)
-2. 然后通过内部路由跳转到目标页面(如 `pages/health/reminder`)
+1. 小程序首先启动到默认首页(如 `pages/public/index/index`)
+2. 然后通过内部路由跳转到目标页面(如 `pages/public/health/reminder`)
 3. 在这个跳转过程中,启动参数(launch options)会丢失
 
 ### 参数传递机制区别
@@ -58,7 +58,7 @@ if (pages && pages.length >= 2) {
   const prevPage = pages[pages.length - 2]
   const prevRoute = prevPage?.route || prevPage?.__route || prevPage?.$page?.route
   
-  if (prevRoute && prevRoute.includes('pages/health/index')) {
+  if (prevRoute && prevRoute.includes('pages/public/health/index')) {
     // 来自健康首页
   }
 }
@@ -99,7 +99,7 @@ function detectEntrySource() {
       const prevPage = pages[pages.length - 2]
       const prevRoute = prevPage?.route || prevPage?.__route || prevPage?.$page?.route
       
-      if (prevRoute && prevRoute.includes('pages/health/index')) {
+      if (prevRoute && prevRoute.includes('pages/public/health/index')) {
         return 'healthIndex'
       }
     }

+ 3 - 3
src/components/custom-nav.vue

@@ -40,13 +40,13 @@ const handleLeft = () => {
   if (t === 'home') {
     // 若首页为 tabBar 页,优先使用 switchTab
     try {
-      uni.switchTab({ url: '/pages/index/index' })
+      uni.switchTab({ url: '/pages/public/index/index' })
     } catch (e) {
       // fallback 到 reLaunch / navigateTo
       try {
-        uni.reLaunch({ url: '/pages/index/index' })
+        uni.reLaunch({ url: '/pages/public/index/index' })
       } catch (e2) {
-        uni.navigateTo({ url: '/pages/index/index' })
+        uni.navigateTo({ url: '/pages/public/index/index' })
       }
     }
     return

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

@@ -22,17 +22,17 @@ const onTabClick = (index: number) => {
   switch (index) {
     case 0: // 慢病首页
       uni.switchTab({
-        url: '/pages/index/index'
+        url: '/pages/public/index/index'
       })
       break
     case 1: // 健康数据
       uni.switchTab({
-        url: '/pages/health/index'
+        url: '/pages/public/health/index'
       })
       break
     case 2: // 个人中心
       uni.switchTab({
-        url: '/pages/profile/index'
+        url: '/pages/public/profile/index'
       })
       break
   }

+ 61 - 0
src/composables/useAuth.ts

@@ -0,0 +1,61 @@
+// 简单的 Auth composable:基于本地存储判断登录态与角色
+// 说明:按当前要求不向后端请求角色信息,直接读取 local storage 的 'token' 和 'role'
+// TODO: 如需更可靠的判断,可增加 fetchUserInfo() 调用后端并以服务器返回为准;还可加入 token 刷新逻辑
+import { ref } from 'vue'
+
+const tokenKey = 'token'
+const roleKey = 'role'
+
+export function isLoggedIn(): boolean {
+  try {
+    const t = uni.getStorageSync(tokenKey)
+    return !!t
+  } catch (err) {
+    console.error('isLoggedIn: getStorageSync error', err)
+    return false
+  }
+}
+
+export function getRole(): number | null {
+  try {
+    const r = uni.getStorageSync(roleKey)
+    const rn = typeof r === 'number' ? r : (r ? Number(r) : null)
+    return Number.isFinite(rn) ? rn : null
+  } catch (err) {
+    console.error('getRole: getStorageSync error', err)
+    return null
+  }
+}
+
+export function roleToString(r: number | null): string {
+  if (r === 2) return '医生'
+  if (r === 3) return '患者'
+  if (r === 4) return '患者家属'
+  return r === null ? '' : String(r)
+}
+
+/**
+ * 如果当前已登录且本地 role===3(患者),则切换到患者端健康页(tab 页面)。
+ * 返回:true 表示已发起跳转,false 表示未跳转。
+ */
+export function ensurePatientRedirect(): boolean {
+  try {
+    if (!isLoggedIn()) return false
+    const r = getRole()
+    if (r === 3) {
+      // patient health 在 tabBar 中,使用 switchTab
+      uni.switchTab({ url: '/pages/patient/health/index' })
+      return true
+    }
+    return false
+  } catch (err) {
+    console.error('ensurePatientRedirect error', err)
+    return false
+  }
+}
+
+// 可选:导出 reactive 的状态以便组件订阅(当前未被使用,但保留以备扩展)
+export const authState = {
+  logged: ref<boolean>(isLoggedIn()),
+  role: ref<number | null>(getRole())
+}

+ 25 - 17
src/pages.json

@@ -1,65 +1,69 @@
 {
 	"pages": [
 		{
-			"path": "pages/index/index",
+			"path": "pages/public/index/index",
 			"style": {
 				"navigationBarTitleText": "慢病APP"
 			}
 		},
 		{
-			"path": "pages/login/index",
+			"path": "pages/public/login/index",
 			"style": {
 				"navigationBarTitleText": "登录"
 			}
 		},
 		{
-			"path": "pages/health/index",
+			"path": "pages/public/health/index",
 			"style": {
-				"navigationBarTitleText": "健康数据"
+				"navigationBarTitleText": "健康数据-公共页"
 			}
 		},
 		{
-			"path": "pages/health/details/height",
+			"path": "pages/patient/health/index",
+			"style": { "navigationBarTitleText": "健康数据-患者页" }
+		},
+		{
+			"path": "pages/patient/health/details/height",
 			"style": { "navigationBarTitleText": "身高" }
 		},
 		{
-			"path": "pages/health/details/weight",
+			"path": "pages/patient/health/details/weight",
 			"style": { "navigationBarTitleText": "体重" }
 		},
 		{
-			"path": "pages/health/details/bmi",
+			"path": "pages/patient/health/details/bmi",
 			"style": { "navigationBarTitleText": "BMI" }
 		},
 		{
-			"path": "pages/health/details/blood-pressure",
+			"path": "pages/patient/health/details/blood-pressure",
 			"style": { "navigationBarTitleText": "血压" }
 		},
 		{
-			"path": "pages/health/details/blood-glucose",
+			"path": "pages/patient/health/details/blood-glucose",
 			"style": { "navigationBarTitleText": "血糖" }
 		},
 		{
-			"path": "pages/health/details/heart-rate",
+			"path": "pages/patient/health/details/heart-rate",
 			"style": { "navigationBarTitleText": "心率" }
 		},
 		{
-			"path": "pages/health/reminder",
+			"path": "pages/patient/health/reminder",
 			"style": { "navigationBarTitleText": "健康提醒" }
 		},
 		{
-			"path": "pages/profile/index",
+			"path": "pages/public/profile/index",
 			"style": {
 				"navigationBarTitleText": "个人中心"
 			}
 		},
 		{
-			"path": "pages/profile/infos/base-info",
+			"path": "pages/public/profile/infos/base-info",
 			"style": {
 				"navigationBarTitleText": "基本信息设定"
 			}
 		},
 		{
-			"path": "pages/profile/infos/patient-filing",
+			"path": "pages/public/profile/infos/patient-filing",
 			"style": {
 				"navigationBarTitleText": "建档信息设定"
 			}
@@ -83,15 +87,19 @@
 		"custom": true,
 		"list": [
 			{
-				"pagePath": "pages/index/index",
+				"pagePath": "pages/public/index/index",
 				"text": "慢病首页"
 			},
 			{
-				"pagePath": "pages/health/index",
+				"pagePath": "pages/public/health/index",
+				"text": "健康数据"
+			},
+			{
+				"pagePath": "pages/patient/health/index",
 				"text": "健康数据"
 			},
 			{
-				"pagePath": "pages/profile/index",
+				"pagePath": "pages/public/profile/index",
 				"text": "个人中心"
 			}
 		]

+ 0 - 0
src/pages/health/details/blood-glucose.vue → src/pages/patient/health/details/blood-glucose.vue


+ 0 - 0
src/pages/health/details/blood-pressure.vue → src/pages/patient/health/details/blood-pressure.vue


+ 0 - 0
src/pages/health/details/bmi.vue → src/pages/patient/health/details/bmi.vue


+ 0 - 0
src/pages/health/details/heart-rate.vue → src/pages/patient/health/details/heart-rate.vue


+ 0 - 0
src/pages/health/details/height.vue → src/pages/patient/health/details/height.vue


+ 0 - 0
src/pages/health/details/weight.vue → src/pages/patient/health/details/weight.vue


+ 2 - 2
src/pages/health/index.vue → src/pages/patient/health/index.vue

@@ -60,11 +60,11 @@ import TabBar from '@/components/tab-bar.vue'
 const title = ref('健康数据')
 
 const openDetail = (type: string) => {
-  uni.navigateTo({ url: `/pages/health/details/${type}` })
+  uni.navigateTo({ url: `patient/patient/health/details/${type}` })
 }
 
 const openReminder = () => {
-  uni.navigateTo({ url: '/pages/health/reminder' })
+  uni.navigateTo({ url: 'patient/patient/health/reminder' })
 }
 </script>
 

+ 1 - 1
src/pages/health/reminder.vue → src/pages/patient/health/reminder.vue

@@ -128,7 +128,7 @@ onMounted(() => {
       const prev = pages[pages.length - 2]
       const prevRoute = prev?.route || prev?.__route || prev?.$page?.route
       console.log('上一页路由:', prevRoute)
-      if (prevRoute && String(prevRoute).includes('pages/health/index')) {
+      if (prevRoute && String(prevRoute).includes('pages/public/health/index')) {
         entrySource.value = 'healthIndex'
         console.log('识别来源为健康首页')
       }

+ 70 - 0
src/pages/public/health/index.vue

@@ -0,0 +1,70 @@
+<template>
+	<CustomNav title="健康数据" leftType="back" />
+
+	<view class="page-container">
+		<view class="content">
+			<view v-if="!isLoggedIn" class="not-login">
+				<text class="tip">您需要登录才能体验健康数据管理功能。</text>
+				<button class="login-btn" @click="goLogin">去登录</button>
+			</view>
+
+			<!-- 已登录时不显示任何文字;重定向由脚本逻辑处理 -->
+		</view>
+	</view>
+	<TabBar />
+</template>
+
+<script setup lang="ts">
+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, roleToString, ensurePatientRedirect } from '@/composables/useAuth'
+
+const isLoggedIn = ref<boolean>(checkLogin())
+const roleDisplay = ref<string>('')
+
+function goLogin() {
+	uni.navigateTo({ url: '/pages/public/login/index' })
+}
+
+// TODO: 目前我们不向后端请求来确认角色(虽然更可靠),而是直接使用本地存储中的 role 字段。
+// 注:后续可改为 fetchUserInfo 接口以后端返回的角色为准,或实现 token 刷新/校验逻辑。
+onShow(() => {
+	try {
+		const logged = checkLogin()
+		isLoggedIn.value = logged
+		if (!logged) {
+			roleDisplay.value = ''
+			return
+		}
+
+		const r = getRole()
+		roleDisplay.value = roleToString(r)
+
+		// 仅当角色为患者(3)时,发起跳转
+		ensurePatientRedirect()
+	} catch (err) {
+		console.error('检查登录态时出错:', err)
+		isLoggedIn.value = false
+		roleDisplay.value = ''
+	}
+})
+</script>
+
+<style>
+.page-container {
+	min-height: 100vh;
+	padding-top: calc(var(--status-bar-height) + 44px);
+	box-sizing: border-box;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+}
+.content { width: 100%; max-width: 720rpx; padding: 40rpx }
+.not-login, .logged { background: #fff; padding: 40rpx; border-radius: 16rpx; text-align:center; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.08) }
+.tip { display:block; font-size:28rpx; color:#333; margin-bottom:20rpx }
+.hint { display:block; font-size:22rpx; color:#666; margin-bottom:20rpx }
+.login-btn { background: linear-gradient(135deg,#07C160 0%, #00A854 100%); color: #fff; padding: 18rpx 30rpx; border-radius: 12rpx; border:none; font-size:28rpx }
+</style>

+ 0 - 0
src/pages/index/index.vue → src/pages/public/index/index.vue


+ 1 - 1
src/pages/login/index.vue → src/pages/public/login/index.vue

@@ -72,7 +72,7 @@ async function onSelectRole(role: number) {
     uni.showToast({ title: '登录成功', icon: 'success' })
     // 跳转到个人信息页面
     setTimeout(() => {
-      uni.switchTab({ url: '/pages/profile/index' })
+      uni.switchTab({ url: '/pages/public/profile/index' })
     }, 1500)
   } catch (err) {
     console.error('Login error:', err)

+ 5 - 5
src/pages/profile/index.vue → src/pages/public/profile/index.vue

@@ -107,7 +107,7 @@ const fetchUserInfo = async () => {
       uni.removeStorageSync('token')
       uni.removeStorageSync('role')
       user.value = {}
-      uni.reLaunch({ url: '/pages/login/index' })
+      uni.reLaunch({ url: '/pages/public/login/index' })
       return
     }
     if (resp && resp.code === 200 && resp.data) {
@@ -116,7 +116,7 @@ const fetchUserInfo = async () => {
       uni.setStorageSync('user_info', resp.data)
       // 检查 nickname 和 avatar
       if (!resp.data.nickname || !resp.data.avatar) {
-        uni.navigateTo({ url: '/pages/profile/infos/base-info' })
+        uni.navigateTo({ url: '/pages/public/profile/infos/base-info' })
       }
     }
   } catch (err) {
@@ -130,7 +130,7 @@ onShow(() => {
   const token = uni.getStorageSync('token')
   if (!token) {
     // 使用 uni.reLaunch 替代 navigateTo,确保页面栈被清空
-    uni.reLaunch({ url: '/pages/login/index' })
+    uni.reLaunch({ url: '/pages/public/login/index' })
   } else {
     fetchUserInfo()
   }
@@ -140,12 +140,12 @@ const onMenuClick = (type: string) => {
   console.log('Menu clicked:', type)
   if (type === 'base-info') {
     // 跳转到修改个人信息页面
-    uni.navigateTo({ url: '/pages/profile/infos/base-info' })
+    uni.navigateTo({ url: '/pages/public/profile/infos/base-info' })
     return
   }
   if (type === 'patient-filing') {
     // 跳转到建档信息设定页面
-    uni.navigateTo({ url: '/pages/profile/infos/patient-filing' })
+    uni.navigateTo({ url: '/pages/public/profile/infos/patient-filing' })
     return
   }
   uni.showToast({

+ 1 - 1
src/pages/profile/infos/base-info.vue → src/pages/public/profile/infos/base-info.vue

@@ -109,7 +109,7 @@ const fetchUserInfo = async () => {
     if (response.statusCode === 401) {
       // 未授权,跳转登录
       uni.removeStorageSync('token')
-      uni.reLaunch({ url: '/pages/login/index' })
+      uni.reLaunch({ url: '/pages/public/login/index' })
       return
     }
     const resp = response.data as any

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