From ee54919d10dea640af38b8b3873710bca356d82a Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期五, 03 七月 2026 15:16:43 +0800
Subject: [PATCH] 部分优化

---
 traditionalApp/pages/courseDetail/service.js                                                                |    3 
 traditionalApp/pages/home/home.vue                                                                          |  158 ++++---
 traditionalApp/pages/mine/mine.vue                                                                          |   76 ++-
 traditionalApp/pages/liveDetail/liveDetail.vue                                                              |   38 +
 traditionalApp/pages/watchLive/watchLive.vue                                                                |  412 ++++++++++++++-----
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java |   11 
 traditionalApp/pages/courseDetail/courseDetail.vue                                                          |   65 ++
 traditionalApp/pages/articleDetail/service.js                                                               |    4 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListItemVO.java         |    1 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java    |    3 
 tcm-training-admin/src/pages/live/health/manage.tsx                                                         |    6 
 traditionalApp/pages/articleDetail/articleDetail.vue                                                        |   24 +
 traditionalApp/pages/home/service.js                                                                        |   14 
 tcm-training-admin/src/pages/live/health/detail.tsx                                                         |    2 
 traditionalApp/pages/protocol/protocol.vue                                                                  |  266 ++++++------
 traditionalApp/pages/liveDetail/service.js                                                                  |    5 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java        |    8 
 tcm-training-admin/src/pages/live/health/index.tsx                                                          |    7 
 traditionalApp/utils/request.js                                                                             |   44 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListRow.java            |    1 
 tcm-training-admin-backend/src/main/resources/application.yml                                               |    2 
 traditionalApp/pages/login/login.vue                                                                        |    7 
 traditionalApp/utils/push.js                                                                                |    4 
 .tmp_watchlive_frames/video.html                                                                            |    1 
 traditionalApp/pages/protocol/service.js                                                                    |    4 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppAuthProperties.java      |   31 +
 traditionalApp/utils/public-request.js                                                                      |   26 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthCourseSaveRequest.java       |    2 
 traditionalApp/manifest.json                                                                                |    6 
 29 files changed, 847 insertions(+), 384 deletions(-)

diff --git a/.tmp_watchlive_frames/video.html b/.tmp_watchlive_frames/video.html
new file mode 100644
index 0000000..db4e512
--- /dev/null
+++ b/.tmp_watchlive_frames/video.html
@@ -0,0 +1 @@
+<!doctype html><html><body style="margin:0;background:#111"><video id="v" src="file:///C:/Users/PC/Videos/%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B6/%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B6%202026-06-23%20201151.mp4" controls style="width:100vw;height:100vh;object-fit:contain"></video></body></html>
\ No newline at end of file
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppAuthProperties.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppAuthProperties.java
new file mode 100644
index 0000000..4f51122
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppAuthProperties.java
@@ -0,0 +1,31 @@
+package com.tcm.training.admin.app.auth.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * APP 登录认证配置。
+ */
+@Component
+@ConfigurationProperties(prefix = "app.auth")
+public class AppAuthProperties {
+
+    /**
+     * APP 登录 token 有效期,默认 30 天;设置为 -1 时表示永久有效。
+     */
+    private long tokenTimeoutSeconds = 30L * 24 * 60 * 60;
+
+    /**
+     * 获取 APP 登录 token 有效期秒数。
+     */
+    public long getTokenTimeoutSeconds() {
+        return tokenTimeoutSeconds;
+    }
+
+    /**
+     * 设置 APP 登录 token 有效期秒数。
+     */
+    public void setTokenTimeoutSeconds(long tokenTimeoutSeconds) {
+        this.tokenTimeoutSeconds = tokenTimeoutSeconds;
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
index e684517..3ba9c8b 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
@@ -1,5 +1,6 @@
 package com.tcm.training.admin.app.auth.service;
 
+import com.tcm.training.admin.app.auth.config.AppAuthProperties;
 import com.tcm.training.admin.app.auth.support.StpAppUserUtil;
 import cn.dev33.satoken.stp.SaTokenInfo;
 import org.springframework.stereotype.Service;
@@ -10,11 +11,17 @@
 @Service
 public class AppAuthSessionService {
 
+    private final AppAuthProperties appAuthProperties;
+
+    public AppAuthSessionService(AppAuthProperties appAuthProperties) {
+        this.appAuthProperties = appAuthProperties;
+    }
+
     /**
-     * 为指定 APP 用户建立永久登录态。
+     * 为指定 APP 用户建立登录态。
      */
     public void login(Long userId) {
-        StpAppUserUtil.login(userId);
+        StpAppUserUtil.login(userId, appAuthProperties.getTokenTimeoutSeconds());
     }
 
     /**
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
index 44adc37..735881d 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
@@ -10,16 +10,14 @@
 public final class StpAppUserUtil {
 
     private static final StpLogic STP_LOGIC = new StpLogic("app-user");
-    private static final long NEVER_EXPIRE_TIMEOUT = -1L;
-
     private StpAppUserUtil() {
     }
 
     /**
-     * 为 APP 用户建立永久登录态。
+     * 为 APP 用户建立指定有效期的登录态。
      */
-    public static void login(Object loginId) {
-        STP_LOGIC.login(loginId, new SaLoginModel().setTimeout(NEVER_EXPIRE_TIMEOUT));
+    public static void login(Object loginId, long timeoutSeconds) {
+        STP_LOGIC.login(loginId, new SaLoginModel().setTimeout(timeoutSeconds));
     }
 
     /**
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthCourseSaveRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthCourseSaveRequest.java
index b454b7f..33d83dd 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthCourseSaveRequest.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthCourseSaveRequest.java
@@ -58,4 +58,6 @@
 
     @NotNull(message = "请选择是否首页推荐")
     private Integer isHomeRecommend;
+
+    private Integer sort;
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java
index 743ff41..34eaa0e 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java
@@ -120,7 +120,6 @@
         course.setFavoriteCount(0);
         course.setTotalDurationSeconds(0);
         course.setVideoCount(0);
-        course.setSort(0);
         course.setDeleted(0);
         healthCourseMapper.insert(course);
         return detail(course.getId());
@@ -421,6 +420,7 @@
         course.setBaseSalesCount(req.getBaseSalesCount() == null ? 0 : req.getBaseSalesCount());
         course.setStatus(req.getStatus());
         course.setIsHomeRecommend(req.getIsHomeRecommend());
+        course.setSort(defaultNumber(req.getSort()));
     }
 
     private void fillVideo(HealthCourseVideo video, HealthCourseVideoSaveRequest req) {
@@ -596,6 +596,7 @@
         vo.setCoverImg(row.getCoverImg());
         vo.setStatus(row.getStatus());
         vo.setIsHomeRecommend(row.getIsHomeRecommend());
+        vo.setSort(defaultNumber(row.getSort()));
         vo.setCreatedAt(row.getCreatedAt());
         return vo;
     }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListItemVO.java
index c039624..134f5e7 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListItemVO.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListItemVO.java
@@ -24,5 +24,6 @@
     private String coverImg;
     private Integer status;
     private Integer isHomeRecommend;
+    private Integer sort;
     private LocalDateTime createdAt;
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListRow.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListRow.java
index 4c583c0..51035c7 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListRow.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthCourseListRow.java
@@ -27,5 +27,6 @@
     private String coverImg;
     private Integer status;
     private Integer isHomeRecommend;
+    private Integer sort;
     private LocalDateTime createdAt;
 }
diff --git a/tcm-training-admin-backend/src/main/resources/application.yml b/tcm-training-admin-backend/src/main/resources/application.yml
index 1fbb6f2..1cf905c 100644
--- a/tcm-training-admin-backend/src/main/resources/application.yml
+++ b/tcm-training-admin-backend/src/main/resources/application.yml
@@ -79,6 +79,8 @@
     # 活动到期积分结算扫描间隔(毫秒)
     settlement-interval-ms: 60000
   auth:
+    # APP 登录 token 有效期,默认 30 天;如需永久有效可改为 -1
+    token-timeout-seconds: 2592000
     sms:
       # 短信验证码统一有效期 300 秒,即 5 分钟
       expire-seconds: 300
diff --git a/tcm-training-admin/src/pages/live/health/detail.tsx b/tcm-training-admin/src/pages/live/health/detail.tsx
index 9c7049f..574b749 100644
--- a/tcm-training-admin/src/pages/live/health/detail.tsx
+++ b/tcm-training-admin/src/pages/live/health/detail.tsx
@@ -30,6 +30,7 @@
   favoriteCount?: number;
   totalDurationSeconds?: number;
   status: number;
+  sort?: number;
   expert?: {
     id: number;
     name: string;
@@ -273,6 +274,7 @@
               <div>课程介绍:{course?.summary || '-'}</div>
               <div>适用人群:{course?.targetUsers || '-'}</div>
               <div>状态:{course?.status === 1 ? '启用' : '禁用'}</div>
+              <div>排序:{course?.sort ?? 0}</div>
               <div>基础购买量:{course?.baseSalesCount || 0}</div>
               <div>销量:{course?.salesCount || 0}</div>
               <div>收藏量:{course?.favoriteCount || 0}</div>
diff --git a/tcm-training-admin/src/pages/live/health/index.tsx b/tcm-training-admin/src/pages/live/health/index.tsx
index 173cb56..5f47466 100644
--- a/tcm-training-admin/src/pages/live/health/index.tsx
+++ b/tcm-training-admin/src/pages/live/health/index.tsx
@@ -36,6 +36,7 @@
   favoriteCount?: number;
   coverImg?: string;
   status: number;
+  sort?: number;
   expert?: {
     id: number;
     name: string;
@@ -210,6 +211,12 @@
       render: (value) => value?.name || '-',
     },
     {
+      title: '排序',
+      dataIndex: 'sort',
+      width: 90,
+      render: (value) => value ?? 0,
+    },
+    {
       title: '状态',
       dataIndex: 'status',
       width: 90,
diff --git a/tcm-training-admin/src/pages/live/health/manage.tsx b/tcm-training-admin/src/pages/live/health/manage.tsx
index 496c7c8..a6aeb9f 100644
--- a/tcm-training-admin/src/pages/live/health/manage.tsx
+++ b/tcm-training-admin/src/pages/live/health/manage.tsx
@@ -74,6 +74,7 @@
             isHomeRecommend: false,
             salePrice: 0,
             baseSalesCount: 0,
+            sort: 0,
             summary: '',
             targetUsers: '',
             bannerImgs: [],
@@ -96,6 +97,7 @@
           coverImg: course.coverImg,
           bannerImgs: course.bannerImgs || [],
           baseSalesCount: course.baseSalesCount,
+          sort: course.sort ?? 0,
           status: course.status === 1,
           isHomeRecommend: course.isHomeRecommend === 1,
           content: course.content,
@@ -117,6 +119,7 @@
       coverImg,
       bannerImgs,
       content: values.content,
+      sort: Number(values.sort || 0),
       status: values.status ? 1 : 0,
       isHomeRecommend: values.isHomeRecommend ? 1 : 0,
     };
@@ -171,6 +174,9 @@
                 disabled={loading}
               />
             </Form.Item>
+            <Form.Item label="排序" name="sort">
+              <InputNumber min={0} precision={0} placeholder="数值越小越靠前" className="health-course-edit-form__number" />
+            </Form.Item>
             <div className="health-course-edit-form__switch-group">
               <div className="health-course-edit-form__switch-item">
                 <span className="health-course-edit-form__switch-label">是否启用</span>
diff --git a/traditionalApp/manifest.json b/traditionalApp/manifest.json
index b3efb10..db08613 100644
--- a/traditionalApp/manifest.json
+++ b/traditionalApp/manifest.json
@@ -1,10 +1,10 @@
 {
     "sassImplementationName" : "node-sass",
     "name" : "海诊康道",
-    "appid" : "__UNI__1A90541",
+    "appid" : "__UNI__B2F54BE",
     "description" : "",
-    "versionName" : "1.0.14",
-    "versionCode" : 114,
+    "versionName" : "1.0.18",
+    "versionCode" : 118,
     "transformPx" : false,
     /* 5+App特有相关 */
     "app-plus" : {
diff --git a/traditionalApp/pages/articleDetail/articleDetail.vue b/traditionalApp/pages/articleDetail/articleDetail.vue
index 8404f2b..8e9860e 100644
--- a/traditionalApp/pages/articleDetail/articleDetail.vue
+++ b/traditionalApp/pages/articleDetail/articleDetail.vue
@@ -85,15 +85,35 @@
 				statusBarHeight.value = res.statusBarHeight || 0
 			}
 		})
+		setTimeout(() => {
+			const pageOptions = getCurrentPageOptions()
+			if (!popularizationId.value) {
+				popularizationId.value = normalizeRouteParam(pageOptions?.id)
+			}
+			loadDetail()
+		}, 0)
 	})
 
 	/**
 	 * 页面加载时读取科普主键并拉取详情接口。
 	 */
 	onLoad((options) => {
-		popularizationId.value = typeof options?.id === 'string' ? options.id : ''
+		popularizationId.value = normalizeRouteParam(options?.id)
 		loadDetail()
 	})
