mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update:
This commit is contained in:
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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'"` // 数据来源
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -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)
|
||||||
|
|||||||
@@ -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 获取热播剧名称列表(公共方法)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user