详情页评论改推荐列表

This commit is contained in:
谢宇宁
2026-09-29 16:45:18 +07:00
parent d546160cc7
commit 41a45c8975
16 changed files with 160 additions and 1207 deletions
-43
View File
@@ -1,43 +0,0 @@
package web
import (
"net/http"
"strconv"
"videoweb/app/service/commentser"
"github.com/gin-gonic/gin"
)
// CommentList BFF:代理 GET /api/app/comment/list。
// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/get_api_app_comment_list
func CommentList(c *gin.Context) {
objectID, _ := strconv.ParseInt(c.Query("objectId"), 10, 64)
objectType, _ := strconv.Atoi(c.Query("objectType"))
pageNum, _ := strconv.Atoi(c.Query("pageNum"))
pageSize, _ := strconv.Atoi(c.Query("pageSize"))
sortOrder, _ := strconv.Atoi(c.Query("sortOrder"))
if objectID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "objectId required",
"tip": "缺少评论对象",
"data": nil,
})
return
}
data := commentser.FetchListOrEmpty(c, commentser.ListQuery{
ObjectID: objectID,
ObjectType: objectType,
PageNum: pageNum,
PageSize: pageSize,
SortOrder: sortOrder,
})
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "ok",
"tip": "成功",
"data": data,
})
}
// CommentSend BFF:代理 POST /api/app/comment/send。
+4 -6
View File
@@ -10,14 +10,12 @@ import (
func Help(c *gin.Context) {
HTMLOK(c, "help.html", gin.H{
"faqItems": []seoser.FAQItem{
{Question: "黄果短剧网页版可以看全集吗?", Answer: "可以。免费集直接观看,付费集可用金币按集解锁,解锁后永久有效。"},
{Question: "解锁过的剧集还需要再付费吗?", Answer: "不需要。已解锁的剧集可在登录后继续观看,无需重复付费。"},
{Question: "如何充值金币?", Answer: "登录后进入个人中心,选择金币充值,即可查看可用的充值方式。"},
{Question: "支持哪些支付方式?", Answer: "可用的支付方式会在充值页面列出,请以页面显示为准。"},
{Question: "忘记密码怎么办?", Answer: "请通过意见反馈联系我们,提交账号信息后由客服协助处理。"},
{Question: "黄果短剧网页版可以看全集吗?", Answer: "免费集可直接在网页观看;标记为 App 专享的剧集,下载黄果短剧 App 即可免费观看完整内容。"},
{Question: "网页版需要注册账号吗?", Answer: "不需要。网页版无需注册或登录,打开即可浏览和观看。"},
{Question: "如何下载黄果短剧 App?", Answer: "点击页面顶部或播放页的「下载 App」按钮,按提示完成安装即可。"},
{Question: "网站打不开怎么办?", Answer: "请收藏页脚的最新地址、备用地址与海外永久地址,任一地址均可访问。"},
{Question: "内容多久更新一次?", Answer: "黄果短剧持续更新内容,欢迎关注首页的新剧和最近上新。"},
{Question: "如何联系客服或洽谈合作?", Answer: "请发送邮件至 support@huanguoai.net,我们会尽快回复。"},
{Question: "如何注销账号?", Answer: "请通过意见反馈或客服邮箱提交注销申请。"},
},
})
}
+52 -118
View File
@@ -2,7 +2,6 @@ package web
import (
"strings"
"videoweb/app/service/commentser"
"videoweb/app/service/playletser"
"videoweb/app/service/videoser"
@@ -76,20 +75,6 @@ func Play(c *gin.Context) {
})
}
commentObjectID := playlet.ID
commentObjectType := commentser.ObjectTypePlaylet
if chapterID > 0 {
commentObjectID = chapterID
commentObjectType = commentser.ObjectTypeChapter
}
comments := commentser.FetchListOrEmpty(c, commentser.ListQuery{
ObjectID: commentObjectID,
ObjectType: commentObjectType,
PageNum: 1,
PageSize: 4,
SortOrder: commentser.SortHot,
})
canWatch := false
episode := 0
if chapter != nil {
@@ -119,45 +104,36 @@ func Play(c *gin.Context) {
}
data := gin.H{
"title": title,
"navActive": "list",
"seoInfo": seo,
"playlet": playlet,
"chapter": chapter,
"chapterID": chapterID,
"episode": episode,
"canWatch": canWatch,
"mainURL": mainURL,
"previewURL": previewURL,
"hasPreview": previewURL != "",
"nextChapterID": playletser.NextChapterID(playlet, chapterID),
"advitise": playlet.Advitise,
"related": playlet.RelatedRecommendData,
"guessLike": guessLike.List,
"guessPage": guessLike.PageNum,
"guessPageSize": guessLike.PageSize,
"guessTotal": guessLike.TotalCount,
"hotList": hotList,
"hotMoreURL": playletser.AllListURL("all", 0, 0, 2, 1),
"comments": comments.List,
"commentTotal": comments.TotalCount,
"commentPage": comments.PageNum,
"commentSize": comments.PageSize,
"commentObjectID": commentObjectID,
"commentObjectType": commentObjectType,
"title": title,
"navActive": "list",
"seoInfo": seo,
"playlet": playlet,
"chapter": chapter,
"chapterID": chapterID,
"episode": episode,
"canWatch": canWatch,
"mainURL": mainURL,
"previewURL": previewURL,
"hasPreview": previewURL != "",
"nextChapterID": playletser.NextChapterID(playlet, chapterID),
"advitise": playlet.Advitise,
"related": playlet.RelatedRecommendData,
"guessLike": guessLike.List,
"guessPage": guessLike.PageNum,
"guessPageSize": guessLike.PageSize,
"guessTotal": guessLike.TotalCount,
"hotList": hotList,
"hotMoreURL": playletser.AllListURL("all", 0, 0, 2, 1),
"playResponse": gin.H{
"playlet": playlet,
"chapter": chapter,
"chapterID": chapterID,
"canWatch": canWatch,
"mainURL": mainURL,
"previewURL": previewURL,
"hasPreview": previewURL != "",
"guessLike": guessLike,
"hotList": hotList,
"comments": comments,
"commentObjectID": commentObjectID,
"commentObjectType": commentObjectType,
"playlet": playlet,
"chapter": chapter,
"chapterID": chapterID,
"canWatch": canWatch,
"mainURL": mainURL,
"previewURL": previewURL,
"hasPreview": previewURL != "",
"guessLike": guessLike,
"hotList": hotList,
},
}
data["canonicalPath"] = playletser.PlayPath(playlet.Slug, playlet.ID)
@@ -192,27 +168,6 @@ func SeriesPlay(c *gin.Context) {
videoURL := strings.TrimSpace(video.Video.URL)
canWatch := videoURL != ""
commentObjectID := video.ID
// 视频评论:objectType=20(swagger:10-短剧 11-短剧剧集 20-视频)
commentObjectType := commentser.ObjectTypeVideo
comments := commentser.FetchListOrEmpty(c, commentser.ListQuery{
ObjectID: commentObjectID,
ObjectType: commentObjectType,
PageNum: 1,
PageSize: 4,
SortOrder: commentser.SortHot,
})
if comments.PageNum <= 0 {
comments.PageNum = 1
}
if comments.PageSize <= 0 {
comments.PageSize = 4
}
commentTotal := comments.TotalCount
if commentTotal <= 0 && video.CountComment > 0 {
commentTotal = video.CountComment
}
hot := playletser.FetchAllListOrEmpty(c, playletser.AllListQuery{
PageNum: 1,
PageSize: 8,
@@ -245,51 +200,30 @@ func SeriesPlay(c *gin.Context) {
}
data := gin.H{
"title": title,
"navActive": "aiSeries",
"seoInfo": video.SeoInfo,
"video": video,
"videoID": video.ID,
"videoSlug": video.Slug,
"cover": cover,
"videoURL": videoURL,
"canWatch": canWatch,
"guessLike": guessLike.List,
"guessPage": guessLike.PageNum,
"guessPageSize": guessLike.PageSize,
"guessTotal": guessLike.TotalCount,
"guessPlayletID": guessPlayletID,
"advitise": video.Advitise,
"comments": comments.List,
"commentTotal": commentTotal,
"commentPage": comments.PageNum,
"commentSize": comments.PageSize,
"commentObjectID": commentObjectID,
"commentObjectType": commentObjectType,
"hotList": hotList,
"hotMoreURL": playletser.AllListURL("all", 0, 0, 2, 1),
"title": title,
"navActive": "aiSeries",
"seoInfo": video.SeoInfo,
"video": video,
"videoID": video.ID,
"videoSlug": video.Slug,
"cover": cover,
"videoURL": videoURL,
"canWatch": canWatch,
"guessLike": guessLike.List,
"guessPage": guessLike.PageNum,
"guessPageSize": guessLike.PageSize,
"guessTotal": guessLike.TotalCount,
"guessPlayletID": guessPlayletID,
"advitise": video.Advitise,
"hotList": hotList,
"hotMoreURL": playletser.AllListURL("all", 0, 0, 2, 1),
"videoResponse": gin.H{
"video": video,
"videoURL": videoURL,
"canWatch": canWatch,
"comments": comments,
"hotList": hotList,
"guessLike": guessLike,
"guessPlayletID": guessPlayletID,
"commentObjectID": commentObjectID,
"commentObjectType": commentObjectType,
},
"commentDebug": gin.H{
"path": "/api/app/comment/list",
"objectId": commentObjectID,
"objectType": commentObjectType, // 20-视频
"pageNum": comments.PageNum,
"pageSize": comments.PageSize,
"totalCount": commentTotal,
"list": comments.List,
"apis": []string{
"GET /api/app/comment/list",
},
"video": video,
"videoURL": videoURL,
"canWatch": canWatch,
"hotList": hotList,
"guessLike": guessLike,
"guessPlayletID": guessPlayletID,
},
"playDebug": gin.H{
"canWatch": canWatch,
+2
View File
@@ -1,6 +1,8 @@
#!/bin/bash
set -e
echo "开始"
# 本机 Go 装在 ~/sdk/go,未加入 PATH 时在这里补上
command -v go >/dev/null 2>&1 || export PATH="$HOME/sdk/go/bin:$PATH"
rm -rf ./web_huangguo2_h5 ./web_huangguo2_h5.tar.gz
GOOS=linux GOARCH=amd64 go build -o web_huangguo2_h5
tar zvcf web_huangguo2_h5.tar.gz \
-5
View File
@@ -8,7 +8,6 @@ import (
"strings"
"videoweb/app/api/web"
"videoweb/app/appg"
"videoweb/app/service/commentser"
"videoweb/app/service/playletser"
"videoweb/app/service/seoser"
"videoweb/app/service/translationser"
@@ -62,9 +61,6 @@ func setGinMode() {
func setupEngine(engine *gin.Engine) {
tplFuncs := playletser.TemplateFuncs()
for name, fn := range commentser.TemplateFuncs() {
tplFuncs[name] = fn
}
tplFuncs["InJSONScript"] = inJSONScript
tplFuncs["navHref"] = translationser.NavHref
tplFuncs["navIsDownload"] = translationser.NavIsDownload
@@ -209,7 +205,6 @@ func registerRoutes(r *gin.Engine) {
// 播放页 BFF
r.GET("/api/playlet/guess-like/list", web.GuessLikeList)
r.GET("/api/comment/list", web.CommentList)
r.GET("/api/web/search/suggest", web.SearchSuggest)
// 搜索引擎抓取、发现与站点所有权验证。
-135
View File
@@ -1,135 +0,0 @@
package commentser
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
"videoweb/app/service/upstream"
"videoweb/pub/common/log"
"github.com/gin-gonic/gin"
)
const (
listPath = "/api/app/comment/list"
defaultCommentSize = 10
)
// ListQuery 评论列表查询参数。
type ListQuery struct {
ObjectID int64
ObjectType int
PageNum int
PageSize int
SortOrder int
}
// Normalize 校正分页与排序默认值。
func (q *ListQuery) Normalize() {
if q.PageNum <= 0 {
q.PageNum = 1
}
if q.PageSize <= 0 {
q.PageSize = defaultCommentSize
}
if q.PageSize > 50 {
q.PageSize = 50
}
q.ObjectType = NormalizeObjectType(q.ObjectType)
if q.SortOrder != SortLatest {
q.SortOrder = SortHot
}
}
// FetchList 分页查询评论。
func FetchList(c *gin.Context, q ListQuery) (*GetCommentListResp, error) {
q.Normalize()
if q.ObjectID <= 0 {
return nil, fmt.Errorf("comment list invalid objectId")
}
params := url.Values{}
params.Set("objectId", strconv.FormatInt(q.ObjectID, 10))
params.Set("objectType", strconv.Itoa(q.ObjectType))
params.Set("pageNum", strconv.Itoa(q.PageNum))
params.Set("pageSize", strconv.Itoa(q.PageSize))
params.Set("sortOrder", strconv.Itoa(q.SortOrder))
requestPath := listPath + "?" + params.Encode()
env, err := fetchListOnce(c, requestPath)
if err != nil {
return nil, err
}
if env.Code != 200 {
return nil, fmt.Errorf("comment list code=%d msg=%s", env.Code, env.Msg)
}
return &env.Data, nil
}
func fetchListOnce(c *gin.Context, path string) (*APIEnvelope[GetCommentListResp], error) {
var env APIEnvelope[GetCommentListResp]
if err := upstream.GetJSON(c, path, &env); err != nil {
return nil, err
}
return &env, nil
}
// FetchListOrEmpty 失败时返回空列表。
func FetchListOrEmpty(c *gin.Context, q ListQuery) *GetCommentListResp {
data, err := FetchList(c, q)
if err != nil {
log.Error("fetch comment list failed", log.E(err))
return &GetCommentListResp{
ObjectID: q.ObjectID,
PageNum: q.PageNum,
PageSize: q.PageSize,
}
}
if data == nil {
return &GetCommentListResp{
ObjectID: q.ObjectID,
PageNum: q.PageNum,
PageSize: q.PageSize,
}
}
return data
}
// ParseSortOrder 解析站点评论排序(1 热门 / 2 最新)。
func ParseSortOrder(s string) int {
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil || n != SortLatest {
return SortHot
}
return SortLatest
}
// FormatCreatedAt 展示用时间格式化(非字段映射)。
func FormatCreatedAt(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
layouts := []string{
time.RFC3339Nano,
time.RFC3339,
"2006-01-02T15:04:05Z07:00",
"2006-01-02 15:04:05",
}
for _, layout := range layouts {
if t, err := time.Parse(layout, raw); err == nil {
return t.Local().Format("2006/1/2 15:04")
}
}
return raw
}
// AvatarOrDefault 头像空时回退默认图。
func AvatarOrDefault(avatar string) string {
avatar = strings.TrimSpace(avatar)
if avatar == "" {
return "/static/images/profile/avatar.png"
}
return avatar
}
-11
View File
@@ -1,11 +0,0 @@
package commentser
import "html/template"
// TemplateFuncs 评论展示辅助。
func TemplateFuncs() template.FuncMap {
return template.FuncMap{
"formatCommentTime": FormatCreatedAt,
"commentAvatar": AvatarOrDefault,
}
}
-83
View File
@@ -1,83 +0,0 @@
package commentser
// CommentSendReq 对应 swagger proto.CommentSendReq
// parentsId 为 0 发一级评论,大于 0 发二级评论;replyCommentId 为实际被回复的评论。
type CommentSendReq struct {
ObjectID int64 `json:"objectId"`
ObjectType int `json:"objectType"`
Text string `json:"text"`
ParentsID int64 `json:"parentsId"`
ReplyCommentID int64 `json:"replyCommentId"`
}
// CommentLikeSetReq 对应 swagger proto.CommentLikeSetReq
type CommentLikeSetReq struct {
CommentID int64 `json:"commentId"`
IsLike bool `json:"isLike"`
}
// CommentLikeSetResp 对应 swagger proto.CommentLikeSetResp
type CommentLikeSetResp struct {
CommentID int64 `json:"commentId"`
CountLike int64 `json:"countLike"`
IsLike int `json:"isLike"`
}
// CommentInfo 对应 swagger proto.CommentInfo
type CommentInfo struct {
ID int64 `json:"id"`
ObjectID int64 `json:"objectId"`
ObjectType int `json:"objectType"`
Text string `json:"text"`
UserID int64 `json:"userId"`
UserName string `json:"userName"`
UserAvatar string `json:"userAvatar"`
CountLike int64 `json:"countLike"`
CommentCount int64 `json:"commentCount"`
IsLike int `json:"isLike"`
CreatedAt string `json:"createdAt"`
Level int `json:"level,omitempty"`
ParentsID int64 `json:"parentsId,omitempty"`
ReplyCommentID int64 `json:"replyCommentId,omitempty"`
ReplyUserName string `json:"replyUserName,omitempty"`
Children []CommentInfo `json:"children,omitempty"`
}
// GetCommentListResp 对应 swagger proto.GetCommentListResp
type GetCommentListResp struct {
List []CommentInfo `json:"list"`
ObjectID int64 `json:"objectId"`
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
TotalCount int64 `json:"totalCount"`
}
// APIEnvelope 上游统一响应壳
type APIEnvelope[T any] struct {
Code int `json:"code"`
Msg string `json:"msg"`
Tip string `json:"tip"`
Data T `json:"data"`
}
const (
// ObjectTypePlaylet 短剧评论
ObjectTypePlaylet = 10
// ObjectTypeChapter 短剧剧集评论
ObjectTypeChapter = 11
// ObjectTypeVideo 视频评论
ObjectTypeVideo = 20
SortHot = 1 // 热门
SortLatest = 2 // 最新
)
// NormalizeObjectType 校正评论对象类型:10-短剧 11-短剧剧集 20-视频。
func NormalizeObjectType(objectType int) int {
switch objectType {
case ObjectTypeChapter, ObjectTypeVideo:
return objectType
default:
return ObjectTypePlaylet
}
}
+29
View File
@@ -1857,3 +1857,32 @@ a.play-synopsis__tag:hover {
line-height: 16px;
}
}
/* 猜你喜欢放在左栏(原评论位置),右栏为热门榜单 */
.play-dual > .play-like {
flex: 1;
min-width: 0;
padding: 0;
}
.play-dual .play-like__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 20px 16px;
}
.play-dual .card--play {
width: auto;
min-width: 0;
}
@media (max-width: 768px) {
.play-dual > .play-like {
padding: 0;
}
.play-dual .play-like__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px 8px;
}
}
-396
View File
@@ -1,396 +0,0 @@
/* 播放页评论区(含输入框、列表、内联回复) */
.play-comments {
flex: 1 1 0;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.play-comments__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px;
}
.play-comments__title {
margin: 0;
font-size: 22px;
font-weight: 600;
line-height: 30px;
}
.play-comments__count {
color: var(--text-dim);
font-size: 14px;
line-height: 22px;
}
.play-composer {
display: flex;
align-items: center;
gap: 12px;
}
.play-composer__avatar {
width: 40px;
height: 40px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background: #232328;
border-radius: 999px;
overflow: hidden;
}
.play-composer__avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.play-composer__field {
flex: 1 1 0;
min-width: 0;
height: 44px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--line);
border-radius: var(--radius-md);
}
.play-composer__input {
flex: 1 1 0;
min-width: 0;
height: 100%;
padding: 0;
border: 0;
outline: none;
background: transparent;
color: var(--text);
font: inherit;
font-size: 15px;
line-height: 24px;
}
.play-composer__input::placeholder {
color: var(--text-dim);
}
.play-composer__count {
flex-shrink: 0;
color: var(--text-dim);
font-size: 14px;
line-height: 20px;
}
.play-composer__login,
.play-composer__send {
flex-shrink: 0;
}
.play-composer__send:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.play-comment-list {
margin: 0;
padding: 0;
list-style: none;
}
.play-comment {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 0;
border-bottom: 1px solid var(--line-soft);
}
.play-comment:last-child {
border-bottom: 0;
}
.play-comment__avatar {
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 999px;
object-fit: cover;
}
.play-comment__body {
flex: 1 1 0;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.play-comment__meta {
display: flex;
align-items: center;
gap: 10px;
}
.play-comment__name {
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.play-comment__time {
color: var(--text-dim);
font-size: 12px;
line-height: 18px;
}
.play-comment__text {
margin: 0;
color: var(--text-muted);
font-size: 14px;
line-height: 22px;
}
.play-comment--empty {
justify-content: center;
align-items: center;
border-bottom: 0;
}
.play-comment--empty .play-comment__text {
padding: 12px 0;
color: var(--muted, #888);
text-align: center;
width: 100%;
}
.play-comment__reply {
padding: 10px 14px;
color: var(--text-muted);
font-size: 13px;
line-height: 20px;
background: rgba(255, 255, 255, 0.04);
border-radius: var(--radius-sm);
}
.play-comment__actions {
display: flex;
align-items: center;
gap: 20px;
}
.play-comment__action {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0;
border: 0;
background: transparent;
color: var(--text-dim);
font: inherit;
font-size: 12px;
line-height: 18px;
cursor: pointer;
}
.play-comment__action:disabled {
cursor: default;
opacity: 0.7;
}
.play-comment__action.is-active {
color: var(--brand, #e9c977);
}
.play-comment__action.is-active .icon {
opacity: 1;
filter: none;
}
.play-comment__action .icon {
width: 14px;
height: 14px;
opacity: 0.75;
}
.play-comment__inline {
width: 100%;
margin-top: 4px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--line-soft);
border-radius: var(--radius-sm);
box-sizing: border-box;
}
.play-comment__inline-hint {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--text-dim);
font-size: 12px;
line-height: 18px;
}
.play-comment__inline-hint button {
flex-shrink: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--brand, #e9c977);
font: inherit;
font-size: 12px;
line-height: 18px;
cursor: pointer;
}
.play-comment__inline-row {
display: flex;
align-items: center;
gap: 8px;
}
.play-comment__inline-input {
flex: 1 1 0;
min-width: 0;
height: 36px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(0, 0, 0, 0.35);
color: var(--text);
font: inherit;
font-size: 13px;
line-height: 20px;
outline: none;
}
.play-comment__inline-input::placeholder {
color: var(--text-dim);
}
.play-comment__inline-input:focus,
.play-comment__inline-input:focus-visible {
border-color: rgba(233, 201, 119, 0.55);
}
.play-comment__inline-send {
flex-shrink: 0;
height: 36px;
padding: 0 14px;
border: 0;
border-radius: 999px;
background: var(--brand, #e9c977);
color: #141417;
font: inherit;
font-size: 13px;
font-weight: 600;
line-height: 20px;
cursor: pointer;
}
.play-comment__inline-send:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.play-comments__more {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding-top: 12px;
color: var(--text-muted);
font-size: 14px;
line-height: 22px;
background: transparent;
border: 0;
cursor: pointer;
}
.play-comments__more[hidden] {
display: none;
}
.play-comments__more:disabled {
opacity: 0.6;
cursor: default;
}
.play-comments__more .icon {
width: 16px;
height: 16px;
transform: rotate(90deg);
}
@media (max-width: 768px) {
.play-comments {
width: 100%;
padding: 0;
gap: 16px;
}
.play-comments__head {
gap: 14px;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.play-comments__head::-webkit-scrollbar {
display: none;
}
.play-comments__title {
flex-shrink: 0;
font-size: 22px;
line-height: 30px;
}
.play-comments__count {
flex-shrink: 0;
}
.play-composer {
width: 100%;
gap: 12px;
}
.play-composer__avatar {
flex-shrink: 0;
}
.play-composer__field {
min-width: 0;
}
.play-composer__login,
.play-composer__send {
flex-shrink: 0;
height: 40px;
padding: 0 16px;
}
.play-comment-list {
width: 100%;
}
.play-comments__more {
width: 100%;
}
}
+1 -1
View File
@@ -702,7 +702,7 @@
if (btn.getAttribute("data-play-bound") === "1") return;
btn.setAttribute("data-play-bound", "1");
btn.addEventListener("click", function (event) {
// 预告片免登录 / 免解锁;仅正片未解锁时拦充值
// 预告片直接播放;仅正片为 App 专享时弹下载提示
if (
btn.hasAttribute("data-play-coin-open") &&
!isPreviewMode()
-260
View File
@@ -1,260 +0,0 @@
// 播放页评论:只读列表 + 「查看更多评论」追加翻页
(function () {
var commentsRoot = document.querySelector("[data-play-comments]");
if (!commentsRoot) return;
var listEl = document.querySelector("[data-comment-list]");
var totalEl = document.querySelector("[data-comment-total-label]");
var moreBtn = document.querySelector("[data-comment-more]");
var loadingMore = false;
var PAGE_SIZE = 4;
function toast(message, type) {
if (typeof window.showToast === "function") {
window.showToast(message, type);
return;
}
}
function toInt(v, fallback) {
var n = parseInt(v, 10);
return isNaN(n) ? fallback || 0 : n;
}
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function getPageSize() {
return Math.max(
1,
toInt(commentsRoot.getAttribute("data-comment-size"), PAGE_SIZE)
);
}
function getPage() {
return Math.max(
1,
toInt(commentsRoot.getAttribute("data-comment-page"), 1)
);
}
function getLoaded() {
if (!listEl) return 0;
return listEl.querySelectorAll(".play-comment:not(.play-comment--empty)").length;
}
function setPagerState(page, loaded, total) {
if (page != null) commentsRoot.setAttribute("data-comment-page", String(page));
if (loaded != null) commentsRoot.setAttribute("data-comment-loaded", String(loaded));
if (total != null) commentsRoot.setAttribute("data-comment-total", String(total));
syncMoreButton(total != null ? total : toInt(commentsRoot.getAttribute("data-comment-total"), 0));
}
function syncMoreButton(total) {
if (!moreBtn) return;
var loaded = getLoaded();
var hasMore = total > loaded;
if (hasMore) {
moreBtn.hidden = false;
moreBtn.removeAttribute("hidden");
} else {
moreBtn.hidden = true;
moreBtn.setAttribute("hidden", "");
}
}
function formatTime(raw) {
var s = String(raw || "").trim();
if (!s) return "刚刚";
var d = new Date(s);
if (isNaN(d.getTime())) return s;
return (
d.getFullYear() +
"/" +
(d.getMonth() + 1) +
"/" +
d.getDate() +
" " +
String(d.getHours()).padStart(2, "0") +
":" +
String(d.getMinutes()).padStart(2, "0")
);
}
function renderCommentItem(item) {
item = item || {};
var avatar = String(item.userAvatar || "").trim();
var name = escapeHtml(item.userName || "匿名用户");
var text = escapeHtml(item.text || "");
var time = escapeHtml(formatTime(item.createdAt));
var like = toInt(item.countLike, 0);
var avatarHtml = avatar
? '<img class="play-comment__avatar lazy" src="/static/images/placeholder.png?v=2" data-src="' +
escapeHtml(avatar) +
'" alt="" loading="lazy" decoding="async">'
: '<img class="play-comment__avatar" src="/static/images/placeholder.png?v=2" data-src="/static/images/profile/avatar.png" alt="" loading="lazy" decoding="async">';
var children = Array.isArray(item.children) ? item.children : [];
var replies = children
.map(function (child) {
var cName = escapeHtml(child.userName || "用户");
var replyTo = child.replyUserName
? " 回复 " + escapeHtml(child.replyUserName)
: "";
return (
'<div class="play-comment__reply">' +
cName +
replyTo +
":" +
escapeHtml(child.text || "") +
"</div>"
);
})
.join("");
var commentId = escapeHtml(item.id || "");
return (
'<li class="play-comment" data-comment-id="' +
commentId +
'">' +
avatarHtml +
'<div class="play-comment__body">' +
'<div class="play-comment__meta">' +
'<span class="play-comment__name">' +
name +
"</span>" +
'<span class="play-comment__time">' +
time +
"</span>" +
"</div>" +
'<p class="play-comment__text">' +
text +
"</p>" +
replies +
'<div class="play-comment__actions">' +
'<span class="play-comment__action">' +
'<img class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/heart.svg" alt="" loading="lazy" decoding="async">' +
"<span>" +
escapeHtml(like) +
"</span>" +
"</span>" +
"</div></div></li>"
);
}
function getLoadedIds() {
var ids = Object.create(null);
if (!listEl) return ids;
listEl.querySelectorAll(".play-comment[data-comment-id]").forEach(function (el) {
var id = String(el.getAttribute("data-comment-id") || "").trim();
if (id) ids[id] = true;
});
return ids;
}
function updateTotal(n) {
if (!totalEl) return;
totalEl.textContent = n > 0 ? n + " 条" : "暂无评论";
}
// 下一页追加到现有列表底部,不替换已展示评论。
function appendList(list, totalCount) {
if (!listEl) return;
list = Array.isArray(list) ? list : [];
var total =
typeof totalCount === "number"
? totalCount
: toInt(commentsRoot.getAttribute("data-comment-total"), 0);
if (typeof totalCount === "number") {
updateTotal(totalCount);
}
if (!list.length) {
setPagerState(getPage(), getLoaded(), total);
return;
}
var empty = listEl.querySelector(".play-comment--empty");
if (empty) empty.remove();
var existing = getLoadedIds();
var fresh = list.filter(function (item) {
var id = String((item && item.id) || "").trim();
return id && !existing[id];
});
if (!fresh.length) {
setPagerState(getPage(), getLoaded(), total);
return;
}
var wrap = document.createElement("ul");
wrap.innerHTML = fresh.map(renderCommentItem).join("");
var nodes = [];
while (wrap.firstChild) {
nodes.push(wrap.firstChild);
listEl.appendChild(wrap.firstChild);
}
if (typeof window.initLazyImages === "function") {
nodes.forEach(function (node) {
if (node && node.nodeType === 1) window.initLazyImages(node);
});
window.initLazyImages(listEl);
}
setPagerState(getPage(), getLoaded(), total);
}
function fetchCommentPage(pageNum) {
if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") {
return Promise.reject(new Error("评论服务未就绪"));
}
var objectId = toInt(commentsRoot.getAttribute("data-object-id"), 0);
var objectType = toInt(commentsRoot.getAttribute("data-object-type"), 10);
if (objectId <= 0) return Promise.reject(new Error("缺少评论对象"));
var pageSize = getPageSize();
return window.HttpRequest.get("/api/comment/list", {
params: {
objectId: objectId,
objectType: objectType,
pageNum: pageNum,
pageSize: pageSize,
sortOrder: 1,
},
}).then(function (res) {
if (!res || Number(res.code) !== 200 || !res.data) {
throw new Error((res && (res.tip || res.msg)) || "加载失败");
}
return res.data;
});
}
function loadMoreComments() {
if (loadingMore || !moreBtn) return;
var nextPage = getPage() + 1;
loadingMore = true;
moreBtn.disabled = true;
fetchCommentPage(nextPage)
.then(function (data) {
var list = data.list || [];
var total = toInt(data.totalCount, 0);
commentsRoot.setAttribute("data-comment-page", String(nextPage));
// 只追加,绝不 renderList 整表替换
appendList(list, total);
})
.catch(function (err) {
toast((err && err.message) || "加载失败,请稍后重试", "error");
})
.finally(function () {
loadingMore = false;
if (moreBtn) moreBtn.disabled = false;
});
}
if (moreBtn) {
moreBtn.addEventListener("click", function (event) {
event.preventDefault();
loadMoreComments();
});
}
})();
-69
View File
@@ -1,69 +0,0 @@
{{define "playComments"}}
<link rel="stylesheet" href="/static/css/playComments.css">
{{/* 播放页评论区:首屏 4 条,超出显示「查看更多评论」客户端翻页 */}}
{{$c := .chapter}}
{{$pageSize := .commentSize}}
{{$total := .commentTotal}}
{{$shown := len .comments}}
<div
class="play-comments"
data-play-comments
data-comment-page="{{if .commentPage}}{{.commentPage}}{{else}}1{{end}}"
data-comment-size="{{if gt $pageSize 0}}{{$pageSize}}{{else}}4{{end}}"
data-comment-total="{{$total}}"
data-comment-loaded="{{$shown}}"
data-object-id="{{.commentObjectID}}"
data-object-type="{{.commentObjectType}}"
>
<div class="play-comments__head">
<h2 class="play-comments__title">评论</h2>
<span class="play-comments__count" data-comment-total-label>{{if gt $total 0}}{{$total}} 条{{else if and $c (gt $c.CountComment 0)}}{{$c.CountComment}} 条{{else if and .video (gt .video.CountComment 0)}}{{.video.CountComment}} 条{{else}}暂无评论{{end}}</span>
</div>
<ul class="play-comment-list" data-comment-list>
{{range .comments}}
<li class="play-comment" data-comment-id="{{.ID}}">
{{$avatar := commentAvatar .UserAvatar}}
{{if .UserAvatar}}
<img decoding="async" loading="lazy" class="play-comment__avatar lazy" src="/static/images/placeholder.png?v=2" data-src="{{$avatar}}" alt="评论用户头像">
{{else}}
<img decoding="async" loading="lazy" class="play-comment__avatar" src="/static/images/placeholder.png?v=2" data-src="{{$avatar}}" alt="评论用户头像">
{{end}}
<div class="play-comment__body">
<div class="play-comment__meta">
<span class="play-comment__name">{{if .UserName}}{{.UserName}}{{else}}匿名用户{{end}}</span>
<span class="play-comment__time">{{formatCommentTime .CreatedAt}}</span>
</div>
<p class="play-comment__text">{{.Text}}</p>
{{range .Children}}
<div class="play-comment__reply">
{{if .UserName}}{{.UserName}}{{else}}用户{{end}}{{if .ReplyUserName}} 回复 {{.ReplyUserName}}{{end}}:{{.Text}}
</div>
{{end}}
<div class="play-comment__actions">
<span class="play-comment__action">
<img decoding="async" loading="lazy" class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/heart.svg" alt="点赞数">
<span>{{.CountLike}}</span>
</span>
</div>
</div>
</li>
{{else}}
<li class="play-comment play-comment--empty">
<p class="play-comment__text">暂无评论</p>
</li>
{{end}}
</ul>
<button
class="play-comments__more"
type="button"
data-comment-more
{{if le $total $shown}}hidden{{end}}
>
查看更多评论
<img decoding="async" loading="lazy" class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/chevron-right.svg" alt="前往下一项">
</button>
</div>
<script defer src="/static/js/pages/playComments.js?v=5"></script>
{{end}}
-4
View File
@@ -49,10 +49,6 @@
<summary>如何联系客服或洽谈合作?<span class="help-faq__chevron" aria-hidden="true"></span></summary>
<p>请发送邮件至 support@huanguoai.net,我们会尽快回复。</p>
</details>
<details class="help-faq__item">
<summary>如何注销账号?<span class="help-faq__chevron" aria-hidden="true"></span></summary>
<p>请通过意见反馈或客服邮箱提交注销申请。</p>
</details>
</section>
<aside class="help-aside">
+36 -38
View File
@@ -17,7 +17,7 @@
<script src="/static/js/common/compat.js?v=4"></script>
<link rel="stylesheet" href="/static/css/base.css?v=2">
<link rel="stylesheet" href="/static/common/DPlayer.min.css">
<link rel="stylesheet" href="/static/css/play.css?v=26">
<link rel="stylesheet" href="/static/css/play.css?v=27">
<script defer src="/static/common/hls.min.js"></script>
<script defer src="/static/common/DPlayer.min.js?v=2"></script>
</head>
@@ -331,8 +331,40 @@
</section>
{{end}}
<section class="play-dual" aria-label="评论与热榜">
{{template "playComments" .}}
<section class="play-dual" aria-label="推荐与热榜">
{{if .guessLike}}
<section class="play-like" aria-label="猜你喜欢">
<div class="play-like__head">
<div class="play-like__titles">
<h2 class="play-like__title">猜你喜欢</h2>
<span class="play-like__desc only-pc">根据本剧题材推荐</span>
</div>
<button
type="button"
class="play-like__refresh"
data-guess-refresh
data-playlet-id="{{.playlet.ID}}"
data-page="{{.guessPage}}"
data-page-size="{{if .guessPageSize}}{{.guessPageSize}}{{else}}6{{end}}"
data-total="{{.guessTotal}}"
>
换一批
<img decoding="async" loading="lazy" class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/chevron-right.svg" alt="前往下一项">
</button>
</div>
<div class="play-like__grid" id="play-like" data-guess-grid>
{{range .guessLike}}
<article class="card card--play">
<a class="card__cover lazy" href="{{playURL .Slug .ID}}" style="{{cssCoverPlaceholder}}" data-src="{{.Cover}}">
{{if .TotalEpisode}}<span class="card__ep">全 {{.TotalEpisode}} 集</span>{{else}}<span class="card__ep">{{updateStatusText .UpdateStatus}}</span>{{end}}
</a>
<h3 class="card__title"><a href="{{playURL .Slug .ID}}">{{playletDisplayName .PlayName .Title}}</a></h3>
<p class="card__meta">{{range $ti, $tag := cardTags .}}{{if $ti}} {{end}}{{if $tag.URL}}<a href="{{$tag.URL}}" {{externalLinkAttrs $tag.URL}}>{{$tag.Name}}</a>{{else}}{{$tag.Name}}{{end}}{{end}}</p>
</article>
{{end}}
</div>
</section>
{{end}}
<aside class="play-hot" aria-label="热门短剧">
<div class="play-hot__head">
@@ -362,46 +394,12 @@
</div>
</li>
{{else}}
<li><p class="play-comment__text" style="padding:12px 0;color:var(--muted, #888)">暂无热榜数据</p></li>
<li><p class="play-hot__empty" style="margin:0;padding:12px 0;color:var(--text-dim)">暂无热榜数据</p></li>
{{end}}
</ol>
</aside>
</section>
{{if .guessLike}}
<section class="play-like" aria-label="猜你喜欢">
<div class="play-like__head">
<div class="play-like__titles">
<h2 class="play-like__title">猜你喜欢</h2>
<span class="play-like__desc only-pc">根据本剧题材推荐</span>
</div>
<button
type="button"
class="play-like__refresh"
data-guess-refresh
data-playlet-id="{{.playlet.ID}}"
data-page="{{.guessPage}}"
data-page-size="{{if .guessPageSize}}{{.guessPageSize}}{{else}}6{{end}}"
data-total="{{.guessTotal}}"
>
换一批
<img decoding="async" loading="lazy" class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/chevron-right.svg" alt="前往下一项">
</button>
</div>
<div class="play-like__grid" id="play-like" data-guess-grid>
{{range .guessLike}}
<article class="card card--play">
<a class="card__cover lazy" href="{{playURL .Slug .ID}}" style="{{cssCoverPlaceholder}}" data-src="{{.Cover}}">
{{if .TotalEpisode}}<span class="card__ep">全 {{.TotalEpisode}} 集</span>{{else}}<span class="card__ep">{{updateStatusText .UpdateStatus}}</span>{{end}}
</a>
<h3 class="card__title"><a href="{{playURL .Slug .ID}}">{{playletDisplayName .PlayName .Title}}</a></h3>
<p class="card__meta">{{range $ti, $tag := cardTags .}}{{if $ti}} {{end}}{{if $tag.URL}}<a href="{{$tag.URL}}" {{externalLinkAttrs $tag.URL}}>{{$tag.Name}}</a>{{else}}{{$tag.Name}}{{end}}{{end}}</p>
</article>
{{end}}
</div>
</section>
{{end}}
<div class="play-promo play-promo--bottom">
{{if .advitise}}{{template "ads" (adSlice .advitise.DetailBannerList 2 2)}}{{end}}
</div>
+36 -38
View File
@@ -17,7 +17,7 @@
<script src="/static/js/common/compat.js?v=4"></script>
<link rel="stylesheet" href="/static/css/base.css?v=2">
<link rel="stylesheet" href="/static/common/DPlayer.min.css">
<link rel="stylesheet" href="/static/css/play.css?v=26">
<link rel="stylesheet" href="/static/css/play.css?v=27">
<script defer src="/static/common/hls.min.js"></script>
<script defer src="/static/common/DPlayer.min.js?v=2"></script>
</head>
@@ -181,8 +181,40 @@
{{if .advitise}}{{template "ads" (adSlice .advitise.DetailBannerList 0 2)}}{{end}}
</div>
<section class="play-dual" aria-label="评论与热榜">
{{template "playComments" .}}
<section class="play-dual" aria-label="推荐与热榜">
{{if .guessLike}}
<section class="play-like" aria-label="猜你喜欢">
<div class="play-like__head">
<div class="play-like__titles">
<h2 class="play-like__title">猜你喜欢</h2>
<span class="play-like__desc only-pc">根据本剧题材推荐</span>
</div>
<button
type="button"
class="play-like__refresh"
data-guess-refresh
data-playlet-id="{{.guessPlayletID}}"
data-page="{{.guessPage}}"
data-page-size="{{if .guessPageSize}}{{.guessPageSize}}{{else}}6{{end}}"
data-total="{{.guessTotal}}"
>
换一批
<img decoding="async" loading="lazy" class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/chevron-right.svg" alt="前往下一项">
</button>
</div>
<div class="play-like__grid" id="play-like" data-guess-grid>
{{range .guessLike}}
<article class="card card--play">
<a class="card__cover lazy" href="{{playURL .Slug .ID}}" style="{{cssCoverPlaceholder}}" data-src="{{.Cover}}">
{{if .TotalEpisode}}<span class="card__ep">全 {{.TotalEpisode}} 集</span>{{else}}<span class="card__ep">{{updateStatusText .UpdateStatus}}</span>{{end}}
</a>
<h3 class="card__title"><a href="{{playURL .Slug .ID}}">{{playletDisplayName .PlayName .Title}}</a></h3>
<p class="card__meta">{{range $ti, $tag := cardTags .}}{{if $ti}} {{end}}{{if $tag.URL}}<a href="{{$tag.URL}}" {{externalLinkAttrs $tag.URL}}>{{$tag.Name}}</a>{{else}}{{$tag.Name}}{{end}}{{end}}</p>
</article>
{{end}}
</div>
</section>
{{end}}
<aside class="play-hot" aria-label="热门短剧">
<div class="play-hot__head">
@@ -212,46 +244,12 @@
</div>
</li>
{{else}}
<li><p class="play-comment__text" style="padding:12px 0;color:var(--muted, #888)">暂无热榜数据</p></li>
<li><p class="play-hot__empty" style="margin:0;padding:12px 0;color:var(--text-dim)">暂无热榜数据</p></li>
{{end}}
</ol>
</aside>
</section>
{{if .guessLike}}
<section class="play-like" aria-label="猜你喜欢">
<div class="play-like__head">
<div class="play-like__titles">
<h2 class="play-like__title">猜你喜欢</h2>
<span class="play-like__desc only-pc">根据本剧题材推荐</span>
</div>
<button
type="button"
class="play-like__refresh"
data-guess-refresh
data-playlet-id="{{.guessPlayletID}}"
data-page="{{.guessPage}}"
data-page-size="{{if .guessPageSize}}{{.guessPageSize}}{{else}}6{{end}}"
data-total="{{.guessTotal}}"
>
换一批
<img decoding="async" loading="lazy" class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/chevron-right.svg" alt="前往下一项">
</button>
</div>
<div class="play-like__grid" id="play-like" data-guess-grid>
{{range .guessLike}}
<article class="card card--play">
<a class="card__cover lazy" href="{{playURL .Slug .ID}}" style="{{cssCoverPlaceholder}}" data-src="{{.Cover}}">
{{if .TotalEpisode}}<span class="card__ep">全 {{.TotalEpisode}} 集</span>{{else}}<span class="card__ep">{{updateStatusText .UpdateStatus}}</span>{{end}}
</a>
<h3 class="card__title"><a href="{{playURL .Slug .ID}}">{{playletDisplayName .PlayName .Title}}</a></h3>
<p class="card__meta">{{range $ti, $tag := cardTags .}}{{if $ti}} {{end}}{{if $tag.URL}}<a href="{{$tag.URL}}" {{externalLinkAttrs $tag.URL}}>{{$tag.Name}}</a>{{else}}{{$tag.Name}}{{end}}{{end}}</p>
</article>
{{end}}
</div>
</section>
{{end}}
<div class="play-promo play-promo--bottom">
{{if .advitise}}{{template "ads" (adSlice .advitise.DetailBannerList 2 2)}}{{end}}
</div>