[i=s] 本帖最后由 jamyer 于 2025-1-7 17:27 编辑 [/i]<br />
<br />
前言
基础的东西就不说了,可以参考madray大佬的教程(真大佬,看着他的教程学习的),这里主要说一下一些个人探索来的东西,暂时没有做安卓版本的适配。
总体的美化探索分为以下几部分:桌宠、翻页时钟和周进度组件、美化侧边栏,除此之外 还有一些试错经验的分享。以下是美化完成后的大致效果:
<iframe src="https://player.bilibili.com/player.html?isOutside=true&aid=113783011809678&bvid=BV1z7ryYeEhx&cid=27737722528&p=1&danmaku=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>
第一部分:桌宠小猫咪
刚按照大佬部署好sun-panel的天气和侧边栏还有播放器后,属实是觉得过分单调,跟伴侣讨论了一下,觉得还是需要有点动的东西,遂想着做个桌宠,查了一下资料,发现只用html+css+js传统三板斧想做出一些比较有风格的桌宠属实有点难度,再加上目前还没探索出docker环境下Sun-panel如何连接后台与数据库交互,所以就选择了这么个比较可爱的猫咪桌宠,使用的是CSDN的Pan-peter佬发布的前端——原生HTML猫猫max桌宠(附源码),各位佬有兴趣的可以去看一下。
接下来讲一下实现过程:
首先是html部分:
这里我就不细致讲解了,直接给出代码,大伙根据注释和根据自己的需要修改就可以了:
注意要先把源地址的源码给下载下来,因为里面有图片包在这里不方便逐一上传,如果想要不用修改代码的话,就跟我一样放到Sun-panel的/uploads/Cat/里面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXX</title>
<!-- 桌宠样式开始 注意 这里的样式代码不可以放到cat.css里,否则会出错 -->
<style>
.cywl {
position: fixed;
width: 100px;
height: 100px;
z-index: 9999;
left: 50px;
bottom: 50px;
/*下面的地址改成你的图片位置*/
cursor: url("/uploads/Cat/Maxwell_Who-CatCursor/alternate.ico"),
default;
display: flex;
/* background-color: #5ee7df; */
}
.cywl img {
width: 90px;
height: 90px;
pointer-events: none;
user-select: none;
}
</style>
<!-- 桌宠样式结束 -->
<!-- 引入all.js 注意地址改成你的all.js所在地址 -->
<script src="/custom/all.js"></script>
</head>
<body>
<!-- 宠物开始 body部分也是直接复制就可以了 注意地址修改 -->
<div id="pet" class="cywl">
<img id="pet-img" src="/uploads/Cat/max/stand1.png">
</div>
<!-- 宠物结束 -->
</body>
</html>
接下来就是cat.css部分了:
/* 鼠标样式开始 */
body {
/* 默认箭头 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/pointer.ico'),
default;
}
a,
img,
button {
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/alternate.ico'),
default;
}
html {
/* 文字输入 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/text.ico'),
text;
/* 链接 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/link.ico'),
pointer;
/* 移动 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/move.ico'),
move;
/* 拖拽 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/grab.ico'),
grab;
/* 关闭 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/not-allowed.ico'),
not-allowed;
/* 等待/加载 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/busy.ico'),
wait;
/* 改变大小 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/resize.ico'),
col-resize;
/* 左右调整 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/resize.ico'),
row-resize;
/* 上下调整 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/resize.ico'),
nwse-resize;
/* 左上到右下调整 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/resize.ico'),
nesw-resize;
/* 右上到左下调整 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/resize.ico'),
ew-resize;
/* 左右调整 */
cursor: url('/uploads/Cat/Maxwell_Who-CatCursor/resize.ico'),
ns-resize;
/* 上下调整 */
}
直接复制即可,记住上面每个图片的地址都要改成自己的图片放置的位置
最后是cat.js部分:
// cat.js
// 创建 pet 对象
const pet = {
x: 50, // 宠物初始位置的横坐标 (左下角开始)
y: 50,
vx: 1, // 水平方向上宠物前进的速度
vy: 0 // // 垂直方向上宠物前进的速度
};
// -------------------------------------------------------------动作
// 动作权重
// 权重问题:将所有权重相加,(得到一个大范围,那么就让随机数落到这个范围内,而对应的权重,就是落到的对应位置中)
var actions = {
// 普通
walkleft: 1,
walkright: 1,
fish: 1,
sleep: 1,
**: 1,
stand: 1,
};
// 存储宠物行走动画帧的数组
const LeftwalkFrames = []; // 左走
const RightwalkFrames = []; // 右走
const DragFrames = []; // 拖拽
const fishFrames = [];
const **Frames = [];
const sleepFrames = [];
const standFrames = [];
const fallingFrames = [];
// 初始化 定时器
var ttt = null;
var dragTime = null;
var action = 'stand';
//1、将对象中的动作按照权重转换为数组。可以使用 Object.keys 方法获取对象的键,
//2、再使用 Array.map 方法将每个键转换为对象 {name: key, weight: actions[key]}。
//3、最后使用 Array.reduce 方法将多个对象合成一个数组
var actionList = Object.keys(actions).map(function (key) {
return {
name: key,
weight: actions[key]
};
}).reduce(function (prev, curr) {
return prev.concat(curr);
}, []);
// 根据权重随机选择动作
// 权重问题:将所有权重相加,(得到一个大范围,那么就让随机数落到这个范围内,而对应的权重,就是落到的对应位置中)
function randomAction() {
var totalWeight = actionList.reduce(function (prev, curr) {
return prev + curr.weight;
}, 0);
// console.log(totalWeight);
var randomNum = Math.random() * totalWeight;
for (var i = 0; i < actionList.length; i++) {
if (randomNum <= actionList[i].weight) {
return actionList[i].name;
}
randomNum -= actionList[i].weight;
}
}
// -------------------------------------------------------------动作
const img111 = new Image();
img111.src = `/uploads/Cat/max/falling1.png`;
fallingFrames.push(img111);
// 创建动画序列
for (let i = 1; i < 13; i++) {
// 将行走动画帧添加到 walkFrames 数组中
const img = new Image();
img.src = `/uploads/Cat/max/walkright${i}.png`;
RightwalkFrames.push(img);
const img2 = new Image();
img2.src = `/uploads/Cat/max/walkleft${i}.png`;
LeftwalkFrames.push(img2);
// 其他
const img3 = new Image();
img3.src = `/uploads/Cat/max/drag${i}.png`;
DragFrames.push(img3);
const img4 = new Image();
img4.src = `/uploads/Cat/max/fish${i}.png`;
fishFrames.push(img4);
const img5 = new Image();
img5.src = `/uploads/Cat/max/**${i}.png`;
**Frames.push(img5);
const img6 = new Image();
img6.src = `/uploads/Cat/max/sleep${i}.png`;
sleepFrames.push(img6);
const img7 = new Image();
img7.src = `/uploads/Cat/max/stand${i}.png`;
standFrames.push(img7);
}
// 绘制宠物
function drawPet(anyFrames = RightwalkFrames) {
const frameIndex = Math.floor(Date.now() / 100) % anyFrames.length; // 计算当前应该绘制的动画帧的索引
const img = anyFrames[frameIndex]; // 获取当前应该绘制的动画帧
document.querySelector('.cywl img').src = img.src; // 更新宠物的显示图像
}
// 更新宠物位置
function updatePet() {
pet.x += pet.vx; // 更新宠物在水平方向的位置
// pet.y += pet.vy;
// 超出屏幕左边
if (pet.x < 0) {
action = 'walkright'
}
// 超出屏幕右边
if (pet.x + petDiv.clientWidth > window.innerWidth) {
action = 'walkleft'
}
petDiv.style.left = pet.x + 'px'; // 更新宠物所在 div 元素的横坐标位置
petDiv.style.bottom = pet.y + 'px';
}
// ----------------------------------------------------------------------处理用户交互
// 定位
const petDiv = document.querySelector('#pet');
petDiv.style.left = pet.x + 'px'; // 更新宠物所在 div 元素的横坐标位置
petDiv.style.bottom = pet.y + 'px';
// 禁用图片点击
const petImg = document.querySelector('#pet-img');
petImg.style.pointerEvents = 'none';
var isDragging = false; // 标记是否正在拖拽中
var diffX = 0; // 鼠标指针与盒子左上角的偏移量
var diffY = 0;
let animationId = null;
// 鼠标按下
petDiv.addEventListener('mousedown', function (event) {
action = '';
clearInterval(petTimer); // 暂停宠物行动的定时器
// 判断鼠标是否在 div 元素内按下,并记录下偏移量
if (event.target === petDiv) {
isDragging = true;
diffX = event.clientX - petDiv.offsetLeft;
diffY = event.clientY - petDiv.offsetTop;
// 拖拽定时器
dragTime = setInterval(function drag() {
// 显示拖拽 图片
drawPet(DragFrames);
}, 100);
}
});
// 鼠标拖动
document.addEventListener('mousemove', function (event) {
// 如果正在拖拽中,则更新盒子位置
if (isDragging === true) {
pet.x = event.clientX - diffX
pet.y = event.clientY - diffY
petDiv.style.left = pet.x + 'px';
petDiv.style.top = pet.y + 'px';
}
});
// 鼠标抬起
document.addEventListener('mouseup', function (event) {
if (isDragging === true) {
// 清除旧的定时器
clearInterval(ttt);
clearInterval(dragTime);
// 停止拖拽
isDragging = false;
action = 'stand';
ttt = setInterval(function name() {
action = randomAction();
}, 3000);
// 超出屏幕 回到 指定位置
if (pet.y < 0 || pet.y + petDiv.clientHeight > window.innerHeight || pet.x + petDiv.clientWidth > window.innerWidth || pet.x < 0) {
pet.x = 500;
pet.y = 500;
const petDiv = document.querySelector('#pet');
petDiv.style.left = 500 + 'px'; // 更新宠物所在 div 元素的位置
petDiv.style.top = 500 + 'px';
}
// 显示下落 图片
drawPet(fallingFrames);
}
});
// 宠物行动的定时器,每 3 秒执行一次 doAction 函数
var petTimer = setInterval(function name() {
action = randomAction();
}, 3000);
// ----------------------------------------------------------------------
// 主循环
function loop() {
console.log(action);
if (action == 'walkleft') {
// 执行 leftwalk 动作
pet.vx = -0.5;
updatePet();
drawPet(LeftwalkFrames);
}
if (action == 'walkright') {
pet.vx = 0.5;
updatePet();
drawPet(RightwalkFrames);
}
if (action == 'fish') {
drawPet(fishFrames);
}
if (action == '**') {
drawPet(**Frames);
}
if (action == 'sleep') {
drawPet(sleepFrames);
}
if (action == 'stand') {
drawPet(standFrames);
}
requestAnimationFrame(loop); // 浏览器提供的 API,用于优化动画性能并在重绘之前在主线程上执行指定的函数
}
// 启动循环
loop();
这样cat部分的代码基本就完成了(记得改其中图片的引用地址)。
只需要按照madray大佬教程写好all.js,然后把cat.js和cat.css加入进去即可。
这里还是贴一下all.js的代码:
//加载脚本函数
function loadScript(url){
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.type = 'text/javascript';
script.onload = resolve;
script.onerror = () => reject(new Error(`Failed to load script ${url}`));
document.head.appendChild(script);
});
}
//加载样式表函数
function loadStyle(url){
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel ='stylesheet';
link.href = url;
link.onload = resolve;
link.onerror = () => reject(new Error(`Failed to load style ${url}`));
document.head.appendChild(link);
});
}
//加载多个脚本
async function loadScripts(scripts){
for (const script of scripts){
try{
await loadScript(script);
}catch(error){
console.error(error);
}
}
}
//加载多个样式表
async function loadStyles(styles){
const stylePromises = styles.map(style => loadStyle(style).catch(error => {
console.error(error);
return null; // 返回 null 或其他值以继续执行
}));
try{
await Promise.all(stylePromises);
}catch(error){
console.error(error);
}
}
//调用函数加载多个脚本
const scriptsToLoad = [
'/custom/Sidebar.js',
'/custom/oneword.js',
'/custom/music.js',
'/custom/cat.js',
'/custom/week.js',
'/custom/clock.js'
//'/custom/other.js'
];
//调用函数加载多个样式表
const stylesToLoad = [
'/custom/action.css',
'/custom/cat.css',
'/custom/logo.css',
'/custom/bkline.css',
'/custom/font.css',
'/custom/clock.css',
'/custom/Sidebar.css',
'/custom/footer.css',
'/custom/weather.css'
//'/custom/other.css'
];
// 调用函数加载脚本和样式表
async function loadResources() {
try {
await Promise.all([
loadStyles(stylesToLoad),
loadScripts(scriptsToLoad)
]);
} catch (error) {
console.error("Error loading resources:", error);
}
}
loadResources();
根据自己目前所需要的.css和.js进行修改即可,注意一点,尽量不要修改成并行加载,否则问题居多。
关于桌宠带来的各种问题:
为什么要把桌宠放在第一个部分来讲呢。。。说实话这东西部署起来其实算是比较简单的,Pan-peter大佬的帖子基本已经算是把饭喂到嘴巴里面了。但是小猫的动作原理实际上属于是不停的修改DOM,本质上会导致许多功能上依赖DOM变化的东西出现或多或少的问题,这里举一个具体的例子,像是madray大佬教程的一言,我这边不清楚什么原因,用firefox和chrome都无法正常使用,只有在edge中能正常替换搜索框中的内容,用开发者工具发现一言的js根本没有触发,因此我选择使用监听DOM变化的方式来主动触发,这时候问题来了,因为桌宠脚本持续改变DOM,导致一言一秒钟请求了十多次,直接被判断成刷量的锁ip了,因此只能继续修改一言的js,以保证请求成功后立刻停止请求,在这里也是把修改后的一言js发出来,减少大家折腾的时间:
// 标志变量,标记是否已经发送过请求
var hasRequested = false;
function updatePlaceholder() {
// 如果已经发送过请求,直接返回
if (hasRequested) {
console.log('已经发送过请求,跳过本次操作');
return;
}
var in**lements = document.querySelectorAll('input[placeholder="请输入搜索内容"]');
if (in**lements.length > 0) {
// 标记为已发送请求
hasRequested = true;
console.log('发送请求获取 hitokoto...');
fetch('https://v1.hitokoto.cn/')
.then(response => response.json())
.then(data => {
if (data.hitokoto) {
// 更新所有目标 input 的 placeholder
in**lements.forEach(input => {
input.placeholder = data.hitokoto;
});
console.log('placeholder 更新成功:', data.hitokoto);
// 停止监听 DOM 变化
observer.disconnect();
console.log('DOM 变化监听已停止');
} else {
console.error('返回数据格式不正确,缺少 hitokoto 字段');
}
})
.catch(error => {
console.error('网络请求失败:', error);
// 如果请求失败,重置标志变量,允许重试
hasRequested = false;
console.log('请求失败,允许重试...');
});
} else {
console.log('未找到目标 input 元素,等待 DOM 变化...');
}
}
// 监听 DOM 变化
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
console.log('检测到 DOM 变化,触发更新...');
updatePlaceholder();
}
});
});
// 开始监听 DOM 变化
observer.observe(document.body, { childList: true, subtree: true });
// 初始执行
window.addEventListener('load', function() {
console.log('页面加载完成,触发更新...');
updatePlaceholder();
});
桌宠脚本所引发的问题在后面翻页时钟中还会出现,因此放在第一个来分享。
第二部分:翻页时钟和周进度组件
Sun-panel原来是自带时间日期的,但是感觉少点创意,因此作为基于ai编程的飞屋前端佬,还是得狠狠地使用为数不多的可以折腾的位置。
周进度组件是轻轻小组件 中的一个组件,里面挺多组件都是不错的,这里为了整体的协调选择用周进度组件和翻页时钟组件进行搭配。
翻页时钟使用的是稀土掘金的好好吃饭好好睡觉大佬发布的翻页时钟更改版 ,只能说国内各大论坛的大佬实在太有实力了!
下面是详细实现过程:
周进度组件:

