update:

This commit is contained in:
Kerwin
2025-09-09 16:27:07 +08:00
parent e481775e27
commit cafe2ce406
10 changed files with 548 additions and 418 deletions

View File

@@ -29,6 +29,7 @@ func HotDramaToResponse(drama *entity.HotDrama) *dto.HotDramaResponse {
PosterURL: drama.PosterURL, PosterURL: drama.PosterURL,
Category: drama.Category, Category: drama.Category,
SubType: drama.SubType, SubType: drama.SubType,
Rank: drama.Rank,
Source: drama.Source, Source: drama.Source,
DoubanID: drama.DoubanID, DoubanID: drama.DoubanID,
DoubanURI: drama.DoubanURI, DoubanURI: drama.DoubanURI,
@@ -49,6 +50,7 @@ func RequestToHotDrama(req *dto.HotDramaRequest) *entity.HotDrama {
Actors: req.Actors, Actors: req.Actors,
Category: req.Category, Category: req.Category,
SubType: req.SubType, SubType: req.SubType,
Rank: req.Rank,
Source: req.Source, Source: req.Source,
DoubanID: req.DoubanID, DoubanID: req.DoubanID,
} }

View File

@@ -16,6 +16,7 @@ type HotDramaRequest struct {
PosterURL string `json:"poster_url"` PosterURL string `json:"poster_url"`
Category string `json:"category"` Category string `json:"category"`
SubType string `json:"sub_type"` SubType string `json:"sub_type"`
Rank int `json:"rank"`
Source string `json:"source"` Source string `json:"source"`
DoubanID string `json:"douban_id"` DoubanID string `json:"douban_id"`
DoubanURI string `json:"douban_uri"` DoubanURI string `json:"douban_uri"`
@@ -41,6 +42,7 @@ type HotDramaResponse struct {
PosterURL string `json:"poster_url"` PosterURL string `json:"poster_url"`
Category string `json:"category"` Category string `json:"category"`
SubType string `json:"sub_type"` SubType string `json:"sub_type"`
Rank int `json:"rank"`
Source string `json:"source"` Source string `json:"source"`
DoubanID string `json:"douban_id"` DoubanID string `json:"douban_id"`
DoubanURI string `json:"douban_uri"` DoubanURI string `json:"douban_uri"`

View File

@@ -27,6 +27,7 @@ type HotDrama struct {
// 分类信息 // 分类信息
Category string `json:"category" gorm:"size:50"` // 分类(电影/电视剧) Category string `json:"category" gorm:"size:50"` // 分类(电影/电视剧)
SubType string `json:"sub_type" gorm:"size:50"` // 子类型(华语/欧美/韩国/日本等) SubType string `json:"sub_type" gorm:"size:50"` // 子类型(华语/欧美/韩国/日本等)
Rank int `json:"rank" gorm:"default:0"` // 排序(豆瓣返回顺序)
// 数据来源 // 数据来源
Source string `json:"source" gorm:"size:50;default:'douban'"` // 数据来源 Source string `json:"source" gorm:"size:50;default:'douban'"` // 数据来源

View File

@@ -12,6 +12,7 @@ type HotDramaRepository interface {
FindByID(id uint) (*entity.HotDrama, error) FindByID(id uint) (*entity.HotDrama, error)
FindAll(page, pageSize int) ([]entity.HotDrama, int64, error) FindAll(page, pageSize int) ([]entity.HotDrama, int64, error)
FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error) FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error)
FindByCategoryAndSubType(category, subType string, page, pageSize int) ([]entity.HotDrama, int64, error)
FindByDoubanID(doubanID string) (*entity.HotDrama, error) FindByDoubanID(doubanID string) (*entity.HotDrama, error)
Upsert(drama *entity.HotDrama) error Upsert(drama *entity.HotDrama) error
Delete(id uint) error Delete(id uint) error
@@ -59,7 +60,7 @@ func (r *hotDramaRepository) FindAll(page, pageSize int) ([]entity.HotDrama, int
} }
// 获取分页数据 // 获取分页数据
err := r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error err := r.db.Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -80,7 +81,28 @@ func (r *hotDramaRepository) FindByCategory(category string, page, pageSize int)
} }
// 获取分页数据 // 获取分页数据
err := r.db.Where("category = ?", category).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error err := r.db.Where("category = ?", category).Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error
if err != nil {
return nil, 0, err
}
return dramas, total, nil
}
// FindByCategoryAndSubType 根据分类和子类型查找热播剧(分页)
func (r *hotDramaRepository) FindByCategoryAndSubType(category, subType string, page, pageSize int) ([]entity.HotDrama, int64, error) {
var dramas []entity.HotDrama
var total int64
offset := (page - 1) * pageSize
// 获取总数
if err := r.db.Model(&entity.HotDrama{}).Where("category = ? AND sub_type = ?", category, subType).Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := r.db.Where("category = ? AND sub_type = ?", category, subType).Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }

View File

