mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-03-07 17:09:32 +08:00
Compare commits
11 Commits
v0.0.2
...
5334ee9d41
| Author | SHA1 | Date | |
|---|---|---|---|
| 5334ee9d41 | |||
| c8a7ea5490 | |||
| 61de3f44dc | |||
| 69abe80264 | |||
| 34848e7b91 | |||
| 9ec9a2ba91 | |||
| 3c1f29e4ef | |||
| ae82557545 | |||
| fecbd014b3 | |||
| 907e158f44 | |||
| f7fc3fa506 |
@@ -101,3 +101,5 @@ BingPaper 支持通过配置文件(YAML)和环境变量进行配置。
|
|||||||
- `BINGPAPER_STORAGE_TYPE=s3`
|
- `BINGPAPER_STORAGE_TYPE=s3`
|
||||||
- `BINGPAPER_STORAGE_S3_BUCKET=my-images`
|
- `BINGPAPER_STORAGE_S3_BUCKET=my-images`
|
||||||
- `BINGPAPER_ADMIN_PASSWORD_BCRYPT="$2a$10$..."`
|
- `BINGPAPER_ADMIN_PASSWORD_BCRYPT="$2a$10$..."`
|
||||||
|
- `HOST_PORT=8080` (仅限 Docker Compose 部署,控制宿主机映射到外部的端口)
|
||||||
|
- `BINGPAPER_SERVER_PORT=8080` (控制应用监听端口及容器内部端口)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
# Stage 1: Build Frontend
|
# Stage 1: Build Frontend
|
||||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS node-builder
|
FROM --platform=$BUILDPLATFORM node:20-alpine AS node-builder
|
||||||
|
ARG NPM_REGISTRY
|
||||||
WORKDIR /webapp
|
WORKDIR /webapp
|
||||||
# 复制 package.json 和 lock 文件以利用 layer 缓存
|
# 复制 package.json 和 lock 文件以利用 layer 缓存
|
||||||
COPY webapp/package*.json ./
|
COPY webapp/package*.json ./
|
||||||
|
# 如果设置了 NPM_REGISTRY,则配置 npm 镜像
|
||||||
|
RUN if [ -n "$NPM_REGISTRY" ]; then npm config set registry $NPM_REGISTRY; fi
|
||||||
# 使用 npm ci 以获得更快且可重现的构建(如果存在 package-lock.json)
|
# 使用 npm ci 以获得更快且可重现的构建(如果存在 package-lock.json)
|
||||||
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
||||||
# 复制其余源码并构建
|
# 复制其余源码并构建
|
||||||
@@ -11,6 +14,8 @@ RUN npm run build
|
|||||||
|
|
||||||
# Stage 2: Build Backend
|
# Stage 2: Build Backend
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.25.5-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.25.5-alpine AS builder
|
||||||
|
ARG GOPROXY
|
||||||
|
ENV GOPROXY=$GOPROXY
|
||||||
# 安装 Git 以支持某些 Go 模块依赖
|
# 安装 Git 以支持某些 Go 模块依赖
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ api:
|
|||||||
|
|
||||||
cron:
|
cron:
|
||||||
enabled: true
|
enabled: true
|
||||||
daily_spec: "0 10 * * *"
|
daily_spec: "20 8-23/4 * * *"
|
||||||
|
|
||||||
retention:
|
retention:
|
||||||
days: 0
|
days: 0
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
services:
|
services:
|
||||||
bingpaper:
|
bingpaper:
|
||||||
build: .
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}
|
||||||
|
- NPM_REGISTRY=${NPM_REGISTRY:-https://registry.npmjs.org/}
|
||||||
container_name: bingpaper
|
container_name: bingpaper
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "${HOST_PORT:-8080}:${BINGPAPER_SERVER_PORT:-8080}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
environment:
|
environment:
|
||||||
- BINGPAPER_SERVER_PORT=8080
|
- TZ=${TZ:-Asia/Shanghai}
|
||||||
- BINGPAPER_LOG_LEVEL=info
|
- BINGPAPER_SERVER_PORT=${BINGPAPER_SERVER_PORT:-8080}
|
||||||
- BINGPAPER_API_MODE=local
|
- BINGPAPER_LOG_LEVEL=${BINGPAPER_LOG_LEVEL:-info}
|
||||||
- BINGPAPER_CRON_ENABLED=true
|
- BINGPAPER_API_MODE=${BINGPAPER_API_MODE:-local}
|
||||||
- BINGPAPER_DB_TYPE=sqlite
|
- BINGPAPER_CRON_ENABLED=${BINGPAPER_CRON_ENABLED:-true}
|
||||||
- BINGPAPER_DB_DSN=data/bing_paper.db
|
- BINGPAPER_DB_TYPE=${BINGPAPER_DB_TYPE:-sqlite}
|
||||||
- BINGPAPER_STORAGE_TYPE=local
|
- BINGPAPER_DB_DSN=${BINGPAPER_DB_DSN:-data/bing_paper.db}
|
||||||
- BINGPAPER_STORAGE_LOCAL_ROOT=data/picture
|
- BINGPAPER_STORAGE_TYPE=${BINGPAPER_STORAGE_TYPE:-local}
|
||||||
- BINGPAPER_RETENTION_DAYS=30
|
- BINGPAPER_STORAGE_LOCAL_ROOT=${BINGPAPER_STORAGE_LOCAL_ROOT:-data/picture}
|
||||||
|
- BINGPAPER_RETENTION_DAYS=${BINGPAPER_RETENTION_DAYS:-30}
|
||||||
# S3 配置 (可选)
|
# S3 配置 (可选)
|
||||||
# - BINGPAPER_STORAGE_S3_ENDPOINT=
|
# - BINGPAPER_STORAGE_S3_ENDPOINT=${BINGPAPER_STORAGE_S3_ENDPOINT:-}
|
||||||
# - BINGPAPER_STORAGE_S3_REGION=
|
# - BINGPAPER_STORAGE_S3_REGION=${BINGPAPER_STORAGE_S3_REGION:-}
|
||||||
# - BINGPAPER_STORAGE_S3_BUCKET=
|
# - BINGPAPER_STORAGE_S3_BUCKET=${BINGPAPER_STORAGE_S3_BUCKET:-}
|
||||||
# - BINGPAPER_STORAGE_S3_ACCESS_KEY=
|
# - BINGPAPER_STORAGE_S3_ACCESS_KEY=${BINGPAPER_STORAGE_S3_ACCESS_KEY:-}
|
||||||
# - BINGPAPER_STORAGE_S3_SECRET_KEY=
|
# - BINGPAPER_STORAGE_S3_SECRET_KEY=${BINGPAPER_STORAGE_S3_SECRET_KEY:-}
|
||||||
# - BINGPAPER_STORAGE_S3_PUBLIC_URL_PREFIX=
|
# - BINGPAPER_STORAGE_S3_PUBLIC_URL_PREFIX=${BINGPAPER_STORAGE_S3_PUBLIC_URL_PREFIX:-}
|
||||||
# WebDAV 配置 (可选)
|
# WebDAV 配置 (可选)
|
||||||
# - BINGPAPER_STORAGE_WEBDAV_URL=
|
# - BINGPAPER_STORAGE_WEBDAV_URL=${BINGPAPER_STORAGE_WEBDAV_URL:-}
|
||||||
# - BINGPAPER_STORAGE_WEBDAV_USERNAME=
|
# - BINGPAPER_STORAGE_WEBDAV_USERNAME=${BINGPAPER_STORAGE_WEBDAV_USERNAME:-}
|
||||||
# - BINGPAPER_STORAGE_WEBDAV_PASSWORD=
|
# - BINGPAPER_STORAGE_WEBDAV_PASSWORD=${BINGPAPER_STORAGE_WEBDAV_PASSWORD:-}
|
||||||
# - BINGPAPER_STORAGE_WEBDAV_PUBLIC_URL_PREFIX=
|
# - BINGPAPER_STORAGE_WEBDAV_PUBLIC_URL_PREFIX=${BINGPAPER_STORAGE_WEBDAV_PUBLIC_URL_PREFIX:-}
|
||||||
@@ -36,6 +36,20 @@ func Init(webFS embed.FS, configPath string) *gin.Engine {
|
|||||||
// 2. 初始化日志
|
// 2. 初始化日志
|
||||||
util.InitLogger(cfg.Log)
|
util.InitLogger(cfg.Log)
|
||||||
|
|
||||||
|
// 以 debug 级别输出配置加载详情和环境变量覆盖情况
|
||||||
|
util.Logger.Debug("Configuration loading details",
|
||||||
|
zap.String("config_file", config.GetRawViper().ConfigFileUsed()),
|
||||||
|
)
|
||||||
|
envOverrides := config.GetEnvOverrides()
|
||||||
|
if len(envOverrides) > 0 {
|
||||||
|
for _, override := range envOverrides {
|
||||||
|
util.Logger.Debug("Environment variable override applied", zap.String("detail", override))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
util.Logger.Debug("No environment variable overrides detected")
|
||||||
|
}
|
||||||
|
util.Logger.Debug("Full effective configuration:\n" + config.GetFormattedSettings())
|
||||||
|
|
||||||
// 输出配置信息
|
// 输出配置信息
|
||||||
util.Logger.Info("Application configuration loaded")
|
util.Logger.Info("Application configuration loaded")
|
||||||
util.Logger.Info("├─ Config file", zap.String("path", config.GetRawViper().ConfigFileUsed()))
|
util.Logger.Info("├─ Config file", zap.String("path", config.GetRawViper().ConfigFileUsed()))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -156,7 +157,7 @@ func Init(configPath string) error {
|
|||||||
v.SetDefault("log.db_log_level", "info")
|
v.SetDefault("log.db_log_level", "info")
|
||||||
v.SetDefault("api.mode", "local")
|
v.SetDefault("api.mode", "local")
|
||||||
v.SetDefault("cron.enabled", true)
|
v.SetDefault("cron.enabled", true)
|
||||||
v.SetDefault("cron.daily_spec", "0 10 * * *")
|
v.SetDefault("cron.daily_spec", "20 8-23/4 * * *")
|
||||||
v.SetDefault("retention.days", 0)
|
v.SetDefault("retention.days", 0)
|
||||||
v.SetDefault("db.type", "sqlite")
|
v.SetDefault("db.type", "sqlite")
|
||||||
v.SetDefault("db.dsn", "data/bing_paper.db")
|
v.SetDefault("db.dsn", "data/bing_paper.db")
|
||||||
@@ -254,6 +255,38 @@ func GetRawViper() *viper.Viper {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllSettings 返回所有生效配置项
|
||||||
|
func GetAllSettings() map[string]interface{} {
|
||||||
|
return v.AllSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFormattedSettings 以 key: value 形式返回所有配置项的字符串
|
||||||
|
func GetFormattedSettings() string {
|
||||||
|
keys := v.AllKeys()
|
||||||
|
sort.Strings(keys)
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, k := range keys {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s: %v\n", k, v.Get(k)))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvOverrides 返回环境变量覆盖详情(已排序)
|
||||||
|
func GetEnvOverrides() []string {
|
||||||
|
var overrides []string
|
||||||
|
keys := v.AllKeys()
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, key := range keys {
|
||||||
|
// 根据 viper 的配置生成对应的环境变量名
|
||||||
|
// Prefix: BINGPAPER, KeyReplacer: . -> _
|
||||||
|
envKey := strings.ToUpper(fmt.Sprintf("BINGPAPER_%s", strings.ReplaceAll(key, ".", "_")))
|
||||||
|
if val, ok := os.LookupEnv(envKey); ok {
|
||||||
|
overrides = append(overrides, fmt.Sprintf("%s: %s=%s", key, envKey, val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return overrides
|
||||||
|
}
|
||||||
|
|
||||||
func GetTokenTTL() time.Duration {
|
func GetTokenTTL() time.Duration {
|
||||||
ttl, err := time.ParseDuration(GetConfig().Token.DefaultTTL)
|
ttl, err := time.ParseDuration(GetConfig().Token.DefaultTTL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,3 +22,46 @@ func TestDefaultConfig(t *testing.T) {
|
|||||||
t.Errorf("Expected DB type sqlite, got %s", cfg.DB.Type)
|
t.Errorf("Expected DB type sqlite, got %s", cfg.DB.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDebugFunctions(t *testing.T) {
|
||||||
|
// 设置一个环境变量
|
||||||
|
os.Setenv("BINGPAPER_SERVER_PORT", "9999")
|
||||||
|
defer os.Unsetenv("BINGPAPER_SERVER_PORT")
|
||||||
|
|
||||||
|
err := Init("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to init config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := GetAllSettings()
|
||||||
|
serverCfg, ok := settings["server"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected server config map, got %v", settings["server"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viper numbers in AllSettings are often int
|
||||||
|
portValue := serverCfg["port"]
|
||||||
|
// 允许不同的数字类型,因为 viper 内部实现可能变化
|
||||||
|
portStr := fmt.Sprintf("%v", portValue)
|
||||||
|
if portStr != "9999" {
|
||||||
|
t.Errorf("Expected port 9999 in settings, got %v (%T)", portValue, portValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides := GetEnvOverrides()
|
||||||
|
found := false
|
||||||
|
for _, o := range overrides {
|
||||||
|
if strings.Contains(o, "server.port") && strings.Contains(o, "9999") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected server.port override in %v", overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证格式化输出
|
||||||
|
formatted := GetFormattedSettings()
|
||||||
|
if !strings.Contains(formatted, "server.port: 9999") {
|
||||||
|
t.Errorf("Expected formatted settings to contain server.port: 9999, got %s", formatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func GetToday(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleImageResponse(c, img)
|
handleImageResponse(c, img, 7200) // 2小时
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTodayMeta 获取今日图片元数据
|
// GetTodayMeta 获取今日图片元数据
|
||||||
@@ -68,6 +68,7 @@ func GetTodayMeta(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.Header("Cache-Control", "public, max-age=7200") // 2小时
|
||||||
c.JSON(http.StatusOK, formatMeta(img))
|
c.JSON(http.StatusOK, formatMeta(img))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ func GetRandom(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleImageResponse(c, img)
|
handleImageResponse(c, img, 0) // 禁用缓存
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRandomMeta 获取随机图片元数据
|
// GetRandomMeta 获取随机图片元数据
|
||||||
@@ -102,6 +103,7 @@ func GetRandomMeta(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
c.JSON(http.StatusOK, formatMeta(img))
|
c.JSON(http.StatusOK, formatMeta(img))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ func GetByDate(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleImageResponse(c, img)
|
handleImageResponse(c, img, 604800) // 7天
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByDateMeta 获取指定日期图片元数据
|
// GetByDateMeta 获取指定日期图片元数据
|
||||||
@@ -140,6 +142,7 @@ func GetByDateMeta(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
||||||
c.JSON(http.StatusOK, formatMeta(img))
|
c.JSON(http.StatusOK, formatMeta(img))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +206,7 @@ func ListImages(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleImageResponse(c *gin.Context, img *model.Image) {
|
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")
|
||||||
|
|
||||||
@@ -228,22 +231,30 @@ func handleImageResponse(c *gin.Context, img *model.Image) {
|
|||||||
mode := config.GetConfig().API.Mode
|
mode := config.GetConfig().API.Mode
|
||||||
if mode == "redirect" {
|
if mode == "redirect" {
|
||||||
if selected.PublicURL != "" {
|
if selected.PublicURL != "" {
|
||||||
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
if maxAge > 0 {
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||||
|
} else {
|
||||||
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
}
|
||||||
c.Redirect(http.StatusFound, selected.PublicURL)
|
c.Redirect(http.StatusFound, selected.PublicURL)
|
||||||
} else if img.URLBase != "" {
|
} else if img.URLBase != "" {
|
||||||
// 兜底重定向到原始 Bing
|
// 兜底重定向到原始 Bing
|
||||||
bingURL := fmt.Sprintf("https://www.bing.com%s_%s.jpg", img.URLBase, selected.Variant)
|
bingURL := fmt.Sprintf("https://www.bing.com%s_%s.jpg", img.URLBase, selected.Variant)
|
||||||
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
if maxAge > 0 {
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||||
|
} else {
|
||||||
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
}
|
||||||
c.Redirect(http.StatusFound, bingURL)
|
c.Redirect(http.StatusFound, bingURL)
|
||||||
} else {
|
} else {
|
||||||
serveLocal(c, selected.StorageKey, img.Date)
|
serveLocal(c, selected.StorageKey, img.Date, maxAge)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
serveLocal(c, selected.StorageKey, img.Date)
|
serveLocal(c, selected.StorageKey, img.Date, maxAge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveLocal(c *gin.Context, key string, etag string) {
|
func serveLocal(c *gin.Context, key string, etag string, maxAge int) {
|
||||||
if etag != "" {
|
if etag != "" {
|
||||||
c.Header("ETag", fmt.Sprintf("\"%s\"", etag))
|
c.Header("ETag", fmt.Sprintf("\"%s\"", etag))
|
||||||
if c.GetHeader("If-None-Match") == fmt.Sprintf("\"%s\"", etag) {
|
if c.GetHeader("If-None-Match") == fmt.Sprintf("\"%s\"", etag) {
|
||||||
@@ -263,7 +274,12 @@ func serveLocal(c *gin.Context, key string, etag string) {
|
|||||||
if contentType != "" {
|
if contentType != "" {
|
||||||
c.Header("Content-Type", contentType)
|
c.Header("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
|
||||||
|
if maxAge > 0 {
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||||
|
} else {
|
||||||
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
}
|
||||||
io.Copy(c.Writer, reader)
|
io.Copy(c.Writer, reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestHandleImageResponseRedirect(t *testing.T) {
|
|||||||
c, _ := gin.CreateTestContext(w)
|
c, _ := gin.CreateTestContext(w)
|
||||||
c.Request, _ = http.NewRequest("GET", "/api/v1/image/today?variant=UHD", nil)
|
c.Request, _ = http.NewRequest("GET", "/api/v1/image/today?variant=UHD", nil)
|
||||||
|
|
||||||
handleImageResponse(c, img)
|
handleImageResponse(c, img, 0)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusFound, w.Code)
|
assert.Equal(t, http.StatusFound, w.Code)
|
||||||
assert.Contains(t, w.Header().Get("Location"), "bing.com")
|
assert.Contains(t, w.Header().Get("Location"), "bing.com")
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func SetupRouter(webFS embed.FS) *gin.Engine {
|
|||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
|
|
||||||
// 如果请求的是 API 或 Swagger,则不处理静态资源 (让其返回 404)
|
// 如果请求的是 API 或 Swagger,则不处理静态资源 (让其返回 404)
|
||||||
if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/swagger") {
|
if strings.HasPrefix(path, "/api/v1") || strings.HasPrefix(path, "/swagger") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ set PLATFORMS=linux/amd64 linux/arm64 windows/amd64 windows/arm64 darwin/amd64 d
|
|||||||
for %%p in (%PLATFORMS%) do (
|
for %%p in (%PLATFORMS%) do (
|
||||||
for /f "tokens=1,2 delims=/" %%a in ("%%p") do (
|
for /f "tokens=1,2 delims=/" %%a in ("%%p") do (
|
||||||
set OUTPUT_NAME=%APP_NAME%-%%a-%%b
|
set OUTPUT_NAME=%APP_NAME%-%%a-%%b
|
||||||
set BINARY_NAME=!OUTPUT_NAME!
|
set BINARY_NAME=%APP_NAME%
|
||||||
if "%%a"=="windows" set BINARY_NAME=!OUTPUT_NAME!.exe
|
if "%%a"=="windows" set BINARY_NAME=%APP_NAME%.exe
|
||||||
|
|
||||||
echo 正在编译 %%a/%%b...
|
echo 正在编译 %%a/%%b...
|
||||||
|
|
||||||
@@ -47,10 +47,10 @@ for %%p in (%PLATFORMS%) do (
|
|||||||
copy /y config.example.yaml !PACKAGE_DIR!\ >nul
|
copy /y config.example.yaml !PACKAGE_DIR!\ >nul
|
||||||
copy /y README.md !PACKAGE_DIR!\ >nul
|
copy /y README.md !PACKAGE_DIR!\ >nul
|
||||||
|
|
||||||
pushd %OUTPUT_DIR%
|
pushd !PACKAGE_DIR!
|
||||||
tar -czf !OUTPUT_NAME!.tar.gz !OUTPUT_NAME!
|
tar -czf ..\!OUTPUT_NAME!.tar.gz .
|
||||||
rd /s /q !OUTPUT_NAME!
|
|
||||||
popd
|
popd
|
||||||
|
rd /s /q !PACKAGE_DIR!
|
||||||
|
|
||||||
echo %%a/%%b 打包完成: !OUTPUT_NAME!.tar.gz
|
echo %%a/%%b 打包完成: !OUTPUT_NAME!.tar.gz
|
||||||
) else (
|
) else (
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ foreach ($Platform in $Platforms) {
|
|||||||
$Arch = $parts[1]
|
$Arch = $parts[1]
|
||||||
|
|
||||||
$OutputName = "$AppName-$OS-$Arch"
|
$OutputName = "$AppName-$OS-$Arch"
|
||||||
$BinaryName = $OutputName
|
$BinaryName = $AppName
|
||||||
if ($OS -eq "windows") {
|
if ($OS -eq "windows") {
|
||||||
$BinaryName = "$OutputName.exe"
|
$BinaryName = "$AppName.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "正在编译 $OS/$Arch..."
|
Write-Host "正在编译 $OS/$Arch..."
|
||||||
@@ -63,10 +63,10 @@ foreach ($Platform in $Platforms) {
|
|||||||
Copy-Item "README.md" $PackageDir\
|
Copy-Item "README.md" $PackageDir\
|
||||||
|
|
||||||
$CurrentDir = Get-Location
|
$CurrentDir = Get-Location
|
||||||
Set-Location $OutputDir
|
Set-Location $PackageDir
|
||||||
tar -czf "${OutputName}.tar.gz" $OutputName
|
tar -czf "../${OutputName}.tar.gz" .
|
||||||
Remove-Item -Recurse -Force $OutputName
|
|
||||||
Set-Location $CurrentDir
|
Set-Location $CurrentDir
|
||||||
|
Remove-Item -Recurse -Force $PackageDir
|
||||||
|
|
||||||
Write-Host " $OS/$Arch 打包完成: ${OutputName}.tar.gz"
|
Write-Host " $OS/$Arch 打包完成: ${OutputName}.tar.gz"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ for PLATFORM in "${PLATFORMS[@]}"; do
|
|||||||
# 设置输出名称
|
# 设置输出名称
|
||||||
OUTPUT_NAME="${APP_NAME}-${OS}-${ARCH}"
|
OUTPUT_NAME="${APP_NAME}-${OS}-${ARCH}"
|
||||||
if [ "$OS" = "windows" ]; then
|
if [ "$OS" = "windows" ]; then
|
||||||
BINARY_NAME="${OUTPUT_NAME}.exe"
|
BINARY_NAME="${APP_NAME}.exe"
|
||||||
else
|
else
|
||||||
BINARY_NAME="${OUTPUT_NAME}"
|
BINARY_NAME="${APP_NAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "正在编译 ${OS}/${ARCH}..."
|
echo "正在编译 ${OS}/${ARCH}..."
|
||||||
@@ -71,7 +71,7 @@ for PLATFORM in "${PLATFORMS[@]}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# 压缩为 tar.gz
|
# 压缩为 tar.gz
|
||||||
tar -czf "${OUTPUT_DIR}/${OUTPUT_NAME}.tar.gz" -C "${OUTPUT_DIR}" "${OUTPUT_NAME}"
|
tar -czf "${OUTPUT_DIR}/${OUTPUT_NAME}.tar.gz" -C "${PACKAGE_DIR}" .
|
||||||
|
|
||||||
# 删除临时打包目录
|
# 删除临时打包目录
|
||||||
rm -rf "$PACKAGE_DIR"
|
rm -rf "$PACKAGE_DIR"
|
||||||
|
|||||||
101
scripts/deploy.sh
Normal file
101
scripts/deploy.sh
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 定时任务示例 (每30分钟执行一次):
|
||||||
|
# */30 * * * * /path/to/project/scripts/deploy.sh >> /path/to/project/scripts/deploy_cron.log 2>&1
|
||||||
|
|
||||||
|
# 项目根目录
|
||||||
|
# 假设脚本位于 scripts 目录下
|
||||||
|
PROJECT_DIR=$(cd $(dirname $0)/.. && pwd)
|
||||||
|
cd $PROJECT_DIR
|
||||||
|
|
||||||
|
# 日志文件
|
||||||
|
LOG_FILE="$PROJECT_DIR/scripts/deploy.log"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确保在项目根目录
|
||||||
|
if [ ! -f "docker-compose.yml" ]; then
|
||||||
|
log "错误: 未能在 $PROJECT_DIR 找到 docker-compose.yml,请确保脚本位置正确。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "开始检查更新..."
|
||||||
|
|
||||||
|
# 获取远程代码
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
# 获取当前分支名
|
||||||
|
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
|
# 检查本地分支是否有上游分支
|
||||||
|
UPSTREAM=$(git rev-parse --abbrev-ref @{u} 2>/dev/null)
|
||||||
|
if [ -z "$UPSTREAM" ]; then
|
||||||
|
log "错误: 当前分支 $BRANCH 没有设置上游分支,无法自动对比更新。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查本地是否落后于远程
|
||||||
|
LOCAL=$(git rev-parse HEAD)
|
||||||
|
REMOTE=$(git rev-parse "$UPSTREAM")
|
||||||
|
|
||||||
|
if [ "$LOCAL" = "$REMOTE" ]; then
|
||||||
|
log "代码已是最新 ($LOCAL),无需更新。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "检测到远程变更 ($LOCAL -> $REMOTE),准备开始升级..."
|
||||||
|
|
||||||
|
# 检查是否有本地修改
|
||||||
|
HAS_CHANGES=$(git status --porcelain)
|
||||||
|
|
||||||
|
if [ -n "$HAS_CHANGES" ]; then
|
||||||
|
log "检测到本地修改,正在暂存以保留个性化配置..."
|
||||||
|
git stash
|
||||||
|
STASHED=true
|
||||||
|
else
|
||||||
|
STASHED=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 拉取新代码
|
||||||
|
log "正在拉取远程代码 ($BRANCH)..."
|
||||||
|
if git pull origin "$BRANCH"; then
|
||||||
|
log "代码拉取成功。"
|
||||||
|
else
|
||||||
|
log "错误: 代码拉取失败。"
|
||||||
|
if [ "$STASHED" = true ]; then
|
||||||
|
git stash pop
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 恢复本地修改
|
||||||
|
if [ "$STASHED" = true ]; then
|
||||||
|
log "正在恢复本地修改..."
|
||||||
|
if git stash pop; then
|
||||||
|
log "本地修改已成功恢复。"
|
||||||
|
else
|
||||||
|
log "警告: 恢复本地修改时发生冲突,请手动检查 docker-compose.yml 等文件。"
|
||||||
|
# 即使冲突也尝试继续,或者你可以选择在此退出
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 确定 docker-compose 命令
|
||||||
|
DOCKER_COMPOSE_BIN="docker-compose"
|
||||||
|
if ! command -v $DOCKER_COMPOSE_BIN &> /dev/null; then
|
||||||
|
DOCKER_COMPOSE_BIN="docker compose"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 执行 docker-compose 部署
|
||||||
|
log "正在执行 $DOCKER_COMPOSE_BIN 部署..."
|
||||||
|
if $DOCKER_COMPOSE_BIN up -d --build; then
|
||||||
|
log "服务升级成功!"
|
||||||
|
# 清理无用镜像(可选)
|
||||||
|
docker image prune -f
|
||||||
|
else
|
||||||
|
log "错误: $DOCKER_COMPOSE_BIN 部署失败。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "部署任务完成。"
|
||||||
@@ -173,13 +173,18 @@
|
|||||||
<span>加载中...</span>
|
<span>加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div
|
||||||
v-else-if="hasMore"
|
v-else-if="hasMore"
|
||||||
@click="loadMore"
|
ref="loadMoreTrigger"
|
||||||
class="px-8 py-3 bg-white/10 backdrop-blur-md text-white rounded-lg font-semibold hover:bg-white/20 transition-all border border-white/30"
|
class="inline-block"
|
||||||
>
|
>
|
||||||
加载更多
|
<button
|
||||||
</button>
|
@click="loadMore"
|
||||||
|
class="px-8 py-3 bg-white/10 backdrop-blur-md text-white rounded-lg font-semibold hover:bg-white/20 transition-all border border-white/30"
|
||||||
|
>
|
||||||
|
加载更多
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-else class="text-white/40">
|
<p v-else class="text-white/40">
|
||||||
已加载全部图片
|
已加载全部图片
|
||||||
@@ -246,8 +251,8 @@ const router = useRouter()
|
|||||||
// 获取今日图片
|
// 获取今日图片
|
||||||
const { image: todayImage, loading: todayLoading } = useTodayImage()
|
const { image: todayImage, loading: todayLoading } = useTodayImage()
|
||||||
|
|
||||||
// 获取图片列表(使用服务端分页和筛选)
|
// 获取图片列表(使用服务端分页和筛选,每页15张)
|
||||||
const { images, loading, hasMore, loadMore, filterByMonth } = useImageList(30)
|
const { images, loading, hasMore, loadMore, filterByMonth } = useImageList(15)
|
||||||
|
|
||||||
// 筛选相关状态
|
// 筛选相关状态
|
||||||
const selectedYear = ref('')
|
const selectedYear = ref('')
|
||||||
@@ -258,6 +263,10 @@ const imageRefs = ref<(HTMLElement | null)[]>([])
|
|||||||
const imageVisibility = ref<boolean[]>([])
|
const imageVisibility = ref<boolean[]>([])
|
||||||
let observer: IntersectionObserver | null = null
|
let observer: IntersectionObserver | null = null
|
||||||
|
|
||||||
|
// 无限滚动加载
|
||||||
|
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||||
|
let loadMoreObserver: IntersectionObserver | null = null
|
||||||
|
|
||||||
// 计算可用的年份列表(基于当前日期生成,从2020年到当前年份)
|
// 计算可用的年份列表(基于当前日期生成,从2020年到当前年份)
|
||||||
const availableYears = computed(() => {
|
const availableYears = computed(() => {
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
@@ -360,16 +369,6 @@ const setupObserver = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化懒加载状态
|
|
||||||
onMounted(() => {
|
|
||||||
// 初始化时设置
|
|
||||||
if (images.value.length > 0) {
|
|
||||||
imageVisibility.value = new Array(images.value.length).fill(false)
|
|
||||||
setTimeout(() => {
|
|
||||||
setupObserver()
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听 images 变化,动态更新 imageVisibility
|
// 监听 images 变化,动态更新 imageVisibility
|
||||||
watch(() => images.value.length, (newLength, oldLength) => {
|
watch(() => images.value.length, (newLength, oldLength) => {
|
||||||
@@ -399,11 +398,51 @@ watch(() => images.value.length, (newLength, oldLength) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设置无限滚动 Observer
|
||||||
|
const setupLoadMoreObserver = () => {
|
||||||
|
if (loadMoreObserver) {
|
||||||
|
loadMoreObserver.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && !loading.value && hasMore.value) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
rootMargin: '100px',
|
||||||
|
threshold: 0.1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (loadMoreTrigger.value) {
|
||||||
|
loadMoreObserver.observe(loadMoreTrigger.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时设置无限滚动
|
||||||
|
onMounted(() => {
|
||||||
|
if (images.value.length > 0) {
|
||||||
|
imageVisibility.value = new Array(images.value.length).fill(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
setupObserver()
|
||||||
|
setupLoadMoreObserver()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 清理
|
// 清理
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (observer) {
|
if (observer) {
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
}
|
}
|
||||||
|
if (loadMoreObserver) {
|
||||||
|
loadMoreObserver.disconnect()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
@click="previousDay"
|
@click="previousDay"
|
||||||
:disabled="navigating"
|
:disabled="navigating || !hasPreviousDay"
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur-md text-white rounded-lg hover:bg-white/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
class="flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur-md text-white rounded-lg hover:bg-white/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
@click="nextDay"
|
@click="nextDay"
|
||||||
:disabled="navigating || isToday"
|
:disabled="navigating || !hasNextDay"
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur-md text-white rounded-lg hover:bg-white/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
class="flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur-md text-white rounded-lg hover:bg-white/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span class="hidden sm:inline">后一天</span>
|
<span class="hidden sm:inline">后一天</span>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useImageByDate } from '@/composables/useImages'
|
import { useImageByDate } from '@/composables/useImages'
|
||||||
import { bingPaperApi } from '@/lib/api-service'
|
import { bingPaperApi } from '@/lib/api-service'
|
||||||
@@ -153,6 +153,11 @@ const currentDate = ref(route.params.date as string)
|
|||||||
const showInfo = ref(true)
|
const showInfo = ref(true)
|
||||||
const navigating = ref(false)
|
const navigating = ref(false)
|
||||||
|
|
||||||
|
// 前后日期可用性
|
||||||
|
const hasPreviousDay = ref(true)
|
||||||
|
const hasNextDay = ref(true)
|
||||||
|
const checkingDates = ref(false)
|
||||||
|
|
||||||
// 拖动相关状态
|
// 拖动相关状态
|
||||||
const infoPanel = ref<HTMLElement | null>(null)
|
const infoPanel = ref<HTMLElement | null>(null)
|
||||||
const infoPanelPos = ref({ x: 0, y: 0 })
|
const infoPanelPos = ref({ x: 0, y: 0 })
|
||||||
@@ -230,9 +235,50 @@ const stopDrag = () => {
|
|||||||
// 使用 composable 获取图片数据(传递 ref,自动响应日期变化)
|
// 使用 composable 获取图片数据(传递 ref,自动响应日期变化)
|
||||||
const { image, loading, error } = useImageByDate(currentDate)
|
const { image, loading, error } = useImageByDate(currentDate)
|
||||||
|
|
||||||
|
// 检测指定日期是否有数据
|
||||||
|
const checkDateAvailability = async (dateStr: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await bingPaperApi.getImageMetaByDate(dateStr)
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测前后日期可用性
|
||||||
|
const checkAdjacentDates = async () => {
|
||||||
|
if (checkingDates.value) return
|
||||||
|
|
||||||
|
checkingDates.value = true
|
||||||
|
const date = new Date(currentDate.value)
|
||||||
|
|
||||||
|
// 检测前一天
|
||||||
|
const prevDate = new Date(date)
|
||||||
|
prevDate.setDate(prevDate.getDate() - 1)
|
||||||
|
hasPreviousDay.value = await checkDateAvailability(prevDate.toISOString().split('T')[0])
|
||||||
|
|
||||||
|
// 检测后一天(不能超过今天)
|
||||||
|
const nextDate = new Date(date)
|
||||||
|
nextDate.setDate(nextDate.getDate() + 1)
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
if (nextDate.toISOString().split('T')[0] > today) {
|
||||||
|
hasNextDay.value = false
|
||||||
|
} else {
|
||||||
|
hasNextDay.value = await checkDateAvailability(nextDate.toISOString().split('T')[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingDates.value = false
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化位置
|
// 初始化位置
|
||||||
initPanelPosition()
|
initPanelPosition()
|
||||||
|
|
||||||
|
// 监听日期变化,检测前后日期可用性
|
||||||
|
import { watch } from 'vue'
|
||||||
|
watch(currentDate, () => {
|
||||||
|
checkAdjacentDates()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
const formatDate = (dateStr?: string) => {
|
const formatDate = (dateStr?: string) => {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
@@ -245,12 +291,6 @@ const formatDate = (dateStr?: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是今天
|
|
||||||
const isToday = computed(() => {
|
|
||||||
const today = new Date().toISOString().split('T')[0]
|
|
||||||
return currentDate.value === today
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取完整图片 URL
|
// 获取完整图片 URL
|
||||||
const getFullImageUrl = () => {
|
const getFullImageUrl = () => {
|
||||||
return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg')
|
return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg')
|
||||||
@@ -265,7 +305,7 @@ const goBack = () => {
|
|||||||
|
|
||||||
// 前一天
|
// 前一天
|
||||||
const previousDay = () => {
|
const previousDay = () => {
|
||||||
if (navigating.value) return
|
if (navigating.value || !hasPreviousDay.value) return
|
||||||
|
|
||||||
navigating.value = true
|
navigating.value = true
|
||||||
const date = new Date(currentDate.value)
|
const date = new Date(currentDate.value)
|
||||||
@@ -282,7 +322,7 @@ const previousDay = () => {
|
|||||||
|
|
||||||
// 后一天
|
// 后一天
|
||||||
const nextDay = () => {
|
const nextDay = () => {
|
||||||
if (navigating.value || isToday.value) return
|
if (navigating.value || !hasNextDay.value) return
|
||||||
|
|
||||||
navigating.value = true
|
navigating.value = true
|
||||||
const date = new Date(currentDate.value)
|
const date = new Date(currentDate.value)
|
||||||
@@ -299,9 +339,9 @@ const nextDay = () => {
|
|||||||
|
|
||||||
// 键盘导航
|
// 键盘导航
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'ArrowLeft') {
|
if (e.key === 'ArrowLeft' && hasPreviousDay.value) {
|
||||||
previousDay()
|
previousDay()
|
||||||
} else if (e.key === 'ArrowRight' && !isToday.value) {
|
} else if (e.key === 'ArrowRight' && hasNextDay.value) {
|
||||||
nextDay()
|
nextDay()
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
goBack()
|
goBack()
|
||||||
|
|||||||
Reference in New Issue
Block a user