mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-02-15 07:19:33 +08:00
增加前后端图片传输时的缓存支持,优化前端页面
This commit is contained in:
@@ -173,20 +173,30 @@ func handleImageResponse(c *gin.Context, img *model.Image) {
|
||||
mode := config.GetConfig().API.Mode
|
||||
if mode == "redirect" {
|
||||
if selected.PublicURL != "" {
|
||||
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
||||
c.Redirect(http.StatusFound, selected.PublicURL)
|
||||
} else if img.URLBase != "" {
|
||||
// 兜底重定向到原始 Bing
|
||||
bingURL := fmt.Sprintf("https://www.bing.com%s_%s.jpg", img.URLBase, selected.Variant)
|
||||
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
||||
c.Redirect(http.StatusFound, bingURL)
|
||||
} else {
|
||||
serveLocal(c, selected.StorageKey)
|
||||
serveLocal(c, selected.StorageKey, img.Date)
|
||||
}
|
||||
} else {
|
||||
serveLocal(c, selected.StorageKey)
|
||||
serveLocal(c, selected.StorageKey, img.Date)
|
||||
}
|
||||
}
|
||||
|
||||
func serveLocal(c *gin.Context, key string) {
|
||||
func serveLocal(c *gin.Context, key string, etag string) {
|
||||
if etag != "" {
|
||||
c.Header("ETag", fmt.Sprintf("\"%s\"", etag))
|
||||
if c.GetHeader("If-None-Match") == fmt.Sprintf("\"%s\"", etag) {
|
||||
c.AbortWithStatus(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reader, contentType, err := storage.GlobalStorage.Get(context.Background(), key)
|
||||
if err != nil {
|
||||
util.Logger.Error("Failed to get image from storage", zap.String("key", key), zap.Error(err))
|
||||
@@ -198,6 +208,7 @@ func serveLocal(c *gin.Context, key string) {
|
||||
if contentType != "" {
|
||||
c.Header("Content-Type", contentType)
|
||||
}
|
||||
c.Header("Cache-Control", "public, max-age=604800") // 7天
|
||||
io.Copy(c.Writer, reader)
|
||||
}
|
||||
|
||||
|
||||
288
webapp/BUILD.md
288
webapp/BUILD.md
@@ -1,288 +0,0 @@
|
||||
# BingPaper WebApp 构建说明
|
||||
|
||||
## 构建配置优化
|
||||
|
||||
本项目已优化构建配置,支持自定义后端路径和自动输出到上级目录的 `web` 文件夹。
|
||||
|
||||
## 环境配置
|
||||
|
||||
### 环境变量
|
||||
|
||||
项目支持通过环境变量配置后端 API 地址:
|
||||
|
||||
- **开发环境** (`.env.development`):使用完整的后端服务器地址(直连,不使用代理)
|
||||
- **生产环境** (`.env.production`):使用相对路径 `/api/v1` 访问后端
|
||||
- **默认配置** (`.env`):通用配置
|
||||
|
||||
### 开发环境 vs 生产环境
|
||||
|
||||
#### 开发环境(npm run dev)
|
||||
- 直接使用完整的后端 API 地址:`http://localhost:8080/api/v1`
|
||||
- 不使用代理,前端直接请求后端服务
|
||||
- 需要确保后端服务器支持 CORS 跨域请求
|
||||
- 优点:配置简单,调试方便,可以清楚看到实际请求
|
||||
|
||||
#### 生产环境(npm run build)
|
||||
- 使用相对路径:`/api/v1`
|
||||
- 前后端部署在同一域名下,无跨域问题
|
||||
- Go 服务器同时提供静态文件和 API 服务
|
||||
|
||||
### 自定义后端路径
|
||||
|
||||
可以通过修改环境变量 `VITE_API_BASE_URL` 来自定义后端 API 路径:
|
||||
|
||||
```bash
|
||||
# 开发环境 (.env.development)
|
||||
VITE_API_BASE_URL=http://localhost:8080/api/v1
|
||||
|
||||
# 或使用其他端口/域名
|
||||
VITE_API_BASE_URL=http://192.168.1.100:8080/api/v1
|
||||
VITE_API_BASE_URL=https://api.example.com/api/v1
|
||||
|
||||
# 生产环境 (.env.production)
|
||||
VITE_API_BASE_URL=/api/v1
|
||||
|
||||
# 或自定义路径
|
||||
VITE_API_BASE_URL=/custom/api/path
|
||||
```
|
||||
|
||||
## 构建命令
|
||||
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 启动开发服务器(使用完整后端 URL)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
开发环境会直接请求配置的后端服务器地址,无需代理。
|
||||
|
||||
**注意**:需要确保后端服务器配置了 CORS,允许来自 `http://localhost:5173` 的请求。
|
||||
|
||||
Go 后端 CORS 配置示例(使用 Gin):
|
||||
```go
|
||||
import "github.com/gin-contrib/cors"
|
||||
|
||||
r := gin.Default()
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:5173"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
```
|
||||
|
||||
### 生产环境构建
|
||||
|
||||
```bash
|
||||
# 标准构建(生产模式)
|
||||
npm run build
|
||||
|
||||
# 显式生产环境构建
|
||||
npm run build:prod
|
||||
|
||||
# 开发模式构建(包含 sourcemap)
|
||||
npm run build:dev
|
||||
```
|
||||
|
||||
### 清理构建
|
||||
|
||||
```bash
|
||||
# 清理输出目录
|
||||
npm run clean
|
||||
```
|
||||
|
||||
## 输出目录
|
||||
|
||||
构建产物会自动输出到项目上级目录的 `web` 文件夹:
|
||||
|
||||
```
|
||||
go-project/
|
||||
├── webapp/ # Vue 项目源码
|
||||
│ ├── src/
|
||||
│ ├── package.json
|
||||
│ └── vite.config.ts
|
||||
├── web/ # 构建输出目录(自动生成)
|
||||
│ ├── index.html
|
||||
│ ├── assets/
|
||||
│ └── ...
|
||||
└── main.go # Go 主程序
|
||||
```
|
||||
|
||||
## Go 服务器配置
|
||||
|
||||
Go 服务器需要配置静态文件服务来访问 `web` 目录:
|
||||
|
||||
```go
|
||||
// 示例:Gin 框架配置
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-contrib/cors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 开发环境:配置 CORS
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:5173"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
}
|
||||
|
||||
// API 路由
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
// ... 你的 API 路由
|
||||
}
|
||||
|
||||
// 静态文件服务(生产环境)
|
||||
r.Static("/assets", "./web/assets")
|
||||
r.StaticFile("/", "./web/index.html")
|
||||
|
||||
// SPA fallback
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.File("./web/index.html")
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
## API 使用示例
|
||||
|
||||
项目提供了完整的 TypeScript API 客户端:
|
||||
|
||||
```typescript
|
||||
import { bingPaperApi } from '@/lib/api-service'
|
||||
|
||||
// 获取今日图片元数据
|
||||
const todayMeta = await bingPaperApi.getTodayImageMeta()
|
||||
|
||||
// 获取图片列表
|
||||
const images = await bingPaperApi.getImages({ limit: 10 })
|
||||
|
||||
// 管理员登录
|
||||
const token = await bingPaperApi.login({ password: 'admin123' })
|
||||
bingPaperApi.setAuthToken(token.token)
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ ├── api-config.ts # API 配置管理
|
||||
│ ├── api-types.ts # TypeScript 类型定义
|
||||
│ ├── api-service.ts # API 服务封装
|
||||
│ ├── http-client.ts # HTTP 客户端
|
||||
│ └── utils.ts # 工具函数
|
||||
├── components/ # Vue 组件
|
||||
├── views/ # 页面组件
|
||||
│ ├── Home.vue # 首页画廊
|
||||
│ └── ImageView.vue # 图片查看
|
||||
├── composables/ # 组合式函数
|
||||
│ └── useImages.ts # 图片数据管理
|
||||
├── router/ # 路由配置
|
||||
│ └── index.ts
|
||||
├── App.vue
|
||||
└── main.ts
|
||||
```
|
||||
|
||||
## 部署注意事项
|
||||
|
||||
1. **构建顺序**:确保在 Go 服务启动前完成前端构建
|
||||
2. **路径配置**:Go 服务器的 API 路径应与前端配置的 `VITE_API_BASE_URL` 一致
|
||||
3. **静态文件**:Go 服务器需要正确配置静态文件服务路径
|
||||
4. **路由处理**:对于 SPA 应用,需要配置 fallback 到 `index.html`
|
||||
5. **CORS 配置**:开发环境需要配置 CORS,生产环境不需要(同域)
|
||||
|
||||
## 故障排除
|
||||
|
||||
### API 请求失败(开发环境)
|
||||
|
||||
1. 检查环境变量配置是否正确
|
||||
2. 确认 Go 服务器已启动在 `http://localhost:8080`
|
||||
3. 检查 Go 服务器是否配置了 CORS
|
||||
4. 在浏览器控制台查看具体错误信息
|
||||
|
||||
### CORS 错误
|
||||
|
||||
如果看到类似以下错误:
|
||||
```
|
||||
Access to fetch at 'http://localhost:8080/api/v1/...' from origin 'http://localhost:5173'
|
||||
has been blocked by CORS policy
|
||||
```
|
||||
|
||||
解决方案:
|
||||
1. 在 Go 服务器添加 CORS 中间件
|
||||
2. 或者在 vite.config.ts 中添加代理配置(不推荐,因为会隐藏实际的请求路径)
|
||||
|
||||
### 构建失败
|
||||
|
||||
1. 清理 `node_modules` 并重新安装依赖
|
||||
2. 检查 TypeScript 类型错误
|
||||
3. 确认输出目录权限
|
||||
|
||||
### 静态资源加载失败
|
||||
|
||||
1. 检查 Go 服务器静态文件配置
|
||||
2. 确认构建产物的路径结构
|
||||
3. 检查 Vite 构建配置中的 `base` 和 `assetsDir`
|
||||
|
||||
## 开发工作流
|
||||
|
||||
### 典型的开发流程
|
||||
|
||||
1. **启动后端服务**
|
||||
```bash
|
||||
cd .. # 回到 Go 项目根目录
|
||||
go run main.go
|
||||
```
|
||||
|
||||
2. **启动前端开发服务器**
|
||||
```bash
|
||||
cd webapp
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **访问应用**
|
||||
```
|
||||
http://localhost:5173/
|
||||
```
|
||||
|
||||
4. **开发完成后构建**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. **测试生产构建**
|
||||
```bash
|
||||
# 停止开发服务器,启动 Go 服务器
|
||||
cd ..
|
||||
go run main.go
|
||||
# 访问 http://localhost:8080/
|
||||
```
|
||||
|
||||
## 配置对比
|
||||
|
||||
| 配置项 | 开发环境 | 生产环境 |
|
||||
|--------|----------|----------|
|
||||
| API Base URL | `http://localhost:8080/api/v1` | `/api/v1` |
|
||||
| 请求方式 | 直接请求 | 相对路径 |
|
||||
| CORS | 需要 | 不需要 |
|
||||
| 服务器 | 前后端分离 | 同一服务器 |
|
||||
| 端口 | 前端 5173 + 后端 8080 | 8080 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **开发环境**使用完整 URL,便于调试和查看实际请求
|
||||
2. **生产环境**使用相对路径,简化部署
|
||||
3. 保持 `.env.development` 和 `.env.production` 文件同步更新
|
||||
4. 在 Go 服务器中使用环境变量区分开发/生产模式
|
||||
5. 定期测试生产构建,确保配置正确
|
||||
@@ -1,255 +0,0 @@
|
||||
# Go 后端 CORS 配置示例
|
||||
|
||||
## 问题说明
|
||||
|
||||
开发环境中,前端运行在 `http://localhost:5173`,后端运行在 `http://localhost:8080`。
|
||||
由于跨域限制,需要在后端配置 CORS 才能正常访问 API。
|
||||
|
||||
## 使用 Gin 框架
|
||||
|
||||
### 1. 安装 CORS 中间件
|
||||
|
||||
```bash
|
||||
go get github.com/gin-contrib/cors
|
||||
```
|
||||
|
||||
### 2. 配置 CORS
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-contrib/cors"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 开发环境:配置 CORS
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
config := cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:5173"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}
|
||||
r.Use(cors.New(config))
|
||||
}
|
||||
|
||||
// API 路由
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
// 图片相关
|
||||
api.GET("/images", getImages)
|
||||
api.GET("/image/today/meta", getTodayImageMeta)
|
||||
api.GET("/image/date/:date/meta", getImageMetaByDate)
|
||||
api.GET("/image/random/meta", getRandomImageMeta)
|
||||
|
||||
// 管理员相关
|
||||
api.POST("/admin/login", adminLogin)
|
||||
// ... 其他路由
|
||||
}
|
||||
|
||||
// 生产环境:静态文件服务
|
||||
if gin.Mode() == gin.ReleaseMode {
|
||||
r.Static("/assets", "./web/assets")
|
||||
r.StaticFile("/", "./web/index.html")
|
||||
|
||||
// SPA fallback
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.File("./web/index.html")
|
||||
})
|
||||
}
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 更灵活的 CORS 配置
|
||||
|
||||
```go
|
||||
// 根据环境变量动态配置
|
||||
func setupCORS(r *gin.Engine) {
|
||||
allowOrigins := os.Getenv("ALLOW_ORIGINS")
|
||||
if allowOrigins == "" {
|
||||
allowOrigins = "http://localhost:5173"
|
||||
}
|
||||
|
||||
origins := strings.Split(allowOrigins, ",")
|
||||
|
||||
config := cors.Config{
|
||||
AllowOrigins: origins,
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}
|
||||
|
||||
r.Use(cors.New(config))
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 只在开发环境启用 CORS
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
setupCORS(r)
|
||||
}
|
||||
|
||||
// ... 其他配置
|
||||
}
|
||||
```
|
||||
|
||||
## 使用标准库
|
||||
|
||||
如果不使用 Gin 框架,可以手动实现 CORS:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置 CORS 头
|
||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "43200") // 12 hours
|
||||
|
||||
// 处理预检请求
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 注册路由
|
||||
mux.HandleFunc("/api/v1/images", getImages)
|
||||
// ... 其他路由
|
||||
|
||||
// 包装 CORS 中间件
|
||||
handler := corsMiddleware(mux)
|
||||
|
||||
http.ListenAndServe(":8080", handler)
|
||||
}
|
||||
```
|
||||
|
||||
## 配置文件方式
|
||||
|
||||
可以通过配置文件管理 CORS 设置:
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
server:
|
||||
port: 8080
|
||||
mode: debug # debug 或 release
|
||||
|
||||
cors:
|
||||
enabled: true
|
||||
allow_origins:
|
||||
- http://localhost:5173
|
||||
- http://127.0.0.1:5173
|
||||
allow_methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
allow_headers:
|
||||
- Origin
|
||||
- Content-Type
|
||||
- Accept
|
||||
- Authorization
|
||||
allow_credentials: true
|
||||
max_age: 43200
|
||||
```
|
||||
|
||||
```go
|
||||
type CORSConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AllowOrigins []string `yaml:"allow_origins"`
|
||||
AllowMethods []string `yaml:"allow_methods"`
|
||||
AllowHeaders []string `yaml:"allow_headers"`
|
||||
AllowCredentials bool `yaml:"allow_credentials"`
|
||||
MaxAge int `yaml:"max_age"`
|
||||
}
|
||||
|
||||
func setupCORSFromConfig(r *gin.Engine, cfg *CORSConfig) {
|
||||
if !cfg.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
config := cors.Config{
|
||||
AllowOrigins: cfg.AllowOrigins,
|
||||
AllowMethods: cfg.AllowMethods,
|
||||
AllowHeaders: cfg.AllowHeaders,
|
||||
AllowCredentials: cfg.AllowCredentials,
|
||||
MaxAge: time.Duration(cfg.MaxAge) * time.Second,
|
||||
}
|
||||
|
||||
r.Use(cors.New(config))
|
||||
}
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **生产环境不要启用 CORS**:生产环境前后端在同一域名下,不需要 CORS
|
||||
2. **限制允许的源**:不要使用 `*`,明确指定允许的域名
|
||||
3. **使用环境变量**:允许的源应该通过环境变量配置,不要硬编码
|
||||
4. **限制方法和头**:只允许必要的 HTTP 方法和请求头
|
||||
5. **注意凭证**:如果使用 `AllowCredentials: true`,不能使用通配符源
|
||||
|
||||
## 测试 CORS 配置
|
||||
|
||||
可以使用 curl 测试 CORS:
|
||||
|
||||
```bash
|
||||
# 测试预检请求
|
||||
curl -X OPTIONS http://localhost:8080/api/v1/images \
|
||||
-H "Origin: http://localhost:5173" \
|
||||
-H "Access-Control-Request-Method: GET" \
|
||||
-v
|
||||
|
||||
# 测试实际请求
|
||||
curl -X GET http://localhost:8080/api/v1/images \
|
||||
-H "Origin: http://localhost:5173" \
|
||||
-v
|
||||
```
|
||||
|
||||
应该能看到响应头中包含:
|
||||
```
|
||||
Access-Control-Allow-Origin: http://localhost:5173
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
||||
Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization
|
||||
Access-Control-Allow-Credentials: true
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么要区分开发和生产环境?
|
||||
A: 生产环境前后端部署在同一域名下,不需要 CORS。只在开发环境需要。
|
||||
|
||||
### Q: 可以使用代理吗?
|
||||
A: 可以在 Vite 中配置代理,但不推荐。直接配置 CORS 更接近生产环境,便于发现问题。
|
||||
|
||||
### Q: 如何处理认证?
|
||||
A: 如果使用 Cookie 或需要发送凭证,必须设置 `AllowCredentials: true` 和明确的源。
|
||||
|
||||
### Q: 预检请求是什么?
|
||||
A: 浏览器在某些跨域请求前会发送 OPTIONS 请求,询问服务器是否允许该跨域请求。
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
BingPaper 的前端 Web 应用,使用 Vue 3 + TypeScript + Vite 构建。
|
||||
|
||||
> 💡 **性能优化提示**:已配置浏览器缓存优化,可减少 60-80% 带宽!
|
||||
> 👉 后端配置:查看 [缓存配置快速参考](./CACHE_QUICK_REF.md)
|
||||
|
||||
## 特性
|
||||
|
||||
- ✨ Vue 3 组合式 API
|
||||
@@ -9,6 +12,7 @@ BingPaper 的前端 Web 应用,使用 Vue 3 + TypeScript + Vite 构建。
|
||||
- 📦 TypeScript 类型支持
|
||||
- 🔧 完整的 API 客户端封装
|
||||
- 🚀 优化的构建配置
|
||||
- ⚡ 浏览器缓存优化(内容哈希 + 代码分割)
|
||||
- 🌐 支持自定义后端路径
|
||||
- 📁 自动输出到上级目录的 web 文件夹
|
||||
|
||||
@@ -105,18 +109,22 @@ src/
|
||||
└── main.ts # 入口文件
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
## 📚 文档
|
||||
|
||||
- [Vue 3](https://vuejs.org/) - 渐进式 JavaScript 框架
|
||||
- [TypeScript](https://www.typescriptlang.org/) - 类型安全的 JavaScript
|
||||
- [Vite](https://vitejs.dev/) - 下一代前端构建工具
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - 实用优先的 CSS 框架
|
||||
- [shadcn-vue](https://www.shadcn-vue.com/) - 高质量的 Vue 组件库
|
||||
### 核心文档
|
||||
- [README.md](./README.md) - 项目概览(本文件)
|
||||
- [BUILD.md](./BUILD.md) - 构建说明
|
||||
- [USAGE.md](./USAGE.md) - 使用指南
|
||||
|
||||
## IDE 支持
|
||||
### 性能优化 ⚡
|
||||
- [CACHE_QUICK_REF.md](./CACHE_QUICK_REF.md) - **缓存配置快速参考**(推荐从这里开始)
|
||||
- [CACHE_CONFIG.md](./CACHE_CONFIG.md) - 详细的缓存配置指南
|
||||
- [CACHE_OPTIMIZATION_SUMMARY.md](./CACHE_OPTIMIZATION_SUMMARY.md) - 优化总结
|
||||
- [CACHE_TEST.html](./CACHE_TEST.html) - 缓存测试页面
|
||||
|
||||
推荐使用 [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) 扩展。
|
||||
### API 相关
|
||||
- [CORS_CONFIG.md](./CORS_CONFIG.md) - CORS 配置
|
||||
- [API_EXAMPLES.md](./API_EXAMPLES.md) - API 使用示例
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
### 其他
|
||||
- [CHANGELOG.md](./CHANGELOG.md) - 更新日志
|
||||
|
||||
201
webapp/USAGE.md
201
webapp/USAGE.md
@@ -1,201 +0,0 @@
|
||||
# 必应每日一图 - 使用说明
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
### 首页(画廊视图)
|
||||
|
||||
1. **顶部大图区域**
|
||||
- 全屏展示今日必应图片
|
||||
- 显示图片标题、版权信息和日期
|
||||
- 渐变遮罩效果,突出文字内容
|
||||
- 悬停时内容向上浮动动画
|
||||
|
||||
2. **操作按钮**
|
||||
- **查看大图**:跳转到全屏图片查看页面
|
||||
- **了解更多**:打开必应相关链接(基于 quiz 字段)
|
||||
|
||||
3. **历史图片画廊**
|
||||
- 网格布局展示历史图片(响应式:手机1列、平板2列、桌面3列)
|
||||
- 鼠标悬停显示元数据信息
|
||||
- 卡片放大和图片缩放动画效果
|
||||
- 点击任意图片打开详情页
|
||||
|
||||
4. **无限滚动**
|
||||
- 支持"加载更多"按钮
|
||||
- 自动检测是否还有更多图片
|
||||
- 加载状态提示
|
||||
|
||||
### 图片查看页面
|
||||
|
||||
1. **全屏展示**
|
||||
- 以最高清晰度(UHD)展示图片
|
||||
- 黑色背景,图片居中显示
|
||||
- 顶部和底部渐变工具栏
|
||||
|
||||
2. **信息悬浮层**(类似 Windows 聚焦壁纸)
|
||||
- 半透明背景,毛玻璃效果
|
||||
- 显示图片标题、版权信息
|
||||
- "了解更多信息"链接到必应相关页面
|
||||
- 可以通过按钮或键盘切换显示/隐藏
|
||||
|
||||
3. **日期导航**
|
||||
- **前一天** / **后一天** 按钮
|
||||
- 支持键盘方向键导航(← / →)
|
||||
- 今天的图片禁用"后一天"按钮
|
||||
- 导航时有节流保护
|
||||
|
||||
4. **键盘快捷键**
|
||||
- `←` : 前一天
|
||||
- `→` : 后一天
|
||||
- `Esc` : 返回首页
|
||||
- `I` : 切换信息显示
|
||||
|
||||
## 🎨 设计亮点
|
||||
|
||||
### 视觉效果
|
||||
- 深色主题,突出图片内容
|
||||
- 渐变背景和遮罩层
|
||||
- 毛玻璃效果(backdrop-blur)
|
||||
- 平滑的过渡动画
|
||||
- 响应式设计,适配各种屏幕
|
||||
|
||||
### 交互体验
|
||||
- 悬停效果(卡片放大、图片缩放)
|
||||
- 加载状态提示
|
||||
- 按钮禁用状态
|
||||
- 键盘快捷键支持
|
||||
- 流畅的页面切换
|
||||
|
||||
## 🚀 技术实现
|
||||
|
||||
### 组件结构
|
||||
```
|
||||
src/
|
||||
├── views/
|
||||
│ ├── Home.vue # 首页画廊视图
|
||||
│ └── ImageView.vue # 图片查看页面
|
||||
├── composables/
|
||||
│ └── useImages.ts # 数据获取逻辑
|
||||
├── router/
|
||||
│ └── index.ts # 路由配置
|
||||
└── lib/
|
||||
├── api-service.ts # API 服务
|
||||
├── api-config.ts # API 配置
|
||||
├── api-types.ts # 类型定义
|
||||
└── http-client.ts # HTTP 客户端
|
||||
```
|
||||
|
||||
### 路由配置
|
||||
- `/` - 首页画廊视图
|
||||
- `/image/:date` - 图片查看页面(动态路由)
|
||||
|
||||
### API 集成
|
||||
使用封装好的 API 服务:
|
||||
- `getTodayImageMeta()` - 获取今日图片元数据
|
||||
- `getImages({ limit })` - 获取图片列表
|
||||
- `getImageMetaByDate(date)` - 获取指定日期图片
|
||||
- `getTodayImageUrl()` - 获取今日图片 URL
|
||||
- `getImageUrlByDate(date, variant)` - 获取指定日期图片 URL
|
||||
|
||||
### 数据管理
|
||||
使用 Vue Composables 模式:
|
||||
- `useTodayImage()` - 管理今日图片数据
|
||||
- `useImageList()` - 管理图片列表(支持分页)
|
||||
- `useImageByDate()` - 管理特定日期图片数据
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
### 断点
|
||||
- 手机:< 640px(1列)
|
||||
- 平板:640px - 1024px(2列)
|
||||
- 桌面:> 1024px(3列)
|
||||
|
||||
### 适配特性
|
||||
- 响应式网格布局
|
||||
- 动态字体大小
|
||||
- 移动端优化的按钮文字
|
||||
- 触摸友好的交互
|
||||
|
||||
## 🎯 使用流程
|
||||
|
||||
1. **访问首页**
|
||||
```
|
||||
http://localhost:5173/
|
||||
```
|
||||
|
||||
2. **浏览今日图片**
|
||||
- 顶部大图展示当天必应图片
|
||||
- 查看标题和版权信息
|
||||
|
||||
3. **点击"了解更多"**
|
||||
- 自动拼接完整的必应 URL
|
||||
- 在新标签页打开
|
||||
|
||||
4. **浏览历史图片**
|
||||
- 向下滚动查看历史图片网格
|
||||
- 鼠标悬停查看详细信息
|
||||
- 点击"加载更多"获取更多图片
|
||||
|
||||
5. **全屏查看图片**
|
||||
- 点击任意图片进入全屏模式
|
||||
- 使用按钮或键盘导航
|
||||
- 按 `Esc` 返回首页
|
||||
|
||||
## 🔧 开发
|
||||
|
||||
### 启动开发服务器
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 构建生产版本
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 预览构建结果
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **API 配置**
|
||||
- 确保后端 API 服务正在运行
|
||||
- 开发环境会自动代理 `/api` 请求到 `http://localhost:8080`
|
||||
|
||||
2. **图片分辨率**
|
||||
- 首页顶部大图:UHD
|
||||
- 画廊缩略图:1920x1080
|
||||
- 全屏查看:UHD
|
||||
|
||||
3. **必应链接**
|
||||
- quiz 字段需要拼接 `https://www.bing.com` 前缀
|
||||
- 例如:`/search?q=...` → `https://www.bing.com/search?q=...`
|
||||
|
||||
4. **日期格式**
|
||||
- 使用 ISO 格式:`YYYY-MM-DD`
|
||||
- 例如:`2024-01-27`
|
||||
|
||||
## 🎨 自定义样式
|
||||
|
||||
可以通过修改 Tailwind CSS 类来调整样式:
|
||||
- 渐变颜色:`bg-gradient-to-*`
|
||||
- 透明度:`bg-black/80`
|
||||
- 模糊效果:`backdrop-blur-*`
|
||||
- 动画:`transition-*`、`animate-*`
|
||||
|
||||
## 🐛 已知问题
|
||||
|
||||
- 需要确保 API 返回正确的图片元数据
|
||||
- 图片加载依赖网络速度
|
||||
- 大图可能需要一些时间加载
|
||||
|
||||
## 🚀 未来优化
|
||||
|
||||
- [ ] 添加图片预加载
|
||||
- [ ] 支持手势滑动切换(移动端)
|
||||
- [ ] 添加图片下载功能
|
||||
- [ ] 支持收藏功能
|
||||
- [ ] 添加搜索和筛选
|
||||
- [ ] 支持暗色/亮色主题切换
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from '@/views/Home.vue'
|
||||
import ImageView from '@/views/ImageView.vue'
|
||||
import ApiDocs from '@/views/ApiDocs.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -20,6 +21,14 @@ const router = createRouter({
|
||||
meta: {
|
||||
title: '图片详情'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/api-docs',
|
||||
name: 'ApiDocs',
|
||||
component: ApiDocs,
|
||||
meta: {
|
||||
title: 'API 文档'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
448
webapp/src/views/ApiDocs.vue
Normal file
448
webapp/src/views/ApiDocs.vue
Normal file
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-b from-gray-900 via-gray-800 to-gray-900">
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 bg-gray-900/80 backdrop-blur-lg border-b border-white/10 z-40">
|
||||
<div class="max-w-7xl mx-auto px-4 md:px-8 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<router-link
|
||||
to="/"
|
||||
class="flex items-center gap-2 text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
<span>返回首页</span>
|
||||
</router-link>
|
||||
<span class="text-white/20">|</span>
|
||||
<h1 class="text-xl font-bold text-white">API 文档</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-white/40">
|
||||
v1.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="max-w-7xl mx-auto px-4 md:px-8 py-12">
|
||||
<!-- Intro Section -->
|
||||
<section class="mb-12">
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-2xl p-8 border border-white/10">
|
||||
<h2 class="text-3xl font-bold text-white mb-4">必应每日一图 API</h2>
|
||||
<p class="text-white/70 text-lg leading-relaxed">
|
||||
提供必应每日一图的公共 API 接口,支持获取今日图片、历史图片、随机图片等功能。
|
||||
所有接口均为 RESTful 风格,返回 JSON 格式数据或图片流。
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap gap-3">
|
||||
<div class="px-4 py-2 bg-green-500/20 text-green-300 rounded-lg text-sm font-medium border border-green-500/30">
|
||||
✓ 无需认证
|
||||
</div>
|
||||
<div class="px-4 py-2 bg-blue-500/20 text-blue-300 rounded-lg text-sm font-medium border border-blue-500/30">
|
||||
RESTful API
|
||||
</div>
|
||||
<div class="px-4 py-2 bg-purple-500/20 text-purple-300 rounded-lg text-sm font-medium border border-purple-500/30">
|
||||
JSON / 图片流
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Base URL -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">基础地址</h2>
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<code class="text-green-400 font-mono">{{ baseURL }}</code>
|
||||
<button
|
||||
@click="copyToClipboard(baseURL)"
|
||||
class="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||
title="复制"
|
||||
>
|
||||
<svg class="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-white/50 text-sm">所有 API 请求都基于此地址</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Image APIs -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">图片 API</h2>
|
||||
|
||||
<!-- Get Today's Image -->
|
||||
<div class="mb-8">
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="px-3 py-1 bg-green-500/20 text-green-300 rounded-lg text-sm font-semibold">GET</span>
|
||||
<h3 class="text-xl font-semibold text-white">获取今日图片</h3>
|
||||
</div>
|
||||
<code class="text-blue-400 font-mono text-sm">/image/today</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-white/70 mb-4">返回今日必应图片,支持不同分辨率和格式</p>
|
||||
|
||||
<!-- Parameters -->
|
||||
<div class="mb-4">
|
||||
<h4 class="text-white/80 font-semibold mb-2">查询参数</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">variant</code>
|
||||
<span class="text-white/50">分辨率: UHD, 1920x1080, 1366x768 (默认: UHD)</span>
|
||||
</div>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">format</code>
|
||||
<span class="text-white/50">格式: jpg (默认: jpg)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example -->
|
||||
<div class="bg-black/30 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-white/50 text-sm">示例</span>
|
||||
<button
|
||||
@click="copyToClipboard(getTodayImageExample())"
|
||||
class="text-white/60 hover:text-white text-sm flex items-center gap-1"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<code class="text-green-400 font-mono text-sm">{{ getTodayImageExample() }}</code>
|
||||
</div>
|
||||
|
||||
<!-- Try It -->
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
:href="getTodayImageExample()"
|
||||
target="_blank"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
||||
</svg>
|
||||
在新窗口打开
|
||||
</a>
|
||||
<button
|
||||
@click="showImagePreview(getTodayImageExample())"
|
||||
class="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
预览图片
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Get Image by Date -->
|
||||
<div class="mb-8">
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="px-3 py-1 bg-green-500/20 text-green-300 rounded-lg text-sm font-semibold">GET</span>
|
||||
<h3 class="text-xl font-semibold text-white">获取指定日期图片</h3>
|
||||
</div>
|
||||
<code class="text-blue-400 font-mono text-sm">/image/date/:date</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-white/70 mb-4">根据日期返回对应的必应图片</p>
|
||||
|
||||
<!-- Parameters -->
|
||||
<div class="mb-4">
|
||||
<h4 class="text-white/80 font-semibold mb-2">路径参数</h4>
|
||||
<div class="space-y-2 mb-3">
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">date</code>
|
||||
<span class="text-white/50">日期 (格式: YYYY-MM-DD)</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="text-white/80 font-semibold mb-2">查询参数</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">variant</code>
|
||||
<span class="text-white/50">分辨率 (默认: UHD)</span>
|
||||
</div>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">format</code>
|
||||
<span class="text-white/50">格式 (默认: jpg)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example -->
|
||||
<div class="bg-black/30 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-white/50 text-sm">示例</span>
|
||||
<button
|
||||
@click="copyToClipboard(getDateImageExample())"
|
||||
class="text-white/60 hover:text-white text-sm flex items-center gap-1"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<code class="text-green-400 font-mono text-sm">{{ getDateImageExample() }}</code>
|
||||
</div>
|
||||
|
||||
<!-- Try It -->
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
:href="getDateImageExample()"
|
||||
target="_blank"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
||||
</svg>
|
||||
在新窗口打开
|
||||
</a>
|
||||
<button
|
||||
@click="showImagePreview(getDateImageExample())"
|
||||
class="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
预览图片
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Get Random Image -->
|
||||
<div class="mb-8">
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="px-3 py-1 bg-green-500/20 text-green-300 rounded-lg text-sm font-semibold">GET</span>
|
||||
<h3 class="text-xl font-semibold text-white">获取随机图片</h3>
|
||||
</div>
|
||||
<code class="text-blue-400 font-mono text-sm">/image/random</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-white/70 mb-4">随机返回一张历史图片</p>
|
||||
|
||||
<!-- Parameters -->
|
||||
<div class="mb-4">
|
||||
<h4 class="text-white/80 font-semibold mb-2">查询参数</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">variant</code>
|
||||
<span class="text-white/50">分辨率 (默认: UHD)</span>
|
||||
</div>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">format</code>
|
||||
<span class="text-white/50">格式 (默认: jpg)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example -->
|
||||
<div class="bg-black/30 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-white/50 text-sm">示例</span>
|
||||
<button
|
||||
@click="copyToClipboard(getRandomImageExample())"
|
||||
class="text-white/60 hover:text-white text-sm flex items-center gap-1"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<code class="text-green-400 font-mono text-sm">{{ getRandomImageExample() }}</code>
|
||||
</div>
|
||||
|
||||
<!-- Try It -->
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
:href="getRandomImageExample()"
|
||||
target="_blank"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
||||
</svg>
|
||||
在新窗口打开
|
||||
</a>
|
||||
<button
|
||||
@click="showImagePreview(getRandomImageExample())"
|
||||
class="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
预览图片
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metadata APIs -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">元数据 API</h2>
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<p class="text-white/70 mb-4">获取图片的元数据信息(标题、版权、日期等),只返回 JSON 数据不返回图片</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<code class="text-blue-400 font-mono">/image/today/meta</code>
|
||||
<span class="text-white/50">-</span>
|
||||
<span class="text-white/60">今日图片元数据</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<code class="text-blue-400 font-mono">/image/date/:date/meta</code>
|
||||
<span class="text-white/50">-</span>
|
||||
<span class="text-white/60">指定日期图片元数据</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<code class="text-blue-400 font-mono">/image/random/meta</code>
|
||||
<span class="text-white/50">-</span>
|
||||
<span class="text-white/60">随机图片元数据</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<code class="text-blue-400 font-mono">/images?limit=30</code>
|
||||
<span class="text-white/50">-</span>
|
||||
<span class="text-white/60">图片列表(支持分页)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Tips -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">使用提示</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-10 h-10 bg-blue-500/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white">直接使用</h3>
|
||||
</div>
|
||||
<p class="text-white/60 text-sm">
|
||||
所有图片 API 都可以直接在 HTML <code class="text-yellow-400"><img></code> 标签中使用,无需认证。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-10 h-10 bg-green-500/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white">CORS 支持</h3>
|
||||
</div>
|
||||
<p class="text-white/60 text-sm">
|
||||
API 支持跨域请求,可以从任何网站调用,适合用作壁纸服务。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-10 h-10 bg-purple-500/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white">多种分辨率</h3>
|
||||
</div>
|
||||
<p class="text-white/60 text-sm">
|
||||
支持 UHD、1920x1080、1366x768 等多种分辨率,适配不同设备。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-10 h-10 bg-orange-500/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white">每日更新</h3>
|
||||
</div>
|
||||
<p class="text-white/60 text-sm">
|
||||
图片数据每日自动更新,与必应官方保持同步。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Image Preview Modal -->
|
||||
<div
|
||||
v-if="previewImage"
|
||||
class="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
|
||||
@click="previewImage = null"
|
||||
>
|
||||
<div class="relative max-w-6xl w-full">
|
||||
<button
|
||||
@click="previewImage = null"
|
||||
class="absolute top-4 right-4 p-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<img
|
||||
:src="previewImage"
|
||||
alt="Preview"
|
||||
class="w-full h-auto rounded-lg shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { API_BASE_URL } from '@/lib/api-config'
|
||||
|
||||
const baseURL = ref(API_BASE_URL)
|
||||
const previewImage = ref<string | null>(null)
|
||||
|
||||
// 获取今日图片示例
|
||||
const getTodayImageExample = () => {
|
||||
return `${baseURL.value}/image/today?variant=UHD&format=jpg`
|
||||
}
|
||||
|
||||
// 获取指定日期图片示例
|
||||
const getDateImageExample = () => {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
return `${baseURL.value}/image/date/${today}?variant=1920x1080&format=jpg`
|
||||
}
|
||||
|
||||
// 获取随机图片示例
|
||||
const getRandomImageExample = () => {
|
||||
return `${baseURL.value}/image/random?variant=UHD&format=jpg`
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
// 可以添加一个 toast 提示
|
||||
console.log('已复制到剪贴板')
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示图片预览
|
||||
const showImagePreview = (url: string) => {
|
||||
previewImage.value = url
|
||||
}
|
||||
</script>
|
||||
@@ -122,8 +122,41 @@
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="py-8 text-center text-white/40 border-t border-white/10">
|
||||
<p>数据来源于必应每日一图 API</p>
|
||||
<footer class="py-12 px-4 border-t border-white/10">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div class="text-white/60 text-sm">
|
||||
<p>数据来源于必应每日一图 API</p>
|
||||
<p class="mt-1 text-white/40">BingPaper © 2026</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6">
|
||||
<router-link
|
||||
to="/api-docs"
|
||||
class="text-white/60 hover:text-white transition-colors text-sm flex items-center gap-2 group"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||
</svg>
|
||||
<span>API 文档</span>
|
||||
<svg class="w-4 h-4 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</router-link>
|
||||
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
class="text-white/60 hover:text-white transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,7 +24,27 @@ export default defineConfig(({ mode }) => {
|
||||
// 静态资源处理
|
||||
assetsDir: 'assets',
|
||||
// 生成 sourcemap 用于生产环境调试
|
||||
sourcemap: mode === 'development'
|
||||
sourcemap: mode === 'development',
|
||||
// 静态资源内联阈值(小于此大小的资源会被内联为 base64)
|
||||
assetsInlineLimit: 4096,
|
||||
// chunk 分割策略
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 静态资源文件名(包含内容哈希,利于长期缓存)
|
||||
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||
// JS chunk 文件名
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
// 入口文件名
|
||||
entryFileNames: 'assets/[name]-[hash].js',
|
||||
// 手动分割代码
|
||||
manualChunks: {
|
||||
// 将 Vue 相关代码单独打包
|
||||
'vue-vendor': ['vue', 'vue-router'],
|
||||
// 将 UI 组件库单独打包(如果有的话)
|
||||
// 'ui-vendor': ['其他UI库']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 开发服务器配置
|
||||
server: {
|
||||
|
||||
Reference in New Issue
Block a user