@@ -1,13 +1,17 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time"
"github.com/ctwj/urldb/db/converter" "github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo" "github.com/ctwj/urldb/db/repo"
"github.com/go-resty/resty/v2"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -94,6 +98,87 @@ func CreateHotDrama(c *gin.Context) {
SuccessResponse(c, response) SuccessResponse(c, response)
} }
// GetPosterImage 获取海报图片代理
func GetPosterImage(c *gin.Context) {
url := c.Query("url")
if url == "" {
ErrorResponse(c, "图片URL不能为空", http.StatusBadRequest)
return
}
// 简单的URL验证
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
ErrorResponse(c, "无效的图片URL", http.StatusBadRequest)
return
}
// 检查If-Modified-Since头实现条件请求
ifModifiedSince := c.GetHeader("If-Modified-Since")
if ifModifiedSince != "" {
// 如果存在,说明浏览器有缓存,检查是否过期
ifLastModified, err := time.Parse("Mon, 02 Jan 2006 15:04:05 GMT", ifModifiedSince)
if err == nil && time.Since(ifLastModified) < 86400*time.Second { // 24小时内
c.Status(http.StatusNotModified)
return
}
}
// 检查ETag头 - 基于URL生成保证相同URL有相同ETag
ifNoneMatch := c.GetHeader("If-None-Match")
if ifNoneMatch != "" {
etag := fmt.Sprintf(`"%x"`, len(url)) // 简单的基于URL长度的ETag
if ifNoneMatch == etag {
c.Status(http.StatusNotModified)
return
}
}
client := resty.New().
SetTimeout(30 * time.Second).
SetRetryCount(2).
SetRetryWaitTime(1 * time.Second)
resp, err := client.R().
SetHeaders(map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Referer": "https://m.douban.com/",
"Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}).
Get(url)
if err != nil {
ErrorResponse(c, "获取图片失败: "+err.Error(), http.StatusInternalServerError)
return
}
if resp.StatusCode() != 200 {
ErrorResponse(c, fmt.Sprintf("获取图片失败,状态码: %d", resp.StatusCode()), http.StatusInternalServerError)
return
}
// 设置响应头
contentType := resp.Header().Get("Content-Type")
if contentType == "" {
contentType = "image/jpeg"
}
c.Header("Content-Type", contentType)
// 增强缓存策略
c.Header("Cache-Control", "public, max-age=604800, s-maxage=86400") // 客户端7天代理1天
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
// 设置缓存验证头基于URL长度生成的简单ETag
etag := fmt.Sprintf(`"%x"`, len(url))
c.Header("ETag", etag)
c.Header("Last-Modified", time.Now().Add(-86400*time.Second).Format("Mon, 02 Jan 2006 15:04:05 GMT")) // 设为1天前避免立即过期
// 返回图片数据
c.Data(resp.StatusCode(), contentType, resp.Body())
}
// UpdateHotDrama 更新热播剧记录 // UpdateHotDrama 更新热播剧记录
func UpdateHotDrama(c *gin.Context) { func UpdateHotDrama(c *gin.Context) {
idStr := c.Param("id") idStr := c.Param("id")
@@ -149,6 +234,7 @@ func GetHotDramaList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
category := c.Query("category") category := c.Query("category")
subType := c.Query("sub_type")
var dramas []entity.HotDrama var dramas []entity.HotDrama
var total int64 var total int64
@@ -156,13 +242,17 @@ func GetHotDramaList(c *gin.Context) {
// 如果page_size很大比如>=1000则获取所有数据 // 如果page_size很大比如>=1000则获取所有数据
if pageSize >= 1000 { if pageSize >= 1000 {
if category != "" { if category != "" && subType != "" {
dramas, total, err = repoManager.HotDramaRepository.FindByCategoryAndSubType(category, subType, 1, 10000)
} else if category != "" {
dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, 1, 10000) dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, 1, 10000)
} else { } else {
dramas, total, err = repoManager.HotDramaRepository.FindAll(1, 10000) dramas, total, err = repoManager.HotDramaRepository.FindAll(1, 10000)
} }
} else { } else {
if category != "" { if category != "" && subType != "" {
dramas, total, err = repoManager.HotDramaRepository.FindByCategoryAndSubType(category, subType, page, pageSize)
} else if category != "" {
dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, page, pageSize) dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, page, pageSize)
} else { } else {
dramas, total, err = repoManager.HotDramaRepository.FindAll(page, pageSize) dramas, total, err = repoManager.HotDramaRepository.FindAll(page, pageSize)

View File

@@ -281,6 +281,7 @@ func main() {
api.POST("/hot-dramas", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateHotDrama) api.POST("/hot-dramas", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateHotDrama)
api.PUT("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateHotDrama) api.PUT("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateHotDrama)
api.DELETE("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteHotDrama) api.DELETE("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteHotDrama)
api.GET("/hot-dramas/poster", handlers.GetPosterImage)
// 任务管理路由 // 任务管理路由
api.POST("/tasks/transfer", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.CreateBatchTransferTask) api.POST("/tasks/transfer", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.CreateBatchTransferTask)

View File

@@ -90,13 +90,26 @@ func (h *HotDramaScheduler) processHotDramaNames(dramaNames []string) {
// 收集所有数据 // 收集所有数据
var allDramas []*entity.HotDrama var allDramas []*entity.HotDrama
// 获取电影数据 // 获取最近热门电影数据
movieDramas := h.processMovieData() recentMovieDramas := h.processRecentMovies()
allDramas = append(allDramas, movieDramas...) allDramas = append(allDramas, recentMovieDramas...)
// 获取电视剧数据 // 获取最近热门剧集数据
tvDramas := h.processTvData() recentTVDramas := h.processRecentTVs()
allDramas = append(allDramas, tvDramas...) allDramas = append(allDramas, recentTVDramas...)
// 获取最近热门综艺数据
recentShowDramas := h.processRecentShows()
allDramas = append(allDramas, recentShowDramas...)
// 获取豆瓣电影Top250数据
top250Dramas := h.processTop250Movies()
allDramas = append(allDramas, top250Dramas...)
// 设置排名顺序(保持豆瓣返回的顺序)
for i, drama := range allDramas {
drama.Rank = i
}
// 清空数据库 // 清空数据库
utils.Info("准备清空数据库,当前共有 %d 条数据", len(allDramas)) utils.Info("准备清空数据库,当前共有 %d 条数据", len(allDramas))
@@ -121,111 +134,127 @@ func (h *HotDramaScheduler) processHotDramaNames(dramaNames []string) {
utils.Info("热播剧数据处理完成") utils.Info("热播剧数据处理完成")
} }
// processMovieData 处理电影数据 // processRecentMovies 处理最近热门电影数据
func (h *HotDramaScheduler) processMovieData() []*entity.HotDrama { func (h *HotDramaScheduler) processRecentMovies() []*entity.HotDrama {
utils.Info("开始处理电影数据...") utils.Info("开始处理最近热门电影数据...")
var movieDramas []*entity.HotDrama var recentMovies []*entity.HotDrama
// 使用GetTypePage方法获取电影数据 items, err := h.doubanService.GetRecentHotMovies()
movieResult, err := h.doubanService.GetTypePage("movie_top250", "全部")
if err != nil { if err != nil {
utils.Error(fmt.Sprintf("获取电影榜单失败: %v", err)) utils.Error(fmt.Sprintf("获取最近热门电影失败: %v", err))
return movieDramas return recentMovies
} }
if movieResult.Success && movieResult.Data != nil { utils.Info("最近热门电影获取到 %d 个数据", len(items))
utils.Info("电影获取到 %d 个数据", len(movieResult.Data.Items))
for _, item := range movieResult.Data.Items { for _, item := range items {
drama := &entity.HotDrama{ drama := h.convertDoubanItemToHotDrama(item, "电影", "热门")
Title: item.Title, recentMovies = append(recentMovies, drama)
CardSubtitle: item.CardSubtitle, utils.Info("收集最近热门电影: %s (评分: %.1f, 年份: %s, 地区: %s)",
EpisodesInfo: item.EpisodesInfo, item.Title, item.Rating.Value, item.Year, item.Region)
IsNew: item.IsNew,
Rating: item.Rating.Value,
RatingCount: item.Rating.Count,
Year: item.Year,
Region: item.Region,
Genres: strings.Join(item.Genres, ", "),
Directors: strings.Join(item.Directors, ", "),
Actors: strings.Join(item.Actors, ", "),
PosterURL: item.Pic.Normal,
Category: "电影",
SubType: "热门",
Source: "douban",
DoubanID: item.ID,
DoubanURI: item.URI,
}
movieDramas = append(movieDramas, drama)
utils.Info("收集电影: %s (评分: %.1f, 年份: %s, 地区: %s)",
item.Title, item.Rating.Value, item.Year, item.Region)
}
} else {
utils.Warn("电影获取数据失败或为空")
} }
utils.Info("电影数据处理完成,共收集 %d 条数据", len(movieDramas)) utils.Info("最近热门电影数据处理完成,共收集 %d 条数据", len(recentMovies))
return movieDramas return recentMovies
} }
// processTvData 处理电视剧数据 // processRecentTVs 处理最近热门剧集数据
func (h *HotDramaScheduler) processTvData() []*entity.HotDrama { func (h *HotDramaScheduler) processRecentTVs() []*entity.HotDrama {
utils.Info("开始处理电视剧数据...") utils.Info("开始处理最近热门剧集数据...")
var tvDramas []*entity.HotDrama var recentTVs []*entity.HotDrama
// 获取所有tv类型 items, err := h.doubanService.GetRecentHotTVs()
tvTypes := h.doubanService.GetAllTvTypes() if err != nil {
utils.Info("获取到 %d 个tv类型: %v", len(tvTypes), tvTypes) utils.Error(fmt.Sprintf("获取最近热门剧集失败: %v", err))
return recentTVs
// 遍历每个type分别请求数据
for _, tvType := range tvTypes {
utils.Info("正在处理tv类型: %s", tvType)
// 使用GetTypePage方法请求数据
tvResult, err := h.doubanService.GetTypePage("tv", tvType)
if err != nil {
utils.Error(fmt.Sprintf("获取tv类型 %s 数据失败: %v", tvType, err))
continue
}
if tvResult.Success && tvResult.Data != nil {
utils.Info("tv类型 %s 获取到 %d 个数据", tvType, len(tvResult.Data.Items))
for _, item := range tvResult.Data.Items {
drama := &entity.HotDrama{
Title: item.Title,
CardSubtitle: item.CardSubtitle,
EpisodesInfo: item.EpisodesInfo,
IsNew: item.IsNew,
Rating: item.Rating.Value,
RatingCount: item.Rating.Count,
Year: item.Year,
Region: item.Region,
Genres: strings.Join(item.Genres, ", "),
Directors: strings.Join(item.Directors, ", "),
Actors: strings.Join(item.Actors, ", "),
PosterURL: item.Pic.Normal,
Category: "电视剧",
SubType: tvType, // 使用具体的tv类型
Source: "douban",
DoubanID: item.ID,
DoubanURI: item.URI,
}
tvDramas = append(tvDramas, drama)
utils.Info("收集tv类型 %s: %s (评分: %.1f, 年份: %s, 地区: %s)",
tvType, item.Title, item.Rating.Value, item.Year, item.Region)
}
} else {
utils.Warn("tv类型 %s 获取数据失败或为空", tvType)
}
} }
utils.Info("电视剧数据处理完成,共收集 %d 数据", len(tvDramas)) utils.Info("最近热门剧集获取到 %d 数据", len(items))
return tvDramas
for _, item := range items {
drama := h.convertDoubanItemToHotDrama(item, "电视剧", "热门")
recentTVs = append(recentTVs, drama)
utils.Info("收集最近热门剧集: %s (评分: %.1f, 年份: %s, 地区: %s)",
item.Title, item.Rating.Value, item.Year, item.Region)
}
utils.Info("最近热门剧集数据处理完成,共收集 %d 条数据", len(recentTVs))
return recentTVs
}
// processRecentShows 处理最近热门综艺数据
func (h *HotDramaScheduler) processRecentShows() []*entity.HotDrama {
utils.Info("开始处理最近热门综艺数据...")
var recentShows []*entity.HotDrama
items, err := h.doubanService.GetRecentHotShows()
if err != nil {
utils.Error(fmt.Sprintf("获取最近热门综艺失败: %v", err))
return recentShows
}
utils.Info("最近热门综艺获取到 %d 个数据", len(items))
for _, item := range items {
drama := h.convertDoubanItemToHotDrama(item, "综艺", "热门")
recentShows = append(recentShows, drama)
utils.Info("收集最近热门综艺: %s (评分: %.1f, 年份: %s, 地区: %s)",
item.Title, item.Rating.Value, item.Year, item.Region)
}
utils.Info("最近热门综艺数据处理完成,共收集 %d 条数据", len(recentShows))
return recentShows
}
// processTop250Movies 处理豆瓣电影Top250数据
func (h *HotDramaScheduler) processTop250Movies() []*entity.HotDrama {
utils.Info("开始处理豆瓣电影Top250数据...")
var top250Movies []*entity.HotDrama
items, err := h.doubanService.GetTop250Movies()
if err != nil {
utils.Error(fmt.Sprintf("获取豆瓣电影Top250失败: %v", err))
return top250Movies
}
utils.Info("豆瓣电影Top250获取到 %d 个数据", len(items))
for _, item := range items {
drama := h.convertDoubanItemToHotDrama(item, "电影", "Top250")
top250Movies = append(top250Movies, drama)
utils.Info("收集豆瓣Top250电影: %s (评分: %.1f, 年份: %s, 地区: %s)",
item.Title, item.Rating.Value, item.Year, item.Region)
}
utils.Info("豆瓣电影Top250数据处理完成共收集 %d 条数据", len(top250Movies))
return top250Movies
}
// convertDoubanItemToHotDrama 转换DoubanItem为HotDrama实体
func (h *HotDramaScheduler) convertDoubanItemToHotDrama(item utils.DoubanItem, category, subType string) *entity.HotDrama {
return &entity.HotDrama{
Title: item.Title,
CardSubtitle: item.CardSubtitle,
EpisodesInfo: item.EpisodesInfo,
IsNew: item.IsNew,
Rating: item.Rating.Value,
RatingCount: item.Rating.Count,
Year: item.Year,
Region: item.Region,
Genres: strings.Join(item.Genres, ", "),
Directors: strings.Join(item.Directors, ", "),
Actors: strings.Join(item.Actors, ", "),
PosterURL: item.Pic.Normal,
Category: category,
SubType: subType,
Source: "douban",
DoubanID: item.ID,
DoubanURI: item.URI,
}
} }
// GetHotDramaNames 获取热播剧名称列表(公共方法) // GetHotDramaNames 获取热播剧名称列表(公共方法)

View File

@@ -3,12 +3,16 @@ package utils
import ( import (
"encoding/json" "encoding/json"
"log" "log"
"strconv"
"strings" "strings"
"time" "time"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )
// top250
// api: https://m.douban.com/rexxar/api/v2/subject_collection/movie_top250/items?start=0&count=10&items_only=1&type_tag=&for_mobile=1
// 最近热门电影 https://movie.douban.com/explore // 最近热门电影 https://movie.douban.com/explore
// api: https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie?start=0&limit=20 // api: https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie?start=0&limit=20
@@ -113,229 +117,129 @@ func NewDoubanService() *DoubanService {
client.SetRetryWaitTime(1 * time.Second) client.SetRetryWaitTime(1 * time.Second)
client.SetRetryMaxWaitTime(5 * time.Second) client.SetRetryMaxWaitTime(5 * time.Second)
// 初始化剧集榜单配置
tvCategories := map[string]map[string]map[string]string{
"最近热门剧集": {
// "国产剧": {"category": "tv", "type": "tv_domestic"},
"欧美剧": {"category": "tv", "type": "tv_american"},
"日剧": {"category": "tv", "type": "tv_japanese"},
"韩剧": {"category": "tv", "type": "tv_korean"},
"动画": {"category": "tv", "type": "tv_animation"},
"纪录片": {"category": "tv", "type": "tv_documentary"},
},
"最近热门综艺": {
// "综合": {"category": "show", "type": "show"},
"国内": {"category": "show", "type": "show_domestic"},
"国外": {"category": "show", "type": "show_foreign"},
},
}
return &DoubanService{ return &DoubanService{
baseURL: "https://m.douban.com/rexxar/api/v2", baseURL: "https://m.douban.com/rexxar/api/v2",
client: client, client: client,
TvCategories: tvCategories,
} }
} }
// GetTypePage 获取指定类型的数据 // GetRecentHotMovies fetches recent hot movies
func (ds *DoubanService) GetTypePage(category, rankingType string) (*DoubanResult, error) { func (ds *DoubanService) GetRecentHotMovies() ([]DoubanItem, error) {
// 构建请求参数 url := "https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie"
params := map[string]string{ params := map[string]string{
"start": "0", "start": "0",
"limit": "50", "limit": "20",
"os": "window",
"_": "0",
"loc_id": "108288",
} }
items := []DoubanItem{}
for {
pageItems, total, err := ds.fetchPage(url, params)
if err != nil {
return nil, err
}
items = append(items, pageItems...)
if len(items) >= total {
break
}
start := len(items)
params["start"] = strconv.Itoa(start)
}
return items, nil
}
Debug("请求参数: %+v", params) // GetRecentHotTVs fetches recent hot TV shows
Debug("请求URL: %s/subject_collection/%s/items", ds.baseURL, rankingType) func (ds *DoubanService) GetRecentHotTVs() ([]DoubanItem, error) {
url := "https://m.douban.com/rexxar/api/v2/subject/recent_hot/tv"
params := map[string]string{
"start": "0",
"limit": "300",
}
items := []DoubanItem{}
for {
pageItems, total, err := ds.fetchPage(url, params)
if err != nil {
return nil, err
}
items = append(items, pageItems...)
if len(items) >= total {
break
}
start := len(items)
params["start"] = strconv.Itoa(start)
}
return items, nil
}
// GetRecentHotShows fetches recent hot shows
func (ds *DoubanService) GetRecentHotShows() ([]DoubanItem, error) {
url := "https://m.douban.com/rexxar/api/v2/subject/recent_hot/tv"
params := map[string]string{
"limit": "300",
"category": "show",
"type": "show",
"start": "0",
}
items := []DoubanItem{}
for {
pageItems, total, err := ds.fetchPage(url, params)
if err != nil {
return nil, err
}
items = append(items, pageItems...)
if len(items) >= total {
break
}
start := len(items)
params["start"] = strconv.Itoa(start)
}
return items, nil
}
// GetTop250Movies fetches top 250 movies
func (ds *DoubanService) GetTop250Movies() ([]DoubanItem, error) {
url := "https://m.douban.com/rexxar/api/v2/subject_collection/movie_top250/items"
params := map[string]string{
"start": "0",
"count": "250",
"items_only": "1",
"type_tag": "",
"for_mobile": "1",
}
items, _, err := ds.fetchPage(url, params)
return items, err
}
// fetchPage fetches a page of items from a given URL and parameters
func (ds *DoubanService) fetchPage(url string, params map[string]string) ([]DoubanItem, int, error) {
var response *resty.Response var response *resty.Response
var err error var err error
// 尝试调用豆瓣API
Debug("开始发送HTTP请求...")
response, err = ds.client.R(). response, err = ds.client.R().
SetQueryParams(params). SetQueryParams(params).
Get(ds.baseURL + "/subject_collection/" + rankingType + "/items") Get(url)
if err != nil { if err != nil {
Error("=== 豆瓣API调用失败 ===") return nil, 0, err
Error("错误详情: %v", err)
return &DoubanResult{
Success: false,
Message: "API调用失败: " + err.Error(),
}, nil
} }
Debug("=== HTTP请求成功 ===")
Debug("响应状态码: %d", response.StatusCode())
Debug("响应体长度: %d bytes", len(response.Body()))
// 记录响应体的前500个字符用于调试
responseBody := string(response.Body())
Debug("响应体原始长度: %d 字符", len(responseBody))
if len(responseBody) > 500 {
Debug("响应体前500字符: %s...", responseBody[:500])
} else {
Debug("完整响应体: %s", responseBody)
}
// 检查响应体是否包含有效JSON
if len(responseBody) == 0 {
Warn("=== 响应体为空 ===")
return &DoubanResult{
Success: false,
Message: "响应体为空",
}, nil
}
// 尝试解析JSON
var apiResponse map[string]interface{} var apiResponse map[string]interface{}
if err := json.Unmarshal(response.Body(), &apiResponse); err != nil { if err := json.Unmarshal(response.Body(), &apiResponse); err != nil {
Error("=== 解析API响应失败 ===") return nil, 0, err
Error("JSON解析错误: %v", err)
Debug("响应体内容: %s", string(response.Body()))
// 尝试检查是否是HTML错误页面
if len(responseBody) > 100 && (strings.Contains(responseBody, "<html>") || strings.Contains(responseBody, "<!DOCTYPE")) {
Warn("检测到HTML响应可能是错误页面")
return &DoubanResult{
Success: false,
Message: "返回HTML错误页面",
}, nil
}
return &DoubanResult{
Success: false,
Message: "解析API响应失败: " + err.Error(),
}, nil
} }
log.Printf("=== JSON解析成功 ===")
log.Printf("解析后的数据结构: %+v", apiResponse)
// 打印完整的API响应JSON
log.Printf("=== 完整API响应JSON ===")
if responseBytes, err := json.MarshalIndent(apiResponse, "", " "); err == nil {
log.Printf("完整响应:\n%s", string(responseBytes))
} else {
log.Printf("序列化响应失败: %v", err)
}
// 处理豆瓣移动端API的响应格式
items := ds.extractItems(apiResponse) items := ds.extractItems(apiResponse)
categories := ds.extractCategories(apiResponse) total := ds.extractTotal(apiResponse)
log.Printf("提取到的数据数量: %d", len(items)) return items, total, nil
log.Printf("提取到的分类数量: %d", len(categories)) }
// 如果没有获取到真实数据,返回空结果 // extractTotal extracts the total number of items from the API response
if len(items) == 0 { func (ds *DoubanService) extractTotal(response map[string]interface{}) int {
log.Printf("=== API返回空数据 ===") if totalData, ok := response["total"]; ok {
return &DoubanResult{
Success: true,
Data: &DoubanResponse{
Items: []DoubanItem{},
Total: 0,
Categories: []DoubanCategory{},
},
}, nil
}
// 如果没有获取到categories使用默认分类
if len(categories) == 0 {
log.Printf("=== 使用默认分类 ===")
categories = []DoubanCategory{
{Category: category, Selected: true, Type: rankingType, Title: rankingType},
}
}
// 根据请求的category和type更新selected状态
for i := range categories {
categories[i].Selected = categories[i].Category == category && categories[i].Type == rankingType
}
// 限制返回数量最多50条
limit := 50
if len(items) > limit {
log.Printf("限制返回数量从 %d 到 %d", len(items), limit)
items = items[:limit]
}
// 获取总数优先使用API返回的total字段
total := len(items)
if totalData, ok := apiResponse["total"]; ok {
if totalFloat, ok := totalData.(float64); ok { if totalFloat, ok := totalData.(float64); ok {
total = int(totalFloat) return int(totalFloat)
} }
} }
return 0
result := &DoubanResponse{
Items: items,
Total: total,
Categories: categories,
IsMockData: false,
MockReason: "",
}
log.Printf("=== 数据获取完成 ===")
log.Printf("最终返回数据数量: %d", len(items))
return &DoubanResult{
Success: true,
Data: result,
}, nil
}
// GetTvByType 获取指定type的全部剧集数据
func (ds *DoubanService) GetTvByType(tvType string) ([]map[string]interface{}, error) {
url := ds.baseURL + "/subject_collection/" + tvType + "/items"
params := map[string]string{
"start": "0",
"limit": "1000", // 假设不会超过1000条
}
resp, err := ds.client.R().
SetQueryParams(params).
Get(url)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return nil, err
}
items, ok := result["subject_collection_items"].([]interface{})
if !ok {
return nil, nil // 没有数据
}
// 转换为[]map[string]interface{}
var out []map[string]interface{}
for _, item := range items {
if m, ok := item.(map[string]interface{}); ok {
out = append(out, m)
}
}
return out, nil
}
// GetAllTvTypes 获取所有tv类型type列表
func (ds *DoubanService) GetAllTvTypes() []string {
types := []string{}
for _, sub := range ds.TvCategories {
for _, v := range sub {
if t, ok := v["type"]; ok {
types = append(types, t)
}
}
}
return types
} }
// extractItems 从API响应中提取项目列表 // extractItems 从API响应中提取项目列表
@@ -413,16 +317,3 @@ func (ds *DoubanService) parseCardSubtitle(item *DoubanItem) {
} }
} }
} }
// extractCategories 从API响应中提取分类列表
func (ds *DoubanService) extractCategories(response map[string]interface{}) []DoubanCategory {
var categories []DoubanCategory
if categoriesData, ok := response["categories"]; ok {
if categoriesBytes, err := json.Marshal(categoriesData); err == nil {
json.Unmarshal(categoriesBytes, &categories)
}
}
return categories
}