+
+	function normalizeRouteParam(value : unknown) {
+		if (value === undefined || value === null) {
+			return ''
+		}
+		return String(value).trim()
+	}
+
+	function getCurrentPageOptions() {
+		const pages = getCurrentPages()
+		const currentPage = pages[pages.length - 1] as { options ?: Record<string, unknown>; $page ?: { options ?: Record<string, unknown> } } | undefined
+		return currentPage?.options || currentPage?.$page?.options || {}
+	}
 
 	/**
 	 * 拉取健康科普详情接口并更新页面状态。
@@ -245,4 +265,4 @@
 		line-height: 40rpx;
 		color: #86909C;
 	}
-</style>
\ No newline at end of file
+</style>
diff --git a/traditionalApp/pages/articleDetail/service.js b/traditionalApp/pages/articleDetail/service.js
index 83f8d91..ad46ff2 100644
--- a/traditionalApp/pages/articleDetail/service.js
+++ b/traditionalApp/pages/articleDetail/service.js
@@ -1,8 +1,8 @@
-import request from '@/utils/request.js'
+import { publicGet } from '@/utils/public-request.js'
 
 /**
  * 获取 APP 健康科普详情。
  */
 export const getHealthPopularizationDetail = (popularizationId) => {
-	return request.get(`/api/app/health-popularizations/${popularizationId}`, {}, false, false)
+	return publicGet(`/api/app/health-popularizations/${popularizationId}`)
 }
diff --git a/traditionalApp/pages/courseDetail/courseDetail.vue b/traditionalApp/pages/courseDetail/courseDetail.vue
index 892c44c..7a2ec50 100644
--- a/traditionalApp/pages/courseDetail/courseDetail.vue
+++ b/traditionalApp/pages/courseDetail/courseDetail.vue
@@ -31,7 +31,7 @@
 					</swiper-item>
 				</swiper>
 				<view v-if="!isReadonlyDisplayMode && allVideos.length > 0" class="poster-play-btn"
-					@tap.stop="playLatestUploadedVideoFromPoster">
+					@tap.stop="playFirstCourseVideoFromPoster">
 					<text class="poster-play-icon">▶</text>
 				</view>
 				<!-- 图片指示器 -->
@@ -307,7 +307,7 @@
 </template>
 
 <script setup lang="ts">
-	import { computed, getCurrentInstance, nextTick, ref } from 'vue'
+	import { computed, getCurrentInstance, nextTick, onMounted, ref } from 'vue'
 	import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
 	import config from '@/config/index.js'
 	import {
@@ -417,6 +417,7 @@
 	const activePurchaseVideoId = ref<number | null>(null)
 	const courseId = ref('')
 	const shouldPlayLatestVideoOnLoad = ref(false)
+	const shouldPlayFirstVideoOnLoad = ref(false)
 	const courseDetail = ref<CourseDetail>(createDefaultCourseDetail())
 	const activeVideoId = ref<number>(0)
 	const showVideoPlayer = ref(false)
@@ -541,14 +542,33 @@
 	onLoad((options) => {
 		courseId.value = resolveCourseId(options)
 		shouldPlayLatestVideoOnLoad.value = resolveBooleanRouteParam(options, 'playLatest')
+		shouldPlayFirstVideoOnLoad.value = resolveBooleanRouteParam(options, 'playFirst')
 		loadCourseDetail()
 	})
+
+	onMounted(() => {
+		setTimeout(() => {
+			const pageOptions = getCurrentPageOptions()
+			if (!courseId.value) {
+				courseId.value = resolveCourseId(pageOptions)
+				shouldPlayLatestVideoOnLoad.value = resolveBooleanRouteParam(pageOptions, 'playLatest')
+				shouldPlayFirstVideoOnLoad.value = resolveBooleanRouteParam(pageOptions, 'playFirst')
+			}
+			loadCourseDetail()
+		}, 0)
+	})
+
+	function getCurrentPageOptions() {
+		const pages = getCurrentPages()
+		const currentPage = pages[pages.length - 1] as { options ?: Record<string, unknown>; $page ?: { options ?: Record<string, unknown> } } | undefined
+		return currentPage?.options || currentPage?.$page?.options || {}
+	}
 
 	/**
 	 * 统一解析课程 id,兼容 App 路由参数和 H5 分享页地址参数。
 	 */
 	function resolveCourseId(options ?: Record<string, unknown>) {
-		const routeCourseId = typeof options?.id === 'string' ? options.id.trim() : ''
+		const routeCourseId = normalizeOptionalText(options?.id)
 		if (routeCourseId) {
 			return routeCourseId
 		}
@@ -666,6 +686,7 @@
 				currentSwiperIndex.value = 0
 			}
 			syncActiveVideoAfterDetailLoad()
+			handlePlayFirstVideoAfterDetailLoad()
 			handlePlayLatestVideoAfterDetailLoad()
 		} catch {
 			showLoadFailedToast()
@@ -731,10 +752,21 @@
 	}
 
 	/**
-	 * 课程介绍封面播放按钮:播放最新上传章节。
+	 * 首页精选课程播放按钮要求直接播放第一个章节,详情接口返回后再执行。
 	 */
