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>
这个提交包含在:
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 下载小红书/游记封面图片到本地
|
||||
*
|
||||
* 小红书 CDN 签名 URL 会过期且有防盗链,
|
||||
* 此脚本将封面图下载到 public/uploads/covers/ 并更新数据库。
|
||||
*
|
||||
* 运行: node scripts/download-covers.mjs
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3'
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = join(__dirname, '..')
|
||||
const DB_PATH = join(ROOT, 'server/database/hulai.db')
|
||||
const COVERS_DIR = join(ROOT, 'public/uploads/covers')
|
||||
|
||||
// 确保目录存在
|
||||
mkdirSync(COVERS_DIR, { recursive: true })
|
||||
|
||||
/**
|
||||
* 将签名 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}`
|
||||
}
|
||||
}
|
||||
// 已经是永久链接
|
||||
if (parsed.hostname === 'ci.xiaohongshu.com') {
|
||||
return url
|
||||
}
|
||||
} catch {}
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片到本地
|
||||
*/
|
||||
async function downloadImage(url, filename) {
|
||||
const localPath = join(COVERS_DIR, filename)
|
||||
if (existsSync(localPath)) {
|
||||
console.log(` ⏭ 已存在: ${filename}`)
|
||||
return `/uploads/covers/${filename}`
|
||||
}
|
||||
|
||||
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) {
|
||||
console.log(` ✗ 下载失败 (${res.status}): ${filename} <- ${downloadUrl}`)
|
||||
return null
|
||||
}
|
||||
const buffer = Buffer.from(await res.arrayBuffer())
|
||||
writeFileSync(localPath, buffer)
|
||||
console.log(` ✓ 已下载: ${filename} (${(buffer.length / 1024).toFixed(1)}KB)`)
|
||||
return `/uploads/covers/${filename}`
|
||||
} catch (err) {
|
||||
console.log(` ✗ 下载出错: ${filename} - ${err.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 中提取文件扩展名
|
||||
*/
|
||||
function getExtFromUrl(url) {
|
||||
if (url.includes('webp')) return '.webp'
|
||||
if (url.includes('.jpg') || url.includes('.jpeg')) return '.jpg'
|
||||
if (url.includes('.png')) return '.png'
|
||||
return '.webp' // 小红书默认 webp
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const db = new Database(DB_PATH)
|
||||
|
||||
for (const key of ['xiaohongshu-wall', 'youji']) {
|
||||
console.log(`\n📦 处理: ${key}`)
|
||||
|
||||
const row = db.prepare('SELECT data FROM site_content WHERE key = ?').get(key)
|
||||
if (!row) {
|
||||
console.log(` ⚠ 未找到数据: ${key}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const data = JSON.parse(row.data)
|
||||
const notes = data.notes || []
|
||||
let updated = 0
|
||||
|
||||
for (const note of notes) {
|
||||
if (!note.cover) continue
|
||||
// 跳过已经是本地路径的
|
||||
if (note.cover.startsWith('/')) {
|
||||
console.log(` ⏭ 已是本地: ${note.id}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const ext = getExtFromUrl(note.cover)
|
||||
const filename = `${key.replace('xiaohongshu-wall', 'xhs')}-${note.id}${ext}`
|
||||
const localUrl = await downloadImage(note.cover, filename)
|
||||
|
||||
if (localUrl) {
|
||||
note.cover = localUrl
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
if (updated > 0) {
|
||||
db.prepare('UPDATE site_content SET data = ? WHERE key = ?')
|
||||
.run(JSON.stringify(data), key)
|
||||
console.log(`\n ✅ 已更新 ${updated} 张封面到本地路径`)
|
||||
} else {
|
||||
console.log(`\n ℹ 无需更新`)
|
||||
}
|
||||
}
|
||||
|
||||
db.close()
|
||||
console.log('\n🎉 完成!')
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
在新工单中引用
屏蔽一个用户