先复制好嵌入链接,然后根据以下的代码进行修改即可:
// 隐藏原来的时间
const timeElement = document.querySelector('.clock-time');
if (timeElement) {
timeElement.style.display = 'none';
}
// 创建 iframe 元素来嵌入组件
const iframe = document.createElement('iframe');
iframe.src = "把复制的链接粘贴到这里";
iframe.style.width = "100%"; // 设置 iframe 宽度
iframe.style.height = "30px"; // 设置 iframe 高度
iframe.style.border = "none"; // 去掉 iframe 边框
// 将 iframe 插入到原来时间的位置
if (timeElement && timeElement.parentElement) {
timeElement.parentElement.insertBefore(iframe, timeElement);
}
保存为week.js放到Sun-panel目录内,然后在all.js内加载即可。
翻页时钟:
翻页时钟不能直接生成嵌入链接,然后我们又要替换原来的元素,因此我们选择把html的内容用js进行插入,这样还避免了修改html(每次都复制到vs里面再复制回来属实有一丢丢麻烦),属实是双赢。下面直接上代码:
clock.js:
// 隐藏原来的日期
const dateElement = document.querySelector('.clock-date');
if (dateElement) {
dateElement.style.display = 'none';
}
// 隐藏星期元素
const weekElement = document.querySelector('.clock-week');
if (weekElement) {
weekElement.style.display = 'none';
// 创建一个MutationObserver实例
const observer = new MutationObserver((mutations) => {
// 检查星期元素的display是否被设置为none
if (getCom**dStyle(weekElement).display === 'none') {
// 插入时钟的HTML
const showTimeClock = document.createElement('div');
showTimeClock.innerHTML = `
<div class="clock-show">
<ul class="time">
<!-- 小时 -->
<li>
<div class="upBox beforeTime"></div>
<div class="downBox beforeTime"></div>
<div class="upBox afterTime"></div>
<div class="downBox afterTime"></div>
</li>
<li>:</li>
<!-- 分钟 -->
<li>
<div class="upBox beforeTime"></div>
<div class="downBox beforeTime"></div>
<div class="upBox afterTime"></div>
<div class="downBox afterTime"></div>
</li>
<li>:</li>
<!-- 秒钟 -->
<li>
<div class="upBox beforeTime"></div>
<div class="downBox beforeTime change"></div>
<div class="upBox afterTime change"></div>
<div class="downBox afterTime"></div>
</li>
</ul>
</div>
`;
weekElement.parentNode.insertBefore(showTimeClock, weekElement.nextSibling);
// 执行时钟初始化代码
initializeClock();
// 停止观察
observer.disconnect();
}
});
// 配置观察选项,监视属性变化
const config = { attributes: true, attributeFilter: ['style'] };
// 开始观察星期元素
observer.observe(weekElement, config);
// 立即检查一次,防止display已经被设置为none
if (getCom**dStyle(weekElement).display === 'none') {
// 执行相同的插入和初始化代码
const showTimeClock = document.createElement('div');
showTimeClock.innerHTML = `
<div class="clock-show">
<ul class="time">
<!-- 小时 -->
<li>
<div class="upBox beforeTime"></div>
<div class="downBox beforeTime"></div>
<div class="upBox afterTime"></div>
<div class="downBox afterTime"></div>
</li>
<li>:</li>
<!-- 分钟 -->
<li>
<div class="upBox beforeTime"></div>
<div class="downBox beforeTime"></div>
<div class="upBox afterTime"></div>
<div class="downBox afterTime"></div>
</li>
<li>:</li>
<!-- 秒钟 -->
<li>
<div class="upBox beforeTime"></div>
<div class="downBox beforeTime change"></div>
<div class="upBox afterTime change"></div>
<div class="downBox afterTime"></div>
</li>
</ul>
</div>
`;
weekElement.parentNode.insertBefore(showTimeClock, weekElement.nextSibling);
initializeClock();
observer.disconnect();
}
}
// 翻页时钟的初始化函数
function initializeClock() {
// 格式化时间
const formatTime = (time) => {
if (time < 10) time = '0' + time;
return time;
};
// 翻转前面上面的盒子向下旋转0到-180°
const rotateUp = (ele, time, n) => {
let rotateDeg = 0;
ele.style.zIndex = 50;
const allDownBox = document.querySelector(`li:nth-child(${n})`).querySelectorAll('.downBox');
const beforeTime = document.querySelector(`li:nth-child(${n})`).querySelectorAll('.beforeTime');
allDownBox[1].style.display = 'none';
allDownBox[1].style.transform = `rotateX(90deg)`;
const animation = () => {
rotateDeg -= 3;
ele.style.transform = `perspective(500px) rotateX(${rotateDeg}deg)`;
if (rotateDeg == -90) {
ele.innerHTML = time;
ele.style.zIndex = -1;
allDownBox[1].style.display = 'block';
allDownBox[1].style.zIndex = 100;
rotateDown(allDownBox[1]);
}
if (rotateDeg == -150) {
beforeTime[1].innerHTML = time;
}
if (rotateDeg > -180) {
requestAnimationFrame(animation);
}
};
animation();
};
// 翻转后面下面的盒子旋转90-0°
const rotateDown = (ele) => {
let rotateDeg = 90;
const animation = () => {
rotateDeg -= 3;
ele.style.transform = `perspective(500px) rotateX(${rotateDeg}deg)`;
if (rotateDeg > 0) {
requestAnimationFrame(animation);
}
};
animation();
};
// 初始化时间显示
let time = new Date();
let oldHour = time.getHours();
let oldMinute = time.getMinutes();
let oldSecond = time.getSeconds();
document.querySelector('li:nth-child(1)').querySelectorAll('.beforeTime').forEach(ele => ele.innerHTML = formatTime(oldHour));
document.querySelector('li:nth-child(3)').querySelectorAll('.beforeTime').forEach(ele => ele.innerHTML = formatTime(oldMinute));
document.querySelector('li:nth-child(5)').querySelectorAll('.beforeTime').forEach(ele => ele.innerHTML = formatTime(oldSecond));
const changeTime = () => {
let time = new Date();
let hour = time.getHours();
let minute = time.getMinutes();
let second = time.getSeconds();
const setHourBox = document.querySelector('li:nth-child(1)').querySelectorAll('.afterTime');
if (!setHourBox[0].innerHTML || setHourBox[0].innerHTML != formatTime(hour)) {
if (setHourBox[0].innerHTML) {
rotateUp(document.querySelector('li:nth-child(1)').querySelectorAll('.beforeTime')[0], formatTime(hour), 1);
}
setHourBox.forEach(ele => ele.innerHTML = formatTime(hour));
}
const setMinuteBox = document.querySelector('li:nth-child(3)').querySelectorAll('.afterTime');
if (!setMinuteBox[0].innerHTML || setMinuteBox[0].innerHTML != formatTime(minute)) {
if (setMinuteBox[0].innerHTML) {
rotateUp(document.querySelector('li:nth-child(3)').querySelectorAll('.beforeTime')[0], formatTime(minute), 3);
}
setMinuteBox.forEach(ele => ele.innerHTML = formatTime(minute));
}
const setSecondBox = document.querySelector('li:nth-child(5)').querySelectorAll('.afterTime');
setSecondBox.forEach(ele => ele.innerHTML = formatTime(second));
rotateUp(document.querySelector('li:nth-child(5)').querySelectorAll('.beforeTime')[0], formatTime(second), 5);
setTimeout(changeTime, 1000);
};
changeTime();
}
代码上的问题我就不过多解释了,看大佬的帖子有十分详细的解释(再次膜拜),这里的难点在于插入的时机,DOM由于桌宠的存在永远不存在加载完成的可能,因此选择使用MutationObserver监控DOM变化,当观察到 .clock-week被隐藏时,执行插入操作以插入翻页时钟的HTML结构。
美中不足的是,原本的星期和日期两个位置现在只替换成了一个翻页时钟(因为翻页时钟比较大,已经缩放到原来的三分之一了才勉强适配),或许后面会有大佬探索出更好的排版方式。
clock.css:
* {
list-style-type: none;
padding: 0;
margin: 0;
}
.clock-show {
width: 267px; /* 800 / 3 */
height: 40px; /* 300 / 3 */
margin: 33px auto; /* 100 / 3 */
position: relative;
background-color: rgba(255, 255, 255, 0);
color: #fff;
}
.time {
display: flex;
flex: 1;
font-size: 53px; /* 160 / 3 */
text-align: center;
line-height: 100px; /* 300 / 3 */
padding: 0 7px; /* 0 20 / 3 */
overflow: hidden;
position: absolute;
top: 0;
left: 0;
right: 0;
}
.beforeTime {
z-index: 100;
}
.time li:nth-child(2n) {
flex: 1;
}
.time li:nth-child(2n+1) {
flex: 4;
background-color: #3b3d3b;
position: relative;
height: 67px; /* 200 / 3 */
line-height: 67px; /* 200 / 3 */
margin: auto 7px; /* auto 20 / 3 */
border-radius: 3px; /* 10 / 3 */
box-shadow: 0 0 7px 1px white; /* 调整阴影参数 */
}
.time li:nth-child(2n+1) .upBox,
.time li:nth-child(2n+1) .downBox {
position: absolute;
left: 0;
right: 0;
overflow: hidden;
}
.time li:nth-child(2n+1) .upBox {
top: 0;
bottom: 50%;
box-sizing: border-box;
border-bottom: solid 1px #3b3d3b; /* 3 / 3 */
background-color: #3b3d3b;
transform-origin: bottom;
}
.time li:nth-child(2n+1) .downBox {
top: 50%;
bottom: 0;
line-height: 0;
box-sizing: border-box;
background-color: #3b3d3b;
overflow: hidden;
transform-origin: top;
}
.change {
border-bottom: 1px solid white; /* 2 / 3 */
}
自行调节参数即可。
当clock.js和clock.css都配置完成后,放到Sun-panel目录内,用all.js引入即可。
第三部分:美化侧边栏
碎碎念:
这个侧边栏的灵感来源于Github的Sun-panel-js-plugin中baoyu0大佬的issue ,ta写的侧边栏做了手机端和电脑端的适配,我感觉用起来挺舒服的,然后根据自己的审美改进了一下,改进内容如下:
1.将唤起按钮放在了页面左侧中间,并保证侧边栏始终在垂直方向居中,随分组增多逐渐变长