-	function playLatestUploadedVideoFromPoster() {
-		playLatestUploadedVideo()
+	function handlePlayFirstVideoAfterDetailLoad() {
+		if (!shouldPlayFirstVideoOnLoad.value) {
+			return
+		}
+		shouldPlayFirstVideoOnLoad.value = false
+		playFirstCourseVideo()
+	}
+
+	/**
+	 * 课程介绍封面播放按钮:播放第一个章节。
+	 */
+	function playFirstCourseVideoFromPoster() {
+		playFirstCourseVideo()
 	}
 
 	function playLatestUploadedVideo() {
@@ -754,6 +786,27 @@
 		handlePaidVideoTap(latestVideo)
 	}
 
+	function playFirstCourseVideo() {
+		const firstVideo = resolveFirstCourseVideo(allVideos.value)
+		if (!firstVideo) {
+			uni.showToast({
+				title: '暂无可播放课程视频',
+				icon: 'none',
+			})
+			return
+		}
+		currentTab.value = 'list'
+		if (canPlayVideo(firstVideo)) {
+			playSelectedVideo(firstVideo)
+			return
+		}
+		handlePaidVideoTap(firstVideo)
+	}
+
+	function resolveFirstCourseVideo(videos : CourseVideo[]) {
+		return videos.find((item) => !!item?.videoUrl) || null
+	}
+
 	/**
 	 * 优先按上传时间取最新章节;旧接口无时间字段时按自增 id 兜底。
 	 */
diff --git a/traditionalApp/pages/courseDetail/service.js b/traditionalApp/pages/courseDetail/service.js
index 598318e..45f13df 100644
--- a/traditionalApp/pages/courseDetail/service.js
+++ b/traditionalApp/pages/courseDetail/service.js
@@ -1,10 +1,11 @@
 import request from '@/utils/request.js'
+import { publicGet } from '@/utils/public-request.js'
 
 /**
  * 获取 APP 养生课程详情。
  */
 export const getHealthCourseDetail = (courseId) => {
-	return request.get(`/api/app/health-courses/${courseId}`, {}, false, false)
+	return publicGet(`/api/app/health-courses/${courseId}`)
 }
 
 /**
diff --git a/traditionalApp/pages/home/home.vue b/traditionalApp/pages/home/home.vue
index 8a95ea9..bd7df53 100644
--- a/traditionalApp/pages/home/home.vue
+++ b/traditionalApp/pages/home/home.vue
@@ -4,16 +4,10 @@
 			<image src="/static/image/home/bg.png" mode="widthFix" class="bg-img"></image>
 		</view>
 
-		<!-- Custom Navigation Bar -->
-		<view class="custom-navbar" :style="{ backgroundColor: navbarBackgroundColor }">
-			<view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
-			<view class="nav-content">
-				<view class="nav-title">海诊康道</view>
-			</view>
+		<view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
+		<view class="home-nav-bar">
+			<view class="nav-title">海诊康道</view>
 		</view>
-
-		<!-- Spacer to push the content down below the fixed custom-navbar -->
-		<view class="custom-navbar-spacer" :style="{ height: navbarHeightPx + 'px' }"></view>
 
 		<view class="content">
 			<!-- Search Bar -->
@@ -36,9 +30,13 @@
 				</view>
 			</view>
 
-			<view class="AI" style="width: 702rpx;height: 160rpx;margin: 0 auto;margin-top: 26rpx;"
+			<view class="health-test-entry" hover-class="health-test-entry--pressed" hover-stay-time="80"
 				@tap="goMiniProgram">
-				<image src="/static/image/home/AI.png" mode="aspectFill" class="w100 h100"></image>
+				<image src="/static/image/home/AI.png" mode="aspectFill" class="health-test-entry__bg"></image>
+				<view class="health-test-entry__cta">
+					<text>立即测一测</text>
+					<text class="health-test-entry__arrow">›</text>
+				</view>
 			</view>
 
 
@@ -81,7 +79,7 @@
 						<view class="course-cover-wrap">
 							<image :src="item.coverImage || defaultCourseCover" mode="aspectFill" class="course-img">
 							</image>
-							<view class="course-play-btn" @tap.stop="playLatestCourseVideo(item.id)">
+							<view class="course-play-btn" @tap.stop="playFirstCourseVideo(item.id)">
 								<text class="course-play-icon">▶</text>
 							</view>
 						</view>
@@ -116,8 +114,8 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted, computed } from "vue";
-	import { onPageScroll, onPullDownRefresh, onShow } from "@dcloudio/uni-app";
+	import { ref, onMounted } from "vue";
+	import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
 	import { getBannerList, getHomeFeaturedContent, getHomeLiveCourseList } from "./service";
 
 	interface HomeBannerItem {
@@ -164,7 +162,6 @@
 	}
 
 	const statusBarHeight = ref(0);
-	const scrollY = ref(0);
 	const bannerList = ref<HomeBannerItem[]>([]);
 	const bannerActiveIndex = ref(0);
 	const liveCourseList = ref<HomeLiveCourseItem[]>([]);
@@ -184,6 +181,9 @@
 				statusBarHeight.value = res.statusBarHeight || 0;
 			},
 		});
+		setTimeout(() => {
+			refreshHomeData();
+		}, 0);
 	});
 
 	/**
@@ -202,33 +202,6 @@
 		} finally {
 			uni.stopPullDownRefresh();
 		}
-	});
-
-	/**
-	 * 监听页面滚动以驱动导航栏渐变。
-	 */
-	onPageScroll((e) => {
-		scrollY.value = e.scrollTop;
-	});
-
-	/**
-	 * 计算自定义导航栏占位高度。
-	 */
-	const navbarHeightPx = computed(() => {
-		const navContentHeightRpx = 88;
-		const screenWidthPx = uni.getSystemInfoSync().screenWidth;
-		const navContentHeightPx = (navContentHeightRpx / 750) * screenWidthPx;
-		return statusBarHeight.value + navContentHeightPx;
-	});
-
-	/**
-	 * 根据滚动距离切换导航栏背景色。
-	 */
-	const navbarBackgroundColor = computed(() => {
-		const thresholdRpx = 50;
-		const screenWidthPx = uni.getSystemInfoSync().screenWidth;
-		const thresholdPx = (thresholdRpx / 750) * screenWidthPx;
-		return scrollY.value > thresholdPx ? 'white' : 'transparent';
 	});
 
 	/**
@@ -304,7 +277,8 @@
 				liveCourseList.value = [];
 				return;
 			}
-			liveCourseList.value = res.data.filter((item : HomeLiveCourseItem) => !!item?.courseId);
+			const nextList = res.data.filter((item : HomeLiveCourseItem) => !!item?.courseId);
+			liveCourseList.value = nextList;
 		} catch {
 			liveCourseList.value = [];
 		}
@@ -322,12 +296,14 @@
 				return;
 			}
 			const responseData = res.data as HomeFeaturedContentResponse;
-			featuredHealthCourseList.value = Array.isArray(responseData.healthCourses)
+			const nextCourses = Array.isArray(responseData.healthCourses)
 				? responseData.healthCourses.filter((item : HomeFeaturedHealthCourseItem) => !!item?.id)
 				: [];
-			featuredPopularizationList.value = Array.isArray(responseData.healthPopularizations)
+			const nextPopularizations = Array.isArray(responseData.healthPopularizations)
 				? responseData.healthPopularizations.filter((item : HomeFeaturedPopularizationItem) => !!item?.id)
 				: [];
+			featuredHealthCourseList.value = nextCourses;
+			featuredPopularizationList.value = nextPopularizations;
 		} catch {
 			featuredHealthCourseList.value = [];
 			featuredPopularizationList.value = [];
@@ -343,7 +319,7 @@
 		}
 		refreshing.value = true;
 		try {
-			await Promise.all([
+			await Promise.allSettled([
 				loadBannerList(),
 				loadHomeLiveCourseList(),
 				loadFeaturedContent()
@@ -470,8 +446,9 @@
 	 * 跳转直播详情页。
 	 */
 	const goLiveDetail = (courseId ?: number | string | null) => {
+		const url = courseId ? `/pages/liveDetail/liveDetail?id=${courseId}` : '/pages/liveDetail/liveDetail';
 		uni.navigateTo({
-			url: courseId ? `/pages/liveDetail/liveDetail?id=${courseId}` : '/pages/liveDetail/liveDetail'
+			url
 		});
 	};
 
@@ -479,20 +456,21 @@
 	 * 跳转养生课程详情页。
 	 */
 	const goCourseDetail = (courseId ?: number | string | null) => {
+		const url = courseId ? `/pages/courseDetail/courseDetail?id=${courseId}` : '/pages/courseDetail/courseDetail';
 		uni.navigateTo({
-			url: courseId ? `/pages/courseDetail/courseDetail?id=${courseId}` : '/pages/courseDetail/courseDetail'
+			url
 		});
 	};
 
 	/**
-	 * 从首页精选课程直接进入详情并播放最新上传章节。
+	 * 从首页精选课程直接进入详情并播放第一个章节。
 	 */
-	const playLatestCourseVideo = (courseId ?: number | string | null) => {
+	const playFirstCourseVideo = (courseId ?: number | string | null) => {
 		if (!courseId) {
 			return;
 		}
 		uni.navigateTo({
-			url: `/pages/courseDetail/courseDetail?id=${courseId}&playLatest=1`
+			url: `/pages/courseDetail/courseDetail?id=${courseId}&playFirst=1`
 		});
 	};
 
@@ -500,10 +478,11 @@
 	 * 跳转健康科普详情页。
 	 */
 	const goPopularizationDetail = (popularizationId ?: number | string | null) => {
+		const url = popularizationId
+			? `/pages/articleDetail/articleDetail?id=${popularizationId}`
+			: '/pages/articleDetail/articleDetail';
 		uni.navigateTo({
-			url: popularizationId
-				? `/pages/articleDetail/articleDetail?id=${popularizationId}`
-				: '/pages/articleDetail/articleDetail'
+			url
 		});
 	};
 
@@ -550,33 +529,24 @@
 		}
 	}
 
-	.custom-navbar {
-		position: fixed;
-		top: 0;
-		left: 0;
-		width: 100%;
-		z-index: 100;
-		transition: background-color 0.3s ease-in-out;
-	}
-
 	.status-bar-placeholder {
 		width: 100%;
-		/* height is set dynamically via style binding */
+		position: relative;
+		z-index: 10;
 	}
 
-	.nav-content {
+	.home-nav-bar {
 		display: flex;
 		align-items: center;
 		justify-content: center;
 		height: 88rpx;
-		/* Assuming 88rpx for the nav content part */
 		width: 100%;
-		background-color: transparent;
-		/* Or specific color if needed */
+		position: relative;
+		z-index: 10;
 
 		.nav-title {
 			font-size: 36rpx;
-			font-weight: bold;
+			font-weight: 600;
 			color: #333;
 		}
 	}
@@ -588,8 +558,6 @@
 		z-index: 10;
 		// padding: 0 24rpx;
 		padding-top: 24rpx;
-		margin-top: -30rpx;
-		/* Re-added for overlap with top-bg */
 	}
 
 	/* Search Box */
@@ -668,6 +636,54 @@
 		}
 	}
 
+	.health-test-entry {
+		position: relative;
+		width: 702rpx;
+		height: 160rpx;
+		margin: 26rpx auto 0;
+		border-radius: 20rpx;
+		overflow: hidden;
+		background: #fff7de;
+		border: 2rpx solid rgba(214, 151, 57, 0.22);
+		box-shadow: 0 14rpx 28rpx rgba(156, 103, 31, 0.16);
+		transform: translateZ(0);
+		transition: transform 0.12s ease, box-shadow 0.12s ease;
+	}
+
+	.health-test-entry--pressed {
+		transform: scale(0.985);
+		box-shadow: 0 8rpx 18rpx rgba(156, 103, 31, 0.12);
+	}
+
+	.health-test-entry__bg {
+		width: 702rpx;
+		height: 160rpx;
+		display: block;
+	}
+
+	.health-test-entry__cta {
+		position: absolute;
+		right: 24rpx;
+		bottom: 22rpx;
+		height: 48rpx;
+		padding: 0 18rpx 0 22rpx;
+		border-radius: 999rpx;
+		background: linear-gradient(90deg, #D94436 0%, #BA241F 100%);
+		color: #fff;
+		font-size: 24rpx;
+		font-weight: 600;
+		display: flex;
+		align-items: center;
+		box-shadow: 0 8rpx 18rpx rgba(198, 53, 32, 0.28);
+	}
+
+	.health-test-entry__arrow {
+		margin-left: 8rpx;
+		font-size: 36rpx;
+		line-height: 1;
+		transform: translateY(-1rpx);
+	}
+
 	/* Sections */
 	.section {
 
diff --git a/traditionalApp/pages/home/service.js b/traditionalApp/pages/home/service.js
index 482e11d..bd4d223 100644
--- a/traditionalApp/pages/home/service.js
+++ b/traditionalApp/pages/home/service.js
@@ -1,24 +1,32 @@
 import request from '@/utils/request.js'
+import { publicGet as rawPublicGet } from '@/utils/public-request.js'
+
+/**
+ * 首页是游客可见模块,公共接口不要经过登录态刷新、请求队列或鉴权拦截逻辑。
+ */
+function publicGet(url, data = {}) {
+	return rawPublicGet(url, data)
+}
 
 /**
  * 获取 APP 首页顶部 Banner 列表。
  */
 export const getBannerList = () => {
-	return request.get('/api/app/home/banners', {}, false, false)
+	return publicGet('/api/app/home/banners')
 }
 
 /**
  * 获取 APP 首页养生直播模块列表。
  */
 export const getHomeLiveCourseList = () => {
-	return request.get('/api/app/home/live-courses', {}, false, false)
+	return publicGet('/api/app/home/live-courses')
 }
 
 /**
  * 获取 APP 首页精选课程介绍模块数据。
  */
 export const getHomeFeaturedContent = () => {
-	return request.get('/api/app/home/featured-content', {}, false, false)
+	return publicGet('/api/app/home/featured-content')
 }
 
 /**
diff --git a/traditionalApp/pages/liveDetail/liveDetail.vue b/traditionalApp/pages/liveDetail/liveDetail.vue
index 6970a62..4bb2ce6 100644
--- a/traditionalApp/pages/liveDetail/liveDetail.vue
+++ b/traditionalApp/pages/liveDetail/liveDetail.vue
@@ -26,9 +26,6 @@
 				<image v-if="isWatchable" class="play-btn"
 					src="http://obs.haizhentong.com/file/play-bc5effa8a858450a8851f4304a083fb0.png" mode="aspectFit"
 					@tap="handlePrimaryAction"></image>
-				<image v-if="isWatchable" class="fullscreen-icon"
-					src="http://obs.haizhentong.com/file/screen-d29662792d3f4890b5f094333feca7ca.png" mode="aspectFit"
-					@tap="goToWatchLive"></image>
 			</view>
 		</view>
 
@@ -101,7 +98,7 @@
 </template>
 
 <script setup lang="ts">
-	import { computed, ref } from 'vue'
+	import { computed, onMounted, ref } from 'vue'
 	import { onLoad, onShow } from '@dcloudio/uni-app'
 	import { getLiveCourseDetail, reserveLiveCourseSession } from './service'
 	import config from '@/config/index.js'
@@ -294,13 +291,30 @@
 	 */
 	onLoad((options) => {
 		courseId.value = resolveCourseId(options)
+		loadLiveDetail()
 	})
+
+	onMounted(() => {
+		setTimeout(() => {
+			const pageOptions = getCurrentPageOptions()
+			if (!courseId.value) {
+				courseId.value = resolveCourseId(pageOptions)
+			}
+			loadLiveDetail()
+		}, 0)
+	})
+
+	function getCurrentPageOptions() {
+		const pages = getCurrentPages()
+		const currentPage = pages[pages.length - 1] as { options ?: Record<string, unknown>; $page ?: { options ?: Record<string, unknown> } } | undefined
+		return currentPage?.options || currentPage?.$page?.options || {}
+	}
 
 	/**
 	 * 统一解析直播课程 id,兼容 App 路由参数和 H5 分享地址参数。
 	 */
 	function resolveCourseId(options ?: Record<string, unknown>) {
-		const routeCourseId = typeof options?.id === 'string' ? options.id.trim() : ''
+		const routeCourseId = normalizeRouteParam(options?.id)
 		if (routeCourseId) {
 			return routeCourseId
 		}
@@ -317,6 +331,13 @@
 		}
 		// #endif
 		return ''
+	}
+
+	function normalizeRouteParam(value : unknown) {
+		if (value === undefined || value === null) {
+			return ''
+		}
+		return String(value).trim()
 	}
 
 	/**
@@ -658,13 +679,6 @@
 			height: 140rpx;
 		}
 
-		.fullscreen-icon {
-			position: absolute;
-			right: 24rpx;
-			bottom: 24rpx;
-			width: 48rpx;
-			height: 48rpx;
-		}
 	}
 
 	.scroll-content {
diff --git a/traditionalApp/pages/liveDetail/service.js b/traditionalApp/pages/liveDetail/service.js
index a8c89c8..fc891fc 100644
--- a/traditionalApp/pages/liveDetail/service.js
+++ b/traditionalApp/pages/liveDetail/service.js
@@ -1,10 +1,11 @@
 import request from '@/utils/request.js'
+import { publicGet } from '@/utils/public-request.js'
 
 /**
  * 获取 APP 养生直播详情。
  */
 export const getLiveCourseDetail = (courseId) => {
-	return request.get(`/api/app/live-courses/${courseId}`, {}, false, false)
+	return publicGet(`/api/app/live-courses/${courseId}`)
 }
 
 /**
@@ -18,5 +19,5 @@
  * 获取 APP 养生直播专家详情。
  */
 export const getLiveExpertDetail = (expertId) => {
-	return request.get(`/api/app/live-experts/${expertId}`, {}, false, false)
+	return publicGet(`/api/app/live-experts/${expertId}`)
 }
diff --git a/traditionalApp/pages/login/login.vue b/traditionalApp/pages/login/login.vue
index ad33f09..67c2a4d 100644
--- a/traditionalApp/pages/login/login.vue
+++ b/traditionalApp/pages/login/login.vue
@@ -229,7 +229,7 @@
 				})
 				return
 			}
