hulai-website/server/api/admin/download-image.post.js
刘涛 8832762e51 fix: 修复首页空白、小红书/游记封面图、定制页二维码
1. 首页大面积空白:useScrollReveal 添加 MutationObserver,
   确保异步子组件的 .reveal 元素也能触发入场动画

2. 小红书/游记封面图不显示:
   - 小红书 CDN 签名链接过期+防盗链,全部下载到本地
   - 新增 download-covers.mjs 脚本批量下载封面
   - 新增 useImageProxy composable 转换外部 URL
   - 页面 img 标签添加 referrerpolicy="no-referrer"
   - 管理端保存时自动下载外部封面图到本地
   - 种子数据更新为本地路径

3. 定制页微信号文字替换为二维码图片

4. prerender 路由添加 /xiaohongshu 和 /youji

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:37:58 +08:00

92 行
2.5 KiB
JavaScript

import { writeFileSync, existsSync, mkdirSync } from 'fs'
import { join } from 'path'
import { requireAdmin } from '../../utils/auth.js'
/**
* 下载外部图片到本地
* POST /api/admin/download-image
* Body: { url: "https://..." }
* Returns: { localUrl: "/uploads/covers/xxx.webp" }
*/
const ALLOWED_HOSTS = [
'sns-webpic-qc.xhscdn.com',
'ci.xiaohongshu.com',
'sns-img-bd.xhscdn.com',
]
/**
* 将签名 CDN URL 转为永久可下载的 URL
*/
function toPermanentUrl(url) {
try {
const parsed = new URL(url)
if (parsed.hostname === 'sns-webpic-qc.xhscdn.com') {
const parts = parsed.pathname.split('/')
if (parts.length >= 4) {
const imagePath = parts.slice(3).join('/')
return `https://ci.xiaohongshu.com/${imagePath}`
}
}
} catch {}
return url
}
export default defineEventHandler(async (event) => {
requireAdmin(event)
const { url } = await readBody(event)
if (!url || !url.startsWith('http')) {
throw createError({ statusCode: 400, message: '请提供有效的图片 URL' })
}
// 验证域名白名单
let parsedUrl
try {
parsedUrl = new URL(url)
} catch {
throw createError({ statusCode: 400, message: '无效的 URL' })
}
if (!ALLOWED_HOSTS.includes(parsedUrl.hostname)) {
throw createError({ statusCode: 403, message: '该域名不支持自动下载,请手动上传图片' })
}
// 生成本地文件名
const pathParts = parsedUrl.pathname.split('/')
const imageId = pathParts[pathParts.length - 1].split('!')[0] || `img-${Date.now()}`
const filename = `${imageId}.webp`
const coversDir = join(process.cwd(), 'public', 'uploads', 'covers')
if (!existsSync(coversDir)) {
mkdirSync(coversDir, { recursive: true })
}
const localPath = join(coversDir, filename)
const localUrl = `/uploads/covers/${filename}`
// 如果已下载过,直接返回
if (existsSync(localPath)) {
return { localUrl }
}
// 下载图片
const downloadUrl = toPermanentUrl(url)
try {
const res = await fetch(downloadUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
},
})
if (!res.ok) {
throw createError({ statusCode: 502, message: `图片下载失败 (${res.status})` })
}
const buffer = Buffer.from(await res.arrayBuffer())
writeFileSync(localPath, buffer)
return { localUrl }
} catch (err) {
if (err.statusCode) throw err
throw createError({ statusCode: 502, message: `图片下载出错: ${err.message}` })
}
})