Explorar o código

在构建时,提前将epic接口的图片提前缓存,提高页面响应速度

hsj hai 1 mes
pai
achega
50afd51e4a
Modificáronse 5 ficheiros con 198 adicións e 2 borrados
  1. 3 0
      .gitignore
  2. 5 1
      package.json
  3. 9 1
      pages/index.vue
  4. 163 0
      scripts/download-epic-images.mjs
  5. 18 0
      server/api/epic-manifest.ts

+ 3 - 0
.gitignore

@@ -6,3 +6,6 @@ node_modules
 .output
 .env
 dist
+
+# Epic 游戏图片缓存(构建时自动生成)
+public/epic-images

+ 5 - 1
package.json

@@ -1,7 +1,11 @@
 {
   "private": true,
   "scripts": {
+    "download:epic": "node scripts/download-epic-images.mjs",
+    "prebuild": "npm run download:epic",
     "build": "nuxt build",
+    "pregenerate": "npm run download:epic",
+    "predev": "npm run download:epic",
     "dev": "nuxt dev",
     "generate": "nuxt generate",
     "preview": "nuxt preview",
@@ -16,4 +20,4 @@
     "axios": "^1.1.2",
     "normalize.css": "^8.0.1"
   }
-}
+}

+ 9 - 1
pages/index.vue

@@ -25,16 +25,24 @@ const { data: originalArticles } = await useFetch('/api/articles')
 // 调用 Epic 免费游戏 API
 const { data: epicData } = await useFetch('https://uapis.cn/api/v1/game/epic-free')
 
+// 获取本地图片 manifest(通过 API 读取)
+const { data: imageManifest } = await useFetch('/api/epic-manifest', {
+  default: () => ({})
+})
+
 // 将 Epic 游戏数据转换为文章格式
 const epicArticles = computed(() => {
   if (!epicData.value || !epicData.value.data) return []
+  const manifest = imageManifest.value || {}
+  
   return epicData.value.data
     .filter(game => !game.title.includes('神秘游戏'))
     .map(game => ({
       id: game.id,
       title: game.title,
       briefContent: `${game.free_start}到${game.free_end}的EPIC免费游戏为${game.title}`,
-      coverImage: game.cover
+      // 优先使用本地缓存图片,如果没有则使用原始 URL
+      coverImage: manifest[game.id] || game.cover
     }))
 })
 

+ 163 - 0
scripts/download-epic-images.mjs