-			saveLoginSession(res.data)
+			saveLoginSession(res.data, normalizedPhone)
 			uni.showToast({
 				title: '登录成功',
 				icon: 'success'
@@ -608,11 +608,14 @@
 	/**
 	 * 将登录响应写入本地缓存,供后续请求与页面读取。
 	 */
-	const saveLoginSession = (loginData : AppLoginPayload) => {
+	const saveLoginSession = (loginData : AppLoginPayload, fallbackPhone = '') => {
 		if (!loginData) {
 			return
 		}
 		saveAuthSession(loginData, {
+			userOverrides: {
+				phone: normalizeOptionalText(loginData.user?.phone) || fallbackPhone
+			},
 			redirectMeta: {
 				inviteUserId: inviteUserId.value,
 				inviteType: inviteType.value
diff --git a/traditionalApp/pages/mine/mine.vue b/traditionalApp/pages/mine/mine.vue
index 46260e7..0be6d69 100644
--- a/traditionalApp/pages/mine/mine.vue
+++ b/traditionalApp/pages/mine/mine.vue
@@ -36,7 +36,7 @@
 			</view>
 
 			<view class="list-section card-box">
-				<view class="list-item" v-for="(item, index) in bottomMenuList" :key="`${item.title}-${index}`" @tap="goPage(item.url)">
+				<view class="list-item" v-for="(item, index) in bottomMenuList" :key="`${item.title}-${index}`" @tap="goPage(item)">
 					<view class="list-left">
 						<image class="list-icon" :class="item.iconClass" :src="item.icon" mode="aspectFit"></image>
 						<text class="list-title">{{ item.title }}</text>
@@ -56,6 +56,8 @@
 import { getNoticeList } from '../notice/service'
 import { getUserCenter } from './service'
 import { getRuntimeVersionName } from '@/utils/app-version.js'
+import { hasStoredSession } from '@/utils/auth-session.js'
+import manifestConfig from '../../manifest.json'
 
 type UserRole = 'personal' | 'doctor'
 
@@ -88,6 +90,7 @@
 	icon: string
 	url: string
 	iconClass?: string
+	requiresLogin?: boolean
 }
 
 interface NoticeSummaryItem {
@@ -96,13 +99,14 @@
 
 const statusBarHeight = ref(0)
 const unreadNoticeTotal = ref(0)
-const appVersionText = ref('1.0.14')
+const manifestVersionName = String(manifestConfig.versionName || '')
+const appVersionText = ref(manifestVersionName)
 const userCenter = ref<UserCenterData>(createDefaultUserCenter())
 const userRole = computed<UserRole>(() => {
 	return Number(userCenter.value.type) === 1 ? 'doctor' : 'personal'
 })
 const displayName = computed(() => {
-	return normalizeOptionalText(userCenter.value.nickname) || normalizeOptionalText(userCenter.value.phone) || '游客'
+	return normalizeOptionalText(userCenter.value.nickname) || normalizeOptionalText(userCenter.value.phone) || (hasStoredSession() ? '已登录用户' : '游客')
 })
 const displayAvatar = computed(() => {
 	return normalizeOptionalText(userCenter.value.avatar) || '/static/image/mine/header.png'
@@ -117,26 +121,26 @@
 const gridMenuList = computed<MenuItem[]>(() => {
 	if (userRole.value === 'doctor') {
 		return [
-			{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord' },
-			{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites' },
-			{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads' },
-			{ title: '我的患者', icon: '/static/image/mine/person.png', url: '/pages/myPatients/myPatients' },
-			{ title: '诊所信息', icon: '/static/image/mine/hosptial.png', url: '/pages/clinicInfo/clinicInfo' },
-			{ title: '二维码', icon: '/static/image/mine/code.png', url: '/pages/inviteUser/inviteUser' },
+			{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord', requiresLogin: true },
+			{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites', requiresLogin: true },
+			{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads', requiresLogin: true },
+			{ title: '我的患者', icon: '/static/image/mine/person.png', url: '/pages/myPatients/myPatients', requiresLogin: true },
+			{ title: '诊所信息', icon: '/static/image/mine/hosptial.png', url: '/pages/clinicInfo/clinicInfo', requiresLogin: true },
+			{ title: '二维码', icon: '/static/image/mine/code.png', url: '/pages/inviteUser/inviteUser', requiresLogin: true },
 		]
 	}
 	return [
-		{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord' },
-		{ title: '兑换订单', icon: '/static/image/mine/Shopping@2x.png', url: '/pages/exchangeRecord/exchangeRecord' },
-		{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites' },
-		{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads' },
-		{ title: '我的积分', icon: '/static/image/mine/point.png', url: '/pages/point/point' },
+		{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord', requiresLogin: true },
+		{ title: '兑换订单', icon: '/static/image/mine/Shopping@2x.png', url: '/pages/exchangeRecord/exchangeRecord', requiresLogin: true },
+		{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites', requiresLogin: true },
+		{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads', requiresLogin: true },
+		{ title: '我的积分', icon: '/static/image/mine/point.png', url: '/pages/point/point', requiresLogin: true },
 	]
 })
 
 const bottomMenuList = computed<MenuItem[]>(() => {
 	const baseList = [
-		{ title: '我的课程', icon: '/static/image/mine/myClass.png', url: '/pages/myCourses/myCourses', iconClass: 'icon-myclass' },
+		{ title: '我的课程', icon: '/static/image/mine/myClass.png', url: '/pages/myCourses/myCourses', iconClass: 'icon-myclass', requiresLogin: true },
 		{ title: '帮助中心', icon: '/static/image/mine/help.png', url: '/pages/helpCenter/helpCenter', iconClass: 'icon-help' },
 		{ title: '意见反馈', icon: '/static/image/mine/message.png', url: '/pages/feedback/feedback', iconClass: 'icon-feedback' },
 	]
@@ -253,13 +257,13 @@
  * 将接口或缓存中的用户信息归一化为页面可直接消费的结构。
  */
 function normalizeUserCenter(value: unknown): UserCenterData {
-	const source = (value && typeof value === 'object' ? value : {}) as Partial<UserCenterData>
+	const source = (value && typeof value === 'object' ? value : {}) as Partial<UserCenterData> & Record<string, unknown>
 	const type = Number(source.type)
 	return {
 		id: Number.isFinite(Number(source.id)) ? Number(source.id) : null,
-		nickname: normalizeOptionalText(source.nickname),
-		avatar: normalizeOptionalText(source.avatar),
-		phone: normalizeOptionalText(source.phone),
+		nickname: normalizeOptionalText(source.nickname) || normalizeOptionalText(source.nickName) || normalizeOptionalText(source.name),
+		avatar: normalizeOptionalText(source.avatar) || normalizeOptionalText(source.headImgUrl),
+		phone: normalizeOptionalText(source.phone) || normalizeOptionalText(source.mobile),
 		type: Number.isFinite(type) ? type : 0,
 		typeText: normalizeOptionalText(source.typeText),
 		memberExpireAt: normalizeOptionalText(source.memberExpireAt),
@@ -295,14 +299,19 @@
 	if (!item.url) {
 		return
 	}
-	goPage(item.url)
+	goPage(item)
 }
 
 /**
  * 统一跳转个人中心相关页面。
  */
-function goPage(url: string) {
+function goPage(target: string | MenuItem) {
+	const url = typeof target === 'string' ? target : target.url
 	if (!url) {
+		return
+	}
+	if (typeof target !== 'string' && target.requiresLogin && !hasStoredSession()) {
+		redirectToLogin()
 		return
 	}
 	uni.navigateTo({
@@ -311,13 +320,28 @@
 }
 
 /**
+ * 未登录访问个人资产类页面时,先进入登录页。
+ */
+function redirectToLogin() {
+	uni.showToast({
+		title: '请先登录',
+		icon: 'none',
+	})
+	setTimeout(() => {
+		uni.navigateTo({
+			url: '/pages/login/login',
+		})
+	}, 200)
+}
+
+/**
  * APP 环境读取当前安装包版本,非 APP 环境使用 manifest 版本兜底。
  */
 function resolveAppVersion() {
-	getRuntimeVersionName('1.0.14').then((versionName) => {
-		appVersionText.value = versionName || '1.0.14'
+	getRuntimeVersionName(manifestVersionName).then((versionName) => {
+		appVersionText.value = versionName || manifestVersionName
 	}).catch(() => {
-		appVersionText.value = '1.0.14'
+		appVersionText.value = manifestVersionName
 	})
 }
 
@@ -375,7 +399,7 @@
 	display: flex;
 	justify-content: flex-end;
 	align-items: center;
-	padding-top: 20rpx;
+	padding-top: 80rpx;
 	gap: 32rpx;
 
 	.notice-action {
@@ -410,7 +434,7 @@
 .user-info {
 	display: flex;
 	align-items: center;
-	margin-top: 40rpx;
+	margin-top: 24rpx;
 
 	.avatar-wrap {
 		position: relative;
diff --git a/traditionalApp/pages/protocol/protocol.vue b/traditionalApp/pages/protocol/protocol.vue
index d1f58ec..3762124 100644
--- a/traditionalApp/pages/protocol/protocol.vue
+++ b/traditionalApp/pages/protocol/protocol.vue
@@ -11,151 +11,156 @@
 				<rich-text :nodes="expertIntroNodes" class="section-rich-content"></rich-text>
 			</view>
 		</view>
-		<rich-text v-else :nodes="htmlContent" class="rich-content"></rich-text>
+		<view v-else class="protocol-content">
+			<rich-text :nodes="htmlContent" class="rich-content"></rich-text>
+		</view>
 	</view>
 </template>
 
-<script setup lang="ts">
-import { ref } from 'vue'
-import { onLoad } from '@dcloudio/uni-app'
+<script>
 import { getLiveExpertDetail } from '../liveDetail/service'
 import { getAgreementContentDetail } from './service'
-
-interface ExpertDetail {
-	id?: number | string | null
-	name: string
-	avatar: string
-	specialty: string
-	intro: string
-}
 
 const DEFAULT_USER_AGREEMENT_TITLE = '用户服务协议'
 const DEFAULT_PRIVACY_TITLE = '隐私协议'
 
-const pageType = ref('')
-const htmlContent = ref('')
-const expertDetail = ref<ExpertDetail>(createFallbackExpertDetail())
-const expertIntroNodes = ref(buildRichTextNodes(createFallbackExpertDetail().intro, '暂无专家介绍'))
+export default {
+	data() {
+		return {
+			pageType: '',
+			htmlContent: '',
+			expertDetail: this.createFallbackExpertDetail(),
+			expertIntroNodes: this.buildRichTextNodes(this.createFallbackExpertDetail().intro, '暂无专家介绍')
+		}
+	},
+	onLoad(options) {
+		this.pageType = typeof options?.type === 'string' ? options.type : ''
 
-/**
- * 创建专家介绍页的兜底静态数据,避免旧入口无参时页面空白。
- */
-function createFallbackExpertDetail(): ExpertDetail {
-	return {
-		id: null,
-		name: '张雨泽',
-		avatar: '/static/image/home/avatr.png',
-		specialty: '中医文化传播',
-		intro: '专家老师详细介绍内容。张雨泽老师在相关领域拥有多年的丰富经验,致力于传播中医文化,培养优秀的专业人才。',
-	}
-}
-
-/**
- * 加载协议详情页真实接口,未配置时返回空富文本兜底。
- */
-async function loadAgreementContent(type: string) {
-	uni.setNavigationBarTitle({
-		title: resolveAgreementTitle(type),
-	})
-	htmlContent.value = buildRichTextNodes('', '暂无协议内容')
-	try {
-		const response = await getAgreementContentDetail(type)
-		if (response?.code !== 0 || !response.data) {
+		if (this.pageType === 'user') {
+			this.loadAgreementContent(this.pageType)
 			return
 		}
-		const agreementName = normalizeText(response.data.agreementName) || resolveAgreementTitle(type)
-		uni.setNavigationBarTitle({
-			title: agreementName,
-		})
-		htmlContent.value = buildRichTextNodes(normalizeText(response.data.contentHtml), '暂无协议内容')
-	} catch {
-		// 请求层会统一处理异常提示,这里保留兜底空内容即可。
-	}
-}
 
-/**
- * 将接口返回的富文本正文转换为 `rich-text` 可直接渲染的节点字符串。
- */
-function buildRichTextNodes(content?: string, fallbackText?: string): string {
-	if (typeof content === 'string' && content.trim()) {
-		return content
-	}
-	if (typeof fallbackText === 'string' && fallbackText.trim()) {
-		return `<p>${fallbackText.trim()}</p>`
-	}
-	return '<p></p>'
-}
-
-/**
- * 根据页面类型返回协议页默认标题。
- */
-function resolveAgreementTitle(type: string) {
-	if (type === 'privacy') {
-		return DEFAULT_PRIVACY_TITLE
-	}
-	return DEFAULT_USER_AGREEMENT_TITLE
-}
-
-/**
- * 将可选文本归一化为空字符串。
- */
-function normalizeText(value: unknown) {
-	if (typeof value !== 'string') {
-		return ''
-	}
-	return value.trim()
-}
-
-/**
- * 加载专家介绍页真实接口,失败时回退到静态内容。
- */
-async function loadExpertDetail(expertId?: string) {
-	if (!expertId) {
-		expertDetail.value = createFallbackExpertDetail()
-		expertIntroNodes.value = buildRichTextNodes(expertDetail.value.intro, '暂无专家介绍')
-		return
-	}
-	try {
-		const response = await getLiveExpertDetail(expertId)
-		if (response?.code !== 0 || !response.data) {
-			expertDetail.value = createFallbackExpertDetail()
-			expertIntroNodes.value = buildRichTextNodes(expertDetail.value.intro, '暂无专家介绍')
+		if (this.pageType === 'privacy') {
+			this.loadAgreementContent(this.pageType)
 			return
 		}
-		expertDetail.value = {
-			...createFallbackExpertDetail(),
-			...response.data,
+
+		if (this.pageType === 'expert') {
+			uni.setNavigationBarTitle({
+				title: '专家老师介绍'
+			})
+			this.loadExpertDetail(typeof options?.id === 'string' ? options.id : '')
 		}
-		expertIntroNodes.value = buildRichTextNodes(expertDetail.value.intro, '暂无专家介绍')
-	} catch {
-		expertDetail.value = createFallbackExpertDetail()
-		expertIntroNodes.value = buildRichTextNodes(expertDetail.value.intro, '暂无专家介绍')
+	},
+	methods: {
+		/**
+		 * 创建专家介绍页的兜底静态数据,避免旧入口无参时页面空白。
+		 */
+		createFallbackExpertDetail() {
+			return {
+				id: null,
+				name: '张雨泽',
+				avatar: '/static/image/home/avatr.png',
+				specialty: '中医文化传播',
+				intro: '专家老师详细介绍内容。张雨泽老师在相关领域拥有多年的丰富经验,致力于传播中医文化,培养优秀的专业人才。',
+			}
+		},
+
+		/**
+		 * 加载协议详情页真实接口,未配置时返回空富文本兜底。
+		 */
+		async loadAgreementContent(type) {
+			uni.setNavigationBarTitle({
+				title: this.resolveAgreementTitle(type),
+			})
+			this.htmlContent = this.buildRichTextNodes('', '暂无协议内容')
+
+			try {
+				const response = await getAgreementContentDetail(type)
+				if (!response || response.code !== 0 || !response.data) {
+					return
+				}
+
+				const agreementName = this.normalizeText(response.data.agreementName) || this.resolveAgreementTitle(type)
+				uni.setNavigationBarTitle({
+					title: agreementName,
+				})
+				const contentHtml = this.normalizeText(response.data.contentHtml)
+				if (!contentHtml) {
+					return
+				}
+
+				this.htmlContent = this.buildRichTextNodes(contentHtml, '暂无协议内容')
+			} catch (error) {
+				// 请求层会统一处理异常提示,这里保留兜底空内容即可。
+			}
+		},
+
+		/**
+		 * 将接口返回的富文本正文转换为 `rich-text` 可直接渲染的节点字符串。
+		 */
+		buildRichTextNodes(content, fallbackText) {
+			if (typeof content === 'string' && content.trim()) {
+				return content
+					.replace(/<img/gi, '<img style="max-width:100%;height:auto;margin:0 auto;display:block"')
+					.replace(/<p/gi, '<p style="margin:10px 0;line-height:1.8;word-break:break-all"')
+					.replace(/<div/gi, '<div style="word-break:break-all"')
+			}
+			if (typeof fallbackText === 'string' && fallbackText.trim()) {
+				return `<p>${fallbackText.trim()}</p>`
+			}
+			return '<p></p>'
+		},
+
+		/**
+		 * 根据页面类型返回协议页默认标题。
+		 */
+		resolveAgreementTitle(type) {
+			if (type === 'privacy') {
+				return DEFAULT_PRIVACY_TITLE
+			}
+			return DEFAULT_USER_AGREEMENT_TITLE
+		},
+
+		/**
+		 * 将可选文本归一化为空字符串。
+		 */
+		normalizeText(value) {
+			if (typeof value !== 'string') {
+				return ''
+			}
+			return value.trim()
+		},
+
+		/**
+		 * 加载专家介绍页真实接口,失败时回退到静态内容。
+		 */
+		async loadExpertDetail(expertId) {
+			if (!expertId) {
+				this.expertDetail = this.createFallbackExpertDetail()
+				this.expertIntroNodes = this.buildRichTextNodes(this.expertDetail.intro, '暂无专家介绍')
+				return
+			}
+			try {
+				const response = await getLiveExpertDetail(expertId)
+				if (response?.code !== 0 || !response.data) {
+					this.expertDetail = this.createFallbackExpertDetail()
+					this.expertIntroNodes = this.buildRichTextNodes(this.expertDetail.intro, '暂无专家介绍')
+					return
+				}
+				this.expertDetail = {
+					...this.createFallbackExpertDetail(),
+					...response.data,
+				}
+				this.expertIntroNodes = this.buildRichTextNodes(this.expertDetail.intro, '暂无专家介绍')
+			} catch {
+				this.expertDetail = this.createFallbackExpertDetail()
+				this.expertIntroNodes = this.buildRichTextNodes(this.expertDetail.intro, '暂无专家介绍')
+			}
+		}
 	}
 }
-
-/**
- * 页面加载时根据 type 参数分发到协议页或专家介绍页。
- */
-onLoad((options) => {
-	pageType.value = typeof options?.type === 'string' ? options.type : ''
-
-	if (pageType.value === 'user') {
-		loadAgreementContent(pageType.value)
-		return
-	}
-
-	if (pageType.value === 'privacy') {
-		loadAgreementContent(pageType.value)
-		return
-	}
-
-	if (pageType.value === 'expert') {
-		uni.setNavigationBarTitle({
-			title: '专家老师介绍'
-		})
-		loadExpertDetail(typeof options?.id === 'string' ? options.id : '')
-	}
-})
 </script>
 
 <style lang="scss" scoped>
@@ -165,8 +170,15 @@
 	box-sizing: border-box;
 }
 
+.protocol-content {
+	padding: 32rpx;
+}
+
 .rich-content {
 	width: 100%;
+	font-size: 28rpx;
+	line-height: 1.8;
+	color: #333;
 }
 
 .expert-page {
diff --git a/traditionalApp/pages/protocol/service.js b/traditionalApp/pages/protocol/service.js
index 2409c27..c65f592 100644
--- a/traditionalApp/pages/protocol/service.js
+++ b/traditionalApp/pages/protocol/service.js
@@ -1,8 +1,8 @@
-import request from '@/utils/request.js'
+import { publicGet } from '@/utils/public-request.js'
 
 /**
  * 获取协议内容详情。
  */
 export const getAgreementContentDetail = (type) => {
-	return request.get(`/api/app/agreement-contents/${type}`, {}, false, false)
+	return publicGet(`/api/app/agreement-contents/${type}`)
 }
diff --git a/traditionalApp/pages/watchLive/watchLive.vue b/traditionalApp/pages/watchLive/watchLive.vue
index 22692b5..630680f 100644
--- a/traditionalApp/pages/watchLive/watchLive.vue
+++ b/traditionalApp/pages/watchLive/watchLive.vue
@@ -15,12 +15,12 @@
 			<view v-if="isVideoPlayable" class="video-player-wrapper" :style="videoSectionStyle" @tap="handleVideoWrapperTap">
 				<video id="live-watch-player" class="video-player" :style="videoSectionStyle" :src="liveDetail.videoUrl"
 					:poster="videoPoster" :initial-time="liveInitialTime" :controls="false" :autoplay="false"
-					:show-center-play-btn="false" :show-fullscreen-btn="false" :show-play-btn="false"
+					:show-center-play-btn="showNativeCenterPlayButton" :show-fullscreen-btn="false" :show-play-btn="false"
 					:show-progress="false" :enable-progress-gesture="false" :muted="isReadonlyDisplayMode" object-fit="contain"
 					play-btn-position="center" @loadedmetadata="handleVideoLoadedMetadata" @canplay="handleVideoCanPlay"
 					@timeupdate="handleVideoTimeUpdate" @play="handleVideoPlay" @pause="handleVideoPause"
 					@ended="handleVideoEnded" @error="handleVideoError" @fullscreenchange="handleFullscreenChange"></video>
-				<view class="video-touch-layer"></view>
+				<cover-view v-if="showVideoGestureBlocker" class="video-touch-layer"></cover-view>
 				<view v-if="showAutoPlayLoader" class="video-loader-mask">
 					<view class="video-loader-ring" :style="videoLoaderRingStyle">
 						<view class="video-loader-ring__inner">
@@ -29,17 +29,23 @@
 						</view>
 					</view>
 				</view>
-				<view v-if="showPlaybackRetry" class="video-retry-mask" @tap.stop="retryPlayback">
-					<text class="video-retry-text">点击开始播放</text>
-				</view>
-				<view v-if="showCustomFullscreenButton" class="video-fullscreen-btn" @tap.stop="toggleFullscreen">
-					<view class="video-fullscreen-btn__icon">
-						<view class="video-fullscreen-btn__corner video-fullscreen-btn__corner--lt"></view>
-						<view class="video-fullscreen-btn__corner video-fullscreen-btn__corner--rt"></view>
-						<view class="video-fullscreen-btn__corner video-fullscreen-btn__corner--lb"></view>
-						<view class="video-fullscreen-btn__corner video-fullscreen-btn__corner--rb"></view>
-					</view>
-				</view>
+				<cover-view v-if="showManualPlayLoader" class="video-cover-loader-mask">
+					<cover-view class="video-cover-loader-ring" :style="videoLoaderRingStyle">
+						<cover-view class="video-cover-loader-ring__inner">
+							<cover-view class="video-cover-loader-percent">{{ autoPlayLoaderPercentText }}</cover-view>
+							<cover-view class="video-cover-loader-label">加载中</cover-view>
+						</cover-view>
+					</cover-view>
+				</cover-view>
+				<cover-view v-if="showCustomPlayButton" class="video-cover-play-button" @tap="startManualPlayback">
+					▶
+				</cover-view>
+				<cover-view v-if="showEnterFullscreenButton" class="video-cover-fullscreen-btn" @tap="toggleFullscreen">
+					全屏
+				</cover-view>
+				<cover-view v-if="showExitFullscreenButton" class="video-cover-fullscreen-btn video-cover-fullscreen-btn--exit" @tap="toggleFullscreen">
+					退出
+				</cover-view>
 			</view>
 			<image v-else class="video-bg" :src="videoPoster" mode="aspectFill"></image>
 			<view v-if="!isVideoPlayable" class="video-placeholder">
@@ -173,7 +179,7 @@
 </template>
 
 <script setup lang="ts">
-	import { computed, getCurrentInstance, nextTick, ref } from 'vue';
+	import { computed, getCurrentInstance, nextTick, onMounted, ref } from 'vue';
 	import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app';
 	import { getLiveCourseDetail, getLiveQuizStatus, submitLiveQuiz, uploadLiveWatchRecord } from './service';
 	import { getHelpCenter } from '../helpCenter/service';
@@ -311,6 +317,8 @@
 	const playbackFailed = ref(false)
 	const pendingPlaybackSync = ref(false)
 	const playbackSyncAttempts = ref(0)
+	const isManualPlayLoaderVisible = ref(false)
+	const shouldResyncOnNextPlay = ref(false)
 	const lastMediaCurrentTime = ref(0)
 	const currentWatchStartedAt = ref(0)
 	const autoPlayLoaderPercent = ref(0)
@@ -322,6 +330,7 @@
 	let videoContext : ReturnType<typeof uni.createVideoContext> | null = null
 	let playbackStartupTimer : ReturnType<typeof setTimeout> | null = null
 	let playbackSyncTimer : ReturnType<typeof setTimeout> | null = null
+	let manualPlayLoaderTimer : ReturnType<typeof setTimeout> | null = null
 	let quizAvailabilityTimer : ReturnType<typeof setInterval> | null = null
 	let autoPlayLoaderTimer : ReturnType<typeof setInterval> | null = null
 	let autoPlayLoaderFinishTimer : ReturnType<typeof setTimeout> | null = null
@@ -517,23 +526,28 @@
 
 	const pageTitle = computed(() => liveDetail.value.title || '直播观看')
 	const isReadonlyDisplayMode = isH5
-	const isLoaderGatedPlayback = computed(() => isReadonlyDisplayMode || isAppPlus)
+	const isLoaderGatedPlayback = computed(() => isReadonlyDisplayMode)
+	const showNativeCenterPlayButton = computed(() => false)
 	const videoPoster = computed(() => liveDetail.value.displayImage || liveDetail.value.coverImage || defaultVideoPoster)
 	const videoSectionStyle = computed(() => ({
 		height: showAnswer.value ? '500rpx' : '1234rpx',
 	}))
-	const showPlaybackRetry = computed(() => {
-		if (isReadonlyDisplayMode) {
-			return false
-		}
-		if (!isVideoPlayable.value || !isVideoReady.value) {
-			return false
-		}
-		if (playbackFailed.value) {
-			return true
-		}
-		// H5 浏览器禁止未交互自动播放时,直接展示点击播放提示,避免首屏触发 NotAllowedError。
-		return isH5 && !isPlaybackStarted.value
+	const showCustomPlayButton = computed(() => {
+		return !isReadonlyDisplayMode
+			&& isVideoPlayable.value
+			&& !isManualPlayLoaderVisible.value
+			&& (!isPlaybackStarted.value || isVideoPaused.value || playbackFailed.value)
+	})
+	const showManualPlayLoader = computed(() => {
+		return !isReadonlyDisplayMode
+			&& isVideoPlayable.value
+			&& isManualPlayLoaderVisible.value
+	})
+	const showVideoGestureBlocker = computed(() => {
+		return isVideoPlayable.value
+			&& !showAutoPlayLoader.value
+			&& !showManualPlayLoader.value
+			&& !showCustomPlayButton.value
 	})
 	const activeSessionId = computed(() => {
 		if (liveDetail.value.actionSessionId != null && liveDetail.value.actionSessionId !== '') {
@@ -646,7 +660,7 @@
 			return '直播已结束,请完成答题'
 		}
 		if (!liveDetail.value.videoUrl) {
-			return '直播视频准备中'
+			return liveDetail.value.displayStatus === 'ENDED' ? '直播已结束,请完成答题' : '直播视频准备中'
 		}
 		return '当前直播暂不可观看'
 	})
@@ -661,10 +675,22 @@
 			background: `conic-gradient(#ffffff ${Math.min(autoPlayLoaderPercent.value, 100) * 3.6}deg, rgba(255, 255, 255, 0.2) 0deg)`,
 		}
 	})
-	const showCustomFullscreenButton = computed(() => {
-		return isVideoPlayable.value
+	const showEnterFullscreenButton = computed(() => {
+		return isAppPlus
+			&& isVideoPlayable.value
 			&& !showAutoPlayLoader.value
+			&& !showManualPlayLoader.value
+			&& !showCustomPlayButton.value
 			&& !playbackFailed.value
+			&& !isReadonlyDisplayMode
+			&& !isVideoFullscreen.value
+	})
+	const showExitFullscreenButton = computed(() => {
+		return isAppPlus
+			&& isVideoPlayable.value
+			&& !playbackFailed.value
+			&& !isReadonlyDisplayMode
+			&& isVideoFullscreen.value
 	})
 
 	/**
@@ -676,12 +702,31 @@
 		sessionId.value = resolvedParams.sessionId
 	})
 
+	onMounted(() => {
+		setTimeout(() => {
+			if (courseId.value) {
+				loadLiveDetail()
+				return
+			}
+			const resolvedParams = resolveWatchRouteParams(getCurrentPageOptions())
+			courseId.value = resolvedParams.courseId
+			sessionId.value = resolvedParams.sessionId
+			loadLiveDetail()
+		}, 0)
+	})
+
+	function getCurrentPageOptions() {
+		const pages = getCurrentPages()
+		const currentPage = pages[pages.length - 1] as { options ?: Record<string, unknown>; $page ?: { options ?: Record<string, unknown> } } | undefined
+		return currentPage?.options || currentPage?.$page?.options || {}
+	}
+
 	/**
 	 * 统一解析观看页参数,兼容 App 路由和 H5 分享地址。
 	 */
 	function resolveWatchRouteParams(options ?: Record<string, unknown>) {
-		const routeCourseId = typeof options?.courseId === 'string' ? options.courseId.trim() : ''
-		const routeSessionId = typeof options?.sessionId === 'string' ? options.sessionId.trim() : ''
+		const routeCourseId = normalizeRouteParam(options?.courseId)
+		const routeSessionId = normalizeRouteParam(options?.sessionId)
 		if (routeCourseId || routeSessionId) {
 			return {
 				courseId: routeCourseId,
@@ -715,11 +760,23 @@
 		}
 	}
 
+	function normalizeRouteParam(value : unknown) {
+		if (value === undefined || value === null) {
+			return ''
+		}
+		return String(value).trim()
+	}
+
 	/**
 	 * 每次展示观看页时刷新详情,保证取到最新可观看视频与答题状态。
 	 */
 	onShow(() => {
-		loadLiveDetail()
+		const shouldResumeAfterRefresh = !isReadonlyDisplayMode
+			&& isVideoPlayable.value
+			&& (isVideoPaused.value || playbackFailed.value)
+		loadLiveDetail({
+			resumeIfPaused: shouldResumeAfterRefresh,
+		})
 	})
 
 	/**
@@ -728,7 +785,9 @@
 	onHide(() => {
 		clearPlaybackStartupTimer()
 		clearPlaybackSyncTimer()
+		clearManualPlayLoaderTimer()
 		clearAutoPlayLoaderTimers()
+		isManualPlayLoaderVisible.value = false
 		clearQuizAvailabilityTimer()
 		submitCurrentWatchSegment()
 	})
@@ -739,7 +798,9 @@
 	onUnload(() => {
 		clearPlaybackStartupTimer()
 		clearPlaybackSyncTimer()
+		clearManualPlayLoaderTimer()
 		clearAutoPlayLoaderTimers()
+		isManualPlayLoaderVisible.value = false
 		clearQuizAvailabilityTimer()
 		submitCurrentWatchSegment()
 	})
@@ -747,11 +808,12 @@
 	/**
 	 * 拉取直播详情并回填观看页视频信息。
 	 */
-	async function loadLiveDetail() {
+	async function loadLiveDetail(options : { resumeIfPaused ?: boolean } = {}) {
 		if (!courseId.value) {
 			loadFailed.value = true
 			return
 		}
+		const shouldResumeIfPaused = !!options.resumeIfPaused
 		resetVideoPlaybackState()
 		resetQuizState()
 		loading.value = true
@@ -789,6 +851,10 @@
 			await nextTick()
 			initVideoContext()
 			startAutoPlayLoaderIfNeeded()
+			if (shouldResumeIfPaused && isVideoPlayable.value) {
+				shouldResyncOnNextPlay.value = true
+				retryPlayback()
+			}
 			if (hasLoginToken() && activeSessionId.value) {
 				await loadQuizStatus()
 			}
@@ -850,6 +916,7 @@
 	function resetVideoPlaybackState() {
 		clearPlaybackStartupTimer()
 		clearPlaybackSyncTimer()
+		clearManualPlayLoaderTimer()
 		clearAutoPlayLoaderTimers()
 		currentWatchStartedAt.value = 0
 		isVideoReady.value = false
@@ -861,6 +928,8 @@
 		playbackFailed.value = false
 		pendingPlaybackSync.value = false
 		playbackSyncAttempts.value = 0
+		isManualPlayLoaderVisible.value = false
+		shouldResyncOnNextPlay.value = false
 		lastMediaCurrentTime.value = 0
 		autoPlayLoaderPercent.value = 0
 		isAutoPlayLoaderVisible.value = false
@@ -1021,6 +1090,48 @@
 	}
 
 	/**
+	 * 清理手动播放加载遮罩的进度定时器。
+	 */
+	function clearManualPlayLoaderTimer() {
+		if (manualPlayLoaderTimer) {
+			clearTimeout(manualPlayLoaderTimer)
+			manualPlayLoaderTimer = null
+		}
+	}
+
+	/**
+	 * 推进手动播放加载环到 99%,真实播放成功后再收尾到 100%。
+	 */
+	function updateManualPlayLoaderProgress(startTimestamp : number) {
+		const elapsed = Date.now() - startTimestamp
+		autoPlayLoaderPercent.value = Math.min(
+			AUTO_PLAY_LOADER_TARGET,
+			Math.floor((elapsed / AUTO_PLAY_LOADER_DURATION) * AUTO_PLAY_LOADER_TARGET),
+		)
+		if (!isManualPlayLoaderVisible.value) {
+			manualPlayLoaderTimer = null
+			return
+		}
+		manualPlayLoaderTimer = setTimeout(() => {
+			updateManualPlayLoaderProgress(startTimestamp)
+		}, 60)
+	}
+
+	/**
+	 * 播放真正成功后,把手动加载环补到 100% 并立即关闭遮罩。
+	 */
+	function finishManualPlayLoader() {
+		if (!isManualPlayLoaderVisible.value) {
+			return
+		}
+		clearManualPlayLoaderTimer()
+		autoPlayLoaderPercent.value = 100
+		setTimeout(() => {
+			isManualPlayLoaderVisible.value = false
+		}, 120)
+	}
+
+	/**
 	 * 清理播放进度同步重试定时器,避免页面隐藏后继续 seek。
 	 */
 	function clearPlaybackSyncTimer() {
@@ -1045,7 +1156,7 @@
 	}
 
 	/**
-	 * 进入伪直播后先展示加载环,再进入横屏全屏并按当前直播进度播放。
+	 * 进入伪直播后先展示加载环,加载完成后交给用户手动播放。
 	 */
 	function startAutoPlayLoaderIfNeeded() {
 		if (!isLoaderGatedPlayback.value || !isVideoPlayable.value || hasStartedReadonlyAutoPlay.value) {
@@ -1066,9 +1177,7 @@
 			if (elapsed >= AUTO_PLAY_LOADER_DURATION) {
 				clearAutoPlayLoaderTimers()
 				autoPlayLoaderPercent.value = AUTO_PLAY_LOADER_TARGET
-				hasQueuedReadonlyAutoPlay.value = true
-				requestLandscapeFullscreenIfNeeded()
-				tryStartPlayback()
+				isAutoPlayLoaderVisible.value = false
 			}
 		}, 60)
 	}
@@ -1095,6 +1204,8 @@
 				isStartingPlayback.value = false
 				playbackFailed.value = true
 				isVideoPaused.value = true
+				clearManualPlayLoaderTimer()
+				isManualPlayLoaderVisible.value = false
 			}
 		}, 1200)
 	}
@@ -1232,17 +1343,19 @@
 			}
 			return
 		}
-		syncPlaybackWhenReady(true)
+		isVideoPaused.value = true
+		if (pendingPlaybackSync.value || hasQueuedReadonlyAutoPlay.value) {
+			syncPlaybackWhenReady(hasQueuedReadonlyAutoPlay.value)
+		}
 	}
 
 	/**
 	 * 视频区域点击仅保留给 App 端暂停/恢复,分享页不提供手动播放按钮。
 	 */
 	function handleVideoWrapperTap() {
-		if (isReadonlyDisplayMode) {
+		if (!isReadonlyDisplayMode) {
 			return
 		}
-		togglePlayback()
 	}
 
 	/**
@@ -1262,7 +1375,39 @@
 	/**
 	 * 手动重试当前视频播放,复用统一的自动播放拉起逻辑。
 	 */
+	function startManualPlayback() {
+		if (!isVideoPlayable.value || isManualPlayLoaderVisible.value) {
+			return
+		}
+		clearManualPlayLoaderTimer()
+		autoPlayLoaderPercent.value = 0
+		playbackFailed.value = false
+		isVideoPaused.value = false
+		isManualPlayLoaderVisible.value = true
+		updateManualPlayLoaderProgress(Date.now())
+		retryPlayback()
+	}
+
+	/**
+	 * 按当前伪直播进度拉起播放。
+	 */
 	function retryPlayback() {
+		if (!videoContext) {
+			initVideoContext()
+		}
+		hasQueuedReadonlyAutoPlay.value = true
+		isVideoPaused.value = false
+		playbackFailed.value = false
+		if (!supportsSimulatedPlayback.value || !isVideoReady.value || mediaDurationSeconds.value <= 0) {
+			pendingPlaybackSync.value = true
+			triggerVideoPlay()
+			return
+		}
+		if (shouldResyncOnNextPlay.value) {
+			shouldResyncOnNextPlay.value = false
+			syncPlaybackWhenReady(true)
+			return
+		}
 		tryStartPlayback()
 	}
 
@@ -1336,6 +1481,17 @@
 		isPlaybackStarted.value = true
 		playbackFailed.value = false
 		isVideoPaused.value = false
+		finishManualPlayLoader()
+		if (shouldResyncOnNextPlay.value && supportsSimulatedPlayback.value && isVideoReady.value && mediaDurationSeconds.value > 0) {
+			shouldResyncOnNextPlay.value = false
+			seekToExpectedPlayback(false, false)
+		}
+		if (!hasQueuedReadonlyAutoPlay.value) {
+			hasQueuedReadonlyAutoPlay.value = true
+			if (supportsSimulatedPlayback.value && isVideoReady.value && mediaDurationSeconds.value > 0) {
+				seekToExpectedPlayback(false, false)
+			}
+		}
 		if (isLoaderGatedPlayback.value) {
 			clearAutoPlayLoaderTimers()
 			autoPlayLoaderPercent.value = 100
@@ -1373,6 +1529,14 @@
 		if (isSyncingPlayback.value) {
 			return
 		}
+		if (isVideoFullscreen.value) {
+			isVideoPaused.value = false
+			if (!isStartingPlayback.value) {
+				shouldResyncOnNextPlay.value = true
+				retryPlayback()
+			}
+			return
+		}
 		if (isStartingPlayback.value && !currentWatchStartedAt.value) {
 			clearPlaybackStartupTimer()
 			isStartingPlayback.value = false
@@ -1408,11 +1572,13 @@
 	 */
 	function handleVideoError(event : VideoErrorEvent) {
 		clearPlaybackStartupTimer()
+		clearManualPlayLoaderTimer()
 		clearAutoPlayLoaderTimers()
 		isStartingPlayback.value = false
 		isPlaybackStarted.value = false
 		playbackFailed.value = true
 		isVideoPaused.value = true
+		isManualPlayLoaderVisible.value = false
 		isAutoPlayLoaderVisible.value = false
 		if (event?.detail?.errMsg) {
 			uni.showToast({
@@ -1670,16 +1836,19 @@
 	}
 
 	/**
-	 * 记录全屏切换状态,驱动自绘按钮文案与退出逻辑。
+	 * 记录原生全屏状态,驱动自绘全屏按钮。
 	 */
 	function handleFullscreenChange(event : VideoFullscreenChangeEvent) {
 		isVideoFullscreen.value = !!(event?.detail?.fullScreen || event?.detail?.fullscreen)
 	}
 
 	/**
-	 * 使用自绘按钮触发全屏,避免依赖原生播放控制条。
+	 * 使用原生 video 全屏,避免伪全屏放大页面;原生 controls 仍保持关闭。
 	 */
 	function toggleFullscreen() {
+		if (!videoContext) {
+			initVideoContext()
+		}
 		if (!videoContext) {
 			return
 		}
@@ -1689,6 +1858,9 @@
 		}
 		videoContext.requestFullScreen({
 			direction: 90,
+			success: () => {
+				isVideoFullscreen.value = true
+			},
 		})
 	}
 </script>
@@ -1822,87 +1994,105 @@
 			line-height: 1.2;
 		}
 
-		.video-retry-mask {
+		.video-cover-play-button {
 			position: absolute;
-			inset: 0;
-			z-index: 3;
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			background: rgba(0, 0, 0, 0.38);
-		}
-
-		.video-retry-text {
-			padding: 20rpx 36rpx;
-			border-radius: 999rpx;
-			background: rgba(255, 255, 255, 0.18);
+			left: 315rpx;
+			top: 50%;
+			z-index: 101;
+			width: 120rpx;
+			height: 120rpx;
+			line-height: 120rpx;
+			margin-top: -60rpx;
+			border-radius: 60rpx;
+			background-color: rgba(0, 0, 0, 0.68);
 			color: #ffffff;
-			font-size: 28rpx;
-			line-height: 40rpx;
+			font-size: 52rpx;
+			font-weight: 600;
+			text-align: center;
 		}
 
-		.video-fullscreen-btn {
+		.video-cover-loader-mask {
 			position: absolute;
-			right: 24rpx;
-			bottom: 24rpx;
-			z-index: 4;
-			width: 76rpx;
-			height: 76rpx;
-			border-radius: 20rpx;
-			background: rgba(0, 0, 0, 0.52);
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			backdrop-filter: blur(8rpx);
+			left: 0;
+			top: 0;
+			z-index: 102;
+			width: 750rpx;
+			height: 100%;
+			background-color: rgba(0, 0, 0, 0.28);
 		}
 
-		.video-fullscreen-btn__icon {
-			position: relative;
-			width: 32rpx;
+		.video-cover-loader-ring {
+			position: absolute;
+			left: 285rpx;
+			top: 50%;
+			width: 180rpx;
+			height: 180rpx;
+			margin-top: -90rpx;
+			border-radius: 90rpx;
+			padding: 10rpx;
+			box-sizing: border-box;
+			background-color: rgba(255, 255, 255, 0.2);
+		}
+
+		.video-cover-loader-ring__inner {
+			position: absolute;
+			left: 10rpx;
+			top: 10rpx;
+			width: 160rpx;
+			height: 160rpx;
+			border-radius: 80rpx;
+			background-color: rgba(12, 12, 12, 0.78);
+		}
+
+		.video-cover-loader-percent {
+			position: absolute;
+			left: 0;
+			top: 42rpx;
+			width: 160rpx;
+			height: 48rpx;
+			line-height: 48rpx;
+			color: #ffffff;
+			font-size: 40rpx;
+			font-weight: 600;
+			text-align: center;
+		}
+
+		.video-cover-loader-label {
+			position: absolute;
+			left: 0;
+			top: 96rpx;
+			width: 160rpx;
 			height: 32rpx;
+			line-height: 32rpx;
+			color: rgba(255, 255, 255, 0.82);
+			font-size: 22rpx;
+			text-align: center;
 		}
 
-		.video-fullscreen-btn__corner {
+		.video-cover-fullscreen-btn {
 			position: absolute;
-			width: 12rpx;
-			height: 12rpx;
-			border-color: #ffffff;
+			right: 28rpx;
+			bottom: 28rpx;
+			z-index: 120;
+			width: 88rpx;
+			height: 88rpx;
+			line-height: 88rpx;
+			border-radius: 44rpx;
+			border-width: 2rpx;
+			border-style: solid;
+			border-color: rgba(255, 255, 255, 0.9);
+			background-color: rgba(0, 0, 0, 0.86);
+			color: #ffffff;
+			font-size: 42rpx;
+			font-weight: 600;
+			text-align: center;
 		}
 
-		.video-fullscreen-btn__corner--lt {
-			top: 0;
-			left: 0;
-			border-top-width: 3rpx;
-			border-left-width: 3rpx;
-			border-top-style: solid;
-			border-left-style: solid;
-		}
-
-		.video-fullscreen-btn__corner--rt {
-			top: 0;
-			right: 0;
-			border-top-width: 3rpx;
-			border-right-width: 3rpx;
-			border-top-style: solid;
-			border-right-style: solid;
-		}
-
-		.video-fullscreen-btn__corner--lb {
-			left: 0;
-			bottom: 0;
-			border-bottom-width: 3rpx;
-			border-left-width: 3rpx;
-			border-bottom-style: solid;
-			border-left-style: solid;
-		}
-
-		.video-fullscreen-btn__corner--rb {
-			right: 0;
-			bottom: 0;
-			border-right-width: 3rpx;
-			border-bottom-width: 3rpx;
-			border-right-style: solid;
-			border-bottom-style: solid;
+		.video-cover-fullscreen-btn--exit {
+			position: fixed;
+			right: 32rpx;
+			top: 32rpx;
+			z-index: 9999;
 		}
 
 		.video-placeholder {
diff --git a/traditionalApp/utils/public-request.js b/traditionalApp/utils/public-request.js
new file mode 100644
index 0000000..b30b971
--- /dev/null
+++ b/traditionalApp/utils/public-request.js
@@ -0,0 +1,26 @@
+import config from '@/config/index.js'
+
+/**
+ * 游客可访问接口专用请求,避免被本地登录态刷新、旧 token、请求队列影响。
+ */
+export function publicGet(url, data = {}) {
+	const requestUrl = `${config.BASE_URL}${url}`
+	return new Promise((resolve, reject) => {
+		uni.request({
+			url: requestUrl,
+			method: 'GET',
+			data,
+			timeout: 10000,
+			header: {
+				'content-type': 'application/json',
+				lang: uni.getStorageSync('locale') === 'zh-Hans' ? 'zh_CN' : 'zh-tw',
+			},
+			success: (res) => {
+				resolve(res.data || {})
+			},
+			fail: (error) => {
+				reject(error)
+			},
+		})
+	})
+}
diff --git a/traditionalApp/utils/push.js b/traditionalApp/utils/push.js
index 5a8b1fb..bd58f9b 100644
--- a/traditionalApp/utils/push.js
+++ b/traditionalApp/utils/push.js
@@ -93,7 +93,7 @@
 		return null
 	}
 	if (typeof jpushModule.setLoggerEnable === 'function') {
-		jpushModule.setLoggerEnable(true)
+		jpushModule.setLoggerEnable(false)
 	}
 	if (typeof jpushModule.initJPushService === 'function') {
 		jpushModule.initJPushService()
@@ -101,7 +101,6 @@
 	if (typeof jpushModule.addConnectEventListener === 'function') {
 		jpushModule.addConnectEventListener((result) => {
 			const connectEnable = !!(result && result.connectEnable)
-			console.log('极光连接状态', result)
 			if (!connectEnable) {
 				return
 			}
@@ -110,7 +109,6 @@
 	}
 	if (typeof jpushModule.addNotificationListener === 'function') {
 		jpushModule.addNotificationListener((result) => {
-			console.log('48948949841weqwe', result);
 			handleJPushNotification(result)
 		})
 	}
diff --git a/traditionalApp/utils/request.js b/traditionalApp/utils/request.js
index 7d8e61c..66d15ba 100644
--- a/traditionalApp/utils/request.js
+++ b/traditionalApp/utils/request.js
@@ -18,9 +18,14 @@
 const TOKEN_REFRESH_BUFFER_MS = 30 * 60 * 1000
 const SUCCESS_CODES = [0, 200]
 const UNAUTHORIZED_CODES = [401, 504, 505, 510]
+const SESSION_INVALID_MESSAGES = ['用户不存在', '用户已禁用']
 const PENDING_BIND_ALLOWED_URLS = [
 	'/api/app/settings/phone/code',
 	'/api/app/settings/phone/bind',
+]
+const PUBLIC_APP_URL_PREFIXES = [
+	'/api/app/home/',
+	'/api/v1/app/home/',
 ]
 
 const waitingList = []
@@ -49,6 +54,17 @@
 }
 
 /**
+ * 后端部分已登录接口会以业务错误返回“用户不存在/已禁用”,这里统一视为登录态失效。
+ */
+function isSessionInvalidPayload(payload) {
+	if (!getStoredToken()) {
+		return false
+	}
+	const message = resolveMessage(payload).trim()
+	return SESSION_INVALID_MESSAGES.includes(message)
+}
+
+/**
  * 读取当前语言请求头值。
  */
 function resolveLangHeader() {
@@ -59,21 +75,35 @@
  * 构造当前请求头。
  */
 function buildHeaders(type, url) {
-	return {
+	const authorization = resolveAuthorizationHeader(url)
+	const headers = {
 		'content-type': type ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/json',
-		Authorization: resolveAuthorizationHeader(url),
 		lang: resolveLangHeader(),
 	}
+	if (authorization) {
+		headers.Authorization = authorization
+	}
+	return headers
 }
 
 /**
  * 解析请求鉴权头,待绑定手机号临时 token 仅允许访问绑定手机号接口。
  */
 function resolveAuthorizationHeader(url) {
+	if (isPublicAppUrl(url)) {
+		return ''
+	}
 	if (!hasPendingPhoneBindSession()) {
 		return getStoredToken()
 	}
 	return isPendingBindAllowedUrl(url) ? getStoredToken() : ''
+}
+
+/**
+ * 首页公共接口允许游客访问,不能被本地旧登录态或 token 刷新失败影响。
+ */
+function isPublicAppUrl(url) {
+	return PUBLIC_APP_URL_PREFIXES.some((prefix) => url.startsWith(prefix))
 }
 
 /**
@@ -122,6 +152,11 @@
 					}
 					if (payload.code === 502 || payload.code === 501) {
 						showErrorToast(payload)
+						resolve(payload)
+						return
+					}
+					if (isSessionInvalidPayload(payload)) {
+						handleUnauthorized(payload)
 						resolve(payload)
 						return
 					}
@@ -252,6 +287,9 @@
  * 判断当前请求是否需要在发送前尝试刷新 token。
  */
 function shouldAttemptTokenRefresh(url) {
+	if (isPublicAppUrl(url)) {
+		return false
+	}
 	const token = getStoredToken()
 	if (!token || !hasBoundPhone()) {
 		return false
@@ -287,7 +325,7 @@
 					resolve(payload)
 					return
 				}
-				if (UNAUTHORIZED_CODES.includes(payload.code)) {
+				if (UNAUTHORIZED_CODES.includes(payload.code) || isSessionInvalidPayload(payload)) {
 					handleUnauthorized(payload)
 				}
 				reject(payload)

--
Gitblit v1.7.1