一个分组的时候

三个分组的时候
2.做了侧边栏的拉出和收回动画
3.做了侧边栏设置按钮,目前设置只有背景图片设置。实现思路是点开设置会出现一个模态框,可以根据自己需求填写图片的网页地址或者存放路径(如/uploads/Sidebar/xxx.jpg),点击确认后会自动下载Sidebar.json配置文件,并弹出提示“请将下载的 Sidebar.json 文件保存到 /custom/Sidebar 目录中。” 说实话我知道这样比较麻烦,但是实在没能成功进行前后端交互链接数据库,否则这个功能应该会是一个不错的功能,拓展性更高。接下来也会继续探索其他方式,缓存cookies这些试过了真不不行,希望能够实现自由快捷地换侧边栏背景图。

接下来是具体实现过程:
同样是仅css和js文件,不修改html,方便部署和删除。
Sidebar.css:
:root {
--btnBg: rgba(255, 255, 255, 0.5); /* 按钮背景色 */
--btnColor: #333333; /* 按钮文字颜色 */
--contentBg: rgba(255, 255, 255, 0.8); /* 内容背景色 */
--textColor: #333333; /* 文字颜色 */
--hoverBg: rgba(0, 0, 0, 0.05); /* 悬停背景色 */
--slipColor: #3498db; /* 滑块颜色 */
--accentColor: #3498db; /* 强调色 */
--shadowColor: rgba(0, 0, 0, 0.1); /* 阴影颜色 */
}
#sun-panel-toc-dom #toc-btn {
position: fixed;
top: 50%; /* 上下居中 */
left: 20px; /* 距离左侧 20px */
transform: translateY(-50%); /* 确保垂直居中 */
width: 40px; /* 按钮宽度 */
height: 40px; /* 按钮高度 */
background-color: var(--btnBg);
color: var(--btnColor);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
z-index: 1000;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 2px 5px var(--shadowColor);
border: none;
outline: none;
backdrop-filter: blur(5px);
font-size: 16px; /* 按钮文字大小 */
font-weight: bold; /* 按钮文字加粗 */
}
#sun-panel-toc-dom #toc-btn:hover {
background-color: var(--hoverBg);
transform: translateY(-50%) scale(1.1); /* 悬停时放大 */
box-shadow: 0 4px 8px var(--shadowColor);
}
#sun-panel-toc-dom #toc-content {
position: fixed;
top: 50%; /* 上下居中 */
left: -250px; /* 初始隐藏在左侧 */
width: 250px;
max-height: 70vh;
background-color: var(--contentBg);
border-radius: 10px;
padding: 20px;
overflow-y: auto;
z-index: 999;
box-shadow: 0 4px 15px var(--shadowColor);
transform: translateY(-50%); /* 确保垂直居中 */
backdrop-filter: blur(10px);
opacity: 0;
visibility: hidden; /* 初始隐藏 */
transition: opacity 0.4s ease-out, visibility 0.4s ease-out; /* 过渡效果 */
}
#sun-panel-toc-dom #toc-content.show {
left: 20px; /* 显示侧边栏 */
opacity: 1;
visibility: visible; /* 确保侧边栏可见 */
animation: slideInLeft 0.4s ease-out; /* 左右平移动画 */
}
#sun-panel-toc-dom #toc-content.hide {
left: -250px; /* 隐藏侧边栏 */
opacity: 0;
visibility: hidden; /* 确保侧边栏隐藏 */
animation: slideOutLeft 0.4s ease-out; /* 左右缩回动画 */
}
#sun-panel-toc-dom .toc-sidebar-title {
font-size: 18px;
font-weight: bold;
color: var(--textColor);
margin: 0; /* 去除默认的 margin */
text-align: center;
flex-grow: 1; /* 让标题占据剩余空间 */
}
#settings-btn {
background-color: transparent;
color: var(--btnColor);
border: none;
border-radius: 50%;
padding: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
font-size: 20px; /* 调整图标大小 */
display: flex;
align-items: center;
justify-content: center;
width: 40px; /* 设置按钮宽度 */
height: 40px; /* 设置按钮高度 */
position: absolute; /* 绝对定位 */
right: 0; /* 放置在右侧 */
}
#settings-btn:hover {
background-color: var(--hoverBg);
transform: scale(1.1); /* 悬停时放大 */
}
#sun-panel-toc-dom .title-bar-box {
display: flex;
align-items: center;
padding: 8px 10px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
border-radius: 5px;
margin-bottom: 5px;
}
#sun-panel-toc-dom .title-bar-box:hover,
#sun-panel-toc-dom .title-bar-box.active {
background-color: var(--hoverBg);
transform: translateX(5px);
animation: pulse 0.5s ease-in-out;
}
#sun-panel-toc-dom .title-bar-slip {
width: 3px;
height: 0;
background-color: var(--slipColor);
margin-right: 10px;
transition: all 0.2s ease;
border-radius: 3px;
}
#sun-panel-toc-dom .title-bar-box:hover .title-bar-slip,
#sun-panel-toc-dom .title-bar-box.active .title-bar-slip {
height: 20px;
}
#sun-panel-toc-dom .title-bar-title {
font-size: 14px;
color: var(--textColor);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.2s ease;
}
#sun-panel-toc-dom .title-bar-box:hover .title-bar-title,
#sun-panel-toc-dom .title-bar-box.active .title-bar-title {
color: var(--accentColor);
}
#sun-panel-toc-dom #toc-content::-webkit-scrollbar {
width: 5px;
}
#sun-panel-toc-dom #toc-content::-webkit-scrollbar-thumb {
background-color: var(--slipColor);
border-radius: 5px;
}
#sun-panel-toc-dom #toc-content::-webkit-scrollbar-track {
background-color: var(--hoverBg);
border-radius: 5px;
}
/* 模态框样式 */
#modal {
display: none; /* 初始隐藏 */
position: fixed;
z-index: 1001;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: var(--contentBg);
margin: 15% auto;
padding: 20px;
border-radius: 10px;
width: 80%;
max-width: 400px;
position: relative;
}
.close {
color: var(--btnColor);
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: var(--accentColor);
text-decoration: none;
cursor: pointer;
}
.modal-content h3 {
margin-top: 0;
}
.modal-content input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid var(--shadowColor);
border-radius: 5px;
}
.modal-content button {
width: 100%;
padding: 10px;
background-color: var(--btnBg);
color: var(--btnColor);
border: none;
border-radius: 5px;
cursor: pointer;
}
.modal-content button:hover {
background-color: var(--hoverBg);
}
@keyframes slideInLeft {
from {
left: -250px; /* 从左侧隐藏位置开始 */
opacity: 0;
}
to {
left: 20px; /* 移动到显示位置 */
opacity: 1;
}
}
@keyframes slideOutLeft {
from {
left: 20px; /* 从显示位置开始 */
opacity: 1;
}
to {
left: -250px; /* 移动到左侧隐藏位置 */
opacity: 0;
}
}
@media (max-width: 800px) {
#sun-panel-toc-dom #toc-content {
width: 200px;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
Sidebar.js:
(function () {
// =========== Config Start ===========
const scrollOffset = 80;
const displayStyle = 'auto';
const mobileWidth = 800;
const SunPanelTOCDomIdName = 'sun-panel-toc-dom';
const svgTocIcon = '>>'; // 初始按钮文字
const scrollContainerElementClassName = '.scroll-container';
// =========== Config End ===========
const lightTheme = {
btnBg: 'rgba(255, 255, 255, 0.5)',
btnColor: '#333333',
contentBg: 'rgba(255, 255, 255, 0.8)',
textColor: '#333333',
hoverBg: 'rgba(0, 0, 0, 0.05)',
slipColor: '#3498db',
accentColor: '#3498db',
shadowColor: 'rgba(0, 0, 0, 0.1)'
};
const darkTheme = {
btnBg: 'rgba(42, 42, 42, 0.5)',
btnColor: '#ffffff',
contentBg: 'rgba(30, 30, 30, 0.8)',
textColor: '#e0e0e0',
hoverBg: 'rgba(255, 255, 255, 0.1)',
slipColor: '#3498db',
accentColor: '#3498db',
shadowColor: 'rgba(0, 0, 0, 0.3)'
};
const getSystemColorScheme = () => window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const getTheme = () => getSystemColorScheme() === 'dark' ? darkTheme : lightTheme;
const isMobile = () => displayStyle === 'mobile' || (displayStyle === 'auto' && window.innerWidth < mobileWidth);
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function createDom() {
const existingElement = document.getElementById(SunPanelTOCDomIdName);
if (existingElement) existingElement.remove();
const SunPanelTOCDom = document.createElement('div');
SunPanelTOCDom.id = SunPanelTOCDomIdName;
document.body.appendChild(SunPanelTOCDom);
const style = document.createElement('style');
const SunPanelTOCDomStyleId = `#${SunPanelTOCDomIdName}`;
function updateStyles() {
const theme = getTheme();
document.documentElement.style.setProperty('--btnBg', theme.btnBg);
document.documentElement.style.setProperty('--btnColor', theme.btnColor);
document.documentElement.style.setProperty('--contentBg', theme.contentBg);
document.documentElement.style.setProperty('--textColor', theme.textColor);
document.documentElement.style.setProperty('--hoverBg', theme.hoverBg);
document.documentElement.style.setProperty('--slipColor', theme.slipColor);
document.documentElement.style.setProperty('--accentColor', theme.accentColor);
document.documentElement.style.setProperty('--shadowColor', theme.shadowColor);
}
updateStyles();
SunPanelTOCDom.appendChild(style);
const tocBtn = document.createElement('button');
tocBtn.id = 'toc-btn';
tocBtn.textContent = svgTocIcon; // 设置按钮文字
tocBtn.setAttribute('aria-label', '打开目录');
SunPanelTOCDom.appendChild(tocBtn);
const tocContent = document.createElement('nav');
tocContent.id = 'toc-content';
tocContent.setAttribute('aria-label', '文章目录');
SunPanelTOCDom.appendChild(tocContent);
// 创建标题容器
const titleContainer = document.createElement('div');
titleContainer.style.display = 'flex';
titleContainer.style.alignItems = 'center'; // 垂直居中
titleContainer.style.justifyContent = 'center'; // 水平居中
titleContainer.style.position = 'relative'; // 设置相对定位
titleContainer.style.width = '100%'; // 占满整个宽度
// 创建目录标题
const sidebarTitle = document.createElement('h2');
sidebarTitle.className = 'toc-sidebar-title';
sidebarTitle.textContent = '目录';
titleContainer.appendChild(sidebarTitle);
// 创建设置按钮
const settingsBtn = document.createElement('button');
settingsBtn.id = 'settings-btn';
settingsBtn.innerHTML = '⚙️'; // 使用齿轮图标
settingsBtn.setAttribute('aria-label', '设置背景');
settingsBtn.style.position = 'absolute'; // 设置绝对定位
settingsBtn.style.right = '0'; // 放置在右侧
titleContainer.appendChild(settingsBtn);
// 将标题容器添加到侧边栏内容中
tocContent.insertBefore(titleContainer, tocContent.firstChild);
// 添加模态框结构
const modal = document.createElement('div');
modal.id = 'modal';
modal.style.display = 'none'; // 初始隐藏
modal.innerHTML = `
<div class="modal-content">
<span class="close" onclick="document.getElementById('modal').style.display='none';">×</span>
<h3>设置背景图片</h3>
<input type="text" id="imageUrl" placeholder="请输入背景图片的URL">
<button id="saveSettings">确认</button>
</div>
`;
SunPanelTOCDom.appendChild(modal);
// 设置按钮点击事件显示模态框
settingsBtn.addEventListener('click', () => {
document.getElementById('modal').style.display = 'block';
});
// 确认按钮点击事件生成并下载Sidebar.json
document.getElementById('saveSettings').addEventListener('click', () => {
const imageUrl = document.getElementById('imageUrl').value;
if (imageUrl) {
// 创建Sidebar.json内容
const sidebarJson = JSON.stringify({ backgroundImage: imageUrl }, null, 2);
// 创建下载链接
const a = document.createElement('a');
a.href = 'data:text/json;charset=utf-8,' + encodeURIComponent(sidebarJson);
a.download = 'Sidebar.json';
a.click();
// 提示用户保存位置
**('请将下载的 Sidebar.json 文件保存到 /custom/Sidebar 目录中。');
// 隐藏模态框
document.getElementById('modal').style.display = 'none';
} else {
**('请输入背景图片的URL。');
}
});
// 检测Sidebar.json文件并加载背景图片
fetch('/custom/Sidebar/Sidebar.json')
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('文件不存在或无法访问。');
}
})
.then(data => {
if (data.backgroundImage) {
tocContent.style.backgroundImage = `url(${data.backgroundImage})`;
tocContent.style.backgroundSize = 'cover';
tocContent.style.backgroundPosition = 'center';
}
})
.catch(error => {
console.error('无法加载 Sidebar.json 文件。', error);
// 默认无背景图片
});
function createTocItem(item, className) {
const titleBarBox = document.createElement('div');
titleBarBox.className = 'title-bar-box';
titleBarBox.dataset.groupClassName = className;
const titleBarSlip = document.createElement('div');
titleBarSlip.className = 'title-bar-slip';
const titleBarTitle = document.createElement('div');
titleBarTitle.className = 'title-bar-title';
const titleElement = item.querySelector('.group-title');
const titleText = titleElement ? titleElement.textContent : item.id;
titleBarTitle.textContent = titleText;
titleBarBox.appendChild(titleBarSlip);
titleBarBox.appendChild(titleBarTitle);
titleBarBox.addEventListener('mouseenter', () => {
requestAnimationFrame(() => {
titleBarSlip.style.height = '20px';
titleBarTitle.style.color = getTheme().slipColor;
});
});
titleBarBox.addEventListener('mouseleave', () => {
requestAnimationFrame(() => {
if (!titleBarBox.classList.contains('active')) {
titleBarSlip.style.height = '0';
titleBarTitle.style.color = getTheme().textColor;
}
});
});
return titleBarBox;
}
const items = document.querySelectorAll('[class*="item-group-index-"]');
items.forEach((item) => {
item.classList.forEach((className) => {
if (className.startsWith('item-group-index-')) {
tocContent.appendChild(createTocItem(item, className));
}
});
});
function toggleTocContent() {
const i**panded = !tocContent.classList.contains('show');
if (i**panded) {
// 显示侧边栏
tocContent.classList.remove('hide'); // 移除 hide 类
tocContent.classList.add('show'); // 添加 show 类
tocBtn.setAttribute('aria-expanded', 'true');
tocBtn.setAttribute('aria-label', '关闭目录');
tocBtn.textContent = '<<';
tocBtn.style.left = '270px';
} else {
// 隐藏侧边栏
tocContent.classList.remove('show'); // �移除 show 类
tocContent.classList.add('hide'); // 添加 hide 类
tocBtn.setAttribute('aria-expanded', 'false');
tocBtn.setAttribute('aria-label', '打开目录');
tocBtn.textContent = '>>';
tocBtn.style.left = '20px';
// 动画结束后移除 hide 类
tocContent.addEventListener('animationend', () => {
tocContent.classList.remove('hide');
}, { once: true });
}
}
tocBtn.addEventListener('click', toggleTocContent);
const scrollContainer = document.querySelector(scrollContainerElementClassName);
if (scrollContainer) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const currentClassName = Array.from(entry.target.classList).find(className => className.startsWith('item-group-index-'));
requestAnimationFrame(() => {
document.querySelectorAll('.title-bar-box').forEach((box) => {
box.classList.toggle('active', box.dataset.groupClassName === currentClassName);
});
});
}
});
}, { root: scrollContainer, rootMargin: `-${scrollOffset}px 0px 0px 0px`, threshold: 0.1 });
items.forEach((item) => observer.observe(item));
}
tocContent.addEventListener('click', (event) => {
const titleBarBox = event.target.closest('.title-bar-box');
if (titleBarBox && titleBarBox.dataset.groupClassName) {
const targetElement = document.querySelector(`.${titleBarBox.dataset.groupClassName}`);
if (targetElement) {
const targetTop = targetElement.offsetTop;
const scrollContainerElement = document.querySelector(scrollContainerElementClassName);
if (scrollContainerElement) {
scrollContainerElement.scrollTo({
top: targetTop - scrollOffset,
behavior: 'smooth',
});
}
}
toggleTocContent();
}
});
document.addEventListener('click', (event) => {
if (!tocBtn.contains(event.target) && !tocContent.contains(event.target) && !modal.contains(event.target)) {
tocContent.classList.remove('show');
tocBtn.setAttribute('aria-expanded', 'false');
tocBtn.setAttribute('aria-label', '打开目录');
tocBtn.textContent = '>>'; // 恢复按钮文字
tocBtn.style.left = '20px'; // 恢复按钮位置
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && tocContent.classList.contains('show')) {
toggleTocContent();
}
});
tocContent.addEventListener('keydown', (event) => {
const activeElement = document.activeElement;
const titleBarBoxes = tocContent.querySelectorAll('.title-bar-box');
const currentIndex = Array.from(titleBarBoxes).indexOf(activeElement);
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
if (currentIndex < titleBarBoxes.length - 1) {
titleBarBoxes[currentIndex + 1].focus();
}
break;
case 'ArrowUp':
event.preventDefault();
if (currentIndex > 0) {
titleBarBoxes[currentIndex - 1].focus();
}
break;
case 'Enter':
case ' ':
event.preventDefault();
activeElement.click();
break;
}
});
tocContent.querySelectorAll('.title-bar-box').forEach((box) => {
box.setAttribute('tabindex', '0');
});
const debouncedHandleResize = debounce(() => {
if (isMobile()) {
tocBtn.classList.remove('hidden');
tocContent.classList.remove('show');
} else {
tocBtn.classList.remove('hidden');
}
}, 200);
window.addEventListener('resize', debouncedHandleResize);
debouncedHandleResize();
window.matchMedia('(prefers-color-scheme: dark)').addListener(() => {
updateStyles();
document.querySelectorAll('.title-bar-box').forEach(box => {
box.style.color = getTheme().textColor;
});
});
}
const items = document.querySelectorAll('[class*="item-group-index-"]');
if (items.length > 0) {
createDom();
} else {
const interval = setInterval(() => {
const items = document.querySelectorAll('[class*="item-group-index-"]');
if (items.length > 0) {
createDom();
clearInterval(interval);
}
}, 1000);
}
})();
将Sidebar.css和Sidebar.js在all.js中加载即可。
结语
教程中写了很多比较啰嗦的话,主要是想把一些从中遇到的问题说一下,避免大家遇到相同的问题像我一样在某一步卡住做无用功,感谢大家的观看。如果有什么问题欢迎指出!