28 changed files with 764 additions and 170 deletions
@ -0,0 +1,3 @@ |
|||
{ |
|||
"CodeFree.index": true |
|||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,424 @@ |
|||
|
|||
class BgSwitcher { |
|||
constructor(images, options = {}) { |
|||
this.images = images; |
|||
// 从localStorage中读取保存的索引
|
|||
const savedIndex = localStorage.getItem('bgSwitcherIndex'); |
|||
if (savedIndex !== null && !isNaN(savedIndex)) { |
|||
this.index = parseInt(savedIndex); |
|||
} else { |
|||
this.index = 0; |
|||
} |
|||
this.container = options.container || document.body; |
|||
this.interval = options.interval || 3000; |
|||
this.effect = options.effect || BgSwitcher.fadeEffect; |
|||
this.timer = null; |
|||
this.apiTimer = null; |
|||
this.apiUrl = null; |
|||
this.apiInterval = 30000; |
|||
this.startTime = 0; |
|||
// 从localStorage中读取保存的剩余时间
|
|||
const savedRemainingTime = localStorage.getItem('bgSwitcherRemainingTime'); |
|||
this.remainingTime = savedRemainingTime !== null && !isNaN(savedRemainingTime) && savedRemainingTime >= 0 ? parseInt(savedRemainingTime) : this.interval; |
|||
this.bgLayer = document.createElement('div'); |
|||
this.isInitialLoad = true; |
|||
|
|||
// 从localStorage中读取API模式状态
|
|||
const isApiMode = localStorage.getItem('bgSwitcherIsApiMode') === 'true'; |
|||
if (isApiMode) { |
|||
this.apiUrl = localStorage.getItem('bgSwitcherApiUrl') || null; |
|||
const savedApiInterval = localStorage.getItem('bgSwitcherApiInterval'); |
|||
this.apiInterval = savedApiInterval !== null && !isNaN(savedApiInterval) ? parseInt(savedApiInterval) : 30000; |
|||
} |
|||
// 监听页面可见性变化
|
|||
this.handleVisibilityChange = this.handleVisibilityChange.bind(this); |
|||
document.addEventListener('visibilitychange', this.handleVisibilityChange); |
|||
|
|||
// 监听页面卸载事件,确保保存状态
|
|||
this.handleBeforeUnload = this.handleBeforeUnload.bind(this); |
|||
window.addEventListener('beforeunload', this.handleBeforeUnload); |
|||
this.bgLayer.style.position = 'fixed'; |
|||
this.bgLayer.style.top = 0; |
|||
this.bgLayer.style.left = 0; |
|||
this.bgLayer.style.width = '100vw'; |
|||
this.bgLayer.style.height = '100vh'; |
|||
this.bgLayer.style.zIndex = '-1'; |
|||
this.bgLayer.style.transition = 'opacity 1s'; |
|||
this.bgLayer.style.opacity = 1; |
|||
this.bgLayer.style.backgroundSize = 'cover'; |
|||
this.bgLayer.style.backgroundColor = '#000000'; |
|||
this.bgLayer.style.backgroundPosition = 'center'; |
|||
this.bgLayer.style.filter = 'brightness(0.68)' |
|||
this.container.style.backgroundColor = '#000000'; |
|||
} |
|||
|
|||
setBg(url) { |
|||
// 切换时先预加载目标图片,加载完成后再切换显示
|
|||
const img = new Image(); |
|||
img.onload = () => { |
|||
if (this.isInitialLoad) { |
|||
// 初始加载时,先设置背景图再添加到页面
|
|||
this.bgLayer.style.backgroundImage = `url(${url})`; |
|||
this.container.appendChild(this.bgLayer); |
|||
this.isInitialLoad = false; |
|||
} else { |
|||
this.effect(this.bgLayer, url); |
|||
} |
|||
if(!this.isApiMode) { |
|||
this.scheduleNext(); |
|||
} |
|||
}; |
|||
img.onerror = () => { |
|||
// 加载失败时处理
|
|||
console.warn('背景图片加载失败:', url); |
|||
if (this.isInitialLoad) { |
|||
// 初始加载失败时,也添加背景层到页面
|
|||
this.container.appendChild(this.bgLayer); |
|||
this.isInitialLoad = false; |
|||
} |
|||
}; |
|||
img.src = url; |
|||
} |
|||
|
|||
next() { |
|||
const nextIndex = (this.index + 1) % this.images.length; |
|||
const nextUrl = this.images[nextIndex]; |
|||
// 切换前先预加载
|
|||
const img = new Image(); |
|||
img.onload = () => { |
|||
this.index = nextIndex; |
|||
// 保存索引到localStorage
|
|||
localStorage.setItem('bgSwitcherIndex', this.index); |
|||
this.effect(this.bgLayer, nextUrl); |
|||
this.scheduleNext(); |
|||
}; |
|||
img.onerror = () => { |
|||
// 加载失败时跳过
|
|||
console.warn('背景图片加载失败:', nextUrl); |
|||
this.scheduleNext(); |
|||
}; |
|||
img.src = nextUrl; |
|||
} |
|||
|
|||
prev() { |
|||
const prevIndex = (this.index - 1 + this.images.length) % this.images.length; |
|||
const prevUrl = this.images[prevIndex]; |
|||
// 切换前先预加载
|
|||
const img = new Image(); |
|||
img.onload = () => { |
|||
this.index = prevIndex; |
|||
// 保存索引到localStorage
|
|||
localStorage.setItem('bgSwitcherIndex', this.index); |
|||
this.effect(this.bgLayer, prevUrl); |
|||
this.scheduleNext(); |
|||
}; |
|||
img.onerror = () => { |
|||
// 加载失败时跳过
|
|||
console.warn('背景图片加载失败:', prevUrl); |
|||
this.scheduleNext(); |
|||
}; |
|||
img.src = prevUrl; |
|||
} |
|||
|
|||
start() { |
|||
if (this.timer || this.apiTimer) return; |
|||
|
|||
// 如果处于API模式,启动API请求
|
|||
if (this.apiUrl) { |
|||
this.fetchRandomWallpaper(); |
|||
} else { |
|||
// 否则使用默认轮播
|
|||
this.setBg(this.images[this.index]); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 安排下一次背景切换 |
|||
*/ |
|||
scheduleNext() { |
|||
if (this.timer) { |
|||
clearTimeout(this.timer); |
|||
} |
|||
|
|||
// 记录开始时间
|
|||
this.startTime = Date.now(); |
|||
|
|||
// 使用剩余时间或默认间隔
|
|||
const timeToWait = this.remainingTime > 0 ? this.remainingTime : this.interval; |
|||
|
|||
this.timer = setTimeout(() => { |
|||
this.remainingTime = this.interval; // 重置剩余时间
|
|||
// 保存剩余时间到localStorage
|
|||
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime); |
|||
this.next(); |
|||
}, timeToWait); |
|||
} |
|||
|
|||
/** |
|||
* 处理页面可见性变化 |
|||
*/ |
|||
handleVisibilityChange() { |
|||
if (document.hidden) { |
|||
// 页面不可见时,暂停计时器并计算剩余时间
|
|||
if (this.timer) { |
|||
const elapsedTime = Date.now() - this.startTime; |
|||
this.remainingTime = Math.max(0, this.remainingTime - elapsedTime); |
|||
// 保存剩余时间到localStorage
|
|||
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime); |
|||
clearTimeout(this.timer); |
|||
this.timer = null; |
|||
} |
|||
// 暂停API计时器
|
|||
if (this.apiTimer) { |
|||
const elapsedTime = Date.now() - this.startTime; |
|||
this.remainingTime = Math.max(0, this.remainingTime - elapsedTime); |
|||
// 保存剩余时间到localStorage
|
|||
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime); |
|||
clearTimeout(this.apiTimer); |
|||
this.apiTimer = null; |
|||
} |
|||
} else { |
|||
// 页面可见时,恢复计时器
|
|||
if (!this.timer && !this.apiTimer && !this.apiUrl) { |
|||
// 如果没有活跃的计时器,使用默认的轮播
|
|||
this.scheduleNext(); |
|||
} else if (this.apiTimer === null && this.apiUrl) { |
|||
// 如果处于API模式但计时器未运行,恢复API请求
|
|||
this.scheduleNextApiRequest(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理页面卸载事件,确保保存状态 |
|||
*/ |
|||
handleBeforeUnload() { |
|||
// 保存当前索引
|
|||
localStorage.setItem('bgSwitcherIndex', this.index); |
|||
|
|||
// 保存API模式状态
|
|||
localStorage.setItem('bgSwitcherIsApiMode', !!this.apiUrl); |
|||
if (this.apiUrl) { |
|||
localStorage.setItem('bgSwitcherApiUrl', this.apiUrl); |
|||
localStorage.setItem('bgSwitcherApiInterval', this.apiInterval); |
|||
} |
|||
|
|||
// 如果计时器在运行,计算并保存剩余时间
|
|||
if (this.timer || this.apiTimer) { |
|||
const elapsedTime = Date.now() - this.startTime; |
|||
this.remainingTime = Math.max(0, this.remainingTime - elapsedTime); |
|||
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime); |
|||
} |
|||
} |
|||
|
|||
stop() { |
|||
if (this.timer) { |
|||
clearTimeout(this.timer); |
|||
this.timer = null; |
|||
} |
|||
// 重置剩余时间
|
|||
this.remainingTime = this.interval; |
|||
// 保存剩余时间到localStorage
|
|||
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime); |
|||
} |
|||
|
|||
/** |
|||
* 从API获取随机壁纸并定期更新 |
|||
* @param {string} apiUrl - 获取随机壁纸的API地址 |
|||
* @param {number} interval - 请求间隔时间(毫秒) |
|||
*/ |
|||
startRandomApiSwitch(apiUrl, interval = 30000) { |
|||
this.stop(); // 停止当前的轮播
|
|||
this.apiInterval = interval; |
|||
this.apiUrl = apiUrl; |
|||
|
|||
// 创建专用的API计时器
|
|||
this.apiTimer = null; |
|||
|
|||
// 立即请求一次
|
|||
this.fetchRandomWallpaper(); |
|||
} |
|||
|
|||
/** |
|||
* 从API获取随机壁纸 |
|||
*/ |
|||
fetchRandomWallpaper() { |
|||
// 记录开始时间,用于计算剩余时间
|
|||
this.startTime = Date.now(); |
|||
this.remainingTime = this.apiInterval; |
|||
|
|||
fetch(this.apiUrl) |
|||
.then(response => { |
|||
console.log(response); |
|||
|
|||
if (!response.ok) { |
|||
throw new Error(`HTTP error! status: ${response.status}`); |
|||
} |
|||
return response.json(); |
|||
}) |
|||
.then(data => { |
|||
// 假设API返回的数据格式为 { wallpaperUrl: '图片地址' }
|
|||
const wallpaperUrl = data.wallpaperUrl || data.url || data.image; |
|||
if (wallpaperUrl) { |
|||
// 预加载图片
|
|||
const img = new Image(); |
|||
img.onload = () => { |
|||
if (this.isInitialLoad) { |
|||
// 初始加载时,先设置背景图再添加到页面
|
|||
this.container.appendChild(this.bgLayer); |
|||
this.isInitialLoad = false; |
|||
} |
|||
// 保存当前索引(使用-1标记这是API获取的图片)
|
|||
this.index = -1; |
|||
localStorage.setItem('bgSwitcherIndex', -1); |
|||
this.effect(this.bgLayer, wallpaperUrl); |
|||
this.scheduleNextApiRequest(); |
|||
}; |
|||
img.onerror = () => { |
|||
console.warn('API返回的壁纸加载失败:', wallpaperUrl); |
|||
this.scheduleNextApiRequest(); |
|||
}; |
|||
img.src = wallpaperUrl; |
|||
|
|||
} else { |
|||
console.warn('背景图片加载失败:', url); |
|||
if (this.isInitialLoad) { |
|||
console.warn('API返回的数据格式不正确,未找到壁纸地址'); |
|||
// 初始加载失败时,也添加背景层到页面
|
|||
this.container.appendChild(this.bgLayer); |
|||
this.isInitialLoad = false; |
|||
} |
|||
this.scheduleNextApiRequest(); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('获取随机壁纸失败:', error); |
|||
this.scheduleNextApiRequest(); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 安排下一次API请求 |
|||
*/ |
|||
scheduleNextApiRequest() { |
|||
if (this.apiTimer) { |
|||
clearTimeout(this.apiTimer); |
|||
} |
|||
|
|||
// 使用剩余时间或默认间隔
|
|||
const timeToWait = this.remainingTime > 0 ? this.remainingTime : this.apiInterval; |
|||
|
|||
this.apiTimer = setTimeout(() => { |
|||
this.remainingTime = this.apiInterval; // 重置剩余时间
|
|||
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime); |
|||
this.fetchRandomWallpaper(); |
|||
}, timeToWait); |
|||
} |
|||
|
|||
/** |
|||
* 停止API随机壁纸请求 |
|||
*/ |
|||
stopRandomApiSwitch() { |
|||
if (this.apiTimer) { |
|||
clearTimeout(this.apiTimer); |
|||
this.apiTimer = null; |
|||
} |
|||
this.apiUrl = null |
|||
// 重置剩余时间
|
|||
this.remainingTime = this.interval; |
|||
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime); |
|||
} |
|||
|
|||
static fadeEffect(layer, url) { |
|||
layer.style.transition = 'opacity 1s'; |
|||
layer.style.opacity = 0; |
|||
setTimeout(() => { |
|||
layer.style.backgroundImage = `url(${url})`; |
|||
layer.style.opacity = 1; |
|||
}, 500); |
|||
} |
|||
} |
|||
|
|||
// 使用示例
|
|||
// 1. 默认本地图片轮播
|
|||
// const images = [
|
|||
// '/static/bg2.webp',
|
|||
// '/static/bg.jpg',
|
|||
// ];
|
|||
// const bgSwitcher = new BgSwitcher(images, { interval: 5000 });
|
|||
// 启动轮播
|
|||
// bgSwitcher.start();
|
|||
|
|||
// 2. 随机API壁纸示例
|
|||
// 创建一个新的BgSwitcher实例用于API模式
|
|||
const apiBgSwitcher = new BgSwitcher([], { interval: 5000 }); // API模式不需要本地图片列表
|
|||
|
|||
// 模拟API函数,实际使用时替换为真实API地址
|
|||
function createMockWallpaperApi() { |
|||
// 模拟壁纸地址库
|
|||
const mockWallpapers = [ |
|||
'/static/bg2.webp', |
|||
'/static/bg.jpg', |
|||
]; |
|||
|
|||
// 创建一个简单的服务器端点
|
|||
if (window.mockWallpaperServer) { |
|||
clearInterval(window.mockWallpaperServer); |
|||
} |
|||
|
|||
// 模拟API响应
|
|||
window.fetchRandomWallpaper = function () { |
|||
return new Promise((resolve) => { |
|||
setTimeout(() => { |
|||
const randomIndex = Math.floor(Math.random() * mockWallpapers.length); |
|||
resolve({ |
|||
ok: true, |
|||
json() { |
|||
return { |
|||
wallpaperUrl: mockWallpapers[randomIndex] |
|||
} |
|||
} |
|||
}); |
|||
}, 500); |
|||
}); |
|||
}; |
|||
|
|||
// 替换原生fetch以模拟API调用
|
|||
window.originalFetch = window.fetch; |
|||
window.fetch = function (url) { |
|||
if (url === '/api/random-wallpaper') { |
|||
return window.fetchRandomWallpaper(); |
|||
} |
|||
return window.originalFetch(url); |
|||
}; |
|||
|
|||
console.log('模拟壁纸API已启动'); |
|||
} |
|||
|
|||
// 初始化模拟API
|
|||
createMockWallpaperApi(); |
|||
|
|||
// 启动API模式的随机壁纸切换(每10秒请求一次)
|
|||
apiBgSwitcher.startRandomApiSwitch('/api/random-wallpaper', 10000); |
|||
|
|||
fetch("https://pic.xieyaxin.top/random.php") |
|||
.then(response => { |
|||
if(response.body instanceof ReadableStream) { |
|||
return response.blob() |
|||
} |
|||
return response.json() |
|||
}) |
|||
.then(data => { |
|||
console.log(URL.createObjectURL(data)); |
|||
|
|||
}) |
|||
|
|||
// 要停止API模式,使用
|
|||
// apiBgSwitcher.stopRandomApiSwitch();
|
|||
|
|||
// 要切换回本地图片轮播,使用
|
|||
// apiBgSwitcher.stopRandomApiSwitch();
|
|||
// apiBgSwitcher.start();
|
|||
|
|||
// 启动默认轮播
|
|||
// bgSwitcher.start();
|
After Width: | Height: | Size: 105 KiB |
@ -1,54 +1,52 @@ |
|||
import CommonError from "utils/error/CommonError" |
|||
// src/plugins/errorHandler.js
|
|||
// 错误处理中间件插件
|
|||
|
|||
function formatError(ctx, status, message, stack) { |
|||
const accept = ctx.accepts('json', 'html', 'text'); |
|||
const isDev = process.env.NODE_ENV === 'development'; |
|||
if (accept === 'json') { |
|||
ctx.type = 'application/json'; |
|||
ctx.body = isDev && stack |
|||
? { success: false, error: message, stack } |
|||
: { success: false, error: message }; |
|||
} else if (accept === 'html') { |
|||
ctx.type = 'html'; |
|||
ctx.body = ` |
|||
<html> |
|||
<head><title>${status} Error</title></head> |
|||
<body> |
|||
<h1>${status} Error</h1> |
|||
<p>${message}</p> |
|||
${isDev && stack ? `<pre style='color:red;'>${stack}</pre>` : ''} |
|||
</body> |
|||
</html> |
|||
`;
|
|||
} else { |
|||
ctx.type = 'text'; |
|||
ctx.body = isDev && stack |
|||
? `${status} - ${message}\n${stack}` |
|||
: `${status} - ${message}`; |
|||
} |
|||
ctx.status = status; |
|||
async function formatError(ctx, status, message, stack) { |
|||
const accept = ctx.accepts("json", "html", "text") |
|||
const isDev = process.env.NODE_ENV === "development" |
|||
if (accept === "json") { |
|||
ctx.type = "application/json" |
|||
ctx.body = isDev && stack ? { success: false, error: message, stack } : { success: false, error: message } |
|||
} else if (accept === "html") { |
|||
ctx.type = "html" |
|||
await ctx.render("error/index", { status, message, stack, isDev }) |
|||
} else { |
|||
ctx.type = "text" |
|||
ctx.body = isDev && stack ? `${status} - ${message}\n${stack}` : `${status} - ${message}` |
|||
} |
|||
ctx.status = status |
|||
} |
|||
|
|||
export default function errorHandler() { |
|||
return async (ctx, next) => { |
|||
// 拦截 Chrome DevTools 探测请求,直接返回 204
|
|||
if (ctx.path === '/.well-known/appspecific/com.chrome.devtools.json') { |
|||
ctx.status = 204; |
|||
ctx.body = ''; |
|||
return; |
|||
} |
|||
try { |
|||
await next(); |
|||
if (ctx.status === 404) { |
|||
formatError(ctx, 404, 'Resource not found'); |
|||
} |
|||
} catch (err) { |
|||
const isDev = process.env.NODE_ENV === 'development'; |
|||
if (isDev && err.stack) { |
|||
console.error(err.stack); |
|||
} |
|||
formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error', isDev ? err.stack : undefined); |
|||
return async (ctx, next) => { |
|||
// 拦截 Chrome DevTools 探测请求,直接返回 204
|
|||
if (ctx.path === "/.well-known/appspecific/com.chrome.devtools.json") { |
|||
ctx.status = 204 |
|||
ctx.body = "" |
|||
return |
|||
} |
|||
try { |
|||
await next() |
|||
if (ctx.status === 404) { |
|||
await formatError(ctx, 404, "Resource not found") |
|||
} |
|||
} catch (err) { |
|||
console.error(err); |
|||
const isDev = process.env.NODE_ENV === "development" |
|||
if (isDev && err.stack) { |
|||
console.error(err.stack) |
|||
} |
|||
if (err instanceof CommonError) { |
|||
ctx.cookies.set("toast", JSON.stringify({ type: "error", message: encodeURIComponent(err.message) }), { |
|||
maxAge: 1, |
|||
httpOnly: false, |
|||
path: "/", |
|||
}) |
|||
ctx.redirect(ctx.path) |
|||
return |
|||
} |
|||
await formatError(ctx, err.statusCode || 500, err.message || err || "Internal server error", isDev ? err.stack : undefined) |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
|
@ -0,0 +1,7 @@ |
|||
export default class CommonError extends Error { |
|||
constructor(message, redirect) { |
|||
super(message) |
|||
this.name = "CommonError" |
|||
this.status = 500 |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
html |
|||
head |
|||
title #{status} Error |
|||
body |
|||
h1 #{status} Error |
|||
p #{message} |
|||
if isDev && stack |
|||
pre(style="color:red;") #{stack} |
@ -1,4 +0,0 @@ |
|||
if title |
|||
h1 <a href="/page/htmx">#{title}</a> |
|||
else |
|||
h1 默认标题 |
@ -1,2 +0,0 @@ |
|||
nav.navbar |
|||
.title 首页 |
@ -0,0 +1,17 @@ |
|||
extends /layouts/pure.pug |
|||
|
|||
block pageHead |
|||
+css("css/page/index.css") |
|||
|
|||
block pageContent |
|||
.home-hero |
|||
.avatar-container |
|||
.author #{$site.site_author} |
|||
img.avatar(src=$site.site_author_avatar, alt="") |
|||
.card |
|||
div 人生轨迹 |
|||
+include() |
|||
- var timeLine = [{icon: "第一份工作",title: "???", desc: `做游戏的。`, } ] |
|||
include /htmx/timeline.pug |
|||
//- div(hx-get="/htmx/timeline" hx-trigger="load") |
|||
//- div(style="text-align:center;color:white") Loading |
Loading…
Reference in new issue