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,38 @@
|
||||
/**
|
||||
* 小红书图片 URL 转换
|
||||
*
|
||||
* 小红书 CDN 图片 (sns-webpic-qc.xhscdn.com) 使用签名 URL,
|
||||
* 格式: https://sns-webpic-qc.xhscdn.com/{timestamp}/{hash}/{path}
|
||||
* 这些 URL 会过期且有防盗链,第三方站点无法直接使用。
|
||||
*
|
||||
* 解决方案:转换为 ci.xiaohongshu.com 的永久 URL
|
||||
* 格式: https://ci.xiaohongshu.com/{path}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 将小红书签名 URL 转换为永久可访问的 URL
|
||||
*/
|
||||
export function proxyImage(url) {
|
||||
if (!url) return ''
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
// sns-webpic-qc.xhscdn.com/{timestamp}/{hash}/{actual_path}
|
||||
// 提取 actual_path 部分,转到 ci.xiaohongshu.com
|
||||
if (parsed.hostname === 'sns-webpic-qc.xhscdn.com') {
|
||||
const parts = parsed.pathname.split('/')
|
||||
// 去掉前导空字符串、timestamp、hash,剩下的就是图片路径
|
||||
// pathname: /202603231146/hash/notes_pre_post/image_id
|
||||
if (parts.length >= 4) {
|
||||
const imagePath = parts.slice(3).join('/')
|
||||
return `https://ci.xiaohongshu.com/${imagePath}`
|
||||
}
|
||||
}
|
||||
// 已经是 ci.xiaohongshu.com 的永久链接,直接用
|
||||
if (parsed.hostname === 'ci.xiaohongshu.com') {
|
||||
return url
|
||||
}
|
||||
} catch {
|
||||
// 不是合法 URL(可能是本地路径),直接返回
|
||||
}
|
||||
return url
|
||||
}
|
||||
@@ -2,28 +2,44 @@
|
||||
* 滚动入场动画 composable
|
||||
* 元素进入视口时触发淡入上滑动画
|
||||
* 自动尊重 prefers-reduced-motion
|
||||
* 使用 MutationObserver 确保异步渲染的子组件也能被观察到
|
||||
*/
|
||||
export default function useScrollReveal() {
|
||||
onMounted(() => {
|
||||
// 尊重用户减少动效偏好
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible')
|
||||
observer.unobserve(entry.target)
|
||||
io.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
)
|
||||
|
||||
document.querySelectorAll('.reveal').forEach((el) => {
|
||||
observer.observe(el)
|
||||
})
|
||||
// 观察当前已有的 .reveal 元素
|
||||
const observed = new WeakSet()
|
||||
function observeAll() {
|
||||
document.querySelectorAll('.reveal:not(.is-visible)').forEach((el) => {
|
||||
if (!observed.has(el)) {
|
||||
observed.add(el)
|
||||
io.observe(el)
|
||||
}
|
||||
})
|
||||
}
|
||||
observeAll()
|
||||
|
||||
onUnmounted(() => observer.disconnect())
|
||||
// 监听 DOM 变化,捕获异步渲染的子组件中的 .reveal 元素
|
||||
const mo = new MutationObserver(observeAll)
|
||||
mo.observe(document.body, { childList: true, subtree: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
io.disconnect()
|
||||
mo.disconnect()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户