From af9004adafc56f3b171e15323464f6e16d575c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=AD=86?= <2685475410@qq.com> Date: Sat, 10 Oct 2026 12:49:00 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(review):=20=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=86=85=E5=88=87=E6=8D=A2=E6=8E=92=E5=BA=8F=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=A4=B1=E8=B4=A5=EF=BC=8C=E8=AF=84=E8=AE=BA=E9=A1=B5?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A=E9=A1=B6=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 评论页注入绝对 API_BASE:弹窗 WebView(loadDataWithBaseURL) 中相对路径 fetch 解析不可靠,切换排序/懒加载/回复全部改走绝对地址 - externalBaseURL 识别 X-Forwarded-Proto:正文气泡 ident 与评论页地址 不再硬编码 http://,避免反代 301 - 布局改造:顶栏固定(含拖拽把手视觉)+评论区独立滚动容器,滚动/回顶 逻辑迁移至容器;switchSort 显式传参替代隐式 event --- internal/handlers/content.go | 16 ++---- internal/handlers/review.go | 16 +++++- internal/handlers/templates/review.html | 75 +++++++++++++++++-------- 3 files changed, 70 insertions(+), 37 deletions(-) diff --git a/internal/handlers/content.go b/internal/handlers/content.go index d35b356..cd01595 100644 --- a/internal/handlers/content.go +++ b/internal/handlers/content.go @@ -124,23 +124,17 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string { } } - // 获取基础 URL + // 获取基础 URL(反代场景识别 X-Forwarded-Proto,避免生成 http:// 被 301) cfg := config.GetConfig() baseURL := "" - if cfg != nil { + if r.Host != "" { + baseURL = externalBaseURL(r) + } else if cfg != nil { host := cfg.Server.Host if host == "0.0.0.0" { host = "localhost" } - port := cfg.Server.Port - scheme := "http" - - // 检查是否来自外部请求 - if r.Host != "" { - baseURL = scheme + "://" + r.Host - } else { - baseURL = scheme + "://" + host + ":" + port - } + baseURL = "http://" + host + ":" + cfg.Server.Port } // 生成带段评标记的内容 diff --git a/internal/handlers/review.go b/internal/handlers/review.go index 9fd00d1..5d3cd9c 100644 --- a/internal/handlers/review.go +++ b/internal/handlers/review.go @@ -12,6 +12,16 @@ import ( "time" ) +// externalBaseURL 取外部可访问的基础 URL(nginx 反代下识别 X-Forwarded-Proto, +// 避免生成 http:// 链接被 301,弹窗 WebView 内 fetch 对 301 处理不稳) +func externalBaseURL(r *http.Request) string { + scheme := "http" + if p := r.Header.Get("X-Forwarded-Proto"); p != "" { + scheme = p + } + return scheme + "://" + r.Host +} + // validateParams 验证必需参数 func validateParams(r *http.Request, required ...string) (map[string]string, error) { params := make(map[string]string) @@ -207,7 +217,7 @@ func HandleReviewHTML(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) - html := generateReviewHTML(data, params["item_id"], bookID, para, page, pageSize, sort) + html := generateReviewHTML(r, data, params["item_id"], bookID, para, page, pageSize, sort) fmt.Fprint(w, html) } @@ -318,6 +328,7 @@ var reviewTmpl = template.Must(template.New("review").Parse(reviewHTML)) // reviewPageData 评论页模板数据 type reviewPageData struct { ItemID, BookID string + BaseURL string ParaPlus1, Para, Sort int Total, NextOffset int PageSize, CommentCount int @@ -327,7 +338,7 @@ type reviewPageData struct { } // generateReviewHTML 生成评论 HTML 页面 -func generateReviewHTML(data map[string]interface{}, itemID, bookID string, para, page, pageSize, sort int) string { +func generateReviewHTML(r *http.Request, data map[string]interface{}, itemID, bookID string, para, page, pageSize, sort int) string { total := 0 hasMore := false nextOffset := 0 @@ -383,6 +394,7 @@ func generateReviewHTML(data map[string]interface{}, itemID, bookID string, para var b strings.Builder if err := reviewTmpl.Execute(&b, reviewPageData{ ItemID: itemID, BookID: bookID, + BaseURL: externalBaseURL(r), ParaPlus1: para + 1, Para: para, Sort: sort, Total: total, NextOffset: nextOffset, PageSize: pageSize, CommentCount: len(pageComments), diff --git a/internal/handlers/templates/review.html b/internal/handlers/templates/review.html index fbe04da..521775e 100644 --- a/internal/handlers/templates/review.html +++ b/internal/handlers/templates/review.html @@ -49,25 +49,50 @@ box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } + html, body { height: 100%; } body { background-color: var(--bg-primary); color: var(--text-primary); - padding: 15px; max-width: 600px; margin: 0 auto; + display: flex; + flex-direction: column; + overflow: hidden; transition: background-color 0.3s ease, color 0.3s ease; } .header { + flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; - padding: 10px 15px; + padding: 10px 15px 12px; background: var(--bg-secondary); - border-radius: 12px; + border-radius: 20px 20px 0 0; + border-bottom: 1px solid var(--border-color); box-shadow: 0 2px 8px var(--shadow); + position: relative; + z-index: 10; transition: background-color 0.3s ease; } + /* BottomSheet 顶部拖拽把手 */ + .header::before { + content: ''; + position: absolute; + top: 6px; + left: 50%; + transform: translateX(-50%); + width: 36px; + height: 4px; + border-radius: 2px; + background: var(--border-color); + } + #comment-list { + flex: 1; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding: 15px; + overscroll-behavior: contain; + } .header h1 { font-size: 18px; color: var(--text-primary); @@ -333,8 +358,8 @@
-
最热
-
最新
+
最热
+
最新
@@ -370,6 +395,8 @@ const bookID = '{{.BookID}}'; const para = {{.Para}}; let currentSort = {{.Sort}}; + // 服务端注入的绝对地址:弹窗 WebView(loadDataWithBaseURL) 中相对路径解析不可靠 + const API_BASE = '{{.BaseURL}}'; const commentList = document.getElementById('comment-list'); const imageViewer = document.getElementById('imageViewer'); @@ -380,7 +407,7 @@ // 已加载回复的评论 const loadedReplies = new Set(); - // URL 构建 + // URL 构建(绝对地址,兼容弹窗 WebView) function buildReviewURL(params) { const baseParams = { item_id: itemID, @@ -388,7 +415,7 @@ para: para }; const query = new URLSearchParams({...baseParams, ...params}); - return '/review?' + query.toString(); + return API_BASE + '/review?' + query.toString(); } // 时间格式化 @@ -429,11 +456,11 @@ }; } - // 滚动信息获取 + // 滚动信息获取(评论区为独立滚动容器) function getScrollInfo() { - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - const scrollHeight = document.documentElement.scrollHeight; - const clientHeight = document.documentElement.clientHeight; + const scrollTop = commentList.scrollTop; + const scrollHeight = commentList.scrollHeight; + const clientHeight = commentList.clientHeight; return { scrollTop, scrollHeight, @@ -443,11 +470,11 @@ } // 切换排序 - async function switchSort(newSort) { + async function switchSort(newSort, tab) { if (currentSort === newSort) return; - - document.querySelectorAll('.sort-tab').forEach(tab => tab.classList.remove('active')); - event.target.closest('.sort-tab').classList.add('active'); + + document.querySelectorAll('.sort-tab').forEach(t => t.classList.remove('active')); + if (tab) tab.classList.add('active'); commentList.innerHTML = LOADING_HTML; @@ -479,7 +506,7 @@ } loadedReplies.clear(); - window.scrollTo({ top: 0, behavior: 'smooth' }); + commentList.scrollTo({ top: 0, behavior: 'smooth' }); } else { commentList.innerHTML = '
加载失败,请重试
'; commentIndex = 0; @@ -751,25 +778,25 @@ // 回到顶部 function scrollToTop() { - window.scrollTo({ + commentList.scrollTo({ top: 0, behavior: 'smooth' }); } - - // 滚动监听 + + // 滚动监听(挂在评论容器上,顶栏固定不参与滚动) const handleScroll = debounce(() => { const scroll = getScrollInfo(); - + // 显示/隐藏回到顶部按钮 backToTopBtn.classList.toggle('show', scroll.scrollTop > 300); - + // 距离底部 200px 时触发加载 if (scroll.distanceFromBottom <= 200) { loadMoreComments(); } }, 100); - - window.addEventListener('scroll', handleScroll, { passive: true }); + + commentList.addEventListener('scroll', handleScroll, { passive: true });