@@ -0,0 +1,163 @@
+#!/usr/bin/env node
+/**
+ * 下载 Epic 免费游戏图片到本地
+ * 运行方式: node scripts/download-epic-images.mjs
+ */
+
+import fs from 'fs'
+import path from 'path'
+import https from 'https'
+import http from 'http'
+import { fileURLToPath } from 'url'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+const EPIC_API_URL = 'https://uapis.cn/api/v1/game/epic-free'
+const OUTPUT_DIR = path.join(__dirname, '../public/epic-images')
+const MANIFEST_PATH = path.join(OUTPUT_DIR, 'manifest.json')
+
+// 发起 HTTPS GET 请求获取 JSON
+function fetchJson(url) {
+    return new Promise((resolve, reject) => {
+        https.get(url, (response) => {
+            if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
+                fetchJson(response.headers.location).then(resolve).catch(reject)
+                return
+            }
+
+            if (response.statusCode !== 200) {
+                reject(new Error(`HTTP ${response.statusCode}`))
+                return
+            }
+
+            let data = ''
+            response.on('data', chunk => data += chunk)
+            response.on('end', () => {
+                try {
+                    resolve(JSON.parse(data))
+                } catch (e) {
+                    reject(e)
+                }
+            })
+        }).on('error', reject)
+    })
+}
+
+// 确保输出目录存在
+function ensureDir(dir) {
+    if (!fs.existsSync(dir)) {
+        fs.mkdirSync(dir, { recursive: true })
+        console.log(`📁 Created directory: ${dir}`)
+    }
+}
+
+// 下载文件
+function downloadFile(url, destPath) {
+    return new Promise((resolve, reject) => {
+        const protocol = url.startsWith('https') ? https : http
+
+        const request = protocol.get(url, (response) => {
+            // 处理重定向
+            if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
+                downloadFile(response.headers.location, destPath).then(resolve).catch(reject)
+                return
+            }
+
+            if (response.statusCode !== 200) {
+                reject(new Error(`Failed to download ${url}: ${response.statusCode}`))
+                return
+            }
+
+            const fileStream = fs.createWriteStream(destPath)
+            response.pipe(fileStream)
+
+            fileStream.on('finish', () => {
+                fileStream.close()
+                resolve()
+            })
+
+            fileStream.on('error', (err) => {
+                fs.unlink(destPath, () => { }) // 删除失败的文件
+                reject(err)
+            })
+        })
+
+        request.on('error', reject)
+        request.setTimeout(30000, () => {
+            request.destroy()
+            reject(new Error(`Timeout downloading ${url}`))
+        })
+    })
+}
+
+// 获取文件扩展名
+function getExtension(url) {
+    const match = url.match(/\.(\w+)(?:\?|$)/)
+    return match ? `.${match[1]}` : '.jpg'
+}
+
+// 生成安全的文件名
+function safeFileName(id, url) {
+    const ext = getExtension(url)
+    return `${id}${ext}`
+}
+
+// 主函数
+async function main() {
+    console.log('🎮 开始下载 Epic 免费游戏图片...\n')
+
+    // 获取 Epic API 数据
+    console.log('📡 正在获取 Epic API 数据...')
+    const result = await fetchJson(EPIC_API_URL)
+
+    if (!result.data || !Array.isArray(result.data)) {
+        console.error('❌ API 返回数据格式错误')
+        process.exit(1)
+    }
+
+    // 过滤掉神秘游戏
+    const games = result.data.filter(game => !game.title.includes('神秘游戏'))
+    console.log(`📋 找到 ${games.length} 个游戏\n`)
+
+    // 确保输出目录存在
+    ensureDir(OUTPUT_DIR)
+
+    // 下载每个游戏的封面图片
+    const manifest = {}
+
+    for (const game of games) {
+        if (!game.cover) {
+            console.log(`⚠️  跳过 ${game.title}: 无封面图片`)
+            continue
+        }
+
+        const fileName = safeFileName(game.id, game.cover)
+        const destPath = path.join(OUTPUT_DIR, fileName)
+
+        // 检查文件是否已存在
+        if (fs.existsSync(destPath)) {
+            console.log(`✅ 已存在: ${game.title} (${fileName})`)
+            manifest[game.id] = `/epic-images/${fileName}`
+            continue
+        }
+
+        try {
+            console.log(`⬇️  下载中: ${game.title}...`)
+            await downloadFile(game.cover, destPath)
+            console.log(`✅ 完成: ${game.title} (${fileName})`)
+            manifest[game.id] = `/epic-images/${fileName}`
+        } catch (error) {
+            console.error(`❌ 失败: ${game.title} - ${error.message}`)
+            // 失败时保留原始 URL
+            manifest[game.id] = game.cover
+        }
+    }
+
+    // 保存 manifest 文件
+    fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2))
+    console.log(`\n📄 Manifest 已保存到: ${MANIFEST_PATH}`)
+    console.log('🎉 完成!')
+}
+
+main().catch(console.error)

+ 18 - 0
server/api/epic-manifest.ts

@@ -0,0 +1,18 @@
+import { readFileSync, existsSync } from 'fs'
+import { join } from 'path'
+
+export default defineEventHandler((event) => {
+    const manifestPath = join(process.cwd(), 'public', 'epic-images', 'manifest.json')
+
+    if (!existsSync(manifestPath)) {
+        return {}
+    }
+
+    try {
+        const content = readFileSync(manifestPath, 'utf-8')
+        return JSON.parse(content)
+    } catch (e) {
+        console.error('Failed to read manifest:', e)
+        return {}
+    }
+})