mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-02-15 07:29:33 +08:00
增加多地区每日图片抓取能力
This commit is contained in:
@@ -36,11 +36,15 @@ BingPaper 支持通过配置文件(YAML)和环境变量进行配置。
|
|||||||
- `mode`: API 行为模式。
|
- `mode`: API 行为模式。
|
||||||
- `local`: (默认) 接口直接返回图片的二进制流,适合图片存储对外部不可见的情况。
|
- `local`: (默认) 接口直接返回图片的二进制流,适合图片存储对外部不可见的情况。
|
||||||
- `redirect`: 接口返回 302 重定向到图片的 `PublicURL`,适合配合 S3 或 WebDAV 的公共访问。
|
- `redirect`: 接口返回 302 重定向到图片的 `PublicURL`,适合配合 S3 或 WebDAV 的公共访问。
|
||||||
|
- `enable_mkt_fallback`: 当请求的地区不存在或无数据时,是否允许兜底回退到默认地区或任意可用地区,默认 `true`。
|
||||||
|
|
||||||
#### cron (定时任务)
|
#### cron (定时任务)
|
||||||
- `enabled`: 是否启用定时抓取,默认 `true`。
|
- `enabled`: 是否启用定时抓取,默认 `true`。
|
||||||
- `daily_spec`: Cron 表达式,定义每日抓取时间。默认 `"0 10 * * *"` (每日上午 10:00)。
|
- `daily_spec`: Cron 表达式,定义每日抓取时间。默认 `"0 10 * * *"` (每日上午 10:00)。
|
||||||
|
|
||||||
|
#### fetcher (抓取配置)
|
||||||
|
- `regions`: 需要抓取的地区编码列表(如 `zh-CN`, `en-US` 等)。如果不设置,默认为包括主要国家在内的 17 个地区。
|
||||||
|
|
||||||
#### retention (数据保留)
|
#### retention (数据保留)
|
||||||
- `days`: 图片及元数据保留天数。超过此天数的数据可能会被清理任务处理。设置为 `0` 表示永久保留,不进行自动清理。默认 `0`。
|
- `days`: 图片及元数据保留天数。超过此天数的数据可能会被清理任务处理。设置为 `0` 表示永久保留,不进行自动清理。默认 `0`。
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ go run .
|
|||||||
- `GET /api/v1/image/random`:返回随机图片
|
- `GET /api/v1/image/random`:返回随机图片
|
||||||
- `GET /api/v1/image/date/:yyyy-mm-dd`:返回指定日期图片
|
- `GET /api/v1/image/date/:yyyy-mm-dd`:返回指定日期图片
|
||||||
- **查询参数**:
|
- **查询参数**:
|
||||||
|
- `mkt`:地区编码 (zh-CN, en-US, ja-JP 等),默认 `zh-CN`
|
||||||
- `variant`:分辨率 (UHD, 1920x1080, 1366x768),默认 `UHD`
|
- `variant`:分辨率 (UHD, 1920x1080, 1366x768),默认 `UHD`
|
||||||
- `format`:格式 (jpg),默认 `jpg`
|
- `format`:格式 (jpg),默认 `jpg`
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ log:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
mode: local # local | redirect
|
mode: local # local | redirect
|
||||||
|
enable_mkt_fallback: true # 当请求的地区不存在时,是否回退到默认地区
|
||||||
|
|
||||||
cron:
|
cron:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
101
docs/docs.go
101
docs/docs.go
@@ -413,6 +413,12 @@ const docTemplate = `{
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "UHD",
|
"default": "UHD",
|
||||||
@@ -455,6 +461,12 @@ const docTemplate = `{
|
|||||||
"name": "date",
|
"name": "date",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -478,6 +490,12 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"summary": "获取随机图片",
|
"summary": "获取随机图片",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "UHD",
|
"default": "UHD",
|
||||||
@@ -513,6 +531,14 @@ const docTemplate = `{
|
|||||||
"image"
|
"image"
|
||||||
],
|
],
|
||||||
"summary": "获取随机图片元数据",
|
"summary": "获取随机图片元数据",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -534,6 +560,12 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"summary": "获取今日图片",
|
"summary": "获取今日图片",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "UHD",
|
"default": "UHD",
|
||||||
@@ -569,6 +601,14 @@ const docTemplate = `{
|
|||||||
"image"
|
"image"
|
||||||
],
|
],
|
||||||
"summary": "获取今日图片元数据",
|
"summary": "获取今日图片元数据",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -614,6 +654,12 @@ const docTemplate = `{
|
|||||||
"description": "按月份过滤 (格式: YYYY-MM)",
|
"description": "按月份过滤 (格式: YYYY-MM)",
|
||||||
"name": "month",
|
"name": "month",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -628,12 +674,39 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/regions": {
|
||||||
|
"get": {
|
||||||
|
"description": "返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"summary": "获取支持的地区列表",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/util.Region"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"config.APIConfig": {
|
"config.APIConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"enableMktFallback": {
|
||||||
|
"description": "当请求的地区不存在时,是否回退到默认地区",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"description": "local | redirect",
|
"description": "local | redirect",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -666,6 +739,9 @@ const docTemplate = `{
|
|||||||
"feature": {
|
"feature": {
|
||||||
"$ref": "#/definitions/config.FeatureConfig"
|
"$ref": "#/definitions/config.FeatureConfig"
|
||||||
},
|
},
|
||||||
|
"fetcher": {
|
||||||
|
"$ref": "#/definitions/config.FetcherConfig"
|
||||||
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"$ref": "#/definitions/config.LogConfig"
|
"$ref": "#/definitions/config.LogConfig"
|
||||||
},
|
},
|
||||||
@@ -717,6 +793,17 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config.FetcherConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"regions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"config.LocalConfig": {
|
"config.LocalConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -917,6 +1004,9 @@ const docTemplate = `{
|
|||||||
"hsh": {
|
"hsh": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"mkt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"quiz": {
|
"quiz": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1006,6 +1096,17 @@ const docTemplate = `{
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"util.Region": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
|||||||
@@ -407,6 +407,12 @@
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "UHD",
|
"default": "UHD",
|
||||||
@@ -449,6 +455,12 @@
|
|||||||
"name": "date",
|
"name": "date",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -472,6 +484,12 @@
|
|||||||
],
|
],
|
||||||
"summary": "获取随机图片",
|
"summary": "获取随机图片",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "UHD",
|
"default": "UHD",
|
||||||
@@ -507,6 +525,14 @@
|
|||||||
"image"
|
"image"
|
||||||
],
|
],
|
||||||
"summary": "获取随机图片元数据",
|
"summary": "获取随机图片元数据",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -528,6 +554,12 @@
|
|||||||
],
|
],
|
||||||
"summary": "获取今日图片",
|
"summary": "获取今日图片",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "UHD",
|
"default": "UHD",
|
||||||
@@ -563,6 +595,14 @@
|
|||||||
"image"
|
"image"
|
||||||
],
|
],
|
||||||
"summary": "获取今日图片元数据",
|
"summary": "获取今日图片元数据",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -608,6 +648,12 @@
|
|||||||
"description": "按月份过滤 (格式: YYYY-MM)",
|
"description": "按月份过滤 (格式: YYYY-MM)",
|
||||||
"name": "month",
|
"name": "month",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "地区编码 (如 zh-CN, en-US)",
|
||||||
|
"name": "mkt",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -622,12 +668,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/regions": {
|
||||||
|
"get": {
|
||||||
|
"description": "返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"summary": "获取支持的地区列表",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/util.Region"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"config.APIConfig": {
|
"config.APIConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"enableMktFallback": {
|
||||||
|
"description": "当请求的地区不存在时,是否回退到默认地区",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"description": "local | redirect",
|
"description": "local | redirect",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -660,6 +733,9 @@
|
|||||||
"feature": {
|
"feature": {
|
||||||
"$ref": "#/definitions/config.FeatureConfig"
|
"$ref": "#/definitions/config.FeatureConfig"
|
||||||
},
|
},
|
||||||
|
"fetcher": {
|
||||||
|
"$ref": "#/definitions/config.FetcherConfig"
|
||||||
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"$ref": "#/definitions/config.LogConfig"
|
"$ref": "#/definitions/config.LogConfig"
|
||||||
},
|
},
|
||||||
@@ -711,6 +787,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config.FetcherConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"regions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"config.LocalConfig": {
|
"config.LocalConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -911,6 +998,9 @@
|
|||||||
"hsh": {
|
"hsh": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"mkt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"quiz": {
|
"quiz": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1000,6 +1090,17 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"util.Region": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ basePath: /api/v1
|
|||||||
definitions:
|
definitions:
|
||||||
config.APIConfig:
|
config.APIConfig:
|
||||||
properties:
|
properties:
|
||||||
|
enableMktFallback:
|
||||||
|
description: 当请求的地区不存在时,是否回退到默认地区
|
||||||
|
type: boolean
|
||||||
mode:
|
mode:
|
||||||
description: local | redirect
|
description: local | redirect
|
||||||
type: string
|
type: string
|
||||||
@@ -23,6 +26,8 @@ definitions:
|
|||||||
$ref: '#/definitions/config.DBConfig'
|
$ref: '#/definitions/config.DBConfig'
|
||||||
feature:
|
feature:
|
||||||
$ref: '#/definitions/config.FeatureConfig'
|
$ref: '#/definitions/config.FeatureConfig'
|
||||||
|
fetcher:
|
||||||
|
$ref: '#/definitions/config.FetcherConfig'
|
||||||
log:
|
log:
|
||||||
$ref: '#/definitions/config.LogConfig'
|
$ref: '#/definitions/config.LogConfig'
|
||||||
retention:
|
retention:
|
||||||
@@ -56,6 +61,13 @@ definitions:
|
|||||||
writeDailyFiles:
|
writeDailyFiles:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
config.FetcherConfig:
|
||||||
|
properties:
|
||||||
|
regions:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
config.LocalConfig:
|
config.LocalConfig:
|
||||||
properties:
|
properties:
|
||||||
root:
|
root:
|
||||||
@@ -190,6 +202,8 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
hsh:
|
hsh:
|
||||||
type: string
|
type: string
|
||||||
|
mkt:
|
||||||
|
type: string
|
||||||
quiz:
|
quiz:
|
||||||
type: string
|
type: string
|
||||||
startdate:
|
startdate:
|
||||||
@@ -248,6 +262,13 @@ definitions:
|
|||||||
updated_at:
|
updated_at:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
util.Region:
|
||||||
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
host: localhost:8080
|
host: localhost:8080
|
||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
@@ -501,6 +522,10 @@ paths:
|
|||||||
name: date
|
name: date
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- description: 地区编码 (如 zh-CN, en-US)
|
||||||
|
in: query
|
||||||
|
name: mkt
|
||||||
|
type: string
|
||||||
- default: UHD
|
- default: UHD
|
||||||
description: 分辨率
|
description: 分辨率
|
||||||
in: query
|
in: query
|
||||||
@@ -530,6 +555,10 @@ paths:
|
|||||||
name: date
|
name: date
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- description: 地区编码 (如 zh-CN, en-US)
|
||||||
|
in: query
|
||||||
|
name: mkt
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -544,6 +573,10 @@ paths:
|
|||||||
get:
|
get:
|
||||||
description: 随机返回一张已抓取的图片流或重定向
|
description: 随机返回一张已抓取的图片流或重定向
|
||||||
parameters:
|
parameters:
|
||||||
|
- description: 地区编码 (如 zh-CN, en-US)
|
||||||
|
in: query
|
||||||
|
name: mkt
|
||||||
|
type: string
|
||||||
- default: UHD
|
- default: UHD
|
||||||
description: 分辨率
|
description: 分辨率
|
||||||
in: query
|
in: query
|
||||||
@@ -567,6 +600,11 @@ paths:
|
|||||||
/image/random/meta:
|
/image/random/meta:
|
||||||
get:
|
get:
|
||||||
description: 随机获取一张已抓取图片的元数据
|
description: 随机获取一张已抓取图片的元数据
|
||||||
|
parameters:
|
||||||
|
- description: 地区编码 (如 zh-CN, en-US)
|
||||||
|
in: query
|
||||||
|
name: mkt
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -581,6 +619,10 @@ paths:
|
|||||||
get:
|
get:
|
||||||
description: 根据参数返回今日必应图片流或重定向
|
description: 根据参数返回今日必应图片流或重定向
|
||||||
parameters:
|
parameters:
|
||||||
|
- description: 地区编码 (如 zh-CN, en-US)
|
||||||
|
in: query
|
||||||
|
name: mkt
|
||||||
|
type: string
|
||||||
- default: UHD
|
- default: UHD
|
||||||
description: 分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480,
|
description: 分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480,
|
||||||
640x480, 640x360, 480x360, 400x240, 320x240)
|
640x480, 640x360, 480x360, 400x240, 320x240)
|
||||||
@@ -605,6 +647,11 @@ paths:
|
|||||||
/image/today/meta:
|
/image/today/meta:
|
||||||
get:
|
get:
|
||||||
description: 获取今日必应图片的标题、版权等元数据
|
description: 获取今日必应图片的标题、版权等元数据
|
||||||
|
parameters:
|
||||||
|
- description: 地区编码 (如 zh-CN, en-US)
|
||||||
|
in: query
|
||||||
|
name: mkt
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -637,6 +684,10 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
name: month
|
name: month
|
||||||
type: string
|
type: string
|
||||||
|
- description: 地区编码 (如 zh-CN, en-US)
|
||||||
|
in: query
|
||||||
|
name: mkt
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -649,6 +700,21 @@ paths:
|
|||||||
summary: 获取图片列表
|
summary: 获取图片列表
|
||||||
tags:
|
tags:
|
||||||
- image
|
- image
|
||||||
|
/regions:
|
||||||
|
get:
|
||||||
|
description: 返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/util.Region'
|
||||||
|
type: array
|
||||||
|
summary: 获取支持的地区列表
|
||||||
|
tags:
|
||||||
|
- image
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
BearerAuth:
|
BearerAuth:
|
||||||
in: header
|
in: header
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"BingPaper/internal/config"
|
"BingPaper/internal/config"
|
||||||
"BingPaper/internal/cron"
|
"BingPaper/internal/cron"
|
||||||
@@ -52,10 +53,11 @@ func Init(webFS embed.FS, configPath string) *gin.Engine {
|
|||||||
|
|
||||||
// 输出配置信息
|
// 输出配置信息
|
||||||
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()))
|
||||||
util.Logger.Info("├─ Database ", zap.String("type", cfg.DB.Type))
|
util.Logger.Info("├─ Database ", zap.String("type", cfg.DB.Type))
|
||||||
util.Logger.Info("├─ Storage ", zap.String("type", cfg.Storage.Type))
|
util.Logger.Info("├─ Storage ", zap.String("type", cfg.Storage.Type))
|
||||||
util.Logger.Info("└─ Server ", zap.Int("port", cfg.Server.Port))
|
util.Logger.Info("├─ Server ", zap.Int("port", cfg.Server.Port))
|
||||||
|
util.Logger.Info("└─ Active Mkt ", zap.Strings("regions", cfg.Fetcher.Regions))
|
||||||
|
|
||||||
// 根据存储类型输出更多信息
|
// 根据存储类型输出更多信息
|
||||||
switch cfg.Storage.Type {
|
switch cfg.Storage.Type {
|
||||||
@@ -147,5 +149,6 @@ func LogWelcomeInfo() {
|
|||||||
fmt.Printf(" - 管理后台: %s/admin\n", baseURL)
|
fmt.Printf(" - 管理后台: %s/admin\n", baseURL)
|
||||||
fmt.Printf(" - API 文档: %s/swagger/index.html\n", baseURL)
|
fmt.Printf(" - API 文档: %s/swagger/index.html\n", baseURL)
|
||||||
fmt.Printf(" - 今日图片: %s/api/v1/image/today\n", baseURL)
|
fmt.Printf(" - 今日图片: %s/api/v1/image/today\n", baseURL)
|
||||||
|
fmt.Printf(" - 激活地区: %s\n", strings.Join(cfg.Fetcher.Regions, ", "))
|
||||||
fmt.Println("---------------------------------------------------------")
|
fmt.Println("---------------------------------------------------------")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"BingPaper/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -25,6 +27,7 @@ type Config struct {
|
|||||||
Token TokenConfig `mapstructure:"token" yaml:"token"`
|
Token TokenConfig `mapstructure:"token" yaml:"token"`
|
||||||
Feature FeatureConfig `mapstructure:"feature" yaml:"feature"`
|
Feature FeatureConfig `mapstructure:"feature" yaml:"feature"`
|
||||||
Web WebConfig `mapstructure:"web" yaml:"web"`
|
Web WebConfig `mapstructure:"web" yaml:"web"`
|
||||||
|
Fetcher FetcherConfig `mapstructure:"fetcher" yaml:"fetcher"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -57,7 +60,8 @@ 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"` // 当请求的地区不存在时,是否回退到默认地区
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronConfig struct {
|
type CronConfig struct {
|
||||||
@@ -118,6 +122,10 @@ type WebConfig struct {
|
|||||||
Path string `mapstructure:"path" yaml:"path"`
|
Path string `mapstructure:"path" yaml:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FetcherConfig struct {
|
||||||
|
Regions []string `mapstructure:"regions" yaml:"regions"`
|
||||||
|
}
|
||||||
|
|
||||||
// Bing 默认配置 (内置)
|
// Bing 默认配置 (内置)
|
||||||
const (
|
const (
|
||||||
BingMkt = "zh-CN"
|
BingMkt = "zh-CN"
|
||||||
@@ -157,6 +165,7 @@ 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", "local")
|
v.SetDefault("api.mode", "local")
|
||||||
|
v.SetDefault("api.enable_mkt_fallback", true)
|
||||||
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)
|
||||||
@@ -167,6 +176,13 @@ func Init(configPath string) error {
|
|||||||
v.SetDefault("token.default_ttl", "168h")
|
v.SetDefault("token.default_ttl", "168h")
|
||||||
v.SetDefault("feature.write_daily_files", true)
|
v.SetDefault("feature.write_daily_files", true)
|
||||||
v.SetDefault("web.path", "web")
|
v.SetDefault("web.path", "web")
|
||||||
|
|
||||||
|
// 默认抓取所有支持的地区
|
||||||
|
var defaultRegions []string
|
||||||
|
for _, r := range util.AllRegions {
|
||||||
|
defaultRegions = append(defaultRegions, r.Value)
|
||||||
|
}
|
||||||
|
v.SetDefault("fetcher.regions", defaultRegions)
|
||||||
v.SetDefault("admin.password_bcrypt", "$2a$10$fYHPeWHmwObephJvtlyH1O8DIgaLk5TINbi9BOezo2M8cSjmJchka") // 默认密码: admin123
|
v.SetDefault("admin.password_bcrypt", "$2a$10$fYHPeWHmwObephJvtlyH1O8DIgaLk5TINbi9BOezo2M8cSjmJchka") // 默认密码: admin123
|
||||||
|
|
||||||
// 绑定环境变量
|
// 绑定环境变量
|
||||||
@@ -311,3 +327,11 @@ func GetTokenTTL() time.Duration {
|
|||||||
}
|
}
|
||||||
return ttl
|
return ttl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDefaultMkt 返回生效的默认地区编码
|
||||||
|
func (c *Config) GetDefaultMkt() string {
|
||||||
|
if len(c.Fetcher.Regions) > 0 {
|
||||||
|
return c.Fetcher.Regions[0]
|
||||||
|
}
|
||||||
|
return BingMkt
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"BingPaper/internal/config"
|
"BingPaper/internal/config"
|
||||||
@@ -27,6 +28,7 @@ type ImageVariantResp struct {
|
|||||||
|
|
||||||
type ImageMetaResp struct {
|
type ImageMetaResp struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
|
Mkt string `json:"mkt"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Copyright string `json:"copyright"`
|
Copyright string `json:"copyright"`
|
||||||
CopyrightLink string `json:"copyrightlink"`
|
CopyrightLink string `json:"copyrightlink"`
|
||||||
@@ -41,13 +43,15 @@ type ImageMetaResp struct {
|
|||||||
// @Summary 获取今日图片
|
// @Summary 获取今日图片
|
||||||
// @Description 根据参数返回今日必应图片流或重定向
|
// @Description 根据参数返回今日必应图片流或重定向
|
||||||
// @Tags image
|
// @Tags image
|
||||||
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Param variant query string false "分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240)" default(UHD)
|
// @Param variant query string false "分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240)" default(UHD)
|
||||||
// @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
|
||||||
// @Router /image/today [get]
|
// @Router /image/today [get]
|
||||||
func GetToday(c *gin.Context) {
|
func GetToday(c *gin.Context) {
|
||||||
img, err := image.GetTodayImage()
|
mkt := c.Query("mkt")
|
||||||
|
img, err := image.GetTodayImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
@@ -59,11 +63,13 @@ func GetToday(c *gin.Context) {
|
|||||||
// @Summary 获取今日图片元数据
|
// @Summary 获取今日图片元数据
|
||||||
// @Description 获取今日必应图片的标题、版权等元数据
|
// @Description 获取今日必应图片的标题、版权等元数据
|
||||||
// @Tags image
|
// @Tags image
|
||||||
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ImageMetaResp
|
// @Success 200 {object} ImageMetaResp
|
||||||
// @Router /image/today/meta [get]
|
// @Router /image/today/meta [get]
|
||||||
func GetTodayMeta(c *gin.Context) {
|
func GetTodayMeta(c *gin.Context) {
|
||||||
img, err := image.GetTodayImage()
|
mkt := c.Query("mkt")
|
||||||
|
img, err := image.GetTodayImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
@@ -76,13 +82,15 @@ func GetTodayMeta(c *gin.Context) {
|
|||||||
// @Summary 获取随机图片
|
// @Summary 获取随机图片
|
||||||
// @Description 随机返回一张已抓取的图片流或重定向
|
// @Description 随机返回一张已抓取的图片流或重定向
|
||||||
// @Tags image
|
// @Tags image
|
||||||
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Param variant query string false "分辨率" default(UHD)
|
// @Param variant query string false "分辨率" default(UHD)
|
||||||
// @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
|
||||||
// @Router /image/random [get]
|
// @Router /image/random [get]
|
||||||
func GetRandom(c *gin.Context) {
|
func GetRandom(c *gin.Context) {
|
||||||
img, err := image.GetRandomImage()
|
mkt := c.Query("mkt")
|
||||||
|
img, err := image.GetRandomImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
@@ -94,11 +102,13 @@ func GetRandom(c *gin.Context) {
|
|||||||
// @Summary 获取随机图片元数据
|
// @Summary 获取随机图片元数据
|
||||||
// @Description 随机获取一张已抓取图片的元数据
|
// @Description 随机获取一张已抓取图片的元数据
|
||||||
// @Tags image
|
// @Tags image
|
||||||
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ImageMetaResp
|
// @Success 200 {object} ImageMetaResp
|
||||||
// @Router /image/random/meta [get]
|
// @Router /image/random/meta [get]
|
||||||
func GetRandomMeta(c *gin.Context) {
|
func GetRandomMeta(c *gin.Context) {
|
||||||
img, err := image.GetRandomImage()
|
mkt := c.Query("mkt")
|
||||||
|
img, err := image.GetRandomImage(mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
@@ -112,6 +122,7 @@ func GetRandomMeta(c *gin.Context) {
|
|||||||
// @Description 根据日期返回图片流或重定向 (yyyy-mm-dd)
|
// @Description 根据日期返回图片流或重定向 (yyyy-mm-dd)
|
||||||
// @Tags image
|
// @Tags image
|
||||||
// @Param date path string true "日期 (yyyy-mm-dd)"
|
// @Param date path string true "日期 (yyyy-mm-dd)"
|
||||||
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Param variant query string false "分辨率" default(UHD)
|
// @Param variant query string false "分辨率" default(UHD)
|
||||||
// @Param format query string false "格式" default(jpg)
|
// @Param format query string false "格式" default(jpg)
|
||||||
// @Produce image/jpeg
|
// @Produce image/jpeg
|
||||||
@@ -119,7 +130,8 @@ func GetRandomMeta(c *gin.Context) {
|
|||||||
// @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")
|
||||||
img, err := image.GetImageByDate(date)
|
mkt := c.Query("mkt")
|
||||||
|
img, err := image.GetImageByDate(date, mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
@@ -132,12 +144,14 @@ func GetByDate(c *gin.Context) {
|
|||||||
// @Description 根据日期获取图片元数据 (yyyy-mm-dd)
|
// @Description 根据日期获取图片元数据 (yyyy-mm-dd)
|
||||||
// @Tags image
|
// @Tags image
|
||||||
// @Param date path string true "日期 (yyyy-mm-dd)"
|
// @Param date path string true "日期 (yyyy-mm-dd)"
|
||||||
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ImageMetaResp
|
// @Success 200 {object} ImageMetaResp
|
||||||
// @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")
|
||||||
img, err := image.GetImageByDate(date)
|
mkt := c.Query("mkt")
|
||||||
|
img, err := image.GetImageByDate(date, mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
@@ -154,6 +168,7 @@ func GetByDateMeta(c *gin.Context) {
|
|||||||
// @Param page query int false "页码 (从1开始)"
|
// @Param page query int false "页码 (从1开始)"
|
||||||
// @Param page_size query int false "每页数量"
|
// @Param page_size query int false "每页数量"
|
||||||
// @Param month query string false "按月份过滤 (格式: YYYY-MM)"
|
// @Param month query string false "按月份过滤 (格式: YYYY-MM)"
|
||||||
|
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} ImageMetaResp
|
// @Success 200 {array} ImageMetaResp
|
||||||
// @Router /images [get]
|
// @Router /images [get]
|
||||||
@@ -162,10 +177,12 @@ func ListImages(c *gin.Context) {
|
|||||||
pageStr := c.Query("page")
|
pageStr := c.Query("page")
|
||||||
pageSizeStr := c.Query("page_size")
|
pageSizeStr := c.Query("page_size")
|
||||||
month := c.Query("month")
|
month := c.Query("month")
|
||||||
|
mkt := c.Query("mkt")
|
||||||
|
|
||||||
// 记录请求参数,便于排查过滤失效问题
|
// 记录请求参数,便于排查过滤失效问题
|
||||||
util.Logger.Debug("ListImages parameters",
|
util.Logger.Debug("ListImages parameters",
|
||||||
zap.String("month", month),
|
zap.String("month", month),
|
||||||
|
zap.String("mkt", mkt),
|
||||||
zap.String("page", pageStr),
|
zap.String("page", pageStr),
|
||||||
zap.String("page_size", pageSizeStr),
|
zap.String("page_size", pageSizeStr),
|
||||||
zap.String("limit", limitStr))
|
zap.String("limit", limitStr))
|
||||||
@@ -192,7 +209,7 @@ func ListImages(c *gin.Context) {
|
|||||||
offset = 0
|
offset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
images, err := image.GetImageList(limit, offset, month)
|
images, err := image.GetImageList(limit, offset, month, mkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.Logger.Error("ListImages service call failed", zap.Error(err))
|
util.Logger.Error("ListImages service call failed", zap.Error(err))
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -291,7 +308,7 @@ func formatMeta(img *model.Image) gin.H {
|
|||||||
if url == "" && cfg.API.Mode == "redirect" && img.URLBase != "" {
|
if url == "" && cfg.API.Mode == "redirect" && img.URLBase != "" {
|
||||||
url = fmt.Sprintf("https://www.bing.com%s_%s.jpg", img.URLBase, v.Variant)
|
url = fmt.Sprintf("https://www.bing.com%s_%s.jpg", img.URLBase, v.Variant)
|
||||||
} else if cfg.API.Mode == "local" || url == "" {
|
} else if cfg.API.Mode == "local" || url == "" {
|
||||||
url = fmt.Sprintf("%s/api/v1/image/date/%s?variant=%s&format=%s", cfg.Server.BaseURL, img.Date, v.Variant, v.Format)
|
url = fmt.Sprintf("%s/api/v1/image/date/%s?variant=%s&format=%s&mkt=%s", cfg.Server.BaseURL, img.Date, v.Variant, v.Format, img.Mkt)
|
||||||
}
|
}
|
||||||
variants = append(variants, gin.H{
|
variants = append(variants, gin.H{
|
||||||
"variant": v.Variant,
|
"variant": v.Variant,
|
||||||
@@ -304,6 +321,7 @@ func formatMeta(img *model.Image) gin.H {
|
|||||||
|
|
||||||
return gin.H{
|
return gin.H{
|
||||||
"date": img.Date,
|
"date": img.Date,
|
||||||
|
"mkt": img.Mkt,
|
||||||
"title": img.Title,
|
"title": img.Title,
|
||||||
"copyright": img.Copyright,
|
"copyright": img.Copyright,
|
||||||
"copyrightlink": img.CopyrightLink,
|
"copyrightlink": img.CopyrightLink,
|
||||||
@@ -314,3 +332,46 @@ func formatMeta(img *model.Image) gin.H {
|
|||||||
"variants": variants,
|
"variants": variants,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRegions 获取支持的地区列表
|
||||||
|
// @Summary 获取支持的地区列表
|
||||||
|
// @Description 返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。
|
||||||
|
// @Tags image
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} util.Region
|
||||||
|
// @Router /regions [get]
|
||||||
|
func GetRegions(c *gin.Context) {
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
pinned := cfg.Fetcher.Regions
|
||||||
|
|
||||||
|
// 创建副本以避免修改原始全局变量
|
||||||
|
all := make([]util.Region, len(util.AllRegions))
|
||||||
|
copy(all, util.AllRegions)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -66,3 +67,27 @@ func TestHandleImageResponseRedirect(t *testing.T) {
|
|||||||
assert.Contains(t, variants[0]["url"].(string), "/api/v1/image/date/")
|
assert.Contains(t, variants[0]["url"].(string), "/api/v1/image/date/")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetRegions(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
t.Run("GetRegions should respect pinned order", func(t *testing.T) {
|
||||||
|
// Setup config with custom pinned regions
|
||||||
|
config.Init("")
|
||||||
|
config.GetConfig().Fetcher.Regions = []string{"en-US", "ja-JP"}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
GetRegions(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var regions []map[string]string
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), ®ions)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, len(regions), 2)
|
||||||
|
assert.Equal(t, "en-US", regions[0]["value"])
|
||||||
|
assert.Equal(t, "ja-JP", regions[1]["value"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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("/regions", handlers.GetRegions)
|
||||||
|
|
||||||
// 管理接口
|
// 管理接口
|
||||||
admin := api.Group("/admin")
|
admin := api.Group("/admin")
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
|
|
||||||
type Image struct {
|
type Image struct {
|
||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
Date string `gorm:"uniqueIndex;type:varchar(10)" json:"date"` // YYYY-MM-DD
|
Date string `gorm:"uniqueIndex:idx_date_mkt;type:varchar(10)" json:"date"` // YYYY-MM-DD
|
||||||
|
Mkt string `gorm:"uniqueIndex:idx_date_mkt;type:varchar(10)" json:"mkt"` // zh-CN, en-US etc.
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Copyright string `json:"copyright"`
|
Copyright string `json:"copyright"`
|
||||||
CopyrightLink string `json:"copyrightlink"`
|
CopyrightLink string `json:"copyrightlink"`
|
||||||
|
|||||||
@@ -55,7 +55,30 @@ func NewFetcher() *Fetcher {
|
|||||||
|
|
||||||
func (f *Fetcher) Fetch(ctx context.Context, n int) error {
|
func (f *Fetcher) Fetch(ctx context.Context, n int) error {
|
||||||
util.Logger.Info("Starting fetch task", zap.Int("n", n))
|
util.Logger.Info("Starting fetch task", zap.Int("n", n))
|
||||||
url := fmt.Sprintf("%s?format=js&idx=0&n=%d&uhd=1&mkt=%s", config.BingAPIBase, n, config.BingMkt)
|
regions := config.GetConfig().Fetcher.Regions
|
||||||
|
if len(regions) == 0 {
|
||||||
|
regions = []string{config.GetConfig().GetDefaultMkt()}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mkt := range regions {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
// 第二次 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util.Logger.Info("Fetch task completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
util.Logger.Debug("Requesting Bing API", zap.String("url", url))
|
util.Logger.Debug("Requesting Bing API", zap.String("url", url))
|
||||||
resp, err := f.httpClient.Get(url)
|
resp, err := f.httpClient.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -70,29 +93,28 @@ func (f *Fetcher) Fetch(ctx context.Context, n int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
util.Logger.Info("Fetched images from Bing", zap.Int("count", len(bingResp.Images)))
|
util.Logger.Info("Fetched images from Bing", zap.String("mkt", mkt), zap.Int("count", len(bingResp.Images)))
|
||||||
|
|
||||||
for _, bingImg := range bingResp.Images {
|
for _, bingImg := range bingResp.Images {
|
||||||
if err := f.processImage(ctx, bingImg); err != nil {
|
if err := f.processImage(ctx, bingImg, mkt); err != nil {
|
||||||
util.Logger.Error("Failed to process image", zap.String("date", bingImg.Enddate), zap.Error(err))
|
util.Logger.Error("Failed to process image", zap.String("date", bingImg.Enddate), zap.String("mkt", mkt), zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
util.Logger.Info("Fetch task completed")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage, mkt string) error {
|
||||||
dateStr := fmt.Sprintf("%s-%s-%s", bingImg.Enddate[0:4], bingImg.Enddate[4:6], bingImg.Enddate[6:8])
|
dateStr := fmt.Sprintf("%s-%s-%s", bingImg.Enddate[0:4], bingImg.Enddate[4:6], bingImg.Enddate[6:8])
|
||||||
|
|
||||||
// 幂等检查
|
// 幂等检查
|
||||||
var existing model.Image
|
var existing model.Image
|
||||||
if err := repo.DB.Where("date = ?", dateStr).First(&existing).Error; err == nil {
|
if err := repo.DB.Where("date = ? AND mkt = ?", dateStr, mkt).First(&existing).Error; err == nil {
|
||||||
util.Logger.Info("Image already exists, skipping", zap.String("date", dateStr))
|
util.Logger.Info("Image already exists, skipping", zap.String("date", dateStr), zap.String("mkt", mkt))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
util.Logger.Info("Processing new image", zap.String("date", dateStr), zap.String("title", bingImg.Title))
|
util.Logger.Info("Processing new image", zap.String("date", dateStr), zap.String("mkt", mkt), zap.String("title", bingImg.Title))
|
||||||
|
|
||||||
// UHD 探测
|
// UHD 探测
|
||||||
imgURL, variantName := f.probeUHD(bingImg.URLBase)
|
imgURL, variantName := f.probeUHD(bingImg.URLBase)
|
||||||
@@ -113,6 +135,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
|||||||
// 创建 DB 记录
|
// 创建 DB 记录
|
||||||
dbImg := model.Image{
|
dbImg := model.Image{
|
||||||
Date: dateStr,
|
Date: dateStr,
|
||||||
|
Mkt: mkt,
|
||||||
Title: bingImg.Title,
|
Title: bingImg.Title,
|
||||||
Copyright: bingImg.Copyright,
|
Copyright: bingImg.Copyright,
|
||||||
CopyrightLink: bingImg.CopyrightLink,
|
CopyrightLink: bingImg.CopyrightLink,
|
||||||
@@ -124,7 +147,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := repo.DB.Clauses(clause.OnConflict{
|
if err := repo.DB.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "date"}},
|
Columns: []clause.Column{{Name: "date"}, {Name: "mkt"}},
|
||||||
DoNothing: true,
|
DoNothing: true,
|
||||||
}).Create(&dbImg).Error; err != nil {
|
}).Create(&dbImg).Error; err != nil {
|
||||||
util.Logger.Error("Failed to create image record", zap.Error(err))
|
util.Logger.Error("Failed to create image record", zap.Error(err))
|
||||||
@@ -134,7 +157,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
|||||||
// 再次检查 dbImg.ID 是否被填充,如果没有被填充(说明由于冲突未插入),则需要查询出已有的 ID
|
// 再次检查 dbImg.ID 是否被填充,如果没有被填充(说明由于冲突未插入),则需要查询出已有的 ID
|
||||||
if dbImg.ID == 0 {
|
if dbImg.ID == 0 {
|
||||||
var existing model.Image
|
var existing model.Image
|
||||||
if err := repo.DB.Where("date = ?", dateStr).First(&existing).Error; err != nil {
|
if err := repo.DB.Where("date = ? AND mkt = ?", dateStr, mkt).First(&existing).Error; err != nil {
|
||||||
util.Logger.Error("Failed to query existing image record after conflict", zap.Error(err))
|
util.Logger.Error("Failed to query existing image record after conflict", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -188,7 +211,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
|||||||
// 保存今日额外文件
|
// 保存今日额外文件
|
||||||
today := time.Now().Format("2006-01-02")
|
today := time.Now().Format("2006-01-02")
|
||||||
if dateStr == today && config.GetConfig().Feature.WriteDailyFiles {
|
if dateStr == today && config.GetConfig().Feature.WriteDailyFiles {
|
||||||
f.saveDailyFiles(srcImg, imgData)
|
f.saveDailyFiles(srcImg, imgData, mkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -213,7 +236,7 @@ func (f *Fetcher) downloadImage(url string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) saveVariant(ctx context.Context, img *model.Image, variant, format string, data []byte) error {
|
func (f *Fetcher) saveVariant(ctx context.Context, img *model.Image, variant, format string, data []byte) error {
|
||||||
key := fmt.Sprintf("%s/%s_%s.%s", img.Date, img.Date, variant, format)
|
key := fmt.Sprintf("%s/%s/%s_%s.%s", img.Mkt, img.Date, img.Date, variant, format)
|
||||||
contentType := "image/jpeg"
|
contentType := "image/jpeg"
|
||||||
if format == "webp" {
|
if format == "webp" {
|
||||||
contentType = "image/webp"
|
contentType = "image/webp"
|
||||||
@@ -239,20 +262,21 @@ func (f *Fetcher) saveVariant(ctx context.Context, img *model.Image, variant, fo
|
|||||||
}).Create(&vRecord).Error
|
}).Create(&vRecord).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte) {
|
func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte, mkt string) {
|
||||||
util.Logger.Info("Saving daily files")
|
util.Logger.Info("Saving daily files", zap.String("mkt", mkt))
|
||||||
localRoot := config.GetConfig().Storage.Local.Root
|
localRoot := config.GetConfig().Storage.Local.Root
|
||||||
if localRoot == "" {
|
if localRoot == "" {
|
||||||
localRoot = "data"
|
localRoot = "data"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(localRoot, 0755); err != nil {
|
mktDir := filepath.Join(localRoot, mkt)
|
||||||
util.Logger.Error("Failed to create directory", zap.String("path", localRoot), zap.Error(err))
|
if err := os.MkdirAll(mktDir, 0755); err != nil {
|
||||||
|
util.Logger.Error("Failed to create directory", zap.String("path", mktDir), zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// daily.jpeg (quality 100)
|
// daily.jpeg (quality 100)
|
||||||
jpegPath := filepath.Join(localRoot, "daily.jpeg")
|
jpegPath := filepath.Join(mktDir, "daily.jpeg")
|
||||||
fJpeg, err := os.Create(jpegPath)
|
fJpeg, err := os.Create(jpegPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.Logger.Error("Failed to create daily.jpeg", zap.Error(err))
|
util.Logger.Error("Failed to create daily.jpeg", zap.Error(err))
|
||||||
@@ -262,8 +286,21 @@ func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// original.jpeg (quality 100)
|
// original.jpeg (quality 100)
|
||||||
originalPath := filepath.Join(localRoot, "original.jpeg")
|
originalPath := filepath.Join(mktDir, "original.jpeg")
|
||||||
if err := os.WriteFile(originalPath, originalData, 0644); err != nil {
|
if err := os.WriteFile(originalPath, originalData, 0644); err != nil {
|
||||||
util.Logger.Error("Failed to write original.jpeg", zap.Error(err))
|
util.Logger.Error("Failed to write original.jpeg", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同时也保留一份在根目录下(兼容旧逻辑,且作为默认地区图片)
|
||||||
|
// 如果是默认地区或者是第一个抓取的地区,可以覆盖根目录的文件
|
||||||
|
if mkt == config.GetConfig().GetDefaultMkt() {
|
||||||
|
jpegPathRoot := filepath.Join(localRoot, "daily.jpeg")
|
||||||
|
fJpegRoot, err := os.Create(jpegPathRoot)
|
||||||
|
if err == nil {
|
||||||
|
jpeg.Encode(fJpegRoot, srcImg, &jpeg.Options{Quality: 100})
|
||||||
|
fJpegRoot.Close()
|
||||||
|
}
|
||||||
|
originalPathRoot := filepath.Join(localRoot, "original.jpeg")
|
||||||
|
os.WriteFile(originalPathRoot, originalData, 0644)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,43 +50,97 @@ func CleanupOldImages(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTodayImage() (*model.Image, error) {
|
func GetTodayImage(mkt string) (*model.Image, error) {
|
||||||
today := time.Now().Format("2006-01-02")
|
today := time.Now().Format("2006-01-02")
|
||||||
var img model.Image
|
var img model.Image
|
||||||
err := repo.DB.Where("date = ?", today).Preload("Variants").First(&img).Error
|
tx := repo.DB.Where("date = ?", today)
|
||||||
|
if mkt != "" {
|
||||||
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
|
}
|
||||||
|
err := tx.Preload("Variants").First(&img).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 如果今天没有,尝试获取最近的一张
|
// 如果今天没有,尝试获取最近的一张
|
||||||
err = repo.DB.Order("date desc").Preload("Variants").First(&img).Error
|
tx = repo.DB.Order("date desc")
|
||||||
|
if mkt != "" {
|
||||||
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
|
}
|
||||||
|
err = tx.Preload("Variants").First(&img).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兜底逻辑:如果指定地区没找到,且开启了兜底开关,则尝试获取默认地区的图片
|
||||||
|
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
||||||
|
defaultMkt := config.GetConfig().GetDefaultMkt()
|
||||||
|
if mkt != defaultMkt {
|
||||||
|
return GetTodayImage(defaultMkt)
|
||||||
|
}
|
||||||
|
return GetTodayImage("")
|
||||||
|
}
|
||||||
|
|
||||||
return &img, err
|
return &img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRandomImage() (*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()
|
||||||
// 简单起见,先查总数再 Offset
|
// 简单起见,先查总数再 Offset
|
||||||
var count int64
|
var count int64
|
||||||
repo.DB.Model(&model.Image{}).Count(&count)
|
tx := repo.DB.Model(&model.Image{})
|
||||||
|
if mkt != "" {
|
||||||
|
tx = tx.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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这种方法不适合海量数据,但对于 30 天的数据没问题
|
// 这种方法不适合海量数据,但对于 30 天的数据没问题
|
||||||
err := repo.DB.Order("RANDOM()").Preload("Variants").First(&img).Error
|
tx = repo.DB.Order("RANDOM()")
|
||||||
|
if mkt != "" {
|
||||||
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
|
}
|
||||||
|
err := tx.Preload("Variants").First(&img).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 适配 MySQL
|
// 适配 MySQL
|
||||||
err = repo.DB.Order("RAND()").Preload("Variants").First(&img).Error
|
tx = repo.DB.Order("RAND()")
|
||||||
|
if mkt != "" {
|
||||||
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
|
}
|
||||||
|
err = tx.Preload("Variants").First(&img).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兜底逻辑
|
||||||
|
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
||||||
|
defaultMkt := config.GetConfig().GetDefaultMkt()
|
||||||
|
if mkt != defaultMkt {
|
||||||
|
return GetRandomImage(defaultMkt)
|
||||||
|
}
|
||||||
|
return GetRandomImage("")
|
||||||
|
}
|
||||||
|
|
||||||
return &img, err
|
return &img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetImageByDate(date string) (*model.Image, error) {
|
func GetImageByDate(date string, mkt string) (*model.Image, error) {
|
||||||
var img model.Image
|
var img model.Image
|
||||||
err := repo.DB.Where("date = ?", date).Preload("Variants").First(&img).Error
|
tx := repo.DB.Where("date = ?", date)
|
||||||
|
if mkt != "" {
|
||||||
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
|
}
|
||||||
|
err := tx.Preload("Variants").First(&img).Error
|
||||||
|
|
||||||
|
// 兜底逻辑
|
||||||
|
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
||||||
|
defaultMkt := config.GetConfig().GetDefaultMkt()
|
||||||
|
if mkt != defaultMkt {
|
||||||
|
return GetImageByDate(date, defaultMkt)
|
||||||
|
}
|
||||||
|
return GetImageByDate(date, "")
|
||||||
|
}
|
||||||
|
|
||||||
return &img, err
|
return &img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetImageList(limit int, offset int, month string) ([]model.Image, error) {
|
func GetImageList(limit int, offset int, month string, mkt string) ([]model.Image, error) {
|
||||||
var images []model.Image
|
var images []model.Image
|
||||||
tx := repo.DB.Model(&model.Image{})
|
tx := repo.DB.Model(&model.Image{})
|
||||||
|
|
||||||
@@ -97,6 +151,10 @@ func GetImageList(limit int, offset int, month string) ([]model.Image, error) {
|
|||||||
tx = tx.Where("date LIKE ?", month+"%")
|
tx = tx.Where("date LIKE ?", month+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mkt != "" {
|
||||||
|
tx = tx.Where("mkt = ?", mkt)
|
||||||
|
}
|
||||||
|
|
||||||
tx = tx.Order("date desc").Preload("Variants")
|
tx = tx.Order("date desc").Preload("Variants")
|
||||||
|
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
|
|||||||
26
internal/util/regions.go
Normal file
26
internal/util/regions.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
type Region struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var AllRegions = []Region{
|
||||||
|
{Value: "zh-CN", Label: "中国 (zh-CN)"},
|
||||||
|
{Value: "en-US", Label: "美国 (en-US)"},
|
||||||
|
{Value: "ja-JP", Label: "日本 (ja-JP)"},
|
||||||
|
{Value: "en-AU", Label: "澳大利亚 (en-AU)"},
|
||||||
|
{Value: "en-GB", Label: "英国 (en-GB)"},
|
||||||
|
{Value: "de-DE", Label: "德国 (de-DE)"},
|
||||||
|
{Value: "en-NZ", Label: "新西兰 (en-NZ)"},
|
||||||
|
{Value: "en-CA", Label: "加拿大 (en-CA)"},
|
||||||
|
{Value: "fr-FR", Label: "法国 (fr-FR)"},
|
||||||
|
{Value: "it-IT", Label: "意大利 (it-IT)"},
|
||||||
|
{Value: "es-ES", Label: "西班牙 (es-ES)"},
|
||||||
|
{Value: "pt-BR", Label: "巴西 (pt-BR)"},
|
||||||
|
{Value: "ko-KR", Label: "韩国 (ko-KR)"},
|
||||||
|
{Value: "en-IN", Label: "印度 (en-IN)"},
|
||||||
|
{Value: "ru-RU", Label: "俄罗斯 (ru-RU)"},
|
||||||
|
{Value: "zh-HK", Label: "中国香港 (zh-HK)"},
|
||||||
|
{Value: "zh-TW", Label: "中国台湾 (zh-TW)"},
|
||||||
|
}
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
<div class="fixed inset-0 z-40">
|
<div class="fixed inset-0 z-40">
|
||||||
<div
|
<div
|
||||||
ref="calendarPanel"
|
ref="calendarPanel"
|
||||||
class="fixed bg-gradient-to-br from-black/30 via-black/20 to-black/30 backdrop-blur-xl rounded-3xl p-3 sm:p-4 w-[calc(100%-1rem)] sm:w-full max-w-[95vw] sm:max-w-[420px] shadow-2xl border border-white/10 cursor-move select-none"
|
class="fixed bg-gradient-to-br from-black/30 via-black/20 to-black/30 backdrop-blur-xl rounded-3xl p-3 sm:p-4 w-[calc(100%-1rem)] sm:w-full max-w-[95vw] sm:max-w-[420px] shadow-2xl border border-white/10 cursor-move select-none touch-none"
|
||||||
:style="{ left: panelPos.x + 'px', top: panelPos.y + 'px' }"
|
:style="{ left: panelPos.x + 'px', top: panelPos.y + 'px' }"
|
||||||
@mousedown="startDrag"
|
@mousedown="startDrag"
|
||||||
@touchstart="startDrag"
|
@touchstart.passive="startDrag"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<!-- 拖动手柄指示器 -->
|
<!-- 拖动手柄指示器 -->
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<!-- 年月选择器 -->
|
<!-- 年月选择器 -->
|
||||||
<div class="flex items-center justify-center gap-1 sm:gap-1.5 mb-0.5">
|
<div class="flex items-center justify-center gap-1 sm:gap-1.5 mb-0.5">
|
||||||
<!-- 年份选择 -->
|
<!-- 年份选择 -->
|
||||||
<Select v-model="currentYearString" @update:modelValue="onYearChange">
|
<Select v-model="currentYearString">
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
class="w-[90px] sm:w-[105px] h-6 sm:h-7 bg-white/10 text-white border-white/20 hover:bg-white/20 backdrop-blur-md font-bold text-xs sm:text-sm px-1.5 sm:px-2"
|
class="w-[90px] sm:w-[105px] h-6 sm:h-7 bg-white/10 text-white border-white/20 hover:bg-white/20 backdrop-blur-md font-bold text-xs sm:text-sm px-1.5 sm:px-2"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<!-- 月份选择 -->
|
<!-- 月份选择 -->
|
||||||
<Select v-model="currentMonthString" @update:modelValue="onMonthChange">
|
<Select v-model="currentMonthString">
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
class="w-[65px] sm:w-[75px] h-6 sm:h-7 bg-white/10 text-white border-white/20 hover:bg-white/20 backdrop-blur-md font-bold text-xs sm:text-sm px-1.5 sm:px-2"
|
class="w-[65px] sm:w-[75px] h-6 sm:h-7 bg-white/10 text-white border-white/20 hover:bg-white/20 backdrop-blur-md font-bold text-xs sm:text-sm px-1.5 sm:px-2"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -214,7 +214,8 @@ interface CalendarDay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
selectedDate: string // YYYY-MM-DD
|
selectedDate?: string,
|
||||||
|
mkt?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -229,10 +230,14 @@ const panelPos = ref({ x: 0, y: 0 })
|
|||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragStart = ref({ x: 0, y: 0 })
|
const dragStart = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// 响应式窗口大小
|
||||||
|
const windowSize = ref({ width: window.innerWidth, height: window.innerHeight })
|
||||||
|
const isMobile = computed(() => windowSize.value.width < 768)
|
||||||
|
|
||||||
// 计算图片实际显示区域(与ImageView保持一致)
|
// 计算图片实际显示区域(与ImageView保持一致)
|
||||||
const getImageDisplayBounds = () => {
|
const getImageDisplayBounds = () => {
|
||||||
const windowWidth = window.innerWidth
|
const windowWidth = windowSize.value.width
|
||||||
const windowHeight = window.innerHeight
|
const windowHeight = windowSize.value.height
|
||||||
|
|
||||||
// 必应图片通常是16:9或类似宽高比
|
// 必应图片通常是16:9或类似宽高比
|
||||||
const imageAspectRatio = 16 / 9
|
const imageAspectRatio = 16 / 9
|
||||||
@@ -271,11 +276,10 @@ const getImageDisplayBounds = () => {
|
|||||||
const initPanelPosition = () => {
|
const initPanelPosition = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const bounds = getImageDisplayBounds()
|
const bounds = getImageDisplayBounds()
|
||||||
const isMobile = window.innerWidth < 640 // sm breakpoint
|
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile.value) {
|
||||||
// 移动端:在图片区域内居中显示
|
// 移动端:居中显示,尽量在图片内,但不强求
|
||||||
const panelWidth = Math.min(bounds.width - 16, window.innerWidth - 16)
|
const panelWidth = Math.min(bounds.width - 16, windowSize.value.width - 16)
|
||||||
const panelHeight = 580 // 估计高度
|
const panelHeight = 580 // 估计高度
|
||||||
panelPos.value = {
|
panelPos.value = {
|
||||||
x: Math.max(bounds.left, bounds.left + (bounds.width - panelWidth) / 2),
|
x: Math.max(bounds.left, bounds.left + (bounds.width - panelWidth) / 2),
|
||||||
@@ -316,20 +320,6 @@ const currentMonthString = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 年份改变处理
|
|
||||||
const onYearChange = (value: any) => {
|
|
||||||
if (value !== null && value !== undefined) {
|
|
||||||
currentYear.value = Number(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 月份改变处理
|
|
||||||
const onMonthChange = (value: any) => {
|
|
||||||
if (value !== null && value !== undefined) {
|
|
||||||
currentMonth.value = Number(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成年份选项(从2009年到当前年份+10年)
|
// 生成年份选项(从2009年到当前年份+10年)
|
||||||
const yearOptions = computed(() => {
|
const yearOptions = computed(() => {
|
||||||
const currentYearValue = new Date().getFullYear()
|
const currentYearValue = new Date().getFullYear()
|
||||||
@@ -379,8 +369,20 @@ const loadHolidaysForYear = async (year: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 窗口缩放处理
|
||||||
|
const handleResize = () => {
|
||||||
|
windowSize.value = {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
}
|
||||||
|
initPanelPosition()
|
||||||
|
}
|
||||||
|
|
||||||
// 组件挂载时加载当前年份的假期数据
|
// 组件挂载时加载当前年份的假期数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
const currentYearValue = currentYear.value
|
const currentYearValue = currentYear.value
|
||||||
loadHolidaysForYear(currentYearValue)
|
loadHolidaysForYear(currentYearValue)
|
||||||
// 预加载前后一年的数据
|
// 预加载前后一年的数据
|
||||||
@@ -404,7 +406,9 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault()
|
if (e instanceof MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
|
|
||||||
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
|
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
|
||||||
@@ -417,7 +421,7 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
|
|
||||||
document.addEventListener('mousemove', onDrag)
|
document.addEventListener('mousemove', onDrag)
|
||||||
document.addEventListener('mouseup', stopDrag)
|
document.addEventListener('mouseup', stopDrag)
|
||||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
document.addEventListener('touchmove', onDrag, { passive: true })
|
||||||
document.addEventListener('touchend', stopDrag)
|
document.addEventListener('touchend', stopDrag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,7 +429,7 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
const onDrag = (e: MouseEvent | TouchEvent) => {
|
const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
|
|
||||||
if (e instanceof TouchEvent) {
|
if (e instanceof MouseEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,15 +439,26 @@ const onDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
const newX = clientX - dragStart.value.x
|
const newX = clientX - dragStart.value.x
|
||||||
const newY = clientY - dragStart.value.y
|
const newY = clientY - dragStart.value.y
|
||||||
|
|
||||||
// 限制在图片实际显示区域内
|
// 限制在有效区域内
|
||||||
if (calendarPanel.value) {
|
if (calendarPanel.value) {
|
||||||
const rect = calendarPanel.value.getBoundingClientRect()
|
const rect = calendarPanel.value.getBoundingClientRect()
|
||||||
const bounds = getImageDisplayBounds()
|
|
||||||
|
|
||||||
const minX = bounds.left
|
let minX, maxX, minY, maxY
|
||||||
const maxX = bounds.right - rect.width
|
|
||||||
const minY = bounds.top
|
if (isMobile.value) {
|
||||||
const maxY = bounds.bottom - rect.height
|
// 移动端:不限制区域,限制在视口内即可
|
||||||
|
minX = 0
|
||||||
|
maxX = windowSize.value.width - rect.width
|
||||||
|
minY = 0
|
||||||
|
maxY = windowSize.value.height - rect.height
|
||||||
|
} else {
|
||||||
|
// 桌面端:限制在图片实际显示区域内
|
||||||
|
const bounds = getImageDisplayBounds()
|
||||||
|
minX = bounds.left
|
||||||
|
maxX = bounds.right - rect.width
|
||||||
|
minY = bounds.top
|
||||||
|
maxY = bounds.bottom - rect.height
|
||||||
|
}
|
||||||
|
|
||||||
panelPos.value = {
|
panelPos.value = {
|
||||||
x: Math.max(minX, Math.min(newX, maxX)),
|
x: Math.max(minX, Math.min(newX, maxX)),
|
||||||
@@ -517,7 +532,7 @@ const createDayObject = (date: Date, isCurrentMonth: boolean): CalendarDay => {
|
|||||||
const today = new Date()
|
const today = new Date()
|
||||||
today.setHours(0, 0, 0, 0)
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
const selectedDate = new Date(props.selectedDate)
|
const selectedDate = new Date(props.selectedDate || new Date())
|
||||||
selectedDate.setHours(0, 0, 0, 0)
|
selectedDate.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
// 转换为农历
|
// 转换为农历
|
||||||
@@ -626,6 +641,9 @@ const goToToday = () => {
|
|||||||
// 清理
|
// 清理
|
||||||
import { onUnmounted } from 'vue'
|
import { onUnmounted } from 'vue'
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
document.removeEventListener('touchmove', onDrag)
|
document.removeEventListener('touchmove', onDrag)
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { ref, onMounted, watch } from 'vue'
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { bingPaperApi } from '@/lib/api-service'
|
import { bingPaperApi } from '@/lib/api-service'
|
||||||
import type { ImageMeta } from '@/lib/api-types'
|
import type { ImageMeta } from '@/lib/api-types'
|
||||||
|
import { getDefaultMkt } from '@/lib/mkt-utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取今日图片
|
* 获取今日图片
|
||||||
*/
|
*/
|
||||||
export function useTodayImage() {
|
export function useTodayImage(mkt?: string) {
|
||||||
const image = ref<ImageMeta | null>(null)
|
const image = ref<ImageMeta | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<Error | null>(null)
|
const error = ref<Error | null>(null)
|
||||||
@@ -15,7 +16,7 @@ export function useTodayImage() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
image.value = await bingPaperApi.getTodayImageMeta()
|
image.value = await bingPaperApi.getTodayImageMeta(mkt || getDefaultMkt())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e as Error
|
error.value = e as Error
|
||||||
console.error('Failed to fetch today image:', e)
|
console.error('Failed to fetch today image:', e)
|
||||||
@@ -46,8 +47,9 @@ export function useImageList(pageSize = 30) {
|
|||||||
const hasMore = ref(true)
|
const hasMore = ref(true)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const currentMonth = ref<string | undefined>(undefined)
|
const currentMonth = ref<string | undefined>(undefined)
|
||||||
|
const currentMkt = ref<string | undefined>(getDefaultMkt())
|
||||||
|
|
||||||
const fetchImages = async (page = 1, month?: string) => {
|
const fetchImages = async (page = 1, month?: string, mkt?: string) => {
|
||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -55,7 +57,8 @@ export function useImageList(pageSize = 30) {
|
|||||||
try {
|
try {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
page,
|
page,
|
||||||
page_size: pageSize
|
page_size: pageSize,
|
||||||
|
mkt: mkt || currentMkt.value || getDefaultMkt()
|
||||||
}
|
}
|
||||||
if (month) {
|
if (month) {
|
||||||
params.month = month
|
params.month = month
|
||||||
@@ -84,7 +87,7 @@ export function useImageList(pageSize = 30) {
|
|||||||
|
|
||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
if (!loading.value && hasMore.value) {
|
if (!loading.value && hasMore.value) {
|
||||||
fetchImages(currentPage.value + 1, currentMonth.value)
|
fetchImages(currentPage.value + 1, currentMonth.value, currentMkt.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +95,14 @@ export function useImageList(pageSize = 30) {
|
|||||||
currentMonth.value = month
|
currentMonth.value = month
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
hasMore.value = true
|
hasMore.value = true
|
||||||
fetchImages(1, month)
|
fetchImages(1, month, currentMkt.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByMkt = (mkt?: string) => {
|
||||||
|
currentMkt.value = mkt
|
||||||
|
currentPage.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
fetchImages(1, currentMonth.value, mkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -106,10 +116,11 @@ export function useImageList(pageSize = 30) {
|
|||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
filterByMonth,
|
filterByMonth,
|
||||||
|
filterByMkt,
|
||||||
refetch: () => {
|
refetch: () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
hasMore.value = true
|
hasMore.value = true
|
||||||
fetchImages(1, currentMonth.value)
|
fetchImages(1, currentMonth.value, currentMkt.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +128,7 @@ export function useImageList(pageSize = 30) {
|
|||||||
/**
|
/**
|
||||||
* 获取指定日期的图片
|
* 获取指定日期的图片
|
||||||
*/
|
*/
|
||||||
export function useImageByDate(dateRef: Ref<string>) {
|
export function useImageByDate(dateRef: Ref<string>, mktRef?: Ref<string | undefined>) {
|
||||||
const image = ref<ImageMeta | null>(null)
|
const image = ref<ImageMeta | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<Error | null>(null)
|
const error = ref<Error | null>(null)
|
||||||
@@ -126,7 +137,7 @@ export function useImageByDate(dateRef: Ref<string>) {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
image.value = await bingPaperApi.getImageMetaByDate(dateRef.value)
|
image.value = await bingPaperApi.getImageMetaByDate(dateRef.value, mktRef?.value || getDefaultMkt())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e as Error
|
error.value = e as Error
|
||||||
console.error(`Failed to fetch image for date ${dateRef.value}:`, e)
|
console.error(`Failed to fetch image for date ${dateRef.value}:`, e)
|
||||||
@@ -135,10 +146,16 @@ export function useImageByDate(dateRef: Ref<string>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听日期变化,自动重新获取
|
// 监听日期和地区变化,自动重新获取
|
||||||
watch(dateRef, () => {
|
if (mktRef) {
|
||||||
fetchImage()
|
watch([dateRef, mktRef], () => {
|
||||||
}, { immediate: true })
|
fetchImage()
|
||||||
|
}, { immediate: true })
|
||||||
|
} else {
|
||||||
|
watch(dateRef, () => {
|
||||||
|
fetchImage()
|
||||||
|
}, { immediate: true })
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
image,
|
image,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
UpdateTokenRequest,
|
UpdateTokenRequest,
|
||||||
ChangePasswordRequest,
|
ChangePasswordRequest,
|
||||||
Config,
|
Config,
|
||||||
|
Region,
|
||||||
ImageMeta,
|
ImageMeta,
|
||||||
ImageListParams,
|
ImageListParams,
|
||||||
ManualFetchRequest,
|
ManualFetchRequest,
|
||||||
@@ -109,6 +110,7 @@ export class BingPaperApiService {
|
|||||||
if (params?.page) searchParams.set('page', params.page.toString())
|
if (params?.page) searchParams.set('page', params.page.toString())
|
||||||
if (params?.page_size) searchParams.set('page_size', params.page_size.toString())
|
if (params?.page_size) searchParams.set('page_size', params.page_size.toString())
|
||||||
if (params?.month) searchParams.set('month', params.month)
|
if (params?.month) searchParams.set('month', params.month)
|
||||||
|
if (params?.mkt) searchParams.set('mkt', params.mkt)
|
||||||
|
|
||||||
const queryString = searchParams.toString()
|
const queryString = searchParams.toString()
|
||||||
const endpoint = queryString ? `/images?${queryString}` : '/images'
|
const endpoint = queryString ? `/images?${queryString}` : '/images'
|
||||||
@@ -116,48 +118,61 @@ export class BingPaperApiService {
|
|||||||
return apiClient.get<ImageMeta[]>(endpoint)
|
return apiClient.get<ImageMeta[]>(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的地区列表
|
||||||
|
*/
|
||||||
|
async getRegions(): Promise<Region[]> {
|
||||||
|
return apiClient.get<Region[]>('/regions')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取今日图片元数据
|
* 获取今日图片元数据
|
||||||
*/
|
*/
|
||||||
async getTodayImageMeta(): Promise<ImageMeta> {
|
async getTodayImageMeta(mkt?: string): Promise<ImageMeta> {
|
||||||
return apiClient.get<ImageMeta>('/image/today/meta')
|
const endpoint = mkt ? `/image/today/meta?mkt=${mkt}` : '/image/today/meta'
|
||||||
|
return apiClient.get<ImageMeta>(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定日期图片元数据
|
* 获取指定日期图片元数据
|
||||||
*/
|
*/
|
||||||
async getImageMetaByDate(date: string): Promise<ImageMeta> {
|
async getImageMetaByDate(date: string, mkt?: string): Promise<ImageMeta> {
|
||||||
return apiClient.get<ImageMeta>(`/image/date/${date}/meta`)
|
const endpoint = mkt ? `/image/date/${date}/meta?mkt=${mkt}` : `/image/date/${date}/meta`
|
||||||
|
return apiClient.get<ImageMeta>(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取随机图片元数据
|
* 获取随机图片元数据
|
||||||
*/
|
*/
|
||||||
async getRandomImageMeta(): Promise<ImageMeta> {
|
async getRandomImageMeta(mkt?: string): Promise<ImageMeta> {
|
||||||
return apiClient.get<ImageMeta>('/image/random/meta')
|
const endpoint = mkt ? `/image/random/meta?mkt=${mkt}` : '/image/random/meta'
|
||||||
|
return apiClient.get<ImageMeta>(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建图片 URL
|
* 构建图片 URL
|
||||||
*/
|
*/
|
||||||
getTodayImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg'): string {
|
getTodayImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg', mkt?: string): string {
|
||||||
const params = new URLSearchParams({ variant, format })
|
const params = new URLSearchParams({ variant, format })
|
||||||
|
if (mkt) params.set('mkt', mkt)
|
||||||
return `${apiConfig.baseURL}/image/today?${params.toString()}`
|
return `${apiConfig.baseURL}/image/today?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建指定日期图片 URL
|
* 构建指定日期图片 URL
|
||||||
*/
|
*/
|
||||||
getImageUrlByDate(date: string, variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg'): string {
|
getImageUrlByDate(date: string, variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg', mkt?: string): string {
|
||||||
const params = new URLSearchParams({ variant, format })
|
const params = new URLSearchParams({ variant, format })
|
||||||
|
if (mkt) params.set('mkt', mkt)
|
||||||
return `${apiConfig.baseURL}/image/date/${date}?${params.toString()}`
|
return `${apiConfig.baseURL}/image/date/${date}?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建随机图片 URL
|
* 构建随机图片 URL
|
||||||
*/
|
*/
|
||||||
getRandomImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg'): string {
|
getRandomImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg', mkt?: string): string {
|
||||||
const params = new URLSearchParams({ variant, format })
|
const params = new URLSearchParams({ variant, format })
|
||||||
|
if (mkt) params.set('mkt', mkt)
|
||||||
return `${apiConfig.baseURL}/image/random?${params.toString()}`
|
return `${apiConfig.baseURL}/image/random?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +212,7 @@ export const {
|
|||||||
manualFetch,
|
manualFetch,
|
||||||
manualCleanup,
|
manualCleanup,
|
||||||
getImages,
|
getImages,
|
||||||
|
getRegions,
|
||||||
getTodayImageMeta,
|
getTodayImageMeta,
|
||||||
getImageMetaByDate,
|
getImageMetaByDate,
|
||||||
getRandomImageMeta,
|
getRandomImageMeta,
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export interface Config {
|
|||||||
Token: TokenConfig
|
Token: TokenConfig
|
||||||
Feature: FeatureConfig
|
Feature: FeatureConfig
|
||||||
Web: WebConfig
|
Web: WebConfig
|
||||||
|
Fetcher: FetcherConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetcherConfig {
|
||||||
|
Regions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminConfig {
|
export interface AdminConfig {
|
||||||
@@ -69,6 +74,7 @@ export interface AdminConfig {
|
|||||||
|
|
||||||
export interface APIConfig {
|
export interface APIConfig {
|
||||||
Mode: string // 'local' | 'redirect'
|
Mode: string // 'local' | 'redirect'
|
||||||
|
EnableMktFallback: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CronConfig {
|
export interface CronConfig {
|
||||||
@@ -147,6 +153,7 @@ export interface WebConfig {
|
|||||||
|
|
||||||
export interface ImageMeta {
|
export interface ImageMeta {
|
||||||
date?: string
|
date?: string
|
||||||
|
mkt?: string
|
||||||
title?: string
|
title?: string
|
||||||
copyright?: string
|
copyright?: string
|
||||||
copyrightlink?: string // 图片的详细版权链接(指向 Bing 搜索页面)
|
copyrightlink?: string // 图片的详细版权链接(指向 Bing 搜索页面)
|
||||||
@@ -173,6 +180,12 @@ export interface ImageListParams extends PaginationParams {
|
|||||||
page?: number // 页码(从1开始)
|
page?: number // 页码(从1开始)
|
||||||
page_size?: number // 每页数量
|
page_size?: number // 每页数量
|
||||||
month?: string // 按月份过滤(格式:YYYY-MM)
|
month?: string // 按月份过滤(格式:YYYY-MM)
|
||||||
|
mkt?: string // 地区编码
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Region {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ManualFetchRequest {
|
export interface ManualFetchRequest {
|
||||||
|
|||||||
75
webapp/src/lib/mkt-utils.ts
Normal file
75
webapp/src/lib/mkt-utils.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
const MKT_STORAGE_KEY = 'bing_paper_selected_mkt'
|
||||||
|
const DEFAULT_MKT = 'zh-CN'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认地区列表 (兜底用)
|
||||||
|
*/
|
||||||
|
export const DEFAULT_REGIONS = [
|
||||||
|
{ value: 'zh-CN', label: '中国 (zh-CN)' },
|
||||||
|
{ value: 'en-US', label: '美国 (en-US)' },
|
||||||
|
{ value: 'ja-JP', label: '日本 (ja-JP)' },
|
||||||
|
{ value: 'en-AU', label: '澳大利亚 (en-AU)' },
|
||||||
|
{ value: 'en-GB', label: '英国 (en-GB)' },
|
||||||
|
{ value: 'de-DE', label: '德国 (de-DE)' },
|
||||||
|
{ value: 'en-NZ', label: '新西兰 (en-NZ)' },
|
||||||
|
{ value: 'en-CA', label: '加拿大 (en-CA)' },
|
||||||
|
{ value: 'fr-FR', label: '法国 (fr-FR)' },
|
||||||
|
{ value: 'it-IT', label: '意大利 (it-IT)' },
|
||||||
|
{ value: 'es-ES', label: '西班牙 (es-ES)' },
|
||||||
|
{ value: 'pt-BR', label: '巴西 (pt-BR)' },
|
||||||
|
{ value: 'ko-KR', label: '韩国 (ko-KR)' },
|
||||||
|
{ value: 'en-IN', label: '印度 (en-IN)' },
|
||||||
|
{ value: 'ru-RU', label: '俄罗斯 (ru-RU)' },
|
||||||
|
{ value: 'zh-HK', label: '中国香港 (zh-HK)' },
|
||||||
|
{ value: 'zh-TW', label: '中国台湾 (zh-TW)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的地区列表 (优先使用后端提供的)
|
||||||
|
*/
|
||||||
|
export let SUPPORTED_REGIONS = [...DEFAULT_REGIONS]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新支持的地区列表
|
||||||
|
*/
|
||||||
|
export function setSupportedRegions(regions: typeof DEFAULT_REGIONS): void {
|
||||||
|
SUPPORTED_REGIONS = regions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取浏览器首选地区
|
||||||
|
*/
|
||||||
|
export function getBrowserMkt(): string {
|
||||||
|
const lang = navigator.language || (navigator as any).userLanguage
|
||||||
|
if (!lang) return DEFAULT_MKT
|
||||||
|
|
||||||
|
// 尝试精确匹配
|
||||||
|
const exactMatch = SUPPORTED_REGIONS.find(r => r.value.toLowerCase() === lang.toLowerCase())
|
||||||
|
if (exactMatch) return exactMatch.value
|
||||||
|
|
||||||
|
// 尝试模糊匹配 (前两个字符,如 en-GB 匹配 en-US)
|
||||||
|
const prefix = lang.split('-')[0].toLowerCase()
|
||||||
|
const prefixMatch = SUPPORTED_REGIONS.find(r => r.value.split('-')[0].toLowerCase() === prefix)
|
||||||
|
if (prefixMatch) return prefixMatch.value
|
||||||
|
|
||||||
|
return DEFAULT_MKT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选择的地区 (优先从 localStorage 获取,其次从浏览器获取)
|
||||||
|
*/
|
||||||
|
export function getDefaultMkt(): string {
|
||||||
|
const saved = localStorage.getItem(MKT_STORAGE_KEY)
|
||||||
|
if (saved && SUPPORTED_REGIONS.some(r => r.value === saved)) {
|
||||||
|
return saved
|
||||||
|
}
|
||||||
|
return getBrowserMkt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存选择的地区
|
||||||
|
*/
|
||||||
|
export function setSavedMkt(mkt: string): void {
|
||||||
|
localStorage.setItem(MKT_STORAGE_KEY, mkt)
|
||||||
|
}
|
||||||
@@ -102,6 +102,16 @@
|
|||||||
local: 直接返回图片流; redirect: 重定向到存储位置
|
local: 直接返回图片流; redirect: 重定向到存储位置
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Label for="api-fallback">启用地区不存在时兜底</Label>
|
||||||
|
<Switch
|
||||||
|
id="api-fallback"
|
||||||
|
v-model="config.API.EnableMktFallback"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
如果请求的地区无数据,自动回退到默认地区
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -372,6 +382,31 @@
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- 抓取配置 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>抓取配置</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>抓取地区</Label>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
|
||||||
|
<div v-for="region in allRegions" :key="region.value" class="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
:id="'region-'+region.value"
|
||||||
|
:checked="config.Fetcher.Regions.includes(region.value)"
|
||||||
|
@update:checked="(checked: any) => toggleRegion(region.value, !!checked)"
|
||||||
|
/>
|
||||||
|
<Label :for="'region-'+region.value" class="text-sm font-normal cursor-pointer">{{ region.label }}</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
勾选需要定期抓取壁纸的地区。如果不勾选任何地区,默认将只抓取 zh-CN。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- 功能特性配置 -->
|
<!-- 功能特性配置 -->
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -414,6 +449,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { apiService } from '@/lib/api-service'
|
import { apiService } from '@/lib/api-service'
|
||||||
import type { Config } from '@/lib/api-types'
|
import type { Config } from '@/lib/api-types'
|
||||||
@@ -424,9 +460,12 @@ const loadError = ref('')
|
|||||||
const saveLoading = ref(false)
|
const saveLoading = ref(false)
|
||||||
const dsnError = ref('')
|
const dsnError = ref('')
|
||||||
|
|
||||||
|
// 所有可选地区列表
|
||||||
|
const allRegions = ref<any[]>([])
|
||||||
|
|
||||||
const config = ref<Config>({
|
const config = ref<Config>({
|
||||||
Admin: { PasswordBcrypt: '' },
|
Admin: { PasswordBcrypt: '' },
|
||||||
API: { Mode: 'local' },
|
API: { Mode: 'local', EnableMktFallback: true },
|
||||||
Cron: { Enabled: true, DailySpec: '0 9 * * *' },
|
Cron: { Enabled: true, DailySpec: '0 9 * * *' },
|
||||||
DB: { Type: 'sqlite', DSN: '' },
|
DB: { Type: 'sqlite', DSN: '' },
|
||||||
Feature: { WriteDailyFiles: true },
|
Feature: { WriteDailyFiles: true },
|
||||||
@@ -464,12 +503,37 @@ const config = ref<Config>({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Token: { DefaultTTL: '168h' },
|
Token: { DefaultTTL: '168h' },
|
||||||
Web: { Path: './webapp/dist' }
|
Web: { Path: './webapp/dist' },
|
||||||
|
Fetcher: { Regions: [] }
|
||||||
})
|
})
|
||||||
|
|
||||||
const configJson = ref('')
|
const configJson = ref('')
|
||||||
const jsonError = ref('')
|
const jsonError = ref('')
|
||||||
|
|
||||||
|
// 获取所有地区
|
||||||
|
const fetchRegions = async () => {
|
||||||
|
try {
|
||||||
|
const data = await apiService.getRegions()
|
||||||
|
allRegions.value = data
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取地区列表失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRegion = (regionValue: string, checked: boolean) => {
|
||||||
|
if (!config.value.Fetcher.Regions) {
|
||||||
|
config.value.Fetcher.Regions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
if (!config.value.Fetcher.Regions.includes(regionValue)) {
|
||||||
|
config.value.Fetcher.Regions.push(regionValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.value.Fetcher.Regions = config.value.Fetcher.Regions.filter(r => r !== regionValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DSN 示例
|
// DSN 示例
|
||||||
const dsnExamples = computed(() => {
|
const dsnExamples = computed(() => {
|
||||||
switch (config.value.DB.Type) {
|
switch (config.value.DB.Type) {
|
||||||
@@ -602,6 +666,7 @@ const handleSaveConfig = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
fetchRegions()
|
||||||
fetchConfig()
|
fetchConfig()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -103,6 +103,10 @@
|
|||||||
<code class="text-yellow-400 min-w-24">format</code>
|
<code class="text-yellow-400 min-w-24">format</code>
|
||||||
<span class="text-white/50">格式: jpg (默认: jpg)</span>
|
<span class="text-white/50">格式: jpg (默认: jpg)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-4 text-sm">
|
||||||
|
<code class="text-yellow-400 min-w-24">mkt</code>
|
||||||
|
<span class="text-white/50">地区编码 (如 zh-CN, en-US, ja-JP),默认由服务器自动探测</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,6 +186,10 @@
|
|||||||
<code class="text-yellow-400 min-w-24">format</code>
|
<code class="text-yellow-400 min-w-24">format</code>
|
||||||
<span class="text-white/50">格式 (默认: jpg)</span>
|
<span class="text-white/50">格式 (默认: jpg)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-4 text-sm">
|
||||||
|
<code class="text-yellow-400 min-w-24">mkt</code>
|
||||||
|
<span class="text-white/50">地区编码 (如 zh-CN, en-US, ja-JP)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -254,6 +262,10 @@
|
|||||||
<code class="text-yellow-400 min-w-24">format</code>
|
<code class="text-yellow-400 min-w-24">format</code>
|
||||||
<span class="text-white/50">格式 (默认: jpg)</span>
|
<span class="text-white/50">格式 (默认: jpg)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-4 text-sm">
|
||||||
|
<code class="text-yellow-400 min-w-24">mkt</code>
|
||||||
|
<span class="text-white/50">地区编码 (如 zh-CN, en-US, ja-JP)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -334,6 +346,10 @@
|
|||||||
<code class="text-yellow-400 min-w-32">date</code>
|
<code class="text-yellow-400 min-w-32">date</code>
|
||||||
<span class="text-white/60">图片日期(格式:YYYY-MM-DD)</span>
|
<span class="text-white/60">图片日期(格式:YYYY-MM-DD)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<code class="text-yellow-400 min-w-32">mkt</code>
|
||||||
|
<span class="text-white/60">地区编码(如 zh-CN, en-US)</span>
|
||||||
|
</div>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<code class="text-yellow-400 min-w-32">title</code>
|
<code class="text-yellow-400 min-w-32">title</code>
|
||||||
<span class="text-white/60">图片标题</span>
|
<span class="text-white/60">图片标题</span>
|
||||||
@@ -458,24 +474,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { API_BASE_URL } from '@/lib/api-config'
|
import { API_BASE_URL } from '@/lib/api-config'
|
||||||
|
import { getDefaultMkt } from '@/lib/mkt-utils'
|
||||||
|
|
||||||
const baseURL = ref(API_BASE_URL)
|
const baseURL = ref(API_BASE_URL)
|
||||||
const previewImage = ref<string | null>(null)
|
const previewImage = ref<string | null>(null)
|
||||||
|
const defaultMkt = getDefaultMkt()
|
||||||
|
|
||||||
// 获取今日图片示例
|
// 获取今日图片示例
|
||||||
const getTodayImageExample = () => {
|
const getTodayImageExample = () => {
|
||||||
return `${baseURL.value}/image/today?variant=UHD&format=jpg`
|
return `${baseURL.value}/image/today?variant=UHD&format=jpg&mkt=${defaultMkt}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定日期图片示例
|
// 获取指定日期图片示例
|
||||||
const getDateImageExample = () => {
|
const getDateImageExample = () => {
|
||||||
const today = new Date().toISOString().split('T')[0]
|
const today = new Date().toISOString().split('T')[0]
|
||||||
return `${baseURL.value}/image/date/${today}?variant=1920x1080&format=jpg`
|
return `${baseURL.value}/image/date/${today}?variant=1920x1080&format=jpg&mkt=${defaultMkt}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取随机图片示例
|
// 获取随机图片示例
|
||||||
const getRandomImageExample = () => {
|
const getRandomImageExample = () => {
|
||||||
return `${baseURL.value}/image/random?variant=UHD&format=jpg`
|
return `${baseURL.value}/image/random?variant=UHD&format=jpg&mkt=${defaultMkt}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
|
|||||||
@@ -79,6 +79,23 @@
|
|||||||
|
|
||||||
<!-- 筛选器 -->
|
<!-- 筛选器 -->
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<!-- 地区选择 -->
|
||||||
|
<Select v-model="selectedMkt" @update:model-value="onMktChange">
|
||||||
|
<SelectTrigger class="w-[180px] bg-white/10 backdrop-blur-md text-white border-white/20 hover:bg-white/15 hover:border-white/30 focus:ring-white/50 shadow-lg">
|
||||||
|
<SelectValue placeholder="选择地区" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent class="bg-gray-900/95 backdrop-blur-xl border-white/20 text-white">
|
||||||
|
<SelectItem
|
||||||
|
v-for="region in regions"
|
||||||
|
:key="region.value"
|
||||||
|
:value="region.value"
|
||||||
|
class="focus:bg-white/10 focus:text-white cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ region.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<!-- 年份选择 -->
|
<!-- 年份选择 -->
|
||||||
<Select v-model="selectedYear" @update:model-value="onYearChange">
|
<Select v-model="selectedYear" @update:model-value="onYearChange">
|
||||||
<SelectTrigger class="w-[180px] bg-white/10 backdrop-blur-md text-white border-white/20 hover:bg-white/15 hover:border-white/30 focus:ring-white/50 shadow-lg">
|
<SelectTrigger class="w-[180px] bg-white/10 backdrop-blur-md text-white border-white/20 hover:bg-white/15 hover:border-white/30 focus:ring-white/50 shadow-lg">
|
||||||
@@ -153,7 +170,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
:src="getImageUrl(image.date!)"
|
:src="getImageUrl(image)"
|
||||||
:alt="image.title || 'Bing Image'"
|
:alt="image.title || 'Bing Image'"
|
||||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -248,6 +265,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|||||||
import { useImageList } from '@/composables/useImages'
|
import { useImageList } from '@/composables/useImages'
|
||||||
import { bingPaperApi } from '@/lib/api-service'
|
import { bingPaperApi } from '@/lib/api-service'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { getDefaultMkt, setSavedMkt, SUPPORTED_REGIONS, setSupportedRegions } from '@/lib/mkt-utils'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -258,18 +276,21 @@ import {
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 地区列表
|
||||||
|
const regions = ref(SUPPORTED_REGIONS)
|
||||||
|
|
||||||
// 顶部最新图片(独立加载,不受筛选影响)
|
// 顶部最新图片(独立加载,不受筛选影响)
|
||||||
const latestImage = ref<any>(null)
|
const latestImage = ref<any>(null)
|
||||||
const todayLoading = ref(false)
|
const todayLoading = ref(false)
|
||||||
|
|
||||||
// 历史图片列表(使用服务端分页和筛选,每页15张)
|
// 历史图片列表(使用服务端分页和筛选,每页15张)
|
||||||
const { images, loading, hasMore, loadMore, filterByMonth } = useImageList(15)
|
const { images, loading, hasMore, loadMore, filterByMonth, filterByMkt } = useImageList(15)
|
||||||
|
|
||||||
// 加载顶部最新图片
|
// 加载顶部最新图片
|
||||||
const loadLatestImage = async () => {
|
const loadLatestImage = async () => {
|
||||||
todayLoading.value = true
|
todayLoading.value = true
|
||||||
try {
|
try {
|
||||||
const params = { page: 1, page_size: 1 }
|
const params: any = { page: 1, page_size: 1, mkt: selectedMkt.value }
|
||||||
const result = await bingPaperApi.getImages(params)
|
const result = await bingPaperApi.getImages(params)
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
latestImage.value = result[0]
|
latestImage.value = result[0]
|
||||||
@@ -281,8 +302,17 @@ const loadLatestImage = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化加载顶部图片
|
// 初始化加载
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const backendRegions = await bingPaperApi.getRegions()
|
||||||
|
if (backendRegions && backendRegions.length > 0) {
|
||||||
|
regions.value = backendRegions
|
||||||
|
setSupportedRegions(backendRegions)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch regions:', error)
|
||||||
|
}
|
||||||
loadLatestImage()
|
loadLatestImage()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -315,9 +345,22 @@ const nextUpdateTime = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 筛选相关状态
|
// 筛选相关状态
|
||||||
|
const selectedMkt = ref(getDefaultMkt())
|
||||||
const selectedYear = ref('')
|
const selectedYear = ref('')
|
||||||
const selectedMonth = ref('')
|
const selectedMonth = ref('')
|
||||||
|
|
||||||
|
const onMktChange = () => {
|
||||||
|
setSavedMkt(selectedMkt.value)
|
||||||
|
filterByMkt(selectedMkt.value)
|
||||||
|
loadLatestImage()
|
||||||
|
|
||||||
|
// 重置懒加载状态
|
||||||
|
imageVisibility.value = []
|
||||||
|
setTimeout(() => {
|
||||||
|
setupObserver()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
// 懒加载相关
|
// 懒加载相关
|
||||||
const imageRefs = ref<(HTMLElement | null)[]>([])
|
const imageRefs = ref<(HTMLElement | null)[]>([])
|
||||||
const imageVisibility = ref<boolean[]>([])
|
const imageVisibility = ref<boolean[]>([])
|
||||||
@@ -374,11 +417,14 @@ const onFilterChange = () => {
|
|||||||
|
|
||||||
// 重置筛选
|
// 重置筛选
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
|
selectedMkt.value = getDefaultMkt()
|
||||||
selectedYear.value = ''
|
selectedYear.value = ''
|
||||||
selectedMonth.value = ''
|
selectedMonth.value = ''
|
||||||
|
|
||||||
// 重置为加载默认数据
|
// 重置为加载默认数据
|
||||||
|
filterByMkt(selectedMkt.value)
|
||||||
filterByMonth(undefined)
|
filterByMonth(undefined)
|
||||||
|
loadLatestImage()
|
||||||
|
|
||||||
// 重置懒加载状态
|
// 重置懒加载状态
|
||||||
imageVisibility.value = []
|
imageVisibility.value = []
|
||||||
@@ -521,12 +567,12 @@ const formatDate = (dateStr?: string) => {
|
|||||||
// 获取最新图片 URL(顶部大图使用UHD高清)
|
// 获取最新图片 URL(顶部大图使用UHD高清)
|
||||||
const getLatestImageUrl = () => {
|
const getLatestImageUrl = () => {
|
||||||
if (!latestImage.value?.date) return ''
|
if (!latestImage.value?.date) return ''
|
||||||
return bingPaperApi.getImageUrlByDate(latestImage.value.date, 'UHD', 'jpg')
|
return bingPaperApi.getImageUrlByDate(latestImage.value.date, 'UHD', 'jpg', latestImage.value.mkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取图片 URL(缩略图 - 使用较小分辨率节省流量)
|
// 获取图片 URL(缩略图 - 使用较小分辨率节省流量)
|
||||||
const getImageUrl = (date: string) => {
|
const getImageUrl = (image: any) => {
|
||||||
return bingPaperApi.getImageUrlByDate(date, '640x480', 'jpg')
|
return bingPaperApi.getImageUrlByDate(image.date!, '640x480', 'jpg', image.mkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看图片详情
|
// 查看图片详情
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<!-- 拖动手柄 -->
|
<!-- 拖动手柄 -->
|
||||||
<div
|
<div
|
||||||
@mousedown="startDrag"
|
@mousedown="startDrag"
|
||||||
@touchstart="startDrag"
|
@touchstart.passive="startDrag"
|
||||||
class="absolute top-2 left-1/2 -translate-x-1/2 w-12 h-1 bg-white/30 rounded-full cursor-move hover:bg-white/50 transition-colors touch-none"
|
class="absolute top-2 left-1/2 -translate-x-1/2 w-12 h-1 bg-white/30 rounded-full cursor-move hover:bg-white/50 transition-colors touch-none"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
@@ -178,10 +178,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, onMounted, onUnmounted, computed } 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'
|
||||||
|
import { getDefaultMkt } from '@/lib/mkt-utils'
|
||||||
import Calendar from '@/components/ui/calendar/Calendar.vue'
|
import Calendar from '@/components/ui/calendar/Calendar.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -202,12 +203,17 @@ const getInitialCalendarState = (): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentDate = ref(route.params.date as string)
|
const currentDate = ref(route.params.date as string)
|
||||||
|
const currentMkt = ref(route.query.mkt as string || getDefaultMkt())
|
||||||
const showInfo = ref(true)
|
const showInfo = ref(true)
|
||||||
const showCalendar = ref(getInitialCalendarState())
|
const showCalendar = ref(getInitialCalendarState())
|
||||||
const navigating = ref(false)
|
const navigating = ref(false)
|
||||||
const imageOpacity = ref(1)
|
const imageOpacity = ref(1)
|
||||||
const imageTransitioning = ref(false)
|
const imageTransitioning = ref(false)
|
||||||
|
|
||||||
|
// 响应式窗口大小
|
||||||
|
const windowSize = ref({ width: window.innerWidth, height: window.innerHeight })
|
||||||
|
const isMobile = computed(() => windowSize.value.width < 768)
|
||||||
|
|
||||||
// 前后日期可用性
|
// 前后日期可用性
|
||||||
const hasPreviousDay = ref(true)
|
const hasPreviousDay = ref(true)
|
||||||
const hasNextDay = ref(true)
|
const hasNextDay = ref(true)
|
||||||
@@ -222,11 +228,10 @@ let animationFrameId: number | null = null
|
|||||||
|
|
||||||
// 计算图片实际显示区域(考虑图片宽高比和object-contain)
|
// 计算图片实际显示区域(考虑图片宽高比和object-contain)
|
||||||
const getImageDisplayBounds = () => {
|
const getImageDisplayBounds = () => {
|
||||||
const windowWidth = window.innerWidth
|
const windowWidth = windowSize.value.width
|
||||||
const windowHeight = window.innerHeight
|
const windowHeight = windowSize.value.height
|
||||||
|
|
||||||
// 必应图片通常是16:9或类似宽高比
|
// 必应图片通常是16:9
|
||||||
// 使用UHD分辨率: 1920x1080 (16:9)
|
|
||||||
const imageAspectRatio = 16 / 9
|
const imageAspectRatio = 16 / 9
|
||||||
const windowAspectRatio = windowWidth / windowHeight
|
const windowAspectRatio = windowWidth / windowHeight
|
||||||
|
|
||||||
@@ -259,22 +264,35 @@ const getImageDisplayBounds = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化浮窗位置(居中偏下,限制在图片显示区域内)
|
// 初始化浮窗位置(限制在图片显示区域内,移动端默认展示在底部)
|
||||||
const initPanelPosition = () => {
|
const initPanelPosition = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const bounds = getImageDisplayBounds()
|
const bounds = getImageDisplayBounds()
|
||||||
const panelWidth = Math.min(bounds.width * 0.9, 448) // max-w-md = 448px
|
|
||||||
|
|
||||||
infoPanelPos.value = {
|
if (isMobile.value) {
|
||||||
x: bounds.left + (bounds.width - panelWidth) / 2,
|
// 移动端:默认居中靠下,不严格限制在图片内(因为要求可以不限制)
|
||||||
y: Math.max(bounds.top, bounds.bottom - 280) // 距底部280px,避免与控制栏重叠
|
// 但为了好看,我们还是给它一个默认位置
|
||||||
|
const panelWidth = windowSize.value.width * 0.9
|
||||||
|
infoPanelPos.value = {
|
||||||
|
x: (windowSize.value.width - panelWidth) / 2,
|
||||||
|
y: windowSize.value.height - 240 // 靠下
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 桌面端:限制在图片区域内
|
||||||
|
const panelWidth = Math.min(bounds.width * 0.9, 448) // max-w-md = 448px
|
||||||
|
infoPanelPos.value = {
|
||||||
|
x: bounds.left + (bounds.width - panelWidth) / 2,
|
||||||
|
y: Math.max(bounds.top, bounds.bottom - 280) // 距底部280px,避免与控制栏重叠
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始拖动
|
// 开始拖动
|
||||||
const startDrag = (e: MouseEvent | TouchEvent) => {
|
const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||||
e.preventDefault()
|
if (e instanceof MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
|
|
||||||
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
|
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
|
||||||
@@ -287,7 +305,7 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
|
|
||||||
document.addEventListener('mousemove', onDrag, { passive: false })
|
document.addEventListener('mousemove', onDrag, { passive: false })
|
||||||
document.addEventListener('mouseup', stopDrag)
|
document.addEventListener('mouseup', stopDrag)
|
||||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
document.addEventListener('touchmove', onDrag, { passive: true })
|
||||||
document.addEventListener('touchend', stopDrag)
|
document.addEventListener('touchend', stopDrag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +313,9 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
const onDrag = (e: MouseEvent | TouchEvent) => {
|
const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
|
|
||||||
e.preventDefault()
|
if (e instanceof MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
// 取消之前的动画帧
|
// 取消之前的动画帧
|
||||||
if (animationFrameId !== null) {
|
if (animationFrameId !== null) {
|
||||||
@@ -310,15 +330,26 @@ const onDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
const newX = clientX - dragStart.value.x
|
const newX = clientX - dragStart.value.x
|
||||||
const newY = clientY - dragStart.value.y
|
const newY = clientY - dragStart.value.y
|
||||||
|
|
||||||
// 限制在图片实际显示区域内,考虑底部控制栏高度(约80px)
|
// 限制在有效区域内
|
||||||
if (infoPanel.value) {
|
if (infoPanel.value) {
|
||||||
const rect = infoPanel.value.getBoundingClientRect()
|
const rect = infoPanel.value.getBoundingClientRect()
|
||||||
const bounds = getImageDisplayBounds()
|
|
||||||
|
|
||||||
const minX = bounds.left
|
let minX, maxX, minY, maxY
|
||||||
const maxX = bounds.right - rect.width
|
|
||||||
const minY = bounds.top
|
if (isMobile.value) {
|
||||||
const maxY = bounds.bottom - rect.height - 80 // 预留底部控制栏空间
|
// 移动端:不限制区域,限制在视口内即可
|
||||||
|
minX = 0
|
||||||
|
maxX = windowSize.value.width - rect.width
|
||||||
|
minY = 0
|
||||||
|
maxY = windowSize.value.height - rect.height
|
||||||
|
} else {
|
||||||
|
// 桌面端:限制在图片实际显示区域内,考虑底部控制栏高度(约80px)
|
||||||
|
const bounds = getImageDisplayBounds()
|
||||||
|
minX = bounds.left
|
||||||
|
maxX = bounds.right - rect.width
|
||||||
|
minY = bounds.top
|
||||||
|
maxY = bounds.bottom - rect.height - 80 // 预留底部控制栏空间
|
||||||
|
}
|
||||||
|
|
||||||
infoPanelPos.value = {
|
infoPanelPos.value = {
|
||||||
x: Math.max(minX, Math.min(newX, maxX)),
|
x: Math.max(minX, Math.min(newX, maxX)),
|
||||||
@@ -347,12 +378,12 @@ const stopDrag = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用 composable 获取图片数据(传递 ref,自动响应日期变化)
|
// 使用 composable 获取图片数据(传递 ref,自动响应日期变化)
|
||||||
const { image, loading, error } = useImageByDate(currentDate)
|
const { image, loading, error } = useImageByDate(currentDate, currentMkt)
|
||||||
|
|
||||||
// 检测指定日期是否有数据
|
// 检测指定日期是否有数据
|
||||||
const checkDateAvailability = async (dateStr: string): Promise<boolean> => {
|
const checkDateAvailability = async (dateStr: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await bingPaperApi.getImageMetaByDate(dateStr)
|
await bingPaperApi.getImageMetaByDate(dateStr, currentMkt.value)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false
|
return false
|
||||||
@@ -384,9 +415,6 @@ const checkAdjacentDates = async () => {
|
|||||||
checkingDates.value = false
|
checkingDates.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化位置
|
|
||||||
initPanelPosition()
|
|
||||||
|
|
||||||
// 监听showCalendar变化并自动保存到localStorage
|
// 监听showCalendar变化并自动保存到localStorage
|
||||||
watch(showCalendar, (newValue) => {
|
watch(showCalendar, (newValue) => {
|
||||||
try {
|
try {
|
||||||
@@ -401,6 +429,20 @@ watch(currentDate, () => {
|
|||||||
checkAdjacentDates()
|
checkAdjacentDates()
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 监听路由变化,支持前进后退
|
||||||
|
watch(() => route.params.date, (newDate) => {
|
||||||
|
if (newDate && newDate !== currentDate.value) {
|
||||||
|
currentDate.value = newDate as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => route.query.mkt, (newMkt) => {
|
||||||
|
const mkt = (newMkt as string) || getDefaultMkt()
|
||||||
|
if (mkt !== currentMkt.value) {
|
||||||
|
currentMkt.value = mkt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
const formatDate = (dateStr?: string) => {
|
const formatDate = (dateStr?: string) => {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
@@ -415,7 +457,7 @@ const formatDate = (dateStr?: string) => {
|
|||||||
|
|
||||||
// 获取完整图片 URL
|
// 获取完整图片 URL
|
||||||
const getFullImageUrl = () => {
|
const getFullImageUrl = () => {
|
||||||
return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg')
|
return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg', currentMkt.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预加载图片
|
// 预加载图片
|
||||||
@@ -432,10 +474,10 @@ const preloadImage = (url: string): Promise<void> => {
|
|||||||
const preloadImageAndData = async (date: string): Promise<void> => {
|
const preloadImageAndData = async (date: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// 并行预加载图片和数据
|
// 并行预加载图片和数据
|
||||||
const imageUrl = bingPaperApi.getImageUrlByDate(date, 'UHD', 'jpg')
|
const imageUrl = bingPaperApi.getImageUrlByDate(date, 'UHD', 'jpg', currentMkt.value)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
preloadImage(imageUrl),
|
preloadImage(imageUrl),
|
||||||
bingPaperApi.getImageMetaByDate(date)
|
bingPaperApi.getImageMetaByDate(date, currentMkt.value)
|
||||||
])
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to preload image or data:', error)
|
console.warn('Failed to preload image or data:', error)
|
||||||
@@ -461,7 +503,7 @@ const switchToDate = async (newDate: string) => {
|
|||||||
|
|
||||||
// 3. 更新日期(此时图片和数据已经预加载完成)
|
// 3. 更新日期(此时图片和数据已经预加载完成)
|
||||||
currentDate.value = newDate
|
currentDate.value = newDate
|
||||||
router.replace(`/image/${newDate}`)
|
router.replace(`/image/${newDate}?mkt=${currentMkt.value}`)
|
||||||
|
|
||||||
// 4. 等待一个微任务,确保 DOM 更新
|
// 4. 等待一个微任务,确保 DOM 更新
|
||||||
await new Promise(resolve => setTimeout(resolve, 50))
|
await new Promise(resolve => setTimeout(resolve, 50))
|
||||||
@@ -534,18 +576,29 @@ const handleKeydown = (e: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加键盘事件监听
|
// 窗口缩放处理
|
||||||
if (typeof window !== 'undefined') {
|
const handleResize = () => {
|
||||||
window.addEventListener('keydown', handleKeydown)
|
windowSize.value = {
|
||||||
window.addEventListener('resize', initPanelPosition)
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
}
|
||||||
|
initPanelPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理
|
// 生命周期钩子
|
||||||
import { onUnmounted } from 'vue'
|
onMounted(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('keydown', handleKeydown)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
// 初始化浮窗位置
|
||||||
|
initPanelPosition()
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.removeEventListener('keydown', handleKeydown)
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
window.removeEventListener('resize', initPanelPosition)
|
window.removeEventListener('resize', handleResize)
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
document.removeEventListener('touchmove', onDrag)
|
document.removeEventListener('touchmove', onDrag)
|
||||||
|
|||||||
Reference in New Issue
Block a user