mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-02-15 10:19:32 +08:00
国家地区接口优化
This commit is contained in:
@@ -17,6 +17,7 @@ log:
|
|||||||
api:
|
api:
|
||||||
mode: local # local | redirect
|
mode: local # local | redirect
|
||||||
enable_mkt_fallback: true # 当请求的地区不存在时,是否回退到默认地区
|
enable_mkt_fallback: true # 当请求的地区不存在时,是否回退到默认地区
|
||||||
|
enable_on_demand_fetch: false # 是否开启按需抓取(当数据库中没有请求的地区图片时,实时从 Bing 抓取)
|
||||||
|
|
||||||
cron:
|
cron:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
23
docs/docs.go
23
docs/docs.go
@@ -675,6 +675,29 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/images/global/today": {
|
||||||
|
"get": {
|
||||||
|
"description": "获取配置文件中所有已开启地区的今日必应图片元数据(缩略图)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"summary": "获取所有地区的今日图片列表",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handlers.ImageMetaResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/regions": {
|
"/regions": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。",
|
"description": "返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。",
|
||||||
|
|||||||
@@ -669,6 +669,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/images/global/today": {
|
||||||
|
"get": {
|
||||||
|
"description": "获取配置文件中所有已开启地区的今日必应图片元数据(缩略图)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"summary": "获取所有地区的今日图片列表",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handlers.ImageMetaResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/regions": {
|
"/regions": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。",
|
"description": "返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。",
|
||||||
|
|||||||
@@ -700,6 +700,21 @@ paths:
|
|||||||
summary: 获取图片列表
|
summary: 获取图片列表
|
||||||
tags:
|
tags:
|
||||||
- image
|
- image
|
||||||
|
/images/global/today:
|
||||||
|
get:
|
||||||
|
description: 获取配置文件中所有已开启地区的今日必应图片元数据(缩略图)
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/handlers.ImageMetaResp'
|
||||||
|
type: array
|
||||||
|
summary: 获取所有地区的今日图片列表
|
||||||
|
tags:
|
||||||
|
- image
|
||||||
/regions:
|
/regions:
|
||||||
get:
|
get:
|
||||||
description: 返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。
|
description: 返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ func (c LogConfig) GetShowDBLog() bool { return c.ShowDBLog }
|
|||||||
func (c LogConfig) GetDBLogLevel() string { return c.DBLogLevel }
|
func (c LogConfig) GetDBLogLevel() string { return c.DBLogLevel }
|
||||||
|
|
||||||
type APIConfig struct {
|
type APIConfig struct {
|
||||||
Mode string `mapstructure:"mode" yaml:"mode"` // local | redirect
|
Mode string `mapstructure:"mode" yaml:"mode"` // local | redirect
|
||||||
EnableMktFallback bool `mapstructure:"enable_mkt_fallback" yaml:"enable_mkt_fallback"` // 当请求的地区不存在时,是否回退到默认地区
|
EnableMktFallback bool `mapstructure:"enable_mkt_fallback" yaml:"enable_mkt_fallback"` // 当请求的地区不存在时,是否回退到默认地区
|
||||||
|
EnableOnDemandFetch bool `mapstructure:"enable_on_demand_fetch" yaml:"enable_on_demand_fetch"` // 是否启用按需抓取
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronConfig struct {
|
type CronConfig struct {
|
||||||
@@ -165,7 +166,8 @@ func Init(configPath string) error {
|
|||||||
v.SetDefault("log.show_db_log", false)
|
v.SetDefault("log.show_db_log", false)
|
||||||
v.SetDefault("log.db_log_level", "info")
|
v.SetDefault("log.db_log_level", "info")
|
||||||
v.SetDefault("api.mode", "redirect")
|
v.SetDefault("api.mode", "redirect")
|
||||||
v.SetDefault("api.enable_mkt_fallback", true)
|
v.SetDefault("api.enable_mkt_fallback", false)
|
||||||
|
v.SetDefault("api.enable_on_demand_fetch", false)
|
||||||
v.SetDefault("cron.enabled", true)
|
v.SetDefault("cron.enabled", true)
|
||||||
v.SetDefault("cron.daily_spec", "20 8-23/4 * * *")
|
v.SetDefault("cron.daily_spec", "20 8-23/4 * * *")
|
||||||
v.SetDefault("retention.days", 0)
|
v.SetDefault("retention.days", 0)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"BingPaper/internal/config"
|
"BingPaper/internal/config"
|
||||||
"BingPaper/internal/model"
|
"BingPaper/internal/model"
|
||||||
@@ -48,12 +48,13 @@ type ImageMetaResp struct {
|
|||||||
// @Param format query string false "格式 (jpg)" default(jpg)
|
// @Param format query string false "格式 (jpg)" default(jpg)
|
||||||
// @Produce image/jpeg
|
// @Produce image/jpeg
|
||||||
// @Success 200 {file} binary
|
// @Success 200 {file} binary
|
||||||
|
// @Failure 404 {object} map[string]string "图片未找到,响应体包含具体原因"
|
||||||
// @Router /image/today [get]
|
// @Router /image/today [get]
|
||||||
func GetToday(c *gin.Context) {
|
func GetToday(c *gin.Context) {
|
||||||
mkt := c.Query("mkt")
|
mkt := c.Query("mkt")
|
||||||
img, err := image.GetTodayImage(mkt)
|
img, err := image.GetTodayImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
sendImageNotFound(c, mkt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleImageResponse(c, img, 7200) // 2小时
|
handleImageResponse(c, img, 7200) // 2小时
|
||||||
@@ -66,12 +67,13 @@ func GetToday(c *gin.Context) {
|
|||||||
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ImageMetaResp
|
// @Success 200 {object} ImageMetaResp
|
||||||
|
// @Failure 404 {object} map[string]string "图片未找到,响应体包含具体原因"
|
||||||
// @Router /image/today/meta [get]
|
// @Router /image/today/meta [get]
|
||||||
func GetTodayMeta(c *gin.Context) {
|
func GetTodayMeta(c *gin.Context) {
|
||||||
mkt := c.Query("mkt")
|
mkt := c.Query("mkt")
|
||||||
img, err := image.GetTodayImage(mkt)
|
img, err := image.GetTodayImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
sendImageNotFound(c, mkt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Header("Cache-Control", "public, max-age=7200") // 2小时
|
c.Header("Cache-Control", "public, max-age=7200") // 2小时
|
||||||
@@ -87,12 +89,13 @@ func GetTodayMeta(c *gin.Context) {
|
|||||||
// @Param format query string false "格式" default(jpg)
|
// @Param format query string false "格式" default(jpg)
|
||||||
// @Produce image/jpeg
|
// @Produce image/jpeg
|
||||||
// @Success 200 {file} binary
|
// @Success 200 {file} binary
|
||||||
|
// @Failure 404 {object} map[string]string "图片未找到,响应体包含具体原因"
|
||||||
// @Router /image/random [get]
|
// @Router /image/random [get]
|
||||||
func GetRandom(c *gin.Context) {
|
func GetRandom(c *gin.Context) {
|
||||||
mkt := c.Query("mkt")
|
mkt := c.Query("mkt")
|
||||||
img, err := image.GetRandomImage(mkt)
|
img, err := image.GetRandomImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
sendImageNotFound(c, mkt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleImageResponse(c, img, 0) // 禁用缓存
|
handleImageResponse(c, img, 0) // 禁用缓存
|
||||||
@@ -105,12 +108,13 @@ func GetRandom(c *gin.Context) {
|
|||||||
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ImageMetaResp
|
// @Success 200 {object} ImageMetaResp
|
||||||
|
// @Failure 404 {object} map[string]string "图片未找到,响应体包含具体原因"
|
||||||
// @Router /image/random/meta [get]
|
// @Router /image/random/meta [get]
|
||||||
func GetRandomMeta(c *gin.Context) {
|
func GetRandomMeta(c *gin.Context) {
|
||||||
mkt := c.Query("mkt")
|
mkt := c.Query("mkt")
|
||||||
img, err := image.GetRandomImage(mkt)
|
img, err := image.GetRandomImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
sendImageNotFound(c, mkt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
@@ -127,13 +131,14 @@ func GetRandomMeta(c *gin.Context) {
|
|||||||
// @Param format query string false "格式" default(jpg)
|
// @Param format query string false "格式" default(jpg)
|
||||||
// @Produce image/jpeg
|
// @Produce image/jpeg
|
||||||
// @Success 200 {file} binary
|
// @Success 200 {file} binary
|
||||||
|
// @Failure 404 {object} map[string]string "图片未找到,响应体包含具体原因"
|
||||||
// @Router /image/date/{date} [get]
|
// @Router /image/date/{date} [get]
|
||||||
func GetByDate(c *gin.Context) {
|
func GetByDate(c *gin.Context) {
|
||||||
date := c.Param("date")
|
date := c.Param("date")
|
||||||
mkt := c.Query("mkt")
|
mkt := c.Query("mkt")
|
||||||
img, err := image.GetImageByDate(date, mkt)
|
img, err := image.GetImageByDate(date, mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
sendImageNotFound(c, mkt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleImageResponse(c, img, 604800) // 7天
|
handleImageResponse(c, img, 604800) // 7天
|
||||||
@@ -147,13 +152,14 @@ func GetByDate(c *gin.Context) {
|
|||||||
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ImageMetaResp
|
// @Success 200 {object} ImageMetaResp
|
||||||
|
// @Failure 404 {object} map[string]string "图片未找到,响应体包含具体原因"
|
||||||
// @Router /image/date/{date}/meta [get]
|
// @Router /image/date/{date}/meta [get]
|
||||||
func GetByDateMeta(c *gin.Context) {
|
func GetByDateMeta(c *gin.Context) {
|
||||||
date := c.Param("date")
|
date := c.Param("date")
|
||||||
mkt := c.Query("mkt")
|
mkt := c.Query("mkt")
|
||||||
img, err := image.GetImageByDate(date, mkt)
|
img, err := image.GetImageByDate(date, mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
sendImageNotFound(c, mkt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
||||||
@@ -223,6 +229,55 @@ func ListImages(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListGlobalTodayImages 获取所有地区的今日图片列表
|
||||||
|
// @Summary 获取所有地区的今日图片列表
|
||||||
|
// @Description 获取配置文件中所有已开启地区的今日必应图片元数据(缩略图)
|
||||||
|
// @Tags image
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} ImageMetaResp
|
||||||
|
// @Router /images/global/today [get]
|
||||||
|
func ListGlobalTodayImages(c *gin.Context) {
|
||||||
|
images, err := image.GetAllRegionsTodayImages()
|
||||||
|
if err != nil {
|
||||||
|
util.Logger.Error("ListGlobalTodayImages service call failed", zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []gin.H{}
|
||||||
|
for _, img := range images {
|
||||||
|
result = append(result, formatMetaSummary(&img))
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendImageNotFound(c *gin.Context, mkt string) {
|
||||||
|
cfg := config.GetConfig().API
|
||||||
|
message := "image not found"
|
||||||
|
|
||||||
|
if mkt != "" {
|
||||||
|
reasons := []string{}
|
||||||
|
if !util.IsValidRegion(mkt) {
|
||||||
|
reasons = append(reasons, fmt.Sprintf("[%s] is not a standard region code", mkt))
|
||||||
|
} else {
|
||||||
|
if !cfg.EnableOnDemandFetch {
|
||||||
|
reasons = append(reasons, "on-demand fetch is disabled")
|
||||||
|
}
|
||||||
|
if !cfg.EnableMktFallback {
|
||||||
|
reasons = append(reasons, "region fallback is disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reasons) > 0 {
|
||||||
|
message = fmt.Sprintf("Image not found for region [%s]. Reasons: %s.", mkt, strings.Join(reasons, ", "))
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("Image not found for region [%s] even after on-demand fetch and fallback attempts.", mkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": message})
|
||||||
|
}
|
||||||
|
|
||||||
func handleImageResponse(c *gin.Context, img *model.Image, maxAge int) {
|
func handleImageResponse(c *gin.Context, img *model.Image, maxAge int) {
|
||||||
variant := c.DefaultQuery("variant", "UHD")
|
variant := c.DefaultQuery("variant", "UHD")
|
||||||
format := c.DefaultQuery("format", "jpg")
|
format := c.DefaultQuery("format", "jpg")
|
||||||
@@ -387,34 +442,51 @@ func GetRegions(c *gin.Context) {
|
|||||||
cfg := config.GetConfig()
|
cfg := config.GetConfig()
|
||||||
pinned := cfg.Fetcher.Regions
|
pinned := cfg.Fetcher.Regions
|
||||||
|
|
||||||
// 创建副本以避免修改原始全局变量
|
if len(pinned) == 0 {
|
||||||
all := make([]util.Region, len(util.AllRegions))
|
// 如果没有配置抓取地区,返回所有支持的地区
|
||||||
copy(all, util.AllRegions)
|
c.JSON(http.StatusOK, util.AllRegions)
|
||||||
|
return
|
||||||
if len(pinned) > 0 {
|
|
||||||
// 创建一个 Map 用于快速查找置顶地区及其顺序
|
|
||||||
pinnedMap := make(map[string]int)
|
|
||||||
for i, v := range pinned {
|
|
||||||
pinnedMap[v] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对列表进行稳定排序,使置顶地区排在前面
|
|
||||||
sort.SliceStable(all, func(i, j int) bool {
|
|
||||||
idxI, okI := pinnedMap[all[i].Value]
|
|
||||||
idxJ, okJ := pinnedMap[all[j].Value]
|
|
||||||
|
|
||||||
if okI && okJ {
|
|
||||||
return idxI < idxJ
|
|
||||||
}
|
|
||||||
if okI {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if okJ {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return false // 保持非置顶地区的原有相对顺序
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, all)
|
// 创建一个 Map 用于快速查找配置的地区
|
||||||
|
pinnedMap := make(map[string]bool)
|
||||||
|
for _, v := range pinned {
|
||||||
|
pinnedMap[v] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只返回配置中的地区,并保持配置中的顺序
|
||||||
|
var result []util.Region
|
||||||
|
// 为了保持配置顺序,我们遍历 pinned 而不是 AllRegions
|
||||||
|
for _, pVal := range pinned {
|
||||||
|
for _, r := range util.AllRegions {
|
||||||
|
if r.Value == pVal {
|
||||||
|
result = append(result, r)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果配置了一些不在 AllRegions 里的 mkt,上述循环可能漏掉
|
||||||
|
// 但根据之前的逻辑,AllRegions 是已知的 17 个地区。
|
||||||
|
// 如果用户配置了 fr-CA (不在 17 个内),我们也应该返回它吗?
|
||||||
|
// 需求说 "前端页面对地区进行约束",如果配置了,前端就该显示。
|
||||||
|
// 如果不在 AllRegions 里的,我们直接返回原始编码作为 label 或者查找一下。
|
||||||
|
|
||||||
|
if len(result) < len(pinned) {
|
||||||
|
// 补全不在 AllRegions 里的地区
|
||||||
|
for _, pVal := range pinned {
|
||||||
|
found := false
|
||||||
|
for _, r := range result {
|
||||||
|
if r.Value == pVal {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
result = append(result, util.Region{Value: pVal, Label: pVal})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func SetupRouter(webFS embed.FS) *gin.Engine {
|
|||||||
img.GET("/date/:date/meta", handlers.GetByDateMeta)
|
img.GET("/date/:date/meta", handlers.GetByDateMeta)
|
||||||
}
|
}
|
||||||
api.GET("/images", handlers.ListImages)
|
api.GET("/images", handlers.ListImages)
|
||||||
|
api.GET("/images/global/today", handlers.ListGlobalTodayImages)
|
||||||
api.GET("/regions", handlers.GetRegions)
|
api.GET("/regions", handlers.GetRegions)
|
||||||
|
|
||||||
// 管理接口
|
// 管理接口
|
||||||
|
|||||||
@@ -61,15 +61,8 @@ func (f *Fetcher) Fetch(ctx context.Context, n int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, mkt := range regions {
|
for _, mkt := range regions {
|
||||||
util.Logger.Info("Fetching images for region", zap.String("mkt", mkt))
|
if err := f.FetchRegion(ctx, mkt); err != nil {
|
||||||
// 调用两次 API 获取最多两周的数据
|
util.Logger.Error("Failed to fetch region images", zap.String("mkt", mkt), zap.Error(err))
|
||||||
// 第一次 idx=0&n=8 (今天起往回数 8 张)
|
|
||||||
if err := f.fetchByMkt(ctx, mkt, 0, 8); err != nil {
|
|
||||||
util.Logger.Error("Failed to fetch images", zap.String("mkt", mkt), zap.Int("idx", 0), zap.Error(err))
|
|
||||||
}
|
|
||||||
// 第二次 idx=7&n=8 (7天前起往回数 8 张,与第一次有重叠,确保不漏)
|
|
||||||
if err := f.fetchByMkt(ctx, mkt, 7, 8); err != nil {
|
|
||||||
util.Logger.Error("Failed to fetch images", zap.String("mkt", mkt), zap.Int("idx", 7), zap.Error(err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +70,27 @@ func (f *Fetcher) Fetch(ctx context.Context, n int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchRegion 抓取指定地区的图片
|
||||||
|
func (f *Fetcher) FetchRegion(ctx context.Context, mkt string) error {
|
||||||
|
if !util.IsValidRegion(mkt) {
|
||||||
|
util.Logger.Warn("Skipping fetch for invalid region", zap.String("mkt", mkt))
|
||||||
|
return fmt.Errorf("invalid region code: %s", mkt)
|
||||||
|
}
|
||||||
|
util.Logger.Info("Fetching images for region", zap.String("mkt", mkt))
|
||||||
|
// 调用两次 API 获取最多两周的数据
|
||||||
|
// 第一次 idx=0&n=8 (今天起往回数 8 张)
|
||||||
|
if err := f.fetchByMkt(ctx, mkt, 0, 8); err != nil {
|
||||||
|
util.Logger.Error("Failed to fetch images", zap.String("mkt", mkt), zap.Int("idx", 0), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 第二次 idx=7&n=8 (7天前起往回数 8 张,与第一次有重叠,确保不漏)
|
||||||
|
if err := f.fetchByMkt(ctx, mkt, 7, 8); err != nil {
|
||||||
|
util.Logger.Error("Failed to fetch images", zap.String("mkt", mkt), zap.Int("idx", 7), zap.Error(err))
|
||||||
|
// 第二次失败不一定返回错误,因为可能第一次已经拿到了
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) fetchByMkt(ctx context.Context, mkt string, idx int, n int) error {
|
func (f *Fetcher) fetchByMkt(ctx context.Context, mkt string, idx int, n int) error {
|
||||||
url := fmt.Sprintf("%s?format=js&idx=%d&n=%d&uhd=1&mkt=%s", config.BingAPIBase, idx, n, mkt)
|
url := fmt.Sprintf("%s?format=js&idx=%d&n=%d&uhd=1&mkt=%s", config.BingAPIBase, idx, n, mkt)
|
||||||
util.Logger.Debug("Requesting Bing API", zap.String("url", url))
|
util.Logger.Debug("Requesting Bing API", zap.String("url", url))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"BingPaper/internal/config"
|
"BingPaper/internal/config"
|
||||||
"BingPaper/internal/model"
|
"BingPaper/internal/model"
|
||||||
"BingPaper/internal/repo"
|
"BingPaper/internal/repo"
|
||||||
|
"BingPaper/internal/service/fetcher"
|
||||||
"BingPaper/internal/storage"
|
"BingPaper/internal/storage"
|
||||||
"BingPaper/internal/util"
|
"BingPaper/internal/util"
|
||||||
|
|
||||||
@@ -58,8 +59,19 @@ func GetTodayImage(mkt string) (*model.Image, error) {
|
|||||||
tx = tx.Where("mkt = ?", mkt)
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
}
|
}
|
||||||
err := tx.Preload("Variants").First(&img).Error
|
err := tx.Preload("Variants").First(&img).Error
|
||||||
|
if err != nil && mkt != "" && config.GetConfig().API.EnableOnDemandFetch && util.IsValidRegion(mkt) {
|
||||||
|
// 如果没找到,尝试按需抓取该地区
|
||||||
|
util.Logger.Info("Image not found in DB, attempting on-demand fetch", zap.String("mkt", mkt))
|
||||||
|
f := fetcher.NewFetcher()
|
||||||
|
_ = f.FetchRegion(context.Background(), mkt)
|
||||||
|
|
||||||
|
// 抓取后重新查询
|
||||||
|
tx = repo.DB.Where("date = ?", today).Where("mkt = ?", mkt)
|
||||||
|
err = tx.Preload("Variants").First(&img).Error
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 如果今天没有,尝试获取最近的一张
|
// 如果今天还是没有,尝试获取最近的一张
|
||||||
tx = repo.DB.Order("date desc")
|
tx = repo.DB.Order("date desc")
|
||||||
if mkt != "" {
|
if mkt != "" {
|
||||||
tx = tx.Where("mkt = ?", mkt)
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
@@ -79,6 +91,22 @@ func GetTodayImage(mkt string) (*model.Image, error) {
|
|||||||
return &img, err
|
return &img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAllRegionsTodayImages() ([]model.Image, error) {
|
||||||
|
regions := config.GetConfig().Fetcher.Regions
|
||||||
|
if len(regions) == 0 {
|
||||||
|
regions = []string{config.GetConfig().GetDefaultMkt()}
|
||||||
|
}
|
||||||
|
|
||||||
|
var images []model.Image
|
||||||
|
for _, mkt := range regions {
|
||||||
|
img, err := GetTodayImage(mkt)
|
||||||
|
if err == nil {
|
||||||
|
images = append(images, *img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return images, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetRandomImage(mkt string) (*model.Image, error) {
|
func GetRandomImage(mkt string) (*model.Image, error) {
|
||||||
var img model.Image
|
var img model.Image
|
||||||
// SQLite 使用 RANDOM(), MySQL/Postgres 使用 RANDOM() 或 RAND()
|
// SQLite 使用 RANDOM(), MySQL/Postgres 使用 RANDOM() 或 RAND()
|
||||||
@@ -89,6 +117,17 @@ func GetRandomImage(mkt string) (*model.Image, error) {
|
|||||||
tx = tx.Where("mkt = ?", mkt)
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
}
|
}
|
||||||
tx.Count(&count)
|
tx.Count(&count)
|
||||||
|
if count == 0 && mkt != "" && config.GetConfig().API.EnableOnDemandFetch && util.IsValidRegion(mkt) {
|
||||||
|
// 如果没找到,尝试按需抓取该地区
|
||||||
|
util.Logger.Info("No images found in DB for region, attempting on-demand fetch", zap.String("mkt", mkt))
|
||||||
|
f := fetcher.NewFetcher()
|
||||||
|
_ = f.FetchRegion(context.Background(), mkt)
|
||||||
|
|
||||||
|
// 抓取后重新计数
|
||||||
|
tx = repo.DB.Model(&model.Image{}).Where("mkt = ?", mkt)
|
||||||
|
tx.Count(&count)
|
||||||
|
}
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return nil, fmt.Errorf("no images found")
|
return nil, fmt.Errorf("no images found")
|
||||||
}
|
}
|
||||||
@@ -127,6 +166,16 @@ func GetImageByDate(date string, mkt string) (*model.Image, error) {
|
|||||||
tx = tx.Where("mkt = ?", mkt)
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
}
|
}
|
||||||
err := tx.Preload("Variants").First(&img).Error
|
err := tx.Preload("Variants").First(&img).Error
|
||||||
|
if err != nil && mkt != "" && config.GetConfig().API.EnableOnDemandFetch && util.IsValidRegion(mkt) {
|
||||||
|
// 如果没找到,尝试按需抓取该地区
|
||||||
|
util.Logger.Info("Image not found in DB for date, attempting on-demand fetch", zap.String("mkt", mkt), zap.String("date", date))
|
||||||
|
f := fetcher.NewFetcher()
|
||||||
|
_ = f.FetchRegion(context.Background(), mkt)
|
||||||
|
|
||||||
|
// 抓取后重新查询
|
||||||
|
tx = repo.DB.Where("date = ?", date).Where("mkt = ?", mkt)
|
||||||
|
err = tx.Preload("Variants").First(&img).Error
|
||||||
|
}
|
||||||
|
|
||||||
// 兜底逻辑
|
// 兜底逻辑
|
||||||
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
|
import "golang.org/x/text/language"
|
||||||
|
|
||||||
type Region struct {
|
type Region struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var AllRegions = []Region{
|
// IsValidRegion 校验是否为标准的地区编码 (BCP 47)
|
||||||
{Value: "zh-CN", Label: "中国 (zh-CN)"},
|
func IsValidRegion(mkt string) bool {
|
||||||
{Value: "en-US", Label: "美国 (en-US)"},
|
if mkt == "" {
|
||||||
{Value: "ja-JP", Label: "日本 (ja-JP)"},
|
return false
|
||||||
{Value: "en-AU", Label: "澳大利亚 (en-AU)"},
|
}
|
||||||
{Value: "en-GB", Label: "英国 (en-GB)"},
|
_, err := language.Parse(mkt)
|
||||||
{Value: "de-DE", Label: "德国 (de-DE)"},
|
return err == nil
|
||||||
{Value: "en-NZ", Label: "新西兰 (en-NZ)"},
|
}
|
||||||
{Value: "en-CA", Label: "加拿大 (en-CA)"},
|
|
||||||
{Value: "fr-FR", Label: "法国 (fr-FR)"},
|
var AllRegions = []Region{
|
||||||
{Value: "it-IT", Label: "意大利 (it-IT)"},
|
{Value: "zh-CN", Label: "中国"},
|
||||||
{Value: "es-ES", Label: "西班牙 (es-ES)"},
|
{Value: "en-US", Label: "美国"},
|
||||||
{Value: "pt-BR", Label: "巴西 (pt-BR)"},
|
{Value: "ja-JP", Label: "日本"},
|
||||||
{Value: "ko-KR", Label: "韩国 (ko-KR)"},
|
{Value: "en-AU", Label: "澳大利亚"},
|
||||||
{Value: "en-IN", Label: "印度 (en-IN)"},
|
{Value: "en-GB", Label: "英国"},
|
||||||
{Value: "ru-RU", Label: "俄罗斯 (ru-RU)"},
|
{Value: "de-DE", Label: "德国"},
|
||||||
{Value: "zh-HK", Label: "中国香港 (zh-HK)"},
|
{Value: "en-NZ", Label: "新西兰"},
|
||||||
{Value: "zh-TW", Label: "中国台湾 (zh-TW)"},
|
{Value: "en-CA", Label: "加拿大"},
|
||||||
|
{Value: "fr-FR", Label: "法国"},
|
||||||
|
{Value: "it-IT", Label: "意大利"},
|
||||||
|
{Value: "es-ES", Label: "西班牙"},
|
||||||
|
{Value: "pt-BR", Label: "巴西"},
|
||||||
|
{Value: "ko-KR", Label: "韩国"},
|
||||||
|
{Value: "en-IN", Label: "印度"},
|
||||||
|
{Value: "ru-RU", Label: "俄罗斯"},
|
||||||
|
{Value: "zh-HK", Label: "中国香港"},
|
||||||
|
{Value: "zh-TW", Label: "中国台湾"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,39 @@ export function useTodayImage(mkt?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全球今日图片
|
||||||
|
*/
|
||||||
|
export function useGlobalTodayImages() {
|
||||||
|
const images = ref<ImageMeta[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<Error | null>(null)
|
||||||
|
|
||||||
|
const fetchImages = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
images.value = await bingPaperApi.getGlobalTodayImages()
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e as Error
|
||||||
|
console.error('Failed to fetch global today images:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchImages()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
images,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchImages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取图片列表(支持分页和月份筛选)
|
* 获取图片列表(支持分页和月份筛选)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ export class BingPaperApiService {
|
|||||||
return apiClient.get<ImageMeta[]>(endpoint)
|
return apiClient.get<ImageMeta[]>(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有地区的今日图片列表
|
||||||
|
*/
|
||||||
|
async getGlobalTodayImages(): Promise<ImageMeta[]> {
|
||||||
|
return apiClient.get<ImageMeta[]>('/images/global/today')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取支持的地区列表
|
* 获取支持的地区列表
|
||||||
*/
|
*/
|
||||||
@@ -212,6 +219,7 @@ export const {
|
|||||||
manualFetch,
|
manualFetch,
|
||||||
manualCleanup,
|
manualCleanup,
|
||||||
getImages,
|
getImages,
|
||||||
|
getGlobalTodayImages,
|
||||||
getRegions,
|
getRegions,
|
||||||
getTodayImageMeta,
|
getTodayImageMeta,
|
||||||
getImageMetaByDate,
|
getImageMetaByDate,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export interface AdminConfig {
|
|||||||
export interface APIConfig {
|
export interface APIConfig {
|
||||||
Mode: string // 'local' | 'redirect'
|
Mode: string // 'local' | 'redirect'
|
||||||
EnableMktFallback: boolean
|
EnableMktFallback: boolean
|
||||||
|
EnableOnDemandFetch: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CronConfig {
|
export interface CronConfig {
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ const DEFAULT_MKT = 'zh-CN'
|
|||||||
* 默认地区列表 (兜底用)
|
* 默认地区列表 (兜底用)
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_REGIONS = [
|
export const DEFAULT_REGIONS = [
|
||||||
{ value: 'zh-CN', label: '中国 (zh-CN)' },
|
{ value: 'zh-CN', label: '中国' },
|
||||||
{ value: 'en-US', label: '美国 (en-US)' },
|
{ value: 'en-US', label: '美国' },
|
||||||
{ value: 'ja-JP', label: '日本 (ja-JP)' },
|
{ value: 'ja-JP', label: '日本' },
|
||||||
{ value: 'en-AU', label: '澳大利亚 (en-AU)' },
|
{ value: 'en-AU', label: '澳大利亚' },
|
||||||
{ value: 'en-GB', label: '英国 (en-GB)' },
|
{ value: 'en-GB', label: '英国' },
|
||||||
{ value: 'de-DE', label: '德国 (de-DE)' },
|
{ value: 'de-DE', label: '德国' },
|
||||||
{ value: 'en-NZ', label: '新西兰 (en-NZ)' },
|
{ value: 'en-NZ', label: '新西兰' },
|
||||||
{ value: 'en-CA', label: '加拿大 (en-CA)' },
|
{ value: 'en-CA', label: '加拿大' },
|
||||||
{ value: 'fr-FR', label: '法国 (fr-FR)' },
|
{ value: 'fr-FR', label: '法国' },
|
||||||
{ value: 'it-IT', label: '意大利 (it-IT)' },
|
{ value: 'it-IT', label: '意大利' },
|
||||||
{ value: 'es-ES', label: '西班牙 (es-ES)' },
|
{ value: 'es-ES', label: '西班牙' },
|
||||||
{ value: 'pt-BR', label: '巴西 (pt-BR)' },
|
{ value: 'pt-BR', label: '巴西' },
|
||||||
{ value: 'ko-KR', label: '韩国 (ko-KR)' },
|
{ value: 'ko-KR', label: '韩国' },
|
||||||
{ value: 'en-IN', label: '印度 (en-IN)' },
|
{ value: 'en-IN', label: '印度' },
|
||||||
{ value: 'ru-RU', label: '俄罗斯 (ru-RU)' },
|
{ value: 'ru-RU', label: '俄罗斯' },
|
||||||
{ value: 'zh-HK', label: '中国香港 (zh-HK)' },
|
{ value: 'zh-HK', label: '中国香港' },
|
||||||
{ value: 'zh-TW', label: '中国台湾 (zh-TW)' },
|
{ value: 'zh-TW', label: '中国台湾' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -102,16 +102,33 @@
|
|||||||
local: 直接返回图片流; redirect: 重定向到存储位置
|
local: 直接返回图片流; redirect: 重定向到存储位置
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="space-y-4">
|
||||||
<Label for="api-fallback">启用地区不存在时兜底</Label>
|
<div class="flex items-center justify-between">
|
||||||
<Switch
|
<div class="space-y-0.5">
|
||||||
id="api-fallback"
|
<Label for="api-fallback">启用地区不存在时兜底</Label>
|
||||||
v-model="config.API.EnableMktFallback"
|
<p class="text-xs text-gray-500">
|
||||||
/>
|
如果请求的地区无数据,自动回退到默认地区
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="api-fallback"
|
||||||
|
v-model="config.API.EnableMktFallback"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Label for="api-on-demand">启用按需实时抓取</Label>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
如果请求的地区无数据,尝试实时从 Bing 抓取
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="api-on-demand"
|
||||||
|
v-model="config.API.EnableOnDemandFetch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500">
|
|
||||||
如果请求的地区无数据,自动回退到默认地区
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Global Section - 必应全球 -->
|
||||||
|
<section v-if="globalImages.length > 0" class="py-16 px-4 md:px-8 lg:px-16 bg-white/5 border-y border-white/5">
|
||||||
|
<div class="max-w-7xl mx-auto">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-white">
|
||||||
|
必应全球
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative group">
|
||||||
|
<!-- 左右切换按钮 -->
|
||||||
|
<button
|
||||||
|
@click="scrollGlobal('left')"
|
||||||
|
class="absolute left-[-20px] top-1/2 -translate-y-1/2 z-20 p-2 bg-black/50 backdrop-blur-md rounded-full text-white opacity-0 group-hover:opacity-100 transition-opacity hidden md:block border border-white/10 hover:bg-black/70"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="scrollGlobal('right')"
|
||||||
|
class="absolute right-[-20px] top-1/2 -translate-y-1/2 z-20 p-2 bg-black/50 backdrop-blur-md rounded-full text-white opacity-0 group-hover:opacity-100 transition-opacity hidden md:block border border-white/10 hover:bg-black/70"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="globalScrollContainer"
|
||||||
|
class="flex overflow-x-auto gap-6 pb-6 -mx-4 px-4 md:mx-0 md:px-0 scrollbar-hide snap-x scroll-smooth"
|
||||||
|
@wheel="handleGlobalWheel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="image in globalImages"
|
||||||
|
:key="image.mkt! + image.date!"
|
||||||
|
class="flex-none w-[280px] md:w-[350px] aspect-video rounded-xl overflow-hidden cursor-pointer transform transition-all duration-500 hover:scale-[1.02] hover:shadow-2xl snap-start relative group/card"
|
||||||
|
@click="viewImage(image.date!, image.mkt!)"
|
||||||
|
>
|
||||||
|
<!-- 图片层 -->
|
||||||
|
<img
|
||||||
|
:src="getImageUrl(image)"
|
||||||
|
:alt="image.title"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-700 group-hover/card:scale-110"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 渐变层 -->
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent group-hover/card:via-black/40 transition-colors duration-500"></div>
|
||||||
|
|
||||||
|
<!-- 内容层 -->
|
||||||
|
<div class="absolute inset-0 flex flex-col justify-end p-5">
|
||||||
|
<div class="text-[10px] md:text-xs text-white/60 mb-1 transform translate-y-2 group-hover/card:translate-y-0 transition-transform duration-500">
|
||||||
|
{{ formatDate(image.date) }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mb-2 transform translate-y-2 group-hover/card:translate-y-0 transition-transform duration-500">
|
||||||
|
<span class="text-xs text-white/70 font-medium">
|
||||||
|
{{ getRegionLabel(image.mkt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-white font-bold text-base md:text-lg line-clamp-1 transform translate-y-1 group-hover/card:translate-y-0 transition-transform duration-500">
|
||||||
|
{{ image.title || '必应每日一图' }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左右渐变遮罩 (仅在大屏显示) -->
|
||||||
|
<div class="absolute right-0 top-0 bottom-6 w-24 bg-gradient-to-l from-gray-900 to-transparent pointer-events-none hidden md:block opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Gallery Section - 历史图片 -->
|
<!-- Gallery Section - 历史图片 -->
|
||||||
<section class="py-16 px-4 md:px-8 lg:px-16">
|
<section class="py-16 px-4 md:px-8 lg:px-16">
|
||||||
<div class="max-w-7xl mx-auto">
|
<div class="max-w-7xl mx-auto">
|
||||||
@@ -262,7 +337,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useImageList } from '@/composables/useImages'
|
import { useImageList, useGlobalTodayImages } from '@/composables/useImages'
|
||||||
import { bingPaperApi } from '@/lib/api-service'
|
import { bingPaperApi } from '@/lib/api-service'
|
||||||
import { normalizeImageUrl } from '@/lib/api-config'
|
import { normalizeImageUrl } from '@/lib/api-config'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -284,9 +359,50 @@ const regions = ref(SUPPORTED_REGIONS)
|
|||||||
const latestImage = ref<any>(null)
|
const latestImage = ref<any>(null)
|
||||||
const todayLoading = ref(false)
|
const todayLoading = ref(false)
|
||||||
|
|
||||||
|
// 全球今日图片
|
||||||
|
const { images: globalImages, loading: globalLoading } = useGlobalTodayImages()
|
||||||
|
|
||||||
|
// 滚动功能实现
|
||||||
|
const globalScrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
const scrollGlobal = (direction: 'left' | 'right') => {
|
||||||
|
if (!globalScrollContainer.value) return
|
||||||
|
// 增加翻页数量:滚动容器宽度的 80%,或者固定 3 张卡片的宽度
|
||||||
|
const scrollAmount = globalScrollContainer.value.clientWidth * 0.8 || 1000
|
||||||
|
globalScrollContainer.value.scrollBy({
|
||||||
|
left: direction === 'left' ? -scrollAmount : scrollAmount,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标滚轮横向滑动
|
||||||
|
const handleGlobalWheel = (e: WheelEvent) => {
|
||||||
|
if (!globalScrollContainer.value) return
|
||||||
|
|
||||||
|
const container = globalScrollContainer.value
|
||||||
|
const isAtStart = container.scrollLeft <= 0
|
||||||
|
const isAtEnd = Math.ceil(container.scrollLeft + container.clientWidth) >= container.scrollWidth
|
||||||
|
|
||||||
|
// 当鼠标在滚动区域内,且是纵向滚动时
|
||||||
|
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
||||||
|
// 如果还没滚动到尽头,则拦截纵向滚动并转为横向
|
||||||
|
if ((e.deltaY > 0 && !isAtEnd) || (e.deltaY < 0 && !isAtStart)) {
|
||||||
|
e.preventDefault()
|
||||||
|
// 加大滚动步进以实现“快速翻页”
|
||||||
|
container.scrollLeft += e.deltaY * 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 历史图片列表(使用服务端分页和筛选,每页15张)
|
// 历史图片列表(使用服务端分页和筛选,每页15张)
|
||||||
const { images, loading, hasMore, loadMore, filterByMonth, filterByMkt } = useImageList(15)
|
const { images, loading, hasMore, loadMore, filterByMonth, filterByMkt } = useImageList(15)
|
||||||
|
|
||||||
|
// 获取地区标签
|
||||||
|
const getRegionLabel = (mkt?: string) => {
|
||||||
|
if (!mkt) return ''
|
||||||
|
const region = regions.value.find(r => r.value === mkt)
|
||||||
|
return region ? region.label : mkt
|
||||||
|
}
|
||||||
|
|
||||||
// 加载顶部最新图片
|
// 加载顶部最新图片
|
||||||
const loadLatestImage = async () => {
|
const loadLatestImage = async () => {
|
||||||
todayLoading.value = true
|
todayLoading.value = true
|
||||||
@@ -580,8 +696,9 @@ const getImageUrl = (image: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查看图片详情
|
// 查看图片详情
|
||||||
const viewImage = (date: string) => {
|
const viewImage = (date: string, mkt?: string) => {
|
||||||
router.push(`/image/${date}`)
|
const query = mkt ? `?mkt=${mkt}` : ''
|
||||||
|
router.push(`/image/${date}${query}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开版权详情链接
|
// 打开版权详情链接
|
||||||
@@ -621,4 +738,13 @@ html {
|
|||||||
html::-webkit-scrollbar {
|
html::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari, Opera */
|
display: none; /* Chrome, Safari, Opera */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏横向滚动条 */
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user