View File

@@ -181,7 +181,13 @@ export const useHotDramaApi = () => {
const updateHotDrama = (id: number, data: any) => useApiFetch(`/hot-dramas/${id}`, { method: 'PUT', body: data }).then(parseApiResponse) const updateHotDrama = (id: number, data: any) => useApiFetch(`/hot-dramas/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
const deleteHotDrama = (id: number) => useApiFetch(`/hot-dramas/${id}`, { method: 'DELETE' }).then(parseApiResponse) const deleteHotDrama = (id: number) => useApiFetch(`/hot-dramas/${id}`, { method: 'DELETE' }).then(parseApiResponse)
const fetchHotDramas = () => useApiFetch('/hot-dramas/fetch', { method: 'POST' }).then(parseApiResponse) const fetchHotDramas = () => useApiFetch('/hot-dramas/fetch', { method: 'POST' }).then(parseApiResponse)
return { getHotDramas, createHotDrama, updateHotDrama, deleteHotDrama, fetchHotDramas }
const getPosterUrl = (posterUrl: string): string => {
if (!posterUrl) return ''
return `/api/hot-dramas/poster?url=${encodeURIComponent(posterUrl)}`
}
return { getHotDramas, createHotDrama, updateHotDrama, deleteHotDrama, fetchHotDramas, getPosterUrl }
} }
export const useMonitorApi = () => { export const useMonitorApi = () => {

View File

@@ -30,54 +30,6 @@
</nav> </nav>
</div> </div>
<!-- 统计信息 -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center">
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<i class="fas fa-film text-blue-600 dark:text-blue-400"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">总数量</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ total }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center">
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
<i class="fas fa-video text-green-600 dark:text-green-400"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">电影</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ movieCount }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center">
<div class="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
<i class="fas fa-tv text-purple-600 dark:text-purple-400"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">电视剧</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ tvCount }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<i class="fas fa-star text-yellow-600 dark:text-yellow-400"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">平均评分</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ averageRating }}</p>
</div>
</div>
</div>
</div>
<!-- 筛选器 --> <!-- 筛选器 -->
<div class="mb-6 flex flex-wrap gap-4"> <div class="mb-6 flex flex-wrap gap-4">
<button <button
@@ -105,83 +57,139 @@
<div <div
v-for="drama in filteredDramas" v-for="drama in filteredDramas"
:key="drama.id" :key="drama.id"
class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700" :data-item-id="drama.id"
class="group relative bg-white/10 dark:bg-gray-800/10 backdrop-blur-md rounded-2xl shadow-xl overflow-hidden hover:shadow-2xl transition-all duration-300 border border-white/20 dark:border-gray-700/50 hover:scale-105"
> >
<!-- 海报图片 -->
<div v-if="drama.poster_url" class="relative overflow-hidden">
<!-- 品牌Logo占位符 -->
<div
v-if="!visibleItems.has(drama.id)"
class="w-full h-52 bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-700 dark:to-gray-800 flex flex-col items-center justify-center relative overflow-hidden"
>
<!-- 装饰性背景图形 -->
<div class="absolute inset-0 opacity-20">
<svg viewBox="0 0 200 100" class="w-full h-full">
<circle cx="30" cy="25" r="3" fill="currentColor"/>
<circle cx="80" cy="40" r="2" fill="currentColor"/>
<circle cx="150" cy="20" r="2" fill="currentColor"/>
<circle cx="120" cy="60" r="2" fill="currentColor"/>
<circle cx="50" cy="70" r="2" fill="currentColor"/>
</svg>
</div>
<!-- 主要品牌元素 -->
<div class="flex flex-col items-center space-y-2 z-10">
<!-- 电影院图标 -->
<svg class="w-12 h-12 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 6c0-1.1-.9-2-2-2H7c-1.1 0-2 .9-2 2v1l.669.775C6.537 8.347 7.605 9.334 9.5 9.781c.015 0 .03.003.045.003s.03-.003.045-.003c1.895-.447 2.963-1.434 3.331-1.506L13 7V6h6v1l.669.775C20.537 8.347 21.605 9.334 23.5 9.781c.015 0 .03.003.045.003s.03-.003.045-.003c1.895-.447 2.963-1.434 3.331-1.506L5 7V6H1c0 1.1.9 2 2 2v1l.669.775C4.537 8.347 5.605 9.334 7.5 9.781c.015 0 .03.003.045.003s.03-.003.045-.003c1.895-.447 2.963-1.434 3.331-1.506L13 7V18H7c-1.1 0-2 .9-2 2s.9 2 2 2h10c1.1 0 2-.9 2-2s-.9-2-2-2h-6V6z"/>
</svg>
<!-- 品牌文字 -->
<div class="text-center">
<div class="text-lg font-bold text-blue-600 dark:text-blue-400">热播剧榜单</div>
<div class="text-xs text-gray-500 dark:text-gray-400 animate-pulse">精彩剧集等你发现</div>
</div>
<!-- 装饰线条 -->
<div class="flex items-center space-x-2">
<div class="w-8 h-px bg-blue-300 dark:bg-blue-600"></div>
<div class="w-2 h-2 bg-blue-400 dark:bg-blue-500 rounded-full"></div>
<div class="w-8 h-px bg-blue-300 dark:bg-blue-600"></div>
</div>
</div>
</div>
<!-- 主图片只有在可视区域时才加载 -->
<img
v-if="visibleItems.has(drama.id)"
:src="getPosterUrl(drama.poster_url)"
:alt="drama.title"
class="w-full h-52 object-cover transition-all duration-500 opacity-0"
@load="$event.target.style.opacity = '1'"
@error="handleImageError"
/>
<!-- 图片上的遮罩和信息始终显示 -->
<div v-if="visibleItems.has(drama.id)" class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent"></div>
<!-- 新剧标签 -->
<div
v-if="drama.is_new && visibleItems.has(drama.id)"
class="absolute top-3 right-3 bg-gradient-to-r from-red-500 to-red-600 text-white px-3 py-1 rounded-full text-xs font-semibold shadow-lg z-10"
>
🔥 HOT
</div>
<!-- 评分显示 -->
<div v-if="visibleItems.has(drama.id)" class="absolute bottom-3 left-3 right-3 flex items-center justify-between z-20">
<div class="bg-black/60 backdrop-blur-md px-2 py-1 rounded-lg">
<span class="text-yellow-400 font-bold text-lg">{{ drama.rating }}</span>
<span class="text-white/80 text-sm ml-1"></span>
</div>
<div class="flex gap-1">
<span class="bg-black/60 backdrop-blur-md text-white/90 text-xs px-2 py-1 rounded-lg">{{ drama.category }}</span>
<span v-if="drama.sub_type" class="bg-black/60 backdrop-blur-md text-white/90 text-xs px-2 py-1 rounded-lg">{{ drama.sub_type }}</span>
</div>
</div>
</div>
<!-- 剧集信息 --> <!-- 剧集信息 -->
<div class="p-6"> <div class="p-5">
<div class="flex items-start justify-between mb-3"> <!-- 标题 -->
<h3 class="text-lg font-semibold text-gray-900 dark:text-white line-clamp-2 flex-1"> <div class="mb-3">
<h3 class="text-base font-bold text-gray-900 dark:text-white line-clamp-2 leading-tight">
{{ drama.title }} {{ drama.title }}
</h3> </h3>
<div class="flex items-center ml-2 flex-shrink-0">
<span class="text-yellow-500 text-sm font-medium">{{ drama.rating }}</span>
<span class="text-gray-400 dark:text-gray-500 text-xs ml-1"></span>
</div>
</div> </div>
<!-- 副标题 --> <!-- 副标题 -->
<div v-if="drama.card_subtitle" class="mb-3"> <div v-if="drama.card_subtitle" class="mb-3">
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-1">{{ drama.card_subtitle }}</p> <p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 leading-relaxed">{{ drama.card_subtitle }}</p>
</div> </div>
<!-- 年份地区类型 --> <!-- 年份地区信息 -->
<div class="flex items-center gap-2 mb-3 flex-wrap"> <div class="flex items-center gap-2 mb-3 flex-wrap">
<span v-if="drama.year" class="text-sm text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded"> <span v-if="drama.year" class="text-xs text-white/80 bg-black/40 backdrop-blur-sm px-2 py-1 rounded-md">
{{ drama.year }} {{ drama.year }}
</span> </span>
<span v-if="drama.region" class="text-sm text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded"> <span v-if="drama.region" class="text-xs text-white/80 bg-black/40 backdrop-blur-sm px-2 py-1 rounded-md">
{{ drama.region }} {{ drama.region }}
</span> </span>
<span class="text-sm text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded">
{{ drama.category }}
</span>
<span v-if="drama.sub_type" class="text-sm text-purple-600 dark:text-purple-400 bg-purple-100 dark:bg-purple-900 px-2 py-1 rounded">
{{ drama.sub_type }}
</span>
</div> </div>
<!-- 类型标签 --> <!-- 类型标签 -->
<div v-if="drama.genres" class="mb-3"> <div v-if="drama.genres" class="mb-3">
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-2">
<span <span
v-for="genre in drama.genres.split(',')" v-for="genre in drama.genres.split(',').slice(0, 3)"
:key="genre" :key="genre"
class="text-xs text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded" class="text-xs text-white/90 bg-gradient-to-r from-blue-500/80 to-purple-500/80 backdrop-blur-sm px-2 py-1 rounded-md"
> >
{{ genre.trim() }} {{ genre.trim() }}
</span> </span>
</div> </div>
</div> </div>
<!-- 导演 -->
<div v-if="drama.directors" class="mb-2">
<span class="text-xs text-gray-500 dark:text-gray-400">导演</span>
<span class="text-sm text-gray-700 dark:text-gray-300 line-clamp-1">{{ drama.directors }}</span>
</div>
<!-- 演员 -->
<div v-if="drama.actors" class="mb-3">
<span class="text-xs text-gray-500 dark:text-gray-400">主演</span>
<span class="text-sm text-gray-700 dark:text-gray-300 line-clamp-2">{{ drama.actors }}</span>
</div>
<!-- 集数信息 -->
<div v-if="drama.episodes_info" class="mb-3">
<span class="text-xs text-gray-500 dark:text-gray-400">集数</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ drama.episodes_info }}</span>
</div>
<!-- 评分人数 -->
<div v-if="drama.rating_count" class="mb-3">
<span class="text-xs text-gray-500 dark:text-gray-400">评分人数</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ formatNumber(drama.rating_count) }}</span>
</div>
<!-- 数据来源和时间 --> <!-- 数据来源和时间 -->
<div class="flex items-center justify-between text-xs text-gray-400 dark:text-gray-500 pt-3 border-t border-gray-200 dark:border-gray-600"> <!-- <div class="flex items-center justify-between text-xs pt-4 border-t border-gray-100 dark:border-gray-700/50">
<span>来源{{ drama.source }}</span> <div class="flex items-center gap-2">
<span>{{ formatDate(drama.created_at) }}</span> <span class="text-gray-500 dark:text-gray-400">{{ drama.source }}</span>
</div> <div class="w-1 h-1 bg-gray-300 dark:bg-gray-600 rounded-full"></div>
<span class="text-gray-400 dark:text-gray-500">{{ formatDate(drama.created_at) }}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-green-600 dark:text-green-400 font-medium">{{ drama.episodes_info || '更新中' }}</span>
<div class="w-1 h-1 bg-gray-300 dark:bg-gray-600 rounded-full"></div>
<a
v-if="drama.douban_uri"
:href="drama.douban_uri"
target="_blank"
class="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-3 py-1 rounded-full text-xs font-medium hover:from-blue-600 hover:to-blue-700 transition-all duration-200"
@click.stop
>
View
</a>
</div>
</div> -->
</div> </div>
</div> </div>
</div> </div>
@@ -214,21 +222,25 @@ definePageMeta({
layout: 'default' layout: 'default'
}) })
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useHotDramaApi } from '~/composables/useApi' import { useHotDramaApi } from '~/composables/useApi'
const hotDramaApi = useHotDramaApi() const hotDramaApi = useHotDramaApi()
const { getPosterUrl } = hotDramaApi
// 响应式数据 // 响应式数据
const loading = ref(false) const loading = ref(false)
const dramas = ref([]) const dramas = ref([])
const total = ref(0) const total = ref(0)
const selectedCategory = ref('') const selectedCategory = ref('')
const visibleItems = ref(new Set()) // 存储当前可视区域的项目ID
// 分类选项 // 分类选项
const categories = ref([ const categories = ref([
{ label: '全部', value: '' }, { label: '全部', value: '' },
{ label: '电影', value: '电影' }, { label: '热门电影', value: '电影-热门' },
{ label: '电视剧', value: '电视剧' } { label: '热门电视剧', value: '电视剧-热门' },
{ label: '热门综艺', value: '综艺-热门' },
{ label: '豆瓣Top250', value: '电影-Top250' }
]) ])
// 计算属性 // 计算属性
@@ -236,7 +248,19 @@ const filteredDramas = computed(() => {
if (!selectedCategory.value) { if (!selectedCategory.value) {
return dramas.value return dramas.value
} }
return dramas.value.filter(drama => drama.category === selectedCategory.value) // Handle old categories
if (selectedCategory.value === '电影') {
return dramas.value.filter(drama => drama.category === '电影')
}
if (selectedCategory.value === '电视剧') {
return dramas.value.filter(drama => drama.category === '电视剧')
}
// Handle new combined categories
const [category, subType] = selectedCategory.value.split('-')
if (subType) {
return dramas.value.filter(drama => drama.category === category && drama.sub_type === subType)
}
return dramas.value
}) })
const movieCount = computed(() => { const movieCount = computed(() => {
@@ -262,9 +286,6 @@ const fetchDramas = async () => {
page: 1, page: 1,
page_size: 1000 page_size: 1000
} }
if (selectedCategory.value) {
params.category = selectedCategory.value
}
const response = await hotDramaApi.getHotDramas(params) const response = await hotDramaApi.getHotDramas(params)
if (response && response.items) { if (response && response.items) {
dramas.value = response.items dramas.value = response.items
@@ -296,10 +317,17 @@ const formatNumber = (num) => {
return num.toString() return num.toString()
} }
// 处理图片加载错误 // 处理图片加载错误 - 显示占位图
const handleImageError = (event) => { const handleImageError = (event) => {
console.log('图片加载失败:', event.target.src) console.log('图片加载失败:', event.target.src)
event.target.style.display = 'none' // 设置占位图片
event.target.src = 'data:image/svg+xml;base64,' + btoa(`
<svg width="400" height="208" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#374151"/>
<text x="50%" y="50%" font-family="Arial" font-size="14" fill="#9CA3AF" text-anchor="middle" dy=".35em">暂无封面</text>
</svg>
`)
event.target.style.background = '#374151'
} }
// 处理图片加载成功 // 处理图片加载成功
@@ -309,6 +337,7 @@ const handleImageLoad = (event) => {
// 监听分类变化 // 监听分类变化
watch(selectedCategory, () => { watch(selectedCategory, () => {
visibleItems.value.clear() // 清空可见项目集合
fetchDramas() fetchDramas()
}) })
@@ -318,14 +347,71 @@ onMounted(() => {
fetchDramas() fetchDramas()
}) })
// Intersection Observer 用于懒加载图片
let observer = null
const initIntersectionObserver = () => {
if (observer) observer.disconnect()
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const itemId = entry.target.getAttribute('data-item-id')
if (!itemId) return
if (entry.isIntersecting) {
// 元素进入视窗,添加到可见集合
visibleItems.value.add(Number(itemId))
} else {
// 元素离开视窗,如果需要可以移除
// visibleItems.value.delete(Number(itemId)) // 可选,如果想重复懒加载
}
})
}, {
rootMargin: '100px 0px 100px 0px', // 提前100px和延后100px
threshold: 0.1
})
// 观察所有卡片
nextTick(() => {
const cards = document.querySelectorAll('[data-item-id]')
cards.forEach(card => {
observer?.observe(card)
})
})
}
const cleanupObserver = () => {
if (observer) {
observer.disconnect()
observer = null
}
}
// 监听数据变化 // 监听数据变化
watch(dramas, (newDramas) => { watch(dramas, (newDramas) => {
console.log('dramas数据变化:', newDramas?.length) console.log('dramas数据变化:', newDramas?.length)
if (newDramas && newDramas.length > 0) { if (newDramas && newDramas.length > 0) {
console.log('第一条数据:', newDramas[0]) console.log('第一条数据:', newDramas[0])
console.log('第一条数据的poster_url:', newDramas[0].poster_url) console.log('第一条数据的poster_url:', newDramas[0].poster_url)
visibleItems.value.clear()
// 延迟一帧后初始化观察器
nextTick(() => {
initIntersectionObserver()
})
} }
}, { deep: true }) }, { deep: true })
// 页面加载时获取数据
onMounted(() => {
console.log('热播剧页面加载')
fetchDramas()
})
// 页面卸载时清理观察器
onUnmounted(() => {
cleanupObserver()
})
</script> </script>
<style scoped> <style scoped>